一个多月前,我在做一个多Agent协作的原型项目,三个Agent分工:一个负责拆解任务,一个负责查资料,一个负责写结果。最开始跑通Demo的时候感觉还挺顺畅,可一旦把任务复杂度提上去,问题立刻来了——负责执行的Agent经常“忘记”前面的结论,负责查资料的Agent会把一堆无关背景塞给下游,最后负责写结果的Agent输出了一篇看似完整、实则前后矛盾的方案。我花了一个多星期排查,最后发现问题根源不在任何一个Agent的模型能力上,而在上下文组织上。
这个标题下要聊的,就是多Agent系统中上下文如何组织。它解决的核心问题是:多个Agent共享同一套大模型底座时,消息如何在它们之间流动、保留、裁剪和检索,才能在有限的上下文窗口里保证关键信息不丢、不串、不膨胀。这套东西适用于任何基于大模型的多Agent框架——不管你是用LangGraph、AutoGen、CrewAI还是自研编排层,底层思路都通用。
1. 为什么多Agent系统一定会栽在“上下文”上
1.1 单Agent的“读书逻辑”和多Agent的“开会逻辑”
先说个直觉对比。单个Agent的工作方式像一个人读书:你从头到尾读一本书,读到后面忘了前面,就往前翻几页刷新记忆,但整体上是一个线性的信息流,上下文的管理相对简单——塞得下的就塞,塞不下就丢早先的。
多Agent系统完全不是这个逻辑。几个Agent聚在一起,更像一群人开会。每个人有自己的笔记本,会议室有块公共白板,有人在白板上写字,有人在小本子上记录。如果你在开会时把所有人的小本子、白板、甚至桌上所有草稿纸全部摊开堆在一起,信息量会迅速爆炸,而且大多数内容对当前正在发言的人是噪音。
我踩的第一个坑,就是把所有Agent的历史消息简单拼接成一个长上下文喂给下一个Agent。刚开始只有两三个Agent,还能凑合;加到五个以上,每个Agent每轮输出一两千字,一轮对话下来上下文就直逼窗口上限。更麻烦的是,某个Agent的中间思考过程被塞进了共享上下文,下游Agent把它当成了事实结论,导致最终结果出现偏差。
1.2 消息风暴:上下文窗口是怎么被“烧”光的
来算一笔账。假设一个多Agent系统有四个Agent:Planner、Searcher、Critic、Writer。一次任务大约需要三轮协作:
- 第一轮:Planner输出任务拆解方案,约800 tokens。
- 第二轮:Searcher返回三段资料摘要,约1500 tokens;Critic提出两个质疑,约600 tokens。
- 第三轮:Writer汇总生成最终结果,约1500 tokens。
表面看,每一轮的量都不大,单轮最多一两千tokens。但如果你把每一轮里所有Agent的原始输出都累积保留,三轮之后上下文里已经有这些历史消息的总和,再加上系统提示词、工具返回结果、Agent各自的角色设定,轻轻松松就超过一万tokens。这还只是一次简单任务。
如果任务流程更复杂,Agent之间来回反馈五次、六次,上下文累积速度是指数级的。我见过最夸张的一次,一个五Agent系统跑了大概十分钟的复杂任务,最后上下文里累积了近四万tokens的原始消息,其中真正对最终结果有用的,可能只有五六千tokens。其他全是中间过程的“会议记录”。
这就解释了为什么多Agent系统跑着跑着会变笨——不是模型不行,而是有效信息被淹没在大量中间消息里,模型在有限窗口里能注意到的比例越来越低。
1.3 注意力稀释:上下文越长,关键结论越容易被忽略
大模型的注意力机制决定了,它处理长上下文时不是每个位置都同等关注。虽然各家模型都有针对长上下文的优化,但实践中有一个很明显的现象:如果一个关键结论出现在第七轮对话、被夹在三段无关讨论之间,下游Agent回复时经常忽略它;而这个结论如果出现在最新一轮、且以简洁明确的结构呈现,几乎是百分之百会被采纳。
我把这个现象叫做“中间失忆”。它带来的后果很典型:Critic在第二轮提出了一个重要风险,Planner也在第四轮确认了这个风险,结果Writer在最终生成时完全没提。不是Writer不聪明,而是这个信息被埋在了太长的历史上下文里,它的信号强度已经不敌那些最新鲜、最靠近输出位置的内容。
所以多Agent系统的上下文组织,核心目标不只是“塞得下”,而是“关键信息要在正确的时间、以正确的位置、出现在正确Agent的上下文里”。这比单纯压缩长度难得多,但只有做到这一点,系统才会真正稳定。
2. 上下文分层:把Agent的对话从“大杂烩”变成“三张桌子”
2.1 全局共享上下文——团队的“宪法”
我后来调整思路,把多Agent的上下文按功能拆成三个层次。第一个层次是全局共享上下文,它在整个任务生命周期内基本不变,可以看作整个Agent团队的“宪法”。
这个层次里放的是:
- 任务的最终目标和验收标准
- 所有Agent都要遵守的工作协议(比如“不要编造数据”“所有结论必须标注来源”)
- 团队的角色定义和职责边界
- 全局可见的关键约束(比如“用户预算上限五万元”“只允许使用已授权数据源”)
为什么这些内容要拿出来单独一层?因为它们解决的是“每个Agent打开自己的上下文时,都能看到同一份权威信息”。如果把这些内容混在对话历史里,Agent越往后跑越容易遗忘最初的目标——我见过Searcher埋头查了两轮资料后开始放大搜索范围,完全偏离了Planner最初拆解的子任务。原因就是初始任务描述在上下文里被后续消息不断挤压,最后在注意力层面“失联”了。
全局共享上下文不需要每轮都完整重放,但至少要在每个Agent每次被唤醒时,以固定前缀的方式置于其上下文最靠前的位置。开头位置的信息,模型注意力最集中,这是我能给“宪法”找到的最好位置。
2.2 私有个体上下文——每个人的工作台
第二个层次是私有个体上下文。每个Agent保留自己工作过程中的状态记录、中间结论、待办事项,这些内容只属于它自己,不广播给其他Agent。
为什么需要私有层?因为每个Agent工作的“过程信息”和“结果信息”是两码事。比如Searcher在查询过程中的三个候选关键词、两次试错、一个被否决的搜索策略,这些过程信息对Searcher自己复盘有用,但对Writer毫无价值。Writer需要的只是最终结论——“已找到三条可用资料,分别来自哪里”。
如果把过程信息和结果信息全部丢进共享对话历史,观众的体验就是:本来想看一集精炼的电视剧,结果每一集前面都附带两个小时的幕后花絮。信息密度太低。
我现在用LangGraph实现Agent节点时,会把私有状态存在节点自己的State字段里,比如用一个字段存process_trace(过程轨迹),用一个字段存output_message(对外输出)。这个过程轨迹只在自己节点内部读取,绝不广播。只有当Agent需要对外交付结果时,才会把精简后的output_message写到共享通道。
2.3 临时交互上下文——会议白板
第三个层次是临时交互上下文,也就是Agent之间真正交换消息的地方。我把这一层比作会议室里的白板——上面只保留当前这轮讨论相关的内容,讨论完就擦掉,或者换新的一张,而不是把所有历史讨论内容都贴在墙上。
这个层次最常见的实现方式就是消息队列或消息总线。每个Agent产生的对外消息进入总线,需要这些消息的Agent订阅并消费。消费之后两条路:消息携带的关键结论进入接收方的“工作台”,原始消息本身则可以被归档或者丢弃,不进入长期上下文。
这样组织带来了一个明显的好处:任何时刻,Agent上下文里的“白板区域”都只包含最近一两轮的交互消息,而不是从第一轮到现在所有的对话逐字记录。上下文长度从线性增长变成了有上限的、可管理的长度。
2.4 分层之后带来的直接改善
我按这三层重构了项目之后,拿同一批测试任务重新跑了一遍,最直观的变化是:上下文体积下降了大约60%,而最终输出的质量评分反而提升了将近三成。
原因很简单。分层之后,每个Agent拿到的是“一份宪法(全局)+ 自己的工作笔记(私有)+ 最近的白板记录(共享交互)”,而不是一份杂乱无章的千页会议纪要。模型不需要在噪音里摸索关键信息,它一上来就看到目标、约束、最近进展,输出稳定性自然就上去了。
3. 动手做Token预算:给每个Agent发“本月工资”
3.1 先算总预算:窗口不是全都拿来装对话的
很多人在设计多Agent系统时,想当然地把模型上下文窗口的Token数当作可用的“对话预算”。比如模型支持128K上下文,就觉得最多能往里面塞128K的对话内容。这个想法是我踩过的最贵的一个坑。
实际上,每次请求的Token消耗包含几个固定部分:
- 系统提示词和全局上下文(供所有Agent共享的那份“宪法”)
- Agent的角色设定和行为规范
- 工具调用的函数定义
- 当前对话消息
- 模型输出的预留位置(输出Token也要占用一部分窗口)
以一个128K窗口的模型为例,我通常的预算是这样分的:
| 占用项 | 预估Token | 说明 |
|---|---|---|
| 全局共享上下文 | 3000-5000 | 任务目标、角色定义、工作协议 |
| 工具函数定义 | 2000-4000 | 搜索、代码执行、数据库等工具的schema |
| 当前Agent私有状态 | 1000-3000 | 工作台笔记、中间结论 |
| 临时交互消息 | 4000-8000 | 最近一两轮的共享对话 |
| 输出预留 | 4000-8000 | 留够生成空间,避免截断 |
| 安全余量 | 2000-4000 | 不可预见的工具返回、异常信息 |
| 可用动态区 | 剩余部分 | 任务相关的动态数据 |
算下来,真正能留给动态对话内容的空间,往往是窗口的一半左右,甚至更少。所以我的习惯是:先给各个固定项做预算,再设定一个所有Agent共享的“总动态断言”——任何Agent写入上下文的消息,必须是在这个预算内的,超了就触发裁剪。
3.2 三级优先级模型:什么该留、什么该扔
有了总预算,下一步就是给上下文里的信息排优先级。我用的是一套简单的三级模型:
第一优先:协议数据。包括任务目标、当前阶段、验收标准、上一步传给本Agent的明确指令。这一类信息不允许被裁剪,任何情况下都必须完整保留在上下文中。
第二优先:任务关键信息。包括检索到的核心资料、其他Agent给出的关键结论、当前正在处理的具体数据。这类信息要尽量保留,但如果预算紧张,可以压缩(从原文变成摘要)而不是直接删除。
第三优先:辅助背景。包括推理过程、试错记录、历史版本、已被后续结论覆盖的旧信息。这类信息默认不进入上下文,只在当前步骤需要时才临时拉取。
这套优先级模型为什么有效?因为多Agent系统的绝大多数上下文浪费,都发生在“第三优先”的信息被当成“第二优先”甚至“第一优先”来处理。只要你在设计阶段明确:过程信息不得广播、旧结论被新结论覆盖后立即从共享区移除,预算就能省下一大半。
3.3 实战裁剪策略:滑动窗口、轮次级摘要、关键信息抽取
预算定好了,接下来是具体的裁剪手段。我实际用下来,有三种方法组合效果最好。
第一种是滑动窗口。只保留最近N轮交互消息,更早的消息不管是否有用,直接移出共享上下文。这个策略简单粗暴,但有效。缺点是如果某个重要结论发生在很早的轮次,滑动窗口会把重要信息一起裁掉。所以滑动窗口通常不能单独用,要配合下面两种方法。
第二种是轮次级摘要。每轮协作结束后,用一个专门的“摘要器”(可以是一个单独的LLM调用)把这一轮的对话压缩成3-5条结构化结论,替换掉原始对话存入上下文。这样做有两个好处:一是压缩了体积,二是能提炼出跨Agent一致认可的结论,减少信息矛盾。
我在项目里会给摘要器一个固定模板,要求它按“本轮结论、涉及Agent、待办事项、风险提示”四项输出,用Markdown结构呈现。这样后续Agent读取摘要时,不需要语义理解就能快速定位“有没有与我相关的风险”。
第三种是关键信息抽取。不是所有内容都适合摘要处理,比如代码片段、具体数据数值、引文原文,压缩后容易失真。对这类内容,我会单独抽取成结构化条目,保留在Agent的工作台里,而不是放进共享摘要。举个例子,Searcher找到了一组统计数据,摘要器可以写“找到了来自XX报告的数据,显示增长率为X%”,但数据的完整数值和来源链接要单独存到结构化的memory字段,等Writer需要引用时再精确取用。
3.4 避坑:摘要替换的“递归坍缩”——越缩越没细节
这里必须展开讲一个坑。轮次级摘要用久了,会出现一个现象叫“递归坍缩”,我吃了不少苦头才意识到这个问题。
场景是这样的:第一轮协作产生了大量细节,摘要器压缩成了800字的摘要。压缩后,第二轮协作要引用第一轮的内容,继续基于这份摘要推进,第四轮协作用时,上下文里只保留了对“摘要的摘要”的引用。每一轮压缩,都会丢掉一部分细节。三轮之后,上下文里的描述可能只剩“已找到相关数据,结论待确认”这种空泛的表述,完全失去了可执行的信息量。
我排查这个问题时发现,根本原因是摘要链的每一环都在做“有损压缩”,而设计时没有保留“无损关键信息”的通道。一个数据数值,从原始1000字压缩到80字再压缩到20字,精度损失远超预期。
解决办法是把压缩和提取分开:摘要器只压缩那些可容忍信息损失的叙述性内容,而所有数值、引用、ID、状态标记这些“可结构化”的信息,要走单独的结构化提取通道,原样保留在工作台或长期记忆里。叙述可以减,数据不能丢。
4. 跨Agent消息协议:别让Agent说“人话”,让它们说“结构化的话”
4.1 自然语言消息的三宗罪
刚开始设计多Agent系统时,我让Agent之间直接用自然语言对话,觉得这样最灵活。但很快发现自然语言消息在跨Agent场景下有三个严重问题。
第一是语义歧义。Agent A说“我查了一下,效果还不错”,这个“效果不错”在Agent B那里可能被理解成“可以正式使用”,但A本来想表达“初步看起来有潜力,还需要验证”。同一句话,不同模型角色和不同上下文背景下,解读方向可能南辕北辙。
第二是上下文隐含。Agent之间用自然语言对话时,一句话的完整含义依赖大量未显式写出的背景。A说“用上次的方案就行”,“上次的方案”如果不在当前上下文里,B就全靠猜。结果往往是B采用了A根本没想表达的方案。
第三是体积膨胀。自然语言天然会带一些客套语、过渡句、重复强调。单条消息可能多出30%-50%的无效Token。在多Agent高频交互场景下,这些Token累积起来非常可观。
4.2 一条可靠消息长什么样
后来我参考消息协议的设计思路,给Agent之间的交互定义了结构化消息格式。一条完整的消息包含三个部分:
header部分记录消息的基本元信息:
- sender:发送方Agent
- receiver:目标Agent或广播标记
- msg_type:消息类型(指令、结论、疑问、状态报告、工具结果)
- msg_id:唯一ID
- timestamp:时间戳
- relation:关联的上一条消息ID(追踪对话链)
body部分承载核心数据:
- content:核心内容,尽量用结构化格式书写
- structured_data:关键数据字段(JSON格式,用于传递数值、状态、引用等精确信息)
- references:引用来源列表(便于溯源,又不需要把全部引用原文粘贴进来)
metadata部分记录消息的使用约束:
- ttl:这条消息在共享上下文中保留多久(可以按轮次或时间设定)
- priority:优先级标记(对应前文的优先级模型)
- archive_policy:归档策略(结束后落盘到长期记忆,还是任务完成即删)
我实际用下来的感受是,设置这些字段不会让代码显得复杂,反而会逼着每个Agent在生成消息时把“结论”和“依据”分离。指令类消息必须给出明确指令字段,结论类消息必须给出置信度和来源字段,疑问类消息必须明确列出等待回复的对象。这相当于给Agent上了一套“表达纪律”。
4.3 路由与事件总线:消息该给谁,不该给谁
有了结构化消息,接下来要解决的问题是路由。不是每条消息都该广播给所有Agent。信息发送给不需要它的Agent,不仅浪费Token,还会造成干扰——无关Agent可能把与己无关的消息当重要背景,反而影响判断。
我使用的是事件总线模式,每个消息在header里声明接收方,总线根据receiver字段做定向投递。对于确实需要全员感知的消息,才使用广播类型。
如果消息不是定向投递,而是一个Agent发生了某个事件、可能对多个Agent有用,这时候更可靠的做法是让总线根据“订阅规则”转发,而不是盲目广播。比如Searcher的搜索结果,只有Planner和Writer关心,Critic通常不关心,那么总线就把这条消息转发给Planner和Writer,Critic不会看到。这相当于给会议室配了一个秘书,谁该收到哪份文件,秘书来分派。
我在LangGraph里实现这一点的时候,会给每个节点注册一个消息处理函数,声明它接受哪些类型的消息。总线维护一个简单的消息类型到接收节点的映射表,每次消息进来查表投递。这个思路跟微服务里的消息队列非常像,只是规模小得多。
4.4 基于消息协议做记忆更新
消息协议还能顺手解决记忆更新的问题。当一条消息被标记为“结论”时,总线会自动触发一次记忆写入:把结论中的structured_data字段提取,合并到全局共享上下文的“当前结论集合”里。当一条更新的结论消息发出,旧的结论被标记为superseded(被取代),从共享区移出、归档到长期记忆。
这样做的好处是,上下文里始终只有“最新可用的结论集”,不会出现“A轮说不能用,B轮说可以用,最后Writer同时看到两条矛盾结论”的混乱情况。
5. 跨任务的长期记忆:怎么让Agent记住该记住的
5.1 长期记忆的分区:向量库、事实表、技能库
前面讲的都是单次任务内的上下文组织。但多Agent系统真正要稳定服务用户,还有一个跨任务的问题:Agent如何记住之前任务中积累的经验。
我把长期记忆分成三个区:
向量库区存放可检索的语义记忆,比如“之前处理过类似的报表生成任务,当时用了三步方案”。这类记忆是模糊的、启发式的,适合用向量检索按相似度召回。
事实表区存放精确的结构化事实,比如“用户ID 123对应的项目预算上限是10万元”“上次任务最终选用了数据库A而不是B”。这类记忆必须是精确的、不可模糊的,适合用结构化存储,检索时直接命中。
技能库区存放可复用的流程技能,比如“处理PDF细表时的固定流程:先解析目录,再按章节抽取页码范围,最后按需求切段”。这类技能是Agent自己从成功任务中沉淀出来的“解题套路”,存的时候要附带适用条件和效果评估。
分区的好处是各取所长。向量库解决的问题是“我好像见过类似的情况”,事实表解决的问题是“那个具体数字是什么”,技能库解决的问题是“这类事上次怎么干成了”。
5.2 检索时机比检索方法更关键
长期记忆的检索,最大的坑不是检索算法不够好,而是检索时机太随意。我之前犯过的错误是:Agent每次被唤醒时都从长期记忆里捞一批相关信息,结果捞出来的常常是陈旧经验,干扰了当前任务。
后来我改成了显式的检索触发机制:
- 任务启动时:Planner节点检索一次长期记忆,主要用途是参考历史类似任务的处理方案。
- 关键决策点:当某个节点需要做决策时,主动调用检索,获取与当前决策相关的历史事实。
- 任务结束总结时:此时把本次任务的关键信息写入长期记忆。
不要在每一轮对话中都检索记忆。检索本身会消耗Token,而且检索到的内容如果在当前上下文里迟迟得不到应用,还会稀释注意力。
5.3 写回记忆前的“质量闸门”
写回比检索更容易被忽视,但同样重要。如果任务结束后直接把所有内容都写入长期记忆,很快记忆区就会充满垃圾——一个未完成的中间状态、一次失败的尝试、一条早被推翻的过时结论,都会污染后续任务。
我给记忆写入设置了三道闸门:
结论校验闸门:只有被至少两个Agent确认过的结论,才允许作为事实写入事实表。
去重更新闸门:写入事实表前,先检查是否已存在同一实体下的旧纪录。如果存在,要么更新旧纪录,要么标记旧纪录失效,而不是重复插入一条新数据。
技能评估闸门:技能入库前必须附带成功率评估。如果一次流程执行后最终结果被Critic判定为不合格,这个流程就不允许写入技能库;只有那些经过验证的方案,才允许沉淀为技能。
这套闸门听上去简单,但能显著提升长期记忆的信噪比。我用过一个月,最明显的感受是:检索结果的实用性大幅提升,Agent越来越少把记忆区里的陈旧信息当宝。
6. 一套可以直接抄的落地结构(LangGraph示例)
6.1 状态对象怎么设计
到这里原理就基本讲完了,最后给一套可以直接落地的结构。我目前项目的LangGraph状态对象大致长这样:
from typing import TypedDict, List, Optional, Any from langgraph.graph import StateGraph, END class AgentMessage(TypedDict): msg_id: str sender: str receiver: str # "planner" / "searcher" / "critic" / "writer" / "broadcast" msg_type: str # "instruction" / "conclusion" / "question" / "report" / "tool_result" content: str structured_data: Optional[dict] references: Optional[List[str]] ttl_rounds: int priority: int class AgentState(TypedDict): # 全局共享上下文 global_context: dict # 当前任务的临时交互消息(白板区) shared_messages: List[AgentMessage] # 各Agent私有工作台(用agent_name做key) private_worktables: dict # 长期记忆检索结果缓存 memory_retrieved: List[dict] # 当前任务的最终输出 final_output: Optional[str]这个状态对象的设计核心是:全局和私有分开、消息带协议字段、长期记忆单独缓存。每个节点在运行时只关注与自己相关的部分,不会被其他Agent的私有过程数据干扰。
6.2 关键节点的上下文构建逻辑
每个Agent节点被调用前,都会经过一个上下文构建函数,它把状态里的各部分拼装成该Agent本次运行的实际Prompt。这个拼接顺序是固定的:
def build_agent_prompt(agent_name: str, state: AgentState) -> str: # 第一层:全局共享上下文(宪法) prompt_parts = [render_global_context(state["global_context"])] # 第二层:当前Agent的私有工作台 prompt_parts.append(render_private_worktable(state["private_worktables"][agent_name])) # 第三层:与该Agent相关的最近交互消息(只取白板区的最近两轮) relevant_msgs = filter_relevant_messages(state["shared_messages"], agent_name) prompt_parts.append(render_recent_messages(relevant_msgs)) # 第四层:按需检索的长期记忆 if should_retrieve_memory(agent_name, state): prompt_parts.append(render_memory(state["memory_retrieved"])) return "\n\n---\n\n".join(prompt_parts)为什么是这个顺序?因为注意力分布天然偏向开头和结尾。全局宪法必须放在最开头,才能被最高优先级地注意;最近消息放在靠后位置,保证新鲜度;中间放私有工作台,它是Agent执行当前步骤的直接依据。长期记忆最后按需插入,一旦用完就撤出,避免长期占位。
6.3 实测对比:组织前后效果差异
我把这套结构应用到项目后,用同一批复杂任务做了对照测试。任务类型是“给定主题,由多个Agent协作完成一份调研报告”,每组跑五遍,取平均结果:
| 指标 | 上下文未组织前 | 上下文分层+协议后 |
|---|---|---|
| 单任务平均上下文体积 | 约37000 tokens | 约13000 tokens |
| 最终报告信息完整度 | 67%(经常漏关键结论) | 93% |
| 关键结论传递准确率 | 约70% | 95%以上 |
| 单任务耗时 | 约5分半 | 约3分半 |
体积下降是意料之中,最让我意外的是信息完整度的大幅提升。过去Writer经常漏写一些埋在前几轮的结论,重构之后几乎不再出现。这说明多Agent系统的问题往往不是你选了个多强的模型,而是模型拿到的上下文是否真的“拎得清”。
6.4 这条路上我还要继续踩的坑
虽然这套结构解决了我项目里的大部分问题,但有两个方向我还在继续摸索。
第一个是动态路由的智能化。目前我的路由是靠订阅规则硬编码的,Agent角色固定、消息类型固定,所以问题不大。但如果Agent角色本身是动态生成的(比如系统根据任务临时创建新角色),路由规则就要跟着动态调整,这需要更灵活的设计。
第二个是多Agent系统的上下文监控。现在我是靠日志和事后分析来定位上下文问题的,比较费时。我在考虑开发一个监控节点,实时统计每个Agent上下文中各类信息的占比、关键结论是否出现在预期位置、Token预算是否有超支预警。这样不用等任务跑完报错,跑的过程中就能发现隐患。
多Agent系统的上下文组织,本质上不是单纯的技术优化,而是对“Agent团队协作方式”的设计。每个Agent能看到的上下文,决定了它是否有能力做好自己的工作。把上下文组织好,系统稳定性和输出质量都会上一个台阶;组织不好,模型再强也会在混乱的信息流里“发挥失常”。几年下来我的体会很直接:这个环节花的时间,值。