一个多月前我在做一个多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系统有四个AgentPlanner、Searcher、Critic、Writer。一次任务大约需要三轮协作第一轮Planner输出任务拆解方案约800 tokens。第二轮Searcher返回三段资料摘要约1500 tokensCritic提出两个质疑约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发送方Agentreceiver目标Agent或广播标记msg_type消息类型指令、结论、疑问、状态报告、工具结果msg_id唯一IDtimestamp时间戳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和WriterCritic不会看到。这相当于给会议室配了一个秘书谁该收到哪份文件秘书来分派。我在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能看到的上下文决定了它是否有能力做好自己的工作。把上下文组织好系统稳定性和输出质量都会上一个台阶组织不好模型再强也会在混乱的信息流里“发挥失常”。几年下来我的体会很直接这个环节花的时间值。
