news 2026/10/31 4:07:11

Agent通信格式设计:多Agent协作的隐形地基与工程实践

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
Agent通信格式设计:多Agent协作的隐形地基与工程实践

1. 为什么应用层通信格式是 Agent 落地的隐形地基

做过 Agent 项目的人大概都有过这种体验:单 Agent 跑 demo 的时候丝滑得不行,一旦上多 Agent 协作,或者把 LLM 接进已有的业务系统,各种诡异问题就冒出来了——消息丢了、上下文串了、工具调用参数对不上、流式输出拼不回去。排查半天,最后发现根子不在模型,而在通信格式没设计好。

我前后参与过几个 Agent 平台的建设,从最早的"两个 Agent 用 JSON 硬怼"到后来做统一的通信协议层,踩的坑足够写一本小册子。这篇就围绕LLM/Agent 之间的应用层通信格式这个主题,把我在实际项目里沉淀下来的设计思路、字段约定、踩坑经验完整拆一遍。不管你是刚接触 Agent 开发,还是已经在做多 Agent 编排,只要涉及"两个智能体之间怎么说话"这件事,这里的内容都能直接拿去参考。

先把范围界定清楚。这里说的"应用层通信格式",指的是 Agent 与 Agent、Agent 与 LLM、Agent 与工具/外部系统之间,在应用层约定的消息结构。它不关心底层是 HTTP、WebSocket 还是消息队列,关心的是消息长什么样、字段怎么定义、语义怎么表达、状态怎么流转。这一层设计得好不好,直接决定了你的 Agent 系统是可维护的工程,还是一坨改一处崩三处的意大利面。

我见过太多团队一上来就急着接模型、调 prompt,通信格式随手用个{"role": "user", "content": "..."}就开干。等到要加多轮工具调用、要支持多 Agent 广播、要做可观测性的时候,才发现原来的结构根本撑不住,只能推倒重来。所以我的建议很直接:在写第一行 Agent 业务代码之前,先把通信格式定下来。这件事的投入产出比高得离谱。

2. 通信格式的整体设计思路与选型考量

2.1 从"能跑"到"能扩展"的分水岭

最朴素的 Agent 通信就是 OpenAI 那套 Chat Completions 格式:一个 messages 数组,每个元素有 role 和 content。单轮对话、简单工具调用,这套结构确实够用。但它有几个天生的短板,一旦系统复杂起来就会暴露。

第一个短板是角色语义太窄。role 只有 system、user、assistant、tool 四种,多 Agent 场景下你根本分不清这条消息是哪个 Agent 发的、发给谁的。第二个短板是缺少元数据位。消息 ID、时间戳、追踪 ID、优先级这些工程上必需的东西,标准格式里没有位置放,只能塞进 content 里当字符串,解析起来极其恶心。第三个短板是状态表达缺失。一条消息是"思考中"、"等待工具返回"还是"已完成",格式本身不表达,全靠业务层自己维护状态机。

所以我的设计原则是:在兼容主流 LLM 接口格式的基础上,做一层信封(Envelope)封装。信封负责工程元数据和路由,内层 payload 保持和模型接口兼容。这样既能直接喂给模型,又能承载多 Agent 协作需要的额外信息。

2.2 信封 + 载荷的分层结构

具体来说,我把一条 Agent 消息拆成三层:

  • 传输层信封(Envelope):消息 ID、会话 ID、发送方、接收方、时间戳、追踪链路、优先级、过期时间。这一层和业务无关,纯工程。
  • 语义层头(Header):消息类型(请求/响应/事件/心跳)、意图标签、期望的响应格式、超时设置、重试策略。
  • 业务载荷(Payload):真正的对话内容、工具调用、结构化数据。这一层尽量贴近模型原生格式。

为什么要这么分?因为这三层的变更频率完全不同。传输层几乎不变,语义层偶尔加字段,业务载荷天天在改。分层之后,改业务不会动到路由逻辑,加路由字段也不会污染模型输入。这是典型的关注点分离,在通信格式设计里同样适用。

2.3 为什么不用现成的 RPC 框架直接套

有朋友会问:gRPC、Thrift 这些成熟的 RPC 框架不香吗,为什么要自己设计格式?

我的回答是:RPC 框架解决的是"怎么调用",Agent 通信要解决的是"怎么协作"。RPC 的请求-响应模型是同步的、强类型的、一次性的,而 Agent 之间的交互是异步的、流式的、可能多轮的、可能一对多的。你当然可以用 gRPC 做底层传输,但上层还是得定义一套 Agent 语义的格式。而且 LLM 的输出天然是自然语言和半结构化 JSON 混合,强类型 schema 反而会成为负担。

我的实践是:底层传输用成熟方案(HTTP/WebSocket/gRPC 都行),应用层格式自己定义。两者解耦,各司其职。

3. 核心字段设计与语义约定详解

3.1 消息信封的必备字段

先上一份我在多个项目里验证过的信封字段定义,用 JSON 表示:

{ "msg_id": "01HX2K...", "session_id": "sess_abc123", "trace_id": "trace_xyz789", "parent_msg_id": "01HX2J...", "from": {"agent_id": "planner", "instance": "planner-01"}, "to": {"agent_id": "executor", "instance": "executor-03"}, "timestamp": 1718000000123, "ttl_ms": 30000, "priority": 5, "header": { "type": "request", "intent": "tool_call", "expect_reply": true, "reply_format": "json_schema", "timeout_ms": 15000 }, "payload": { } }

逐个说下关键字段的设计意图。

msg_id用 ULID 而不是 UUID,是因为 ULID 自带时间戳前缀,天然有序,做日志排序和分页特别方便,而且字符串长度比 UUID 短。session_id标识一次完整的会话,多 Agent 协作时所有相关消息共享同一个 session。trace_id和parent_msg_id是给可观测性用的,有了这两个字段,你可以把一次复杂的多 Agent 调用还原成一棵完整的调用树,排查问题时直接看树就行。

from和to用对象而不是字符串,是为了给未来留扩展位。现在只有 agent_id 和 instance,将来要加租户、区域、版本号,直接往对象里塞就行,不用改格式。

ttl_ms和priority这两个字段很多人会忽略,但在生产环境里极其重要。Agent 之间的调用可能因为模型慢、工具卡而堆积,没有 TTL 的消息会一直占着资源。priority 则用于在队列积压时做分级处理,比如用户直接触发的请求优先级高于后台批处理任务。

3.2 语义层 header 的类型体系

header.type 我定义了五种基本类型,覆盖绝大多数场景:

类型用途是否需要回复
request请求对方执行某个动作是
response对 request 的响应否
event单向事件通知,如状态变更否
stream流式数据分片否(有结束标记)
heartbeat心跳保活否

这里有个容易踩的坑:很多人把流式输出当成多次 response。这是错的。流式应该用独立的 stream 类型,每条分片带一个seq序号和is_final标记,接收方按 seq 拼接。如果混用 response,接收方无法区分"这是一次完整响应"还是"这是响应的一部分",逻辑会乱。

intent字段是给路由和策略用的。比如 intent 是tool_call的消息,路由层可以优先调度到有对应工具权限的 Agent;intent 是final_answer的,直接返回给用户。这个字段让路由逻辑和业务逻辑解耦,非常实用。

3.3 payload 的三种形态

payload 我按内容形态分三种,用payload.kind区分:

  • text:纯文本,最常见,直接对应模型的 content。
  • structured:结构化数据,比如工具调用的参数、Agent 之间的状态同步。
  • multimodal:多模态内容,图片、音频等,用引用或 base64 承载。

text 形态下,payload 结构就是{"kind": "text", "content": "..."},简单直接。structured 形态下,我强烈建议带一个schema_ref字段,指向一个 schema 定义(可以是 URL,也可以是注册表里的 ID),这样接收方能校验数据合法性,而不是拿到一坨不知道结构的 JSON 硬解析。

提示:payload 里千万不要放二进制大对象。图片、文件这类内容,统一走对象存储,payload 里只放引用 URL 和校验哈希。我见过把 base64 图片塞进消息体的,一条消息几 MB,消息队列直接被打爆。

4. 多 Agent 协作场景下的通信模式实现

4.1 请求-响应模式的完整实现

这是最基础的模式,但要做好也不简单。核心难点在于异步响应如何正确路由回发起方。

我的做法是:发起方发送 request 时,在 header 里带上reply_to字段(可以是队列名、回调 URL 或 Agent 地址),接收方处理完后,用同一个session_id和parent_msg_id指向原请求,发回 response。发起方根据 parent_msg_id 匹配等待中的请求。

这里有个细节:超时处理。发起方发出请求后,启动一个定时器,超时后不再等待,但要发一条event类型的取消消息给接收方,让对方停止处理。否则接收方可能还在傻傻地跑一个已经没人要的任务,白白消耗模型 token。

# 发起方伪代码 pending = {} def send_request(to_agent, payload, timeout_ms=15000): msg = build_envelope( to=to_agent, header={"type": "request", "expect_reply": True, "timeout_ms": timeout_ms}, payload=payload ) pending[msg["msg_id"]] = {"event": threading.Event(), "result": None} transport.send(msg) if not pending[msg["msg_id"]]["event"].wait(timeout_ms / 1000): # 超时,发取消事件 transport.send(build_cancel(msg["msg_id"], to_agent)) raise TimeoutError(f"agent {to_agent} timeout") return pending.pop(msg["msg_id"])["result"]

4.2 广播与订阅模式的落地

多 Agent 系统里经常需要"一个 Agent 发消息,多个 Agent 都关心"。比如规划 Agent 产出了任务分解,执行 Agent、监控 Agent、日志 Agent 都需要知道。

广播模式的关键是订阅关系要独立于消息格式。我的做法是:消息格式里只写to是broadcast或者一个 topic 名,具体的订阅关系由消息中间件维护。这样 Agent 之间不需要互相知道对方存在,解耦彻底。

但广播有个陷阱:消息风暴。如果每个 Agent 收到广播后又广播,很容易指数级放大。我的经验是给广播消息加一个hop_count字段,每转发一次减一,减到零就丢弃。同时限制广播的 topic 数量,不要什么都广播。

4.3 流式输出的分片与重组

LLM 的流式输出是刚需,但跨 Agent 传递流式数据需要额外设计。我的方案是:

  • 每条 stream 消息带stream_id、seq、is_final三个字段。
  • 接收方按 stream_id 分组,按 seq 排序,收到 is_final 后拼接完整内容。
  • 如果中间丢包(seq 不连续),接收方主动发一条 request 请求重传缺失分片。
{ "header": {"type": "stream"}, "payload": { "kind": "text", "stream_id": "st_001", "seq": 3, "is_final": false, "content": "正在分析" } }

注意:流式分片不要按固定字节数切,要按语义边界切(比如按 token 或按句子)。按字节切会把一个 UTF-8 字符切成两半,接收方拼出来是乱码。这个坑我在早期项目里踩过,排查了一下午。

5. 实操过程与关键环节的完整实现

5.1 从零搭建一套通信格式的步骤

假设你现在要在一个新项目里落地这套格式,我按实际顺序给你捋一遍。

第一步,定义 schema 并注册。用 JSON Schema 或 Protobuf 把信封、header、payload 的结构写清楚,放到一个共享的 schema 仓库里。所有 Agent 都从这个仓库拉取 schema 做校验。这一步看似繁琐,但能省掉后面无数的"字段对不上"问题。

第二步,实现序列化/反序列化层。我推荐 JSON 作为默认格式,因为可读性好、调试方便、和 LLM 生态天然兼容。性能敏感的场景可以上 MessagePack 或 Protobuf,但要注意 LLM 输出的 JSON 可能不规范(比如带 markdown 代码块标记),需要做容错解析。

第三步,实现传输适配层。把 HTTP、WebSocket、消息队列的差异封装起来,上层只看到统一的 send/receive 接口。这样将来换传输方案,业务代码不用动。

第四步,实现路由与匹配。根据 to 字段和 session_id 把消息投递到正确的 Agent 实例。这里要注意 Agent 实例可能动态扩缩容,路由表要能实时更新。

第五步,接入可观测性。每条消息的收发都打点,记录 msg_id、trace_id、耗时、大小。用 trace_id 把一次完整调用串起来,出问题时能快速定位。

5.2 参数选择的计算过程

通信格式里几个关键参数不能拍脑袋定,得算。

TTL 怎么定?取决于下游 Agent 的最长处理时间。假设你的 Agent 调用一次 LLM 平均 3 秒,最坏情况重试 3 次,加上工具调用 5 秒,那么 TTL 至少是 3×3+5=14 秒,留一倍余量取 30 秒比较稳妥。TTL 太短会误杀正常请求,太长会拖垮资源。

消息体大小上限怎么定?假设你的消息队列单条上限是 1MB,那么 payload 里的文本内容要控制在 800KB 以内(留出信封和编码开销)。按中文 UTF-8 每字 3 字节算,大约 26 万字。超过这个长度就该考虑分片或走对象存储了。

并发连接数怎么估?假设有 N 个 Agent 实例,每个实例平均同时处理 M 个会话,那么需要的连接数大约是 N×M。做容量规划时按峰值再乘 1.5 倍。

5.3 一次完整的多 Agent 调用实录

我拿一个实际场景走一遍:用户问"帮我查下北京明天的天气,如果下雨就提醒我带伞"。

规划 Agent 收到用户请求,解析出两个子任务:查天气、条件提醒。它发两条 request 给执行 Agent,session_id 相同,parent_msg_id 都指向用户请求。

执行 Agent 收到查天气的请求,调用天气工具,拿到结果后发 response 回规划 Agent。同时它把"查天气"这个子任务的状态用 event 广播给监控 Agent。

规划 Agent 收到天气结果,判断是否下雨,如果下雨,再发一条 request 给提醒 Agent。提醒 Agent 生成提醒文案,发 response 回规划 Agent。

规划 Agent 汇总所有结果,生成最终回答,发 response 给用户接口。

整个过程涉及 6 条消息,全部共享同一个 trace_id。出问题时,按 trace_id 一查,整条链路清清楚楚。这就是通信格式设计到位带来的可观测性红利。

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

6.1 消息乱序与重复的处理

分布式环境下,消息乱序和重复是常态,不是异常。我的处理原则是:格式层面提供幂等和排序的依据,业务层面做去重和重排。

幂等靠 msg_id,接收方维护一个最近处理过的 msg_id 集合(用布隆过滤器省内存),重复的直接丢弃。排序靠 seq 或 timestamp,但要注意不同机器的时钟可能不一致,所以排序优先用逻辑序号而不是物理时间。

6.2 上下文串台的排查

多 Agent 并发时,最恶心的 bug 是 A 会话的消息跑到了 B 会话里。根因通常是 session_id 没正确传递,或者 Agent 实例复用了全局状态。

排查方法:在每条消息的日志里强制打印 session_id,出问题时 grep 一下,看是哪一步 session_id 变了。预防方法:Agent 处理消息时,把 session_id 作为上下文的一部分显式传递,不要依赖全局变量或线程本地存储。

6.3 常见问题速查表

现象可能原因排查方向
消息丢失TTL 过期、队列满、路由错误查 TTL 配置、队列深度、路由表
响应超时下游慢、死锁、重试风暴查下游耗时分布、调用链、重试次数
内容乱码编码不一致、分片切错统一 UTF-8、检查分片边界
上下文串台session_id 丢失、状态复用检查 session 传递、实例隔离
消息重复重试未幂等、至少一次投递加 msg_id 去重
流式拼接错乱seq 缺失、is_final 未标记检查分片字段完整性

6.4 几个我踩过的坑

坑一:把 trace_id 当成 session_id 用。这俩不是一回事。trace_id 标识一次调用链路,session_id 标识一次会话。一次会话可能包含多次调用,一个调用也可能跨多个会话(比如 Agent 之间转发)。混用会导致追踪和会话管理都乱套。

坑二:payload 里塞了不该塞的东西。有人把整个对话历史塞进每条消息的 payload,消息体越来越大,最后几 MB 一条。正确做法是 payload 只放增量,历史由会话存储统一管理,消息里只带引用。

坑三:忽略消息大小对模型的影响。通信格式里的字段名、嵌套层级都会变成 token 消耗。如果你的 Agent 之间传递的消息要喂给模型,格式越精简越好。我见过用超长字段名的,光字段名就吃掉几百 token,纯浪费。

坑四:没有版本号。通信格式一定会演进,没有版本号就没法平滑升级。我的做法是在信封里加schema_version字段,接收方根据版本号选择对应的解析逻辑,新旧版本共存一段时间再下线旧的。

7. 格式演进与向后兼容的实战策略

通信格式不是定完就一劳永逸的,业务在变,格式必然要演进。关键是演进过程不能让现有系统崩掉。

我的策略是三条:加字段不删字段、新老版本并行、灰度切换。

加字段永远比删字段安全。新增字段时给默认值,老版本接收方忽略不认识的字段即可。要删字段时,先标记为 deprecated,观察一段时间确认没人用了再删。

版本升级用schema_version控制。接收方同时支持 N 和 N+1 两个版本,发送方逐步切到新版本。等所有发送方都切完,再下线旧版本支持。

灰度切换时,先在一个非核心的 Agent 上试新格式,观察一段时间没问题再推广。我一般会保留一个"格式开关",出问题能一键回滚到旧格式。

这套策略听起来保守,但在生产环境里,稳定压倒一切。通信格式是系统的地基,地基出问题,上面全塌。宁可演进慢一点,也不要冒进。

8. 我在实际项目中的几点体会

做了这么多 Agent 项目,关于通信格式,我最深的体会是:它看起来是技术细节,实际上是架构决策。格式定得好,Agent 之间职责清晰、边界明确、可观测性强;定得差,所有 Agent 搅在一起,改一处崩三处。

另一个体会是不要过度设计。我早期做过一个特别复杂的格式,支持各种花哨的语义,结果团队没人愿意用,最后还是回归到简单版本。格式的价值在于被使用,不在于功能多。从最小可用开始,按需演进,比一上来就搞大而全靠谱得多。

最后分享一个小技巧:把通信格式的 schema 当成文档来写。每个字段都写清楚用途、取值范围、示例。新人接手时,看 schema 就能理解系统怎么运转,比看代码快得多。这件事花不了多少时间,但收益是长期的。

如果你正在做 Agent 项目,建议现在就去看看你的消息格式,问自己三个问题:多 Agent 场景下能分清谁发的吗?出问题能追踪到完整链路吗?加个新字段需要改几处代码?如果答案让你犹豫,那这套格式可能该重构了。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/31 4:07:11

Agent通信格式设计:多Agent协作中消息传递的避坑指南

1. 为什么应用层通信格式是Agent系统里最容易被低估的一环做Agent开发的人,十有八九把精力砸在提示词工程、工具调用、记忆管理、编排框架选型上,很少有人一开始就认真琢磨一件事:两个LLM或者两个Agent之间,消息到底长什么样。我见…

作者头像 李华
网站建设 2026/10/31 4:07:07

微软Copilot入口收缩:AI从工具调用转向环境感知的实测分析

1. 从"入口消失"说起:这次更新到底动了什么微软这几年的更新节奏,说实话已经让很多人麻木了。但这次不太一样——不是加了个新功能,而是把原本摆在台面上的东西悄悄挪走了。Copilot 按钮从任务栏、从 Edge 侧边栏、从 Office 顶栏的…

作者头像 李华
网站建设 2026/10/31 4:06:45

生产管理如何破解订单延期难题

1. 生产管理为何成为订单延期的关键瓶颈生产管理作为制造业的核心环节,直接影响着订单交付的准时率。根据美国生产与库存控制协会(APICS)的调研数据,超过68%的订单延期问题源于生产管理环节的失控。这种失控往往表现为三个典型症状…

作者头像 李华
网站建设 2026/10/31 4:06:13

微软淡化AI入口背后:Windows、Office、Teams工作新系统拆解

1. 从"AI入口消失"说起:这次更新到底动了什么微软最近这波操作,我第一反应是"终于想明白了"。如果你最近打开Windows 11的更新推送,或者留意到Office和Teams的界面变化,会发现一个很微妙的信号:那…

作者头像 李华
网站建设 2026/10/31 4:05:51

从存储块到硬件FIFO:PH1 ERAM配置与跨时钟域避坑

把 ERAM 只当成一块“能存数的地方”,是很多人上手 PH1 时最容易形成的思维定式。实际上这颗器件里的 ERAM 内部集成了一套可以直接接管读写指针的硬件 FIFO 控制器,配合多种配置模式,它能在不占一个 LUT、不占一个触发器的前提下&#xff0c…

作者头像 李华
网站建设 2026/10/31 4:05:50

AI时代前端工程师的核心竞争力与进化路径

1. 当AI开始写Vue组件:前端工程师的危机还是转机?最近在技术社区看到不少同行在讨论一个现象:现在AI已经能自动生成Vue组件代码,低代码平台通过拖拽就能完成页面搭建。作为一个有8年实战经验的前端工程师,我不得不承认…

作者头像 李华