让 AI 记住你上一句:老码拆一套能落地的多轮记忆架构

让 AI 记住你上一句:老码拆一套能落地的多轮记忆架构

记录人:老码
适用项目:Spring Boot + Spring AI + DeepSeek + MySQL 的恋爱咨询应用
相关文档:[[01-项目背景与技术选型]]、[[02-Spring AI接入DeepSeek实践]]、[[04-MySQL持久化与建表实践]]

本篇目录

一、开场先唠两句

做客服系统的老同事都懂一个场景:用户第二句上来就问“我刚才不是跟你说过了吗”。你回头看聊天记录,还真说了,只是系统没记住。

大模型也一样。模型 API 不带记忆,你这一轮不把历史喂过去,它下一轮照样问“请问您遇到什么问题”。

所以这一篇讲两件事:恋爱大师这套业务怎么分层,以及“记住上一句”的代码到底写在哪。别慌,问题不大,我们一点一点拆。

二、先把结论摆桌上

三条,记住就够:

  1. 模型无状态,历史得你自己攒;
  2. 攒历史的活可以交给 MessageChatMemoryAdvisor,你只负责实现“存哪里”;
  3. 表里存全量、喂给模型只看窗口,这两件事千万别混。

剩下的都是实现细节。

三、整体架构:接口、分层、数据落在哪

先看图,再看接口,最后看表。顺序别倒,不然容易一上来就陷进某个类的细节里。
在这里插入图片描述

对外就四个接口,够跑通整个闭环:

方法路径作用
POST/api/love/session建会话,返回 sessionId
GET/api/love/session/{id}查会话信息
GET/api/love/session/{id}/history查完整聊天记录
POST/api/love/chat聊一轮

数据落在两张表里:love_session 记会话和阶段,love_message 按行记消息。建表语句和字段设计放在 [[04-MySQL持久化与建表实践]],这里不重复贴,免得一篇文档塞两件事。

四、记忆是怎么转起来的

这事像复诊。医生不会凭空记得你三个月前说过什么,他看的是病历本。病历写得全,判断就准;只留最近三次,那更早的细节还得靠你再讲一遍。

模型是医生,ChatMemory 是病历本,sessionId 是就诊卡号。卡号一换,等于换了个医生。

Spring AI 把这个动作拆成了清晰的步骤:

在这里插入图片描述

看懂这张图,多轮对话就没有神秘感了:请求前取历史、存用户消息;响应后存模型回复。三件事,一个不多。

五、代码落地:五段关键实现

5.1 装配:把记忆挂到 ChatClient 上

@Bean
public ChatClient loveChatClient(ChatModel chatModel, MySQLChatMemory chatMemory) {
    return ChatClient.builder(chatModel)
            .defaultAdvisors(MessageChatMemoryAdvisor.builder(chatMemory).build())
            .build();
}

模型还是那个模型,只是多挂了一个负责翻病历的助手。

5.2 调用:把 sessionId 传进 Advisor

String reply = loveChatClient.prompt()
        .system(LovePrompts.systemPrompt(effectiveStage))
        .user(message)
        .advisors(spec -> spec.param(ChatMemory.CONVERSATION_ID, resolvedSessionId))
        .call()
        .content();

ChatMemory.CONVERSATION_ID 决定这次用哪本病历。前端不传 sessionId,服务端自动建一个新的;传了就接着往下聊。

5.3 存:只追加,不覆盖

@Override
public void add(String conversationId, List<Message> messages) {
    for (Message message : messages) {
        if (message == null) {
            continue;
        }
        jdbcTemplate.update(
                "INSERT INTO love_message (session_uuid, role, content) VALUES (?, ?, ?)",
                conversationId,
                messageRole(message),
                messageText(message));
    }
}

一条消息一行,role 区分用户还是助手。历史全量留在表里,查历史、做审计、以后换摘要策略,都有原料。

5.4 取:只给模型看最近 20 条

@Override
public List<Message> get(String conversationId) {
    List<Map<String, Object>> rows = jdbcTemplate.queryForList(
            "SELECT role, content FROM love_message WHERE session_uuid = ? ORDER BY id DESC LIMIT ?",
            conversationId,
            MAX_MESSAGES);
    Collections.reverse(rows);

    List<Message> messages = new ArrayList<>(rows.size());
    for (Map<String, Object> row : rows) {
        messages.add(toMessage(
                String.valueOf(row.get("role")),
                String.valueOf(row.get("content"))));
    }
    return messages;
}

先倒序取 20 条,再翻回正序。数据库里躺着一万条也没关系,模型只吃这 20 条。

5.5 还原角色:别让模型分不清谁说的话

private Message toMessage(String role, String content) {
    return switch (role.toLowerCase(Locale.ROOT)) {
        case "assistant" -> new AssistantMessage(content);
        case "system" -> new SystemMessage(content);
        default -> new UserMessage(content);
    };
}

这段看着不起眼,少了它,历史消息全变成同一种角色,模型读起来就是一笔糊涂账。

六、为什么不直接用官方的记忆窗口

Spring AI 自带 MessageWindowChatMemoryChatMemoryRepository,看着现成,但有个语义要留意:Repository 的 saveAll覆盖整个会话,而记忆窗口会把消息列表裁剪到固定条数再存。

换句话说,直接拿它落库,聊天记录一超过窗口大小,前面的就被覆盖没了。用户想翻旧账,你只能摊手。

项目的做法是把两件事分开:数据库存全量,喂给模型看窗口。责任清楚,谁也不委屈。

七、实测结果

跑过两轮连续对话:

  1. 第一轮:用户说自己单身,不知道怎么自然认识喜欢的女生;
  2. 第二轮:用户要求“结合我上面说的情况,给一个本周可执行的小计划”;
  3. 模型回答里明确带上了第一轮的背景,没有重新问一遍;
  4. 历史接口返回 4 条消息:user、assistant 各两条。

链路是通的:写进库、下次读出来、拼进 Prompt。三步缺一步,模型就开始装傻。

八、坑点提醒

后果处理
窗口拍脑袋定太大烧钱,太小失忆演示用 20 条,正式按上下文和预算调
sessionId 丢失每次都是新会话前端必须保存并回传
把全量历史塞给模型Token 飙升表存全量,Prompt 只给窗口
阶段靠模型猜答非所问让用户选单身、恋爱、已婚
人设不写边界遇到危机场景乱答提示词里写明建议寻求线下专业帮助
升级 Spring AI 不回归接口签名变化导致启动失败升版本先看变更说明,再跑一遍对话
消息表只增不删数据量越滚越大提前规划归档或冷热分离

九、后面还能加什么

流式回复、会话列表与归档、长会话摘要、RAG 知识库、敏感内容前置识别。地基已经打好了,剩下是拧螺丝的活。

十、老码的总结

给模型加记忆,说穿了就是配一本病历本:记得全、拿得准、别一次全塞给医生。这三条做到,多轮对话就稳。

先看日志,别慌,问题不大。要是既要求记住全部历史,又要求成本不涨——这个需求得加钱。

参考

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

yxlalm

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值