在本系列的过去六篇文章中,我用同一种货币基准测试
构建了六次,每次都改变哪个云下达指令、哪个云接收指令。相同的领域核心、相同的 38 个评估用例、相同的 Decimal 比较器、
每一次跳转两侧都使用相同的公开汇率来源。唯一改变的是编排方向。

现在已覆盖三朵云之间的所有有向边,每条边都已部署并测量。本文是汇总:当把六条边并排放在一起时,数据揭示了什么、哪些问题在多条边上反复出现,以及哪些内容仍未测量。

简要结论:A2A v1.0 在我部署的所有方向上都实现了互操作。
协议从来不是成本最高的部分,也从未是失败的原因,更不是值得优化的地方。真正的问题在于身份、打包和超时。

覆盖矩阵

三朵云、三个框架,六条可能的有向边:

主控(编排方) 远程(应答方) 状态
Microsoft Foundry, Azure Google ADK, Cloud Run 已部署 — 完整 38 用例矩阵 + 托管冒烟测试
Bedrock AgentCore, AWS Google ADK, Cloud Run 已部署 — 38 用例矩阵(本地测试框架) + 托管冒烟测试
Bedrock AgentCore, AWS Microsoft Foundry, Azure 已部署 — 托管冒烟测试
Microsoft Foundry, Azure Bedrock AgentCore, AWS 已部署 — 托管冒烟测试
Google ADK, Cloud Run Microsoft Foundry, Azure 已部署 — 实时测量,5–6 次运行中位数
Google ADK, Cloud Run Bedrock AgentCore, AWS 已部署 — 实时测量,5 次运行中位数

六条边全部部署并完成测量。最后一条边也最具启发性:
在部署前,本地套件已通过一天,且代码已完成;部署后暴露出六处缺陷,其中五处是本地测试无法发现的。这个比例是本系列最可靠的发现之一。

还有一个仓库 gcp-adk-a2a-currency,在 Cloud Run 上运行 ADK 协调器并调用 ADK 叶子代理——但其默认 A2A 跳转始终 GCP 内部。它演练了协议,但未跨越云边界,因此不计入上述六条边。

每次运行都使用相同的三种模式:

模式 路径
mcp_only 主控调用自己的 MCP 汇率工具
a2a_only 主控通过 A2A v1.0 委托给远程代理
verified 两者并发运行;确定性代码比较结果

在每次运行中,模型都会选择工具并解释答案。它从未进行算术运算,也从未判断一致性。这始终由以下代码完成:

difference = abs(primary.converted_amount - verifier.converted_amount)
relative_difference = difference / abs(primary.converted_amount)
agreed = relative_difference <= Decimal("0.005")

Enter fullscreen mode Exit fullscreen mode

数据并排对比

阅读延迟列前请先阅读来源列。其中两行是 114 条记录的评估矩阵;两行是单次托管冒烟测试;两行是多次实时运行的小中位数。它们并非可互换的证据。

主控 → 远程 远程模型 / 托管环境 mcp_only a2a_only verified 来源
Foundry → ADK gemini-2.5-flash, Cloud Run 297 ms 1.69 s 1.71 s 114 条记录矩阵,中位数
AgentCore → ADK gemini-2.5-flash, Cloud Run 286 ms 2.09 s 1.87 s 114 条记录矩阵,中位数
AgentCore → Foundry gpt-5-mini, Foundry hosted ~4.2 s ~18.8 s ~18.1 s 单次托管冒烟测试
Foundry → AgentCore Nova Micro, AgentCore 359 ms 25.1 s 28.2 s 单次托管冒烟测试
ADK → Foundry gpt-5-mini, Foundry hosted n/a 23.4 s n/a 5 次实时运行中位数
ADK → AgentCore Nova Micro, AgentCore 2.96 s 18.92 s 16.19 s 5 次实时运行中位数

两行矩阵数据的 p95:Foundry → ADK a2a_only 4.82 s,verified 4.15 s;AgentCore → ADK a2a_only 6.10 s,verified 4.33 s。ADK → AgentCore 行的范围:mcp_only 2.37–3.84 s,a2a_only 16.30–19.68 s,verified 15.64–22.86 s。

发现 1:差异来自远程运行时,而非协议

表中 A2A 耗时范围从 1.69 s 到 25.1 s——同一协议、同一 SDK、同一跨洲跳跃,却相差 15 倍。协议并非变化来源。

在第六条边出现前,我曾给出一个更简洁的解释:远程的角色是决定因素——叶子代理回答一个问题约需 2 秒,而主控运行多工具推理循环约需 20 秒。但 ADK → AgentCore 的运行推翻了这一理论。其 AgentCore 工作器按任何定义都是叶子代理,仅用一次工具调用回答一个问题,却仍耗时 18.92 秒

六行数据真正对齐的是远程的运行时与模型,而不是角色:

  • 远程为 gemini-2.5-flash on Cloud Run: 1.69–2.09 s
  • 远程为 gpt-5-mini on Foundry 托管: 18.8–23.4 s
  • 远程为 Nova Micro on AgentCore Runtime: 18.9–25.1 s

这一分组在两个方向上都成立,无论哪个云在编排。诚实的解读是:委托成本 = 一次推理 + 远程平台的请求开销,而两款托管代理运行时的单次调用成本比 Cloud Run 上的容器高出一个数量级。我尚未分离其中模型速度与平台开销的比例——这需要同一模型在两种运行时上运行,而我尚未完成。

不是协议的问题。我在 ADK → Foundry 路径上直接测量:一次已认证的代理卡获取耗时 0.35 秒(热)(冷启动 0.51 秒),令牌在调用间缓存。在路径中增加第二朵云——工作站直接调用 21.2 秒 vs 通过 Cloud Run ADK 代理 23.4 秒——仅增加约 2 秒,而单次运行的波动范围为 18.9–29.5 秒。主控本身的波动大于额外云的全部成本。

若想让跨云代理架构更快,协议和代理不是瓶颈所在。

发现 2:verified 模式仅需一次 A2A 调用,而非两次

在每次运行中,verified 的耗时大致等于较慢的那条腿,而非两条腿之和,因为 MCP 和 A2A 是并发执行的:

  • Foundry → ADK:a2a_only 1.69 s,verified 1.71 s
  • AgentCore → ADK:a2a_only 2.09 s,verified 1.87 s(verified 矩阵中位数甚至低于 a2a_only——相同分布,不同用例组合)
  • AgentCore → Foundry:verified 运行中的 A2A 腿约 18.1 s,而 MCP 腿约 3.1 s
  • Foundry → AgentCore:a2a_only 25.1 s,verified 28.2 s
  • ADK → AgentCore:a2a_only 18.92 s,verified 16.19 s——verified 再次低于 a2a_only,差值小于运行间波动。应将这两者视为相等,而非“验证免费还退款”。

并发是显式的,而非偶然:

mcp_task = self._call("mcp", self._mcp, request, failures)
a2a_task = self._call("a2a", self._a2a, request, failures)
mcp_quotes, a2a_quotes = await asyncio.gather(mcp_task, a2a_task)

Enter fullscreen mode Exit fullscreen mode

因此,独立跨云验证的代价是“一次远程调用”,而非“一次远程调用加上你的基线”。

发现 3:在正常路径上,精度差为零

我部署的每一次跨云 verified 运行都报告了完全一致——relative_difference: "0"agreed: true,无警告:

  • AgentCore → ADK,托管:EUR 和 CHF,差值为零
  • AgentCore → Foundry,托管:汇率 0.87873,金额 87.87300,两侧一致
  • Foundry → AgentCore,托管:汇率 0.87138,两侧一致
  • Foundry → ADK,托管:agreed: truerelative_difference: 0
  • ADK → Foundry,实时:五次运行中每一次报价都完全相同
  • ADK → AgentCore,实时:EUR 0.8713887.138 且 CHF 0.8124881.248,均一致,主控 mcp-stdio:frankfurter-live,验证器 aws-agentcore-a2a-worker

这是设计使然。两侧特意读取相同的 Frankfurter 每日参考汇率,因此不一致只能来自协议、模型或代码——绝不会来自数据源偏差。

最后一次运行的一个细节比一致性本身更有价值:两个目标都携带了 rate is stale; check the observation timestamp
Frankfurter 的每日参考汇率时间戳为 2026-07-30T00:00:00Z,而运行时间是 31 日 04:09Z——已超过 24 小时阈值。陈旧规则在真实发布的汇率上触发,而非注入的固件,且协调器将其显露出来,而非让模型将其圆整为安抚性描述,这才是更有用的信号。

114 条记录矩阵中的 96.77% 一致率并不矛盾。38 个用例包含故意注入的故障(超时、陈旧汇率、强制不一致、恶意工具文本),正是这些故障用例将比例拉低到 100% 以下。全部 60 条实时适配器记录均在容差内一致。整个运行中唯一的不一致是我注入的,而验证器全部标记了出来。

这诚实地回答了“验证是否值得”的问题:在正常路径上,你在精度上什么都没买到。你用额外的一秒——或额外二十秒——买到的是独立故障检测:第二种实现、第二朵云、不同供应商的模型,在工具被破坏、陈旧或错误时会大声指出不一致。

多次出现的故障

六次构建、六组缺陷,有大量重叠。以下是反复出现的那些。

A2A v0.3.0 → v1.0 线缆断裂

这影响了六次构建中的五次,且通常表现相同:

a2a.utils.errors.MethodNotFoundError: Method not found

Enter fullscreen mode Exit fullscreen mode

或通过原始 JSON-RPC:

{"error": {"code": -32601, "message": "Method not found",
           "data": [{"reason": "METHOD_NOT_FOUND","metadata": {"detail": "'method' field is not a valid A2A method."}}]}}

Enter fullscreen mode Exit fullscreen mode

v0.3.0 路由 message/send。v1.x 路由 SendMessage 并使用 proto-JSON 参数。
没有回退协商:客户端获取一个明确声明 protocolVersion: 0.3.0 的代理卡,却仍调用 v1.0 方法。

如果你在有完全授权调用的端点上看到 MethodNotFoundError,请在调试其他任何内容前检查两侧的 a2a-sdk 主版本——并检查你自己的客户端方法拼写,因为 Foundry 卡在同一个 URL 上公布了三个接口(JSONRPC 1.0、JSONRPC 0.3、HTTP+JSON 0.3),会愉快地让你选错。

第六次构建找到了第三种产生同样偏差的方式,且这一种更棘手,因为两端都正确。一个 v1.0 客户端调用了一个公布 "protocolVersion": "1.0" 的 v1.0 工作器,却收到:

A2A version '0.3' is not supported by this handler. Expected version '1.0'.

Enter fullscreen mode Exit fullscreen mode

a2a-sdk 1.1.2 确实发送了 A2A-Version: 1.0。AgentCore Runtime 仅转发允许列表中的请求头,因此该头从未到达——而服务端验证器将缺失的头视为 v0.3:

# a2a/utils/version_validator.py
if not actual_version:
    return constants.PROTOCOL_VERSION_0_3

Enter fullscreen mode Exit fullscreen mode

对服务端而言,丢弃的头与旧客户端无法区分。一行运行时配置即可修复:

"requestHeaderAllowlist": ["A2A-Version"]

Enter fullscreen mode Exit fullscreen mode

三次构建,三种机制——传递性 pin、框架扩展,以及代理静默剥离头——都指向同一版本偏差症状。任何位于两个 A2A 端点之间的中介都是协议版本可能丢失的地方,而将缺失默认视为版本,会将传输错误转化为看似客户端错误的问题。

依赖 pin 是协议约束,直到它不再是

在这些构建的不同阶段,生态系统的 pin 曾相互排斥:

a2a-sdk 要求
strands-agents 1.50.2 >=1.0.0,<2
agent-framework-a2a 1.0.0b260721 >=1.0.0,<2
google-adk 2.1.0 – 2.4.0 >=0.3.4,<0.4
google-adk 2.5.0 >=0.3.4,<2
a2ui-agent-sdk (through 0.4.0) <0.4.0
strands-agents[a2a] (latest) <0.4

google-adk 2.5.0 是解除 GCP 侧阻塞的版本。A2UI 被完全放弃,因为一个协议相邻的扩展将整个代理锁定在 v0.3.0——在提交前请审计你的扩展。

Foundry → AgentCore 构建在此提供了有用的教训。AgentCore 的 A2A 服务端扩展需要 0.3 系列;Foundry 侧客户端使用 1.x;将两者安装到同一个 bundle 中无法满足。修复是架构性的,而非版本升级:将 AgentCore 应用 bundle 固定到其兼容的 SDK,保持客户端独立,让两者通信。即使 Python 包版本不共享,网络协议仍实现了互操作。包版本一致性是同一进程的要求,而非线缆要求。

第六次构建采用了另一种可行方案:保留代理框架用于代理循环,移除其 [a2a] 扩展,直接用 a2a-sdk 构建 A2A v1.0 服务端表面。

# NOT strands.multiagent.a2a.A2AServer
from a2a.server.request_handlers import DefaultRequestHandler
from a2a.server.routes import create_agent_card_routes, create_jsonrpc_routes

app = Starlette(routes=[
    *create_agent_card_routes(AGENT_CARD),
    *create_jsonrpc_routes(_request_handler, rpc_url="/"),
])

Enter fullscreen mode Exit fullscreen mode

因此,针对错误线缆版本扩展的两个已验证逃生方案是:分离 bundle,或绕过扩展。两者都有已部署的运行支撑。在每一侧进行 lockfile 测试,可防止依赖升级悄然重新引入不匹配。

每个默认超时都假设只有一朵云

协调器最初的每适配器 10 秒超时在本地是宽裕的,但在六次跨云运行中都是致命的。它干净地失败了,这是唯一的好处:

{"failures": {"a2a": "timeout: adapter timed out"}, "elapsed_ms": 10010}

Enter fullscreen mode Exit fullscreen mode

该失败来自一条直接调用已显示需要 25 秒的路径。适配器没问题;策略错了。最终值:AgentCore 和 Foundry 主控托管路径为 60 秒,ADK 客户端和 Foundry 协调器为 120 秒。

冷启动不仅增加延迟。来自 ADK 工作器的一次冷启动回复完全遗漏了一个请求的目标。由于解析是严格的,这表现为类型化的 protocol 失败,而非静默的短回答。需为部分回复设计。

身份就是全部工作

贯穿六次构建的最强模式:A2A 互操作性是一个披着协议外衣的身份问题。在每个方向上,JSON-RPC 那一半都是容易的那一半。

从 AWS 或 GCP 调用 进入 Foundry:

  • 入站 A2A 是可选且预览的——托管的 Foundry 代理在你 PATCH 开启 A2A 之前使用 Responses。
  • PATCH 的 protocol_configuration 是替换而非合并。我通过探测而非假设确认了这一点:仅 PATCH {"a2a": {}} 会丢弃 Responses,并同时使 A2A 代理卡失效(400)——而请求返回 200
  • 不存在匿名 /.well-known/agent-card.json。卡片位于 <agent>/endpoint/protocols/a2a/agentCard/v1.0 背后,受 Entra 保护。发现本身就是特权调用,因此卡片上的 404 或 401 更可能是缺少角色分配,而非路径错误。
  • 调用者需要项目上的 Foundry Agent Consumer 数据平面角色;部署者需要 Foundry Project Manager。Azure Owner 并不隐含这两者。
  • 首先正确设置令牌 audience:https://ai.azure.com/.default,而非 ARM audience。一个对错误作用域完全有效的令牌会产生一个看起来不像作用域问题的 401。

从 Azure 调用 进入 AgentCore:

  • AgentCore 的默认运行时授权是 IAM/SigV4,这对 AWS 调用者完全正确,但对无法签署 AWS 请求的 Foundry 容器毫无用处。直接的 AWS CLI 调用可以工作;跨云调用则不行。
  • 修复方案是自定义 JWT 授权器加上 Cognito 机器对机器客户端,由 A2A 适配器执行 OAuth client-credentials 交换、缓存令牌,并由授权器验证发行者、客户端和必需的 currencybench/invoke 作用域。

从 GCP 调用 进入 AgentCore——同样的墙,不同的答案:

第六次构建最初采用相同的 CUSTOM_JWT 方法,随后将其替换为无密钥联邦到 AgentCore 的默认 AWS_IAM 授权器:Cloud Run 的元数据服务器铸造 Google OIDC 令牌,STS AssumeRoleWithWebIdentity 将其交换为临时凭证,请求使用 SigV4 签名。容器中不存在任何长期 AWS 凭证。

实现这一目标付出了两个 IAM 缺陷的代价,两者看起来都是正确的配置。

条件键的含义与名称不符。以下信任策略在语法上有效、阅读正确,但永远无法匹配:

"Condition": { "StringEquals": {
  "accounts.google.com:aud": "currencybench-agentcore-worker",
  "accounts.google.com:sub": "1019138736740282766.."
}}

Enter fullscreen mode Exit fullscreen mode

条件键 实际 Google claim
accounts.google.com:oaud 令牌的 aud——你的 audience 字符串
accounts.google.com:aud 令牌的 azp——服务账号的数字客户端 ID
accounts.google.com:sub 令牌的 sub

audience 字符串与一个数字比较,因此 STS 永远回答“令牌 audience 不正确”。请用 oaud 固定 audience。

同时固定主体,因为仅 audience 不足以授权。
这一点在键正确后仍然重要。任何 Google 主体都可以为任意 audience 字符串请求 ID 令牌——audience 是调用者选择的值,而非平台授予的权限。因此,仅基于 audience 的信任策略除了证明“某个 Google 身份铸造了此令牌”之外,什么都证明不了,而在共享组织中,这几乎等于什么都证明不了。

因此策略同时基于 sub——基于服务账号不可变的数字 ID 而非其邮箱,因为邮箱是一个名称,如果账号被删除并重新创建,就可能被释放并重新绑定:

gcloud iam service-accounts describe "$SA_EMAIL" --format='value(uniqueId)'

Enter fullscreen mode Exit fullscreen mode

这也是支持联邦到被调用方原生授权器而非发送 bearer 令牌的论据:信任决策最终落在 IAM 策略中,在 CloudTrail 中有真实主体归因,可通过编辑策略撤销,而非存在于应用配置中的 claim 字符串匹配器。

将 Google 注册为 OIDC 提供商会破坏 Google 联邦。显而易见的下一步——为 accounts.google.com 执行 aws iam create-open-id-connect-provider——是错误的。AWS 原生支持与 Google 联邦;创建显式提供商会切换 STS 使其根据该提供商的指纹验证,每次交换都会失败:

<Code>InvalidIdentityToken</Code>
<Message>The web identity token provided could not be validated.</Message>

Enter fullscreen mode Exit fullscreen mode

删除提供商即可修复。主体是裸域名:
"Federated": "accounts.google.com"

此处有用的诊断是这两个缺陷会产生不同的错误——InvalidIdentityToken 意味着令牌根本无法验证,而信任策略不匹配是 AccessDenied。这一区别是区分“我的提供商设置错误”与“我的条件错误”的最快方法。

因此,进入同一 AWS 运行时的两次跨云调用,有两种不同的答案:来自 Azure 的 bearer 令牌,来自 GCP 的无密钥联邦。两者都已部署并测量。

凭证始终存在于调用方一侧。Google 调用 Azure 意味着 Azure 客户端密钥存在于 Google Secret Manager 中。AWS 调用 Azure 意味着 Entra 服务主体凭证存在于 AWS Secrets Manager 中,作用域限定为单个密钥 ARN。没有办法绕过密钥的存在;但可以避免它出现在你的源代码树、清单或 shell 历史中。

一个值得预算的时机陷阱:数据平面 RBAC 传播需要 105 秒,而 az role assignment list 已显示分配已存在。代理写入在此期间持续返回 403。这是传播问题,而非配置错误。在重新分配角色前请等待。软删除的 Foundry 账号也会阻止重新创建,直到被清除——Azure 资源状态也有记忆。

你的测试导入源代码树;你的用户运行镜像

这类缺陷在三次构建中出现,且每次都对绿色的测试套件不可见。

aiohttp 独立破坏了其中两次。azure.identity.aio 使用 aiohttp 构建其异步传输,而 aiohttp 不是 azure-identity 的硬依赖——它存在于普通工作站上,因此本地运行通过:

File "azure/core/pipeline/transport/__init__.py", line 94, in __getattr__
    raise ImportError("aiohttp package is not installed")

Enter fullscreen mode Exit fullscreen mode

在 AWS 上,仅将其添加到仓库根 requirements 是不够的;CodeZip 独立解析应用 bundle,因此必须锁定在 app/CurrencyCoordinator/pyproject.toml 中。在 GCP 上,uv sync --frozen 在容器中产生了同样的缺失。

同一家族中的另外两个,均来自 ADK 调用 Foundry 的客户端镜像:一条 COPY pyproject.toml uv.lock agent.py ./ 行从未提及 entra_auth.py,因此该模块根本不在镜像中;以及一条平坦的 from entra_auth import EntraAuth 无法解析,因为 ADK 的单代理模式将你的文件导入为 app.agent——一个包的子模块,/sys.path 中而非 /app。相对导入在两种上下文中都有效。

在 Foundry 主控侧再次出现相同模式:一个 MCP stdio 工具会生成一个带有真实导入路径的真实子进程,因此 -m mcp_server.server 无法导入不在部署 bundle 内的 monorepo 同级。修复方案是在将其交给 azd 之前用 rsync 复制到 bundle 中。不优雅;托管运行时不关心你的仓库布局。

第六次构建产生了该模式最清晰的样本。容器构建成功,所有本地测试通过,但在启动时崩溃:

ModuleNotFoundError: No module named 'sse_starlette'
  File ".../google/adk/a2a/_compat.py", line 769, in attach_a2a_routes_to_app

Enter fullscreen mode Exit fullscreen mode

ADK 的 to_a2a() 导入 a2a.server.routes,后者需要 sse_starlette,而它仅随 a2a-sdk[http-server] 扩展提供。google-adk[a2a] 和裸 a2a-sdk 都不会拉入它。与 aiohttp 相同——一个可选的传递依赖,你的工作站恰好有——但它在启动时失败而非首次调用,因此 Cloud Run 将其报告为健康检查超时,gcloud 则显示一个误导性的 Resource 'currency-adk-coordinator' already exists 冲突。实际错误仅存在于修订日志中。

发现这类缺陷的两种方法是:本地加载容器的真实目录布局,或部署并调用它。

from google.adk.cli.utils.agent_loader import AgentLoader
AgentLoader("/app").load_agent("app")   # 修复前出现 ModuleNotFoundError

Enter fullscreen mode Exit fullscreen mode

推论:通过模型传递的错误不是可观测的

调试上述 STS 失败因一个愚蠢的原因耗费了两个部署周期。适配器抛出了精确、可操作的消息——而它返回给我的是:

The remote agent reported an issue with the web identity token.

失败文本作为工具结果返回,因此会通过主控模型,而模型会改写。任何你打算调试的内容都必须在失败点进行服务端日志记录,而不仅仅是抛出。同样适用于上游细节:适配器丢弃了 STS 响应体,仅报告 HTTP 状态,而这正是无法区分 audience 不匹配与不可验证令牌的那一半。

代理卡的可移植性仅取决于其内部的 URL

ADK 的 to_a2a() 将主机、端口和协议烘焙到发布的卡片中。如果保留本地默认,Cloud Run 代理会发出一张宣传 http://127.0.0.1:10001 的卡片,而远程客户端会 dutifully 尝试调用自己。容器必须读取 A2A_PUBLIC_URL——部署脚本只能在第二次通过时设置,因为 Cloud Run 直到首次部署完成才会揭示服务 URL。

我在第一次构建后将其记录为 ADK 的怪癖。但事实并非如此。AgentCore 工作器的卡片宣传 http://127.0.0.1:9000——其自己的容器绑定地址——原因完全相同。a2a-sdk 客户端通过卡片 URL 路由,因此本系列中的每个跨云客户端最终都会将卡片接口重写为实际拨号的端点:

for interface in card.supported_interfaces:
    interface.url = endpoint

Enter fullscreen mode Exit fullscreen mode

跨云边界时,请将“卡片告诉你将消息发送到哪里”视为参考性建议,双向皆然。

A2A 基础 URL 不是看起来像基础 URL 的那个

agentcore status 将运行时 ARN 百分号编码后打印到路径中,并带有 /invocations 后缀。这个完整的 URL(包括后缀)就是 A2A 基础,而卡片位于其下:

https://bedrock-agentcore.us-east-1.amazonaws.com/runtimes/arn%3Aaws%3A...%2F<id>/invocations
                                              └── card at <base>/.well-known/agent-card.json

Enter fullscreen mode Exit fullscreen mode

截断 /invocations 以获得更整洁的基础会返回 UnknownOperationException,用裸运行时 ID 替换编码的 ARN 也会如此。百分号编码还有第二口:用朴素的 ${VAR##*/runtimes/} 为作用域 IAM 策略推导运行时 ID,会产生一个格式错误的 ARN,它部署正常,仅在调用时失败。拆分前请先解码。

从另一个方向看,Foundry 要求你手动编写卡片。在 MCP 工具会自动显示为技能的 ADK 之后,这感觉像倒退。这也是迈向诚实的一步:卡片是一份契约,而在那一侧,你必须声明它。

模型层失败不是协议失败

值得单独列出,因为它们会被解读为互操作性缺陷,但其实不是:

  • Nova Micro 读取“将 100 USD 转换为 EUR”,然后声称目标货币缺失,并要求用户确认。工具可用性不是问题;自然语言参数提取是问题。主控提示现在禁止对已存在的信息请求确认,回归测试保留了这一点。
  • Gemini 2.5 Flash 将一个真正实时的 hosted-adk-a2a 报价报告为非实时,因为该来源名称与固件来源 hosted-local-a2a 共享 hosted- 前缀,而共享指令通过子串区分它们。数据正确;只有描述错误。同一条指令在 Nova Micro 和 gpt-5-mini 上正确标记——这使得它成为共享指令缺陷而非模型缺陷。
  • gpt-5-minigemini-2.5-flash 在每次运行中都遵守“调用工具,绝不计算”。将算术保留在 Decimal 中意味着一致性检查比较的是数字,而非模型描述。

使基准测试在失败时关闭

最重要的非技术教训。一个托管基准测试工具如果在其端点配置缺失时静默回退到确定性固件,就会给你从无到有的干净数据,而你会发布它们。两个托管协调器现在都拒绝:

{ "name": "CURRENCY_REQUIRE_GCP_ADK", "value": "1" }

Enter fullscreen mode Exit fullscreen mode

设置后,a2a_onlyverified 将返回 gcp_adk_not_configured,而非一个看似合理的固件答案。本地固件运行仍可工作,但你必须通过名称请求(CURRENCY_ALLOW_LOCAL_FIXTURES=true)。

经历六次翻转后幸存的部分

领域核心。两个接口——MCP 支持的 ExchangeRateTool 和 A2A 支持的 RemoteCurrencyAgent——Microsoft Agent Framework、Strands 和 ADK 都在其外部,每个托管运行时也都在其外部。

翻转哪个云进行编排从未需要重写领域服务。框架无关的 Python 在六次构建中拥有算术、并发、超时策略和故障分类,相同的 38 个用例和相同的比较器针对每一次运行。这才是本项目真正的成果——远比任何单个延迟数字,更远比将两个 SDK 放入一个进程重要。

所有在六次构建中出现的问题,都发生在接缝处:协议版本、依赖扩展、身份联邦、头转发、URL 形状。领域逻辑中没有出现问题。这就是整个项目结果的形态。

类型化失败在使六次可比较方面发挥了很大作用。每个适配器异常都被归一化为边界上的 validationproviderauthenticationtransporttimeoutprotocol 之一,因为“哪一层出问题”是一个研究问题,而非日志搜索练习。

本文未确立的内容

直白陈述,因为从绿色冒烟测试中过度声明的诱惑正是本系列构建时所要抵制的:

  • 六行中的两行是单次观测。一次托管请求不是延迟分布。Foundry → AgentCore 运行端到端耗时约 45 秒,而测得的适配器工作为 28.2 秒;这一差距是模型和托管响应开销,仅凭一个样本就称其中任一数字为基准是不负责任的。
  • 仅有两个 GCP 远程路径具有完整的 38 用例矩阵。AgentCore ↔ Foundry 对和两个 ADK 作为主控的路径尚未针对该矩阵运行。
  • 令牌使用和云成本在所有地方都未测量。同样未测量节流和令牌过期下的行为,以及重复的冷/热分布。
  • AgentCore → ADK 矩阵在本地测试框架上运行,而非通过 AgentCore 托管层。仅冒烟测试使用了托管。明确保留这一标签可避免将测试框架延迟归因于 AgentCore。
  • 本文未显示任何框架或云优于另一个。延迟差异追踪的是远程使用的模型和运行时,而非平台质量——且我尚未分离模型速度与平台请求开销,这需要同一模型在两种运行时上运行。
  • ADK → AgentCore 数字是在宽松 IAM 策略下获取的。bedrock-agentcore:InvokeAgentRuntime 作用域限定为 runtime/<id>runtime/<id>/* 在代理卡获取时被拒绝(403);仅 Resource: "*" 有效。AgentCore 根据我尚未确定的 ARN 形状进行授权,且数据平面事件默认不出现在 CloudTrail 中,因此我无法读取拒绝。因此该行不是最小权限配置,我不会将其作为最小权限配置发布。
  • 两行是 n=5,一天,一个区域对,一对模型。没有 p95、没有令牌计数、没有成本核算、没有冷/热分离。

六行汇总

  1. A2A v1.0 在所有六个方向上都实现了互操作。无需模式转换,无需框架特定胶水,只需请求可解析的 JSON。
  2. 协议从来不是慢的部分。A2A 腿追踪远程的模型和运行时——到 Cloud Run 容器 1.7–2.1 秒,到任一托管代理运行时 18.8–25.1 秒。一次已认证的卡片获取耗时 0.35 秒;在路径中增加第二朵云约耗时 2 秒。
  3. 跨云 A2A 是一个身份项目。为服务主体、数据平面角色、令牌 audience、非原生认证(在被调用方)和你看不到的 RBAC 传播做好预算。当调用方可以铸造工作负载 OIDC 令牌时,联邦到被调用方的原生授权器优于发送 bearer 令牌——且固定主体,而非仅固定 audience。
  4. 版本偏差失败响亮但晚,通过三种独立机制:传递性 pin、框架扩展,以及代理剥离版本头。包 pin 约束你的进程,而非你的线缆。分离 bundle 或绕过扩展;两者都是已验证的逃生方案。
  5. 你的镜像不是你的源代码树。所有在绿色测试套件中幸存的缺陷都是打包或接缝缺陷——在最后一次构建中,六处缺陷中有五处在真实部署前不可见。
  6. 验证买到的是独立故障检测,而非精度。在正常路径上,两朵云每次都完全一致。注入的故障——以及在真实 Frankfurter 报价上触发的陈旧汇率警告——是第二朵云赚取其第二份价值的地方。

来源

仓库,每个都包含其部署脚本、测试、评估用例和原始结果:

上游代理来自
Getting Started with MCP, ADK and A2A

如果你曾在云边界上运行过 A2A——特别是如果你已解决 AgentCore 实际授权的 ARN 形状问题,或遇到这些六次构建中未遇到的卡片/版本不匹配——我很想交流笔记。