ChatClient 完全掌握:从请求到响应,再到角色预设
前两篇我们完成了从零到接入模型的全过程。你学会了用
chatClient.prompt().user().call().content()发一条消息并收到回复。但这条链里到底发生了什么?call()之外还有哪些调用方式?怎么让 AI “扮演”某个角色?今天一口气把这些都搞明白。
到目前为止,你使用 ChatClient 的方式可能还停留在“丢一句话进去,拿一句话出来”。但在真实项目中,这样的能力远远不够。比如:
- 你需要 AI 以“资深 Java 面试官”的口吻回答问题,而不是泛泛而谈;
- 你希望接口不阻塞主线程,快速返回“任务已提交”,后台慢慢出结果;
- 你想看到请求和响应的原始 JSON,方便调优和问题排查;
- 你担心频繁地
new ChatClient会不会浪费资源。
这篇文章会把 ChatClient 的核心能力逐层拆开,让你从“会用”升级为“用透”。
一、开篇痛点:你会遇到这四个场景
场景一:AI 回答太“中立”了
你做了一个智能客服,用户问:“这东西坏了怎么办?” AI 回复:“请检查一下是否在保修期内,如果不在可以联系售后。” 听起来没毛病,但不够“专业”。你希望它用客服专员的语气,先安抚用户,再给解决方案。直接写在 user message 里提示词太散乱,怎么办?
场景二:接口响应太慢
一个请求可能要等 5 秒才出结果,用户在页面上干等着,体验很差。你想让接口立刻返回“正在处理”,然后让前端轮询或用 WebSocket 拿到结果。怎么把 AI 调用改成异步?
场景三:调试请求就像“黑盒”
AI 给的回复不符合预期,你想看看到底发过去的是什么 JSON、返回的又是什么。但代码里只拿到了最终字符串,中间过程一无所知。怎么把请求和响应日志打印出来?
场景四:ChatClient 和 ChatModel 到底啥关系?
你在文档里看到了 ChatModel 这个接口,也看到了 ChatClient,它们是什么关系?我该直接注入 ChatModel 用吗?
这四个场景,今天一次解决。
二、核心概念快览
在写代码之前,先快速理清几个概念。
2.1 ChatClient:面向用户的高层 API
ChatClient 是 Spring AI 为你提供的最高层级的对话工具。它使用流式(Fluent)API,把“构建请求、发送请求、处理响应”封装成一条链。你不需要手动拼 JSON、不需要管 HTTP 连接池,只管调用就好。
可以把它类比为 RestClient 或 WebClient——让你以声明的方式完成网络调用。
2.2 ChatModel:底层的模型抽象
ChatModel 是 Spring AI 的底层接口,直接对应与 AI 模型的通信协议。它只有一个核心方法:
ChatResponse call(Prompt prompt);
ChatClient 内部正是通过 ChatModel 来完成实际的调用。日常开发中我们直接使用 ChatClient 即可,除非你需要对请求和响应做非常细粒度的控制。
关系总结:ChatClient 是 ChatModel 的“装修工”——它帮你把原始的水泥钢筋(Prompt 对象),装成了拎包入住的精装房(Fluent API)。
2.3 同步调用与异步调用
- 同步调用:代码走到
.call()时会阻塞当前线程,直到 AI 返回完整结果。适合响应速度较快的场景,或者必须等结果才能继续计算的逻辑。 - 异步调用:调用后立即返回一个
CompletableFuture或Flux,不阻塞主线程。适合响应较慢的场景,或者需要同时调用多个 AI 服务再合并结果。
2.4 角色预设(System Prompt)
在和 AI 对话时,消息分为不同角色:
- System(系统消息):用来设定 AI 的行为、身份和语气,优先级最高。
- User(用户消息):你问的问题。
- Assistant(助手消息):AI 的回复(在多轮对话中可以用来传递历史记录)。
ChatClient 的 .system() 方法就是用来设置系统消息的。这是控制 AI 输出风格最简洁、最有效的手段。
2.5 日志打印
Spring AI 基于 Spring Boot 的日志体系,你可以通过配置文件打开 spring.ai 包的 DEBUG 日志,就能在控制台看到完整的请求和响应 JSON。这对开发阶段的调试非常有价值。
三、环境准备
继续使用前两篇搭建的 Spring Boot 项目,依赖不变,仍然是:
<dependency>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-starter-model-openai</artifactId>
</dependency>
配置文件 application.yml 用哪个模型都可以(DeepSeek、OpenAI、通义千问),今天演示以 DeepSeek 为例:
spring:
ai:
openai:
api-key: ${DEEPSEEK_API_KEY}
base-url: https://api.deepseek.com
chat:
options:
model: deepseek-chat
temperature: 0.7
四、代码实战
我们不在原有的 ChatController 上打补丁,而是新建一个 ChatService,把各种调用方式集中演示,然后用一个新的 Controller 暴露接口。
4.1 新建 AIChatService
在项目中创建 service 包,放入 AIChatService.java:
package com.example.springaihelloworld.service;
import org.springframework.ai.chat.client.ChatClient;
import org.springframework.ai.chat.model.ChatResponse;
import org.springframework.ai.chat.prompt.Prompt;
import org.springframework.ai.openai.OpenAiChatModel;
import org.springframework.stereotype.Service;
import java.util.concurrent.CompletableFuture;
@Service
public class AIChatService {
private final ChatClient chatClient;
private final OpenAiChatModel chatModel; // 底层 ChatModel,仅用于演示关系
// 构造器注入 ChatClient 和 ChatModel
public AIChatService(ChatClient chatClient, OpenAiChatModel chatModel) {
this.chatClient = chatClient;
this.chatModel = chatModel;
}
/**
* 1. 同步调用:最基本的调用方式
*/
public String chatSync(String message) {
return chatClient
.prompt()
.user(message)
.call()
.content();
}
/**
* 2. 同步调用并获取完整响应对象(包含元数据、Token 用量等)
*/
public ChatResponse chatSyncWithMetadata(String message) {
return chatClient
.prompt()
.user(message)
.call()
.chatResponse(); // 返回完整的 ChatResponse,而不是仅文本
}
/**
* 3. 异步调用:用 CompletableFuture 包装,不阻塞主线程
* 这里演示 Service 层自己封装的异步,实际 Spring AI 流式调用 stream() 也是一种异步。
* 流式内容将在下一篇文章专门展开。
*/
public CompletableFuture<String> chatAsync(String message) {
return CompletableFuture.supplyAsync(() ->
chatClient
.prompt()
.user(message)
.call()
.content()
);
}
/**
* 4. 角色预设:让 AI 扮演指定的角色
* system 方法设置系统消息,定义 AI 的行为和语气
*/
public String chatWithRole(String userMessage, String roleDescription) {
return chatClient
.prompt()
.system(roleDescription) // 系统提示词
.user(userMessage) // 用户消息
.call()
.content();
}
/**
* 5. 底层 ChatModel 直接调用演示(了解即可,日常不推荐)
*/
public String chatViaChatModel(String message) {
Prompt prompt = new Prompt(message);
ChatResponse response = chatModel.call(prompt);
return response.getResult().getOutput().getContent();
}
}
几个关键点解读:
chatSyncWithMetadata方法返回的ChatResponse对象包含了这次对话的完整元数据,比如 Token 消耗量、结束原因等。想查看消耗时非常有用。chatAsync用CompletableFuture.supplyAsync把同步调用放到独立线程池里,从而实现异步。真实生产环境中应当使用自定义线程池,避免用默认的ForkJoinPool。chatWithRole是角色预设的标准写法:system()定义身份,user()提出具体问题。chatViaChatModel直接使用ChatModel接口,绕过了ChatClient的便捷方法。通常不需要这样做。
4.2 新建 ChatClientController
在 controller 包下创建 ChatClientController.java:
package com.example.springaihelloworld.controller;
import com.example.springaihelloworld.service.AIChatService;
import org.springframework.ai.chat.model.ChatResponse;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import java.util.concurrent.CompletableFuture;
@RestController
public class ChatClientController {
private final AIChatService chatService;
public ChatClientController(AIChatService chatService) {
this.chatService = chatService;
}
// 同步对话
@GetMapping("/chat/sync")
public String chatSync(@RequestParam(defaultValue = "用一句话介绍你自己") String message) {
return chatService.chatSync(message);
}
// 同步对话(返回完整元数据)
@GetMapping("/chat/metadata")
public ChatResponse chatMetadata(@RequestParam(defaultValue = "你好") String message) {
return chatService.chatSyncWithMetadata(message);
}
// 异步对话(接口立即返回,结果通过 Future 在其他地方获取,这里仅做演示)
@GetMapping("/chat/async")
public String chatAsync(@RequestParam String message) {
CompletableFuture<String> future = chatService.chatAsync(message);
// 演示:这里直接等待拿到结果,真实场景中你可以返回给前端轮询
return "任务已提交。结果将异步返回,但为演示方便,我们等一下:" + future.join();
}
// 角色预设:翻译专家
@GetMapping("/chat/translator")
public String chatTranslator(@RequestParam(defaultValue = "Spring AI makes AI development easier for Java developers.") String text) {
String role = "你是一名专业的英文翻译,请将用户输入的任何内容翻译成简洁、准确的中文。如果用户输入已是中文,则翻译成英文。只返回翻译结果,不要添加任何解释。";
return chatService.chatWithRole(text, role);
}
// 角色预设:代码审查员
@GetMapping("/chat/reviewer")
public String chatReviewer(@RequestParam(defaultValue = "public void addUser(String name) { userList.add(name); }") String code) {
String role = "你是一名资深 Java 代码审查员。请检查用户提供的代码片段,指出潜在问题(如线程安全、空指针、性能等),并给出改进建议。语气要专业但友善。";
return chatService.chatWithRole(code, role);
}
// 底层 ChatModel 直接调用演示
@GetMapping("/chat/model")
public String chatModel(@RequestParam(defaultValue = "什么是 ChatModel?") String message) {
return chatService.chatViaChatModel(message);
}
}
4.3 开启请求/响应日志
在 src/main/resources/application.yml 中添加日志配置(也可以单独写在 application.properties 中):
logging:
level:
org.springframework.ai: DEBUG # 打印所有 Spring AI 相关日志
org.springframework.ai.openai: TRACE # 打印 OpenAI 请求/响应详细内容
重启应用后,每次调用 AI 接口时,控制台会输出类似下面的日志:
DEBUG o.s.ai.openai.OpenAiChatModel - Request: {"model":"deepseek-chat","messages":[{"role":"user","content":"你好"}]}
DEBUG o.s.ai.openai.OpenAiChatModel - Response: {"choices":[{"message":{"content":"你好!有什么可以帮你的?"}}]}
这些日志是调试的利器——当你发现 AI 回复不符合预期时,先看一眼实际上发过去的是什么消息结构,往往能快速定位问题。
注意:生产环境不建议开启 TRACE 级别,因为会打印 API Key 等敏感信息。开发调试完请立刻调回 INFO 或 WARN。
五、运行与演示
5.1 启动项目
确认 API Key 环境变量已设置,启动 Spring Boot 应用。
5.2 测试同步调用
http://localhost:8080/chat/sync?message=今天星期几?
返回正常的 AI 回复。
5.3 查看完整元数据
http://localhost:8080/chat/metadata?message=测试
返回 JSON 格式的 ChatResponse,你会看到除了回复文本外,还有 metadata 字段包含 Token 使用量等信息。
5.4 测试角色预设
访问翻译接口:
http://localhost:8080/chat/translator?text=The quick brown fox jumps over the lazy dog.
返回:
那只敏捷的棕色狐狸跳过了那只懒狗。
访问代码审查接口:
http://localhost:8080/chat/reviewer?code=public void addUser(String name) { userList.add(name); }
返回类似:
潜在问题:
1. 线程安全:如果 userList 是共享集合(如 ArrayList),在多线程环境下 add 操作是不安全的,建议使用 CopyOnWriteArrayList 或加锁。
2. 空指针:name 参数没有做 null 检查,可能导致 NPE 或业务异常。
3. 建议:添加参数校验,考虑线程安全方案。
这就是 system prompt 的威力。
六、常见问题与避坑提示
问题一:system prompt 不生效
有些模型(如部分轻量模型)对 system 角色的支持不够好。如果你发现设定角色后 AI 依然我行我素,可以尝试把角色描述直接写在 user message 的开头,作为一种备选方案:
String prompt = "【角色设定】你是翻译专家,只返回译文。\n【用户输入】" + text;
问题二:异步调用时的线程池问题
CompletableFuture.supplyAsync 默认使用 ForkJoinPool.commonPool(),在容器环境中可能导致线程泄漏或性能问题。建议在 Spring Boot 配置文件中定义自己的线程池:
@Configuration
@EnableAsync
public class AsyncConfig {
@Bean("aiTaskExecutor")
public Executor taskExecutor() {
ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
executor.setCorePoolSize(5);
executor.setMaxPoolSize(10);
executor.setQueueCapacity(100);
executor.setThreadNamePrefix("ai-call-");
executor.initialize();
return executor;
}
}
然后在方法上使用 @Async("aiTaskExecutor") 注解,或手动传入自定义线程池。
问题三:日志中出现了 API Key
开启 TRACE 日志后,请求头中可能会包含你的 API Key。调试完毕后务必把级别调回 INFO 以上,否则 Key 泄露后果严重。
问题四:ChatClient 与 ChatModel 我该用哪个?
99% 的场景用 ChatClient。只有当你要实现一些特殊逻辑(例如绕过 Fluent API 手动构造复杂的 Prompt 对象)时,才需要直接使用 ChatModel。
七、小结与下一步预告
本篇回顾
- 掌握了
ChatClient的 5 种调用方式:同步文本、同步元数据、异步包装、角色预设、底层模型直接调用 - 学会了用
.system()方法给 AI 设定专业角色,输出质量直线上升 - 开启了请求/响应日志,告别黑盒调试
- 理清了
ChatClient与ChatModel的层次关系
下一步预告
今天的异步调用还是用 CompletableFuture 手动包装的,真实场景中更优雅的方式是流式(Streaming)返回——AI 一个字一个字地“蹦”出来,就像 ChatGPT 网页版那样。
下一篇我们就来打造这个打字机效果。我们将学习 SSE 协议、Flux 响应式流,以及如何让前端实时消费流式输出。这是提升用户体验的关键一步。
下一篇见。
本系列博客基于 Spring AI 1.1.6 版本编写。建议在实际开发时查阅 Spring AI 官方文档 获取最新信息。
AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。
更多推荐

所有评论(0)