跳到正文

第 4 章:多轮对话与长程记忆管理

讲清多轮对话 / 长程任务里记忆怎么管(短期 vs 长期、历史选择、裁剪 vs 摘要、外部记忆与遗忘、指代消解),并能跑通一个带记忆 + 指代消解的多轮对话 demo

📊 学习时长:55-70 分钟 🎯 完成后能力:讲清多轮对话 / 长程任务里记忆怎么管(短期 vs 长期、历史选择、裁剪 vs 摘要、外部记忆与遗忘、指代消解),并能跑通一个带记忆 + 指代消解的多轮对话 demo 🔗 关联面试题:5 道(覆盖 5 个角度,见 Part 3)

这一章你会学到什么

  • ✓ 跟着做:用零依赖代码跑通一个「带滑动窗口记忆 + 指代消解」的多轮对话,亲眼看「它」怎么被补全
  • ✓ 看懂短期记忆 vs 长期记忆的分工,以及为什么「把全部历史塞给模型」是错的
  • ✓ 掌握短期记忆的三种历史选择策略,以及「截断 vs 摘要」的取舍
  • ✓ 理解长期记忆:外部记忆库怎么写入 / 检索 / 更新 / 遗忘,以及用户画像怎么做
  • ✓ 学会多轮指代消解:把「那它呢」补全成一句能独立检索的问题
  • ✓ 想清记忆和上一章「演进报告 / ReSum」的关系:一个管「对话记忆」,一个管「单次调研的工作状态」

本章按三段式组织:

段落 给谁看 内容 占比
🚀 Part 1 · 主线实战 零基础,想先跑起来 记忆概念 + 带记忆/指代消解的多轮 demo ~50%
🎯 Part 2 · 面试深度 想讲透、备战面试 历史选择 / 裁剪vs摘要 / 长期记忆 / 用户画像 / 存储 + 私货 ~40%
🏆 Part 3 · 验收串题 检验学到位没 关联面试题 + 自检清单 ~10%

🚀 Part 1 · 主线实战 | ~50% 这部分零依赖,直接跑。你会看到多轮对话里两个绕不开的问题,指代和上下文,怎么被记忆模块解决。

先讲个真实场景(这是本章的「为什么」)

前几章的 Agent,处理的都是「一个问题、一轮调研」。但真实的调研助手是多轮对话的,工程师会接着追问。这时两个问题立刻冒出来:

问题一:指代。 第一轮聊完「Qdrant」,第二轮他问:「那它支持元数据过滤吗?」这句话单独拿去检索,知识库根本不知道「它」是谁,直接抓瞎。

问题二:上下文塞不下。 聊了几十轮之后,如果把所有历史都拼进 prompt,很快就超过模型的上下文窗口;就算没超,塞太多旧的、无关的,反而会干扰当前这一轮的生成(还记得「Lost in the Middle」吗)。

一句话:单轮看检索,多轮看记忆。 记忆模块的核心工作就两件:「选」(哪些历史该带上)和**「补」**(把指代、省略补全成能独立理解的问题)。这一章就讲这两件事。

概念 1:短期记忆 vs 长期记忆

记忆分两层,管的东西完全不同:

  • 短期记忆:当前这次对话的最近几轮,解决「上下文连贯」(知道刚才聊了啥)。随会话结束而清空。
  • 长期记忆:跨会话持久保存的东西,用户是谁、有什么偏好、历史上问过哪些重要问题。下次再来,系统还「记得」他。存在外部库里,按需检索。

为什么不能「把全部历史都塞给模型」?

新手最容易的想法:把所有历史拼进 prompt 不就行了?两个硬约束让你不能这么干:

  • 超窗口:模型一次能读的 token 有上限,对话一长直接超。
  • Lost in the Middle:就算没超,塞太多内容时,模型对中间部分的注意力会显著下降,关键信息淹没在一堆旧对话里,反而答不好。

所以记忆模块必须有策略地选和裁,而不是无脑全带。

概念 2:指代消解

指代消解,就是结合对话历史,把「它 / 这个 / 那个」这类指代,补全成一句能独立理解、能直接拿去检索的问题。

「那它支持元数据过滤吗」→ 补全成 →「那 Qdrant 支持元数据过滤吗」。补全后,这句话不依赖上下文也能检索了。

🛠️ 跟着做:带记忆 + 指代消解的多轮对话

下面这段零依赖代码,实现了一个最朴素的记忆模块:滑动窗口(只保留最近 N 轮,更旧的压成摘要)+ 指代消解(把「它」换成最近提到的实体)。新建 mem_demo.py:

# 零依赖,直接 python mem_demo.py
WINDOW = 2  # 滑动窗口:上下文只保留最近 2 轮,更旧的压成摘要

class Memory:
    def __init__(self):
        self.history = []          # [(用户问, 助手答), ...]
        self.summary = ""          # 挤出窗口的旧轮,压成摘要
        self.last_entity = None     # 最近提到的实体(用于指代消解)

    def add(self, user, bot, entity=None):
        if entity:
            self.last_entity = entity                 # 记住最近的实体
        self.history.append((user, bot))
        while len(self.history) > WINDOW:             # 超窗口 → 最旧一轮挤出、压进摘要
            old_u, _ = self.history.pop(0)
            self.summary += f"用户先前问过「{old_u}」;"

def resolve(query, mem):                              # 指代消解:把"它"换成最近实体
    if "它" in query and mem.last_entity:
        return query.replace("它", mem.last_entity)
    return query

mem = Memory()
mem.add("Qdrant 部署复杂吗?", "Qdrant 单容器即可起,部署很轻。", entity="Qdrant")

q2 = "那它支持元数据过滤吗?"
print("轮2 用户原话:", q2)
print("轮2 指代消解后:", resolve(q2, mem))           # 用最近实体补全「它」
mem.add(q2, "支持,Qdrant 的 payload 过滤很强。")

mem.add("pgvector 呢?", "pgvector 复用 PostgreSQL,中小规模够用。", entity="pgvector")

print("\n=== 第 3 轮后的记忆状态 ===")
print("摘要(被挤出窗口的旧轮):", mem.summary or "(空)")
print("最近", WINDOW, "轮(进上下文):", [u for u, _ in mem.history])
print("最近实体(供指代消解):", mem.last_entity)

运行

python mem_demo.py

你应该看到(确定性输出,你跑出来一模一样):

轮2 用户原话: 那它支持元数据过滤吗?
轮2 指代消解后: 那Qdrant支持元数据过滤吗?

=== 第 3 轮后的记忆状态 ===
摘要(被挤出窗口的旧轮): 用户先前问过「Qdrant 部署复杂吗?」;
最近 2 轮(进上下文): ['那它支持元数据过滤吗?', 'pgvector 呢?']
最近实体(供指代消解): pgvector

看明白了三件事:① 指代被补全了(它 → Qdrant);② 最旧那轮被挤出窗口、压成了摘要;③ 最近实体随对话更新(从 Qdrant 变成 pgvector)。这就是记忆模块「选 + 补」的最小骨架。

🐛 跑不通看这里

  • 「它」没被替换?确认轮 1 的 add 传了 entity="Qdrant",真实系统是用模型 / NER 自动抽实体,这里手动传是为了让你看清机制。
  • 摘要是空的?把 WINDOW 调小(比如 1),或多 add 几轮,旧的才会被挤出。

🛠️ 动手实验:把窗口从 2 改成 1

WINDOW = 2 改成 1,再跑。你会看到更多旧轮被压进摘要,进上下文的只剩最近 1 轮。这就是「滑动窗口」在调节「记多少」,窗口越小越省 token、但越容易忘掉刚才的事。这个取舍,就是短期记忆设计的核心。

📌 说清楚:这是教学最小实现。三处简化:① 指代消解只处理「它」、用规则替换(真实系统用小模型 / LLM 做,能处理「这个」「上一个」等各种指代);② 实体靠手动传入(真实系统自动抽取);③ 摘要是字符串拼接(真实系统用 LLM 概括)。机制是真的,工程化见 Part 2 和后面章节。

🧭 你刚才做的,等于真实系统的什么?

  • 你的滑动窗口 + 摘要 = 短期记忆的历史选择 + 裁剪;
  • 你的 resolve() = 指代消解(本质是一种 Query 重写,信息来源是对话历史);
  • 你的 last_entity = 一个极简的对话状态。真实系统会把这套做得更聪明(模型抽实体、LLM 摘要、相关性召回历史)。

🎯 Part 2 · 面试深度 | ~40% 这部分讲生产级记忆系统:短期记忆的策略、长期记忆的存储与遗忘、用户画像,以及和上一章的关系。

2.1 短期记忆:三种历史选择策略

「带哪些历史进上下文」有三种常见做法,各有取舍:

  • 全保留:全带上。简单,但对话一长就爆,只适合短对话。
  • 滑动窗口:只留最近 N 轮(上面 demo 那种)。省 token、实现简单;缺点是会「忘掉」窗口外但其实相关的历史。
  • 相关性召回:把历史也存进向量库,按「当前问题」检索最相关的几轮带上。最聪明,但要维护历史向量库、有检索开销。

实务里常组合用:最近 N 轮(窗口)+ 从更早历史里相关性召回几轮。

2.2 裁剪 vs 摘要:两种「压缩历史」的方式

历史太长,要么裁掉、要么压缩。区别很关键:

  • 裁剪(truncate):直接丢掉旧轮。零成本,但信息真丢了,丢掉的那轮里若有关键信息,后面就再也找不回。
  • 摘要(summary):用一次 LLM 调用,把旧轮概括成精华再保留。信息基本保住,代价是多一次模型调用(慢、贵)。

这其实就是上一章 ReSum 在对话记忆上的体现,上下文将满时,用摘要换空间。

2.3 长期记忆:跨会话「记得住」

短期记忆随会话结束就没了。要让系统「下次还记得你」,需要长期记忆,一个外部记忆库:

  • 写入:对话里值得长期记的信息(用户偏好、重要结论),抽出来存进外部库(通常是向量库)。
  • 检索:新对话来了,按当前问题从长期记忆里召回相关的几条,带进上下文。
  • 更新:用户改了偏好,要能覆盖旧记忆,而不是越堆越多、自相矛盾。
  • 遗忘:不是所有东西都值得永久记,按时间衰减、重要性、访问频率淘汰旧记忆,否则记忆库越来越脏、检索越来越不准。

业内行话:面试聊长期记忆,「遗忘机制」最容易被忽略、也最能体现深度。很多人只讲「存和取」,但真实系统里「怎么忘」和「怎么记」一样重要,没有遗忘,记忆库会被陈旧、矛盾的信息塞满,检索质量直线下降。能讲清「按时间衰减 + 重要性 + 冲突消解来淘汰」,立刻显出你想过生产问题。

2.4 用户画像(Persona Memory)

长期记忆的一个典型应用:跨会话记住「用户是谁」

比如一个常做 RAG 选型的工程师,系统记住他「关注低运维方案、偏好开源、要带 benchmark 出处」。下次他来,不用每次都说明,系统自动按这个画像调整检索和回答风格。代价是要注意隐私和时效(画像也会过时,要更新)。

2.5 海量长期记忆怎么存

当长期记忆攒到海量(几十万条),存储和检索就成了工程问题:

  • 分层:热数据(最近 / 高频)放快存储,冷数据归档,别全堆在一个向量库里。
  • 向量检索 + 元数据过滤:先按用户 ID / 时间等元数据缩小范围,再向量检索,否则海量库里捞,又慢又不准。
  • 同一套审美:这其实又是「先缩小范围、再精检索」的两阶段思路,和向量检索召回是同一套工程审美。

讲师私货:别把「短期记忆」和「长期记忆」混在一个机制里做。我见过有人用同一个向量库,既存当前会话上下文、又存跨会话的用户画像,不分会话内外,结果当前对话的连贯性和偏好检索互相干扰,两头都不讨好。短期管「连贯」、长期管「记得」,分开设计、各用各的策略,系统才清爽。

🧭 这一章到这儿:记忆模块的设计与推理侧实现都讲透了,你能设计得出、跑得通。但有个更深的问题本章没碰:怎么让模型本身更会「用记忆」?知道哪些该记、指代该怎么补、什么时候该去长期记忆里捞,这同样要靠数据加训练,是后面几章的主题。


🏆 Part 3 · 验收串题 | ~10%

关联面试题(5 道,覆盖 5 个角度)

  1. 【记忆系统架构】 设计 AI Agent 的记忆系统时,短期记忆、长期记忆、记忆检索机制分别怎么考虑?
  2. 【为什么不能全塞】 多轮对话里,标准 Attention 机制在上下文上有什么上限?为什么不能把全部历史都喂进去?
  3. 【长上下文改进】 如何设计 / 改进大模型的记忆机制,提升长期上下文建模与任务持续能力?
  4. 【海量记忆存储】 当 Agent 需要维护大规模长期记忆时,存储架构怎么设计?
  5. 【记忆检索效率】 面对海量历史记录,如何保证记忆检索的效率和准确?

自检清单

  • 能讲清短期记忆 vs 长期记忆的分工,以及「为什么不能把全部历史塞给模型」(窗口 + Lost in the Middle)
  • 能在本机跑通带记忆 + 指代消解的 demo,并解释「它」是怎么被补全的、旧轮怎么进摘要
  • 能说出短期记忆的三种历史选择策略,以及「裁剪 vs 摘要」的取舍
  • 能讲清长期记忆的「写入 / 检索 / 更新 / 遗忘」四件事,尤其是遗忘为什么重要
  • 能讲清用户画像的价值与代价,以及海量记忆「分层 + 元数据过滤 + 向量检索」的存储思路

学完这一章,你的调研 Agent 已经「有脑子记事」了,多轮连贯、跨会话记得住。到这里,第 1-4 章把「看懂 + 跑通推理」这条线走完了:为什么做、执行框架、工具集、记忆管理。下一章起,我们进入「怎么把模型训练得真正擅长做调研」这条线,从数据构造开始。