这是同一跨云货币基准的第三次运行,也是首次
箭头反向的测试。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-central1 → us-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-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 小时阈值。陈旧规则在真实发布的汇率上触发,而不是在固定数据集上触发,这里的信号更有用。
本次运行未确立的内容
-
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 形状。六处缺陷中有五处在真实部署之前不可见。如果从本文中获得一个要点:从未离开笔记本电脑的互操作性测试只是在测试你的模拟。
0 Comments
Log in to join the conversation.No comments yet. Be the first to share your thoughts.