A2A 协议:让 Agent 之间互相发现与委派
- 搞懂 A2A 要解决的问题:跨 Agent、跨框架的协作与委派
- 看懂 agent card 是什么、放在哪、声明了哪些关键字段
- 跑通"拉对方 card → 查兼容 → 发任务"这条发现到委派的链路
- 彻底分清 MCP(给 Agent 接工具)和 A2A(让 Agent 之间协作)
- 拿到"什么时候用 A2A、什么时候一个函数调用就够"的判断
上一节我们把一个 Agent 内部的脏活折叠掉了。但真实世界里,你常常需要的不是"我自己的 Agent 多开几个子代理",而是让我的 Agent 去用别人家的 Agent——比如你的客服 Agent 想把一笔退款丢给财务团队那套你完全没参与开发、用的还是另一个框架的报销 Agent。
问题来了:你的 Agent 怎么知道对方会干什么、怎么触达它、要不要鉴权、用什么数据格式?A2A(Agent-to-Agent)协议就是为这件事生的:它定义了一套 Agent 之间互相发现、互相委派的通用规矩,让不同团队、不同框架做出来的 Agent 也能搭上话。 这一节我们把它讲透。
这篇适合谁:已经能写单个 Agent、开始要让多个 Agent(尤其是别人做的)协作的人。读完你能看懂一张 agent card,也能写出"发现→委派"的骨架。
钩子:没有 A2A 时,跨 Agent 协作有多难
设想没有统一协议,你的 Agent 要调别人家 Agent,得问一圈:
- 它部署在哪个地址?走 HTTP 还是别的?
- 它到底能干哪几件事?参数怎么传?
- 要不要 token?哪种鉴权?
- 它回我的是纯文本、JSON、还是流式?
每对接一个 Agent,都要人肉去读文档、写一套专属适配。对接十个 Agent,就是十套互不相通的胶水代码。 这正是当年各种 API 集成的老问题,只不过主角换成了 Agent。
A2A 的解法和"网站有 robots.txt、API 有 OpenAPI 描述"是同一个思路:让每个 Agent 自报家门——发布一张机器可读的"名片",把上面那些问题一次性答清楚。 这张名片就叫 agent card。
最小可用:agent card 长什么样
agent card 是一份放在已知 URL 上的 JSON,声明这个 Agent 的身份、能力、怎么触达、怎么鉴权、用什么数据格式。别的 Agent 想找它合作,先去拉这张 card 读一读,就知道能不能、怎么合作。
下面是一张说明性的 agent card 示例(字段命名以官方规范为准,这里重在让你看懂"一张名片该答清哪些问题"):
{
"name": "报销审批 Agent",
"description": "处理员工报销单的提交、审批与状态查询",
"url": "https://finance.example.com/a2a",
"version": "1.x",
"capabilities": {
"streaming": true
},
"authentication": {
"schemes": ["Bearer"]
},
"defaultInputModes": ["text", "application/json"],
"defaultOutputModes": ["application/json"],
"skills": [
{
"id": "submit_reimbursement",
"name": "提交报销单",
"description": "提交一笔报销,返回单据号与初步审批结果",
"tags": ["finance", "reimbursement"]
},
{
"id": "query_status",
"name": "查询报销状态",
"description": "按单据号查询当前审批进度"
}
]
}
读这张名片,你的 Agent 一眼就能回答前面那几个问题:它叫报销审批 Agent、走 https://finance.example.com/a2a、要 Bearer token、收 text 或 JSON、回 JSON、会两个技能(提交、查状态)。对接成本从"读一篇文档"降到了"解析一个 JSON"。
⚠️ 字段层面:A2A 仍在演进,具体字段名、card 的标准存放路径、请求/响应的精确结构以官方规范为准。本节给的是骨架与思路,不是逐字段的 API 文档——拿去对接前务必核对官方最新定义。
原理:发现 → 查兼容 → 委派,三步走
A2A 协作的核心链路就三步,记住它你就抓住了主干:
- 发现(Discovery):你的 Agent 知道对方的"已知 URL",去把 agent card 拉下来。已知 URL 可以是配置里写死的、注册中心查到的,或对方告诉你的。
- 查兼容(Match):解析 card,确认对方有你要的技能(skill id 对得上)、鉴权你能满足(有没有那个 token)、数据格式你能处理(它收/回的模式你接得住)。对不上就别发了。
- 委派(Delegate):兼容就按 card 声明的地址和格式,发一个任务请求过去,拿回结果。
类比找外包:先要到对方名片(发现)→ 看看人家会不会这活、收不收你(查兼容)→ 把活派过去并验收(委派)。
进阶:一段"发现→委派"的可读骨架
下面是这条链路的可读代码骨架。请求/响应的精确结构以官方为准,这里聚焦把三步的逻辑串清楚:
import requests # 演示用;实际请按官方 SDK/规范调整端点与负载结构
def fetch_agent_card(base_url: str) -> dict:
"""第1步 发现:从对方的已知 URL 拉 agent card(确切路径以官方为准)"""
resp = requests.get(f"{base_url}/.well-known/agent-card", timeout=10)
resp.raise_for_status()
return resp.json()
def is_compatible(card: dict, need_skill: str, my_token: str | None) -> bool:
"""第2步 查兼容:要的技能在不在、鉴权我能不能满足"""
skill_ids = {s["id"] for s in card.get("skills", [])}
if need_skill not in skill_ids:
return False
schemes = card.get("authentication", {}).get("schemes", [])
if schemes and not my_token: # 对方要鉴权但我没 token
return False
return True
def delegate(card: dict, skill_id: str, payload: dict, my_token: str | None) -> dict:
"""第3步 委派:按 card 声明的地址/鉴权发任务(负载结构以官方为准)"""
headers = {"Authorization": f"Bearer {my_token}"} if my_token else {}
resp = requests.post(
card["url"],
json={"skill": skill_id, "input": payload}, # 真实字段名以官方协议为准
headers=headers, timeout=30,
)
resp.raise_for_status()
return resp.json()
# —— 串起来:我的客服 Agent 想把一笔报销委派出去 ——
card = fetch_agent_card("https://finance.example.com")
if is_compatible(card, need_skill="submit_reimbursement", my_token=MY_TOKEN):
result = delegate(card, "submit_reimbursement",
{"amount": 320, "reason": "打车", "user": "老王"}, MY_TOKEN)
print("委派成功:", result)
else:
print("对方 Agent 不兼容这次委派,换一个或本地处理")
逐步预期
fetch_agent_card拿回那张 JSON 名片。is_compatible确认submit_reimbursement在它的 skills 里、且你有 token → 返回 True。delegate按 card 里的url和 Bearer 鉴权发任务,拿回单据号一类的结果。- 任一步对不上(技能没有、没 token、格式不匹配),就走 else 分支,不硬发——这正是 card 的价值:先看名片再动手,别瞎试。
增量一:MCP(给工具)vs A2A(agent 间),别再混
这俩最容易搞混,一句话钉死区别:
- MCP 是给一个 Agent 接"工具/数据源"的——比如让你的 Agent 能读数据库、调某个 API、用某个本地能力。它面对的是"死的、被动的能力",你调它、它返回,不会自己思考。
- A2A 是让"两个 Agent 之间协作"的——对方是一个活的、会自己规划和决策的 Agent,你不是调它的某个函数,而是把一个目标委派给它,让它自己想办法办完。
| 维度 | MCP | A2A |
|---|---|---|
| 对接的是 | 工具 / 数据源(被动) | 另一个 Agent(主动、会决策) |
| 你给它的是 | 一次具体调用 | 一个待办目标,由它自主完成 |
| 类比 | 给员工配一把电钻 | 把一摊活外包给另一个团队 |
| 典型场景 | 让 Agent 能查库存 | 让客服 Agent 把退款甩给财务 Agent |
口诀:MCP 给你的 Agent 配工具,A2A 让你的 Agent 找同行搭把手。 两者不打架,常常一起用——你的 Agent 用 MCP 接自己的工具,同时用 A2A 把超纲的活委派给别的 Agent。
增量二:agent card 字段速查 + 什么时候根本不用 A2A
一张 card 该答清的字段(命名以官方为准):
name/description:它是谁、干啥的。url:往哪发任务。capabilities:支不支持流式等特性。authentication:要不要鉴权、哪种。defaultInputModes/defaultOutputModes:收什么、回什么格式。skills[]:会哪几件事,每件有id/name/description/tags——这是查兼容时最关键的字段。
什么时候用 A2A:对方是你管不到、可能换框架/换实现、由别的团队维护的 Agent,而且你想委派的是"一个目标"而非"一次函数调用"。跨组织、跨技术栈、对方会自主决策——这是 A2A 的主场。
什么时候一个函数调用就够(别为 A2A 而 A2A):
- 那段逻辑就在你自己的代码库里,直接
import调函数最省事。 - 它本质是一个工具/一次确定的调用,没有"自主决策"成分——那是 MCP 或普通函数的活。
- 调用方和被调方永远一起部署、一起发版——加一层协议只是徒增复杂度和网络往返。
一句话:A2A 是为"跨边界、对方是个会自己拿主意的 Agent"准备的;同进程里的确定逻辑,老老实实函数调用。
避坑:故障排查表
| 现象 | 原因 | 怎么破 |
|---|---|---|
| 拉 card 404 | card 路径/已知 URL 用错了 | card 的标准存放路径以官方规范为准,核对后再拉 |
| 委派 401/403 | 鉴权方案没对上 card 声明 | 先读 card 的 authentication,按它要求带 token |
| 发过去对方不认参数 | 没按 card 的输入模式/技能 schema 来 | 委派前查 defaultInputModes 和对应 skill 定义 |
| 字段名对不上官方 | 照搬本文示例字段当真 | 本文字段为说明性,对接前以官方最新规范核对 |
| 明明自己能干却绕一圈 A2A | 把同进程逻辑也协议化了 | 同部署的确定逻辑直接函数调用,别上 A2A |
| 拿工具调用当 Agent 委派 | 混淆了 MCP 和 A2A | 被动工具用 MCP,活的 Agent 才用 A2A |
动手挑战
- 给你自己的 Agent 写一张 agent card(JSON),把它会的两三件事写成
skills,鉴权、输入输出格式都填上。给一个同事看,他能不能光读这张 card 就知道怎么调你? - 实现本节的
is_compatible:传入一张 card 和你要的 skill,正确判断兼容与否。故意传一个对方没有的 skill,确认它返回 False 且不发请求。 - 进阶:查阅官方规范,把骨架里标注"以官方为准"的端点路径和负载结构改成真实的,对着一个示例 Agent 跑通一次真正的发现→委派。
小结 · 你现在掌握了什么
- 你明白了 A2A 要解决的是跨 Agent、跨框架、跨团队的协作——让别人家的 Agent 也能搭上话。
- 你看懂了 agent card:一份放在已知 URL 的 JSON 名片,声明身份/能力/触达/鉴权/数据格式。
- 你能跑通"发现→查兼容→委派"三步,并且懂得对不上时不硬发。
- 你彻底分清了 MCP(给 Agent 配工具) 和 A2A(让 Agent 之间协作)。
- 你拿到了"跨边界且对方会自主决策才用 A2A,同进程确定逻辑就函数调用"的判断。
记住:card 的意义是"先看名片再动手",把跨 Agent 对接从读文档降级成解析 JSON。 至于精确字段和端点,永远以官方规范为准。
下一步:Agent 之间能协作了,下一节我们把 Agent 接到用户真正在用的渠道——微信、飞书、Slack。看 多渠道接入;想看全貌就对照三支柱路线图。基础没打牢先补 AI Agent 是什么。
👉 看看 AI 数字员工落地指南,或了解 数字员工搭建实战课。需要为企业落地方案,欢迎找我们聊 企业服务。