小崔敲下
mvn spring-boot:run,浏览器打开localhost:8080/chat?message=你好,屏幕上出现了 AI 的回复。"成了!"他兴奋地喊道。
第一个 AI 对话应用
定义与作用
本节带你从零构建第一个 Spring AI 对话应用,展示两种调用方式:
- 非流式
call():等待完整回复后一次性返回 - 流式
stream():逐 Token 实时推送,适合聊天场景
核心原理:调用链路
图释:用户请求经过 Controller → ChatClient → ChatModel → LLM 四层传递。ChatClient 负责 Fluent API 和 Advisor 链,ChatModel 负责与 LLM 通信。
完整示例一:非流式调用
场景说明
飞翔科技学生管理系统的第一个 AI 功能——课程智能问答。学生输入问题,AI 给出回答。小崔先做一个最简单的非流式版本。
依赖引入与配置
pom.xml 已按第 1 章配置,application.yml:
spring:
ai:
ollama:
base-url: http://localhost:11434
chat:
options:
model: llama3
temperature: 0.7
关键代码
package com.feixiang.student;
import org.springframework.ai.chat.client.ChatClient;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.annotation.Bean;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
@SpringBootApplication
public class StudentAiApplication {
public static void main(String[] args) {
SpringApplication.run(StudentAiApplication.class, args);
}
// 创建 ChatClient Bean
@Bean
public ChatClient chatClient(ChatClient.Builder builder) {
return builder
.defaultSystem("你是飞翔科技大学的学生助手,回答简洁、专业。")
.build();
}
}
@RestController
class ChatController {
private final ChatClient chatClient;
public ChatController(ChatClient chatClient) {
this.chatClient = chatClient;
}
@GetMapping("/chat")
public String chat(@RequestParam(defaultValue = "你好") String message) {
return chatClient.prompt()
.user(message)
.call()
.content(); // 直接获取文本内容
}
}
运行结果及分析
# 启动应用
mvn spring-boot:run
# 终端调用
curl "http://localhost:8080/chat?message=计算机科学专业有哪些必修课"
AI 回复示例:
飞翔科技大学计算机科学专业的必修课通常包括:数据结构与算法、操作系统、
计算机网络、数据库原理、编译原理、计算机组成原理等核心课程。
具体课程安排请参考各年级培养方案。
完整示例二:流式调用(SSE)
场景说明
孔蓝(产品经理)提需求:"AI 回答太慢了,学生等得不耐烦。要像 ChatGPT 那样一个字一个字往外蹦!"
关键代码
@RestController
class StreamChatController {
private final ChatClient chatClient;
public StreamChatController(ChatClient chatClient) {
this.chatClient = chatClient;
}
// 流式调用:返回 Server-Sent Events
@GetMapping(value = "/chat/stream", produces = "text/event-stream")
public Flux<String> streamChat(@RequestParam(defaultValue = "你好") String message) {
return chatClient.prompt()
.user(message)
.stream() // 流式调用
.content(); // 逐 Token 推送
}
}
运行结果及分析
curl -N "http://localhost:8080/chat/stream?message=介绍飞翔科技大学"
SSE 输出(逐 Token):
data:飞翔
data:科技
data:大学
data:成立于
data:2018
data:年
data:...
流式 vs 非流式对比:
| 维度 | call() | stream() |
|---|---|---|
| 返回类型 | String | Flux<String> |
| 响应方式 | 一次性返回全文 | 逐 Token 推送 |
| 首字延迟 | 高(等全文生成完) | 低(首个 Token 即返回) |
| 适用场景 | API 调用、批量处理 | 聊天界面、实时交互 |
| Content-Type | 任意 | text/event-stream |
操作前后对比:直接 curl OpenAI API vs Spring AI ChatClient
操作前:直接调用 OpenAI API
# 手动构造 JSON、手动管理 HTTP 连接
curl https://api.openai.com/v1/chat/completions \
-H "Content-Type: application/json" \
-H "Authorization: Bearer $OPENAI_API_KEY" \
-d '{
"model": "gpt-4o",
"messages": [
{"role": "system", "content": "你是飞翔科技大学的学生助手"},
{"role": "user", "content": "计算机科学专业有哪些必修课"}
]
}'
返回原始 JSON:
{
"id": "chatcmpl-xxx",
"object": "chat.completion",
"choices": [{
"message": {
"role": "assistant",
"content": "飞翔科技大学计算机科学专业的必修课包括..."
}
}],
"usage": {
"prompt_tokens": 45,
"completion_tokens": 120,
"total_tokens": 165
}
}
操作后:Spring AI ChatClient
// 一行 Fluent API,无需手动管理 JSON 和 HTTP
String answer = chatClient.prompt()
.user("计算机科学专业有哪些必修课")
.call()
.content();
改进总结:
| 维度 | 直接 curl API | Spring AI ChatClient |
|---|---|---|
| HTTP 管理 | 手动构造 URL 和 Header | 自动配置 |
| JSON 拼装 | 手动拼接 JSON 字符串 | 自动序列化 |
| 响应解析 | 从 JSON 中提取 choices[0].message.content | .content() 一行搞定 |
| 模型切换 | 改 URL 和参数 | 改 YAML 配置 |
| 异常处理 | 手动判断 HTTP 状态码 | 自动抛出 Spring 异常体系 |
| 流式调用 | 手动解析 SSE | .stream().content() 返回 Flux |
易错场景与面试考点
易错场景一:忘记调用 .call() 或 .content() 导致空指针
// ❌ 错误:prompt().user() 返回的是 ChatClientRequestSpec,不是 String
@GetMapping("/chat")
public String chat(@RequestParam String message) {
return chatClient.prompt()
.user(message)
.toString(); // toString() 返回对象描述,不是 AI 回复!
}
问题分析
Fluent API 每个阶段返回不同类型:
.prompt()→ChatClientRequestSpec.user()→ChatClientRequestSpec(继续配置).call()→ChatClientCallResponseSpec.content()→String
// ✅ 正确:必须完整走完链
return chatClient.prompt()
.user(message)
.call()
.content();
易错场景二:流式调用未设置 produces
// ❌ 错误:流式调用但未设置 Content-Type
@GetMapping("/chat/stream")
public Flux<String> streamChat(@RequestParam String message) {
return chatClient.prompt().user(message).stream().content();
}
问题分析
浏览器和前端框架依赖 Content-Type: text/event-stream 来识别 SSE 流。不设置 produces 时默认返回 application/json,前端无法正确解析流式数据。
// ✅ 正确:显式设置 produces
@GetMapping(value = "/chat/stream", produces = "text/event-stream")
public Flux<String> streamChat(@RequestParam String message) {
return chatClient.prompt().user(message).stream().content();
}
面试高频题
Q1:ChatClient 的 call() 和 stream() 有什么区别?
call()阻塞等待完整回复后返回ChatClientCallResponseSpec,stream()返回Flux<ChatResponse>逐 Token 推送。前者适合 API 调用场景,后者适合聊天 UI 实时展示。
Q2:Spring AI 相比直接调用 OpenAI API 的优势是什么?
①统一抽象:切换模型只需改 YAML,代码不变;②Fluent API:链式调用简洁直观;③自动配置:无需手动管理 HTTP 连接;④Advisor 链:可插入日志、记忆、RAG 等拦截逻辑;⑤Spring 生态集成:天然支持依赖注入、事务管理、可观察性。
本章小结
- 最小应用只需 3 步:创建 ChatClient Bean → Controller 注入 →
prompt().user().call().content() - 非流式
call()返回完整文本,流式stream()返回Flux<String> - 流式调用必须设置
produces = "text/event-stream" - Spring AI 最直接的价值:用 1 行 Fluent API 替代 20 行手动 HTTP + JSON 拼装