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

相关推荐
汤姆yu1 小时前
Anthropic Claude Fable 5 深度解析
人工智能·ai·大模型·智能体·视频模型
JustNow_Man1 小时前
psmux快捷键
人工智能·python
神奇的小猴程序员2 小时前
提升 AI 与开发效率!两款实用 Skill 开源工具 FunctionCool-Skill & StyleCool-Skill 深度体验
人工智能·开源·s
zhayujie2 小时前
让 Agent 在对话中成长:自进化机制的五层实现
ai·大模型·agent·harness
哈哈,柳暗花明2 小时前
人工智能专业术语详解(L)
人工智能·专业术语
莱歌数字2 小时前
散热测试使用恒温热源和功率热源的应用场景分析
人工智能·科技·制造·散热·液冷散热
码农小白AI2 小时前
AI报告审核通审Agent版搭载IACheck:锅炉压力容器电梯起重设备安装监检核查
人工智能
手写码匠2 小时前
手写 GraphRAG:从零实现图增强检索增强生成系统
人工智能·深度学习·算法·aigc
沪漂阿龙2 小时前
Chat Model:LangChain 如何统一调用不同大模型?
人工智能·langchain