KEEL · 龙骨 · A CURRICULUM FOR THE AI ERA

01 · A2A 解决什么,不解决什么? — keel 龙骨

## 现场:两个 Agent,两套框架,一次谁都不肯让的集成

现场:两个 Agent,两套框架,一次谁都不肯让的集成

Acme 有两个团队各做了一个 Agent。

SRE 团队的事件响应助手跑在 LangGraph 上,能读指标、查工单。平台团队的变更分析 Agent 跑在另一套框架上,能读部署记录、判断某次发布是否可疑。两个 Agent 都是各自团队的资产,谁也不会把源码、提示词和数据库凭据交给对方。

应急响应时人肉串一次要四十分钟:值班工程师在 SRE 助手里问一句,把结论复制出来,再贴进变更分析 Agent,等它回答,再粘回 SRE 助手。

于是有了三个集成方案,三个都失败了:

方案一:给变更 Agent 直接开数据库只读账号。 安全评审直接否了——你给了它读数据的权限,就等于把数据模型的耦合也给了它;哪天表结构改了,两个团队一起挂。

方案二:让 SRE 助手直接调变更 Agent 的内部 HTTP 接口。 上线两周就出事:对方改了字段名没通知,SRE 助手把 deploy_id 当成了 release_id 填进去,返回空,助手回答「没有可疑变更」。值班的人信了。

方案三:把变更分析的提示词复制进 SRE 助手。 结果是上下文暴涨,两个团队的提示词互相污染,谁都说不清一次结论到底依据了什么。

先停一下,预测三个方案的共同点:它们都在试图打通两个系统的内部。而 A2A 的思路正好相反。

直觉模型:MCP 是竖向的,A2A 是横向的

一句话分清两条协议:

方向 边界 典型问题
MCP 竖向:Agent 接工具和数据源 Agent 与它自己的工具 这个工具有没有、能不能调、参数对不对
A2A 横向:Agent 接另一个 Agent 一个自治系统与另一个自治系统 委派什么、进度如何、产物怎么取回

一个 Agent 节点通常同时用两者:对外用 A2A 接别的 Agent,对内用 MCP 接自己的工具。它们不是竞争关系,而是同一张图上的两条边。

精确定义:三个角色与一份不透明约定

一次 A2A 交互里有三个角色(来源:a2a-protocol.org v1.0.1 Core Concepts,检索于 2026-10-05):

角色 是什么 关键性质
User 发起请求的人或自动化服务 不一定是人
A2A Client(Client Agent) 代表用户发起通信的一方 今天它是客户端,明天它也可能是别人的服务端
A2A Server(Remote Agent) 暴露 A2A 端点、受理任务的一方 对调用方不透明(opaque)

「不透明」是整个协议的地基:调用方看不到对方的提示词、模型、记忆和内部工具,也不需要看到。它只看得见四样东西——卡面(Agent Card)、任务(Task)、消息(Message)里的 Part、以及产物(Artifact)。

规范把 A2A 不是什么也写得很清楚(同一来源):

这四条里最容易踩的是第二条:A2A 不做你系统内部的编排。你团队内部的 Supervisor / Worker / Handoff 属于多 Agent 协作那一层,A2A 只在两个自治系统之间那一段起作用。

全链路图

先看形状:左边的进程、右边的进程、中间那条协议边界,以及失败时断在哪。

flowchart TD
    subgraph LOCAL["本地系统 —— 你负责"]
        U[用户目标] --> CA[Client Agent 编排方]
        CA --> MCPL[本地工具 走 MCP]
    end
    subgraph BOUNDARY["协议边界 —— A2A 负责"]
        DISC["① 发现 GET /.well-known/agent-card.json"]
        SEND["② SendMessage 委派"]
        BACK["③ 返回 Task 或 Message"]
        FETCH["④ GetTask 或流式或推送"]
    end
    subgraph REMOTE["远端系统 —— 不透明"]
        RA[Remote Agent] --> MCPR[它自己的工具 走 MCP]
        RA --> INTERNAL[它自己的模型 记忆 提示词]
    end
    CA --> DISC
    DISC -->|版本或绑定不匹配| ERR["错误 reason=VERSION_NOT_SUPPORTED"]
    DISC -->|兼容| SEND
    SEND --> RA
    RA --> BACK
    BACK --> CA
    CA --> FETCH
    FETCH --> RA
    RA -.->|长任务未完 需补输入| BACK
    ERR -.-> CA

图里最关键的是中间那一层:跨进程、可失败、且失败时只回一个 reason,不会告诉你远端内部发生了什么。后面十章就是把这个边界上的每一件事讲清楚。

一次完整运行

python courses/foundation/a2a-protocol-engineering/course/project/examples/01_discovery.py

实跑输出:

A. 发现路径: /.well-known/agent-card.json
B. 必填字段 8 个 -> 缺失: 无
C. 首选接口: JSONRPC 1.0 @ https://agents.acme.internal/a2a/incident-bridge
D. 只会 0.3 的客户端 -> no compatible A2A interface
E. 只想走 gRPC -> no compatible A2A interface
F. 卡上声明的能力: {'streaming': True, 'pushNotifications': True, 'extendedAgentCard': False}
G. 公开技能: ['service.status', 'diagnose.incident']

逐行读:

  1. 发现靠一个约定路径,不需要任何注册中心或配置同步——知道对方的域名就够了。
  2. 卡面有 8 个必填字段,少一个就不是合法卡(第 02 章逐个展开)。
  3. 卡上同时写了协议版本和绑定方式,双方能不能聊得起来,在读卡这一步就能判断完。
  4. D 和 E 是两种不兼容:版本不对、绑定不对,都是「没有可用接口」。注意这里返回的是失败原因,不是「连上了但聊不明白」——在边界上早失败,比在业务里晚失败便宜得多。

失败注入:把「连上了」当成「聊得来」

最典型的误判是:拿到了一张卡,就以为可以发消息了。试一试——把客户端能接受的版本改成 0.3:

choose_interface(card, versions=("0.3",))

实跑输出:

D. 只会 0.3 的客户端 -> no compatible A2A interface

它不会给你一个「勉强可用」的接口。这是刻意的设计:协议版本不匹配必须早失败。如果这里放行,后面每一个字段名、每一个枚举值都可能对不上,而错误会藏在业务语义里——就像现场那个 deploy_id / release_id 的事故。

生产边界

教学实现 生产替换
内存里的卡面字典 HTTP 服务在 /.well-known/agent-card.json 上真发布,带缓存与 ETag
HMAC bearer 替身 卡面声明的真实方案(OAuth 2.0 授权码加 PKCE、Device Code、mTLS)
进程内函数调用 JSON-RPC over HTTPS、gRPC 或 HTTP+JSON 三种绑定之一
未签名卡 JWS 签名卡 + 规范化(RFC 8785),否则无法防伪造(第 08 章)
单一组织内的两个角色 跨组织、跨信任域,凭据与租户隔离

练习与验收

练习(有可观察结果):给 minimal_card_payload 加第二个接口(同一个 url,protocolBinding 用 GRPC,protocolVersion 用 1.0),然后分别用 binding="JSONRPC" 和 binding="GRPC" 调 choose_interface,断言两次拿到的是不同条目;再断言不传 binding 时拿到的永远是数组第一项。

验收标准:不传 binding 的结果必须等于 card["supportedInterfaces"][0]。如果你的实现按优先级数值排序或按字母排序,说明你把「有序数组」当成了「集合」——规范里第一项就是首选,排序权在服务端。

本章检查点

现在能解释什么

你现在能把 A2A 和其他几层分开:它是两个自治系统之间的协议,不是框架、不是内部编排、不是 MCP 的替代品。你也知道它最核心的约定是「不透明」——调用方拿不到对方内部,只能拿到卡面、任务、消息和产物,而这个限制正是它能跨组织工作的原因。下一章讲这套约定的入口:Agent Card 到底写了什么。

进入 keel 阅读