ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

多智能体协作通信设计:消息格式、超时重试与上下文同步实战

多智能体协作通信设计:消息格式、超时重试与上下文同步实战

1. 智能体间通信到底在解决什么问题

1.1 从一个真实踩坑场景说起

去年我参与设计过一套多智能体协作系统,当时团队里有个共识:每个智能体各司其职,一个负责理解用户意图,一个负责检索知识库,一个负责生成最终回复。听起来很清晰对吧?结果上线第一天就出了状况——用户问“帮我对比一下上季度和这季度的销售数据”,意图理解智能体把任务拆成了两个子任务,分别发给检索智能体和数据分析智能体,但这两个智能体各自返回了结果,却没有任何一方知道对方的存在。最终生成智能体收到两份互相矛盾的数据摘要,输出了一段自相矛盾的回答。

这个问题的根源不在于单个智能体的能力,而在于智能体间通信没有设计好。所谓智能体间通信,就是多个智能体在协作完成一个任务时,如何传递信息、如何协商分工、如何处理冲突、如何确认任务完成状态的一整套机制。它决定了多智能体系统是“一群各自为战的散兵”还是“一支配合默契的团队”。

如果你正在做AI产品经理,或者正在设计基于大模型的智能体系统,这个问题迟早会撞上你。单智能体时代,你只需要关注一个提示词写得好不好;多智能体时代,你要关注的是信息在多个节点之间怎么流动、怎么同步、怎么纠错。这篇文章我会把智能体间通信的核心设计思路、实操要点、常见坑和排查方法全部拆开讲清楚,不管你是刚接触智能体设计的新手,还是已经踩过一些坑的从业者,都能从中找到可以直接参考的方案。

1.2 智能体间通信的三种典型模式

在展开细节之前,先建立一个全局认知。目前业界常见的智能体间通信模式大致可以归为三类,每一类都有它的适用场景和代价。

第一种是共享黑板模式。所有智能体读写同一个共享状态空间,就像一群人围着一块白板工作。谁有结果就写上去,谁需要信息就从上面读。这种模式实现简单,适合智能体数量少、任务流程短的场景。但问题也很明显:当智能体数量增多时,黑板上的信息会迅速膨胀,每个智能体都要花大量精力去筛选“哪些信息跟我有关”,效率急剧下降。

第二种是消息传递模式。智能体之间通过显式的消息进行点对点或广播式通信。A智能体完成任务后,把结果封装成消息发给B智能体。这种模式职责清晰,每条消息都有明确的发送者和接收者,便于追踪和调试。但代价是通信拓扑会变得复杂,当智能体数量超过五六个时,消息路由和生命周期管理就成了新的难题。

第三种是编排模式。有一个中心化的编排器(Orchestrator)负责决定谁在什么时候做什么,智能体之间不直接通信,所有信息流转都经过编排器。这种模式控制力最强,适合流程确定性高的场景,但编排器本身容易成为瓶颈,而且一旦编排逻辑设计得不好,整个系统会变得非常僵硬。

实际项目中,这三种模式往往不是互斥的,而是混合使用。比如顶层用编排模式做任务规划,底层执行层用消息传递做具体协作,共享状态用黑板模式做全局上下文同步。

1.3 为什么AI产品经理必须关注通信设计

很多AI产品经理会把精力集中在单个智能体的提示词优化上,觉得通信是工程实现的事。但实际情况是,通信协议的设计直接决定了产品的用户体验。举个例子:用户发出一个请求后,如果智能体之间的通信没有超时机制,一个智能体卡住了,整个系统就会一直转圈,用户看到的就是无限加载。如果通信消息没有版本管理,一个智能体升级了输出格式,另一个智能体还在按旧格式解析,就会静默出错。

这些问题在产品需求文档里如果不提前定义清楚,到了开发阶段就是无休止的扯皮。所以我的经验是,AI产品经理在智能体设计阶段,至少要明确三件事:智能体之间传什么(消息内容规范)、怎么传(通信机制)、传错了怎么办(异常处理策略)。这三件事定义清楚了,后面的开发和调试会顺畅很多。

2. 通信协议设计的核心细节与实操要点

2.1 消息格式:结构化是底线

智能体之间传递的消息,最忌讳的就是用自然语言裸传。我见过不少团队为了图省事,让一个智能体直接把一段文字丢给另一个智能体,结果接收方每次都要重新理解一遍,不仅浪费token,还容易产生歧义。

正确的做法是定义结构化的消息格式。一个最小可用的消息结构至少包含以下字段:

字段名类型说明
message_idstring消息唯一标识,用于追踪和去重
senderstring发送方智能体标识
receiverstring接收方智能体标识,广播时可为空
intentstring消息意图,如“请求数据”“返回结果”“报错”
payloadobject实际数据内容,结构由intent决定
timestampnumber消息创建时间戳
correlation_idstring关联ID,用于把请求和响应串起来
statusstring消息状态,如“pending”“success”“failed”

这个结构看起来简单,但每一个字段都有它的用处。message_id和correlation_id在排查问题时尤其重要——当系统里有几十条消息在飞的时候,没有这两个字段你根本不知道哪条响应对应哪条请求。intent字段则是实现消息路由的基础,接收方根据intent决定用什么逻辑处理payload。

注意:payload的结构应该由intent严格定义,不要在一个intent下塞多种不同结构的数据。我见过一个团队把所有返回结果都塞进一个叫“result”的字段里,结果接收方每次都要写一堆if-else来判断到底返回的是什么。后来他们拆成了“search_result”“analysis_result”“generation_result”三个独立的intent,代码立刻清爽了。

2.2 通信拓扑:什么时候用点对点,什么时候用广播

通信拓扑的选择直接影响到系统的扩展性和可维护性。点对点通信适合有明确依赖关系的场景,比如“检索智能体完成后必须把结果发给分析智能体”。广播通信适合需要通知多个智能体的场景,比如“任务取消,所有智能体停止当前工作”。

但这里有个容易被忽略的问题:广播消息的幂等性。如果广播消息被重复投递,接收方会不会重复执行?比如“停止工作”这个消息如果收到两次,第二次收到时智能体已经停了,会不会报错?这些边界情况在设计阶段就要考虑清楚。

我的建议是,对于广播消息,接收方必须实现幂等处理逻辑。具体做法可以是在消息里带一个全局唯一的event_id,接收方维护一个已处理event_id的集合,收到重复的event_id直接忽略。这个集合不需要永久保存,保留最近几分钟的就够了。

2.3 超时与重试:别让一个智能体拖垮整个系统

超时机制是智能体间通信的生命线。没有超时,一个卡住的智能体就能让整个协作流程无限等待。但超时时间设多少合适?设短了,正常但稍慢的智能体被误杀;设长了,用户体验差。

我的经验值是:根据任务类型分档设置。简单的信息查询类任务,超时设5到10秒;需要调用外部工具的任务,超时设30到60秒;涉及大模型生成的任务,超时设60到120秒。这些值不是拍脑袋定的,而是根据实际运行数据统计出来的P99耗时再上浮20%左右。

重试策略同样需要仔细设计。不是所有失败都值得重试——参数错误重试一百次也没用,网络抖动重试一次可能就好了。我通常会把错误分成三类:

  • 可重试错误:超时、限流、临时性服务不可用。这类错误重试2到3次,每次间隔指数退避。
  • 不可重试错误:参数校验失败、权限不足、资源不存在。这类错误直接返回,不重试。
  • 未知错误:记录日志,重试1次,如果还失败就上报。

实操心得:重试的时候一定要带上原始消息的correlation_id,这样在日志里能把重试链路串起来。我踩过的坑是重试时生成了新的correlation_id,结果排查问题时完全看不出这是同一次请求的重试。

2.4 上下文同步:让每个智能体都看到该看的东西

多智能体协作时,上下文同步是个大问题。每个智能体在执行任务时,都需要一定的上下文信息——用户的历史对话、之前智能体的输出、全局的任务状态等。但这些信息如果全量同步给每个智能体,token消耗会爆炸,而且大量无关信息会干扰智能体的判断。

我的做法是分层同步。把上下文分成三层:

  • 全局上下文:任务目标、用户基本信息、全局约束条件。这层信息量小,同步给所有智能体。
  • 链路上下文:当前执行路径上上游智能体的输出。只同步给直接下游智能体。
  • 局部上下文:智能体自己维护的中间状态。不同步,只在需要时通过消息查询。

这样设计的好处是,每个智能体拿到的上下文都是精简且相关的,既不会信息过载,也不会缺少关键信息。实现上,全局上下文可以放在共享存储里,链路上下文通过消息payload传递,局部上下文由智能体自己管理。

3. 实操过程与核心环节实现

3.1 从零搭建一个最小可用的通信框架

假设我们要搭建一个包含三个智能体的系统:意图理解智能体、知识检索智能体、回复生成智能体。下面是我实际用过的一套实现方案,用Python伪代码展示核心逻辑。

首先定义消息基类:

class AgentMessage: def __init__(self, sender, receiver, intent, payload, correlation_id=None): self.message_id = generate_uuid() self.sender = sender self.receiver = receiver self.intent = intent self.payload = payload self.timestamp = time.time() self.correlation_id = correlation_id or self.message_id self.status = "pending"

然后定义通信总线,负责消息的路由和投递:

class MessageBus: def __init__(self): self.agents = {} self.message_log = [] def register(self, agent_id, handler): self.agents[agent_id] = handler def send(self, message): self.message_log.append(message) if message.receiver in self.agents: try: response = self.agents[message.receiver](message) message.status = "success" return response except Exception as e: message.status = "failed" raise else: raise ValueError(f"Unknown receiver: {message.receiver}")

这个框架虽然简单,但已经包含了通信的核心要素:消息结构、路由、状态追踪。在实际项目中,你需要把MessageBus替换成真正的消息队列(比如基于Redis的Pub/Sub或者RabbitMQ),把同步调用改成异步,加上超时和重试逻辑。

3.2 意图理解智能体的实现细节

意图理解智能体的职责是解析用户输入,判断用户想要什么,然后把任务分发给下游智能体。它的核心逻辑是:

def handle_intent_agent(message): user_input = message.payload["user_input"] # 调用大模型做意图分类 intent_result = llm_classify(user_input) if intent_result["type"] == "knowledge_query": # 需要检索知识库 downstream_msg = AgentMessage( sender="intent_agent", receiver="retrieval_agent", intent="retrieve_knowledge", payload={"query": intent_result["query"], "user_input": user_input}, correlation_id=message.correlation_id ) retrieval_result = message_bus.send(downstream_msg) # 把检索结果传给生成智能体 generation_msg = AgentMessage( sender="intent_agent", receiver="generation_agent", intent="generate_response", payload={"user_input": user_input, "context": retrieval_result.payload}, correlation_id=message.correlation_id ) return message_bus.send(generation_msg)

这里有个关键设计点:意图理解智能体同时承担了编排的职责。它知道整个流程需要经过哪些步骤,并按顺序调用下游智能体。这种设计适合流程相对固定的场景。如果流程变化频繁,建议把编排逻辑抽出来单独做一个编排智能体。

注意事项:意图理解智能体的输出一定要做校验。我遇到过模型把“帮我查一下天气”分类成了“知识检索”,结果检索智能体去知识库里找天气信息,当然找不到。后来加了一层规则校验,对于明显属于实时查询的意图,直接路由到对应的工具智能体,不走知识检索。

3.3 知识检索智能体的通信要点

知识检索智能体接收检索请求,返回相关文档片段。它的通信要点在于结果的结构化封装。不要直接把一堆文档原文丢回去,而是要做好排序、截断和格式化。

def handle_retrieval_agent(message): query = message.payload["query"] # 向量检索 raw_results = vector_store.search(query, top_k=10) # 重排序 reranked = rerank(query, raw_results, top_k=5) # 结构化封装 formatted_results = [] for doc in reranked: formatted_results.append({ "content": doc["content"][:500], # 截断,避免token爆炸 "source": doc["source"], "score": doc["score"] }) return AgentMessage( sender="retrieval_agent", receiver=message.sender, intent="retrieval_result", payload={"results": formatted_results, "query": query}, correlation_id=message.correlation_id )

这里有个经验:检索结果一定要带score。下游智能体可以根据score决定是否采信这个结果。如果所有结果的score都很低,生成智能体可以选择说“我没有找到相关信息”,而不是强行编造一个答案。

3.4 回复生成智能体的通信处理

回复生成智能体是最后一道关卡,它需要把上游传来的所有信息整合成最终回复。它的通信处理要点是容错和降级。

def handle_generation_agent(message): user_input = message.payload["user_input"] context = message.payload.get("context", {}) # 检查上下文完整性 if not context.get("results"): # 没有检索到结果,走降级逻辑 return generate_fallback_response(user_input) # 正常生成 prompt = build_prompt(user_input, context["results"]) response = llm_generate(prompt) return AgentMessage( sender="generation_agent", receiver=message.sender, intent="final_response", payload={"response": response}, correlation_id=message.correlation_id )

降级逻辑非常重要。在多智能体系统里,任何一个环节出问题都可能导致最终无法生成回复。如果没有降级机制,用户看到的就是一个错误提示。有了降级机制,至少能给出一个“抱歉,我暂时无法回答这个问题”的兜底回复,体验会好很多。

3.5 通信日志与可观测性建设

多智能体系统的调试难度远高于单智能体。当最终输出不对时,你很难一眼看出是哪个环节出了问题。所以通信日志是可观测性的基础。

我的做法是每条消息都记录完整的生命周期:创建时间、发送时间、接收时间、处理完成时间、状态变化。这些日志统一收集到一个地方,排查问题时可以按correlation_id把所有相关消息串起来看。

class MessageLogger: def log(self, message, event_type): log_entry = { "message_id": message.message_id, "correlation_id": message.correlation_id, "event": event_type, "sender": message.sender, "receiver": message.receiver, "intent": message.intent, "timestamp": time.time(), "status": message.status } logger.info(json.dumps(log_entry))

有了这套日志,排查问题时你可以清楚地看到:意图理解智能体在什么时间发出了检索请求,检索智能体花了多久返回,返回的结果是什么,生成智能体收到后做了什么处理。整条链路一目了然。

实操心得:日志里不要记录完整的payload,尤其是包含用户隐私数据的时候。记录payload的摘要或者hash就够了,需要详细信息时再通过message_id去查原始数据。这样既保证了可排查性,又避免了隐私泄露风险。

4. 常见问题与排查技巧实录

4.1 消息丢失与重复投递

问题表现:下游智能体没有收到上游发来的消息,或者同一条消息被处理了两次。

排查思路:首先检查消息队列的投递确认机制。如果用的是至少一次投递语义,重复是正常的,需要在消费端做幂等。如果消息完全丢失,检查发送方是否在发送前就发生了异常,导致消息根本没发出去。

解决方案:发送方在发送消息前先把消息持久化到本地,收到接收方的确认后再删除。接收方维护一个已处理message_id的集合,收到重复消息直接返回上次的处理结果。

4.2 循环等待与死锁

问题表现:智能体A等待智能体B的结果,智能体B又在等待智能体A的输入,双方僵持不下。

排查思路:画出智能体之间的依赖关系图,检查是否存在环形依赖。在多智能体系统里,环形依赖往往不是显式的,而是通过共享状态间接形成的。

解决方案:在设计阶段就避免环形依赖。如果业务逻辑确实需要双向通信,引入超时机制,任何一方等待超过阈值就主动放弃并返回错误。另外,可以用有向无环图来约束智能体之间的调用关系,在框架层面禁止反向调用。

4.3 上下文膨胀导致性能下降

问题表现:随着对话轮次增加,智能体之间传递的上下文越来越大,响应时间越来越长,token消耗越来越高。

排查思路:检查每次消息传递时payload的大小,看看是否有大量重复的、无关的信息被反复传递。

解决方案:实施上下文分层和摘要机制。全局上下文只保留最核心的信息,历史对话做摘要压缩,链路上下文只传递当前任务必需的部分。我通常会在每轮对话结束后,让一个专门的摘要智能体把本轮的关键信息提取出来,更新到全局上下文中,原始对话则不再传递。

4.4 智能体版本不兼容

问题表现:升级了某个智能体的输出格式后,下游智能体解析失败,整个流程中断。

排查思路:检查消息的intent和payload结构是否发生了变更,下游智能体的解析逻辑是否同步更新。

解决方案:消息格式引入版本号字段。发送方在消息里带上schema_version,接收方根据版本号选择对应的解析逻辑。新版本上线时,先让接收方支持新版本,再让发送方切换到新版本,实现平滑升级。

4.5 常见问题速查表

问题现象可能原因快速排查方法解决方向
下游收不到消息路由配置错误、队列满、网络分区检查消息日志中是否有发送记录修复路由、扩容队列、加超时重试
消息重复处理至少一次投递语义、确认丢失检查message_id是否重复消费端幂等处理
响应时间越来越长上下文膨胀、智能体性能退化统计每轮payload大小和耗时上下文分层、摘要压缩
输出自相矛盾多路结果未合并、冲突未解决按correlation_id追踪所有相关消息增加结果合并和冲突解决逻辑
系统卡死循环等待、死锁检查依赖关系图是否有环引入超时、打破环形依赖
升级后报错消息格式不兼容对比升级前后的消息结构引入版本号、灰度升级

4.6 几个我踩过的坑和对应的技巧

第一个坑:过度依赖同步通信。早期我设计的系统里,智能体之间全是同步调用,A调B,B调C,C调D,整条链路串行执行。结果一个请求要等好几秒。后来改成异步消息传递,A发消息给B后不等待,继续处理其他事情,B处理完再回调A。整体吞吐量提升了好几倍。但异步也带来了新的复杂度——你得处理消息的顺序问题。我的经验是,对于有严格顺序要求的消息,在payload里带一个sequence_number,接收方按序号处理。

第二个坑:忽略消息的优先级。所有消息一视同仁地排队处理,导致紧急的“取消任务”消息排在大量普通消息后面,等它被处理时任务早就完成了。后来我给消息加了priority字段,高优先级的消息走独立通道,确保及时处理。

第三个坑:没有做消息的TTL。有些消息产生后,如果过了很长时间才被处理,已经失去意义了。比如“查询当前库存”这个消息,如果5分钟后才被处理,查到的库存可能已经变了。给消息设置TTL,过期的消息直接丢弃,避免用陈旧数据做决策。

第四个坑:日志太多反而找不到问题。一开始我把所有消息的完整内容都记下来,结果日志文件每天几十个G,排查问题时在海量日志里翻找,效率极低。后来改成结构化日志加采样,正常消息只记元数据,异常消息记完整内容,排查效率大幅提升。

4.7 通信性能优化的几个实用手段

当智能体数量增多、通信量增大时,性能问题会逐渐暴露出来。除了前面提到的异步化和优先级队列,还有几个手段值得一试。

消息批处理:把短时间内发往同一个接收方的多条消息合并成一条批量消息,减少通信次数。比如检索智能体在100毫秒内收到了5个查询请求,可以合并成一次批量检索,然后分别返回结果。

结果缓存:对于相同的请求,如果短时间内重复出现,直接返回缓存结果。比如两个用户同时问同一个问题,意图理解智能体识别出这是相同的查询,可以直接复用检索结果,不用重复检索。

通信压缩:对于payload较大的消息,在传输前做压缩。尤其是包含大量文本的检索结果,压缩后能显著减少传输时间和存储开销。

连接池化:如果智能体之间是通过HTTP或RPC通信的,维护长连接比每次新建连接效率高得多。连接池的大小根据并发量调整,太小会成为瓶颈,太大浪费资源。

这些优化手段不是一开始就要全部用上,而是根据实际性能数据逐步引入。过早优化会让系统复杂度飙升,反而得不偿失。我的建议是先把基础通信框架搭好,跑通业务流程,然后根据监控数据找到瓶颈点,有针对性地优化。

4.8 关于智能体间通信的一点个人体会

做了几个多智能体项目之后,我最大的体会是:通信设计的好坏,不取决于技术有多先进,而取决于对业务场景的理解有多深。一个简单的共享黑板模式,如果对业务理解到位,可能比一套复杂的消息队列方案效果更好。反过来,如果对业务场景理解不透,再精巧的通信机制也解决不了实际问题。

另外,智能体间通信的设计不是一蹴而就的,它需要随着业务的发展不断演进。一开始可能只有两三个智能体,用最简单的点对点通信就够了。随着智能体数量增加、协作逻辑变复杂,再逐步引入消息队列、编排器、服务发现等机制。不要一开始就追求大而全的架构,那样只会让自己陷入过度设计的泥潭。

最后分享一个实用小技巧:在设计通信协议时,先用手绘的方式把智能体之间的消息流画出来,标清楚每个消息的发送方、接收方、内容和触发条件。这张图比任何文档都直观,而且能帮你发现很多设计阶段就能避免的问题。等图画清楚了,再动手写代码,效率会高很多。

RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进