Skip to content

L01-06 多轮对话与 Memory 持久化 ​

全局中心内容:策略层和存储层分开——存多少由 ChatMemory 定,存在哪由 Repository 定。 全局讲解主线:手写历史会怎样 → 三层结构 → 做出持久化 → 重启证明不失忆。


P1 · 产出页 ​

中心内容:重启应用,用户刚才说的话还在。

  • 页面内容

    • 同 conversationId 下能引用前文
    • 重启后记忆仍在(JDBC)
    • 可裁剪,不会撑爆上下文
  • 讲解技巧

    • 先抛一个反常识的事实:「大部分人做的 AI 应用,重启一次就失忆」。让学员自查有没有这个问题。
    • 结尾预告验收方式:重启后问「我叫什么」——这是本节唯一的硬指标。
  • 时长:20s


P2 · 痛点页:手写历史的三个坑 ​

中心内容:重启即失忆,是生产事故不是小毛病。

  • 页面内容

    • 不知道截多少 → 太短丢上下文,太长爆窗口
    • 无法区分角色 → 用户说的和模型说的混在一起
    • 重启即失忆 → 滚动更新后全部对话失忆
  • 讲解技巧

    • 第三个坑要讲透后果:「用户不会认为这是部署问题,只会认为你的产品很蠢」。这句话很有冲击力。
    • 类比迁移:这跟把 HttpSession 放内存里,一发版用户全掉线是同一个错误。
  • 时长:2min 30s


P3 · 原理图:三层结构 ​

中心内容:存多少(策略)和存在哪(存储)是两件事。

  • 页面内容

    • ChatMemory:MessageWindow / Token 预算
    • ChatMemoryRepository:内存 / JDBC / Redis
    • 换存储不改策略
  • 讲解技巧

    • 黑板留这张图:三层框,中间画一条横线分开「策略」和「存储」。
    • 类比:「这就是 Spring 里事务策略和数据源分离的思路」。对 Java 工程师,这个类比一次讲透。
  • 时长:3min


P4 · 风险页:conversationId ​

中心内容:不传 conversationId,所有用户共享一份记忆。

  • 页面内容

    • 全局唯一且不可预测(UUID,不用自增 ID)
    • 必须校验归属,否则是越权
    • 不传会落到默认值 → 所有人混在一起
  • 讲解技巧

    • 这是本课最重要的安全点。用「A 用户看到了 B 用户的对话」这个场景讲,冲击力足够。
    • 点出为什么本地测不出来:本地只有一个用户。这句话能救很多人的线上事故。
  • 时长:3min


P5 · 选型表:三种裁剪策略 ​

中心内容:窗口 + Token 预算双保险。

  • 页面内容

    • 窗口:保留最近 N 条,简单但丢早期信息
    • Token 预算:不超窗口,实现稍复杂
    • 摘要压缩:保留语义但花钱费时
  • 讲解技巧

    • 明确推荐双保险:窗口控条数、预算控总量。给具体数字(20 条 + 4000 Token)。
    • 说明摘要压缩为什么不在这里讲:它需要额外的模型调用,属于 03C 的长程记忆话题。
  • 时长:2min


P6 · 编码:Bean 装配 ​

中心内容:换存储只换 Repository,策略不变。

  • 页面内容

    • JdbcChatMemoryRepository.builder().jdbcTemplate(...)
    • MessageWindowChatMemory.builder().maxMessages(20)
    • initialize-schema: always
  • 讲解技巧

    • 可视化反差:把 InMemory 版和 JDBC 版的代码并排——只有 Repository 那一行不同。
    • 这就是 P3 那张图的代码兑现,讲的时候回头指一下黑板。
  • 时长:5min


P7 · 编码:挂 Advisor 与传参 ​

中心内容:Advisor 顺序错了不报错,但效果不对。

  • 页面内容

    • MessageChatMemoryAdvisor.builder().chatMemory(...)
    • .advisors(a -> a.param(ChatMemory.CONVERSATION_ID, convId))
    • 顺序:记忆在检索之前,在审计之后
  • 讲解技巧

    • 给出记忆口诀:审计在前、记忆在中、成本在后。
    • 强调这类 bug 的可怕之处:不报错、不崩溃,只是效果不对。排查成本极高。
  • 时长:5min


P8 · 编码:会话归属校验 ​

中心内容:越权校验必须写在 Controller 里。

  • 页面内容

    • conversationService.belongsTo(convId, auth.getName())
    • 新建会话返回 UUID
    • 不给 conversationId 默认值
  • 讲解技巧

    • 先翻车:注释掉校验,用两个不同用户的 token 互相访问会话,成功读到对方内容。这个演示极其震撼。
    • 强调「不要给默认值」——给默认值的代码本地永远测不出问题。
  • 时长:4min


P9 · 运行与重启验证 ​

中心内容:重启后还能答出「木鱼」,才算通过。

  • 页面内容

    • 第一轮:我叫木鱼
    • 第二轮:我叫什么 → 木鱼
    • 重启应用 → 再问 → 仍是木鱼
    • 换 convId → 答不知道
  • 讲解技巧

    • 重启验证必须现场做,而且要真的 Ctrl+C 再启动,不能用热部署糊弄。这一刻是本节的高潮。
    • 顺带测窗口:聊满 20 条后问最早的信息,验证裁剪生效。
  • 时长:4min


P10 · 避坑与小结 ​

中心内容:记忆表没 TTL 会无限膨胀,上线前就要处理。

  • 页面内容

    • 不传 conversationId = 全用户共享记忆
    • 记忆里有敏感信息,落库前要脱敏
    • 无 TTL 的记忆表会膨胀到千万行
  • 讲解技巧

    • 第三条强调「必须在上线前做」:等表长大了再清理,代价高十倍。
    • 结尾悬念:「窗口裁剪会丢掉早期的关键约束,比如用户第一句说『全程用英文』——怎么不丢?」引出 03A 状态管理。
  • 时长:2min


讲师备忘 ​

项内容
课前必做准备两个用户的 token 做越权演示;确认 H2/Postgres 建表语句可用
最容易超时处P5 的裁剪策略,容易展开讲摘要压缩——收住,说「03C 讲」
学员最常问「生产到底用 Redis 还是数据库?」答:高频临时数据用 Redis,需要留档再异步落库
现场备用数据库连接失败 → 切 H2 内存版继续讲主线,课后再排查