ARTICLE DETAIL

资讯详情

深耕网站视觉设计与运营推广的一线实战洞察。

Multi-Agent系统上下文组织实战:从混乱到可控的关键设计

Multi-Agent系统上下文组织实战:从混乱到可控的关键设计 接手过一个看起来不那么复杂的Multi-Agent项目三个Agent协作写一份市场分析报告。结果上线第一周就翻车了——A刚刚确认过的结论B还在用十几轮之前的老数据C为了补全信息把自己的上下文越撑越大最后连系统语气都变得奇怪。后来我才意识到Multi-Agent系统能不能稳定跑起来真正决定下限的往往不是模型选得多大而是上下文怎么组织。这几年Multi-Agent的热度一直没降LangGraph、CrewAI、AutoGen这些框架都有自己的Agent间通信机制但很多方案在玩具Demo里很好用一放到真实任务里就会暴露出上下文混乱问题。这篇文章就以我自己的实战经验为主聊聊Multi-Agent里的上下文组织它到底解决什么问题、常用方案长什么样、怎么落地以及那些文档里不会写但特别关键的坑。适合正在搭建多Agent应用或者已经跑起来但总觉得Agent“各说各话”的朋友。1. Multi-Agent系统的上下文组织到底在解决什么问题1.1 记忆、视野与一致性上下文组织的本质先说一个容易混淆的点上下文不等于Prompt长度。很多人觉得 Multi-Agent 的上下文管理就是“怎么把内容塞进窗口”实际上更核心的问题是系统运行时每个Agent能看见什么、看不见什么以及不同Agent之间对同一个事实的理解是否一致。单个Agent的应用里上下文基本等同于“把聊天记录、系统提示、参考文档拼在一起”模型每次推理都基于这份拼接后的输入。到了Multi-Agent阶段情况复杂得多每个Agent有自己的上下文窗口Agent之间还会互相传递信息。如果只是简单地把所有东西堆到一起很快就会出现三类问题——信息重复、信息冲突、信息失焦。打个生活化的比方。办公室一个项目组里有策划、设计、开发三个人如果所有会议纪要和邮件都抄送给所有人谁也没办法读完如果各干各的A改了一个需求B不知道C还在按旧需求做。上下文组织要解决的本质上就是办公室信息流的“该放桌面上的放桌面、该塞抽屉里的塞抽屉、该写进会议纪要的写进纪要”。所以我的理解是Multi-Agent系统的上下文组织核心目标只有三个——记忆可控、视野清晰、状态一致。记忆可控是说历史信息按需保存视野清晰是每个Agent只看到自己该看的部分状态一致是共享的全局事实在所有Agent心里是同一个版本。1.2 为什么Multi-Agent比单Agent更依赖上下文组织单Agent时代上下文管理可以“粗暴但有效”。窗口不够就截断信息丢了就多问几轮。但Multi-Agent场景下同一个任务被切分成多个角色协同完成信息链路变长问题会被放大。我归纳了一下最常见也最致命的三类问题第一类是上下文漂移。Agent各自基于自己的上下文做推理当共享事实在两个Agent的上下文里以不同版本存在时它们得出的结论就会互相矛盾。比如一个客服系统里意图识别Agent发现用户要退款但订单查询Agent的上下文里还保留着“用户确认收货”的旧结论两个Agent就可能会给用户完全不同的回复。第二类是信息孤岛。A掌握的关键信息B完全不知道。很多时候并不是模型能力不行而是B压根没看到那条信息。这个在真实项目里出现的频率远超预期表现通常是——Agent会用“根据你提供的信息”之类的话来做无根据的假设或者直接答非所问。第三类是状态不同步。多Agent协同的任务往往有阶段收集信息、分析、执行、校验。如果“当前进行到哪一步”这个状态没有被统一维护每个Agent会按照自己的理解去推进轻则重复劳动重则形成死循环。单Agent里窗口溢出顶多就是丢点信息而Multi-Agent一旦上下文组织失序往往是系统性的逻辑混乱。这也是为什么我始终认为多Agent系统的稳定性不是靠提示词“写得细”就能保障的它需要一个有意识的上下文组织层来兜底。2. 常见上下文组织方案共享、隔离、分层与记忆分级2.1 全局共享式最直观但最容易爆掉最容易想到的方案是全局共享所有Agent读写同一个上下文区域像一张公共黑板。实现方式通常是在框架里挂一个共享State比如LangGraph的State、AutoGen的GroupChatManagerAgent往里面写消息、读消息。好处是简单适合Agent数量少、任务线短的小场景。我做过五六个Agent以内的研究类项目时用这种方案确实省事。但问题很快暴露一是内容膨胀每个Agent都要读全量历史Token消耗呈线性增长二是互相干扰A的中间思考、临时结论也会被B读走造成上下文污染。全局共享适合规模小、信息种类单一的场景一旦任务变长基本撑不住。2.2 独立隔离加定向传递更可控的通信方式后来我改用独立隔离加定向传递思路是每个Agent有独立上下文Agent之间不直接共享而是通过消息把信息打包发给指定对象。这有点类似Actor模型消息是显式路由的。这个方案的关键在于消息协议和路由规则。常见实现是定义“下发给谁”的字段配合事件类型。例如A完成后发一条task.completed事件给CoordinatorCoordinator决定要不要转发给C。通用框架里CrewAI的任务和上下文传递、AutoGen的对话消息都能模拟这种模式。独立隔离的优点是清晰、不易互相污染缺点是路由逻辑会变复杂如果消息类型划分不合理照样会出现漏传或重复传。2.3 分层上下文与作用域把权限思维引入上下文管理真正让我觉得“稳了”的方案是给上下文加上作用域Scope和分层结构。举个具体架构最外层是Controller Agent负责理解全局目标、分配任务、收集结果中间层是若干Worker Agent各自负责具体子任务最底层是工具调用和外部数据。上下文按作用域划分global所有Agent共享的全局事实比如项目目标、用户约束、关键决策记录。task当前任务特有的信息比如输入参数、进度状态、阶段性结果。agent单个Agent自己的工作区比如它的临时推理、草稿、局部结论。控制器只维护global和taskWorker主要工作在自己的agent工作区只有当需要汇报结论或请求协助时才写一条结构化消息。这个结构的好处是让信息天然“按需可见”从根上避免了全局共享的膨胀问题。代价是需要额外维护一套作用域管理逻辑对设计者的要求更高。2.4 记忆分级长期记忆、工作记忆与短期记忆最后说记忆分级。严格说这是上下文的另一种组织维度而不是替代方案。长期记忆通常放知识库、向量检索、以及重要且不可变的事实工作记忆放当前任务状态和中间结果短期记忆放最近几轮的对话或消息记录。对应到实际落地长期记忆写入外部数据库或者向量索引需要时通过检索拉取工作记忆放入task作用域由协调者更新短期记忆就是窗口里最新的几条原始消息。分级记忆模型和前面几种方案不冲突它们解决的是不同维度的需求——作用域解决“谁能看”记忆分级解决“看多久、放哪里”。实战中通常组合使用。我把几种方案做了一张对比表方便选型时参考方案适用规模优点主要风险全局共享2~5个Agent短任务实现简单信息完备Token爆炸、上下文污染独立隔离定向传递中等规模角色分明确信息隔离好可观测性强路由复杂容易漏传分层作用域复杂长任务适合生产按需可见扩展性好需要额外维护作用域逻辑记忆分级长记忆需求场景成本可控记忆持久检索质量会影响效果3. 手写一个轻量级上下文总线结构、代码与裁剪策略3.1 核心设计思路上下文总线加工作区纸上谈兵完了直接说落地。我目前最顺手的一套方案是用“上下文总线”加“Agent工作区”的组合。它的思路是Agent不直接读写别人上下文而是通过总线发布和订阅带作用域、带标签的上下文记录。总线上传递的数据不是大段对话历史而是一份份“上下文记录”。每条记录有明确的归属范围、来源、版本和有效期。Agent在工作区内维护自己的推导过程每次产出结果后再决定是否发布到总线上。这样既保留独立隔离的优点又比单纯消息路由更好查询和追溯。用这个思路做出来的系统即使Agent数量增加到十几二十个也只是数据量变大每个Agent的窗口仍然保持精简。3.2 上下文数据模型每条记录都该有哪些字段先定义一个轻量的数据结构。核心字段我总结为五个维度谁能看、是什么、多新鲜、信谁的、怎么用。scopeglobal/task/agent控制可见范围。key记录的业务标识比如“refund_policy”或“order_status”。payload内容主体可以直接是文本也可以是个JSON。ttl有效期超过时间后记录自动失效。version版本号用于处理事实更新和冲突。再加上source_agent标注来源以及tags用于检索和过滤。其实生产环境里还会加request_id来做链路追踪这个在排查问题时极其重要。3.3 代码实现一个可运行的ContextBus示例下面是一个简化但真实可用的Python实现核心是写记录、读记录、订阅变更。生产环境可以在这个基础上加Redis或数据库持久化。import time from collections import defaultdict from dataclasses import dataclass, field dataclass class ContextRecord: scope: str # global / task / agent key: str payload: str source_agent: str ttl: int 300 # 有效期单位秒 version: int 1 create_time: float field(default_factorytime.time) tags: list field(default_factorylist) class ContextBus: def __init__(self): self._records defaultdict(list) self._subscribers defaultdict(set) def _read_last(self, scope: str, key: str): records self._records.get((scope, key), []) return records[-1] if records else None def write(self, record: ContextRecord) - ContextRecord: old self._read_last(record.scope, record.key) if old: record.version old.version 1 self._records[(record.scope, record.key)].append(record) self._notify(record.scope, record.key, record) return record def read(self, scope: str, key: str, check_ttl: bool True): records self._records.get((scope, key), []) for record in reversed(records): if not check_ttl or time.time() - record.create_time record.ttl: return record return None def subscribe(self, scope: str, key: str, agent_name: str): self._subscribers[(scope, key)].add(agent_name) def _notify(self, scope: str, key: str, record: ContextRecord): # 实际项目里这里会触发事件回调或把消息推进消息队列 for subscriber in self._subscribers.get((scope, key), set()): print(f[ContextBus] notify {subscriber} fon {scope}:{key} v{record.version})使用方式也很直观。Coordinator写一条全局事实Worker订阅并响应bus ContextBus() # Coordinator 更新用户意图 bus.write(ContextRecord( scopeglobal, keyuser_intent, payload用户要求退款原因为商品破损, source_agentintent_detector, ttl600, tags[user, refund] )) # Worker 订阅该事实 bus.subscribe(global, user_intent, order_worker) # 业务代码里读取最新版本忽略过期记录 record bus.read(global, user_intent) if record: print(当前意图版本:, record.version)这套代码逻辑不复杂但已经能把“作用域”“TTL”“版本”三个核心机制落地。在实际项目里我还会把write操作接到一个集中的异步EventLoop上避免多个Agent并发写时产生竞态。3.4 上下文裁剪与Token预算分配有总线兜底上下文组织的主干就稳了。接下来要考虑的是预算问题。我给一个常规的Token分配经验假设总上下文窗口是4000个Token预算项配额说明系统提示800角色定义、约束、工具说明全局事实600目标、关键决策、用户约束任务状态600当前阶段、已完成步骤、待办工作区记录1600Agent自己的推理、草稿、参考最近轮次400最近几条原始消息窗口一旦超限优先压缩工作区把早期的推理过程替换成一条摘要或者结论而不是去压缩全局事实。全局事实被截掉整个系统的一致性就会崩。还有一个原则引用代替复制。大文档不要塞进上下文而是把索引或路径放进记录里Agent在需要具体内容时再主动加载。这样既保住Token预算又避免不同Agent持有同一个文档的不同版本。4. 踩坑实录上下文污染、死循环与Token预算失控4.1 上下文污染不知道“什么不能看”比“看不到”更糟第一次踩坑是在一个客服自动回复系统里。当时我用了一个很宽松的全局共享上下文所有Agent都能读写全部消息记录。结果意图识别Agent的内部推理过程比如“这个用户情绪有点激动可能是想找投诉渠道”被回复生成Agent读到了它直接把这个内部猜测当成了真实用户意图来组织语言最后回复变成“您好根据判断您想投诉”——用户都懵了。排查起来也很费劲因为没有作用域隔离所有记录混在一起根本不知道是哪条消息污染了最终输出。后面我把全局共享改成“发布-订阅”模式并且严格区分Fact和Reasoning全局里只放确认过的事实推理过程只存在Agent自己的工作区。经过这次之后我的任何Multi-Agent项目都不再允许跨Agent直接读取内部推理。4.2 死循环状态不同步导致的重复执行另一个印象深刻的坑是在自动化运维场景。系统里有两个Agent一个负责检测服务异常一个负责执行重启。异常检测Agent发出“服务超时”重启Agent修复后检测Agent没等到状态变更通知再次发出告警重启Agent又执行了一次。两边在各自上下文里都认为自己是对的整个流程陷入循环。当时查了很久核心问题就是缺少全局状态版本控制。后来我在每个状态记录上加version写入时递增并且让所有Agent在决策前先读一次最新版本号。如果版本号和上次决策时不一致就放弃当前行动先同步状态。死循环瞬间就解决了。所以我强烈建议任何共享状态都带上版本号这是Multi-Agent系统里性价比极高的一行字段。4.3 Token预算失控每个Agent都在重复搬运同一份大文件还有一个常见的成本问题。最开始我以为“给每个Agent都塞上完整的产品手册”它就能答得更准确。系统里有六个Agent每个窗口都要塞两万Token的手册一次请求直接烧掉十二万Token还没开始干活钱先没了。排查之后发现大多数Agent其实只需要手册中的某一小节。后来我改成“按需检索”把产品手册切块索引进向量库每个Agent在上下文总线里记录自己需要访问哪些章节真正执行时才检索相关内容。成本直接降到原来的十分之一响应速度也快了。这个问题的本质就是前面说的引用代替复制。很多资料不用进上下文只需要进索引。4.4 排查问题的一套实用方法踩了这么多坑之后我形成了一套排查上下文问题的标准套路。第一给每条消息和每次上下文写入都带上request_id方便把一次完整任务的链路追踪出来。第二做“上下文快照”定时把总线上所有记录导出来存一份。出问题时对比不同时刻的快照能很快发现是哪条记录在什么时间被错误改写。第三给Agent设计测试用例时专门准备几个“上下文敏感”的断言比如让两个Agent同时读同一个global事实验证它们拿到的版本一致。下面是一张我在实际项目中用到的快速排查对照表症状常见根因排查动作Agent重复做已完成的事任务状态未同步查看task作用域的进度版本两个Agent结论互相矛盾全局事实被局部改写导出各Agent引用的Fact记录对比版本重要信息被莫名截断Token预算不足按scope分桶统计Token消耗Agent答得模棱两可信息没传到该传的Agent检查订阅关系和消息路由记录这些坑总结起来其实都可以归到一句话上下文组织不是简单的文本拼接而是一种有目的的信息流设计每条信息都应该有边界、有来源、有版本、有生命周期。我个人在实际项目里的体会是不要一上来就想着搞很复杂的架构。先用全局共享把流程跑通一旦开始出现污染和冲突就升级成“作用域 总线”模式。再往下如果长记忆成为瓶颈再引入向量检索和记忆分级。每一步都有明确的问题驱动而不是为了炫技。Multi-Agent的上下文组织说白了就是一件事让每个Agent在正确的时间看到正确范围的信息并且让所有Agent对共享事实保持一致判断。你能把这点做到系统稳定性就已经超过绝大多数Demo了。
返回列表