Spring AI 提示词工程实践:从 Prompt 设计到统一 LLM 调用框架
本文以求职派为例,梳理 Spring AI 的 Prompt、Message、PromptTemplate 核心机制,以及如何通过 LlmCaller 封装、模型路由和上下文管理,构建一套可复用的提示词工程体系。
一、为什么需要提示词工程?
最简单的大模型调用通常是:
Java
chatModel.call("帮我推荐 Java 岗位");
这种方式适合 Demo,但实际业务会遇到很多问题:
-
模型不知道自己的业务角色。
-
不同用户需要个性化回答。
-
多轮对话容易丢失上下文。
-
不同 Agent 重复编写模型调用逻辑。
-
历史消息过长,导致 Token 消耗增加。
因此,需要将简单的模型调用升级成一套完整的提示词管理机制。
核心思想:将提示词构建、模型调用、上下文管理从业务代码中抽离,统一封装。
二、Spring AI 的三层提示词抽象
Spring AI 主要通过 Prompt、Message 和 PromptTemplate 组织模型请求。
1. Message:定义消息角色
Message 表示一次对话中的单条消息。
| 类型
|
作用
|
SystemMessage
|
定义模型角色与行为规则
| |
UserMessage
|
承载用户输入、图片等内容
| |
AssistantMessage
|
记录模型生成的回复
| |
ToolResponseMessage
|
承载工具执行结果
|
例如:
Java
Prompt prompt = new Prompt(
List.of(
new SystemMessage("你是一名 Java 技术面试官"),
new UserMessage("请解释线程池的工作原理")
)
);
SystemMessage 用于描述模型的职责,UserMessage 用于承载具体任务。
在 Agent 系统中,还会加入历史消息和工具交互记录,构成完整的对话上下文。
2. Prompt:封装完整模型请求
Prompt 不只是一段文本,而是:
Prompt
├── Messages
│ ├── SystemMessage
│ ├── UserMessage
│ ├── AssistantMessage
│ └── ToolResponseMessage
│
└── ChatOptions
├── Model
└── Temperature
其中:
-
Messages 决定向模型提供哪些信息。
-
ChatOptions 决定使用哪个模型及其调用参数。
因此,Prompt 可以理解为一次模型调用的完整请求对象。
3. PromptTemplate:动态生成提示词
实际项目中,提示词通常包含动态变量。
例如:
Java
PromptTemplate template = new PromptTemplate(
"你是一名{role},请帮助用户完成{task}"
);
Prompt prompt = template.create(Map.of(
"role", "Java面试官",
"task", "模拟技术面试"
));
通过模板复用,可以避免在业务代码中反复拼接相似的提示词。
需要注意,模板生成的消息角色取决于使用的模板类型和构建方式;系统提示词可以使用 SystemPromptTemplate。
三、提示词工程化:模板外置
当业务提示词越来越复杂时,直接写在 Java 代码中会出现维护困难的问题。
例如,用户画像提取可能需要:
-
角色定义。
-
字段说明。
-
输出格式约束。
-
增量更新规则。
-
Few-shot 示例。
一份提示词可能达到数百行。
因此,求职派将提示词保存为独立的 Markdown 文件。
resources/
└── prompts/
├── identity-extraction.md
├── job-extraction.md
├── conversation-summary.md
└── job-recommend.md
通过 Spring Resource 加载:
Java
@Value("classpath:/prompts/identity-extraction.md")
private Resource promptResource;
调用时注入动态变量:
Java
String prompt = template
.replace("{current_soul}", identity)
.replace("{conversation_history}", history);
这种设计的优势是:
-
提示词与业务代码分离。
-
不同 Agent 独立维护提示词。
-
提示词可以通过 Git 进行版本管理。
-
调整 Prompt 不需要修改 Java 业务逻辑。
不过,String.replace() 适合变量少、结构简单的模板。复杂模板更适合使用正式模板引擎,并处理好变量转义。
沉淀:Prompt 应当作为独立的工程资源进行管理,而不是散落在业务代码中的字符串。
四、统一 LLM 调用框架设计
假设系统有三个 Agent:
ResumeAgent
JobRecommendAgent
InterviewAgent
如果每个 Agent 都直接调用 chatModel.call(),就会出现大量重复逻辑:
选择模型
构造 Prompt
加载 Memory
注入用户画像
注册 Tools
执行模型调用
因此,求职派引入统一调用器 LlmCaller。
Java
public interface LlmCaller {
String call(
UserConversationInfo user,
Prompt prompt
);
Flux<String> stream(
UserConversationInfo user,
Prompt prompt
);
}
通过接口统一同步与流式调用入口。
1. 分层调用器设计
根据业务复杂度划分不同等级的调用器。
BasicLlmCaller
基础模型调用
BizAgentLlmCaller
模型调用 + Memory + ReAct
FullLlmCaller
用户画像 + 工具 + MCP + 文件系统
不同业务选择不同的调用器:
| 业务
|
调用器
|
简单文本提取
|
Basic
| |
多轮业务对话
|
Business
| |
复杂工具型 Agent
|
Full
|
这种设计遵循最小能力原则:简单任务不加载无关工具,避免增加上下文开销与安全风险。
2. 模型自动路由
调用器还负责根据用户输入选择模型。
例如:
Java
boolean hasImage = prompt.getUserMessages()
.stream()
.anyMatch(m -> !m.getMedia().isEmpty());
if (hasImage) {
model = visionModel;
}
具体实现需要做好空值处理和模型能力校验。
整体流程:
用户消息
↓
检查是否包含图片
↓
ModelRouter
├── 文本 → Text Model
└── 图片 → Vision Model
↓
执行模型调用
Agent 无须了解每个模型的具体实现,只需要调用统一接口。
沉淀:通过调用器屏蔽模型选择、上下文处理和工具注册等基础设施细节。
五、上下文管理:解决长对话问题
大模型的上下文窗口有限。
随着对话轮数增加:
历史消息增长
↓
Token 消耗增加
↓
超过上下文预算
↓
调用失败或无法保留全部历史
求职派采用多级上下文管理机制。
1. 过滤低信息量消息
例如:
好的
嗯
收到
OK
这类消息可能对后续对话帮助较小,可以在特定场景下过滤。
但不能简单删除所有短消息。
例如:
用户:确定要投递这家公司吗?
用户下一轮:确定。
这里的「确定」就具有重要业务意义。
因此,过滤策略应保留关键确认、决策和工具交互记录。
2. 基于 Token 预算裁剪
相比只按照消息数量截断历史,Token 预算更贴近模型实际限制。
读取历史消息
↓
估算 Token
↓
是否超过预算?
↓ 是
裁剪较早消息
↓
保留最近对话
需要注意,中文字符数与 Token 数量不存在固定换算关系。
可以使用近似估算做初步控制,但应结合实际模型的 Token 统计能力,并预留输出预算及工具调用开销。
3. 会话摘要
当历史对话过长时,可以让模型将较早的消息压缩成摘要。
完整历史对话
↓
LLM 生成摘要
↓
保留关键事实
↓
摘要 + 最近消息
↓
构建新 Prompt
例如:
用户画像:
3年 Java 开发经验
求职意向:
杭州,后端开发
薪资要求:
20K 以上
此前已推荐:
A 公司、B 公司
摘要可以减少上下文占用,但并不能保证无损保留全部信息。
对于用户明确提出的要求、关键决策、任务状态等信息,应单独结构化保存,避免摘要遗漏。
六、完整的 LLM 调用链路
将上述能力组合起来,可以形成一条统一调用链。
用户输入
↓
获取用户身份与画像
↓
加载 Agent Prompt 模板
↓
加载历史消息
↓
上下文窗口管理
↓
组装 Prompt
↓
LlmCaller
├── ModelRouter
├── Memory
└── Tools / MCP
↓
Spring AI
↓
LLM
↓
返回结果
↓
更新 Memory
↓
返回用户
这里需要明确模块职责:
| 模块
|
职责
|
PromptTemplate
|
管理可复用提示词
| |
Prompt
|
组织一次模型请求
| |
LlmCaller
|
统一执行模型调用
| |
ModelRouter
|
选择具体模型
| |
ChatMemory
|
保存对话历史
| |
ContextManager
|
控制上下文预算
|
它们共同形成 Agent 系统的模型调用基础设施。
七、生产环境还需要关注什么?
除了基本功能,提示词工程还需要关注可靠性和安全性。
| 问题
|
处理思路
|
Prompt 越来越复杂
|
模板外置、版本管理
| |
模型输出不稳定
|
结构化输出、校验、测试
| |
历史消息过长
|
Token 预算、裁剪、摘要
| |
用户画像错误
|
支持用户修改、定期更新
| |
Prompt Injection
|
区分可信指令与外部输入
| |
工具调用越权
|
独立鉴权、最小权限原则
|
特别需要注意:用户画像、网页内容和历史对话都不能被视为可信的系统指令。即使这些内容被注入系统提示词,也不能让其中的恶意指令获得更高权限。
Prompt 负责指导模型行为,但真正的权限控制必须由业务程序执行。
八、总结
Spring AI 提示词工程可以归纳为四个阶段。
| 阶段
|
核心能力
|
基础阶段
|
Prompt + Message
| |
模板阶段
|
PromptTemplate + Markdown 外置
| |
框架阶段
|
LlmCaller + ModelRouter
| |
工程阶段
|
Memory + 上下文窗口 + 会话摘要
|
整个演进过程是:
简单 Prompt
↓
角色与模板管理
↓
统一模型调用
↓
多模型和多模态适配
↓
上下文与记忆管理
↓
可复用的 Agent 调用框架
最终沉淀的架构思想:
Prompt 负责描述任务,Message 负责组织上下文,LlmCaller 负责统一调用,Memory 负责保存历史,ContextManager 负责控制上下文预算。
通过职责拆分,业务 Agent 只需要关注自身的 Prompt、工具和业务逻辑,而不需要重复处理底层模型调用细节。
这就是从简单 Spring AI Demo 走向可维护、多 Agent 系统的重要一步。