前两篇我们完成了从零到接入模型的全过程。你学会了用 chatClient.prompt().user().call().content() 发一条消息并收到回复。但这条链里到底发生了什么?call() 之外还有哪些调用方式?怎么让 AI “扮演”某个角色?今天一口气把这些都搞明白。

到目前为止,你使用 ChatClient 的方式可能还停留在“丢一句话进去,拿一句话出来”。但在真实项目中,这样的能力远远不够。比如:

  • 你需要 AI 以“资深 Java 面试官”的口吻回答问题,而不是泛泛而谈;
  • 你希望接口不阻塞主线程,快速返回“任务已提交”,后台慢慢出结果;
  • 你想看到请求和响应的原始 JSON,方便调优和问题排查;
  • 你担心频繁地 new ChatClient 会不会浪费资源。

这篇文章会把 ChatClient 的核心能力逐层拆开,让你从“会用”升级为“用透”。

一、开篇痛点:你会遇到这四个场景

场景一:AI 回答太“中立”了

你做了一个智能客服,用户问:“这东西坏了怎么办?” AI 回复:“请检查一下是否在保修期内,如果不在可以联系售后。” 听起来没毛病,但不够“专业”。你希望它用客服专员的语气,先安抚用户,再给解决方案。直接写在 user message 里提示词太散乱,怎么办?

场景二:接口响应太慢

一个请求可能要等 5 秒才出结果,用户在页面上干等着,体验很差。你想让接口立刻返回“正在处理”,然后让前端轮询或用 WebSocket 拿到结果。怎么把 AI 调用改成异步?

场景三:调试请求就像“黑盒”

AI 给的回复不符合预期,你想看看到底发过去的是什么 JSON、返回的又是什么。但代码里只拿到了最终字符串,中间过程一无所知。怎么把请求和响应日志打印出来?

场景四:ChatClientChatModel 到底啥关系?

你在文档里看到了 ChatModel 这个接口,也看到了 ChatClient,它们是什么关系?我该直接注入 ChatModel 用吗?

这四个场景,今天一次解决。

二、核心概念快览

在写代码之前,先快速理清几个概念。

2.1 ChatClient:面向用户的高层 API

ChatClient 是 Spring AI 为你提供的最高层级的对话工具。它使用流式(Fluent)API,把“构建请求、发送请求、处理响应”封装成一条链。你不需要手动拼 JSON、不需要管 HTTP 连接池,只管调用就好。

可以把它类比为 RestClientWebClient——让你以声明的方式完成网络调用。

2.2 ChatModel:底层的模型抽象

ChatModel 是 Spring AI 的底层接口,直接对应与 AI 模型的通信协议。它只有一个核心方法:

ChatResponse call(Prompt prompt);

ChatClient 内部正是通过 ChatModel 来完成实际的调用。日常开发中我们直接使用 ChatClient 即可,除非你需要对请求和响应做非常细粒度的控制。

关系总结:ChatClient 是 ChatModel 的“装修工”——它帮你把原始的水泥钢筋(Prompt 对象),装成了拎包入住的精装房(Fluent API)。

2.3 同步调用与异步调用

  • 同步调用:代码走到 .call() 时会阻塞当前线程,直到 AI 返回完整结果。适合响应速度较快的场景,或者必须等结果才能继续计算的逻辑。
  • 异步调用:调用后立即返回一个 CompletableFutureFlux,不阻塞主线程。适合响应较慢的场景,或者需要同时调用多个 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 消耗量、结束原因等。想查看消耗时非常有用。
  • chatAsyncCompletableFuture.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 设定专业角色,输出质量直线上升
  • 开启了请求/响应日志,告别黑盒调试
  • 理清了 ChatClientChatModel 的层次关系

下一步预告

今天的异步调用还是用 CompletableFuture 手动包装的,真实场景中更优雅的方式是流式(Streaming)返回——AI 一个字一个字地“蹦”出来,就像 ChatGPT 网页版那样。

下一篇我们就来打造这个打字机效果。我们将学习 SSE 协议、Flux 响应式流,以及如何让前端实时消费流式输出。这是提升用户体验的关键一步。

下一篇见。


本系列博客基于 Spring AI 1.1.6 版本编写。建议在实际开发时查阅 Spring AI 官方文档 获取最新信息。

Logo

AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。

更多推荐