A2A协议打通了,但三大Agent框架的坑比想象中多
AIAI Summary (BLUF)
本文通过在同一研究Agent上分别使用Google ADK、AWS Strands和微软Agent Framework构建并部署,以A2A协议互联,实际测试揭示了协议虽能互通,但框架在Prompt参数、模型声明方式、工具绑定及任务完成响应上存在显著差异。文章强调区分平台差异与模型差异,并分享了避免踩坑的关键经验。
核心洞察
先说结论:A2A 协议本身真没坏,但协议通了之后,框架之间的差异才真正暴露出来。这篇文章最狠的决定是让三家共用同一个搜索函数,因为各家的原生搜索不在一个水平线上,不这么干结果没法比。有点保留的是,共用搜索函数虽然公平,但你也看不到各家的真实搜索水平。
核心结论
A2A 协议本身没有问题,但协议打通后框架差异才真正显现;在 A2A 中“调用成功”和“拿到答案”是两回事——直接读
task.artifacts在 Microsoft Agent Framework微软的Agent框架,使用Agent类与FoundryChatClient,通过A2AExecutor提供A2A服务。 上会拿到空字符串,且 ADK 的回复会出现双份镜像,导致文本草稿字数翻倍被判超限。Google ADK 生成的 agent card 会把不可路由的
0.0.0.0:8080写入公网端点,导致 ADK 自己的RemoteA2aAgent客户端连不上 ADK 托管的服务器;而 a2a-sdkAWS Strands使用的参考A2A路由SDK。 和 agent-framework 的客户端因不按 card 路由或会重写 interfaces 而正常。AWS AgentCore 不透明传
A2A-Version头,使 a2a-sdk 默认按 0.3 处理并被服务端拒绝,报错却指向协议版本;同时 AgentCore 按会话开微 VM,每次调用新建 session id 时耗时约 5953ms,固定 session id 后降至约 710ms,相差近 8 倍。同一个任务与模型下,评分器选择会完全改变模型排名:rubric 评分下没有模型占优,换模型裁判后 gpt-5-miniOpenAI的GPT-5 mini模型,在Microsoft Foundry上提供。 胜率 43%→87%、Nova 33%→0%;但各模型可用性差异更决定性,Gemini 仅应答 58% 的 brief(Vertex 429 限流),Nova 可用性 100%。
这个项目在折腾什么
这篇文章把同一个研究 agent 在 Google ADK、AWS StrandsAWS的Agent框架,提供Agent类,使用BedrockModel封装模型,用@tool装饰器绑定工具。 和微软 Agent Framework 上各建了一遍,三个 agent 用 A2A 协议跟同一个协调者通信。同一条指令,同一个搜索工具,同一个字数限制。
代码在 github.com/xbill9/multicloud-a2a-subagent,三个实现加协调器,全在里面。
我们想知道的其实很简单:协议通了之后,三家框架的差异到底在哪。
A2A 协议官网把话讲得很满:
在一个 agent 由不同框架、不同厂商构建的世界里,A2A 提供了 agent 互操作性的通用语言。
—— a2a-protocol.org
这句话在线路上是对的。但线路不是全部。
A2A 是什么
A2A(Agent2Agent)是一个开放协议,让不同团队、不同框架建的 agent 互相调用。一个 agent 在 /.well-known/agent-card.json 发布一张卡片,说明自己做什么、怎么联系,然后通过 HTTP 用 JSON-RPC 通信。这个项目跑的是 A2A v1.0。
更多细节:Agent2Agent (A2A) Protocol
三个技术栈
一个研究 agent,一份指令,一个搜索工具,一个字数上限。每个都建了三遍,跑在各自厂商的运行时上:
| AWS | Azure | ||
|---|---|---|---|
| 框架 | ADK LlmAgent |
Strands Agent |
Agent Framework Agent |
| 模型 | gemini-2.5-flash |
us.amazon.nova-micro-v1:0 |
Foundry 上的 gpt-5-mini |
| 服务方式 | to_a2a() |
a2a-sdk 参考路由 |
A2AExecutor |
| 托管位置 | Cloud RunGoogle Cloud 的无服务器容器运行环境,用于部署和扩展后端服务。, us-central1 | Bedrock AgentCoreAWS的Agent托管服务,用于部署Strands Agent。, us-west-2 | Container Apps, westus2 |
一个协调者把同一份任务发给三个 agent,然后统一打分。
下面所有内容,都不是在说 A2A 的毛病。A2A 本身没问题。这篇聊的是协议通了之后剩下的一堆差异,还有一个几乎没人拆开看的问题:哪些差异来自平台,哪些差异来自模型。
哪些东西必须保持一样
第一版跑出来就是个 demo:三个 agent,三个 SDK,三个绿勾。它什么都没告诉我。三个实现在九个地方不一样,你没法把任何一个结果归因到任何一个变量上。
所以后来定了一条规矩:除了被测变量,其他全共享。
| 共享,全项目只有一个实现 | 故意不同 |
|---|---|
| 任务书和它的焦点问题 | agent 框架 |
| 指令,带版本号 | 模型 |
| 搜索工具和它的六次调用预算 | 服务栈 |
| 评分标准,带版本号 | 托管平台 |
| 线上格式:markdown,统一固定头标注 | 凭证机制 |
| 失败分类 | 工具绑定 API |
右边一列是文章的主体。左边一列把这件事从轶事变成了证据。
最容易被质疑的是搜索工具。我让三家云用同一个搜索函数,没用各家的原生搜索。这个决定我最有底气。
三家里面,只有 Google 有现成的搜索工具。微软的 Agent Framework 导出的是 SupportsWebSearchTool,名字看着像工具,但它声明的是"聊天客户端可以支持 web 搜索"这件事,不是一个能扔给 agent 的工具。Foundry 自己的 grounding 要在外面先建一个 Bing 资源连接。Strands 什么都没带。
要是坚持"各自用原生搜索",结果是 Gemini 对接 Google 索引,Foundry 模型对接 Bing,Bedrock 对接一个不存在的东西。三个检索产品,最后报告出来的差距会被当成模型的差距。
真正还保留着原生实现的,是每个框架怎么绑定和驱动工具这一部分。现在会变的,只剩这部分。
三个框架,三种形状
下面是每个云上模型侧的全部代码。不是节选,全部就这些。
Google, ADK:
from google.adk.agents import LlmAgent
LlmAgent(
model="gemini-2.5-flash", # 模型 ID 字符串
name=..., description=...,
instruction=INSTRUCTION, # 参数叫 instruction
tools=[web_search], # 一个普通可调用对象
)
AWS, Strands:
from strands import Agent, tool
from strands.models import BedrockModel
Agent(
model=BedrockModel(model_id="us.amazon.nova-micro-v1:0"),
system_prompt=INSTRUCTION, # 参数叫 system_prompt
tools=[tool(web_search)], # 显式加了装饰器
)
Azure, Agent Framework:
from agent_framework import Agent
from agent_framework.foundry import FoundryChatClient
from azure.identity import DefaultAzureCredential
Agent(
client=FoundryChatClient( # 拿的是 chat client,不是模型
project_endpoint=os.environ["FOUNDRY_PROJECT_ENDPOINT"],
model=model_id(),
credential=DefaultAzureCredential(),
),
instructions=INSTRUCTION, # 参数叫 instructions,复数
tools=[web_search],
default_options={"store": False},
)
系统提示词有三个名字。模型被指定的方式有三个层级:字符串,模型对象,一个攥着 endpoint 和 credential 的 client。工具的约定有三种。
没有一种是难的。但没有一种能照搬到另一个。
不存在一个适配器能把这三样变成同一个对象。我见过有人花时间做这个,最后不过是多了一个要维护的第四样东西,然后这个适配器成了真正被测试的对象。提示词、工具、线上格式可以共享。agent 不要想着共享。
三个框架,两个不给你落脚点
Strands 交给你的就是一个函数。外面想怎么包都行:
async def respond(prompt: str) -> str:
return str(await agent.invoke_async(prompt))
ADK 和 Agent Framework 不行。to_a2a() 接收一个 agent,序列化它的事件流。A2AExecutor 也是直接调 agent。两个都不给你 (prompt) -> reply 的边界,所以任何想插在模型和线路之间的东西,都得塞进框架自己的对象模型里。
这不是风格上的抱怨。它决定了一个事实能在哪一层被记录。这个系统里的每份回复草稿,都带一行:
同一个研究 Agent,在三个云上各建了一遍(二)
服务器写下的那行元数据
三个服务都上线之后,我先抓了每张 agent card 的原始输出。开头那行 HTML 注释长这样:
<!-- a2a-research agent=gcp model=gemini-2.5-flash brain=llm -->
这行是服务器写的,模型碰不了。它带着两样信息,协调器从自己这端的连接里看不出来:真正回答问题的是哪个模型,以及到底有没有模型回答。要是让模型自己上报元数据,一个报错身份的模型就会在审计里把草稿记到别人名下。这种错误,审计从内部发现不了。
在 ADK 上,server 外面那层 wrapper 在我加工具那天变成了承重墙。第一版实现把流里每个事件的文本都拼起来,当时流里只有一条事件,怎么拼都对。挂上 web_search 之后,流里多出了模型在工具调用前后的自言自语。全拼进去,草稿开头先播一段它自己的查资料过程。只留 event.is_final_response()。
任务完成和拿到答案是两回事
ADK 和 Agent Framework 都返回 TASK_STATE_COMPLETED 的 Task,都符合规范。分歧点在于回复放在哪里。
ADK 把回复挂成产物,历史里再留一份。Agent Framework 的 A2AExecutor 跑完整个生命周期,回复作为 ROLE_AGENT 消息躺在历史里,artifacts 为空。a2a-sdk 自带的参考执行器,AWS 那个 agent 就踩在它上面,只入队一条 Message,整个任务生命周期都不存在。
所以那个最直白的客户端写法,读 task.artifacts,对着 Google 一切正常,对着 Microsoft 拿回来一个空字符串。不是报错,不是超时,一次成功的调用,没有内容。然后在下游某个解析环节炸掉,报错指向的层次跟真正原因隔着两层。
把规范允许的每个载体都读一遍,撞上镜像 bug:ADK 的回复出现两次,每个信封各一份。
为什么它能藏这么久,值得说。上一版 agent 返回的是汇率,解析器按目标货币做索引,重复对象悄无声息覆盖掉自己,算出来的答案一直是对的。换成文本草稿,正文变两份,字数翻一倍,评分器把一份完全合规的草稿判成 100% 长度超限。
后来我是读输出才发现。一个云返回 202 个词的文本,另外两个返回 98 个词。
测试套件一直绿的。没有任何一条测试逮住它。现在项目里加了一条常驻测试,断言三个服务栈返回同一段写死的文本,长度完全一致。这是我知道的,对付整类问题最便宜的探测器。
在 A2A 里,"调用成功"和"你拿到了答案"是两回事。
Agent Card 公布了一个连不上的地址
to_a2a(agent, host, port) 会把绑定地址直接写进卡片。部署完 curl 一下就看到:
$ curl -s https://<the-adk-agent>.run.app/.well-known/agent-card.json
{"url": null,
"additionalInterfaces": [
{"url": "http://0.0.0.0:8080", "protocolBinding": "JSONRPC"}
]}
一个公网 HTTPS 端点,公布的是不可路由的明文地址。0.0.0.0 在容器里是合法的绑定地址,离开容器,没人能拿它当目的地。AWS 和 Azure 的 agent 都接受 PUBLIC_URL 并公布它。ADK 缺的就是这个小行为,不是什么高深技术。
这问题本地复现不了。笔记本上绑定地址和拨号地址是同一个字符串。它只有一个部署环境里才出现,所以它能一路活到部署环境里。
哪些客户端能活下来,结果跟直觉相反:
| 客户端 | 对着部署后的 ADK 服务端 |
|---|---|
a2a-sdk |
正常,解析完卡片后重写了 interfaces |
agent-framework 的 A2AAgent |
正常,根本不按卡片路由,坏卡片不起作用 |
google-adk 的 RemoteA2aAgent |
失败,按卡片路由,去连 0.0.0.0:8080 |
ADK 自己的客户端连不上 ADK 自己托管的服务器。两边在 Google 自己的测试里都是绿的,因为本地两个地址相同。那个不需要修补 card 的栈,从来也不需要,它连接的是构造时给它的那个 URL,根本不按 card 走。
然后这个失败会在错误的层次上报:
AttributeError: 'A2AClientError' object has no attribute 'status_code'
错误处理器假定每个 A2AClientError 都带着 status_code,但传输失败没有 status_code。真实原因落在另一条日志行上。两个缺陷叠在一起:第一个把客户端送去不可路由的地址,第二个把送去的痕迹从报错里抹掉。
平台会改你的请求
运行时照样不是部署细节。三家平台对各自的容器有一份契约,互不认同:
| Cloud Run | AgentCore Runtime | Container Apps | |
|---|---|---|---|
| 端口 | $PORT,默认 8080 |
9000 | 8080 |
| 调用路径 | 你自己的 | /(平台暴露 /invocations/) |
你自己的 |
| 健康检查 | 你自己的 | GET /ping → {"status": "Healthy"} |
你自己的 |
| 架构 | 任意 | 必须 ARM64 | amd64 |
| 构建 | 源码、buildpack,不需要 Dockerfile | 镜像 | 镜像 |
| 入口认证 | 一个部署开关 | IAM + CUSTOM_JWT |
单独一步 |
| 冷启动单元 | 实例 | 会话 → microVM | revision 副本 |
其中三行的代价是实打实的时间。
AgentCore 不透传 A2A-Version 头。a2a-sdk 从这个头读协议版本,读不到就默认 0.3,而它自己的 handler 只认 1.0。默认值把请求送进了拒绝分支:
A2A version '0.3' is not supported by this handler. Expected version '1.0'.
Cloud Run 和 Container Apps 都原样透传。同一个客户端,同一份 a2a-sdk,同一份服务端代码,第三个云报一个听起来像是指责协议版本的错,报错里一个字都没提是平台把请求头删了。
修复办法:缺头时按当前版本处理,仅限缺头时。如果头里明确写着 0.3,那是客户端的真实声明,该拒还是拒。缺席不是旧客户端的证据,缺席就是缺席。
AgentCore 为每个会话开一个独立微 VM。我一开始每次调用都新造 session id,等于每次调用都在付一次微 VM 冷启动的钱。它看起来像个固定成本,直到那个慢的启动在客户端之间挪了窝才露馅。固定成本不会挪窝。会挪窝的是按调用计的东西。数据长这样:
google-adk → AWS |
运行次数 | 实测耗时 |
|---|---|---|
| 每次调用新建 session id(默认行为) | 5 | 5953, 5970, 5926, 5984, 6037 ms |
| 固定 session id | 2 | 710, 704 ms |
把 session id 钉住,除非你真的要每次调用互相隔离。另外两个云没有对应的开关,这笔钱按每条链路取平均也看不见。
Container Apps 把"谁能拿令牌"和"谁必须出示令牌"拆成两次部署。一次部署创建联合凭证,另一次在入口强制身份。只做第一半,这条路会开开心心报告认证模式已经启用,然后放任何一个人进来。
这不是假设。2026 年 8 月 13 日,这条路的负向对照组无凭证就通了。直接检查,/health、agent card、JSON-RPC 调用端点全部对匿名请求返回 200。这个 agent 后面挂着按量计费的模型。当时项目里其它信号全是绿的,这就是当初坚持要负向对照的理由。
调用三家 agent 是三份不同的活
客户端侧的框架差异同样存在,而且它决定你手上有哪些维修工具。
agent-framework 的 A2AAgent:从一个 URL 构建,await 一下 .run(prompt),读 .text。两行代码。card 解析和传输全在内部,方便得不行,直到某个服务器公布一张坏 card。
a2a-sdk:解析 card,改 card,建客户端,迭代 typed chunk,关掉。啰嗦,但它是唯一一个足够底层、能绕开上面那个 card 缺陷的客户端。
抓完卡片,拿客户端去戳服务。三种客户端实现,三个服务,交叉打一遍。
先交代一个让我在代码里翻了一阵才发现的设计前提。google-adk 的 RemoteA2aAgent 是个 BaseAgent,设计上待在一个 agent 树内部。拿它当普通客户端用,意味着每次请求都要现场立一个 Runner、一个 session service、一个 session。ADK 那套栈的假设是:A2A 是 agent 之间的事,不是程序直接干的事。
纯客户端、没有模型在链路里、loopback,逐格跑一遍:
client \ server gcp aws azure
-------------------------------------------------
a2a-sdk ok 134ms ok 8ms ok 8ms
agent-framework ok 129ms ok 7ms ok 8ms
google-adk ok 920ms ok 9ms ok 10ms
9/9 attempted cells succeeded
都过了。但先别急着高兴。九个格子是同一个实验的九次展示,不是九个独立实验。三个客户端栈翻到底层,都是同一个 a2a-sdk 的传输实现,三个服务里两个共享一套 serving 脚手架。共享实现到这个程度,全绿是默认值。真正需要解释的是后面那些静默失败:两头都是同一套代码,失败的机会从哪来的?
九格里唯一抢眼的是 google-adk 打 gcp 那格,920ms,同列另两格是 134ms 和 129ms。单次运行别当基准,但 ADK 每次请求现搭 Runner、session service、session,这笔固定开销是实打实的。
rubric 看不见的模型差异
客户端框架先按住不动,看另一个轴。三个模型,故意选得彼此不搭:
| gemini-2.5-flashGoogle的LLM模型。 | nova-micro | gpt-5-mini | |
|---|---|---|---|
| 定位 | 通用快模型 | 小,便宜 | 推理模型 |
| 接入路径 | ADK → Vertex | Strands → Bedrock | Agent Framework → Foundry |
| 为什么选它 | ADK 的默认 | 从两字段查询任务继承来的,写文章是糟糕的默认 | 没得选,下面说 |
gpt-5-mini 那格是我见过最典型的"模型选择不是偏好"。FoundryChatClient 讲的是 OpenAI Responses API。传 store=False 想让服务端别存任何东西,框架就会去要求 reasoning.encrypted_content,gpt-4.1-mini 直接拒绝,只有推理模型接这个字段。
区域也被锁死了。Container App 在 westus2,那里没有 Azure OpenAI 模型,调用要跨到 westus3。两个跟写作质量毫无关系的约束,把这条链路的模型和延迟都定死了。
24 个 brief,每个三家都答,每份答案打两次分。第一次用确定性评分器,第二次拿一个模型当裁判,对同一批存稿做重排:
| 云 / 模型 | 可用性 | 胜率(rubric → 裁判) | 遗憾值(rubric → 裁判) |
|---|---|---|---|
| azure / gpt-5-mini | 96% | 43% → 87% | 0.97 → 0.52 |
| gcp / gemini-2.5-flash | 58% | 43% → 43% | 1.54 → 2.21 |
| aws / nova-micro | 100% | 33% → 0% | 1.32 → 9.38 |
胜率是配对对比里赢的比例,遗憾值是输的时候平均落后多少。箭头左边是 rubric 打出来的,右边是模型裁判给的。这张表砸出四件事,只有一件跟写作有关。
可用性比文采更能拉开差距。Gemini 只答了受邀 brief 的 58%,三家最低,而它那条链路根本不出自家云。记在它头上的失败是 Vertex 429,配额是文档里的因,不是查出来的因。不管怎样,限流这种厂商差异,任何论文打分器都抓不到。在这个语料上,它比文采指标更决定结果。
换评分器,哪个模型好看完全变样。说实话,第一眼看到 43% 跳到 87%,我以为是评分脚本写错了。复查一遍没写错,是评分器换了。rubric 下 Nova 只落后最佳 1.32 分,模型裁判下落后 9.38。rubric 下没有模型占优,模型裁判下 gpt-5-mini 拿下 87%,Nova 挂零。这个样本里谁是最优模型,完全由评分器决定。还有个细节,模型裁判就是 Gemini,它给自己家参与方打了 43%,给了 Azure 87%。这削弱了"裁判偏袒自家"的质疑,但没完全消除。
延迟归因的时候先别怪模型。最慢那条链路是推理模型跨区域调用,起因是一个存储参数。最快的是个小模型,平台在你忘了钉 session 时还会收一次 microVM 启动费。
有没有草稿这回事,换谁都挪不动。可用性列在两个裁判底下完全一样。它是唯一不用等评分器跟人工校准就能读的列。
工具能用,不等于工具被用
三家拿到同一个工具、同一时间点、同一个六次搜索预算。用不用,按模型和指令版本裂开了:
| 零搜索的草稿数 | |
|---|---|
| aws, instruction v1 | 7/7 |
| aws, v2 | 2/9 |
| aws, v3 | 1/7 |
| azure, 所有版本 | 1/16 |
| gcp, v3 | 无,每次跑都烧完六次预算 |
一个发现的两个极端。Nova 被明确说了两遍,七次里仍然跳过搜索。Gemini 在 v3 里每次顶在预算天板,预算本身开始影响我在比较的草稿。
第一次真正带模型的 run 把这个问题暴露得最尖锐:
azure searches=2 evidence 0.0
gcp searches=0 evidence 5.0
aws searches=0 evidence 0.0
证据拿满分的模型一次都没搜。五分长得像引文的文本,背后什么都没有。rubric 数的是"有没有引文"这个动作。搜索功能出现之前,我就把"模型可能假装有引用"记成已知弱点了,现在终于有了实测数据。
原因在模型上游:共享指令从头到尾没叫任何人去搜索。修这个花了三个指令版本。v2 是另一个方向的警示,它写的是"每个具体数字搜一次",Gemini 照字面执行,给一篇 300 词的 brief 花了 24 次搜索,光这一项就能烧光项目整个 Vertex 配额。v3 把工具强制的预算写进指令,模型对着边界做规划,而不是被边界拦腰砍断。
指令要像 rubric 一样做版本管理。指令改动前后的两次 run 答的已经不是同一道题。跨版本取平均的审计,会把一次指令修订报成模型本身的变化。
这条路几乎没有大声的失败
三个云上反复出现的失败形状是这几个:
- 有个 agent 以 llm 模式对外服务,注册的工具数是零。工具连接失败只留下一行 WARNING,期间 /health 一直回 200。
- provider 配额错误返回一个 31 词的响应体,过了最小词数线,被盖戳当成草稿,25 分里拿了 7.97,发回去重写。整个 run 报告 3/3 云在正常回答。
- federated credential 配置完全正确,agent 连着几天向公网提供服务,因为强制校验是另一个独立步骤,当时没启用。
- 最后抓到问题的那个控制 harness 自己带着五个缺陷,其中四个产生假通过。
- 第一次部署后的 run,两个远端都回了 200,然后各吐十个词的拒答。查下来它们还在跑一周前上个项目的 agent。
这些坑让我养成了两个习惯,以后做任何类似的 mesh 都会带上。
给失败分类。transport、protocol、timeout、authentication、provider。这里最值钱的是 provider:模型拒绝回答某个话题,这是 provider 层的结果。把它归成 protocol,"Bedrock 拒了"就变成"AgentCore 把 A2A 弄坏了"。
让 agent 报自己的事实。brain、model、degraded flag、搜索次数,都从 agent 端拿,只有它自己知道。我的矩阵一开始从自己的进程读 mode,部署后矩阵跑在另一个容器里,于是一个三个 agent 都接了模型的 mesh 打出了 brain=direct。
为什么同一套 agent 要建三遍?
一个 agent 对框架什么都说明不了。
这一路测下来,每个发现都是一个差异,差异需要两个对象才比得出来。卡片上的那个缺陷需要一个不是 ADK 的客户端才能暴露。空回复得靠对着别家服务器写的客户端才看得见。请求头被丢,要同一份代码跑在不丢请求头的平台上才能现形。模型结果得把指令、工具、rubric 全部钉死,只让云在动。
只建一次,上面每一条要么看不见,要么看见了也没法归因。
故障速查
HTTP 200、task COMPLETED、回复是空字符串。回复在 task.history 里,不在 artifacts 里,Agent Framework 的 executor 就把它放那儿。规范允许的每个 carrier 都翻一遍。
草稿收到两份,词数翻倍。ADK 把它既放进 artifact 又放进 history。去个重:一个回复装在两个信封里,还是一个回复。
看到 A2A version '0.3' is not supported by this handler,是 AgentCore 把 A2A-Version header 丢了。header 缺失时按当前版本假设,而且只在缺失时这么假设。
AttributeError: 'A2AClientError' object has no attribute 'status_code' 这个报错,是 ADK 客户端拨了 card 里的 bind 地址,连不上。发布一个 PUBLIC_URL,或者在 card 解析之后重写 interfaces。
翻代码才定位的几个问题
单条 leg 跑一次大概六秒。慢的那条总是从 AgentCore 这边发起,跟目标服务是谁没关系。AgentCore 每次调用都拿一个新的 session id,每个 session 背后是一个全新的 microVM,冷启动全算在延迟里。我在 coordinator 里把 session id 钉死之后,延迟才稳定下来。
leg 上报的是 federated auth,可匿名请求照样打得通。ingress 的强制校验和 credential 的创建是两个独立的部署步骤。我建完 credential 直接跑,匿名请求照样 200。得单独把 ingress enforcement 跑一遍,再不带 credential 打一次这个 leg,确认返回 403 才算完。
Azure 那边的角色分配看着没问题,inference 却给我 403。查了半天才发现容器里拿着的是 managed identity 在授权生效前签发的 token。重启 revision 让 token 刷新,就好了。
ADK 的 event stream 也得留意。模型的草稿会以一段研究过程叙述开头,比如“我现在开始搜索……”这种。event stream 里每个 tool call 前后都夹着模型的评论。取结果时只认 is_final_response() 的 event,别把中间叙述当结果存下来。
评测那边有个更隐蔽的。模型一次搜索都没做,证据分拿满。我看 scorer 代码才发现它统计的是“长得像引用”的文本,模型编几个引用格式就蒙混过去了。后来我在每个草稿旁边记录 search count,跟分数放一起看,问题一眼就能揪出来。
配额错误也坑过我。审计日志直接把它记成了分数。provider 返回的 quota error 文本不短,光设一个最小字数门槛根本拦不住。后来我在写入审计结果之前先匹配 provider 的错误签名,命中就直接标失败,不进评分流程。
Discovery 的 403 和 invocation 是两回事。invocation 能通,discovery 却 403。agent card 跟 agent 本体挂在同一个授权策略后面,credential 得挂在 client 上,不能挂在单个 request 上。我一开始把 credential 塞在请求里,discovery 阶段拿不到,自然就 403 了。
总结
三个框架没法统一成一个对象。能共享的是 prompt、tool、报文格式,agent 对象本身别去共享。每个框架都会改写你的请求,agent card 只能当配置看,不能当事实。模型之间差异最大的地方是可用性:一个问题扔过去,有的模型正常返回,有的直接超时,措辞好坏反而在其次。
我跑了 24 个 briefs。这个量算个比率够了,拿去信任一个结论不够。而且我的 briefs 全是技术调研,覆盖面本身就偏。我不会拿这些数字去比较模型,实际上我也不打算这么用。能确认的是趋势:平台差异由架构决定,换个平台同样的问题还会复现。模型差异集中在能不能拿到答案。每个框架都会藏一个问题不让你看见,而且藏的都不一样。
A2A 确实做到了它承诺的事。上面写的都是它做完之后剩下的边角料。换个人接第四个云进来,剩下的边角料又会是另一批。这本身就是我写下来的理由。
仓库里放了三个 agent、共享的 instruction 和 tool、coordinator 和 judge、3×3 互操作矩阵、负对照实验和部署脚本。docs/INTEROP.md 里每条发现都带测量日期,docs/RUNBOOK.md 里标了哪些结论是实测的,哪些还没验证。
github.com/xbill9/multicloud-a2a-subagent
常见问题(FAQ)
A2A协议真能让不同框架的Agent互相调用吗?
A2A协议确实能实现线路层面的互通,但框架差异依然存在。本文实测发现,三家框架在Prompt参数、模型声明、工具绑定等方面有明显差异,需区分平台差异与模型差异。
Google ADK、AWS Strands和微软Agent Framework有什么区别?
三者在模型声明方式上不同:ADK用模型ID字符串,Strands用BedrockModel对象,Agent Framework用FoundryChatClient。系统提示词参数名也各异:instruction、system_prompt、instructions。
多框架Agent对比测试怎么做才公平?
为了公平,文中让三个Agent共用同一个搜索函数、同一份指令和评分标准,只改变框架和模型。这样能区分框架差异与模型差异,避免原生搜索能力不同导致的混淆。
版权与免责声明:本文仅用于信息分享与交流,不构成任何形式的法律、投资、医疗或其他专业建议,也不构成对任何结果的承诺或保证。
文中提及的商标、品牌、Logo、产品名称及相关图片/素材,其权利归各自合法权利人所有。本站内容可能基于公开资料整理,亦可能使用 AI 辅助生成或润色;我们尽力确保准确与合规,但不保证完整性、时效性与适用性,请读者自行甄别并以官方信息为准。
若本文内容或素材涉嫌侵权、隐私不当或存在错误,请相关权利人/当事人联系本站,我们将及时核实并采取删除、修正或下架等处理措施。也请勿在评论或联系信息中提交身份证号、手机号、住址等个人敏感信息。



