Skip to content

第一个 ChatClient:把模型调用变成 Spring Bean ​

1. 本节产出 ​

一个能独立运行的 Spring Boot 服务,提供 POST /api/chat,传入一句话返回模型回答,并且应用启动后可通过健康接口确认模型已就绪。

2. 前置依赖 ​

3. 为什么需要 ChatClient 这层抽象 ​

先看不用它会怎样。裸用 HTTP 客户端调模型的写法:

java
// 反面教材:能跑,但很快就会失控
String body = """
    {"model":"%s","messages":[{"role":"user","content":"%s"}]}
    """.formatted(model, question);
HttpResponse<String> resp = httpClient.send(request, BodyHandlers.ofString());
JsonNode root = objectMapper.readTree(resp.body());
String answer = root.at("/choices/0/message/content").asText();

四个立刻会出现的问题:

问题后果
手写 JSON 拼接用户问题里有引号或换行就崩;还顺带白送一个注入入口
错误处理缺失401/429/500 全都走到成功分支,得到一个 null
厂商强耦合换个模型要改代码、改包路径、改解析逻辑
无法统一治理超时、重试、日志要在每个调用点各写一遍

这四件事在 Spring 生态里早就有标准解法了——回想你在用 JDBC 还是 JdbcTemplate,在用裸 RestTemplate 还是 Feign。ChatClient 就是 Spring AI 版本的 JdbcTemplate:同样的套路,把「怎么调」和「调什么」分开。

4. 核心原理:两层 API 的分工 ​

┌─────────────────────────────────────────┐
│  ChatClient   链式 API,日常用它          │
│  .prompt().user("...").call().content() │
├─────────────────────────────────────────┤
│  ChatModel    底层接口,直接收发 OpenAI    │
│               协议请求,需要精细控制时才用  │
├─────────────────────────────────────────┤
│  HTTP / JSON / 厂商 API                  │
└─────────────────────────────────────────┘

判断标准:90% 的场景用 ChatClient;只有需要直接操作 Prompt/ChatResponse 原始对象(比如自定义元数据传递)时才下沉到 ChatModel。

ChatClient 的三件套配置

配置项作用类比
defaultSystem(...)全局角色设定,所有请求生效@ControllerAdvice
defaultAdvisors(...)请求前后拦截增强Filter 链
defaultOptions(...)模型、温度、超时等参数默认值连接池默认参数

这三样配好之后注入一次即可,业务代码里只管 .prompt().user(text).call()。

5. 代码走查 ​

5.1 依赖 ​

xml
<!-- pom.xml -->
<dependency>
  <groupId>org.springframework.ai</groupId>
  <artifactId>spring-ai-starter-model-openai</artifactId>
</dependency>

版本号由 spring-ai-bom 统一管理,这里不写。选 OpenAI starter 的原因不是「用 OpenAI 的模型」,而是绝大多数国内厂商都提供 OpenAI 兼容端点——换 base-url 即可对接。

5.2 配置 ​

yaml
# application.yml
spring:
  ai:
    openai:
      api-key: ${API_KEY}          # 从环境变量注入,不硬编码
      base-url: ${API_BASE_URL}    # 国内厂商的 OpenAI 兼容端点
      chat:
        options:
          model: ${API_MODEL}
          temperature: 0.7

5.3 定义 Bean ​

java
// src/main/java/com/example/aibasics/config/ChatConfig.java
@Configuration
public class ChatConfig {

    @Bean
    public ChatClient chatClient(ChatClient.Builder builder) {
        return builder
            .defaultSystem("你是一个 Java 技术助理,回答简洁,给出代码示例时带简短说明。")
            .defaultOptions(OpenAiChatOptions.builder()
                .temperature(0.7)
                .build())
            .build();
    }
}

关键点:ChatClient.Builder 由 starter 自动提供并注入了我们配好的 ChatModel。不要自己 new ChatClient——那样会丢掉所有自动配置。

5.4 控制器 ​

java
// src/main/java/com/example/aibasics/controller/ChatController.java
@RestController
@RequestMapping("/api")
public class ChatController {

    private final ChatClient chatClient;

    public ChatController(ChatClient chatClient) {   // 构造器注入
        this.chatClient = chatClient;
    }

    @PostMapping("/chat")
    public String chat(@RequestBody String question) {
        return chatClient.prompt()
                .user(question)
                .call()
                .content();
    }
}

5.5 一个最小测试 ​

java
@SpringBootTest
class FirstChatClientTest {

    @Autowired ChatClient chatClient;

    @Test
    void shouldAnswerQuestion() {
        String answer = chatClient.prompt()
                .user("用一句话说明什么是依赖注入")
                .call()
                .content();
        assertThat(answer).isNotBlank();
    }
}

提示:这个测试会真的花掉几分钱。如果不想每次跑测试都烧钱,用 ChatModel 的 mock 实现替换,后面 01-11 测试章节会专门讲。

6. 跑起来 ​

bash
export API_KEY=sk-xxxx
export API_BASE_URL=https://<厂商域名>
export API_MODEL=<模型名>
mvn spring-boot:run

另开终端:

bash
curl -X POST http://localhost:8080/api/chat \
  -H "Content-Type: text/plain" \
  -d "Spring Boot 的自动配置是怎么生效的?"

期望输出:一句自然语言回答。同时观察应用日志,正常应该看到请求发出与响应返回记录,没有鉴权错误。

如果没起来,按顺序排查:

现象排查
启动报 Could not resolve placeholder 'API_KEY'环境变量没导出,或被 IDE 的启动配置覆盖了
401Key 无效或带了多余空格
404base-url 拼错,重点查有没有重复的 /v1
连接超时本机代理拦截,或厂商域名需要走公司网络策略

7. 生产避坑 ​

  1. 不要在 Controller 方法里 new ChatClient。它每次构建都会重新初始化 Advisor 链和默认参数,看起来能跑,实则浪费且丢配置。正确做法是作为 Bean 注入——这也符合你已有的 Spring 使用直觉。
  2. spring.ai.openai.* 配置项写错不会报错。拼错的属性名会被 Spring 静默忽略,然后你得到一个没有 Key 的客户端,在第一次调用时才炸。写完配置立刻跑一次调用验证,不要等集成时才发现。
  3. 默认超时可能不适合你的场景。模型响应动辄十几秒,网关或前端往往先超时。即便本节不做重试治理,也应该在文档里记下当前超时值——下一课时会正式引入超时与重试。

8. 延伸与锚点 ​

  • 思考题:如果公司要求同时接两家模型厂商做灾备,你现在这个结构需要改几行?(答案会在 04-02 模型网关给出)
  • 代码锚点:git checkout ch01-02-first-chatclient
  • 下一课时:01-03 国内多模型统一适配层(待编写)
  • 对应课件:L01-02 第一个 ChatClient