白歌在白板上画了一条链:"
chatClient.prompt().system(...).user(...).options(...).advisors(...).call().content()——每一步返回不同的 Spec 对象,每一层都可以定制。这就是 Fluent API 的精髓。"
ChatClient 详解
定义与作用
ChatClient 是 Spring AI 中应用与 AI 模型交互的核心门面。它提供 Fluent API(链式调用),让开发者用一条链完成 Prompt 构建、选项配置、工具注册、Advisor 绑定和模型调用。
类比传统 Spring:ChatClient 之于 AI 调用,就像 JdbcTemplate 之于数据库操作——封装底层细节,提供简单直观的编程模型。
核心原理:完整调用链路
图释:ChatClient 在发送 Prompt 前经过 Advisor 链处理(注入历史、RAG 上下文、日志),收到 Response 后再经 Advisor 链处理(过滤、脱敏),最后返回给应用。
Fluent API 全貌
ChatClient 的 Fluent API 分为三个阶段的返回类型:
| 阶段 | 方法 | 返回类型 | 说明 |
|---|---|---|---|
| 构建请求 | .prompt() | ChatClientRequestSpec | 发起请求构建 |
.prompt(Prompt) | ChatClientRequestSpec | 传入已构造的 Prompt | |
| 设置消息 | .system(String) | ChatClientRequestSpec | 设置 System Message |
.user(String) | ChatClientRequestSpec | 设置用户消息 | |
.user(Consumer<UserSpec>) | ChatClientRequestSpec | 多模态用户消息 | |
.messages(Message...) | ChatClientRequestSpec | 手动添加任意消息 | |
| 配置选项 | .options(ChatOptions) | ChatClientRequestSpec | 覆盖模型参数 |
| 工具注册 | .functions(String...) | ChatClientRequestSpec | 本次调用启用的工具 |
.toolCallbacks(ToolCallback...) | ChatClientRequestSpec | 直接注册工具回调 | |
| 顾问注册 | .advisors(Advisor...) | ChatClientRequestSpec | 本次调用的 Advisor |
| 执行 | .call() | ChatClientCallResponseSpec | 阻塞执行 |
.stream() | Flux<ChatResponse> | 流式执行 | |
| 提取结果 | .content() | String | 提取文本内容 |
.chatResponse() | ChatResponse | 获取完整响应 |
创建 ChatClient 的方式
方式一:Builder 注入(推荐)
@Configuration
public class AiConfig {
@Bean
public ChatClient chatClient(ChatClient.Builder builder) {
return builder
.defaultSystem("你是飞翔科技大学的学生助手。")
.defaultOptions(ChatOptions.builder()
.temperature(0.7)
.build())
.build();
}
}
方式二:通过 ChatModel 手动构建
@RestController
public class ManualChatController {
private final ChatModel chatModel;
public ManualChatController(ChatModel chatModel) {
this.chatModel = chatModel;
}
@GetMapping("/manual-chat")
public String manualChat(@RequestParam String message) {
// 手动构建 ChatClient,不经过 Spring 容器
ChatClient client = ChatClient.builder(chatModel).build();
return client.prompt().user(message).call().content();
}
}
方式三:多个 ChatClient Bean
@Configuration
public class MultiClientConfig {
// 通用助手(默认)
@Bean
public ChatClient defaultClient(ChatClient.Builder builder) {
return builder.defaultSystem("你是通用助手。").build();
}
// 课程顾问(专用 System Prompt)
@Bean
public ChatClient courseAdvisor(ChatClient.Builder builder) {
return builder
.defaultSystem("""
你是飞翔科技大学的课程顾问。你了解所有专业的课程设置、
学分要求和毕业条件。请基于培养学生能力的目标给出建议。
""")
.defaultOptions(ChatOptions.builder()
.temperature(0.3) // 课程问题需要更确定
.build())
.build();
}
}
完整示例一:带 SystemMessage 和 ChatOptions 的链式调用
场景说明
高英(用户运营)需要分析学生反馈。她希望 AI 以运营专家的角色来分析,且输出要简洁。
关键代码
@RestController
public class FeedbackController {
private final ChatClient chatClient;
public FeedbackController(ChatClient.Builder builder) {
this.chatClient = builder
.defaultSystem("你是飞翔科技大学的用户运营分析专家。")
.build();
}
@GetMapping("/analyze-feedback")
public String analyze(@RequestParam String feedback) {
return chatClient.prompt()
.user("分析以下学生反馈,给出改进建议(不超过3条):" + feedback)
.options(ChatOptions.builder()
.temperature(0.2) // 分析任务需要低温度
.maxTokens(300)
.build())
.call()
.content();
}
}
运行结果
学生反馈:"选课系统太卡了,每次都抢不到热门课。"
AI 分析:
1. 技术层面:建议引入消息队列异步处理选课请求,缓解峰值压力。
2. 策略层面:可考虑分批次开放选课(高年级优先),分散瞬时流量。
3. 体验层面:增加选课等待队列和实时排队状态提示,降低学生焦虑。
完整示例二:流式调用与 SSE 输出
场景说明
孔蓝要求课程问答像聊天一样实时显示。小崔用流式调用实现。
关键代码
@RestController
public class StreamController {
private final ChatClient chatClient;
public StreamController(ChatClient chatClient) {
this.chatClient = chatClient;
}
// 流式 SSE
@GetMapping(value = "/stream", produces = "text/event-stream")
public Flux<String> stream(@RequestParam String question) {
return chatClient.prompt()
.user(question)
.stream()
.content();
}
// 流式 + 获取完整 ChatResponse(含 Token 用量)
@GetMapping(value = "/stream/detail", produces = "text/event-stream")
public Flux<String> streamDetail(@RequestParam String question) {
return chatClient.prompt()
.user(question)
.stream()
.chatResponse()
.map(resp -> {
String content = resp.getResult().getOutput().getContent();
if (content == null) return "";
// 最后一条附带 Token 用量
var usage = resp.getMetadata().getUsage();
if (usage != null && resp.getResults().size() == 1
&& resp.getResult().getMetadata().getFinishReason() != null) {
return content + "\n[Token: " + usage.getTotalTokens() + "]";
}
return content;
});
}
}
易错场景与面试考点
易错场景一:忘记调用 .call() 或 .content()
// ❌ 错误:ChatClientRequestSpec 转 String 会输出对象描述
@GetMapping("/chat")
public String chat(@RequestParam String msg) {
return chatClient.prompt().user(msg).toString();
// 输出:ChatClientRequestSpec@6d06d69c
}
问题分析
Fluent API 的每个阶段返回不同类型,必须完整走完链才能拿到文本。
// ✅ 正确:完整链
return chatClient.prompt().user(msg).call().content();
易错场景二:.stream() 后调用 .content() 的类型混淆
// ❌ 错误:stream().content() 返回 Flux<String>,不能直接赋值给 String
@GetMapping("/chat")
public String chat(@RequestParam String msg) {
return chatClient.prompt().user(msg).stream().content();
// 编译错误:Flux<String> 不能转为 String
}
问题分析
.stream() 返回 Flux<ChatResponse>,.content() 将其映射为 Flux<String>,必须用 Flux<String> 接收。
// ✅ 正确:返回 Flux<String>
@GetMapping(value = "/chat", produces = "text/event-stream")
public Flux<String> chat(@RequestParam String msg) {
return chatClient.prompt().user(msg).stream().content();
}
面试高频题
Q1:ChatClient 的 defaultSystem 和请求时的 .system() 有什么区别?
defaultSystem在创建 Bean 时设置,对所有请求生效。.system()在单次请求中设置,会覆盖defaultSystem。前者用于全局角色设定(如"你是学生助手"),后者用于临时场景切换。
Q2:Fluent API 中 .options() 做了什么?
.options()覆盖本次请求的模型参数(temperature、maxTokens 等)。它接收的 ChatOptions 会与 defaultOptions 合并,request 级别的优先级更高。
本章小结
- ChatClient 是 Spring AI 的核心门面,Fluent API 涵盖 prompt → message → options → functions → advisors → call → content 的完整链
- 创建方式:Builder 注入(推荐)、手动构建、多 Bean 专用化
defaultSystem设全局角色,.system()按需覆盖- 流式调用返回
Flux<String>,必须配合produces = "text/event-stream" - 链式调用每个阶段返回不同类型,不能中途跳过