准备工作:
创建api-key,以deepSeek为例:DeepSeek开放平台
一、创建项目
Spring Boot 3.3+,JDK 21,添加依赖:
xml
<!-- Spring AI -->
<dependency>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-openai-spring-boot-starter</artifactId>
</dependency>
application.yml:
yaml
spring:
application:
name: java-tech-assistant
ai:
openai:
api-key: ${DEEPSEEK_API_KEY:换成自己的key}
base-url: https://api.deepseek.com
chat:
options:
model: deepseek-chat
temperature: 0.7
server:
port: 8080
servlet:
# 否则流式输出乱码
encoding:
charset: UTF-8
enabled: true
force: true
二、最简单的聊天接口
java
@RestController
public class ChatController {
private final ChatClient chatClient;
public ChatController(ChatClient.Builder builder) {
this.chatClient = builder
.defaultSystem("你是一个专业的Java技术助手,回答简洁准确。")
.build();
}
/**
* 聊天接口 - 完整输出
* GET /chat?message=你好
*/
@GetMapping("/chat")
public String chat(@RequestParam String message) {
return chatClient.prompt()
.user(message)
.call().content();
}
}
接口测试 测试:

AI 返回一连串 Java 八股文------你的第一个 AI 应用就跑通了。
三、加上流式输出(打字机效果)
上面那个是一口气吐完整段回复,没有 ChatGPT 那种逐字输出的效果。加流式输出:
java
/**
* 流式聊天接口 - 逐字输出
* GET /chat/stream?message=你好
*/
@GetMapping(value = "/chat/stream", produces = "text/event-stream;charset=UTF-8")
public Flux<String> streamChat(@RequestParam String message) {
return chatClient.prompt()
/**
* 创建一个 Prompt 对象,并设置用户输入的 message
*/
.user(message)
.stream()
.content();
}

四、加上历史对话记忆
聊到第三句 AI 忘记了第一句?加上记忆:
java
public ChatController(ChatClient.Builder builder, ChatMemory chatMemory) {
this.chatClient = builder
.defaultSystem("你是一个专业的Java技术助手,回答简洁准确。")
.defaultAdvisors(new MessageChatMemoryAdvisor(chatMemory))
.build();
}
@GetMapping(value = "/chat/stream", produces = "text/event-stream;charset=UTF-8")
public Flux<String> streamChat(
@RequestParam String message,
@RequestParam(defaultValue = "default") String sessionId) {
return chatClient.prompt()
.user(message)
// 历史对话
.advisors(a -> a.param("chat_memory_conversation_id", sessionId))
.stream()
.content();
}
核心机制是 Spring AI 的 MessageChatMemoryAdvisor:
数据流:
- → Controller
- → MessageChatMemoryAdvisor.aroundCall/aroundStream
- 从 InMemoryChatMemory 查 sessionId 对应的历史消息
- 将历史消息注入到 Prompt 的 messages 列表中
- 调用 LLM
- LLM 回复后,将本轮 User 消息 + Assistant 回复存入 InMemoryChatMemory
- → 返回结果
一句话总结:
Advisor 拦截请求 → 从 Map<sessionId, 历史消息> 取出前文拼进 prompt → LLM 回复后把本轮问答追加回去。纯内存,默认保留最近 100 条。
五、加上页面测试

六. ChatClient API 速查表
1. 构建输入 --- .prompt()
| 方法 | 作用 |
|---|---|
.prompt() |
创建请求入口,返回 PromptRequestSpec |
.prompt(String text) |
快捷方式,等价于 .prompt().user(text) |
2. 组装消息 --- PromptRequestSpec
| 方法 | 说明 |
|---|---|
.user(String text) |
用户消息 |
.system(String text) |
系统角色设定 |
.messages(Message... msgs) |
自定义消息列表 |
.messages(Consumer) |
编程式追加消息 |
.options(ChatOptions ops) |
模型参数(temperature、topP 等) |
.advisors(Advisor... ads) |
请求拦截器链(RAG、日志、对话记忆) |
.advisors(Consumer) |
编程式组装 advisor 链 |
.toolContext(Map) |
传上下文给 advisor |
.tools(Object... beans) |
注册 Function Calling 工具 |
3. 发送 & 返回形式 --- PromptRequestSpec
| 方法 | 返回类型 | 用途 |
|---|---|---|
.call() |
ChatClientCallResponseSpec | 同步、一次性返回完整结果 |
.stream() |
ChatClientStreamResponseSpec | 流式、逐 token 输出 |
.entity(Class<T>) |
UserInputAdvisor 链 | 结构化提取(提前终止,不调 LLM) |
4. 取结果
call() 的结果:
| 方法 | 返回 |
|---|---|
.chatResponse() |
ChatResponse(含 metadata、token 用量) |
.content() |
String(纯文本,最常用) |
.entity(Class<T>) |
反序列化到实体 |
.entities(ParameterizedTypeRef<T>) |
反序列化到泛型集合 |
stream() 的结果:
| 方法 | 返回 |
|---|---|
.chatResponse() |
Flux |
.content() |
Flux(逐字推送文本) |
.entity(Class<T>) |
Flux |