Spring AI 实战:聊天、提示词、记忆三件套

从"说一句话"开始

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 的好处。

相关推荐
sel_92 分钟前
论文阅读笔记:Learning Multimodal Embeddings with Evidence-Aligned Readout
人工智能·深度学习
霍格沃兹测试学院-小舟畅学2 分钟前
Playwright MCP:AI Agent 驱动 Web 自动化测试的架构与执行机制
前端·人工智能·架构
紫洋葱hh3 分钟前
Go 入门基础:类型、变量与运算符
开发语言·后端·ai·golang
随性而行3607 分钟前
企业微信API接口开发实战:如何实现消息回调接收与业务事件处理?
java·前端·python·微信·机器人·企业微信
thinking_talk8 分钟前
开源AI记忆框架选型与对比
人工智能·开源·ai记忆
美味蛋炒饭.8 分钟前
java八股面试(3)
java·开发语言·面试
达之云*驭影9 分钟前
用AI给视频一键更换文案、字幕与配音,需要哪些技术?
人工智能·音视频
IT研究室11 分钟前
最新计算机毕业设计选题推荐-基于spring boot的美食配送平台-网站-文档指导-Java-springboot
java·spring boot·课程设计
打工仔折腾 AI12 分钟前
Qt网络编程实战:UDP、TCP到HTTP的落地经验与选型
网络·人工智能·后端·python·qt·udp·ai agent 实战
打工仔折腾 AI13 分钟前
SQL 编辑器不是高级记事本:KStudio 与 KSQL Developer 的建表与增删改查手法
数据库·人工智能·后端·python·sql·编辑器·ai agent 实战