
1. 多 Agent 协作最大的坑不是模型是“失忆”先说个我之前踩过的真实事故。当时我在做一个给财务同事用的发票信息提取助手流程很简单Agent A 负责从 PDF 里抽商品明细Agent B 负责按商品名称去数据库查价目最后 Agent C 汇总并生成报销单。单看每一步模型表现都正常但整个流程跑下来Agent B 总会在第三轮之后开始犯迷糊。它反复问 Agent A“你刚才说的那个商品是什么再发一遍。”而 Agent A 明明已经把商品名、规格、数量一次给了出去。问题不是模型能力不行不是提示词写得不好而是 Agent B 压根不记得 Agent A 跟它说过什么。这个场景就是我在标题里想说的“失忆”。多 Agent 协作里最大的坑不是模型聪明不聪明而是上下文在整个协作链条里持续丢失。每个 Agent 看似都有上下文窗口但到了实际协作时上一棒的信息往往在下一棒手里就断了。这篇文章要聊的就是“失忆”这件事为什么发生、发生在哪些环节、以及怎么从工程上把它解决掉。适合正在做 LLM 应用编排、多 Agent 工作流、自动化任务拆解的工程师读也适合产品经理和技术负责人用来判断项目里的风险点到底在哪。很多人一开始和我一样觉得多 Agent 项目最难的是选模型、写提示词、调温度参数。搞到最后才发现这些活儿大概两三天就能摸到门道真正让人反复返工的永远是上下文在多个 Agent 之间传递时的丢失、错乱、串台和过期。这篇文章我会把“失忆”拆成几个具体的临床表现然后给出对应的上下文管理手段最后用一个可以跑的编排示例说明一套不“失忆”的工作流到底长什么样。2. 失忆的三种典型表现你大概率至少中过一种2.1 全局状态交汇所有 Agent 读写同一个上下文却没有人对顺序负责这是最普遍的一种失忆形态。很多编排框架里多个 Agent 共用同一个消息列表Agent A 把它需要传递的信息append到对列表里Agent B 从列表尾部读取。如果 Agent A 写消息的时机和 Agent B 读消息的时机没有对齐B 读了半天发现列表里根本没自己需要的东西或者读到了已经被 Agent C 覆盖过的新内容那协作自然就断了。这里可以做一个生活类比三四个同事共用一块白板A 把采购清单写在白板上就去忙别的了B 过来看到白板并不是直接去找“采购清单”那一栏而是把整块白板从头到尾读一遍结果发现 C 已经在白板上画了一个新的流程图把原来那块内容擦掉改写了一半。B 没来得及看到原始采购数据只好再跑去问 A。这在办公室叫“沟通断层”在 Agent 系统里就是上下文被并发覆盖。根据我实际调试项目的经验这类问题的隐蔽性在于单次运行时它多半不会报错。Agent B 会用模糊的猜测去补足缺失的信息甚至自己编造一个看似合理的答案你不会立刻感知到数据丢了。直到某个下游 Agent 拿着编造的内容做了决策你才追悔莫及。应对方式也很明确不要在多个 Agent 之间裸共享一个消息数组。每个 Agent 的输入数据应该做显式的契约定义谁写给谁读写什么字段什么时候写都必须从代码上约束住而不是靠 Agent 自己去上下文里“悟”。2.2 While 循环假象循环本身不会给 Agent 带来记忆第二种失忆发生在把for或while循环当成迭代推理的场景里。比如你写了一个循环让同一个 Agent 反复分析一段文本期望它在多轮循环中“越思考越深入”。但循环体的每次迭代都向模型发起了独立请求。这个请求里如果只带了当前步骤的输入而不带上一轮的结论那么 Agent 每一轮都是从零开始思考唯一在变化的就是你喂给它的外部变量。从观察者的角度看Agent 调用了很多次、花了很多 token但它并没有在“思考”它只是在用同一个模型、同一个提示词、处理不同的输入数据。这个问题的本质是混淆了“执行多次请求”和“模型具备迭代思考能力”。模型自身没有跨请求的记忆除非你主动把状态传递给它。否则循环再多次也不会产生推理的递进。想踩烂这个坑正确的做法是每一轮循环结束后把这一轮的关键结论结构化成一段文字或一个 JSON 片段拼接到下一轮的输入前缀里。比如一个用于修正文案的 Agent 循环第 2 轮输入里必须显式包含第 1 轮中模型的输出内容以及你基于第 1 轮输出做的判断比如“上一版结尾太啰嗦本轮压缩到 20 字以内”。2.3 持久化缺失重启应用之后一切归零第三种失忆看起来很基础但生产环境里大量存在。用户会话在浏览器里停留了一阵子刷新页面之后之前给 Agent 交代过的偏好设置、业务背景、历史操作全部消失。这在高并发、长会话的应用里尤其致命。很多团队刚开始做 Agent 产品时只把消息放在内存里一重启服务所有上下文灰飞烟灭。之前有群友问了类似的问题他配置好了一个本地模型服务用某个账号管理工具切换了一下配置之前的对话上下文就再也加载不出来了页面反复跳闪也没有正常恢复会话。这种问题多半出在存储和会话标识的绑定上。切换账号或切换模型服务之后新会话 ID 找不到旧的消息记录前端拿不到历史只能原地转圈。哪怕模型本身没有变记忆也丢了。所以无论你用什么管理工具、切哪个服务商都要特别确认一件事会话记录的存储与读取是不是跟着用户标识或会话 ID 走的而不是跟着某个临时的运行时状态走的。只要服务重启或账号切换之后还能从持久化存储里捞回历史消息应用才算真正“有记忆”。3. 上下文管理的本质Token 预算、生命周期与分层视角3.1 上下文不是塞得越多越好很多后来找我咨询的人都有一种思维惯性既然模型会失忆那我多给它点上下文不就行了吗于是把整个会话里所有历史消息、所有工具调用记录、所有中间变量一股脑全塞进请求里。结果就是模型读倒是读得完就是抓不住重点了。这背后有一个核心机制上下文窗口里的 token 数量直接影响模型对每部分信息的注意力权重。当信息密度过高、有效信息只占很小比例时模型在生成答案时会倾向于用平均的注意力扫描全部文本关键指令很容易被稀释。我见过最夸张的一个案例开发者在提示词前头贴了 2000 多行的业务文档Agent 的指令藏在文档末尾结果模型每轮回复都是对文档的复述根本不执行指令。后来把文档压缩成 200 字的背景说明、再把指令单独放在系统提示词里问题直接消失。Token 预算不是无限的。你可以按 70/20/10 的比例分配70% 给当前任务必须的操作指令与刚需数据20% 给短期的历史摘要10% 留给模型输出。这个比例不用特别精确但它强迫你去思考“哪些东西值得占用窗口”。如果一个信息对当前 Agent 的下一步动作没有任何帮助那它就是噪音不该被放进上下文。处理的办法是定期归档旧消息把冗余内容从主上下文移除只保留摘要。3.2 上下文生命周期四阶段创建、写入、归档、遗忘我后来在看 RAG 和数据工程的项目时学到一种思路数据有生命周期上下文同样有。一个上下文对象从创建到销毁应该经过四个阶段创建当用户开启一个会话或任务时构建一个空的上下文容器。写入随着对话推进把关键信息用户偏好、任务目标、中间结果写入上下文。归档当一段交互已经结束或被更重要的信息覆盖时把它压缩成一行摘要并移到单独的历史存储。遗忘当上下文窗口逼近上限时删掉那些既不是当前步骤所需、也不影响未来决策的信息。这四个阶段里新人最容易忽略的是“遗忘”。他们总觉得 AI 应用应该记住所有事情忘了反而显得系统不智能。但从成本和质量看理性遗忘是保证上下文干净的重要手段。一个 Agent 不需要记住用户在三个月前问过的天气它需要记住的是用户长期的称呼习惯、所在城市和通勤方式。区分“长期偏好”和“短期临时信息”是设计上下文时最需要花费心思的地方。3.3 分层视角全局上下文与局部上下文为了不让上下文混为一团我把一份上下文体拆成三层基础指令层系统提示词、模型输出格式、安全规则、工具调用方式。这一层基本不变全链路共享。共享信息层用户的业务数据、业务知识库、跨 Agent 传递的关键结构化输出。这层可以在多个 Agent 之间交棒但必须有明确的 Permission比如哪些 Agent 可写哪些只读。临时交互层单次任务里的中间推理、草稿、候选方案。这一层只属于当前的 Agent任务完成后就归档或丢弃。用工程化的语言讲就是状态的“作用域”应该被显式区分。全局的归全局局部的归局部。如果所有 Agent 都能读到所有内容你很快就会遇到信息串台、历史误导、Token 浪费这三大问题。反过来如果每个 Agent 只能读到它当前需要的那部分上下文协作反而更稳定因为模型不必在无关信息里挑挑拣拣。4. 编排框架的落地选择Swarm 的 Handoff 与上下文变量4.1 轻量框架更适合学习和验证问题理论说了一堆落到工程上总得选一个趁手的编排工具。我常用的是 Swarm 这一类实验性质的编排框架原因很简单它没有把“Agent”包装成黑盒而是把每个 Agent 看作一个“指令 模型 上下文变量”的组合通过 Handoff 让控制权在不同 Agent 之间流转。我们团队之前做过一套订单查询助手的实验就是基于这个模式搭的。前台 Agent 负责识别用户意图后台 Agent 负责查订单、查物流。最开始我们会把两套指令塞进一个 Agent 里做意图路由结果发现指令一旦多起来模型就开始犯晕工具选择的准确率直线下降。后来改成 Swarm 模式前台 Agent 只要分析出“用户想查物流”就立刻把控制权转交给后台 Agent同时把用户提供的订单号、手机号等字段放进上下文变量里交过去。后台 Agent 不需要理解用户的完整意图链路只需要在拿到字段后去调工具。协作一下就顺了。4.2 用 context_variables 把“记忆”装进包裹Swarm 这类框架最值得借鉴的设计是显式的上下文变量传递机制。你可以把context_variables理解为一个“随 Agent 流转的便携包裹”。用户一开始说自己姓王、在北京这条信息放进包裹里之后不论 Agent A、B 还是 C只要拿到 Handoff 的控制权都可以读到这些基础资料。这就解决了之前提到的“全局状态交汇”问题信息不是放在一块所有 Agent 都能乱涂乱画的白板上而是放在一个被明确传递的、带键值结构的数据包里。实际编码里每次 Handoff 都会携带当前的context_variables。因此只要代码里没有覆盖这个变量下一个 Agent 启动时就能读到上一个 Agent 写入的字段。这和直接在系统提示词里拼接历史消息的最大区别是历史消息是一维的文字流模型得像人一样逐字找信息而context_variables是结构化的键值映射你可以让 Agent 只读取它关心的字段剩下的全部忽略。4.3 三个必须注意的配置细节第一context_variables通常是浅拷贝如果你往里面放了一个嵌套对象比如“用户订单列表”是一个list或dict在 Agent 内部修改这个对象的某个字段是有可能影响外部原始数据的。想去掉这个隐患必须在写入前做深拷贝或者干脆约定所有 Agent 只能整体替换字段值不允许修改嵌套内容。第二Handoff 时模型切换要匹配上下文窗口。不同 Agent 可能挂了不同的模型服务有的窗口 4K有的窗口 32K。如果 A 在 context_variables 里放了 8K 的长文档而 B 用的是 4K 窗口的模型那 B 要么截断要么请求直接失败。解决的办法是在 Handoff 前先做裁剪只传输对方真正需要的字段。第三系统提示词的语言风格要和上下文层级匹配。基础指令层用书面语、简洁命令式共享信息层用结构化的数据描述而临时交互层可以让 Agent 用自己的自然语言草稿。如果三层混着一股脑塞进去模型容易被临时视角带偏忘了系统级的目标。5. 实战搭一套不会“失忆”的三步订单查询助手5.1 整体设计思路做一个极简的流程来演示上面的所有思想。这个项目叫“三步订单查询助手”涉及的 Agent 有两个前台 Agentfront_agent负责识别用户问题。用户说“帮我看看我上周买的那双鞋发货没有”它需要把“鞋子”识别成具体商品关键词并尝试找到用户手机号。后台 Agentback_agent负责查询订单状态。它不负责理解复杂意图只负责调用 API在拿到订单号后返回物流信息。想让这个协作不“失忆”我们需要的上下文流是用户最初的会话消息全程保留在主循环里context_variables里存有用户基础资料与查询条件Handoff 时把查询条件传给后台 Agent。后台 Agent 的结果再写入上下文最后交回前台 Agent 做面向用户的回答。整个过程中前台 Agent 不会丢失用户说过的话后台 Agent 也不会丢失自己需要的参数。5.2 可复现的代码结构我习惯用 Python 写一个最小实现便于在命令行里直接跑通再换成正式框架。下面是核心代码没有包含具体框架依赖用函数模拟 Agent 的编排流程# 模拟多Agent协作时的上下文传递 import json # 全局会话消息列表模拟应用持久化的聊天记录 messages [] # context_variables 结构携带用户基础资料和各Agent协作中的关键状态 context_variables { user_profile: { name: 小王, mobile: 13800138000 }, query_condition: {}, query_result: None } class Agent: def __init__(self, name, instructions): self.name name self.instructions instructions def run(self, input_text, context_variables): # 实际场景中这里调用大模型 API执行工具调用 # 此处用模拟返回演示逻辑 if self.name front_agent: return self._run_front(input_text, context_variables) elif self.name back_agent: return self._run_back(input_text, context_variables) def _run_front(self, input_text, context_variables): # 前台Agent识别用户意图后把结构化查询条件写入context_variables print(f--- {self.name} ---) condition { keyword: 运动鞋, time_range: 最近7天, user_mobile: context_variables[user_profile][mobile] } context_variables[query_condition] condition # 将关键结构化信息追加到会话消息 messages.append(ffront_agent 已提取查询条件{json.dumps(condition, ensure_asciiFalse)}) # 返回Handoff指令把控制权交给后台Agent return handoff_back_agent, context_variables def _run_back(self, input_text, context_variables): # 后台Agent只读取它需要的字段不需要重复理解用户原话 print(f--- {self.name} ---) condition context_variables[query_condition] # 模拟第三方物流API查询 result { order_status: 已发货, logistics_company: 顺丰速运, tracking_no: SF1234567890, eta: 明天下午送达 } context_variables[query_result] result messages.append(fback_agent 已查询到结果{json.dumps(result, ensure_asciiFalse)}) return handoff_front_agent, context_variables # 主循环模拟用户会话交互 def main(): front Agent(front_agent, 负责理解用户意图提取查询条件) back Agent(back_agent, 负责调用物流查询API返回订单状态) # 用户首次说出的原话 user_input 帮我看看我上周买的那双鞋发货没有 print(f\n用户: {user_input}) messages.append(f用户: {user_input}) # 第一轮前台Agent接手 action, ctx front.run(user_input, context_variables) # 第二轮根据Handoff交接给后台Agent if action handoff_back_agent: # 把用户原始提问和结构化条件一起传给后台 # 后台Agent只读取ctx[query_condition]避免读整段历史而迷失 action, ctx back.run(ctx[query_condition], ctx) # 第三轮回到前台Agent完整汇总返回用户可读答复 if action handoff_front_agent: result ctx[query_result] reply f您的商品已发货物流公司是{result[logistics_company]}单号{result[tracking_no]}预计明天下午送达。 print(f\n助手: {reply}) # 打印完整的消息列表确认上下文从未断裂 print(\n 本次会话的完整上下文记录 ) for msg in messages: print(f {msg}) if __name__ __main__: main()这段代码跑起来的输出大概是这样的用户: 帮我看看我上周买的那双鞋发货没有 --- front_agent --- --- back_agent --- 助手: 您的商品已发货物流公司是顺丰速运单号SF1234567890预计明天下午送达。 本次会话的完整上下文记录 用户: 帮我看看我上周买的那双鞋发货没有 front_agent 已提取查询条件{keyword: 运动鞋, time_range: 最近7天, user_mobile: 13800138000} back_agent 已查询到结果{order_status: 已发货, logistics_company: 顺丰速运, tracking_no: SF1234567890, eta: 明天下午送达}5.3 这段代码里做了哪些“防失忆”设计一是结构化信息覆盖原始文本。前台 Agent 不直接回传用户原话而是把原话提炼成query_condition字典后台 Agent 读取时不需要再解析“上周买的那双鞋”这种模糊表达看到的直接就是keyword: 运动鞋, time_range: 最近7天。模型处理结构化字段比处理散文要稳定得多。二是每个 Agent 读上下文时只读自己关心的部分。后台 Agent 的函数签名是_run_back(input_text, context_variables)但它实际只用context_variables[query_condition]不去扫描用户历史里的无关消息。这样就绕开了“上下文里塞太多无关信息导致模型抓不住重点”的问题。三是全局消息列表只做审计追踪不做 Agent 的输入。代码里的messages列表作用是记录整个会话的完整流水方便开发者排查问题它不是喂给模型的输入。这很关键。很多新手的错误就在于把messages既当数据库又当模型输入最后模型被历史里前面几轮的噪声干扰得不轻。按这里的写法真正喂给模型的输入由每个 Agent 基于自己的职责自己组装messages只是留给人的审计日志。6. 常见问题与排查技巧实录6.1 对话串台Agent A 读到了 Agent B 的上下文症状Agent A 的回答里出现了只有 Agent B 才知道的信息或者措辞风格突然变成了 B 的语气。排查方向先看两个 Agent 是不是共享了同一个列表或字典。如果你把context_variables和messages定义为模块级全局变量并允许任意 Agent 直接读写那串台是必然的。解决方法是给每个 Agent 分配独立的局部字典只保留显式传递公共信息的入口。说白了全局变量越少串台概率越低。6.2 切换账号或切换模型后原对话一直加载不回来这个现象在本地部署场景特别常见你本地搭了一个模型服务通过某个配置管理工具切换了模型供应商或账号再打开原来的对话页面发现历史消息没加载出来页面上还会一直跳闪、转圈。我排查过几次根因多半是会话加载逻辑绑定在“某一次运行时的进程内存”上或者切换配置时把会话标识session id也换了。前端拿新会话 ID 去查历史当然找不到旧记录。解决办法是让会话记录全部落库并以用户唯一标识或固定的会话标识为键。切换模型、切换账号都不影响键的稳定性历史自然就能加载出来。至于页面跳闪一般是前端拿到空历史后反复重试导致的直接改成空历史时快速路由到新会话就可以。6.3 大模型上下文窗口用完了怎么办如果你用的模型窗口是 8K 或 16K长会话跑着跑着就会触顶。触发后轻则丢失最早的消息重则接口直接报错。处理优先级我建议按下面的顺序来截断把最早期的对话从输入里去掉只保留系统提示词和最近 N 轮消息。优点是好实现缺点是长任务的早期关键信息可能会丢。摘要每过 N 轮就调用一次模型把之前的对话压缩成 200 字的摘要作为后续轮次的背景信息。摘要策略很适合“用户说了很多需求但最终目标一致”的场景。检索把历史消息向量化放到向量库里每次请求只取相关的 top K 条。适合知识库问答式应用但前期工程成本不低。6.4 上下文明明传递了Agent 还是没有用上最后一种情况最气人代码里明明把字段传给了下一个 Agent但它的输出里完全没有利用这些信息。这时候不要怀疑模型笨先检查你的指令模板里有没有明确告诉它“你可以使用这些字段”。很多系统提示词只写了“请完成订单状态查询”却没有写“用户手机号在 context_variables.user_profile.mobile 中订单关键词在 context_variables.query_condition.keyword 中”。模型默认不会自己去深挖上下文里有什么。想让 Agent 用上传递的字段你应该在系统提示词里用一句很直白的引用方式给出来比如“查询订单时所需的全部参数请从上下文中的 query_condition 对象中提取”。我最后想分享一个实际感悟做多 Agent 项目的正确顺序先是信息流设计再是上下文管理最后才是模型选型。每次开工前先用一张纸画出“谁生产了什么信息谁消费了什么信息信息以什么格式传递”把这张图画清楚再动手写代码。模型能力再强也架不住信息从上游到下游这一路上丢三落四。反过来只要信息流设计得干净哪怕用的是一个普通参数的模型整体效果通常也不会差。这套方法值得你直接放进下一个项目里试一试。