核心心法:阿里/OpenAI 走的是 role 字符串,而 Spring AI 将其优雅地抽象成了对象与枚举。理解这套映射,是构建企业级大模型应用的基础。

一、 核心概念映射:角色与类

Spring AI 完美对应了阿里 DashScope 和 OpenAI 的四大核心角色。

阿里/OpenAI 的 roleSpring AI 的类MessageType核心用途
systemSystemMessageSYSTEM人设、规则、输出约束、安全红线
userUserMessageUSER用户输入(可带多模态附件)
assistantAssistantMessageASSISTANT① 模型历史回复 ② few-shot 标准答案 ③ 承载 toolCalls
toolToolResponseMessageTOOL工具执行结果回填,必须与 toolCall id 严格配对

核心结构高度凝练:

java

Prompt prompt = new Prompt(List<Message> messages, ChatOptions options);

Prompt = 有序的消息列表 + 调用参数。所谓“格式化”,本质就是怎么把这个 List 排好、每个消息里写什么。
(注:ChatClient 只是对 Prompt 的链式语法糖封装)


二、 角色实战:四个角色分别怎么写?

1. SystemMessage —— 定规则,不放数据

职责:写好四件事——身份、目标、约束、输出格式。

✅ 优秀示例:
你是一名资深汽车售后诊断专家,服务对象是 4S 店技师。
目标:根据用户提供的车型、故障码和故障描述,给出可执行的排查步骤。
约束:

  1. 只依据 <context> 中的资料回答,资料中没有的内容明确说"资料未覆盖",禁止编造。

  2. 涉及安全件(制动、转向、高压电)必须先给出安全提示。

  3. 不确定时优先追问,而不是猜测。
    输出格式:严格输出 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_descUserMessage 的 <symptom> 段
car_model / dtc_detailsUserMessage 的 <vehicle> / <dtc> 段(或做成 Tool 让模型自己查)
historyChatMemory
lang / intent_typeSystemMessage 的模板变量
模型服务内部拼的 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。

Logo

智能硬件社区聚焦AI智能硬件技术生态,汇聚嵌入式AI、物联网硬件开发者,打造交流分享平台,同步全国赛事资讯、开展 OPC 核心人才招募,助力技术落地与开发者成长。

更多推荐