AI Agent的“记忆焦虑“:一次RAG知识检索的架构升级实录
AI Agent的"记忆焦虑":一次RAG知识检索的架构升级实录
你的AI助手,其实有"金鱼记忆"
你大概有过这样的体验:和AI助手聊了二十轮需求分析,到了最后一轮定稿时,它问你"这个支付系统的核心概念是什么?"——明明第三轮就详细讨论过。
这不是段子。在当前的RAG(Retrieval-Augmented Generation,检索增强生成)架构中,这个问题真实存在,而且影响不小。
问题出在哪里?
在一个基于MCP协议构建的AI Agent系统中,每次工具调用都会独立执行一次向量数据库(Qdrant)检索。检索结果注入当轮LLM的Prompt中使用,调用结束后——直接销毁。
举个真实场景:
Tool 调用 #1:用户说"我们做支付清结算系统,痛点是结算延迟"
→ Qdrant 检索命中"清结算"领域知识 (score=0.85, 0.82)
→ LLM 按指令暂不引用 → 检索结果销毁 ❌
Tool 调用 #2:用户说"B. 什么都不做,合规风险很大"
→ Qdrant 重新检索(query变成了"合规风险")
→ 上轮命中的"清结算"核心知识不可恢复 ❌
问题很明确:第一轮用长文本检索到的高质量知识,第二轮用一句短语重新检索就找不回来了。
这就像你花了一小时读完一本行业白皮书,第二天只凭一句"这本书讲的是金融"的模糊记忆去做决策。
两级缓存:给Agent装一个"工作台"
核心解法其实很直觉:好的知识别扔,放在桌上,后面随时能用。
概念模型
┌─ Session 生命周期 ──────────────────────────────┐
│ │
│ ┌─ Level 1:Session 知识缓存("仓库")──────┐ │
│ │ 存储位置:Redis │ │
│ │ 生命周期:随 Session 创建和过期 │ │
│ │ 容量上限:30 条 │ │
│ │ 入缓存门槛:score ≥ 0.70 │ │
│ │ 淘汰策略:FIFO(满时淘汰最旧条目) │ │
│ └──────────────────────────────────────────┘ │
│ ▲ 每轮合并注入 │
│ ┌─ Level 2:本轮 Qdrant 检索("猎手")──────┐ │
│ │ 每轮用当前 message 做 query,实时检索 │ │
│ │ 结果仅当轮使用,不自动持久化 │ │
│ └──────────────────────────────────────────┘ │
└──────────────────────────────────────────────────┘
Level 1 是仓库,Level 2 是猎手。 猎手每轮根据当前上下文精准狩猎,仓库负责把猎物存起来,供后续随时取用。高质量知识一旦进入仓库,在整个会话中持续可用——只进不出,渐进积累。
每一轮发生了什么?
每轮调用执行六步:
- Level 2 实时检索:用当前用户输入作为query,从Qdrant中实时检索
- Level 1 加载缓存:从Redis Session中读取已有的知识缓存
- 两级合并:基于text MD5去重,保留更高score版本,按score降序排列,截取Top 15
- 注入Prompt:标注来源——
[历史缓存]或[本轮检索] - LLM调用:大模型综合两层知识生成回复
- 更新缓存:本轮高质量结果(score ≥ 0.70)追加进Level 1,超限FIFO淘汰
合并算法的核心逻辑
def merge_levels(level1, level2, max_total=15):
"""
1. Level 1 先加入(基线)
2. Level 2 加入(同 key 时保留更高分)
3. 按 score 降序 + 截断 top N
"""
all_items = {}
for item in level1:
all_items[_text_hash(item.text)] = item
for item in level2:
key = _text_hash(item.text)
if key not in all_items or item.score > all_items[key].score:
all_items[key] = item
sorted_items = sorted(all_items.values(), key=lambda r: r.score, reverse=True)
return sorted_items[:max_total]
关键设计:去重用的text MD5算法必须与检索器一致——唯一的hash计算入口,避免两处独立实现出现不一致。
一个完整会话的时序演示
以BA(业务分析师)需求澄清场景为例:
第1-2轮(初始化):缓存为空,Qdrant检索也无有效结果。正常退化。
第3轮:用户输入长文本"我们做支付清结算系统,痛点是结算延迟、渠道对接复杂…"
→ Level 2命中3条知识(score: 0.85, 0.82, 0.68)
→ 前2条(≥0.70)入缓存,0.68被过滤
→ 缓存 = [清结算术语, 支付业务规则]
第4轮:用户输入"B. 什么都不做,合规风险很大"
→ Level 2命中2条(score: 0.78, 0.62)
→ Level 1缓存仍在!合并注入4条知识,取Top 3
→ LLM看到的是:[历史缓存]清结算术语(0.85) + [历史缓存]支付规则(0.82) + [本轮检索]合规知识(0.78)
→ 缓存 = [清结算术语, 支付规则, 合规知识]
第5轮:用户说"选A,继续"——一句短语
→ Level 2检索:0条有效结果(query无语义)
→ 但Level 1有3条历史知识兜底! LLM仍然拥有完整的领域知识上下文
这就是两级缓存的价值:当用户输入简短到无法触发有效检索时,历史缓存自动补位。
到第20轮定稿阶段,缓存已渐进积累到5-8条覆盖各维度的领域知识,LLM在定稿时能参考整个Session的全部积累——而不只是最后一轮的信息。
工程实现的关键决策
为什么选Redis而不是内存变量?
Session可能跨请求持久存在,内存变量在进程重启后丢失。Redis与现有的Session管理机制天然契合,缓存数据序列化为JSON,随Session TTL自动过期,无需额外清理逻辑。存储体量估算:30条 × ~400字符 ≈ 12-20KB,远低于Redis的512MB单key限制。
为什么用FIFO而不是LRU?
早期版本考虑过加时间戳做LRU(最近最少使用),但评审后发现:当前淘汰基于列表顺序(追加到尾部、从头部淘汰),FIFO已经足够——早期知识会被后期更相关的知识自然替换。移除时间戳字段减少了序列化体积和模型复杂度。如果远期需要基于时间的主动失效策略,可以再加回来。
四个可调参数
| 参数 | 默认值 | 调优区间 |
|---|---|---|
| 缓存开关 | True | 关闭后退化为现有行为 |
| 入缓存质量门槛 | 0.70 | 0.65-0.75 |
| Level 1最大容量 | 30条 | 影响Prompt体积 |
| 注入Prompt上限 | 15条 | 平衡覆盖与体积 |
风险边界
- 缓存噪声:0.70的质量门槛 + 15条注入上限双重控制
- Prompt膨胀:FIFO淘汰 + 注入上限 + 知识层字符数统计日志监控
- 知识过时:FIFO自然淘汰 + 同文本hash新结果覆盖旧结果
- 并发安全:当前MCP协议下同一Session调用是串行的,不存在竞态;预留了分布式锁接口
效果对比
| 场景 | 无缓存(改造前) | 有两级缓存 |
|---|---|---|
| 用户回复"选A,继续" | 知识层为空,LLM无知识支撑 | 3条历史知识自动补位 |
| 多轮后进入定稿阶段 | 所有早期知识已丢失 | 8条完整领域知识注入 |
| 简短回复的多轮对话 | 几乎每轮都检索不到有效结果 | Level 1持续兜底 |
写在最后
RAG系统的知识检索,最核心的不是"每轮能找到什么",而是**“整个会话积累了什么”**。
两级缓存的设计哲学很简单:好知识值得被记住。 通过一个简单的Session级缓存层,让早期阶段用长文本检索到的高质量领域知识,在后续所有阶段中持续可用。代码改动量不大(新增1个工具文件,修改3个文件),但解决了一个在RAG系统中普遍存在的"知识遗忘"问题。
如果你也在做AI Agent的知识检索系统,不妨检查一下:你的检索结果,是不是用完就扔了?
更多推荐
所有评论(0)