KEEL · 龙骨 · A CURRICULUM FOR THE AI ERA

02 · Agent Card:一张卡为什么能撑起整个发现机制 — keel 龙骨

## 现场:能力清单写在 wiki 里,第八周就过期了

现场:能力清单写在 wiki 里,第八周就过期了

平台团队把变更分析 Agent 的能力清单写在了内部 wiki 上:能做什么、怎么连、要什么凭据。SRE 团队照着写客户端,跑通了。

第八周,平台团队给 Agent 加了 gRPC 端点,把 JSON-RPC 的旧地址标记为即将下线,然后忘了改 wiki。SRE 的客户端照旧连旧地址,某天开始收到 404,助手回答「变更分析不可用」。值班工程师查了半小时才发现:服务端没坏,是客户端在按一张过期的清单找人。

这类问题的根源不是谁疏忽,而是把「能力声明」放错了地方:放在人读的文档里,它一定会被程序当作真相来用。 A2A 的解法是把这份声明变成机器读的、由服务端自己发布的、带版本的 JSON——Agent Card。

直觉模型:名片、接线说明与门禁规则,三合一

一张 Agent Card 同时承担三件事:

它在回答的问题 对应字段
你是谁、能干什么 name description skills provider version
怎么连你、用哪种协议 supportedInterfaces
凭什么让你干活 securitySchemes securityRequirements

把它想成一张门禁卡:正面写着身份和能力,背面写着闸机在哪、刷卡规则是什么。少了任何一面,对面都进不来。

精确定义:14 个顶层字段,8 个必填

按规范 v1.0.1 的 AgentCard 定义(检索于 2026-10-05),顶层字段共 14 个:

字段 必填 说明
name 是 人类可读名称
description 是 用途说明,给人和模型读
supportedInterfaces 是 有序接口数组,第一项为首选
version 是 Agent 自己的版本(不是协议版本)
capabilities 是 能力开关:streaming pushNotifications extendedAgentCard
defaultInputModes 是 默认接受的媒体类型
defaultOutputModes 是 默认产出的媒体类型
skills 是 能力单元列表
provider 否 提供方信息
documentationUrl 否 更多文档的链接
securitySchemes 否 认证方案定义
securityRequirements 否 调用所需的认证要求
signatures 否 JWS 签名数组(第 08 章)
iconUrl 否 图标

每个 supportedInterfaces 条目(AgentInterface)自己带四个字段:

字段 必填 说明
url 是 生产环境应是绝对 HTTPS 地址
protocolBinding 是 JSONRPC / GRPC / HTTP+JSON(开放字符串,允许厂商扩展)
protocolVersion 是 该接口暴露的 A2A 版本,如 1.0
tenant 否 多租户时的路由标识

v1.0 相对 v0.3 的三处结构变化

这三处是迁移时最先炸的地方(来源:a2a-protocol.org What's New in v1.0,检索于 2026-10-05):

v0.3.0 v1.0 后果
顶层 url + preferredTransport + additionalInterfaces 合并为有序 supportedInterfaces[] 老客户端读不到地址
顶层 protocolVersion 下沉到每个 AgentInterface 一个 Agent 可以同时暴露 0.3 和 1.0 两种接口
supportsAuthenticatedExtendedCard 移到 capabilities.extendedAgentCard 老客户端拿不到扩展卡

第三行值得多想一层:协议版本从「整个 Agent 的属性」变成了「每个接口的属性」。这是 A2A 能做平滑迁移的技术前提——同一个 Agent 可以在不同 URL 上同时提供新旧两个版本,让客户端按自己的能力挑。

另外,发现路径本身也变过:v0.3.0 起是 /.well-known/agent-card.json,更早是 /.well-known/agent.json。

全链路图:一次发现走过五跳

先看形状再读细节:取卡是一次跨网络的读,校验和选接口全在客户端本地完成——服务端不参与「你该用哪个接口」这件事,它只负责如实地把有序数组发出来。

flowchart TD
    subgraph CLIENT["A2A Client —— 调用方进程"]
        C1["① GET /.well-known/agent-card.json<br/>agentcard.WELL_KNOWN_PATH"]
        C2{"② validate_card<br/>8 个必填字段齐全?"}
        C3["③ compatible_interfaces<br/>版本 ∩ 绑定 求交集"]
        C4{"④ 交集为空?"}
        C5["⑤ choose_interface<br/>取 supportedInterfaces 第一项"]
    end
    subgraph SERVER["A2A Server —— 远端 Agent(不透明)"]
        S1["发布点 /.well-known/agent-card.json<br/>server.agent_card"]
        S2["supportedInterfaces[0]<br/>JSONRPC 1.0 @ .../a2a/incident-bridge"]
    end
    C1 --> S1
    S1 -->|"卡面 JSON"| C2
    C2 -->|"缺 skills 等"| FAIL1["早失败 报出具体缺失字段名"]
    C2 -->|"结构合法"| C3
    S2 --> C3
    C3 --> C4
    C4 -->|"是"| FAIL2["no compatible A2A interface<br/>不降级 不硬聊"]
    C4 -->|"否"| C5
    C5 --> DONE["⑥ 带着 url 与 A2A-Version 发后续请求<br/>server.dispatch"]
    FAIL1 -.-> C1
    FAIL2 -.-> NOTE["卡面过期也会走到这里<br/>取卡时间要自己记"]

图里两个失败点值得对照:

注意 FAIL1 -.-> C1 那条虚线:校验失败后正确的动作是重新取卡并重验,而不是改本地缓存里的字段把洞填上。

一次完整运行

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

实跑输出(节选与卡面直接相关的部分):

H. 卡面字段: ['capabilities', 'defaultInputModes', 'defaultOutputModes', 'description', 'documentationUrl', 'name', 'provider', 'securityRequirements', 'securitySchemes', 'skills', 'supportedInterfaces', 'version']
I. 拿掉 skills 再校验 -> ['skills']
J. 首选接口的完整条目: {'url': 'https://agents.acme.internal/a2a/incident-bridge', 'protocolBinding': 'JSONRPC', 'protocolVersion': '1.0', 'tenant': 'acme'}

逐行读:

  1. H 列出的是本项目实际发布的字段。规范的 14 个里,iconUrl 和 signatures 没发——前者是可选装饰,后者只在签名时才出现(第 08 章)。可选字段不发是合法的,必填字段少一个就不合法。
  2. I 是结构校验:把 skills 置空之后,校验函数指名道姓说缺 skills。注意它只查结构——结构合法不等于内容可信,这是第 08 章签名要解决的问题。
  3. J 是接口的完整条目:地址、绑定、协议版本、租户四件事绑在一起。租户在这里出现,意味着同一个 URL 后面可以按租户路由到不同 Agent——这是 v1.0 给 SaaS 场景留的位置。

失败注入

注入 A:客户端只要 gRPC

choose_interface(card, versions=("1.0",), binding="GRPC")

实跑输出:

E. 只想走 gRPC -> no compatible A2A interface

服务端没声明 gRPC,客户端就必须早失败,不能退回 HTTP 硬聊。原因在第 03 章会展开:三种绑定是功能等价的,但「等价」指的是同一套操作语义,不是说你可以临时换一条路。

注入 B:拿一张过期的卡

把卡里的 supportedInterfaces[0].url 改成旧地址再发消息——服务端会返回连接层错误,客户端如果只做了「拿到卡就发消息」,此时根本分不清是网络问题还是清单过期。

判断标准很实用:每次取卡都要能看到卡上的 version 与当前时间。卡没有「最后更新时间」这个字段,但你应当在自己的客户端里记录「这张卡是什么时候取的、当时的 version 是什么」,排障时才说得清是不是用了旧清单。

生产边界

教学实现 生产替换
函数调用直接返回卡面字典 HTTPS 服务在 /.well-known/agent-card.json 上发布,带缓存与 ETag
每次都全量读卡 本地缓存 + 过期策略 + 失败时重新取卡
单一 tenant 多租户路由,卡面按租户不同
未签名卡 JWS 签名 + 规范化(第 08 章)
客户端硬编码一个地址 注册中心或目录服务,但卡本身仍是权威来源

练习与验收

练习(有可观察结果):给卡加第二个接口(protocolBinding 用 GRPC,protocolVersion 用 1.0,url 指向 gRPC 地址),然后用 compatible_interfaces(card, versions=("1.0",)) 断言返回 2 条;再用 choose_interface(card, versions=("1.0",)) 断言拿到的是第一条(数组顺序即优先级)。

验收标准:把两个接口的顺序调换,断言首选随之变化。如果你的实现固定返回某个绑定(比如「能用 gRPC 就用 gRPC」),说明你替服务端做了它没做的决定——优先级由卡面顺序表达,客户端不该自作主张。

本章检查点

现在能解释什么

你现在能读懂一张 Agent Card:知道 8 个必填字段各自回答什么问题,知道 supportedInterfaces 是有序的、第一项即首选,知道协议版本现在挂在接口上而不是卡上。你也知道发现机制的全部依赖只有一个约定路径——不需要注册中心,但代价是卡必须是最新的、并且最终必须可验签。下一章讲连上之前的那一步:版本怎么协商,三种绑定怎么选。

进入 keel 阅读