这是同一跨云货币基准的第三次运行,也是首次
箭头指向相反方向。Cloud Run 上的 Google ADK 主代理
(Gemini 2.5 Flash,us-central1)拥有基准策略。它通过A2A v1.0调用
Amazon Bedrock AgentCore 工作代理(Strands Agents on Nova Micro,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] extra 提供。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
|
受众字符串被与数字比较。应使用 oaud 而非 aud 固定受众。
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
这是整个项目版本偏差主题的第三次出现:先通过传递性锁定,再通过框架 extra,现在通过代理。
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 也会如此。百分号编码还会破坏从 shell 解析运行时 ID 以生成作用域 IAM 策略时的朴素 ${VAR##*/runtimes/}。
它测量什么
2026-07-31 日,通过已部署的 Cloud Run 协调器(us-central1 → us-east-1),每种模式 5 次热调用:
| 模式 | 中位数 | 最小值 | 最大值 |
|---|---|---|---|
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-central1 到 us-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 小时阈值。陈旧规则在真实发布汇率上触发,而非在 fixture 上触发,是此处更有用的信号。
本次运行未确立的内容
-
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 接受两个鸭子类型适配器,因此哪个云托管主代理是一个部署决策。这点成立:比较逻辑、陈旧规则、故障分类和评估工具在反转后均完好无损。
所有故障都发生在接缝处——协议版本、依赖项 extra、身份联合、头转发、URL 形状。六项缺陷中有五项直到真正部署后才可见。如果你从本文中只记住一件事:从未离开你笔记本的互操作性测试只是在测试你的 mock。
0 Comments
Log in to join the conversation.No comments yet. Be the first to share your thoughts.