白歌对小崔说:"ChatClient 好用,但你要理解底下那层 ChatModel。当你的消息流需要完全手动控制时,直接拿 ChatModel 会更灵活。"
ChatModel 底层抽象
定义与作用
ChatModel 是 Spring AI 中与 AI 供应商通信的底层抽象接口。它定义了与任何 LLM 交互的标准化契约:
public interface ChatModel {
ChatResponse call(Prompt prompt);
Flux<ChatResponse> stream(Prompt prompt);
}
ChatClient 在内部将请求委托给 ChatModel,ChatModel 再通过各提供商的 HTTP 客户端(OpenAI、Ollama 等)发送请求。
核心原理:ChatClient 与 ChatModel 的分工
图释:ChatClient 负责所有"智能"功能(Advisor、Tool Calling、记忆管理),ChatModel 只负责"传输"——把构造好的 Prompt 发给 LLM 并返回 Response。
ChatClient vs ChatModel 对比
| 维度 | ChatClient | ChatModel |
|---|---|---|
| 定位 | 高层门面,开发者主接口 | 底层抽象,与 LLM 直接通信 |
| API 风格 | Fluent API 链式调用 | call(Prompt) / stream(Prompt) |
| Advisor | 自动链式处理 | 不参与,需手动封装 |
| Tool Calling | 自动调度 | 不参与,需手动管理 ToolResponseMessage |
| ChatMemory | 通过 Advisor 自动注入 | 不参与,需手动管理消息列表 |
| 适用场景 | 90% 的日常对话场景 | 底层消息流操控、自定义调度逻辑 |
| 创建方式 | ChatClient.Builder 注入 | 各 Starter 自动配置,直接 @Autowired |
何时直接使用 ChatModel
以下场景 ChatClient 不够灵活,应直接操作 ChatModel:
- 手动管理 Tool Calling 循环:需要精确控制工具调用何时结束
- 动态构造复杂 Prompt:消息列表需要在运行时动态拼接多种类型
- 自定义重试/降级逻辑:需要在一次调用失败后切换备用模型
- 精细的 Token 预算控制:手动计算消息 Token 数并裁剪历史
完整示例一:直接使用 ChatModel 进行底层调用
场景说明
小崔需要手动构建 Message 列表,精确控制每一条消息的内容和角色。
关键代码
@RestController
public class ModelController {
private final ChatModel chatModel;
public ModelController(ChatModel chatModel) {
this.chatModel = chatModel;
}
@GetMapping("/model-chat")
public String modelChat(@RequestParam String question) {
// 手动构造完整的 Prompt
Prompt prompt = new Prompt(List.of(
new SystemMessage("""
你是飞翔科技大学的学生助手。
如果不知道答案,请直接说"我不确定"。
"""),
new UserMessage(question)
));
ChatResponse response = chatModel.call(prompt);
return response.getResult().getOutput().getContent();
}
}
运行结果
GET /model-chat?question=计算机科学专业需要修多少学分
回复:根据飞翔科技大学2024级培养方案,计算机科学专业要求修满170学分,
其中通识教育40学分、学科基础45学分、专业核心50学分、实践环节35学分。
完整示例二:手动管理多轮对话(无 ChatMemory)
场景说明
在没有 ChatMemory 的情况下,小崔手动维护对话历史列表。
关键代码
@RestController
public class ManualMemoryController {
private final ChatModel chatModel;
// 手动维护的消息历史
private final List<Message> history = new ArrayList<>();
public ManualMemoryController(ChatModel chatModel) {
this.chatModel = chatModel;
// 初始化 System Message
history.add(new SystemMessage("你是飞翔科技大学的学生助手。"));
}
@GetMapping("/manual-chat")
public String chat(@RequestParam String message) {
// 追加用户消息到历史
history.add(new UserMessage(message));
// 用完整历史构造 Prompt
Prompt prompt = new Prompt(new ArrayList<>(history));
ChatResponse response = chatModel.call(prompt);
// 追加 AI 回复到历史
String answer = response.getResult().getOutput().getContent();
history.add(new AssistantMessage(answer));
return answer;
}
@GetMapping("/manual-chat/clear")
public String clearHistory() {
history.clear();
history.add(new SystemMessage("你是飞翔科技大学的学生助手。"));
return "对话历史已清除";
}
}
运行结果
GET /manual-chat?message=我叫张三
→ "你好张三!有什么可以帮助你的?"
GET /manual-chat?message=我叫什么名字
→ "你叫张三。" ← 手动维护的历史生效
GET /manual-chat/clear
→ "对话历史已清除"
GET /manual-chat?message=我叫什么名字
→ "抱歉,我不知道你的名字。" ← 历史已清除
易错场景与面试考点
易错场景一:ChatModel 不会自动注入 SystemMessage
// ❌ 错误:以为 ChatModel 会像 ChatClient 一样自动注入 defaultSystem
ChatModel chatModel;
Prompt prompt = new Prompt(List.of(
new UserMessage("今天天气怎么样")
));
// SystemMessage 缺失!模型可能回答得不够聚焦
问题分析
ChatModel 是纯粹的传输层,不做任何自动注入。SystemMessage 必须手动添加到 Prompt 中。
// ✅ 正确:手动添加 SystemMessage
Prompt prompt = new Prompt(List.of(
new SystemMessage("你是专业助手。"),
new UserMessage("今天天气怎么样")
));
易错场景二:流式调用 ChatModel 时错误地收集结果
// ❌ 错误:用 Flux.collectList() 阻塞流式响应
Flux<ChatResponse> flux = chatModel.stream(prompt);
List<ChatResponse> list = flux.collectList().block();
String result = list.get(list.size() - 1) // 取最后一条
.getResult().getOutput().getContent();
// 失去了流式的实时性优势
问题分析
流式的价值在于实时推送。如果最终还是要阻塞等待全部结果,直接使用 call() 更合适。
// ✅ 正确:流式逐 Token 处理
return chatModel.stream(prompt)
.map(resp -> resp.getResult().getOutput().getContent())
.filter(Objects::nonNull);
面试高频题
Q1:ChatClient 和 ChatModel 的分工边界是什么?
ChatClient 负责智能层(Fluent API、Advisor 链、Tool Calling 调度、ChatMemory 注入),ChatModel 负责传输层(把 Prompt 发给 LLM,返回 ChatResponse)。日常开发用 ChatClient,需要手动控制消息流时用 ChatModel。
Q2:什么场景下应该绕过 ChatClient 直接使用 ChatModel?
①需要手动管理 Tool Calling 的多轮循环;②需要动态构造复杂 Prompt 消息序列;③需要自定义重试/降级逻辑(如失败后切换模型);④需要精确的 Token 预算控制(手动裁剪消息历史)。
本章小结
- ChatModel 是底层抽象,只有
call(Prompt)和stream(Prompt)两个方法 - ChatClient 内部委托给 ChatModel,并附加 Advisor、Tool Calling、ChatMemory 等智能层
- 直接使用 ChatModel 需手动管理 SystemMessage、对话历史、工具调用循环
- 90% 场景用 ChatClient,10% 场景(底层消息操控)用 ChatModel