从"说一句话"开始
Spring AI 里和大模型对话的最小单元叫 ChatClient。上一篇文章讲过,它屏蔽了底层模型差异,你只跟它打交道:
java
@RestController
public class ChatController {
private final ChatClient chatClient;
public ChatController(ChatClient.Builder builder) {
this.chatClient = builder.build();
}
@PostMapping("/chat")
public Map<String, String> chat(@RequestBody ChatRequest request) {
String reply = chatClient.prompt()
.user(request.getMessage())
.call()
.content();
return Map.of("reply", reply);
}
}
发个 POST:
json
{ "message": "用一句话解释什么是 Spring AI" }
返回:
json
{ "reply": "Spring AI 是 Spring 生态中统一接入大语言模型能力的框架。" }
到这你已经能跟大模型对话了。但光这样不够------大模型没有"人设",也没有记忆力。
提示词:给模型一个人设
没有提示词的大模型像个百科全书机器人,什么问题都能答,但毫无性格。提示词(Prompt)就是给模型划一个"角色框"------你是什么身份、什么语气、什么不该说。
Spring AI 提供了 PromptTemplate 来管理提示词,支持占位符替换:
java
@Service
public class AssistantService {
private final ChatClient chatClient;
// 提示词模板,用 Resource 从外部文件加载
@Value("classpath:/prompts/assistant.st")
private Resource promptResource;
public AssistantService(ChatClient.Builder builder) {
this.chatClient = builder.build();
}
public String chat(String userName, String message) {
return chatClient.prompt()
.system(spec -> spec
.text(promptResource)
.param("name", userName)
)
.user(message)
.call()
.content();
}
}
prompts/assistant.st 文件内容(StringTemplate 语法):
你是一位名叫小海的技术助手,性格温和、解释耐心。
当前对话的用户叫 {name},请用对方的名字称呼。
回答时遵循以下规则:
1. 涉及代码的问题,给出完整可运行的示例
2. 不猜测用户的技术水平,用对方能听懂的方式解释
3. 如果遇到不确定的问题,诚实说明而不是编造答案
提示词单独放文件而不是硬编码在代码里,好处很明显:改人设不用重新编译部署。
记忆:让模型"记住"你们聊过什么
大模型本身是无状态的。你让它"继续刚才的话题",它一脸懵,因为它不记得上一轮说了什么。
解决办法是把历史对话作为上下文每次一起发给模型。Spring AI 用 ChatMemory 接口来管理这件事:
java
@Configuration
public class ChatConfig {
@Bean
public ChatMemory chatMemory() {
// InMemory 存内存,生产环境可以换成 Redis/JDBC
return new InMemoryChatMemory();
}
}
然后在对话时注入历史消息:
java
@Service
public class MemoryChatService {
private final ChatClient chatClient;
private final ChatMemory chatMemory;
public MemoryChatService(ChatClient.Builder builder, ChatMemory chatMemory) {
this.chatClient = builder.build();
this.chatMemory = chatMemory;
}
public String chat(String conversationId, String userMessage) {
// MessageChatMemoryAdvisor 自动管理消息存取,无需手动 add
return chatClient.prompt()
.advisors(new MessageChatMemoryAdvisor(chatMemory, conversationId, 20))
.user(userMessage)
.call()
.content();
}
}
关键角色是 MessageChatMemoryAdvisor。它会在每次请求前自动从 ChatMemory 拉这个会话的历史消息,拼到 prompt 里一起发给模型。
效果好到什么程度?看这段多轮对话:
用户:我叫张三,我是 Java 后端开发
助手:好的张三,记下了。有什么 Java 相关的问题随时问我。
用户:今天工作中遇到一个 NullPointerException,排查了很久
助手:张三,NPE 确实是 Java 开发的老朋友了。能说说具体是哪个对象为空吗?
用户:就是上次那个订单查询的接口
助手:明白了,之前你提到的订单查询接口。建议从 Service 层返回的 Optional 开始排查......
第三轮"上次那个"------模型知道你在说什么,因为它看到了前两轮的对话上下文。这就是记忆的价值。
一个注意点:记忆不是无限的
大模型有上下文窗口限制(比如 128K tokens),你把 10 轮、20 轮、100 轮历史全塞进去会爆。MessageChatMemoryAdvisor 默认保留最近 20 条消息,可以根据场景缩小:
java
new MessageChatMemoryAdvisor(chatMemory, "default", 10); // 缩小到最近 10 条
更高级的做法是"摘要记忆"------当对话太长时,用模型自己对历史做摘要压缩,然后只存摘要。Spring AI 目前对这块的支持还在完善中,但基本思路是公开的了。
另外注意,InMemoryChatMemory 是在内存中无限累积的,生产环境长时间运行会撑爆 JVM。底层存储换成 Redis 或 JDBC 实现,再配合定时任务清理过期会话即可。
三者拼起来:一个完整的聊天服务
把提示词、记忆、对话串起来,就是一个能用的 AI 聊天系统:
java
@RestController
@RequestMapping("/api/chat")
public class SmartChatController {
private final ChatClient chatClient;
private final ChatMemory chatMemory;
private final Resource systemPrompt;
public SmartChatController(
ChatClient.Builder builder,
ChatMemory chatMemory,
@Value("classpath:/prompts/assistant.st") Resource systemPrompt) {
this.chatClient = builder.build();
this.chatMemory = chatMemory;
this.systemPrompt = systemPrompt;
}
@PostMapping("/{sessionId}")
public ChatResponse chat(
@PathVariable String sessionId,
@RequestBody ChatRequest request) {
String reply = chatClient.prompt()
.system(s -> s.text(systemPrompt))
.advisors(new MessageChatMemoryAdvisor(chatMemory, sessionId, 20))
.user(request.getMessage())
.call()
.content();
return new ChatResponse(reply, sessionId);
}
}
sessionId 区分不同用户/会话,每人一段独立的记忆空间。
收尾
聊天、提示词、记忆------这三个是构建 AI 对话系统的骨架。Spring AI 把它们包装得足够简单,你不用关心 token 怎么算、上下文怎么拼接、对话怎么管理。你只管定义人设、写业务逻辑,剩下的框架兜底。
下一步可以玩的方向:把记忆换成 Redis 实现(多实例共享)、接入 Tool Calling 让 AI 能查数据库、或者加入 RAG 让 AI 读你的文档回答。每个方向都不需要推翻现有代码------这就是 Portable API 的好处。