🔥 深入 Spring AI 聊天补全:ChatClient、PromptTemplate、Advisor 一网打尽!
导读 :还在用裸 HTTP 调大模型?Spring AI 的
ChatClient让你像写流式代码一样调用 AI!今天深度拆解 Prompt 构建、模板渲染、顾问链------看完这篇,你的 AI 应用架构直接上一个台阶!
🎯 前言:为什么你必须搞懂 ChatClient?
上一章我们跑通了第一个 Spring AI 应用,但只停留在 "Hello World" 级别。真正落地生产时,你需要:系统角色设定、提示词模板化、Token 用量监控、请求响应拦截 ......这些 Spring AI 早就给你封装好了!今天这篇,把 ChatClient 的底层到高阶用法,全部盘清楚!建议先收藏,代码可以直接抄!
📌 核心概念速览
在 Spring AI 里,一次对话请求从发起到返回,要经过这么一条链路:
css
你的代码 → ChatClient → [Advisor链] → ChatModel → LLM API → ChatResponse → 你的代码
关键角色分工:
| 组件 | 职责 | 类比 |
|---|---|---|
ChatClient |
面向开发者的流畅 API 入口 | 餐厅前台 |
ChatModel |
底层与 LLM 交互的模型接口 | 后厨大厨 |
Prompt |
封装消息列表 + 选项的请求对象 | 菜单 + 口味备注 |
Message |
单条消息(用户/系统/助手/工具) | 菜单上的一道菜 |
Advisor |
请求/响应的拦截处理链 | 服务员、质检员 |
🧱 第一节:底层接口长啥样?
1.1 通用 Model 接口
Spring AI 设计了一套通用接口,所有 AI 模型都遵循这套契约:
java
public interface Model<TReq extends ModelRequest<?>,
TRes extends ModelResponse<?>> {
TRes call(TReq request);
}
人话翻译 :不管什么模型(GPT、Claude、Qwen),调用方式统一是 call(请求) → 返回 响应。这就是 Spring 的优雅------统一抽象,屏蔽差异。
ModelRequest 管请求(指令 + 选项),ModelResponse 管响应(结果 + 元数据)。ModelResult 里又分 output(AI 生成的内容)和 metadata(生成过程的元数据)。
1.2 ChatModel:专门聊大天的 Model
java
public interface ChatModel extends Model<Prompt, ChatResponse> {
ChatResponse call(Prompt prompt);
ChatOptions getDefaultOptions();
}
ChatModel 特化了请求类型为 Prompt,响应类型为 ChatResponse。每个模型实现(如 OllamaChatModel)都有自己的默认选项。
💬 第二节:Message 的四种身份
一次完整的对话,消息分为四种角色:
| 类型 | 英文 | 作用 | 场景 |
|---|---|---|---|
| 用户消息 | USER |
人说的 | "帮我写个排序算法" |
| 助手消息 | ASSISTANT |
AI 回的 | 生成的代码/回答 |
| 系统消息 | SYSTEM |
给 AI 的"人设" | "你是一个 Java 专家" |
| 工具消息 | TOOL |
函数调用结果 | 查询天气后的返回数据 |
核心洞察 :系统消息(SystemMessage)是控制 AI 行为的"隐形遥控器"。默认的系统消息是 "You're a helpful assistant.",太泛了!生产环境一定要自定义!
java
Prompt prompt = new Prompt(List.of(
new SystemMessage("You are a Java programming expert."),
new UserMessage(input)
));
💡 经验之谈:系统消息写得好,AI 输出质量直接翻倍。把系统消息当成"岗位说明书"来写!
🎛️ 第三节:ChatOptions 调参指南
大模型不是黑盒,有很多可调参数。Spring AI 通过 ChatOptions 暴露这些能力:
java
public interface ChatOptions extends ModelOptions {
String getModel(); // 模型名称
Double getFrequencyPenalty();// 频率惩罚(防重复)
Integer getMaxTokens(); // 最大输出 token 数
Double getPresencePenalty(); // 主题惩罚(防跑题)
List<String> getStopSequences();// 停止词
Float getTemperature(); // 温度(创造性 vs 确定性)
Float getTopP(); // 核采样
Integer getTopK(); // Top-K 采样
}
3.1 全局默认配置(application.yaml)
yaml
spring:
ai:
ollama:
chat:
options:
model: qwen3:0.6b
temperature: 0.7 # 0=保守,1=放飞
3.2 请求级覆盖(代码里动态调)
java
Prompt prompt = new Prompt(input,
OllamaChatOptions.builder()
.temperature(0.1) // 这次要严谨
.stop(List.of("Observation:"))
.build()
);
⚠️ 注意 :请求级覆盖需要底层
ChatModel支持合并选项。大部分官方实现都支持,但自定义模型要留意。
📊 第四节:获取 Token 用量,别被账单吓哭
调云模型是按 Token 计费的,拿到响应后第一件事------看用了多少 Token!
java
ChatResponse response = chatClient.prompt()
.user(input)
.call()
.chatResponse();
// AI 回复的文本
String content = response.getResult().getOutput().getText();
// Token 用量详情
Usage usage = response.getMetadata().getUsage();
int promptTokens = usage.getPromptTokens(); // 输入用了多少
int completionTokens = usage.getCompletionTokens();// 输出用了多少
long totalTokens = usage.getTotalTokens(); // 总计
ChatResponseMetadata 里还有啥?
RateLimit:限流信息(还剩多少次调用、什么时候重置)Usage:Token 消耗明细PromptMetadata:提示词过滤元数据
🔥 生产必做:把 Token 用量打到日志/监控里,账单异常时能第一时间发现!
🛠️ 第五节:ChatClient 流畅 API 实战
ChatClient 是 Spring AI 给开发者的"糖衣炮弹",链式调用爽到飞起:
5.1 最简用法
java
String content = chatClient.prompt()
.system("You are a master chef.") // 设定人设
.user("How to cook fish?") // 用户问题
.call() // 发送请求
.content(); // 取文本结果
5.2 已有 Prompt 对象?直接喂
java
Prompt prompt = new Prompt("hello");
String content = chatClient.prompt(prompt).call().content();
5.3 ChatClient.Builder 预置默认值
java
ChatClient chatClient = builder
.defaultSystem("你是一个专业的技术翻译") // 默认系统消息
.defaultAdvisors(loggingAdvisor) // 默认顾问
.defaultOptions(options) // 默认选项
.build();
Builder 方法速查表:
| 方法 | 作用 |
|---|---|
defaultUser |
预设默认用户消息 |
defaultSystem |
预设默认系统消息 |
defaultTools |
预设可用工具 |
defaultOptions |
预设聊天选项 |
defaultAdvisors |
预设顾问链 |
📝 第六节:PromptTemplate 提示词模板------告别字符串拼接
6.1 为什么需要模板?
想象一个烹饪建议服务,用户只输入 "鱼",但发给 AI 的提示词应该是:"如何在 5 分钟内做一道美味的鱼?" 这种"用户输入 + 固定模板"的场景,用 PromptTemplate 最合适。
6.2 模板文件 + 变量替换
src/main/resources/prompts/cooking.st:
css
How to cook {dish} in 5 minutes?
Java 代码:
java
@Value("classpath:/prompts/cooking.st")
private Resource promptResource;
public String chat(String dish) {
Prompt prompt = new PromptTemplate(promptResource)
.create(Map.of("dish", dish));
return chatClient.prompt(prompt).call().content();
}
✅ 最佳实践:模板放资源文件里,和代码解耦。改提示词不用重新编译!
6.3 模板 + 系统消息组合技
java
Message userMessage = new PromptTemplate(promptResource)
.createMessage(Map.of("dish", dish));
Prompt prompt = new Prompt(List.of(
new SystemMessage("You are a master chef."),
userMessage
));
6.4 Builder 模式 + 默认值
java
PromptTemplate template = PromptTemplate.builder()
.resource(promptResource)
.variables(Map.of("value", "hello")) // 默认值
.build();
// 运行时覆盖
Prompt prompt = template.create(Map.of("value", "world"));
6.5 自定义模板引擎
默认用 StringTemplate({var} 语法),不喜欢?自己写一个:
java
public interface TemplateRenderer
extends BiFunction<String, Map<String, Object>, String> {
String apply(String template, Map<String, Object> variables);
}
// 使用时
PromptTemplate template = PromptTemplate.builder()
.resource(promptResource)
.renderer(myCustomRenderer) // 注入自定义渲染器
.build();
🛡️ 第七节:Advisor 顾问链------Spring AI 的"中间件"
这是 Spring AI 最有架构感 的设计!Advisor 类似 Spring 的 AOP 拦截器,在请求到达模型前后插入通用逻辑。
7.1 核心接口
java
public interface CallAdvisor extends Advisor {
ChatClientResponse adviseCall(
ChatClientRequest request,
CallAdvisorChain chain
);
}
执行原理:多个 Advisor 组成链条,每个 Advisor 可以:
- 修改请求(
ChatClientRequest) - 调用下一个 Advisor(
chain.nextCall()) - 修改响应(
ChatClientResponse) - 直接返回,短路后续执行
7.2 写个日志顾问,调试神器
java
public class LoggingAdvisor implements CallAdvisor {
private static final Logger LOGGER =
LoggerFactory.getLogger("ChatClient.debugger");
@Override
public ChatClientResponse adviseCall(
ChatClientRequest request,
CallAdvisorChain chain) {
debug(request); // 记录请求
var response = chain.nextCall(request); // 继续执行
debug(response); // 记录响应
return response;
}
@Override
public int getOrder() {
return Ordered.LOWEST_PRECEDENCE; // 放最后执行
}
}
启用日志 (application.yaml):
yaml
logging:
level:
ChatClient.debugger: DEBUG
🚀 效果:每次 AI 调用的请求体和响应体,以 JSON 格式完整打印。排查问题时的救命稻草!
7.3 修改请求:重写用户文本
java
public class RewriteUserTextAdvisor implements CallAdvisor {
@Override
public ChatClientResponse adviseCall(
ChatClientRequest request, CallAdvisorChain chain) {
if (request.context().containsKey("updated_user_text")) {
String updatedText = (String) request.context()
.get("updated_user_text");
// 使用 mutate 构建新的不可变请求
var mutatedRequest = request.mutate()
.prompt(request.prompt()
.augmentUserMessage(updatedText))
.build();
return chain.nextCall(mutatedRequest);
}
return chain.nextCall(request);
}
}
REST 控制器里使用:
java
@RestController
public class RewriteController {
private final ChatClient chatClient;
public RewriteController(ChatClient.Builder builder) {
this.chatClient = builder
.defaultAdvisors(new RewriteUserTextAdvisor())
.build();
}
@PostMapping("/rewrite")
public String rewrite(@RequestBody Request req) {
return chatClient.prompt()
.user(req.input())
.advisors(spec -> spec.param(
"updated_user_text",
StringUtils.trimToEmpty(req.updatedInput())
))
.call()
.content();
}
}
7.4 两种启用方式
| 方式 | 代码 | 生效范围 |
|---|---|---|
| 全局默认 | builder.defaultAdvisors(advisor) |
该 Builder 创建的所有 ChatClient |
| 单次请求 | .prompt().advisors(advisor).call() |
仅当前请求 |
7.5 顾问顺序很重要!
Advisor 继承自 Ordered,数字越小越先执行。Spring AI 内置顾问有固定顺序,自定义顾问建议用大于 0 的值,避免冲突。
🔄 第八节:递归顾问------Spring AI 1.1 的王炸特性
Spring AI 1.1 之前,顾问链每个 Advisor 只能执行一次。1.1 引入了递归顾问,让链的一部分可以重复执行------这就能实现复杂的 Agent 工作流!
8.1 核心:copy 方法
CallAdvisorChain.copy(this) 可以复制当前 Advisor 之后的剩余链条,生成一个子链反复调用。
8.2 实战:让 AI 讲三次笑话
java
public class TellJokeAdvisor implements CallAdvisor {
@Override
public ChatClientResponse adviseCall(
ChatClientRequest request, CallAdvisorChain chain) {
var responses = new ArrayList<String>();
for (int i = 0; i < 3; i++) {
// 修改提示词:让 AI 讲笑话
var prompt = request.prompt().augmentUserMessage(
userMsg -> userMsg.mutate()
.text("Tell a joke about " + userMsg.getText())
);
// 复制子链并调用
var response = chain.copy(this)
.nextCall(request.mutate()
.prompt(prompt.build())
.build())
.chatResponse();
responses.add(response.getResult().getOutput().getText());
}
// 合并三次结果返回
return ChatClientResponse.builder()
.chatResponse(ChatResponse.builder()
.generations(List.of(new Generation(
AssistantMessage.builder()
.content(String.join("\n", responses))
.build())))
.build());
}
}
调用效果:输入 "programmer",返回 3 个关于程序员的笑话!
🔥 应用场景 :递归顾问是构建 Agent 评估-优化循环 、多轮工具调用 等高级模式的底层基础设施。玩转这个,你的 AI 应用就从"问答机器人"升级为"智能代理"!
🧠 总结:一张图看懂 Spring AI 聊天补全
scss
┌─────────────────────────────────────────────────────────────┐
│ ChatClient │
│ .prompt() → .system()/.user() → .call()/.stream() │
└────────────────────┬────────────────────────────────────────┘
│
┌────────────────────▼────────────────────────────────────────┐
│ Advisor Chain │
│ [LoggingAdvisor] → [RewriteAdvisor] → [TellJokeAdvisor] │
│ 记录日志 修改请求 递归执行 │
└────────────────────┬────────────────────────────────────────┘
│
┌────────────────────▼────────────────────────────────────────┐
│ ChatModel.call(Prompt) │
│ ┌─────────────────────────────────────────────┐ │
│ │ Prompt │ │
│ │ ├─ SystemMessage (人设/指令) │ │
│ │ ├─ UserMessage (用户问题) │ │
│ │ └─ ChatOptions (temperature/model等) │ │
│ └─────────────────────────────────────────────┘ │
└────────────────────┬────────────────────────────────────────┘
│ HTTP
┌────────────────────▼────────────────────────────────────────┐
│ LLM API │
└────────────────────┬────────────────────────────────────────┘
│
┌────────────────────▼────────────────────────────────────────┐
│ ChatResponse │
│ ├─ Generation → AssistantMessage (文本) │
│ ├─ Usage (Prompt/Completion/Total Tokens) │
│ └─ RateLimit (限流信息) │
└─────────────────────────────────────────────────────────────┘
💬 互动时间
你在用 Spring AI 时遇到过哪些坑?
- A:Prompt 模板管理混乱
- B:Advisor 顺序搞不清,调试到崩溃
- C:Token 用量监控没做,月底账单爆炸
- D:其他,评论区见
这篇文章干货密度很高,建议收藏⭐反复看!有问题评论区留言,我会逐一回复!转发给正在学 AI 开发的 Java 朋友,一起进步! 🚀
📚 下章预告 :
stream流式响应------让 AI 回答像打字一样实时呈现,用户体验直接拉满!关注不迷路~