【撕开黑盒学大模型】拒绝盲目堆砌向量数据库:如何通过长短期记忆分层治理 Agent 的“记忆污染”?
拒绝盲目堆砌向量数据库:如何通过长短期记忆分层治理 Agent 的“记忆污染”?
代码地址:撕开黑盒学大模型-从白盒状态机演进到工业级Agent框架
本文目标:用一个本地可运行 Demo 说明为什么“接入向量库”不等于“做好记忆系统”。
文章目录
1. 问题与背景
Agent 一旦进入多轮对话,就会遇到两个直接问题:
- 所有历史都塞回 Prompt,Token 成本和上下文长度会失控;
- 历史都丢掉,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;
- 阈值过滤结果;
- 隐私擦除验证。
这比只在文章里讲概念更可靠。读者可以直接改 threshold、top_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_messages、summary_memory 和 vector_memory 分层设计。只有这样,记忆才不会从能力变成污染源。
9. 环境与复现范围
本文配套代码是一个无外部服务依赖的教学 Demo,目的是把 Agent 记忆治理的边界拆开,而不是演示某个向量数据库的完整生产部署。
验证环境:
| 项目 | 说明 |
|---|---|
| 操作系统 | Windows 10/11、macOS、Linux 均可 |
| Python | 建议 Python 3.10+ |
| 第三方依赖 | 无,使用 Python 标准库 |
| 运行目录 | 仓库根目录或 v2_memory/ 目录 |
| 输出文件 | v2_memory/trace.json、v2_memory/memory_store.json |
| 可视化页面 | v2_memory/visualization.html |
如果从仓库根目录运行:
python v2_memory\main.py
如果已经进入 v2_memory/ 目录运行:
python main.py
运行后再打开 visualization.html,可以看到无阈值 Top-K、阈值过滤结果、短期窗口、摘要记忆和用户擦除验证。
10. 参考资料
- Chroma Metadata Filtering:说明 query/get 时可以用 metadata 条件过滤结果。
- FAISS 官方文档:FAISS 是用于密集向量相似度搜索和聚类的库。
- LangGraph Persistence:Persistence 同时区分短期记忆和长期记忆。
- LangChain Memory overview:介绍 Agent 为什么需要跨交互记忆。
下一篇:小z疯狂码字ing…
感谢阅读,记得点赞、关注、收藏,欢迎各位评论区交流!!!

更多推荐



所有评论(0)