ChatClient 入门:Fluent API
本教程共 45 篇 · 第 4 篇 · 更新于 2026-08-16 · 约 8 分钟阅读
本节目标:掌握 ChatClient 的创建、链式调用、同步/流式响应和默认配置。学完你能写出规范、可维护的对话代码。
4.1 ChatClient 是什么
ChatClient 是 Spring AI 的对话客户端。名字和用法都向 Spring 家族的 WebClient、RestClient 看齐:Builder 创建,链式调用构造请求,最后 call() 或 stream() 收尾。它从 1.x 就有了,2.0 里升级为一等公民,官方推荐的主入口。
它的底层是 ChatModel,外加一条 Advisor 链。普通对话用 ChatModel 也能写,但记忆、日志、RAG 这些横切能力都要自己拼。ChatClient 把它们变成了链上的一环,代码量立刻降下来。
两者的分工值得再强调一次。ChatModel 只负责发请求、收响应,像一个原始客户端。ChatClient 在它外面包了一层:请求规格、默认值、Advisor 链、响应转换。想理解框架怎么工作,记住”ChatClient 封装 ChatModel”这一句就够。
4.2 创建 ChatClient
三种方式,按场景选:
// 方式一:注入自动配置的 Builder(最常用)
@RestController
public class ChatController {
private final ChatClient chatClient;
public ChatController(ChatClient.Builder builder) {
this.chatClient = builder.build();
}
}
// 方式二:基于已有的 ChatModel 直接创建
ChatClient chatClient = ChatClient.create(chatModel);
// 方式三:用 Builder 做更多默认配置
ChatClient chatClient = ChatClient.builder(chatModel)
.defaultSystem("你是一个友好的助手")
.build();
Spring Boot 自动配置会为项目里的 ChatModel 准备一个 ChatClient.Builder,注入就能用。方式二适合临时拼一个,方式三适合把默认角色、默认 Advisor 都配好,业务代码只管问。方式二和方式三都需要一个 ChatModel 实例,自动配置的模型 Bean 可以直接注入。多个模型时再手动建 Bean,见 4.7。
Builder 上的默认配置可以带占位符,运行时再填参数。比如把角色语气做成可配置的:
@Configuration
class ChatConfig {
@Bean
ChatClient chatClient(ChatClient.Builder builder) {
return builder.defaultSystem("你是一个友好的助手,请用 {voice} 的语气回答")
.build();
}
}
调用时只需要补上 voice 的值:
String answer = chatClient.prompt()
.system(sp -> sp.param("voice", "海盗"))
.user("讲个笑话")
.call()
.content();
默认模板和请求参数各管一段,互不干扰。
4.3 一次调用的完整拆解
String answer = chatClient.prompt()
.user("用一句话介绍 Spring AI")
.call()
.content();
四步,各干一件事:
- prompt():开启一次请求的构建,返回请求规格对象。
- user():设置用户消息。
- call():同步调用模型,阻塞等待完整响应。
- content():从响应里取出文本。
prompt() 有三种重载:无参(链式构建)、传 String(直接给用户消息)、传 Prompt 对象(复用已有 Prompt)。日常用前两种就够。想复用一段带历史的消息列表,就用第三种。
prompt() 返回的请求规格对象支持链式调用,是因为每个方法都返回自身。user()、system()、options()、advisors() 都是它上面的方法,按需拼接。call() 和 stream() 是链的终点,返回响应规格,不能再往上加消息。
4.4 系统消息与模板参数
给模型设定角色,用 system():
String answer = chatClient.prompt()
.system("你现在是一名严肃的 Java 面试官。")
.user(question)
.call()
.content();
消息支持占位符,运行时填参。写法是 lambda 里 text() 写模板、param() 传值:
String answer = chatClient.prompt()
.user(u -> u
.text("列举 {composer} 配乐的 5 部电影")
.param("composer", "John Williams"))
.call()
.content();
占位符默认用花括号。提示词里要写 JSON 时容易冲突,可以换成其他分隔符。user() 和 system() 还支持传 Resource,从文件读模板,适合提示词较长的情况。分隔符也能改:通过 templateRenderer 换成 StTemplateRenderer 并指定新的起止符,比如
4.5 call() 的四种响应形式
| 方法 | 返回 | 适用场景 |
|---|---|---|
| content() | String | 只要文本 |
| chatResponse() | ChatResponse | 要元数据(Token 数、响应详情) |
| entity(Class) | Java 对象 | 结构化输出 |
| chatClientResponse() | ChatClientResponse | 要 Advisor 上下文(如 RAG 检索到的文档) |
结构化输出最常用。定义一个 record,直接映射:
record ActorFilms(String actor, List<String> movies) {}
ActorFilms result = chatClient.prompt()
.user("随机选一位演员,列出他的 5 部电影")
.call()
.entity(ActorFilms.class);
模型输出本质是字符串,entity() 背后是提示词工程加转换器。返回集合时用 entity(ParameterizedTypeReference),这些在结构化输出章节细讲:
List<ActorFilms> films = chatClient.prompt()
.user("列出汤姆·汉克斯和比尔·默瑞各自的 5 部电影")
.call()
.entity(new ParameterizedTypeReference<List<ActorFilms>>() {});
想监控成本,用 chatResponse():
ChatResponse response = chatClient.prompt()
.user("讲个笑话")
.call()
.chatResponse();
Usage usage = response.getMetadata().getUsage();
System.out.println("输入 Token: " + usage.getPromptTokens());
System.out.println("输出 Token: " + usage.getCompletionTokens());
ChatResponse 里除了回答,还有模型名、Token 统计等元数据。生成结果是列表结构,多数厂商一次只返回一个,按单个处理即可。chatClientResponse() 用得少,但排查问题时很有用:它能拿到 Advisor 执行期间的附加数据,比如 RAG 检索到了哪些文档。
4.6 流式响应:stream()
call() 要等模型生成完整个回答。长回答会让人干等。stream() 则边生成边返回,就是 ChatGPT 的打字机效果。
Flux<String> output = chatClient.prompt()
.user("讲一个冷笑话")
.stream()
.content();
Flux 是 Reactor 的异步流,每个元素是模型生成的一段文本。做成 Web 接口时,配合 SSE 让浏览器逐段显示。什么时候用哪个?回答短、要整体处理的用 call();回答长、要给用户即时反馈的用 stream()。一次请求要么同步要么流式,两者不能混用。
@GetMapping(value = "/chat/stream", produces = "text/plain;charset=utf-8")
public Flux<String> stream(@RequestParam String question) {
return chatClient.prompt()
.user(question)
.stream()
.content();
}
stream() 也有 chatResponse() 变体,返回 Flux
返回给前端的 Flux 由 Spring 框架负责订阅和推送,业务代码不用手动管理生命周期。接口的 produces 要设置成 text/plain 或 text/event-stream,浏览器才能正确渲染逐段内容。
Notestream() 走响应式栈。纯 Servlet 项目需要加 spring-boot-starter-webflux 依赖。反过来,响应式项目里用 call() 也要引入 spring-boot-starter-web。工具调用是命令式设计,会阻塞工作流,做流式时要留意。
4.7 默认配置与多模型
默认配置在 Builder 上设置,每次请求自动生效:
ChatClient chatClient = ChatClient.builder(chatModel)
.defaultSystem("你是一个友好的助手")
.defaultOptions(ChatOptions.builder().temperature(0.7))
.defaultAdvisors(new SimpleLoggerAdvisor())
.build();
对应的运行时方法(system()、options()、advisors())可以覆盖默认值,只影响当前这一次调用。想临时换角色,调用链里写 system() 就行,不会改掉默认值。比如平时默认 temperature 是 0.7,某次调用想要更严谨的回答:
String answer = chatClient.prompt()
.options(ChatOptions.builder().temperature(0.2))
.user(question)
.call()
.content();
这适用于同一个客户端在不同场景用不同参数的情况。
一个应用想用多个模型时,先关掉自动配置:
spring.ai.chat.client.enabled=false
再手动定义多个 ChatClient Bean,注入时用 @Qualifier 区分:
@Configuration
public class ChatClientConfig {
@Bean
public ChatClient openAiChatClient(OpenAiChatModel chatModel) {
return ChatClient.create(chatModel);
}
@Bean
public ChatClient anthropicChatClient(AnthropicChatModel chatModel) {
return ChatClient.create(chatModel);
}
}
模型接入章节会配合厂商示例再讲这套组合的完整用法。注意:关掉自动配置后,ChatClient.Builder 不再提供,所有 ChatClient 都要自己声明 Bean,别漏了这一步,否则启动就报找不到 Bean。
4.8 小结
ChatClient 的四步套路:prompt() 开头,user()/system() 装内容,call()/stream() 发起,content()/entity() 取结果。默认配置放 Builder,临时覆盖放调用链。这是全书出现频率最高的 API,务必熟练。