Appearance
第一个 ChatClient:把模型调用变成 Spring Bean
1. 本节产出
一个能独立运行的 Spring Boot 服务,提供 POST /api/chat,传入一句话返回模型回答,并且应用启动后可通过健康接口确认模型已就绪。
2. 前置依赖
- 00-02 环境准备:已有一把可用的 API Key
- 00-03 版本矩阵:依赖已锁定,无版本冲突
- 01-01 LLM 基础概念与选型(待编写,也可先看 00-05 术语表 理解 Token、Temperature)
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.75.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 的启动配置覆盖了 |
| 401 | Key 无效或带了多余空格 |
| 404 | base-url 拼错,重点查有没有重复的 /v1 |
| 连接超时 | 本机代理拦截,或厂商域名需要走公司网络策略 |
7. 生产避坑
- 不要在 Controller 方法里 new ChatClient。它每次构建都会重新初始化 Advisor 链和默认参数,看起来能跑,实则浪费且丢配置。正确做法是作为 Bean 注入——这也符合你已有的 Spring 使用直觉。
spring.ai.openai.*配置项写错不会报错。拼错的属性名会被 Spring 静默忽略,然后你得到一个没有 Key 的客户端,在第一次调用时才炸。写完配置立刻跑一次调用验证,不要等集成时才发现。- 默认超时可能不适合你的场景。模型响应动辄十几秒,网关或前端往往先超时。即便本节不做重试治理,也应该在文档里记下当前超时值——下一课时会正式引入超时与重试。
8. 延伸与锚点
- 思考题:如果公司要求同时接两家模型厂商做灾备,你现在这个结构需要改几行?(答案会在 04-02 模型网关给出)
- 代码锚点:
git checkout ch01-02-first-chatclient - 下一课时:01-03 国内多模型统一适配层(待编写)
- 对应课件:L01-02 第一个 ChatClient