多Agent共享记忆库设计指南:从上下文丢失到长期记忆持久化 自己先交代一下这套东西是我们在做智能食谱方向的研究Demo时折腾出来的。当时手上堆了十几个Agent各管一摊有管菜谱推荐的有管食材采购的还有管烹饪步骤纠错的。表面上看分工清晰实际用起来就露馅了——用户在同一天里问了两次问题第二次系统居然完全想不起第一次聊过什么。更离谱的是采购Agent给用户推荐了菜谱里明明白白写着过敏原的食材。问题根源很清楚Agent本身没有记忆会话一关上下文全丢。所以我们开始认真研究怎么给多个Agent搭一套能共享的长期记忆层最后用到了一个内部代号为openJiuwen的记忆库组件效果算是彻底翻盘。这篇把我们的设计思路、落地过程和踩坑记录都整理出来给同样在做多Agent应用的人一个参考。1. 菜谱场景的记忆断层为什么单Agent记不住多Agent也白搭1.1 上下文窗口不是记忆会话一关就归零刚开始接智能食谱的时候我想的是最常规的方案把用户近几轮的对话拼起来一起塞给大模型。这种做法在单轮问答里够用但一旦对话拉长或者用户隔几天再来上下文窗口里的内容早就被挤掉了。你可以理解为模型像一个只有一小块白板的厨师每次你擦掉白板重写他就忘了你是谁。做菜谱推荐恰恰最怕这种失忆。用户的真实需求往往不是一条查询语句能描述的而是长期积累下来的偏好集合孩子对花生过敏、家里老人牙口不好只能吃软烂的、一家五口里有两个人正在控糖、上周推荐的红烧肉太腻被吐槽了。这些信息散落在十几轮甚至几十轮对话里靠上下文窗口根本装不下。1.2 多Agent并行后失忆被进一步放大单Agent的问题说白了是记不住多Agent的问题就变成了它记了但别的Agent不知道。测试阶段我们拆了三类角色菜谱顾问Agent负责根据用户偏好推荐菜品处理忌口信息采购Agent负责根据菜谱生成食材清单推荐替代食材烹饪指导Agent负责解释步骤细节纠正操作错误这三个Agent各自独立跑对话各自都维护自己的会话上下文。结果就是菜谱顾问记住了用户不能吃香菜但采购Agent生成清单时照样加入香菜烹饪指导Agent也不会主动提醒这道菜里的香菜建议换成芹菜。每个Agent都觉得自己很聪明合在一起就是个漏勺。1.3 缓存式记忆、长期记忆、共享记忆的分工测试过程中我逐渐把记忆需求分成了三层这三层现在也是openJiuwen设计的基础记忆类型生命周期典型内容存储位置上下文缓存单次会话内用户当前提问、上一步操作会话上下文窗口长期记忆跨会话用户偏好、忌口、家庭人数、历史采纳记录记忆库持久化存储共享记忆跨Agent以上所有长期记忆但要求所有Agent可读可写记忆库共享集合不区分这层就很容易设计出一个伪共享系统——比如把记忆存在某个Agent的本地文件里其他Agent根本访问不到。我们最初就是这么干的后来才统一收口到openJiuwen的共享存储上。2. 记忆库的存储模型设计给Agent一本可以翻旧账的菜谱档案2.1 记忆单元的数据结构openJiuwen最核心的概念叫记忆单元Memory Unit相当于档案里的一张条目。每个记忆单元都必须包含固定字段这样才能被不同Agent识别和检索。我们在菜谱场景里最终敲定的结构长这样{ memory_id: mem_20240915_1001, agent_id: recipe_advisor, memory_type: user_preference, collection: household_001, content: 用户的孩子对花生过敏任何菜谱和采购清单中禁止出现花生及花生制品, embedding: [0.015, -0.021, ...], metadata: { source: home_dialog, confidence: 0.97, user_id: u_zhang, related_dish: 宫保鸡丁, timestamp: 2025-04-12T10:23:11Z }, access_policy: { read_agents: [recipe_advisor, purchase_agent, cooking_agent], write_agents: [recipe_advisor] }, ttl: 0, version: 3 }content是给人看也方便模型理解的纯文本embedding是给向量检索引擎用的语义向量metadata里塞了各种结构化标签。核心设计就一句话让记忆既可以被关键词精确检索又可以被语义模糊查找。采购Agent想知道有没有对花生过敏的记录直接查metadata里的allergen标签就行菜谱顾问模糊记得用户好像提过什么食物不能吃就得靠embedding做相似度检索。2.2 集合分组不同家庭的数据隔离酒店管理有房间号记忆管理要有集合名。我们为每个家庭用户创建了一个独立集合collection比如household_001、household_002。这么做有两层考虑一是数据隔离避免张三家的过敏信息跑到李四家的菜谱里去。这一点在食谱场景里不是小事万一串了轻则推荐出错重则引发过敏风险。二是权限控制更自然。openJiuwen允许给集合配置读写白名单household_001这个集合只允许张三家庭关联的Agent访问。以后接入新Agent时只要把它加进白名单它立刻就能共享整套家庭档案不用再手工搬运数据。2.3 为什么记忆要向量化而不是纯靠SQL查询一开始有同事质疑用户偏好这种数据用MySQL加几个标签字段不就完了搞什么向量化我在实测中明确感受到了区别。举个真实例子用户在对话里说上次那个牛肉炖得有点老孩子嚼不动这句话里没有任何软烂偏好忌口之类的关键词。用SQL查牛肉能查到但查老人牙口不好适合吃什么就完全无能为力。把这句话向量化之后embedding会把它和家里有老人适合软烂食物炖肉时间长一点这些语义相近的记忆拉近距离。实际检索时我们用query文本容易咀嚼的高蛋白菜谱去匹配能直接召回这条牛肉炖老了的记忆然后菜谱顾问Agent会据此调整推荐。向量化解决的是不知道怎么精确表达时也能找到记忆的问题这是传统数据库很难替代的。2.4 写入流程与去重合并记忆写入不是简单append。同一个事实可能被多个Agent重复记录比如菜谱顾问写了一条用户不吃香菜采购Agent在另一轮对话里又写了一条和用户确认不要香菜如果不去重时间久了档案里全是自相矛盾的碎片。我们处理方式是先查后写写入前用语义检索在集合里找相似度超过0.85的已有记忆命中后执行覆盖更新同时version加1。每次更新保留历史版本方便后续回滚。这个机制在后面的踩坑部分会详细展开也是我认为openJiuwen最有价值的设计之一。3. 跨Agent读写与同步机制三个角色共用一个档案柜3.1 先声明后使用Agent接入前的权限登记共享记忆最忌讳的是所有Agent一把梭、谁都往里乱写。我们针对觅食助手项目制定了一套接入规则任何Agent要访问记忆库必须先在配置中心做登记声明自己的角色、需要读哪些集合、需要写哪些类型的记忆。from openjiuwen import MemoryClient client MemoryClient( endpointhttp://127.0.0.1:8610, agent_idpurchase_agent, api_keysk-test-key-001 ) client.register_agent( collectionhousehold_001, readTrue, writeTrue, memory_types[shopping_list, price_alert, substitute_record] )采购Agent可以读写购物清单和替代记录但不能写user_preference这类长期偏好。这样做的原因很简单采购Agent每次对话的目标是快速生成清单如果它顺手把用户今天问了西红柿价格写成长期偏好会严重污染档案。权限分离能让写入责任清晰出了错也能快速溯源。3.2 写的时机什么时候值得进档案柜不是所有对话内容都需要持久化。我们的判断标准很简单这条信息在未来其他Agent的决策里会不会被用到。会就写不会就留在会话上下文里。具体到菜谱场景我们总结出三类必须写入的记忆长期偏好忌口、过敏、口味倾向、家庭人数构成历史决策采纳过哪些推荐、拒绝过哪些推荐、拒绝原因动态约束季节偏好、近期采购计划、临时控糖期# 菜谱顾问Agent对话结束后触发 def extract_and_save_memories(conversation_text, client): candidate_memories llm_extract(conversation_text) for mem in candidate_memories: # 只保存关键类型避免碎片化 if mem[type] in [user_preference, rejection_reason]: existing client.search_similar( collectionhousehold_001, querymem[content], threshold0.85 ) if existing: client.update_memory(memory_idexisting[memory_id], contentmem[content]) else: client.insert_memory(collectionhousehold_001, **mem)这套提取-检索-去重-写入的流程基本是每次对话结束时异步执行的不会阻塞用户当前的回答。3.3 读的时机每次生成回答前先翻档案和多Agent框架的传统prompt注入不同我们没有把全部记忆都塞进prompt而是按需检索。用户新提一个问题后系统先基于当前问题做一次语义检索召回最相关的top-5记忆再把它们和当前对话拼在一起发给对应Agent。举个例子用户对烹饪指导Agent说帮我看看红烧肉怎么炖不容易老系统会检索出上次牛肉炖老了孩子嚼不动这条记忆然后自动追加一句指令给Agent注意用户有孩子炖肉类建议延长时间并提示口感软烂。提示不要试图把所有记忆都塞进上下文token放不下而且大量无关杂讯反而会干扰生成质量。top-5这种数量级配合metadata标签过滤是当前性价比最高的方案。3.4 冲突处理乐观锁和最后写入者胜多Agent并发写同一个集合时冲突根本绕不开。我们的方案是给每条记忆加一个乐观锁版本号。Agent更新前携带自己读取到的version服务端写入时对比当前version不一致则返回冲突标记。正常对话场景下冲突概率不高我们用最简单的最后写入者胜策略处理同时保留前一版本。这条机制看起来简陋但在菜谱场景里足够用。真正需要注意的是冲突发生后旧版本别直接删留着审计用。采购Agent和菜谱顾问偶尔会对这个食材能不能替代产生不同意见各写各的版本最后人工review时对照历史记录一眼就能看出谁先谁后、谁改了什么。4. 上下文理解能力的实测变化从每次都问到主动避开过敏原4.1 测试集设计模拟真实家庭的多轮对话为了验证openJiuwen带来的提升我搭了一套模拟评测集模拟了20组家庭用户画像每组包含家庭人数、过敏原、口味禁忌、历史推荐记录四条预期记忆。测试时每种用户执行五轮标准对话流程覆盖以下典型需求第一轮要求推荐一份适合全家晚餐的牛肉菜谱第二轮追问上次那道菜能不能少放酱油第三轮采购Agent生成食材清单第四轮烹饪指导Agent解释炖煮时间和火候第五轮隔天回来问今天再做那道菜需要注意什么评测指标定义了三项记忆召回率五轮内需要调用历史记忆的次数中成功调用的比例、偏好命中率推荐菜品与画像中偏好的匹配比例、禁忌触达率推荐内容触碰过敏原或禁忌的次数。4.2 接入前后数据对比指标无共享记忆基线接入openJiuwen后记忆召回率28.5%93.2%偏好命中率41.3%88.7%禁忌触达率17.6%1.9%用户平均追问次数3.4次/轮1.2次/轮最让我在意的是禁忌触达率从17.6%降到了1.9%。也就是每100次推荐里以前大概有18次会碰到用户明说的雷区现在只有不到2次。剩余的那部分大多是模糊表达导致的比如用户只说少吃辣但没明说辣到什么程度这类情况靠记忆库解决不了只能靠后续澄清。4.3 为什么共享记忆能提升上下文理解很多人以为上下文理解是模型能力问题做了这个实验之后我的体会变了大部分所谓理解不到位其实是信息缺失。模型再聪明不知道用户家中有过敏儿童、不知道上次推荐被吐槽太咸它就只能在那里瞎猜。共享记忆的另一个隐性收益是交叉验证。单个Agent和用户对话时经常被用户的模糊表述带偏。现在三个Agent共享一套档案采购Agent看到菜谱顾问写的用户不吃香菜即使当前对话里没提到也会主动规避。这相当于给每个Agent都配了一个记忆参谋决策质量自然上去了。4.4 用户体感少问一次信任多一分测试用户里有一位家里有挑食孩子的A用户接入共享记忆前系统每轮都要问孩子有什么忌口吗后来我们做了优化第一天推荐时记录了孩子对花生过敏、不喜欢香菜第二天用户直接说今天想吃鸡翅系统自动屏蔽了所有花生酱、香辣口的方案推荐了蜜汁烤鸡翅。A用户原话是这次记得还挺牢。这种细节上的稳定表现比任何花哨的语言能力都更能建立信任。做菜谱助手这类产品用户要的不是偶尔惊艳而是稳定靠谱共享记忆正好补上了这一环。5. 记忆污染、过期清理与回滚多Agent共享后的维护难题5.1 典型污染事故一次价格对话污染了长期偏好上线试运行第三天我们撞上了第一个记忆污染问题。用户在对采购Agent说今天的西红柿比上周贵了三分之一先不买了采购Agent按规则把这条信息写入了shopping_list类型。本来没啥问题但当时检索去重的阈值设得太低结果这条内容被误合并到了user_preference里。后果很直观第二天菜谱顾问推荐番茄炒蛋被系统拦截理由是用户近期拒绝购买西红柿。用户莫名其妙我们排查半天才发现是污染导致。这次事故让我把记忆类型从建议变成了铁律——不同类型的记忆存储位置必须物理隔离绝对不能靠tag区分。5.2 TTL过期策略别让记忆变成永久负担记忆不是越久越好。菜谱场景里有大量短期信息比如本周控糖孩子最近在换牙冰箱里还有剩牛肉。这些信息如果永久存储三个月后还在影响推荐反而成了噪音。openJiuwen支持每个记忆单元设置ttl秒级过期后检索自动忽略后台定时任务再物理清理。我们给不同记忆类型设定的默认TTL如下记忆类型默认TTL过期示例shopping_list7天本周采购清单price_alert30天临时价格波动user_preference永久过敏原、忌口temporary_constraint14天控糖期、换牙期rejection_reason90天某道菜被拒绝的具体原因有一条比较微妙rejection_reason不能永久保留。用户三个月前拒绝过太油腻的菜不代表现在也拒绝食性偏好会随着季节、健康状态变化。给拒绝原因设90天过期相当于给用户一个重新选择的机会不会因为一次小情绪被长期贴标签。5.3 回滚与纠偏错误写入之后的补救措施即使做了权限控制错误记忆仍然防不胜防。我们沉淀出了一套回滚SOP现在已经成了openJiuwen的标配操作# 查看某条记忆的所有历史版本 openjiuwen history get --collection household_001 --memory-id mem_20250915_1001 # 回滚到指定版本 openjiuwen history rollback --memory-id mem_20250915_1001 --version 2实际使用中回滚前必须先查一下这条记忆被哪些Agent读取过读的时候是否已经影响了它们的回复。因为即使你回滚成功采购Agent如果在污染期间已经把错误信息写进了采购清单那条清单还得单独清理。回滚记忆只是治本清理下游影响才是止痛。5.4 记忆审计给档案柜配一个管理员多Agent共享记忆跑起来之后必须有一个定期审计机制。我们的做法是每周生成一份记忆库变更报告列出新增、更新、过期、冲突频次最高的记忆条目以及各Agent的写入量排名。报告作用最明显的一次我们发现菜谱顾问Agent的写入量是其他Agent的四倍一查才发现它在每轮对话结束时会重复写入用户偏好即使内容完全没变化。去重逻辑里阈值设得偏高导致大量相似记忆堆积。修复后整个集合体积缩了60%检索速度也快了不少。注意Agent写入量突然暴涨多半不是正常现象优先怀疑去重失效而不是模型抽风。5.5 最后的经验沉淀这套记忆库方案前后调了将近两个月踩过的坑比预想的多。如果让我给刚接触跨Agent共享记忆的人一个建议我会说先想清楚哪些信息值得共享再动手搭存储。记忆库的难点从来不在技术选型而在于判断什么该记、什么该忘、谁能写、谁能读。openJiuwen给的是工具但归档规则和边界意识得自己一步一个坑试出来。每次回想那顿差点被香菜毁掉的晚餐测试我都觉得多Agent应用想真正的理解用户靠模型参数是走不远的记忆才是地基。希望这篇折腾记录能让你在搭自己的共享记忆时少打开几个不该打开的抽屉。