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

相关推荐
甲维斯9 小时前
绝了!Codex找苹果客服解决Claude订阅问题!
人工智能
斗象科技9 小时前
斗象入选上海市“AI+制造“服务商能力图谱·AI安全服务商
人工智能·安全·制造
明志数科9 小时前
工业采集数据的计量口径分层与可复算质检设计
人工智能·机器人
小七-七牛开发者9 小时前
Coding Agent 推理性能优化:Decode、KV Cache 与多 Session 调度
ai·agent·kvcache·ai coding·speculativedecoding
qq_25294131689 小时前
日本道路病害检测数据集 | 道路病害 裂缝检测 坑洼识别 市政巡检9287期
人工智能·深度学习·yolo·目标检测·计算机视觉·道路检测
yumengxt9 小时前
「屿梦星途GEO」系列 · 每日AI资讯解读
人工智能
Qianxueban9 小时前
如果卷积核大小固定,步长固定,那经过i次卷积,它一个像素的感受野是多大?
人工智能·深度学习·计算机视觉
拆房老料9 小时前
DeepSeek Super-agent 架构解析:从单任务 Agent 到持续运行的任务系统
人工智能·设计模式·系统架构
民乐团扒谱机9 小时前
【一文吃透】机器学习 Embedding 降维打击:从底层空间几何原理到 RAG 向量检索工程实战避坑
人工智能·机器学习·embedding
代码山河10 小时前
基本类型转换:自动类型转换与强制类型转换
java·学习·架构·教程·面向对象·项目