工具调用69 21 分钟

Agent 之间怎么通信?先把消息信封设计好

A2A 场景先要说清的不是协议叫什么,而是消息发给谁、谁有权限、版本怎么兼容、重复了怎么办、结果如何回执。把传输层和任务层分开,换队列或网关也不怕。

原理 实现 边界 追问

本篇阅读顺序 · 三遍读法

一篇文章,读出三种能力。

先弄懂它为什么这样工作,再把条件换一换,看方案还能不能站住,最后用代码和证据复盘一遍。顺序固定,读完才知道自己是真的会了,还是只记住了名词。

01 / 基础知识先回答“它为什么这样工作”

沿着直觉、公式和边界读正文,看到变量就问输入、状态、复杂度分别是什么。

02 / 高频追问再回答“条件变了怎么办”

把面试官的追问当作小型设计评审:更大流量、更少上下文、外部失败或安全约束出现时,局部如何重算。

03 / 从零实现把动作、回执与失败边界写进最小实现

沿代码、表格和证据卡复盘,最后用文末的 60 秒回答确认自己没有只记住名词。

问题面试官到底在判断什么
机制系统如何工作
证据代码、指标与取舍
表达30 秒回答骨架

两个 Agent 互相调用时,最容易把“可以发消息”误认为“可以可靠协作”。真正的系统要回答:我在调用谁?对方能做什么?这条消息是否过期?网络重试会不会重复副作用?回复是建议还是承诺?这些问题属于通信边界,不该散落在提示词里。

先给一个能复述的答案

把 Agent-to-Agent 设计成两层:传输层负责连接、鉴权、重试、大小限制和事件投递;任务层负责目标、能力、上下文、状态、证据和幂等。先定义稳定 envelope,再把它映射到 HTTP、队列或流式通道。协议名称可以变化,任务合同和可观测字段不能跟着变化。

Agent-to-Agent 的传输层与任务层分离

最小 envelope

{
  "envelope_version": "1",
  "message_id": "msg-981",
  "conversation_id": "conv-77",
  "sender": {"id": "router", "tenant": "acme"},
  "receiver": {"id": "billing-agent"},
  "intent": "delegate",
  "capability": "lookup_invoice",
  "payload_ref": "store://payload/981",
  "auth_context": {"scopes": ["invoice:read"]},
  "idempotency_key": "invoice:2026-08:981",
  "expires_at": "2026-08-22T14:10:00Z",
  "reply_to": "queue://agent-events/conv-77"
}

大 payload 放引用,避免把整段文档复制到每个 Agent;但引用必须带租户、版本和访问控制。若只把 payload_ref 当成公开 URL,权限问题就会被推迟到最危险的地方。

能力发现和路由

接收方的能力注册表至少包括:能力名、输入 schema、权限范围、版本、SLO、是否产生副作用。路由器选择能力时要考虑租户、数据位置、当前负载和风险,不是只做字符串匹配。

字段例子作用
capabilitylookup_invoice稳定的任务语义
schemaJSON Schema参数校验和生成约束
scopesinvoice:read最小权限
version2.1兼容与灰度
side_effectnone / write是否需要审批

同步、异步和流式的取舍

模式适合主要风险
同步请求短、无副作用查询长任务占用连接
异步队列长任务、可重试任务状态查询和回执复杂
流式需要过程反馈背压、乱序、断线

任务层的状态机不应依赖某一种传输。无论消息走 HTTP 还是队列,都应该能通过 message_id + idempotency_key 查询最终状态。

兼容和失败边界

新版本加字段时,接收方可以忽略未知可选字段;改变字段含义或必填约束时必须升版本。常见失败码建议分为:

  • invalid_request:合同或 schema 不合法;
  • unauthorized:身份或 scope 不够;
  • unsupported_capability:没有对应能力;
  • retryable_unavailable:暂时不可用,可按策略重试;
  • unknown_commit:副作用状态未知,必须查询;
  • business_rejected:业务规则拒绝,不应盲目重试。

把所有错误都返回成 500,调用方就无法判断下一步是修参数、换路由还是暂停对账。

A2A 失败分类与恢复决策

以“报销 Agent 调用财务 Agent”为例

路由 Agent 只知道用户想查某张发票,不应该把整段会话和所有权限直接转发给财务 Agent。它可以把任务缩成:发票编号、租户、查询能力、截止时间和只读 scope。财务 Agent 返回证据和状态,不返回一个无法验证的“我已经处理好了”。

{
  "intent": "delegate",
  "capability": "invoice.lookup",
  "input": {"invoice_id": "inv-2026-091"},
  "constraints": {"read_only": true, "deadline_ms": 3000},
  "evidence_required": true,
  "reply_mode": "result_or_needs_clarification"
}

这层任务合同让接收方不必理解调用方的内部 prompt,也让审计人员能看出“谁委托了什么、在什么权限下完成”。

消息 envelope 与能力握手

能力发现不是把所有工具广播给所有 Agent

能力注册表可以分为公开元数据和受权详情。公开部分告诉路由器能力名、版本和健康状态;只有在租户、scope 和数据位置校验通过后,才返回输入 schema 和可用实例。能力变化要有生效时间和兼容窗口,避免一个 Agent 刚发现能力就调用了已经撤销的版本。

阶段发送方接收方必须记录
discovercapability registryrouter能力、版本、健康、SLO
authorizerouterpolicy service租户、scope、数据域
delegaterouteragentenvelope、deadline、幂等键
accept/rejectagentrouter合同校验、原因、lease
completeagentevent store结果、证据、终态

跨 Agent 交接的验收合同

Lease、deadline 和 hop budget 要一起传

只传一个超时时间不够。长任务可能在中间被重新调度,调用环也可能不断延长。建议 envelope 里同时带:

deadline_at: 2026-08-22T14:10:00Z
lease_id: lease-981
hop_count: 2
max_hops: 4
remaining_budget:
  model_tokens: 18000
  tool_calls: 6
  wall_ms: 2400

接收方接受任务后要续租或明确拒绝;租约过期后不能继续写入。hop_count 和父调用链用于识别循环,剩余预算则让下游知道是否还有空间做重试或澄清。

版本兼容要测试“行为”,不只是字段

新增可选字段通常可以向后兼容,但能力的语义变化更危险。例如 invoice.lookup v2 把“查到发票”改成“查到发票并自动标记”,字段 schema 可能完全没变,副作用却变了。能力版本发布时要做契约回放:旧调用方、旧数据和失败响应都在新实现上跑一遍,并把 side effect 列为硬门槛。

协议版本与能力版本的兼容矩阵

安全:跨 Agent 不能继承“万能身份”

调用方的用户身份、Agent 的服务身份和目标资源的授权范围要分开。下游只接收完成该任务所需的最小 scope,结果返回时还要重新做租户隔离和证据过滤。不要因为消息来自“可信 Agent”就跳过鉴权;一旦某个 Agent 被提示注入或凭据泄露,万能身份会把事故扩散到所有能力。

跨 Agent 调用的信任边界

故障恢复的三种不同答案

尚未接受。 可以换实例或重试,但要使用同一个幂等键。

已接受但没有终态。 先查询 lease、事件和业务状态;不能直接重放可能有副作用的请求。

已完成但回执丢失。message_id 或业务查询补回证据,向上游发送同一个终态,而不是创建第二个任务。

把这三种情况合并成“请求超时”,是 A2A 系统出现重复扣款、重复写入和互相甩锅的常见原因。

一个完整交互:发现能力、租约和结果回执

以报销 Agent 调用财务 Agent 为例,推荐把一次调用拆成四个事件,而不是一个长连接:

discover(capability=expense.validate)
  → offer(version=v2, scope=tenant:acme, lease=30s)
  → invoke(message_id=m-017, deadline=3s, idempotency=k-017)
  → result(status=committed|unknown, evidence=receipt-017)

发现阶段只返回能力、版本、租户和可接受的输入输出;调用阶段绑定用户和 Agent 身份;结果阶段必须附带业务凭证或未知查询键。这样能力服务升级时,可以先拒绝不兼容的版本,而不会把协议错误伪装成业务失败。

结果合同要比自然语言更窄

财务 Agent 返回“看起来没问题”并不能让上游继续。建议把结果合同固定成:

字段说明缺失时
statusaccepted/running/committed/unknown/rejected拒绝采纳
message_id本次消息唯一标识无法去重
result_version结果版本或业务凭证版本重新查询
evidence_refs原文、回执或查询地址不允许生成确定结论
next_action可继续、查询、人工升级进入安全停止

status=committed 只表示下游已完成它的合同,不代表上游已经把结果写入自己的状态。上游仍要用 CAS 或版本检查采纳结果,避免旧消息覆盖新状态。

Lease、deadline 和 hop budget 的关系

一个多 Agent 请求可能跨过多个服务。租约控制责任归属,deadline 控制时间,hop budget 控制链路深度,三者不能只传一个超时数字:

deadlinechild=min(deadlineparentnetwork_reserve,now+lease)deadline_{child}=\min(deadline_{parent}-network\_reserve, now+lease)
delegation_budget:
  deadline_ms: 3000
  network_reserve_ms: 250
  lease_ms: 1200
  max_hops: 3
  max_messages: 8
  on_exhausted: "return unknown_or_handoff"

如果 child 的 lease 已过期,parent 不能因为自己还有时间就继续要求执行;否则下游可能在上游已经回退之后产生迟到副作用。

兼容性测试要覆盖行为

字段兼容只是第一关。更容易出事故的是旧客户端仍能发送,但新服务对状态、错误码或未知字段的理解发生变化。可以维护一张行为矩阵:

compatibility_tests:
  - client: v1
    server: v2
    case: unknown_required_capability
    expect: reject_with_capability_mismatch
  - client: v1
    server: v2
    case: duplicate_message
    expect: same_result_same_receipt
  - client: v2
    server: v1
    case: extra_optional_field
    expect: ignore_and_process
  - client: v2
    server: v1
    case: unknown_status
    expect: safe_stop

每个版本都要有 replay fixture,特别是 unknown、延迟到达和重复结果。协议升级不能只跑 schema validator。

安全授权要跟着每一跳走

下游收到的不应是“用户拥有所有权限”,而是一张有范围、有期限的授权票据:

{
  "subject": "finance-agent",
  "on_behalf_of": "user-19",
  "tenant": "acme",
  "scopes": ["expense.read"],
  "resources": ["claim:2026-018"],
  "expires_at": "2026-08-22T10:30:00Z",
  "delegation_depth": 1
}

每次再委派都要减少 scope 或增加明确的审批,不能无限继承。发生提示注入或凭据泄露时,最小授权能把事故限制在一条任务链,而不是整个 Agent 网络。

重试矩阵:按失败阶段决定谁可以重试

A2A 调用不能套一个“失败就重试”的通用策略。发现阶段失败可以换能力提供方;执行阶段超时要先查询状态;结果校验失败可以要求补充字段,但不能重复提交副作用:

失败阶段可重试者重试条件用户状态
discover路由器能力版本仍满足约束继续处理中
accept上游 Agent未生成执行凭证等待接单
invoke下游 Agent幂等键未落账处理中
result结果合并器只读且证据可补齐正在核对
commit人工/业务系统外部状态已确认不自动重复

每个 retry 都要带同一个 task_ididempotency_key,并在 trace 中记录“为什么允许这一次重试”。这条记录既方便排查,也能防止多个 Agent 同时把同一个超时当成自己的重试机会。

可观测性要跨越调用边界

上游的 trace 不能在下游重新生成一条孤立链路。至少透传 trace_idparent_span_idtask_id、租户和协议版本;下游再补充自己的能力、模型和工具字段。跨 Agent 的耗时可以拆成发现、排队、执行、回执和合并五段,才能定位到底是网络慢还是下游排队。

高频追问

L1:Agent 间调用和普通 API 有什么不同?

传输上可以复用 API 基础设施,但任务结果通常是异步、分阶段且带证据的;需要额外的能力、状态和交接合同。

L2:为什么要把 payload 放引用?

可以减少复制和泄漏风险,统一版本与权限;但引用服务必须支持鉴权、过期和审计。

L2:协议版本和能力版本怎么区分?

协议版本描述 envelope 的结构,能力版本描述某个任务输入输出与行为。两者独立演进,不能用一个版本号掩盖所有变化。

L3:调用方收到超时怎么做?

先查 message_id 或幂等键对应的状态;只有确认未执行且错误可重试时才重发。

L5:如何防止 Agent 之间形成调用环?

在 envelope 记录 hop count、父调用链和总 deadline;路由器对重复路径和预算耗尽直接拒绝或升级。

60 秒面试回答

我会把 A2A 分成传输层和任务层。传输层处理连接、鉴权、大小、重试和事件投递,任务层定义目标、能力、输入 schema、权限、状态、证据和幂等键。先固定 envelope,再映射到同步 API、队列或流式通道。错误要区分参数、权限、暂不可用、业务拒绝和未知副作用,超时先查状态而不是盲目重放;所有消息带 trace、版本和 hop budget,避免调用环。

自检清单

  • 能画出传输层与任务层的边界
  • envelope 有身份、能力、版本、scope、幂等和过期
  • 失败码能指导下一步动作
  • 有调用环、超时和跨租户访问的防护

相关阅读

资料来源