ChatModel 与 ChatClient 关系完整指南
本文所有 API 签名 / 类关系 / 方法列表均通过
javap反编译 Spring AI 1.1.4 jar 实际验证。参与 jar:
spring-ai-model-1.1.4.jar、spring-ai-client-chat-1.1.4.jar、spring-ai-alibaba-dashscope-1.1.2.2.jar
1. 一句话结论
ChatClient 是高层门面,底层委托给 ChatModel。
· ChatModel = 接口规范 + 各家实现(每个 starter 一个)
· ChatClient = 唯一默认实现(DefaultChatClient),内部持有一个 ChatModel
它们不是替代关系,是分层关系------ChatClient 站在 ChatModel 肩膀上,提供链式 API + Advisor / Tool / 结构化输出等高级能力。
2. 分层架构
┌─────────────────────────────────────────────────────────────────┐
│ ChatClient(应用代码面向的高层门面) │
│ ── 接口 + 唯一默认实现 DefaultChatClient │
│ ── 所在 jar:spring-ai-client-chat │
│ ── 能力:链式 API、Advisor、Tool、模板、结构化输出、流式 │
└──────────────────────────┬──────────────────────────────────────┘
│ 内部 new 出一个具体 ChatModel
│ 通过它发真实请求
▼
┌─────────────────────────────────────────────────────────────────┐
│ ChatModel(能力接口,各家 starter 实现) │
│ ── 接口 + 各供应商各 1 实现 │
│ ── 所在 jar:spring-ai-model │
│ ── 能力:call(Prompt) / stream(Prompt) / call(String) │
└──────────────────────────┬──────────────────────────────────────┘
│ implements
┌────────────────────┼────────────────────┬─────────────┐
▼ ▼ ▼ ▼
OpenAiChatModel DashScopeChatModel OllamaChatModel DeepSeekChatModel
(spring-ai-openai) (spring-ai-alibaba) (spring-ai-ollama) ...
关键事实(javap 验证)
java
// spring-ai-client-chat-1.1.4.jar
public interface org.springframework.ai.chat.client.ChatClient { ... }
public class org.springframework.ai.chat.client.DefaultChatClient
implements ChatClient { ... } // 唯一默认实现
// spring-ai-model-1.1.4.jar
public interface org.springframework.ai.chat.model.ChatModel
extends Model<Prompt, ChatResponse>, StreamingChatModel { ... }
// 各 starter 里
public class org.springframework.ai.openai.OpenAiChatModel
implements ChatModel { ... } // ✅
public class com.alibaba.cloud.ai.dashscope.chat.DashScopeChatModel
implements ChatModel { ... } // ✅
ChatClient 不是各家 starter 各自实现的 ------每家 starter 只实现 ChatModel 接口,ChatClient 在 Spring AI 本体里只有一个 DefaultChatClient 实现。
3. API 签名对比
3.1 ChatModel 的方法(javap 验证)
java
public interface ChatModel extends StreamingChatModel {
// 同步
default String call(String text); // 简写
default String call(Message... messages); // 多消息
abstract ChatResponse call(Prompt prompt); // 完整,能拿元数据
// 流式(继承自 StreamingChatModel)
default Flux<String> stream(String text);
default Flux<String> stream(Message... messages);
abstract Flux<ChatResponse> stream(Prompt prompt);
default ChatOptions getDefaultOptions();
}
3.2 ChatClient 的方法
java
public interface ChatClient {
// 启动方式(3 个重载)
ChatClientRequestSpec prompt(); // 从 defaults 开始
ChatClientRequestSpec prompt(String userText); // 快捷:= prompt().user(x)
ChatClientRequestSpec prompt(Prompt prompt); // 以已有 Prompt 为起点
}
// 链式配置
public interface ChatClientRequestSpec {
.system(String)
.user(String)
.messages(Message...)
.options(ChatOptions)
.advisors(Advisor...)
.tools(Object...)
.templateRenderer(TemplateRenderer)
// ...
CallResponseSpec call();
StreamResponseSpec stream();
}
// 同步响应读取
public interface CallResponseSpec {
String content();
ChatResponse chatResponse();
<T> T entity(Class<T>); // ★ 结构化输出
<T> T entity(ParameterizedTypeReference<T>);
<T> T entity(StructuredOutputConverter<T>);
}
// 流式响应读取
public interface StreamResponseSpec {
Flux<String> content();
Flux<ChatResponse> chatResponse();
Flux<ChatClientResponse> chatClientResponse();
// ⚠️ 没有 entity()!流式无法反序列化到实体
}
4. 同一功能的两种写法对比
4.1 最简问答
java
// ChatModel(一行最短)
String answer = chatModel.call("你好");
// ChatClient(链式稍长,但可扩展)
String answer = chatClient.prompt().user("你好").call().content();
// 或
String answer = chatClient.prompt("你好").call().content();
单次调用 ChatModel 更短,但后续加功能(系统提示/记忆/RAG)时不用重写。
4.2 加系统提示
java
// ChatModel:手工构造 Prompt
Prompt prompt = new Prompt(List.of(
new SystemMessage("你是翻译助手"),
new UserMessage(query)
));
String text = chatModel.call(prompt).getResult().getOutput().getText();
// ChatClient:一行
String text = chatClient.prompt()
.system("你是翻译助手")
.user(query)
.call()
.content();
4.3 多轮对话记忆
java
// ChatModel:自己维护 List<Message>
List<Message> history = loadHistory(userId); // 你自己写存储
history.add(new UserMessage(query));
Prompt prompt = new Prompt(history);
ChatResponse resp = chatModel.call(prompt);
history.add(resp.getResult().getOutput());
saveHistory(userId, history); // 你自己写存储
String text = resp.getResult().getOutput().getText();
// ChatClient:一行 advisor
String text = chatClient.prompt()
.user(query)
.advisors(a -> a.param(MessageChatMemoryAdvisor.CONVERSATION_ID, userId))
.call()
.content();
// (前提:LLMConfig 里把 MessageChatMemoryAdvisor 作为 defaultAdvisor 注册过)
4.4 结构化输出
java
// ChatModel:自己拼 JSON 格式说明 + 自己 parse
Prompt prompt = new Prompt("给我一本书,严格按格式返回:{\"name\":\"...\", \"author\":\"...\"}");
String raw = chatModel.call(prompt).getResult().getOutput().getText();
String cleaned = raw.replaceAll("```json|```", "").trim();
Book book = objectMapper.readValue(cleaned, Book.class); // 可能抛 JsonParseException
// ChatClient:一行
Book book = chatClient.prompt()
.user("给我一本书")
.call()
.entity(Book.class);
// 背后 Spring AI 自动:生成 Schema → 追加到 prompt → 剥 markdown → Jackson 反序列化
4.5 工具调用(Function Calling)
java
// ChatModel:手写 ToolCallback 注册 + 自己处理 tool_calls 响应 + 回送
// (约 50~100 行代码)
// ChatClient:一行
String answer = chatClient.prompt()
.user("查北京今天天气")
.tools(new WeatherTool()) // 类上加 @Tool 方法即可
.call()
.content();
4.6 流式响应
java
// ChatModel
Flux<String> flux = chatModel.stream(query); // 最简 1 行
// 或拿元数据:
Flux<ChatResponse> flux2 = chatModel.stream(prompt);
// ChatClient
Flux<String> flux = chatClient.prompt().user(query).stream().content();
Flux<ChatResponse> flux2 = chatClient.prompt().user(query).stream().chatResponse();
4.7 拿 token 用量 / 元数据
java
// ChatModel:call(Prompt) 直接返回 ChatResponse
ChatResponse resp = chatModel.call(new Prompt(query));
String text = resp.getResult().getOutput().getText();
Usage usage = resp.getMetadata().getUsage();
String id = resp.getMetadata().getId();
String finishReason = resp.getResult().getMetadata().getFinishReason();
// ChatClient:.chatResponse() 拿到同样的对象
ChatResponse resp = chatClient.prompt().user(query).call().chatResponse();
// 以下字段取法一致
5. 能力对比总表
| 场景 | ChatModel | ChatClient |
|---|---|---|
| 单轮问答(最短代码) | ✅ 1 行 call(String) |
⚠️ 4 步链式 |
| 加系统提示词 | ❌ 手工构造 Prompt | ✅ .system(...) 一行 |
| 多轮对话记忆 | ❌ 自己管 List | ✅ .advisors(memoryAdvisor) |
| RAG 检索增强 | ❌ 自己查向量库 | ✅ .advisors(ragAdvisor) |
| 工具调用 | ❌ 自己处理 ToolCallback | ✅ .tools(new XxxTool()) |
| 结构化输出→实体 | ❌ 自己 parse JSON | ✅ .entity(Book.class) |
| 日志观测 | ❌ 自己埋点 | ✅ SimpleLoggerAdvisor |
| 流式 SSE | ✅ 一样支持 | ✅ 一样支持 |
| 拿 token 元数据 | ✅ 直接 ChatResponse |
✅ .chatResponse() |
| 厂商独家参数 | ✅ new Prompt(msg, DashScopeChatOptions) |
✅ .options(DashScopeChatOptions) |
| 多供应商混用 | ✅ 注入 DashScopeChatModel / OllamaChatModel 分别使用 |
⚠️ 需要多 Builder + @Qualifier |
结论:
- 任务越复杂,ChatClient 优势越大(一行 advisor 完成 ChatModel 几十行代码的事)
- 单次一问一答 / 多供应商并行使用,ChatModel 更直接
6. 注入姿势对照
6.1 单 starter 项目
java
// 方式 A:注入通用接口(只有一个实现,Spring 自动匹配)
@Autowired ChatModel chatModel;
// 方式 B:注入具体子类(写死供应商)
@Autowired DashScopeChatModel dashScopeChatModel;
// 方式 C:注入 ChatClient.Builder,自己 build(官方推荐应用代码使用)
@Autowired ChatClient.Builder builder;
ChatClient chatClient = builder.build();
// 方式 D:@Bean ChatClient(LLMConfig 统一配好后)
@Autowired ChatClient chatClient;
6.2 多 starter 共存(例如同时装了 openai + dashscope + ollama)
java
// 必须注入具体子类,否则 @Autowired ChatModel 会报 bean 歧义
@Autowired OpenAiChatModel openAi;
@Autowired DashScopeChatModel dashScope;
@Autowired OllamaChatModel ollama;
// ChatClient 场景:需要通过 @Qualifier 区分不同 Builder
@Autowired @Qualifier("openAiChatClientBuilder") ChatClient.Builder openAiBuilder;
@Autowired @Qualifier("dashScopeChatClientBuilder") ChatClient.Builder dashScopeBuilder;
7. 选型决策树
你要做什么?
│
├─ 写底层工具 / 框架 / 自定义 Advisor(需要精细控制请求响应)
│ └─→ ChatModel
│
├─ 多家供应商并行使用(ChatGPT 做 A 功能、DeepSeek 做 B 功能)
│ └─→ ChatModel(各自注入 XxxChatModel)
│
├─ 只用一家供应商,且要拿 token 元数据、finishReason
│ └─→ ChatModel 最直接;ChatClient .chatResponse() 也行
│
├─ 业务代码(对话、客服、AI 助手、RAG 应用)
│ └─→ ChatClient ✅ 推荐
│ 后续要加记忆 / 工具 / 结构化输出时一行搞定
│
└─ 学习 Spring AI
└─→ 先学 ChatModel 理解底层,再学 ChatClient 写业务
8. 类比理解
| Spring AI | 传统 Java Web | 特点 |
|---|---|---|
ChatModel 各家实现 |
JDBC Driver(MySQL Driver / PG Driver) | 各供应商自家实现 |
ChatModel 接口 |
JDBC API | 规范 |
ChatClient / DefaultChatClient |
JdbcTemplate / MyBatis | 建在驱动之上的好用门面 |
Advisor |
Servlet Filter / Spring AOP 环绕通知 | 拦截器 |
ChatOptions |
Connection Properties | 请求参数 |
Prompt |
SQL 语句 + 参数 | 完整请求载体 |
9. 常见误区
9.1 误区:ChatClient 是"全新的聊天 API"
❌ 错。ChatClient 底层仍然调 ChatModel。它不是替代 ,是装饰。
9.2 误区:".defaultSystem() 写过后,.system() 会追加"
❌ 错。字节码实测:.system(String) 做的是 this.systemText = text 赋值替换,会把默认值完全覆盖。
java
// LLMConfig
builder.defaultSystem("你是严谨助手");
// Controller
chatClient.prompt().system("你是脱口秀演员").user(q).call().content();
// → 实际 system = "你是脱口秀演员",默认的"严谨助手"没了
9.3 误区:".stream().entity(Book.class)" 能流式拿结构化
❌ 编译不过。StreamResponseSpec 没有 entity() 方法------流式 JSON 片段无法反序列化。要"流式 + 结构化"只能:流式显示文本 → 结束后拼完整 JSON → 自己 parse。
9.4 误区:多 starter 共存时 @Autowired ChatModel 能自动选一个
❌ 错。多个 ChatModel bean 共存会触发 NoUniqueBeanDefinitionException。必须注入具体子类或用 @Qualifier。
9.5 误区:ChatClient 不能拿厂商独家参数
❌ 错。可以:
java
chatClient.prompt()
.user(q)
.options(DashScopeChatOptions.builder().enableSearch(true).build())
.call()
.content();
独家参数走 .options() 照样好用------不是非 ChatModel 不可。
10. 验证方式
bash
# 验证 ChatModel 接口
javap -cp spring-ai-model-1.1.4.jar \
org.springframework.ai.chat.model.ChatModel
# 验证 ChatClient 接口 + DefaultChatClient 实现
javap -cp spring-ai-client-chat-1.1.4.jar \
org.springframework.ai.chat.client.ChatClient
jar tf spring-ai-client-chat-1.1.4.jar | grep DefaultChatClient
# 验证 DashScopeChatModel 实现了 Spring AI 的接口
javap -cp spring-ai-alibaba-dashscope-1.1.2.2.jar \
com.alibaba.cloud.ai.dashscope.chat.DashScopeChatModel | head -2
# 输出:public class ... implements org.springframework.ai.chat.model.ChatModel
11. 参考资料
- Spring AI 官方文档:https://docs.spring.io/spring-ai/reference/
spring-ai-01quickstart/SPRING-AI-ARCHITECTURE.md(本项目)------ 整体架构spring-ai-alibaba-06advisors/ADVISORS-AND-MEMORY.md(本项目)------ Advisor 深入spring-ai-alibaba-04chatmodel(本项目)------ ChatModel 代码示例spring-ai-alibaba-05chatclient(本项目)------ ChatClient 代码示例