Spring AI ChatClient:多 Agent 系统的统一 LLM 调用层
做 AI Demo 时,调用模型可能只需要:
chatModel.call(prompt);
但到了生产环境,一次 LLM 调用往往还需要:
模型选择
对话记忆
系统提示词
工具调用
MCP
日志
流式输出
结构化返回
用户画像
如果每个 Agent 都自己处理这些事情,代码很快就会重复。
所以 Spring AI 提供了 ChatClient,而真实项目通常还会在它上面再封装一层自己的 LlmCaller。
1. ChatModel 和 ChatClient
先记住两者区别。
ChatModel
ChatModel 是底层模型接口。
可以理解为:
Prompt
↓
ChatModel
↓
DeepSeek / OpenAI / 智谱
↓
Response
它负责最基本的:
发送请求给模型并获得回答。
ChatClient
ChatClient 是对 ChatModel 的高级封装。
chatClient.prompt()
.user("帮我推荐 Java 岗位")
.call()
.content();
它不仅能调用模型,还可以继续挂:
Prompt
ChatMemory
Advisor
Tools
Structured Output
Streaming
Model Options
所以可以简单理解为:
ChatModel 负责“调用模型”,ChatClient 负责“组织一次完整的模型调用”。
2. ChatClient 是一次 LLM 调用的总装平台
一个完整的调用可能长这样:
ChatClient
│
┌─────────────┼─────────────┐
↓ ↓ ↓
Prompt Advisors Tools
│
ChatMemory
│
↓
ChatModel
↓
LLM
↓
Structured Output
也就是说,我们之前学的很多东西,最后都会汇聚到 ChatClient。
例如:
chatClient.prompt()
.system(systemPrompt)
.user(message)
.tools(jobTools)
.advisors(memoryAdvisor)
.call()
.entity(JobResult.class);
一次调用里已经同时包含:
系统提示词
用户输入
工具
记忆
结构化返回
3. Advisor 是什么?
Advisor 可以理解成:
ChatClient 调用链上的拦截器。
类似 Spring MVC 的 Interceptor。
例如:
用户请求
↓
LoggerAdvisor
↓
MemoryAdvisor
↓
ReActAdvisor
↓
LLM
不同 Advisor 负责不同能力。
Memory Advisor
例如:
MessageChatMemoryAdvisor
负责:
调用前
→ 从 ChatMemory 读取历史
调用模型
调用后
→ 保存新的聊天记录
所以要区分:
ChatMemory
负责存历史
MemoryAdvisor
负责把历史加入模型调用
Logger Advisor
负责记录:
Prompt
Response
Token
调用耗时
方便线上排查问题。
ReAct Advisor
用于控制:
LLM
↓
Tool Call
↓
执行工具
↓
Tool Result
↓
继续 LLM
可以在工具调用前后加入:
权限校验
日志
耗时统计
异常处理
最大迭代次数
4. ChatClient 的几种返回方式
最普通的是文本:
String result = chatClient.prompt()
.user("你好")
.call()
.content();
如果需要完整响应:
ChatResponse response = chatClient.prompt()
.user("你好")
.call()
.chatResponse();
可以获取:
Token Usage
Metadata
Generation
如果业务需要 Java 对象:
UserInfo info = chatClient.prompt()
.user(text)
.call()
.entity(UserInfo.class);
这就是 Structured Output。
如果要流式输出:
chatClient.prompt()
.user(message)
.stream()
.content();
所以可以记成:
content()
→ String
chatResponse()
→ 完整模型响应
entity()
→ Java Object
stream()
→ 流式输出
5. 多模型场景怎么办?
实际系统可能同时接:
DeepSeek
智谱
OpenAI
Claude
视觉模型
文本模型
所以不能把 ChatClient 永久绑定死在一个模型上。
而是:
用户请求
↓
ModelResolver
↓
选择 ChatModel
↓
创建 ChatClient
甚至还可以自动判断:
普通文本
→ TEXT Model
包含图片
→ VISION Model
例如:
用户上传招聘截图
↓
检测到 Media
↓
选择 Vision Model
↓
ChatClient
↓
提取岗位信息
因此:
Model Routing 负责选模型,ChatClient 负责使用模型。
6. 为什么还要再封装 LlmCaller?
如果只有一个 Agent:
ChatClient
基本够用。
但如果有:
岗位推荐 Agent
简历优化 Agent
岗位采集 Agent
任务 Agent
聊天 Agent
每个 Agent 都需要重复:
选择模型
创建 ChatClient
加载 ChatMemory
设置 Conversation ID
加载 Tools
加载 Advisor
设置 System Prompt
加载用户画像
于是就会出现大量重复代码。
所以生产项目通常再抽象:
Agent
↓
LlmCaller
↓
ChatClient
↓
ChatModel
↓
LLM
业务 Agent 只调用:
llmCaller.call(user, prompt);
至于底下到底用了什么模型、什么工具、什么记忆,它不关心。
7. LlmCaller 可以逐层增强
可以设计成三层。
SimpleLlmCaller
负责:
模型选择
TEXT / VISION 判断
ChatClient 创建
适合:
意图分类
摘要
结构化提取
BizAgentLlmCaller
在基础能力上增加:
ChatMemory
Conversation ID
Tools
Advisor
适合普通业务 Agent。
UserLlmCaller
继续增加:
用户画像
个性化 System Prompt
MCP
文件工具
Shell 工具
完整 Agent 工具集
适合真正面向用户的智能 Agent。
整体可以理解成:
Simple
↓
Memory + Tools
↓
User Profile + MCP
能力逐层增加。
8. 为什么这种设计适合多 Agent?
假设岗位 Agent 写:
llmCaller.call(user, prompt);
它不用知道:
这个用户用 DeepSeek 还是智谱
有没有图片
ChatMemory 存在哪里
需要哪个 Conversation ID
工具怎么注册
MCP 怎么连接
ReAct 怎么循环
这些全部由基础设施层完成。
所以最终职责变成:
Agent
负责业务
LlmCaller
负责统一 AI 调用
ChatClient
负责调用编排
Advisor
负责增强调用链
ChatModel
负责适配底层模型
这就是比较清晰的分层。
9. 把前面的知识全部串起来
现在前面几篇可以放到一张图里:
User
↓
Agent
↓
LlmCaller
↓
ChatClient
│
┌──────────────┼──────────────┐
↓ ↓ ↓
Model Routing ChatMemory Function Call
│ │ │
↓ Advisor ↓
ChatModel MCP
│ │
└──────────────┬───────────────┘
↓
LLM
↓
Structured Output
↓
Java Object
分别解决:
Model Routing
→ 用哪个模型?
ChatMemory
→ 模型记住什么?
Advisor
→ 调用过程怎么增强?
Function Call
→ 模型能调用什么工具?
MCP
→ 外部工具怎么接进来?
Structured Output
→ 模型结果怎么变成 Java 对象?
ChatClient
→ 怎么把这些能力组装起来?
LlmCaller
→ 怎么让业务 Agent 不关心这些复杂度?
10. 最值得沉淀的一句话
ChatClient 是 Spring AI 的 LLM 调用编排层,而生产级多 Agent 系统通常会在 ChatClient 上再封装统一的 LlmCaller,将模型路由、ChatMemory、Tool Calling、Advisor、MCP 等公共能力下沉,让业务 Agent 只关注自己的业务逻辑。
所以你可以把 Spring AI 整套东西理解成:
前面几篇
= 学零件
ChatClient
= 把零件组装起来
LlmCaller
= 给业务提供一个简单的开关
以后你自己做 Spring AI 项目,看到多个 Agent 开始重复写 ChatClient.builder()、tools()、advisors() 的时候,就应该意识到:
该抽一个统一的 LLM 调用层了。