版本:Spring AI 2.0.1
目标:弄清一次模型调用在 Spring AI 里的数据形状——谁说了什么、怎么打包发送、回来长什么样,以及如何判断「模型要调工具」。
ChatClient只是少写组装代码;底下流通的仍是本章这些类型。
3.1 先建立心智模型
一次聊天请求,可以看成三条管道:
Message 列表(角色消息)
+ ChatOptions(可选)
↓ 打包
Prompt
↓ ChatModel.call / stream
ChatResponse
├─ Generation(s) → AssistantMessage(文本 / toolCalls)
└─ Metadata → Usage 等

后面各节按这条管道展开:消息 → 请求 → 响应。工具调用并不是另一套协议,只是消息序列里多了几帧。
3.2 Message:四种角色
接口是 Message,常见实现按角色区分;角色枚举对应 MessageType:SYSTEM / USER / ASSISTANT / TOOL。共同底座通常还有文本内容和 metadata。
| 类型 | 角色 | 典型用途 |
|---|---|---|
SystemMessage | SYSTEM | 人设、全局约束、输出格式要求 |
UserMessage | USER | 用户输入;可附带 Media |
AssistantMessage | ASSISTANT | 模型回复;可带 toolCalls 与 Media |
ToolResponseMessage | TOOL | 工具执行结果回灌给模型 |
最小组装示例:
import java.util.List;
import org.springframework.ai.chat.messages.Message;
import org.springframework.ai.chat.messages.SystemMessage;
import org.springframework.ai.chat.messages.UserMessage;
import org.springframework.ai.chat.prompt.Prompt;
Message system = new SystemMessage("你是严谨的技术助手,用中文回答。");
Message user = new UserMessage("解释一下 Prompt 和 Message 的关系");
Prompt prompt = new Prompt(List.of(system, user));
要点:
-
System 更像「说明书」:语言、安全边界、是否允许猜测。
-
User 放单次任务细节;不要把整本知识库塞进 System。
-
Assistant / Tool 出现在多轮与工具循环里;纯问答时,请求侧常常只有 System + User。
ChatClient 的 .system(...) / .user(...),最终都会落到上述消息类型。
3.3 SystemMessage 与 UserMessage
SystemMessage
适合放稳定、跨轮仍成立的约束:
SystemMessage system = new SystemMessage("""
用中文回答。
不知道就说不知道,不要编造数据。
不要输出密钥或内部配置。
""");
单次问题、临时上下文(「请根据下面这段日志…」)更适合放在 User,而不是塞进 System。
UserMessage:文本 + 可选 Media
纯文本:
UserMessage user = new UserMessage("请用三句话总结 Spring AI 的 Prompt");
带图片等附件时,用 builder:
import org.springframework.ai.content.Media;
import org.springframework.ai.chat.messages.UserMessage;
import org.springframework.core.io.Resource;
import org.springframework.util.MimeTypeUtils;
UserMessage withMedia = UserMessage.builder()
.text("这张图里有什么?")
.media(Media.builder()
.mimeType(MimeTypeUtils.IMAGE_PNG)
.data(imageResource) // Resource / byte[] / URI 等 typed 重载
.name("device-fault.png")
.build())
.build();
在 ChatClient 里可以写成:
String answer = chatClient.prompt()
.system("用中文简要回答。")
.user(u -> u
.text("请 OCR 并总结图中文字")
.media(MimeTypeUtils.IMAGE_JPEG, imageResource))
.call()
.content();
注意:
-
多模态是否生效,取决于底层模型和供应商;把图片塞给纯文本模型,结果通常是被忽略或直接报错。
-
系统提示一侧一般不挂 Media。
3.4 Media:附件内容模型
Media 描述附件:MIME 类型、数据内容,以及可选的 id / name。它属于内容模型,不绑定某一家聊天供应商。
Spring AI 2.0.1 使用 typed 的 data(...) 重载(如 Resource、byte[]、URI、URL、字符串)。早期草稿里的 data(Object) 已删除;迁移旧代码时要改成明确类型。
import org.springframework.ai.content.Media;
import org.springframework.util.MimeTypeUtils;
byte[] imageBytes = ...;
Media image = Media.builder()
.mimeType(MimeTypeUtils.IMAGE_JPEG)
.data(imageBytes)
.name("device-fault.jpg") // 建议中性文件名
.build();
关于 name:
-
可能被模型当成指令,存在 prompt injection 风险,建议用中性名(如
img-001.jpg)。 -
部分云厂商对字符集有限制;Spring AI 不会在
Media内强制校验,需要业务自己约束。
看图对话与「文档入库做检索」是两件事:前者是把附件挂在当次 UserMessage 上;后者是离线切分、向量化后再检索注入——不要混成「每次上传都向量化」。
3.5 AssistantMessage 与 ToolResponseMessage
模型输出封装为 AssistantMessage:
| 情况 | 表现 |
|---|---|
| 纯文本回复 | toolCalls 为空,hasToolCalls() == false |
| 需要调工具 | toolCalls 非空;arguments 通常是 JSON 字符串 |
| 部分供应商 | 也可能带 Media |
读取工具调用:
import org.springframework.ai.chat.messages.AssistantMessage;
import org.springframework.ai.chat.model.ChatResponse;
ChatResponse response = chatModel.call(prompt);
AssistantMessage msg = response.getResult().getOutput();
if (msg.hasToolCalls()) {
for (AssistantMessage.ToolCall tc : msg.getToolCalls()) {
String id = tc.id();
String name = tc.name();
String argsJson = tc.arguments(); // 例如 {"city":"北京"}
// 按 name 执行本地工具,再用 ToolResponseMessage 回灌
}
}
工具执行完成后,以 TOOL 角色把结果塞回对话。ToolResponse.id 必须与对应 ToolCall.id 对齐,否则供应商会对不上调用:
import java.util.List;
import org.springframework.ai.chat.messages.ToolResponseMessage;
ToolResponseMessage toolMsg = ToolResponseMessage.builder()
.responses(List.of(
new ToolResponseMessage.ToolResponse(
toolCallId, // == ToolCall.id
"get_weather",
"{\"temp\":28,\"unit\":\"C\"}"
)
))
.build();
一次带工具的完整消息序列:

SystemMessage
UserMessage —— 「北京天气怎么样?」
AssistantMessage —— hasToolCalls=true(请求 get_weather)
ToolResponseMessage —— {"temp":28}(id 对齐)
AssistantMessage —— 最终自然语言回答
从应用视角,同一轮交互还可以画成「应用 ↔ 模型 ↔ 工具」的往返:

手工拼 Tool 循环时,会直接操作这些类型;若使用 ChatClient 的工具能力,框架通常会替你推进这些帧,直到某次助手消息不再请求工具,或触达调用上限。
3.6 Prompt:把消息列表变成请求
Prompt 是面向模型的请求抽象,核心是 消息列表 + 可选 ChatOptions。
import java.util.List;
import org.springframework.ai.chat.messages.SystemMessage;
import org.springframework.ai.chat.messages.UserMessage;
import org.springframework.ai.chat.prompt.ChatOptions;
import org.springframework.ai.chat.prompt.Prompt;
Prompt p1 = new Prompt("hello"); // 单条 user 文本
Prompt p2 = new Prompt(List.of(system, user));
Prompt p3 = new Prompt(List.of(system, user), chatOptions);
常用访问:
| 方法 | 含义 |
|---|---|
getInstructions() | 消息列表 |
getOptions() | 本次 ChatOptions(可能为 null) |
copy() | 复制 |
augmentSystemMessage(...) / augmentUserMessage(...) | 局部改写,不必推翻整个 Prompt |
augment* 很适合「在已有 Prompt 上补一条约束」:
Prompt hardened = prompt.augmentSystemMessage(sys ->
new SystemMessage(sys.getText() + "\n额外:禁止输出密钥。"));
ChatClient 也支持直接传入已组装的 Prompt,方便与底层 ChatModel API 互通。
3.7 ChatOptions:跨供应商选项
ChatOptions 是跨供应商的选项接口,常见字段包括 model、temperature、topP、maxTokens 等;各厂商实现还可扩展私有旋钮。
通用写法:
import org.springframework.ai.chat.prompt.ChatOptions;
ChatOptions options = ChatOptions.builder()
.model("gpt-4o-mini")
.temperature(0.3)
.build();
OpenAI 扩展示例:
import org.springframework.ai.openai.OpenAiChatOptions;
OpenAiChatOptions openAiOptions = OpenAiChatOptions.builder()
.model("gpt-4o")
.temperature(0.2)
.maxTokens(512)
.toolChoice("auto")
.build();
选项常有多层默认值:配置文件默认 → Client / Bean 默认 → 单次请求覆盖。合并与 mutate() 在这种场景下很有用。取「模型当前默认选项」时,优先使用现行的 getOptions() 一类 API,不要继续依赖已废弃的旧方法名。
3.8 ChatResponse、Generation 与 Usage
ChatResponse 是模型调用的返回入口:
| API | 含义 |
|---|---|
getResults() / getResult() | 候选列表 / 第一条(可能为 null) |
getMetadata() | 响应级元数据,含 Usage 等 |
hasToolCalls() | 任意一条 Generation 是否带工具调用 |
Generation.getOutput() 是 AssistantMessage;也可能带 finishReason 一类生成级元数据。若请求要求多条候选,getResults() 长度会大于 1;业务里多数情况只取第一条。
Usage 通常挂在响应元数据上(prompt / completion / total tokens)。若开启了工具循环,用量往往按整次调用累计,而不是只看最后一帧。流式场景下,中间分块可能没有完整 Usage,结束帧更常见。
完整读取示例:
import org.springframework.ai.chat.client.ChatClient;
import org.springframework.ai.chat.model.ChatResponse;
import org.springframework.ai.chat.metadata.Usage;
ChatResponse resp = chatClient.prompt()
.system("用中文简要回答。")
.user("什么是 Prompt?")
.call()
.chatResponse();
String text = resp.getResult().getOutput().getText();
Usage usage = resp.getMetadata().getUsage();
if (usage != null) {
// usage.getPromptTokens() / getCompletionTokens() / getTotalTokens()
}
3.9 两个 hasToolCalls(),别混用
| 方法 | 判断范围 |
|---|---|
AssistantMessage.hasToolCalls() | 这一条助手消息是否带工具调用 |
ChatResponse.hasToolCalls() | 任意 Generation 的输出是否带工具调用 |
手工工具循环可以写成:
import java.util.ArrayList;
import java.util.List;
import org.springframework.ai.chat.messages.Message;
import org.springframework.ai.chat.model.ChatModel;
import org.springframework.ai.chat.model.ChatResponse;
import org.springframework.ai.chat.prompt.Prompt;
List<Message> history = new ArrayList<>(List.of(system, user));
ChatResponse response = chatModel.call(new Prompt(history));
while (response.hasToolCalls()) {
var assistant = response.getResult().getOutput();
history.add(assistant);
// 1) 按 assistant.getToolCalls() 执行本地工具
// 2) 构造 ToolResponseMessage(id 对齐)并 history.add(toolMsg)
response = chatModel.call(new Prompt(history));
}
String finalText = response.getResult().getOutput().getText();
使用 ChatClient 默认工具能力时,业务代码通常看不到这个 while;读日志或写自定义 Advisor 时,这两个方法是很好的探针。
3.10 ChatClient 路径与 ChatModel 路径
两者用的是同一套类型,只是入口不同。
ChatClient(少写组装):
String text = chatClient.prompt()
.system("你是严谨的技术助手")
.user("用一句话解释 ChatResponse")
.call()
.content();
ChatModel(显式 Prompt):
import java.util.List;
import org.springframework.ai.chat.messages.SystemMessage;
import org.springframework.ai.chat.messages.UserMessage;
import org.springframework.ai.chat.model.ChatResponse;
import org.springframework.ai.chat.prompt.Prompt;
import org.springframework.ai.openai.OpenAiChatOptions;
ChatResponse response = chatModel.call(
new Prompt(
List.of(
new SystemMessage("你是严谨的技术助手"),
new UserMessage("用一句话解释 ChatResponse")
),
OpenAiChatOptions.builder().temperature(0.2).build()
)
);
String text = response.getResult().getOutput().getText();
也有便捷重载,例如 chatModel.call("hello")。流式侧对应 stream(Prompt);不支持流式的实现可能直接抛出异常。
选择建议:日常业务优先 ChatClient;需要更薄的调用、手工控制工具循环,或在图节点里做最小依赖时,再直接用 ChatModel。
3.11 Metadata 与模板变量
消息级 metadata
消息上的 metadata 是 Map<String, Object>,可放追踪 ID、租户等业务标记。ChatClient 的 user / system spec 也可写入对应消息。
响应级 metadata
更偏供应商信息:请求 id、model、限流、Usage、原生字段等,适合做观测与计费。
注意:Advisor 之间另有上下文参数(例如记忆用的 conversationId),那是 Advisor 上下文键,不是 MessageType,也不要和消息 metadata 混为一谈。
模板变量
模板渲染发生在变成真正 Message 之前:
String text = chatClient.prompt()
.user(u -> u.text("你好,{name}").param("name", "Tom"))
.call()
.content();
渲染完成后再进入 Prompt。默认可用 StringTemplate 一类实现,也可自定义 TemplateRenderer。
3.12 常见坑(本章范围)
| 现象 | 先查什么 |
|---|---|
getResult() 为 null | 是否真的返回了 Generation;空结果与异常要分开处理 |
| 工具对不上 / 模型反复要同一工具 | ToolResponse.id 是否等于 ToolCall.id;结果 JSON 是否可解析 |
| 图片没生效 | 模型是否支持视觉;MIME 与 data(...) 类型是否正确 |
| token 对不上预期 | 是否发生了多轮工具循环;流式是否只在结束帧有 Usage |
| Options 不生效 | 单次请求 Options 是否被更外层默认覆盖;厂商扩展字段是否写在正确的 Options 类型上 |
3.13 本章小结
到这里应能回答:
-
四类
Message各自承担什么角色,工具循环多了哪几帧 -
Prompt/ChatOptions如何组成一次请求 -
ChatResponse里文本、工具调用与 Usage 如何读取 -
为什么
ChatClient与ChatModel看起来不同,底层类型却一致
掌握这些类型后,后面无论用流畅 API 还是直接 ChatModel.call,读日志、排空结果、跟工具调用都会轻松很多。
407



被折叠的 条评论
为什么被折叠?



