多Agent系统这两年热度一直居高不下,但很多人搭完Demo之后会发现一个很尴尬的事:Agent一多,大家各说各话,A辛辛苦苦算出来的中间结果,B根本看不到,最后汇总出来的东西驴唇不对马嘴。问题就出在一件事上——Agent之间的通信和上下文共享没做好。这不是选哪个框架的问题,而是整个多Agent系统能不能真正跑起来的地基。这篇文章就把我实际做过的几个多Agent项目里,关于通信协议、上下文传递、消息时序、状态一致性这些事的经验和坑一次说清楚。1. 为什么多Agent系统绕不开通信与上下文共享1.1 单体Agent的边界与“分身困境”先抛一个问题:一个Agent能干所有事,为什么要拆成多个?很多人第一反应是“多个Agent更酷”,但实际上真正的驱动力是资源边界。单个Agent的上下文窗口有限,有Token成本,有响应时延。你让一个Agent既做意图识别,又做工具调用,又做长文档总结,又做结果校验,它会在两个地方卡死:一个是上下文被中间产物塞满,还没干正事Token就快烧完了;另一个是职责太杂之后,系统提示词变得无比臃肿,模型反而不知道该听哪条指令。拆开之后,问题就来了:每个Agent都是独立调用大模型,它们本身没有记忆,也没有“看到别人在干什么”的能力。A说“帮我查到订单号是12345”,B说“好的”,但B根本不知道12345是从哪张表里查出来的、筛选条件是什么、有没有经过清洗。信息在Agent之间不是自动流动的,你必须主动设计它们的通信方式和共享空间,否则多Agent就是多个单Agent的简单拼盘,甚至比单体更差。1.2 通信与上下文共享的真正含义很多刚接触的人会把“通信”和“上下文共享”当成一回事,其实它们是两个层次。通信解决的是“消息怎么传到对方手里”,核心是路由、协议、时序——A怎么把消息给B,B怎么知道这条消息是给自己的,消息阻塞了怎么办。上下文共享解决的是“各方对同一件事的理解是一致的”,核心是状态存储、可见性、一致性——B在干活的时候能不能拿到A已经处理过的数据,各个Agent对“当前任务进行到哪一步”有没有共识。打个比方:通信是快递员,上下文共享是仓库。快递员负责把货从这个仓库送到那个仓库,但如果两个仓库的货物清单对不上,收到货的人还是不知道怎么干活。我之前看过不少团队的做法是用消息队列把数据发来发去,消息倒是没丢,但下游Agent拿到的是过期版本的数据,或者拿到一堆原始数据还要重新解析一遍,这种“能通信但没共享”的状态,系统复杂度上去了,效率反而下降了。所以真正健康的架构是:通信负责触发和通知,上下文共享负责提供事实。两者配合,才能让多个Agent像一个团队一样协作,而不是像多个外包员工各做各的。2. 主流通信模式与选型思路2.1 三种基础通信模式我在不同项目里用过三种模式,各有各的适用场景,没有绝对的好坏。第一种是点对点直连。Agent A直接调用Agent B的接口,传参、拿结果。这种模式最简单,调试也方便,但缺点很明显:Agent之间的关系是写死的,加一个新Agent就要改一堆调用关系,而且A在等B的时候,整个链路是阻塞的。它适合两个Agent之间稳定的一问一答,比如“意图识别Agent把结果发给执行Agent”。第二种是发布/订阅。Agent只管往消息中心发事件,不关心谁在听;订阅方按自己的需求接收感兴趣的事件。这种模式好处是解耦,新Agent上线不用改老代码,只需要订阅它关心的事件就行。坏处是排查问题的时候责任链不清晰,你不知道这条消息到底被谁消费了,处理顺序是什么样的。它适合广播类场景,比如“任务状态有变化,通知所有关心这个任务的Agent”。第三种是共享存储/黑板模式。所有Agent都往一个共享空间里读写状态,谁需要什么数据自己去拿,谁更新了什么直接写进去。这是上下文共享最直接的一种落地方式。它的问题在于并发写入时的数据一致性,以及读写冲突。实际项目里,这三种模式不是互斥的。我之前做过一个客服工单处理系统,整体是共享存储为核心的:每个工单的状态和中间结果都放在共享上下文里,各Agent按需读写;但在某些关键节点上用的是发布/订阅——比如“新工单进来了,负责分类的Agent请处理一下”。2.2 模式选型:我的一点实战建议选型的时候,别只看技术时髦度,先问自己三个问题:第一,Agent之间的调用关系是否固定?如果是固定的,点对点就是最高效的,不要为了“解耦”而解耦,把简单的流程复杂化。第二,一个任务的执行是线性链路还是多路并行?线性链路可以用点对点或简单队列,真正复杂的并行协作才需要发布/订阅或者黑板模式。第三,Agent之间共享的数据量级有多大?如果只是传递几个状态字段,没必要上重型存储;如果共享的是长文本、多轮对话历史、业务对象快照,那就要认真设计共享存储了。还有一个容易被忽略的点:Agent通信的触发方式是同步还是异步。很多场景下,下游Agent希望上游处理完马上通知自己,但这不代表要阻塞等待。异步触发配上共享上下文,是目前综合体验最好的组合——触发靠事件,数据靠共享。我用这个组合做过几个项目,代码改动灵活性很高,后面加Agent基本不用推翻重来。3. 上下文共享的落地设计3.1 上下文的分层:会话、工作记忆与长期记忆“共享上下文”这个词听起来抽象,落地之前首先要解剖它。我习惯把上下文分成三层,这三层的存储方式、生命周期和访问权限完全不一样。会话上下文是最基础的一层,相当于一次任务执行过程中的“聊天记录”。包括用户的原始输入、Agent A的中间输出、Agent B的反馈等等。这层的特点是生命周期短——任务结束基本就可以丢了。但因为它的内容天然是长文本,直接全部传给每个Agent要么让Token爆炸,要么让上下文窗口溢出。工作记忆是任务执行中的关键状态,比如当前任务进行到第几步、有哪些待办分支、关键变量值是什么。这层的特征是结构化,适合用KV、JSON或数据库字段来存。工作记忆才是真正需要跨Agent共享的核心,因为这是协作的基础。长期记忆是Agent系统积累下来的经验库。比如历史工单的处理方式、哪些工具调用经常失败、用户偏好等等。这层不一定在每个任务里都用得到,但它是让系统越用越聪明的关键。之前做过一个系统,把过去成功处理相似请求时的消息模式存下来,新请求进来先检索类似历史,作为参考片段注入到规划Agent的提示词里,效果比单纯堆参数好得多。共享上下文设计最常见的一个错误,就是把三层混在一起,全塞进一个大JSON里。看起来什么问题都解决了,实际上维护几天就想删库重来。会话上下文放Redis里做短时缓存,工作记忆放数据库或者独立的状态服务里,长期记忆放向量库或文档存储里,各司其职,才能定位问题的时候不抓瞎。3.2 共享存储的技术选型选型之前先看团队熟悉什么,再看场景需要什么,最后才看技术本身。如果只是单机Demo级别的多Agent协作,用一个全局字典或者最简单的SQLite也够了。但我做过一个要真正跑业务的项目,三个Agent并行处理一批工单,共享上下文的数据要同时给多个进程读,这时候至少得用Redis,因为要处理并发读写和过期策略。如果是需要事务性更新的场景,还得考虑数据库。我之前踩过一个坑:两个Agent同时更新同一个任务的状态字段,最后总有一个更新丢掉了。后来改成数据库行锁加版本号控制,才把一致性问题压下去。很多文章不会跟你提这个,但真实环境里并发写入就是会出现的。再往上走,服务多了之后,需要的是统一的状态访问服务。共享存储本身不如“共享访问层”重要——你不希望各个Agent直接拼各种连接串去读写数据源,而是让它们都走一个统一的上下文服务。服务内部可以对接Redis、MySQL或者对象存储,但对外暴露的就只有“读上下文”“写上下文”“订阅变更”这几个接口。3.3 消息体里的“上下文引用”设计这里我要说一个我认为很关键、但很多人没意识到的细节:跨Agent传递消息的时候,不要传整个上下文快照,而是传一个上下文引用。什么意思呢?就是消息里不携带完整的业务数据,而是带一个上下文ID,比如ctx_20241108_0001,下游Agent拿到这个ID之后,去共享上下文服务里查真正的数据。这样做有几个好处:一是减少消息体体积。你要知道,Agent之间的消息如果走消息队列,消息太大不管是序列化成本还是传输成本都会让你肉疼。二是避免数据过期问题。A传给B的消息是某个时间点的快照,等B真正开始处理,这个快照可能已经过时了。带着引用去查,B拿到的是当前最新的数据。三是方便追踪。出了问题时,你只要看消息里的上下文引用,就能回放整个执行链路,快速定位是哪个环节的数据不对。当然,引用设计要配合缓存和过期策略一起做,不然每次都查底层存储,性能也扛不住。我们一般会在引用上挂一个TTL,比如任务执行期间有效、任务结束就失效,既保证协作需要,又不至于让垃圾数据堆在存储里。4. 一个最小可复用的实战示例4.1 场景设定光讲理论太虚,我拿一个实际做过的简化版场景来演示一下整套链路是怎么跑起来的。场景是一个“智能工单处理”系统,包含三个Agent:意图识别Agent:接收用户工单,判断工单类型(咨询、投诉、售后)。处理Agent:根据意图执行对应动作,如果是售后就查订单信息,如果是投诉就生成升级通知。汇总Agent:收集前面Agent的中间结果,生成最终响应给用户。这三个Agent在物理上是三个独立服务,各自调用大模型。它们之间的协作就完全依赖通信和共享上下文。4.2 消息结构定义先定义统一的消息结构。实践下来,最少要包含这几个字段:{ "message_id": "msg_20241108_001", "msg_type": "ticket.created", "source_agent": "intent_agent", "target_agent": "handler_agent", "ctx_ref": "ctx_ticket_20241108_001", "timestamp": 1731052800000, "payload": { "ticket_id": "TK-20241108-001", "intent": "after_sale" } }message_id用于幂等处理和追踪链路;msg_type是事件类型,让订阅方快速判断是否跟自己相关;source_agent和target_agent在点对点模式下用于路由;ctx_ref是上下文引用,真正完整的数据都挂在共享上下文服务里。4.3 共享上下文服务核心逻辑共享上下文服务不需要很复杂,核心能力就两个:读和写。用Python写一个简化版示意:class SharedContext: def __init__(self): self._storage = {} def write(self, ctx_id, key, value, ttl=None): if ctx_id not in self._storage: self._storage[ctx_id] = {} self._storage[ctx_id][key] = { "value": value, "updated_at": time.time() } # 实际项目中这里可以额外发布一个事件,触发订阅方 def read(self, ctx_id, key=None): if ctx_id not in self._storage: return None if key is None: return self._storage[ctx_id] return self._storage[ctx_id].get(key) def snapshot(self, ctx_id): # 给需要完整上下文的Agent提供一次性快照 return dict(self._storage.get(ctx_id, {}))实际项目里肯定要加锁、加持久化、加分布式,但接口形态基本就是这个样子。Agent之间不直接传数据,都是通过这个服务读写。4.4 两个Agent的协作循环意图识别Agent处理完工单,往共享上下文里写入识别结果,然后发布事件:# 意图识别Agent内部逻辑 intent = llm_analyze(ticket_text) ctx.write("ctx_ticket_20241108_001", "intent", intent) publish("ticket.created", {"ticket_id": "TK-20241108-001", "intent": intent}, target="handler_agent")处理Agent订阅到事件后,先从上下文里读出完整数据再干活:def on_ticket_created(msg): ctx_id = msg["ctx_ref"] ticket = ctx.read(ctx_id, "ticket") intent = ctx.read(ctx_id, "intent") if intent == "after_sale": order_info = query_order(ticket["order_id"]) ctx.write(ctx_id, "order_info", order_info) publish("ticket.processed", {"ticket_id": ticket["ticket_id"]}, target="summary_agent")汇总Agent最后把所有中间状态读出来生成回复。整个过程里,每个Agent只关心自己要写的和要读的,不关心其他Agent的内部实现。这个结构的好处是:再来十个Agent也能往里加,不需要大改。5. 协议、序列化与流式场景的细节处理5.1 为什么消息要比普通JSON多带几个字段很多人写Agent通信,传的还是那种只有业务字段的裸JSON,比如{"ticket_id": "123", "intent": "售后"}。单看一个消息确实够用,但一旦消息多了、Agent多了,就会遇到三个问题:顺序乱了不知道先处理哪个;消息重复投递不知道是不是同一件事;出问题之后没法回溯链路。所以消息里必须带上上面示例中的那几个元信息字段。message_id不是为了好看,它是幂等处理的基础——消费者拿到消息,先查这个ID有没有处理过,处理过就跳过,这在消息队列的重复投递场景下特别关键。timestamp也很重要,尤其是在多个Agent异步处理的时候。它帮你在排查问题时还原事件顺序。我遇到过这样的情况:汇总Agent收到的处理结果和意图识别结果不是同一个时间点产生的,业务上出现了逻辑矛盾。没有时间戳的话,你根本看不出来是谁先谁后。5.2 序列化选择与流式推送Agent之间的消息序列化,我用的最多的还是JSON,人类可读、调试方便、生态兼容好。但如果你对性能和体积实在敏感,可以考虑MessagePack或者Protobuf。我的建议是:项目初期没必要上重型序列化,JSON足够,等流量真上来了再优化也不迟,因为序列化替换是相对可控的差异隔离。流式场景稍微特殊一点。大模型输出本身就是流式的,如果下游Agent需要边接收边处理,再走“积攒完整消息再发”的路线就太慢了。这时候消息类型里要单独定义流式事件,比如ticket.updated.partial,每次只推一个增量片段。对应地,接收方要做片段缓存和按序拼接,最后等一个ticket.updated.complete事件来确认整段数据收齐了。流式推送还有一个陷阱:顺序问题。网络传输里,后发的片段可能先到。所以流式片段消息里务必带上序号片段的起止标识,接收方做重排时要用到。这个细节我一开始没注意,排查了半天才发现是乱序拼接导致的结果错乱。还有一个关乎体验的细节:如果消息量很大,注意控制推送频率。不是推得越频繁越好,有些Agent根本不需要实时看到每一个token,它只需要在关键节点知道结果。我之前把一个工具状态更新从“每次过程都推”改成“只在开始、完成、失败三个节点推”,消息量降了80%,对业务毫无影响。6. 影响范围分析:通信与上下文共享改变了什么6.1 对开发者的影响:从写业务逻辑到设计协作协议以前写单体Agent,核心工作是写提示词、调参数、设计工具调用。但多Agent系统一旦引入通信和上下文共享,开发者的工作重心就变了——你得开始思考“消息类型怎么设计”“谁负责更新状态”“状态冲突怎么解决”这些原来后端系统才考虑的事。这带来一个技能迁移问题。很多做AI应用的同学一开始对并发、状态一致性、消息队列并不熟悉,而这些东西恰恰是多Agent稳定运行的关键。我见过不少项目,Agent单跑都挺好,一组合就各种串状态、丢消息、重复调用工具,根本不是模型能力的问题,而是通信基础设施没跟上。这个趋势其实对开发者是好事。多Agent系统让它从“调大模型”变成了“设计分布式协作系统”,工程含量更高,也更值得深入。只会写提示词的红利期正在过去,懂消息设计、懂状态管理、懂系统架构的人会越来越有优势。6.2 对团队与产品架构的影响对团队来说,上下文共享模式直接改变了分工方式。以前做一个AI功能,一个人从头写到尾;现在按Agent分层之后,有人专门维护共享上下文服务,有人专注意图识别Agent,有人专注工具执行Agent,彼此通过消息结构对齐,不用所有事情都去打扰别人。对产品架构的影响更明显。通信与上下文共享做得好,整个系统是“可插拔”的——新增一个能力Agent,只要声明自己订阅哪些事件、读写哪些上下文,就能接入现有流程。做得不好,每次加需求都要动主干逻辑,产品迭代速度就会被拖死。还有一个对业务的影响容易被忽略:多Agent系统的可观测性直接决定了你能不能上线。你有五个Agent在协作,用户说“结果不对”,你连是哪个Agent搞错的都定位不了,这就不是模型问题而是管理问题了。所以通信层和共享层的日志设计要从一开始就做,别等上线之后再补。上下文ID贯穿全链路,日志里处处带ctx_ref,这是最基本的可观测性要求。7. 常见故障与排查实录7.1 我实际踩过的几个坑第一个坑:上下文被覆盖。两个并行Agent同时写同一个上下文键,最后一个写入的覆盖了前面的。解决方案是给键加上Agent标识或者阶段前缀,比如handler_a.order_info和handler_b.order_info分开存,需要合并时再由汇总Agent统一处理。第二个坑:消息风暴。A处理完发事件,B收到后更新上下文又发新事件,C收到后又发,一旦有循环依赖,整个系统就在疯狂刷消息。我的排查办法是在每个Agent的出口加一个节流和去重,同时用message_id做链路追踪,很快就能发现是谁在循环触发。第三个坑:共享上下文无限膨胀。任务一多,共享上下文里的数据就不清理,最后存储里堆了几百万条过期状态。解决方案是给上下文加生命周期:任务结束就归档,归档之后只能读不能写,再配合定期清理,问题就解决了。第四个坑:大模型幻觉数据写进上下文。Agent在处理过程中,可能会“认为”某个状态是某值然后写进去,但这个值根本没经过验证。后来我们要求Agent写入关键状态之前,必须先引用数据来源的上下文键,否则写入失败。这就逼着Agent的每一步输出都有据可依。7.2 排查工具与套路多Agent系统排查问题的核心思路是“选一个上下文ID,走完整个链路”。我们的做法是:用户每次任务生成一个trace_id,贯穿所有消息和上下文操作。出问题时,按trace_id查所有相关记录,就能看到每个Agent在哪个时间点读写了什么。实际操作中,我会按优先级排查:先看有没有消息丢失(发送记录在,消费记录不在);再看有没有状态覆盖(同一上下文键被多次写入且来源不同);最后看有没有提示词层面的问题(Agent读到了正确数据但没用对)。建议每个Agent都打两类日志:一类是业务日志,记录输入输出和关键决策;另一类是通信日志,记录消息收发、上下文读写。有了这两层日志,大部分问题都能快速定位到具体环节。最后说点实在的做了几个多Agent项目的通信层之后,我最大的感受是:这个系统的复杂度和Agent数量根本就是指数关系,而通信与上下文共享的设计,决定了这个指数增长是可控还是失控。别急着堆Agent,先把消息结构定义清楚、共享上下文分层设计好、日志链路打通,比什么技巧都管用。如果你刚起步,哪怕用最简单的Redis加JSON消息先跑通一套闭环,也比一开始就纠结用哪个消息中间件要实际得多——通信架构的升级可以后做,协作逻辑和上下文设计才是真正难改的部分。
多Agent系统这两年热度一直居高不下,但很多人搭完Demo之后会发现一个很尴尬的事:Agent一多,大家各说各话,A辛辛苦苦算出来的中间结果,B根本看不到,最后汇总出来的东西驴唇不对马嘴。问题就出在一件事上——Agent之间的通信和上下文共享没做好。这不是选哪个框架的问题,而是整个多Agent系统能不能真正跑起来的地基。这篇文章就把我实际做过的几个多Agent项目里,关于通信协议、上下文传递、消息时序、状态一致性这些事的经验和坑一次说清楚。1. 为什么多Agent系统绕不开通信与上下文共享1.1 单体Agent的边界与“分身困境”先抛一个问题:一个Agent能干所有事,为什么要拆成多个?很多人第一反应是“多个Agent更酷”,但实际上真正的驱动力是资源边界。单个Agent的上下文窗口有限,有Token成本,有响应时延。你让一个Agent既做意图识别,又做工具调用,又做长文档总结,又做结果校验,它会在两个地方卡死:一个是上下文被中间产物塞满,还没干正事Token就快烧完了;另一个是职责太杂之后,系统提示词变得无比臃肿,模型反而不知道该听哪条指令。拆开之后,问题就来了:每个Agent都是独立调用大模型,它们本身没有记忆,也没有“看到别人在干什么”的能力。A说“帮我查到订单号是12345”,B说“好的”,但B根本不知道12345是从哪张表里查出来的、筛选条件是什么、有没有经过清洗。信息在Agent之间不是自动流动的,你必须主动设计它们的通信方式和共享空间,否则多Agent就是多个单Agent的简单拼盘,甚至比单体更差。1.2 通信与上下文共享的真正含义很多刚接触的人会把“通信”和“上下文共享”当成一回事,其实它们是两个层次。通信解决的是“消息怎么传到对方手里”,核心是路由、协议、时序——A怎么把消息给B,B怎么知道这条消息是给自己的,消息阻塞了怎么办。上下文共享解决的是“各方对同一件事的理解是一致的”,核心是状态存储、可见性、一致性——B在干活的时候能不能拿到A已经处理过的数据,各个Agent对“当前任务进行到哪一步”有没有共识。打个比方:通信是快递员,上下文共享是仓库。快递员负责把货从这个仓库送到那个仓库,但如果两个仓库的货物清单对不上,收到货的人还是不知道怎么干活。我之前看过不少团队的做法是用消息队列把数据发来发去,消息倒是没丢,但下游Agent拿到的是过期版本的数据,或者拿到一堆原始数据还要重新解析一遍,这种“能通信但没共享”的状态,系统复杂度上去了,效率反而下降了。所以真正健康的架构是:通信负责触发和通知,上下文共享负责提供事实。两者配合,才能让多个Agent像一个团队一样协作,而不是像多个外包员工各做各的。2. 主流通信模式与选型思路2.1 三种基础通信模式我在不同项目里用过三种模式,各有各的适用场景,没有绝对的好坏。第一种是点对点直连。Agent A直接调用Agent B的接口,传参、拿结果。这种模式最简单,调试也方便,但缺点很明显:Agent之间的关系是写死的,加一个新Agent就要改一堆调用关系,而且A在等B的时候,整个链路是阻塞的。它适合两个Agent之间稳定的一问一答,比如“意图识别Agent把结果发给执行Agent”。第二种是发布/订阅。Agent只管往消息中心发事件,不关心谁在听;订阅方按自己的需求接收感兴趣的事件。这种模式好处是解耦,新Agent上线不用改老代码,只需要订阅它关心的事件就行。坏处是排查问题的时候责任链不清晰,你不知道这条消息到底被谁消费了,处理顺序是什么样的。它适合广播类场景,比如“任务状态有变化,通知所有关心这个任务的Agent”。第三种是共享存储/黑板模式。所有Agent都往一个共享空间里读写状态,谁需要什么数据自己去拿,谁更新了什么直接写进去。这是上下文共享最直接的一种落地方式。它的问题在于并发写入时的数据一致性,以及读写冲突。实际项目里,这三种模式不是互斥的。我之前做过一个客服工单处理系统,整体是共享存储为核心的:每个工单的状态和中间结果都放在共享上下文里,各Agent按需读写;但在某些关键节点上用的是发布/订阅——比如“新工单进来了,负责分类的Agent请处理一下”。2.2 模式选型:我的一点实战建议选型的时候,别只看技术时髦度,先问自己三个问题:第一,Agent之间的调用关系是否固定?如果是固定的,点对点就是最高效的,不要为了“解耦”而解耦,把简单的流程复杂化。第二,一个任务的执行是线性链路还是多路并行?线性链路可以用点对点或简单队列,真正复杂的并行协作才需要发布/订阅或者黑板模式。第三,Agent之间共享的数据量级有多大?如果只是传递几个状态字段,没必要上重型存储;如果共享的是长文本、多轮对话历史、业务对象快照,那就要认真设计共享存储了。还有一个容易被忽略的点:Agent通信的触发方式是同步还是异步。很多场景下,下游Agent希望上游处理完马上通知自己,但这不代表要阻塞等待。异步触发配上共享上下文,是目前综合体验最好的组合——触发靠事件,数据靠共享。我用这个组合做过几个项目,代码改动灵活性很高,后面加Agent基本不用推翻重来。3. 上下文共享的落地设计3.1 上下文的分层:会话、工作记忆与长期记忆“共享上下文”这个词听起来抽象,落地之前首先要解剖它。我习惯把上下文分成三层,这三层的存储方式、生命周期和访问权限完全不一样。会话上下文是最基础的一层,相当于一次任务执行过程中的“聊天记录”。包括用户的原始输入、Agent A的中间输出、Agent B的反馈等等。这层的特点是生命周期短——任务结束基本就可以丢了。但因为它的内容天然是长文本,直接全部传给每个Agent要么让Token爆炸,要么让上下文窗口溢出。工作记忆是任务执行中的关键状态,比如当前任务进行到第几步、有哪些待办分支、关键变量值是什么。这层的特征是结构化,适合用KV、JSON或数据库字段来存。工作记忆才是真正需要跨Agent共享的核心,因为这是协作的基础。长期记忆是Agent系统积累下来的经验库。比如历史工单的处理方式、哪些工具调用经常失败、用户偏好等等。这层不一定在每个任务里都用得到,但它是让系统越用越聪明的关键。之前做过一个系统,把过去成功处理相似请求时的消息模式存下来,新请求进来先检索类似历史,作为参考片段注入到规划Agent的提示词里,效果比单纯堆参数好得多。共享上下文设计最常见的一个错误,就是把三层混在一起,全塞进一个大JSON里。看起来什么问题都解决了,实际上维护几天就想删库重来。会话上下文放Redis里做短时缓存,工作记忆放数据库或者独立的状态服务里,长期记忆放向量库或文档存储里,各司其职,才能定位问题的时候不抓瞎。3.2 共享存储的技术选型选型之前先看团队熟悉什么,再看场景需要什么,最后才看技术本身。如果只是单机Demo级别的多Agent协作,用一个全局字典或者最简单的SQLite也够了。但我做过一个要真正跑业务的项目,三个Agent并行处理一批工单,共享上下文的数据要同时给多个进程读,这时候至少得用Redis,因为要处理并发读写和过期策略。如果是需要事务性更新的场景,还得考虑数据库。我之前踩过一个坑:两个Agent同时更新同一个任务的状态字段,最后总有一个更新丢掉了。后来改成数据库行锁加版本号控制,才把一致性问题压下去。很多文章不会跟你提这个,但真实环境里并发写入就是会出现的。再往上走,服务多了之后,需要的是统一的状态访问服务。共享存储本身不如“共享访问层”重要——你不希望各个Agent直接拼各种连接串去读写数据源,而是让它们都走一个统一的上下文服务。服务内部可以对接Redis、MySQL或者对象存储,但对外暴露的就只有“读上下文”“写上下文”“订阅变更”这几个接口。3.3 消息体里的“上下文引用”设计这里我要说一个我认为很关键、但很多人没意识到的细节:跨Agent传递消息的时候,不要传整个上下文快照,而是传一个上下文引用。什么意思呢?就是消息里不携带完整的业务数据,而是带一个上下文ID,比如ctx_20241108_0001,下游Agent拿到这个ID之后,去共享上下文服务里查真正的数据。这样做有几个好处:一是减少消息体体积。你要知道,Agent之间的消息如果走消息队列,消息太大不管是序列化成本还是传输成本都会让你肉疼。二是避免数据过期问题。A传给B的消息是某个时间点的快照,等B真正开始处理,这个快照可能已经过时了。带着引用去查,B拿到的是当前最新的数据。三是方便追踪。出了问题时,你只要看消息里的上下文引用,就能回放整个执行链路,快速定位是哪个环节的数据不对。当然,引用设计要配合缓存和过期策略一起做,不然每次都查底层存储,性能也扛不住。我们一般会在引用上挂一个TTL,比如任务执行期间有效、任务结束就失效,既保证协作需要,又不至于让垃圾数据堆在存储里。4. 一个最小可复用的实战示例4.1 场景设定光讲理论太虚,我拿一个实际做过的简化版场景来演示一下整套链路是怎么跑起来的。场景是一个“智能工单处理”系统,包含三个Agent:意图识别Agent:接收用户工单,判断工单类型(咨询、投诉、售后)。处理Agent:根据意图执行对应动作,如果是售后就查订单信息,如果是投诉就生成升级通知。汇总Agent:收集前面Agent的中间结果,生成最终响应给用户。这三个Agent在物理上是三个独立服务,各自调用大模型。它们之间的协作就完全依赖通信和共享上下文。4.2 消息结构定义先定义统一的消息结构。实践下来,最少要包含这几个字段:{ "message_id": "msg_20241108_001", "msg_type": "ticket.created", "source_agent": "intent_agent", "target_agent": "handler_agent", "ctx_ref": "ctx_ticket_20241108_001", "timestamp": 1731052800000, "payload": { "ticket_id": "TK-20241108-001", "intent": "after_sale" } }message_id用于幂等处理和追踪链路;msg_type是事件类型,让订阅方快速判断是否跟自己相关;source_agent和target_agent在点对点模式下用于路由;ctx_ref是上下文引用,真正完整的数据都挂在共享上下文服务里。4.3 共享上下文服务核心逻辑共享上下文服务不需要很复杂,核心能力就两个:读和写。用Python写一个简化版示意:class SharedContext: def __init__(self): self._storage = {} def write(self, ctx_id, key, value, ttl=None): if ctx_id not in self._storage: self._storage[ctx_id] = {} self._storage[ctx_id][key] = { "value": value, "updated_at": time.time() } # 实际项目中这里可以额外发布一个事件,触发订阅方 def read(self, ctx_id, key=None): if ctx_id not in self._storage: return None if key is None: return self._storage[ctx_id] return self._storage[ctx_id].get(key) def snapshot(self, ctx_id): # 给需要完整上下文的Agent提供一次性快照 return dict(self._storage.get(ctx_id, {}))实际项目里肯定要加锁、加持久化、加分布式,但接口形态基本就是这个样子。Agent之间不直接传数据,都是通过这个服务读写。4.4 两个Agent的协作循环意图识别Agent处理完工单,往共享上下文里写入识别结果,然后发布事件:# 意图识别Agent内部逻辑 intent = llm_analyze(ticket_text) ctx.write("ctx_ticket_20241108_001", "intent", intent) publish("ticket.created", {"ticket_id": "TK-20241108-001", "intent": intent}, target="handler_agent")处理Agent订阅到事件后,先从上下文里读出完整数据再干活:def on_ticket_created(msg): ctx_id = msg["ctx_ref"] ticket = ctx.read(ctx_id, "ticket") intent = ctx.read(ctx_id, "intent") if intent == "after_sale": order_info = query_order(ticket["order_id"]) ctx.write(ctx_id, "order_info", order_info) publish("ticket.processed", {"ticket_id": ticket["ticket_id"]}, target="summary_agent")汇总Agent最后把所有中间状态读出来生成回复。整个过程里,每个Agent只关心自己要写的和要读的,不关心其他Agent的内部实现。这个结构的好处是:再来十个Agent也能往里加,不需要大改。5. 协议、序列化与流式场景的细节处理5.1 为什么消息要比普通JSON多带几个字段很多人写Agent通信,传的还是那种只有业务字段的裸JSON,比如{"ticket_id": "123", "intent": "售后"}。单看一个消息确实够用,但一旦消息多了、Agent多了,就会遇到三个问题:顺序乱了不知道先处理哪个;消息重复投递不知道是不是同一件事;出问题之后没法回溯链路。所以消息里必须带上上面示例中的那几个元信息字段。message_id不是为了好看,它是幂等处理的基础——消费者拿到消息,先查这个ID有没有处理过,处理过就跳过,这在消息队列的重复投递场景下特别关键。timestamp也很重要,尤其是在多个Agent异步处理的时候。它帮你在排查问题时还原事件顺序。我遇到过这样的情况:汇总Agent收到的处理结果和意图识别结果不是同一个时间点产生的,业务上出现了逻辑矛盾。没有时间戳的话,你根本看不出来是谁先谁后。5.2 序列化选择与流式推送Agent之间的消息序列化,我用的最多的还是JSON,人类可读、调试方便、生态兼容好。但如果你对性能和体积实在敏感,可以考虑MessagePack或者Protobuf。我的建议是:项目初期没必要上重型序列化,JSON足够,等流量真上来了再优化也不迟,因为序列化替换是相对可控的差异隔离。流式场景稍微特殊一点。大模型输出本身就是流式的,如果下游Agent需要边接收边处理,再走“积攒完整消息再发”的路线就太慢了。这时候消息类型里要单独定义流式事件,比如ticket.updated.partial,每次只推一个增量片段。对应地,接收方要做片段缓存和按序拼接,最后等一个ticket.updated.complete事件来确认整段数据收齐了。流式推送还有一个陷阱:顺序问题。网络传输里,后发的片段可能先到。所以流式片段消息里务必带上序号片段的起止标识,接收方做重排时要用到。这个细节我一开始没注意,排查了半天才发现是乱序拼接导致的结果错乱。还有一个关乎体验的细节:如果消息量很大,注意控制推送频率。不是推得越频繁越好,有些Agent根本不需要实时看到每一个token,它只需要在关键节点知道结果。我之前把一个工具状态更新从“每次过程都推”改成“只在开始、完成、失败三个节点推”,消息量降了80%,对业务毫无影响。6. 影响范围分析:通信与上下文共享改变了什么6.1 对开发者的影响:从写业务逻辑到设计协作协议以前写单体Agent,核心工作是写提示词、调参数、设计工具调用。但多Agent系统一旦引入通信和上下文共享,开发者的工作重心就变了——你得开始思考“消息类型怎么设计”“谁负责更新状态”“状态冲突怎么解决”这些原来后端系统才考虑的事。这带来一个技能迁移问题。很多做AI应用的同学一开始对并发、状态一致性、消息队列并不熟悉,而这些东西恰恰是多Agent稳定运行的关键。我见过不少项目,Agent单跑都挺好,一组合就各种串状态、丢消息、重复调用工具,根本不是模型能力的问题,而是通信基础设施没跟上。这个趋势其实对开发者是好事。多Agent系统让它从“调大模型”变成了“设计分布式协作系统”,工程含量更高,也更值得深入。只会写提示词的红利期正在过去,懂消息设计、懂状态管理、懂系统架构的人会越来越有优势。6.2 对团队与产品架构的影响对团队来说,上下文共享模式直接改变了分工方式。以前做一个AI功能,一个人从头写到尾;现在按Agent分层之后,有人专门维护共享上下文服务,有人专注意图识别Agent,有人专注工具执行Agent,彼此通过消息结构对齐,不用所有事情都去打扰别人。对产品架构的影响更明显。通信与上下文共享做得好,整个系统是“可插拔”的——新增一个能力Agent,只要声明自己订阅哪些事件、读写哪些上下文,就能接入现有流程。做得不好,每次加需求都要动主干逻辑,产品迭代速度就会被拖死。还有一个对业务的影响容易被忽略:多Agent系统的可观测性直接决定了你能不能上线。你有五个Agent在协作,用户说“结果不对”,你连是哪个Agent搞错的都定位不了,这就不是模型问题而是管理问题了。所以通信层和共享层的日志设计要从一开始就做,别等上线之后再补。上下文ID贯穿全链路,日志里处处带ctx_ref,这是最基本的可观测性要求。7. 常见故障与排查实录7.1 我实际踩过的几个坑第一个坑:上下文被覆盖。两个并行Agent同时写同一个上下文键,最后一个写入的覆盖了前面的。解决方案是给键加上Agent标识或者阶段前缀,比如handler_a.order_info和handler_b.order_info分开存,需要合并时再由汇总Agent统一处理。第二个坑:消息风暴。A处理完发事件,B收到后更新上下文又发新事件,C收到后又发,一旦有循环依赖,整个系统就在疯狂刷消息。我的排查办法是在每个Agent的出口加一个节流和去重,同时用message_id做链路追踪,很快就能发现是谁在循环触发。第三个坑:共享上下文无限膨胀。任务一多,共享上下文里的数据就不清理,最后存储里堆了几百万条过期状态。解决方案是给上下文加生命周期:任务结束就归档,归档之后只能读不能写,再配合定期清理,问题就解决了。第四个坑:大模型幻觉数据写进上下文。Agent在处理过程中,可能会“认为”某个状态是某值然后写进去,但这个值根本没经过验证。后来我们要求Agent写入关键状态之前,必须先引用数据来源的上下文键,否则写入失败。这就逼着Agent的每一步输出都有据可依。7.2 排查工具与套路多Agent系统排查问题的核心思路是“选一个上下文ID,走完整个链路”。我们的做法是:用户每次任务生成一个trace_id,贯穿所有消息和上下文操作。出问题时,按trace_id查所有相关记录,就能看到每个Agent在哪个时间点读写了什么。实际操作中,我会按优先级排查:先看有没有消息丢失(发送记录在,消费记录不在);再看有没有状态覆盖(同一上下文键被多次写入且来源不同);最后看有没有提示词层面的问题(Agent读到了正确数据但没用对)。建议每个Agent都打两类日志:一类是业务日志,记录输入输出和关键决策;另一类是通信日志,记录消息收发、上下文读写。有了这两层日志,大部分问题都能快速定位到具体环节。最后说点实在的做了几个多Agent项目的通信层之后,我最大的感受是:这个系统的复杂度和Agent数量根本就是指数关系,而通信与上下文共享的设计,决定了这个指数增长是可控还是失控。别急着堆Agent,先把消息结构定义清楚、共享上下文分层设计好、日志链路打通,比什么技巧都管用。如果你刚起步,哪怕用最简单的Redis加JSON消息先跑通一套闭环,也比一开始就纠结用哪个消息中间件要实际得多——通信架构的升级可以后做,协作逻辑和上下文设计才是真正难改的部分。