这是同一跨云货币基准的第三次运行,也是首次
箭头反向的测试。Cloud Run 上的 Google ADK 主控
(Gemini 2.5 Flash,us-central1)负责基准策略。它通过 A2A v1.0 调用
Amazon Bedrock AgentCore 工作器(Nova Micro 上的 Strands Agents,us-east-1),
并使用 MCP 汇率工具交叉验证答案。

上一次运行中 Bedrock 是主控、ADK 是工作器。反向操作本应只是部署变更——域核心与框架无关,因此比较逻辑从未移动。但实际发生的情况是,反向暴露了六处互操作性缺陷,其中 五处是本地测试无法发现的。这正是本文的重点,因此本文从失败开始。

本项目试图做什么?

大多数 A2A 演示止步于“HTTP 200 返回”,这只是冒烟测试,而非互操作性基准。

基准运行三种模式,以便将远程验证成本与工作成本分离:

模式 运行内容 目的
mcp_only ADK 主控通过 MCP stdio 调用汇率工具 基线
a2a_only ADK 主控委派给 AgentCore 工作器 远程代理成本
verified 两者并发运行,然后确定性比较 准确性/开销

比较是算术运算,而非模型判断。金额和汇率均为 Decimal。不会让模型判断两个数字是否一致。


六处故障

1. Strands A2A 额外组件使用错误的协议版本

此问题在部署前即被发现,并决定了整个工作器设计。

strands-agents[a2a] 固定 a2a-sdk<0.4 —— 即 A2A v0.3 线协议方法
message/send)。google-adk[a2a]==2.5.0 使用 a2a-sdk 1.x —— v1.0
SendMessage)。v1.0 客户端无法调用 v0.3 服务器,且代理卡不包含版本协商。

这与本项目首次运行中 A2UI 扩展被迫从 ADK 代理中移除的分割相同,只是从相反方向再次出现。

修复方法:仅将 strands-agents 用于代理循环,并直接使用 a2a-sdk 构建 A2A v1.0 服务器表面。

# app/CurrencyWorker/main.py -- 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

测试断言 lockfile 保持 a2a-sdk 在 1.x,以确保依赖升级不会悄然重新引入不匹配。

2. google-adk[a2a] 不安装其自身的服务器依赖

Cloud Run 容器构建成功,通过了所有本地测试,却在启动时崩溃:

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 都不会拉取它。

代价:一次失败的发布。两端现在都声明了 a2a-sdk[http-server]

3. IAM 信任策略永远无法匹配

协调器不持有 AWS 密钥。它使用联合身份:Cloud Run 的元数据服务器签发 Google OIDC 令牌,STS 的 AssumeRoleWithWebIdentity 将其交换为临时凭证,请求使用 SigV4 签名。

信任策略看起来显然正确,却在静默中不可能实现:

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

Enter fullscreen mode Exit fullscreen mode

AWS 不会将这些条件键映射到其名称暗示的声明:

条件键 实际 Google 声明
accounts.google.com:oaud 令牌的 aud
accounts.google.com:aud 令牌的 azp(数字客户端 ID)
accounts.google.com:sub 令牌的 sub

字符串 audience 正在与数字进行比较。应使用 oaud 而非 aud 来固定 audience。

4. 为 Google 创建 OIDC 提供程序会破坏 Google 联合身份

自然的下一步——将 accounts.google.com 注册为 IAM OIDC 身份提供程序——是错误的。AWS 原生支持与 Google 联合身份。创建显式提供程序会使 STS 改为针对该提供程序的指纹进行验证,导致每次交换都失败:

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

Enter fullscreen mode Exit fullscreen mode

删除该提供程序即可修复。主体是裸域名:

"Principal": { "Federated": "accounts.google.com" }

Enter fullscreen mode Exit fullscreen mode

值得注意的是故障模式:无效的 令牌 和被拒绝的 信任策略 是不同的错误(InvalidIdentityToken vs AccessDenied),这种区别是区分这两个 bug 的最快方法。

5. AgentCore 剥离 A2A 版本头,且 SDK 默认使用 v0.3

在联合身份正常工作且 SigV4 签名被接受后,工作器拒绝了自身正确版本的客户端:

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,会发送正确的头(client_factory.py 设置 A2A-Version: 1.0)。工作器是 v1.0。卡片宣称 "protocolVersion": "1.0"

头从未到达。AgentCore 的 A2A 运行时仅转发白名单中的请求头——而服务端验证器将 缺失 的头视为 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

这是整个项目中版本偏差主题的第三次出现:首次通过传递固定,再次通过框架额外组件,现在通过代理。

6. 代理卡在两个方向都宣称绑定地址

AgentCore 工作器的卡片宣称 http://127.0.0.1:9000 —— 其容器的自身绑定地址。ADK 的 to_a2a() 也对 http://127.0.0.1:8080 做同样处理。
a2a-sdk 客户端通过卡片 URL 路由,因此跨云调用会失败,除非客户端将接口重写为实际拨号的端点。

在首次运行中已针对 ADK 记录;这不是 ADK 的怪癖。

附加:A2A 端点不是看起来像基础 URL 的那个 URL

agentcore status 打印的 URL 将运行时 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 也会如此。百分号编码还会破坏从作用域 IAM 策略派生运行时 ID 时的朴素 ${VAR##*/runtimes/} shell 解析。


它测量什么

2026-07-31 日,通过已部署的 Cloud Run 协调器,从 us-central1us-east-1,每个模式进行五次热调用:

模式 中位数 最小值 最大值
mcp_only 2.96 s 2.37 s 3.84 s
a2a_only 18.92 s 16.30 s 19.68 s
verified 16.19 s 15.64 s 22.86 s

有两个突出之处。

远程代理跳跃的成本约为工具基线的 6 倍。这不是网络延迟——us-central1us-east-1 只有数十毫秒。这是一个额外的模型回合:工作器是一个代理,因此委派意味着 Nova Micro 规划工具调用、调用它,并写入结构化回复,叠加在 Gemini 做同样事情之上。A2A 委派带来了独立性,而独立性需要一次推理。

verified 并不比 a2a_only 慢。验证模式同时执行 MCP 调用和 A2A 调用并比较它们,但其中位数更低。协调器并发发出两者:

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

因此 verified ≈ max(mcp, a2a),而不是求和。一旦已经在为远程代理付费,独立验证几乎是免费的。两个中位数相差小于运行间波动,因此应将它们视为相等,而不是 verified 真的更快。

验证模式正确性,100 USD → EUR 和 CHF:

EUR  rate 0.87138  ->  87.138    CHF  rate 0.81248  ->  81.248
primary:  mcp-stdio:frankfurter-live
verifier: aws-agentcore-a2a-worker

Enter fullscreen mode Exit fullscreen mode

两个目标一致。两者还都携带 rate is stale; check the observation timestamp —— MCP 端返回 Frankfurter 的每日参考汇率,时间戳为 2026-07-30T00:00:00Z,而运行时间是 31 日 04:09Z,超过 24 小时阈值。陈旧规则在真实发布的汇率上触发,而不是在固定数据集上触发,这里的信号更有用。


本次运行未确立的内容

  • IAM 策略尚未达到最小权限。bedrock-agentcore:InvokeAgentRuntime 作用域限定为 runtime/<id>runtime/<id>/* 时被拒绝(403);只有 Resource: "*" 有效。AgentCore 针对某种尚未识别的 ARN 形状进行授权,且数据平面事件默认不在 CloudTrail 中。上述测量是在宽松策略下进行的。
  • n = 5,一个区域对,一天,一对模型。没有 p95,没有成本核算,没有令牌计数,没有冷/热分布。38 案例 × 3 模式矩阵尚未在此方向运行。
  • 此处没有任何关于哪个云更快的声明。Gemini 2.5 Flash 和 Nova Micro 是不同价格点的不同模型;6× 是 代理委派 的成本,而不是 AWS 的成本。

值得保留的部分

域核心未发生变化。CurrencyCoordinator 接受两个鸭子类型适配器,因此哪个云托管主控是部署决策。这一点成立:比较逻辑、陈旧规则、失败分类和评估工具在反向操作后均未受影响。

所有故障都发生在接缝处——协议版本、依赖额外组件、身份联合、头转发、URL 形状。六处缺陷中有五处在真实部署之前不可见。如果从本文中获得一个要点:从未离开笔记本电脑的互操作性测试只是在测试你的模拟。