拒绝盲目堆砌向量数据库:如何通过长短期记忆分层治理 Agent 的“记忆污染”?

代码地址:撕开黑盒学大模型-从白盒状态机演进到工业级Agent框架

本文目标:用一个本地可运行 Demo 说明为什么“接入向量库”不等于“做好记忆系统”。



1. 问题与背景

Agent 一旦进入多轮对话,就会遇到两个直接问题:

  1. 所有历史都塞回 Prompt,Token 成本和上下文长度会失控;
  2. 历史都丢掉,Agent 又会失去连续性。

很多方案会马上引入向量数据库,把所有对话都 embedding 后存起来,再用 Top-K 检索。这个方向本身没有问题,但如果缺少治理,会引出另一个问题:记忆污染。

所谓记忆污染,是指 Agent 在当前任务中拿到了不该进入上下文的历史信息,包括:

  • 低相关噪声;
  • 过时结论;
  • 其他用户的隐私数据;
  • 已经被撤销或擦除的信息;
  • 与当前任务相似但语义方向相反的内容。

向量库负责“存和搜”,不负责“哪些记忆应该进入推理”。这就是本文要拆开的边界。

问题分析可以拆成三层:第一层是相关性,低分噪声不该进入 Prompt;第二层是数据边界,其他用户或租户的记忆不该被召回;第三层是生命周期,过期、撤销或已经擦除的信息不该继续影响推理。后面的 Demo 就围绕这三层做最小复现。

2. 三轨制记忆结构

v2_memory/ 中的 MemoryManager 使用三轨结构:

recent_messages: list[Message]
summary_memory: str
vector_store: JsonVectorStore

三类记忆的职责不同:

记忆类型 作用 风险
recent_messages 保留最近几轮原始对话,维持短期连续性 窗口过大导致成本上升
summary_memory 将超出窗口的早期内容压缩为背景摘要 摘要可能丢细节或引入偏差
vector_store 持久化长期经验,用查询语义召回 Top-K 可能带入低相关噪声

当前 demo 的 window_size 是 4,检索阈值 threshold 是 0.22。每新增一条消息,都会进入短期窗口和本地向量存储;当窗口超过窗口大小,最早的消息会被压缩进 summary_memory

def add(self, message: Message) -> None:
    self.recent_messages.append(message)
    self.vector_store.add(message.user_id, message.role, message.content)
    if len(self.recent_messages) > self.window_size:
        expired = self.recent_messages.pop(0)
        self.summary_memory = self._summarize(expired)

这里的 _summarize() 是一个教学用的确定性压缩函数。真实系统中可以替换为大模型摘要节点,但接口语义不变。

3. 为什么不用真实 ChromaDB / FAISS

本 demo 使用 JsonVectorStore,而不是直接依赖 ChromaDB 或 FAISS。原因不是否定向量库,而是为了降低读者复现门槛。

vector_store.py 提供的是向量库门面:

store.add(user_id, role, content)
store.search(query, user_id=user_id, top_k=top_k, threshold=threshold)
store.erase_user(user_id)

当前相似度使用“空格词 + 中文二字片段”的轻量 tokenizer,再做余弦相似度。这不是生产级 embedding,只是无依赖 fallback。真正接入 ChromaDB / FAISS 时,应该替换底层 store,而不是改上层 MemoryManager 的治理语义。

这也是工程设计里重要的一点:先稳定业务边界,再替换实现细节。

生产系统里,向量库通常还会承载 metadata filter、索引维护、批量写入、删除和持久化等能力。本文的 JsonVectorStore 只模拟三个最小接口:写入、检索、擦除。这样做的好处是读者可以先看清治理语义,再决定底层换成 ChromaDB、FAISS、Milvus、pgvector,还是云厂商托管向量检索。

4. Top-K 污染实验

运行:

python v2_memory\main.py

demo 会写入几类数据:

  • 与 Agent 专栏相关的对话;
  • 与记忆污染相关的任务;
  • “午饭、咖啡、天气闲聊”这类噪声;
  • guest 用户的隐私记录。

然后对同一个查询执行两种检索:

blind_top_k = manager.retrieve_without_threshold(query, top_k=8)
thresholded = manager.retrieve(query, top_k=8)

在一次验证中,无阈值 Top-K 输出包括低分记录:

blind top-k:
0.35 如何治理记忆污染?
0.28 请记录,第二篇要讨论记忆污染。
0.21 继续讨论向量检索阈值。
0.20 先做阈值过滤,再把短期窗口、摘要记忆和长期检索分层处理。
0.20 我会基于当前会话窗口和通过阈值筛选的历史记忆回答。
0.08 我在写 Agent 专栏,关注 ReAct 状态机。

阈值过滤后只保留更相关的内容:

thresholded:
0.35 如何治理记忆污染?
0.28 请记录,第二篇要讨论记忆污染。

这就是“不要盲目 Top-K”的直接证据。Top-K 只保证返回 K 条,不保证每条都值得进入 Prompt。

这里的阈值不是为了追求一个通用标准答案,而是为了暴露工程权衡:

阈值策略 结果 风险
不设阈值,只取 Top-K 永远能拿到 K 条候选 低相关噪声、过时信息、相似但方向错误的信息进入 Prompt
阈值过低 召回更多历史 上下文污染仍然存在,模型可能被弱相关记忆带偏
阈值过高 上下文更干净 可能漏掉有用历史,导致 Agent 忘记真正相关的任务背景
阈值 + 用户隔离 + 二次过滤 更适合进入生产链路 需要额外维护评估集、审计日志和失败回放

所以生产里不要只看“召回了几条”,而要看“进入 Prompt 的记忆是否应该被模型看到”。更稳的做法是把相似度阈值、metadata filter、rerank、时间衰减和敏感信息过滤组合起来,而不是把 Top-K 当成最终上下文。

5. 用户隔离与主动擦除

记忆污染不只是相关性问题,也包括数据边界问题。

Message 中包含 user_id

@dataclass(frozen=True)
class Message:
    role: str
    content: str
    user_id: str = "default"

检索时必须按用户过滤:

self.vector_store.search(query, user_id=user_id, top_k=top_k, threshold=self.threshold)

构造上下文时,短期窗口也要按 user_id 过滤:

parts.extend(
    f"recent: {message.role}: {message.content}"
    for message in self.recent_messages
    if message.user_id == user_id
)

这个细节很容易被忽略。如果只隔离向量检索,不隔离短期窗口,其他用户的消息仍然可能通过 recent messages 进入当前上下文。

主动擦除由 erase_user() 完成:

def erase_user(self, user_id: str) -> None:
    self.recent_messages = [message for message in self.recent_messages if message.user_id != user_id]
    self.vector_store.erase_user(user_id)

运行后生成的 trace.json 中会包含 guest_records_after_erase,可视化页面会显示 guest 用户记录是否已经擦除。

这个验证点很关键:如果只在向量检索里按 user_id 过滤,而短期窗口、摘要记忆或删除接口没有同步隔离,隐私数据仍然可能从其他路径回到 Prompt。记忆治理不能只管“长期记忆”,还要覆盖每一条进入上下文的路径。

6. 可视化验证

运行 demo 后打开:

v2_memory/visualization.html

页面会展示:

  • 当前查询;
  • 摘要记忆;
  • 短期窗口;
  • 无阈值 Top-K;
  • 阈值过滤结果;
  • 隐私擦除验证。

这比只在文章里讲概念更可靠。读者可以直接改 thresholdtop_k、噪声内容和 window_size,观察上下文如何变化。

为了确认证据不是手写出来的,运行后可以直接检查 trace.json

{
  "query": "记忆污染 阈值 Agent",
  "thresholded": [
    {
      "score": 0.354,
      "record": {
        "content": "如何治理记忆污染?"
      }
    },
    {
      "score": 0.277,
      "record": {
        "content": "请记录,第二篇要讨论记忆污染。"
      }
    }
  ],
  "guest_records_after_erase": []
}

guest_records_after_erase 是空数组,说明 guest 用户的长期记忆已经从本地 store 中删除。短期窗口也在 erase_user() 里同步过滤,这样才算完成一次最小闭环。

7. 风险边界与从 Demo 迁移到生产的替换路径

当前实现适合教学,但不能直接视为生产记忆系统。生产环境至少还需要补齐:

  • 真实 embedding 模型;
  • 向量库持久化和索引维护;
  • 记忆版本号和过期策略;
  • 摘要质量评估;
  • 用户级、租户级和业务域级隔离;
  • 敏感信息识别和擦除审计;
  • 检索结果进入 Prompt 前的二次过滤。

比较稳妥的迁移路径是:

当前 Demo 生产替换点 迁移时不要改变的语义
JsonVectorStore.add() ChromaDB / FAISS / pgvector / Milvus 写入接口 写入时保留 user_id、来源、时间、版本等 metadata
词法余弦相似度 embedding 模型 + 向量索引 检索结果仍然必须经过阈值或 rerank
user_id 过滤 用户级、租户级、业务域级 metadata filter 不能跨用户、跨租户混召回
erase_user() 删除 API + 审计日志 + 异步清理任务 主动擦除必须覆盖短期、摘要、长期和缓存
_summarize() 大模型摘要节点 摘要要有版本、来源和质量评估,不能无限拼接

如果替换成 ChromaDB,重点是把 user_id、租户、业务域、过期时间写进 metadata,并在 query 时用 metadata 过滤;如果替换成 FAISS,重点是额外维护 id 到 metadata 的映射,因为 FAISS 更偏向高效向量相似度搜索,本身不等于完整的记忆治理层。

但这些能力都建立在同一个原则上:记忆系统不是“把历史存起来”,而是“让正确的历史在正确的边界内进入推理”。

8. 总结

向量数据库不是 Agent 记忆治理的终点。它解决的是长期存储和语义召回,不能替代短期窗口、摘要压缩、阈值过滤、用户隔离和主动擦除。

一个可维护的 Agent 记忆系统,应该把 recent_messagessummary_memoryvector_memory 分层设计。只有这样,记忆才不会从能力变成污染源。

9. 环境与复现范围

本文配套代码是一个无外部服务依赖的教学 Demo,目的是把 Agent 记忆治理的边界拆开,而不是演示某个向量数据库的完整生产部署。

验证环境:

项目 说明
操作系统 Windows 10/11、macOS、Linux 均可
Python 建议 Python 3.10+
第三方依赖 无,使用 Python 标准库
运行目录 仓库根目录或 v2_memory/ 目录
输出文件 v2_memory/trace.jsonv2_memory/memory_store.json
可视化页面 v2_memory/visualization.html

如果从仓库根目录运行:

python v2_memory\main.py

如果已经进入 v2_memory/ 目录运行:

python main.py

运行后再打开 visualization.html,可以看到无阈值 Top-K、阈值过滤结果、短期窗口、摘要记忆和用户擦除验证。

10. 参考资料


下一篇:小z疯狂码字ing…
感谢阅读,记得点赞、关注、收藏,欢迎各位评论区交流!!!

在这里插入图片描述

Logo

中国智能体开发者社区,聚焦智能体与大模型开发,提供前沿资讯、实用工具链、开源项目及行业案例。通过技术沙龙、开发者大赛等活动,促进经验交流与协作,助力开发者快速构建创新智能应用。

更多推荐