跳到正文

分布式通信与协议栈 ​

一句话:Agent 系统的通信不是"选一个消息中间件",而是按四档语义谱系(请求-响应 → 流式 → 任务句柄 → 持久通道)匹配耦合度,按三个通信面(能力面/协作面/内构面)划分治理边界——每档解决不同问题,不是替代关系。

适用:2026 年 Agent 系统分布式通信实践;具体协议(MCP、A2A、MQ)能力以官方规范为准。

为什么需要 ​

Agent 系统的通信复杂度远超传统微服务:Agent 调用工具可能是毫秒级查询也可能是分钟级任务,Agent 之间的协作天然长跑(跨模型推理、跨系统审批),内部组件之间既有同步 RPC 也有异步事件流。如果统一用 HTTP 请求-响应,长任务会被超时切断;如果统一用消息队列,短查询的延迟不可接受。

更深层的问题是耦合度不同。请求-响应在响应期内耦合,流式在连接期内耦合,任务句柄在执行期内耦合,持久通道在生产消费期内耦合。混用会导致该解耦的没解耦(长任务阻塞连接池)、不该解耦的强行解耦(简单查询走消息队列增加延迟)。通信面不同,治理要求也不同——能力面(Agent 调工具)需要鉴权和超时,协作面(Agent 间通信)需要任务关联和预算,内构面(组件间)需要背压和重播。

核心机制 ​

四档交互语义谱系 ​

档位耦合维度典型场景超时处理失败模式
S1 请求-响应响应期内耦合简单工具查询、模型单次调用连接超时调用方阻塞等待
S2 请求域流式连接期内耦合流式输出、长文本生成流式空闲超时连接断开流中断
S3 任务句柄执行与请求解耦分钟到小时级任务任务级超时句柄可查询状态
S4 持久通道生产与消费解耦事件驱动、跨系统通知消息 TTL消费方可重播

关键认知:四档不是替代关系,是不同耦合度的解决方案。S1 适合毫秒级查询,S4 适合需要削峰和重播的场景。选型原则是最低充分语义——能用 S1 解决的不升级到 S2,能用 S3 解决的不强行用 S4。

三个通信面:治理边界不同 ​

通信面参与者协议任务特征治理重点
能力面Agent ↔ 工具MCP毫秒查询到分钟任务工具鉴权、超时、幂等
协作面Agent ↔ AgentA2A任务天然长跑任务关联、预算传递、身份交换
内构面组件之间MQ/事件流/RPC无标准协议背压、重播、通道键治理粒度

关键区分:三个通信面的治理要求不同。能力面关注"这个 Agent 能否调用这个工具",协作面关注"这个任务属于谁、预算从哪来",内构面关注"消费慢了怎么反馈、消息能否重播"。不能把能力面的鉴权逻辑搬到协作面,也不能把内构面的通道设计混入能力面。

通信契约六要素 ​

要素含义缺失后果
标识谁发的、属于哪个任务消息无法关联到任务,审计缺失
顺序消息间的先后关系乱序导致状态不一致
投递与去重至少一次 / 恰好一次重复执行或丢失消息
续传点断连后从哪里继续断连后无法恢复,从头重放
生命周期消息何时过期/删除无限堆积或过早丢失
背压消费慢时如何反馈消费方被压垮或无限堆积

生产级要求:

  • 标识:每条消息必须携带 {agent_id, task_id, message_id},用于关联和审计
  • 顺序:同一任务内的消息必须保序,不同任务可并行
  • 投递与去重:关键操作必须幂等(至少一次投递 + 接收方去重),非关键操作可接受重复
  • 续传点:流式输出必须支持断点续传,任务句柄必须支持状态查询
  • 生命周期:消息 TTL 按业务需求设定,过期自动删除或归档
  • 背压:消费慢时通过限流、队列深度告警、动态扩容反馈

手脑分离架构 ​

手脑分离的核心原则:

  • Brain(推理与任务控制)负责决策,Hands(文件/命令/浏览器)负责执行
  • 已派发的工具调用按原执行标识确认结果——即使超时,也要通过执行标识查询结果,而不是直接重放
  • 超时保留 unknown 而不是直接重放——直接重放可能导致重复执行(如已创建的文件被再次创建)

选型原则 ​

原则说明反例
每通信面取最低充分语义起步能力面短查询用 S1,长任务用 S3所有调用都走消息队列
协议无状态 ≠ 业务无状态HTTP 无状态但任务有状态,需要句柄追踪认为 HTTP 请求结束任务就结束
流式输出 ≠ 任务可离线运行流式输出仍依赖连接,连接断开流中断认为流式输出可以后台运行
内构面通道键决定治理粒度通道键是 {tenant} 还是 {tenant}:{agent} 决定隔离粒度所有租户共用一个通道

实战场景 ​

案例:重播之后钱扣了两次——至少一次投递没配幂等消费 ​

现象:售后退款 Agent 在一段 40 分钟的网络抖动后把同一笔退款执行了两次,重复出账 118 笔(经验值,需校准),T+1 对账才暴露,冲正加客诉处理两天。通信契约写的是至少一次投递 + 接收方去重,线上只落了前半句。

排查过程:分层取证,先分清这是协议问题、实现问题还是容量问题。

证据来源观察判定
L0 模型响应tool_calls[] 只有一条,id 唯一排除模型重复生成
L1 工具日志同一 tool_call_id 两次到达,间隔 31 秒重复来自投递侧
内构面消费组抖动后 rebalance,未 ack 的消息按重播语义重新下发符合契约,协议没坏
去重键取的是 message_id,每次重投都是新值实现问题:去重粒度错

队列深度与消费速率全程正常,排除容量。根因是去重键选了传输层的 message_id——契约的标识要求是 {agent_id, task_id, message_id} 三元组,执行级稳定键必须来自任务侧,不能挂在消息侧。

对症做法:写工具的幂等键换成 {tenant}:{task_id}:{args_hash},存储侧加唯一约束,重复到达时直接返回首次执行的结果,不再执行一次;重试只对读操作或无副作用的创建开放,有副作用的按原执行标识查结果,超时保留 unknown 而不是直接重放;重播消息先查执行终态,再决定是否执行。

yaml
capability_plane:
  - { tool: refund_write, semantic: S3, idempotency_key: "{tenant}:{task_id}:{args_hash}",
      on_duplicate: return_first_result, retry_scope: [read, side_effect_free_create] }
internal_plane:
  - { channel: refund_events, semantic: S4, key: "{tenant}:{agent_id}",
      replay: true, retention_h: 24, dedup_on: [execution_id, final_state] }

复盘要点:至少一次投递约定的是投递次数,执行次数由接收方去重决定。验收判据是能否说出“这条消息投了 3 次、钱只出了一次”,说不出就还没做到幂等消费。

场景:只记 200 不记业务终态——“成功但没结果” ​

现象:报表生成链路一周内有 6% 的任务(经验值,需校准)落在“没有失败记录、也没有产物”:编排方收到 202 和 200,判定成功并向用户播报,产物始终不存在;用户手动重试再生成一份,重复计算按卡时计在推理层。

排查过程:分层的价值就是定位故障——报文解析错在 L0,工具发现错在 L1,任务状态错在 L2。逐层核对:L0 的 tool_calls 与回填 id 对齐正常,不是解析问题;L1 的 tools/list、tools/call 都返回成功,不是发现问题;落到 L2,任务状态只写到 submitted、working,没有终态写入。再看 Trace:traceparent 跨层透传了,但状态字段记的是 HTTP 2xx/4xx,taskId 和 tool_call_id 没挂进 span,成功判定读的是传输层事实。时间线上,Hands 侧在超时后 8 秒完成并按执行标识回填,Brain 已按总超时结束本轮,回填被丢弃——协议无状态不等于业务无状态,请求一结束就认为任务结束,是这类事故的共同形状。

对症做法:成功判定改为“业务终态 + 产物引用”两个条件同时成立,落库 {task_id, execution_id, final_state, artifact_ref};拿不到终态一律记 unknown 进补偿队列,按原执行标识查结果,不重放;unknown 单独设指标(存量与收敛时长),不与 failed 合并统计——合并等于用重放换确定性;200 只作为准入成功的证据,不参与业务成功判定。

复盘要点:200 是传输层事实,业务是否完成要回到终态与产物。第三态没有位置时只有两种填补方式:误报成功,或者盲重放——前者丢信任,后者丢钱。

场景:上游已放弃,下游还在跑——跨 Agent 调用没有 deadline 传播 ​

现象:编排 Agent 把代码评审委派给子 Agent(协作面,A2A),子任务实际跑几十分钟;编排方按同步语义等待,30 秒(示意)超时后重试 3 次。下游收到 3 份任务并各自跑到底,队列被同一件事占住三倍时长,用户在结果页看到三份近似产物,预算按三倍消耗。

排查过程:

证据观察判定
接入与网关全部 202,无 4xx协议层没报错,不代表语义正确
L2 任务表3 个不同 taskId,同一 contextId,入参完全一致重复创建,不是重放同一任务
span 属性没有剩余预算或 deadline 字段下游无法知道上游已经放弃
并发与队列单租户并发上限未触顶排除容量

正文的失败模式在这里逐条命中:把 A2A 当同步 RPC、轮询没有退避。缺的是契约里那一栏——协作面的治理重点是任务关联与预算传递,超时按 L2 的任务 deadline 设,而不是按连接设。

对症做法:请求下行携带剩余 deadline(deadline_unix_ms 随 traceparent 透传),下游在自身预算耗尽前主动返回 canceled,不等对端超时;任务创建带幂等键,同一 {contextId, args_hash} 只允许一份在跑;重试预算全局只有一份,跨层相乘必须能在日志里数出来;长任务一律走 S3 返回句柄,同步等待只用于秒级工具;超过预算返回未完成清单,不继续盲试。

复盘要点:取消是跨 Agent 的显式语义,不传 deadline 的下游只能选择做完或者猜。出现三份产物不是重试次数太多,是没有一份全局重试预算和一条 deadline 链。

最小实践 ​

yaml
communication_patterns:
  capability_plane:  # Agent ↔ 工具 (MCP)
    - { tool: file_read, semantic: S1, timeout_ms: 5000, idempotent: true }
    - { tool: code_gen, semantic: S3, timeout_s: 300, handle: task_id }
  collaboration_plane:  # Agent ↔ Agent (A2A)
    - { task: code_review, semantic: S3, budget_propagation: true, identity: oauth2_exchange }
  internal_plane:  # 组件之间
    - { channel: task_events, semantic: S4, key: "{tenant}:{agent_id}",
        retention_h: 24, replay: true, backpressure: depth_alert_10000 }
  contract:
    id: { agent_id, task_id, message_id }  # 必须完整
    ordering: same_task_ordered
    delivery: at_least_once + receiver_dedup
    resumption: stream_checkpoint + handle_query

验收标准:三面分离各用合适语义、契约六要素完整、手脑分离超时保留 unknown、通道键含租户维度。

生产行动清单 ​

  • [ ] 按四档语义谱系评估所有通信路径,选择最低充分语义
  • [ ] 三个通信面分离:能力面(MCP)、协作面(A2A)、内构面(MQ/事件流/RPC)
  • [ ] 通信契约六要素逐项检查:标识、顺序、投递、续传、生命周期、背压
  • [ ] 手脑分离架构:Brain 负责推理,Hands 负责执行,超时保留 unknown
  • [ ] 内构面通道键包含租户维度,决定治理粒度
  • [ ] 流式输出支持断点续传,任务句柄支持状态查询
  • [ ] 背压机制上线:限流、队列深度告警、动态扩容

边界与误区 ​

适用场景不适用场景
Agent 系统需要多种通信模式共存纯同步请求-响应的简单应用
需要跨系统协作的长任务场景毫秒级响应的实时推理服务
多租户需要隔离治理的平台单租户内部工具

常见误区:

  1. "统一用 HTTP 请求-响应" —— 长任务会被超时切断,该解耦的没解耦
  2. "统一用消息队列" —— 短查询延迟不可接受,不该解耦的强行解耦
  3. "协议无状态 = 业务无状态" —— HTTP 无状态但任务有状态,需要句柄追踪
  4. "流式输出 = 任务可离线运行" —— 流式输出仍依赖连接,连接断开流中断
  5. "超时直接重放" —— 直接重放可能导致重复执行,应保留 unknown 并查询结果
  6. "内构面通道键无所谓" —— 通道键决定治理粒度,必须包含租户维度

讲清楚 ​

分布式通信模式的核心是按四档语义谱系匹配耦合度:S1 请求-响应(响应期内耦合)、S2 流式(连接期内耦合)、S3 任务句柄(执行与请求解耦)、S4 持久通道(生产与消费解耦),四档不是替代关系而是不同耦合度的解决方案。三个通信面划分治理边界:能力面(Agent↔工具,MCP 协议)、协作面(Agent↔Agent,A2A 协议)、内构面(组件之间,MQ/事件流/RPC),各自治理要求不同。通信契约六要素(标识、顺序、投递与去重、续传点、生命周期、背压)必须完整。手脑分离架构中,Brain 负责推理,Hands 负责执行,已派发工具按原执行标识确认结果,超时保留 unknown 而不是直接重放。选型原则是每通信面取最低充分语义起步,协议无状态不等于业务无状态,流式输出不等于任务可离线运行,内构面通道键决定治理粒度。

下篇 · 协议栈与互操作 ​

一句话:L0–L3 是四份各管一段的报文合同——模型怎么提出调用、工具怎么被发现、Agent 怎么委派任务、执行过程怎么流到界面;统一报文能降低接入成本,但替代不了权限、状态与治理。

协议字段、状态取值与传输能力都随版本变化,本篇只讲结构,具体以目标版本的官方文档为准。工具设计原则见 07 工具、MCP 与 Skill,多 Agent 的组织模式与拆不拆判据见 07b 多 Agent 协作与 A2A。

为什么需要 ​

协议知识平时被框架藏起来了:写一个 tool() 就以为工具接好了,直到换模型、上网关、接第三方 Agent 才暴露。最常见的事故不是“不会调”,而是三层假设错位——把 arguments 当对象直接读、网关翻译时漏了 max_tokens、把 A2A 当同步 RPC 等结果、以为 MCP 连上就等于隔离。

这些问题的共同点是:报文格式是明面上的合同,而合同背后的生命周期、身份和失败语义没人写进契约。协议栈这一篇只回答三件事:每层解决什么问题、消息长什么样、坏了会长什么样。

核心机制 ​

四层边界:教学分层,不是严格依赖 ​

这张图是教学分层,不是严格依赖:一次请求可以从 L3 进来,也可以由定时任务从 L2 发起;L2 的执行者自己仍要用 L0 和 L1。分层的价值是定位故障——报文解析错在 L0,工具发现错在 L1,任务状态错在 L2,界面不同步错在 L3。

L0 LLM-API:报文就是合同 ​

两家的工具调用是同一个两轮循环,区别只在报文长在哪。第一轮模型只“说”要调什么,宿主执行后把结果按 id 回填,第二轮模型才看见事实。

这张时序图的关键在“回填”那一步:模型是无状态的,工具结果必须变成消息历史的一部分,下一轮才看得见。回填错 id、丢历史、把结果放在错误角色里,模型就会把 A 的结果当成 B 的,或者直接报错断链。

维度OpenAI Chat CompletionsAnthropic Messages
工具声明tools[].function.{name,parameters}tools[].{name,input_schema}
模型提出调用message.tool_calls[]content[] 里的 tool_use 块
参数载体arguments 是 JSON 字符串,要 parseinput 是已解析对象
调用信号finish_reason: "tool_calls"stop_reason: "tool_use"
结果回填role:"tool" + tool_call_idrole:"user" + tool_result.tool_use_id
max_tokens可选必填,漏了直接 400
强制指定工具tool_choice:{type:"function",function:{name}}tool_choice:{type:"tool",name}

两家的类型选择不同:OpenAI 用一个专门的 tool 角色让历史“类型安全”,Anthropic 把工具结果看成“用户把输出递回来”,复用 user 加内容块。行为等价,历史结构不等价;跨协议迁移时,role:"tool" 要改写成 user 里的 tool_result,assistant 的 tool_use 块必须保留,否则引用断链。表中字段与必填约束以目标版本官方文档为准。

网关翻译的就是这一层:补 max_tokens、拆 content 块、统一 tool_choice、把流式片段拼回完整参数。翻译错了,模型不会报“协议不兼容”,只会输出废答案或空调用。

L1 MCP:发现、调用与断连恢复 ​

MCP 把跨进程工具拆成 Host、Client、Server:Host 管会话和最终授权,一个 Client 对应一条 Server 连接,Server 把外部系统翻译成 Tools、Resources、Prompts。生命周期是固定的:

配置声明 Server → 启动子进程或建立连接 → initialize 交换 protocolVersion 与 capabilities → notifications/initialized → tools/list 取工具 → 宿主加 mcp__server__tool 命名空间放进 tools 数组 → 模型选中后 tools/call → 结果回填。

tools/list 本身就返回每个工具的 inputSchema;“首次调用才取完整 Schema”是宿主侧的延迟加载优化,不是协议行为。它之所以常见,是因为工具名不变时 tools 数组稳定,Prompt 前缀缓存不容易破——这是宿主的取舍,不能当成 Server 必须遵守的约定。

传输只有两类:stdio 把 Server 当子进程,生命周期跟 Host 走,适合本机工具;Streamable HTTP 把 Server 当独立服务,可复用、可扩容,但要补鉴权、限流和出网控制。早期 HTTP+SSE 已被 Streamable HTTP 取代,只作为遗留兼容存在。能力协商是双向的:双方都声明支持,某项能力才生效;一方没声明,另一方不应使用。

失败模式很具体:Server 崩溃后 tools/list 超时,宿主容易误判“工具不存在”,要靠自动重启加缓存上次列表恢复;工具重名要靠命名空间隔离;运行中连接或断开 Server 会改变工具列表,进而打破 Prompt 缓存;长调用挂住连接时,要用 Tasks 扩展或宿主自己的 job 机制,具体方法以目标版本为准。

L2 A2A:发现、异步任务与交付 ​

A2A 的主干只有三段:Agent Card 发现能力、Task 承载异步状态、Artifact 交付产物。

发现走 GET /.well-known/agent-card.json,卡片里的 skills[].examples 不是给人看的,是给编排方做语义匹配的;capabilities.streaming 决定后续用推送还是轮询。任务创建后进入状态机,规范状态至少包括 submitted、working、input-required、completed、failed、canceled、rejected、auth-required——input-required 是正常的中途交互,不是失败;rejected 是接收方拒绝接单,和 failed 的执行失败不是一回事。

方法名用 JSON-RPC 形态:message/send 创建或续接任务,tasks/get 查状态,tasks/cancel 取消,需要流式时另有订阅方法。教学材料里常见的 POST /tasks、GET /tasks/{id} 是 REST 形态的简化,方便理解,不对应完整规范;字段名用 contextId 而不是 sessionId,它标识一段跨任务的上下文,不是一次连接。

A2A 的失败大多来自把它当同步 RPC:子任务跑几十分钟,调用方却按同步接口的 30 秒超时(示意);Task 里塞整份日志或文件,报文超限;轮询没有退避,把对方打满;缺 contextId,多用户任务串在一起。正确做法是创建后立即返回,用推送或指数退避查询,大内容只传 Artifact 引用。

L3 AG-UI:事件流与控制面 ​

AG-UI 补的是“执行过程怎么让前端看见”。它用一条 SSE 事件流把 Agent 到前端的方向打通,再用 REST 控制面处理前端到 Agent 的动作:建会话、发消息、取状态、取消,以及人工在环的 interrupt / resume。

核心事件包括运行开始/结束/错误、文本消息的分段增量、工具调用的开始/参数/结束/结果、状态快照与增量、以及业务自定义事件。本文只列核心事件,规范事件更多,字段与命名以目标版本为准。工具调用独立成事件,是因为它最需要可视化、最容易出错、也最需要插入审批。

HITL 的关键是“暂停”要有明确语义:运行停在某个中断点,任务 deadline 应暂停计时,用户确认后从断点恢复,而不是重新跑一遍。SSE 断连会丢事件,前端要带 Last-Event-ID 重连,并用状态快照对齐,不能只靠重放。

横切:五件事每层都要做 ​

  • 版本固定:L0 的 API 版本、L1 的 protocolVersion、L2 的规范版本都要写进配置和测试,不能“跟着最新走”。
  • 幂等键:L1 的写工具、L2 的任务创建都要能安全重试;重试只对读操作或无副作用的创建开放。
  • 身份透传:下游看到的是真实用户身份,不是共享服务账号;令牌要能区分“谁授权、代谁执行”。
  • Trace 传播:traceparent 要跨 L0/L1/L2/L3 透传,并把 tool_call_id、taskId、contextId 记进 span,否则跨协议排障只能靠猜。
  • 协议不是安全边界:报文合法不等于有权执行;MCP 不是沙箱,A2A 不校验对方意图,AG-UI 不保护业务数据。

最小上手 ​

先用两段报文核对网关翻译是否到位(示意,字段以目标版本为准):

jsonc
// OpenAI:第一轮响应;arguments 是字符串
{"choices":[{"finish_reason":"tool_calls","message":{"role":"assistant","tool_calls":[
  {"id":"call_1","type":"function","function":{"name":"read_file","arguments":"{\"path\":\"/a\"}"}}]}}]}
// 回填:{"role":"tool","tool_call_id":"call_1","content":"42"}
// Anthropic:第一轮响应;input 是对象,max_tokens 必填
{"stop_reason":"tool_use","content":[{"type":"tool_use","id":"toolu_1","name":"read_file","input":{"path":"/a"}}]}
// 回填:{"role":"user","content":[{"type":"tool_result","tool_use_id":"toolu_1","content":"42"}]}

再接一个本地 MCP Server,确认 tools/list 能返回 inputSchema:

json
{"mcpServers":{"files":{"command":"npx","args":["-y","@modelcontextprotocol/server-filesystem","/work"]}}}

跑通判据:两种报文都能完成一次两轮调用;网关日志能看到翻译前后的字段;tools/list 有 Schema;断开 Server 后宿主能重启并恢复工具列表;同一次调用在 Trace 里能串起模型、工具和任务。

生产动作 ​

  • 一致性测试:为每个协议版本准备固定报文样本,覆盖必填字段、并行调用、错误结果、断连恢复;升级前跑一遍,而不是等线上报 400。
  • 网关翻译与降级:网关统一做格式互转、必填补全、模型路由和故障切换;翻译失败要显式报错并降级到备用模型,不能静默丢字段。
  • 超时分层:L0 按模型响应设,L1 按工具类型设,L2 按任务 deadline 设;同步等待只用于秒级工具,长任务一律返回句柄。
  • 幂等与重试:写操作带幂等键;重试加指数退避和抖动;超过预算返回未完成清单,不继续盲试。
  • 跨协议 Trace:一次用户请求一个 trace,L0 的调用、L1 的工具、L2 的任务、L3 的事件都挂同一棵 span 树;采样要覆盖失败请求。
  • 灰度:新协议版本或新 Server 先按租户/工具灰度,观察错误率、延迟和缓存命中;工具列表变化会打破缓存,灰度时要把这部分成本算进去。

边界 ​

  • MCP 不是沙箱:它只负责连接和能力发现,进程权限、文件范围、网络出站仍要由宿主和运行时限制。
  • A2A 不适合进程内调用:同进程的方法调用或子图延迟低几个数量级;跨团队、跨语言、独立部署才值得上协议。
  • AG-UI 不承载业务状态:它传的是执行事件和界面控制,任务状态、权限和产物仍要存在服务端。
  • 协议统一不等于治理统一:四层都用标准报文,也不代表身份、配额、审计和脱敏自动一致;治理要单独建。
  • “兼容”不等于“等价”:OpenAI-compatible 服务可能只实现子集,工具调用、流式和必填字段都要逐项验证。

讲清楚 ​

协议栈的价值不是让所有系统长一样,而是让每一层的契约可核对:L0 看两轮循环和回填 id,L1 看发现、命名空间和断连恢复,L2 看任务状态机和产物引用,L3 看事件流和中断恢复。排障时先定位是哪一层的合同被违反了,再决定改网关、改宿主还是改对端。协议能统一报文,但统一不了权限、状态和治理;这三件事仍然要在每一层单独设计。

适用:2026 年 Agent 协议栈与跨协议集成 | 初稿 2026-10

把 AI 工程讲到既懂也会用