Spring AI 提示词实战指南:四大角色、格式化与避坑
核心心法:阿里/OpenAI 走的是 role 字符串,而 Spring AI 将其优雅地抽象成了对象与枚举。理解这套映射,是构建企业级大模型应用的基础。
一、 核心概念映射:角色与类
Spring AI 完美对应了阿里 DashScope 和 OpenAI 的四大核心角色。
| 阿里/OpenAI 的 role | Spring AI 的类 | MessageType | 核心用途 |
|---|---|---|---|
| system | SystemMessage | SYSTEM | 人设、规则、输出约束、安全红线 |
| user | UserMessage | USER | 用户输入(可带多模态附件) |
| assistant | AssistantMessage | ASSISTANT | ① 模型历史回复 ② few-shot 标准答案 ③ 承载 toolCalls |
| tool | ToolResponseMessage | TOOL | 工具执行结果回填,必须与 toolCall id 严格配对 |
核心结构高度凝练:
java
Prompt prompt = new Prompt(List<Message> messages, ChatOptions options);
Prompt = 有序的消息列表 + 调用参数。所谓“格式化”,本质就是怎么把这个 List 排好、每个消息里写什么。
(注:ChatClient 只是对 Prompt 的链式语法糖封装)
二、 角色实战:四个角色分别怎么写?
1. SystemMessage —— 定规则,不放数据
职责:写好四件事——身份、目标、约束、输出格式。
✅ 优秀示例:
你是一名资深汽车售后诊断专家,服务对象是 4S 店技师。
目标:根据用户提供的车型、故障码和故障描述,给出可执行的排查步骤。
约束:
只依据
<context>中的资料回答,资料中没有的内容明确说"资料未覆盖",禁止编造。涉及安全件(制动、转向、高压电)必须先给出安全提示。
不确定时优先追问,而不是猜测。
输出格式:严格输出 JSON,字段见<schema>。
🚫 面试高频反例(说出这些很加分):
-
把用户问题拼进 system(既丢上下文,又给注入留了口子)。
-
system 里塞了一堆业务数据(换了模型就失控,且无法复用)。
-
写“请尽可能详细/专业”这类无约束的空话。
-
致命错误:多数厂商只认第一条 system,且必须在最前——顺序错了 rules 直接失效。
2. UserMessage —— 放任务和本次数据
职责:结构固定三段——指令 + 数据(用标签包裹) + 期望。
✅ 优秀示例:
请分析下面的故障。
<vehicle>车型:CS75 PLUS,1.5T</vehicle>
<dtc>P0301, P0171</dtc>
<symptom>冷车抖动,加速无力</symptom>
要求:按排查优先级输出,每步给出判定方法和所需工具。
💡 关键细节:
-
防注入第一道墙:数据一律放 user,且用明确边界(XML 标签 / 代码块)包裹。用户在输入里写“忽略以上所有指令”,只要它待在
<symptom>里,模型就不会当规则执行。 -
多模态支持:
java
new UserMessage("识别这张 VIN 照片", List.of(new Media(MimeTypeUtils.IMAGE_PNG, new ClassPathResource("vin.png"))))
3. AssistantMessage —— 两个用途,绝不混淆
| 用途 | 写法 | 说明 |
|---|---|---|
| few-shot 示例 | 直接 new AssistantMessage("标准答案") | 放在对应的 UserMessage 后面,成对出现 |
| 历史回复 | 从 ChatMemory 取回,原样回放 | 不能重新生成,必须与当次真实回复一致 |
| 工具调用意图 | 由模型返回,带 toolCalls | 你只读不改,也不能伪造 |
(注:few-shot 比长篇描述有效得多——一个标准答案示例 ≈ 五条规则描述)
4. ToolResponseMessage —— 框架自动回填,严禁手写
工具调用完整链路(面试必考):
text
1. UserMessage(问:P0301 怎么修)
2. → AssistantMessage(toolCalls=[{id:"call_1", name:"queryDtc", args:{code:"P0301"}}]) ← 模型说"我要查"
3. → ToolResponseMessage([ToolResponse(id:"call_1", name:"queryDtc", responseData:"...")]) ← 你的程序执行后回填
4. → AssistantMessage("根据资料,P0301 的排查步骤是…") ← 模型给最终答案
⚠️ 红线:ToolResponse.id 必须等于 toolCall.id(阿里/OpenAI 的 tool_call_id)。这条配不上,请求直接 400。
三、 格式化工程化:怎么排、怎么写?
1. 消息装配顺序(主干逻辑)
text
[1] SystemMessage ← 规则,永远第一条,且只有一条 [2] User/Assistant 成对 ← few-shot 示例(0~3 组,多了浪费 token) [3] ChatMemory 回放的历史 ← 从旧到新 [4] UserMessage(本次任务) ← 指令 + 数据 [5] AssistantMessage(toolCalls) → ToolResponseMessage → ... ← 工具循环,框架自动 [6] AssistantMessage(最终答案)
代码实现:
java
List<Message> messages = new ArrayList<>();
messages.add(new SystemMessage(systemText)); // 1
messages.add(new UserMessage("P0301 冷车抖动")); // 2 few-shot
messages.add(new AssistantMessage("{\"dtc\":\"P0301\",...}"));
messages.addAll(chatMemory.get(conversationId)); // 3 历史
messages.add(new UserMessage(userText)); // 4 本次
ChatOptions options = DashScopeChatOptions.builder()
.withModel("qwen-plus")
.withTemperature(0.1f) // 诊断场景要低
.withMaxToken(2048)
.build();
ChatResponse resp = chatModel.call(new Prompt(messages, options));
2. 输入格式化:模板外置 + 占位符
不要把提示词写成 Java 字符串常量! 放在 resources/prompts/*.st,好处是:能版本化、能被非开发同学评审、能按语言/租户多份。
java
// prompts/dtc-analyze-system.st
// 你是一名资深汽车售后诊断专家,服务对象是 4S 店技师。当前回复语言:{lang} ...
PromptTemplate tpl = new PromptTemplate(new ClassPathResource("prompts/dtc-analyze-system.st"));
String systemText = tpl.render(Map.of("lang", lang));
3. 输出格式化:record 是标准做法
在 Spring AI 中,使用 record 是最干净的写法,字段上的描述会被自动转成 JSON Schema 塞进 prompt。
java
public record DtcAnalysis(
@JsonPropertyDescription("故障码,例如 P0301") String dtc,
@JsonPropertyDescription("最可能的原因,一句话") String cause,
@JsonPropertyDescription("按优先级排列的排查步骤") List<Step> steps
) {
public record Step(
@JsonPropertyDescription("步骤序号,从1开始") int no,
@JsonPropertyDescription("操作描述") String action,
@JsonPropertyDescription("判定标准") String criteria
) {}
}
// 调用:底层是 BeanOutputConverter,自动生成 JSON Schema 追加到 prompt 末尾,再用 Jackson 反序列化
DtcAnalysis result = chatClient.prompt()
.system(systemText)
.user(userText)
.call()
.entity(DtcAnalysis.class);
三种输出格式器的选择:
| 类型 | Converter | 场景 |
|---|---|---|
| 对象 | .entity(Record.class) | 绝大多数业务场景 |
| 列表 | .entity(new ParameterizedTypeReference<List<X>>(){}) | 批量抽取 |
| Map | .entity(new ParameterizedTypeReference<Map<String,Object>>(){}) | 结构不确定 |
四、 避坑指南:三个必踩的雷区
💣 坑 1:记忆裁剪把 tool 消息拆散
-
现象:
MessageWindowChatMemory按条数裁剪,很容易把AssistantMessage(toolCalls)删掉、留下ToolResponseMessage,下次请求直接 400。 -
解法:按对话轮次/组裁剪,或者工具调用轮次结束后再进记忆。
💣 坑 2:历史回放时 AssistantMessage 被重新生成
-
现象:会话记忆如果按“重新让模型说一遍”来恢复,历史会漂移,多轮很快崩。
-
解法:记忆必须原样存原文,绝不能中途篡改或重新生成。
💣 坑 3:system 放错位置或给了多条
-
现象:Spring AI 不校验顺序,直接把 List 转成厂商格式。你在中间塞一条
SystemMessage,OpenAI 兼容接口可能直接报错,或者模型静默忽略。 -
解法:用
ChatClient.prompt().system()而不要手动add,框架保证放第一位。
五、 简历与面试实战
1. 架构演进对照(简历加分项)
如果你之前是手写 HTTP 请求对接模型,迁移到 Spring AI 后,可以这样讲你的工程化演进:
| 以前(手写拼装) | 现在(Spring AI) |
|---|---|
query / trouble_desc | UserMessage 的 <symptom> 段 |
car_model / dtc_details | UserMessage 的 <vehicle> / <dtc> 段(或做成 Tool 让模型自己查) |
history | ChatMemory |
lang / intent_type | SystemMessage 的模板变量 |
| 模型服务内部拼的 prompt | 应用侧 resources/prompts/*.st(责任前移,版本化) |
| SSE | .stream().content() + SseEmitter |
| PAI-EAS 固定端点 | ChatModel 可替换(DashScope / Ollama 一套代码) |
面试话术:“我既做过手写 HTTP 拼装槽位(知道底层协议长什么样),又做过 Spring AI 抽象(知道怎么让模型可替换)。”——这比只会写 chatClient.prompt() 的人值钱得多。
2. 30 秒高能面试回答
阿里的 role 是四个字符串,Spring AI 把它抽象成 Message 接口的四个实现:
SystemMessage放人设和输出约束、UserMessage放任务和本次数据、AssistantMessage既做 few-shot 和会话历史、也承载模型的toolCalls、ToolResponseMessage回填工具结果且必须和 toolCall id 配对。多个 Message 按序装进Prompt,加上ChatOptions交给ChatModel。格式化我守三条底线:system 只有一条且在首位、用户数据一律放 user 并加 XML 标签防注入、输出用 record + .entity() 让框架自动生成 JSON Schema,不手写“请返回 JSON”。提示词模板必须外置成文件、版本化管理,绝不硬编码在 Java 里。
我踩过最深的一个坑是:会话记忆裁剪把 toolCall 和 ToolResponse 拆散了,导致下次请求直接非法 400。
更多推荐

所有评论(0)