MCP 解决的是"Agent 调工具",A2A 解决的是"Agent 找另一个 Agent 干活"。两者的边界经常被混为一谈:把另一个 Agent 包成一个 MCP 工具也能跑,但那样你就丢掉了任务生命周期、长时任务的进度回传、以及"这个 Agent 能干什么"的机器可读声明。
下面按一次真实交互的顺序走:发现 → 声明 → 建任务 → 推进状态 → 取产物。
1. 场景:为什么不能只靠工具调用
| 对比项 | MCP(Agent ↔ 工具) | A2A(Agent ↔ Agent) |
|---|---|---|
| 对端是什么 | 无状态函数集合 | 有自己计划的自主实体 |
| 调用时长 | 秒级 | 分钟到小时,可能中途要问人 |
| 返回什么 | 一次结果 | 一个可持续查询的 Task |
| 能力描述 | inputSchema |
Agent Card(技能、输入输出模态、鉴权方式) |
| 谁做决策 | 调用方 | 被调用方自己做 |
关键差异是控制权:工具不会中途反问,Agent 会。所以 A2A 的核心不是"请求-响应",是"任务状态机"。
2. 核心对象
| 对象 | 作用 | 关键字段 |
|---|---|---|
| Agent Card | 机器可读的能力声明 | name、description、skills[]、capabilities、securitySchemes |
| Task | 一次委派的工作单元 | id、status.state、artifacts[]、history[] |
| Message | 任务内的一条消息 | role(user/agent)、parts[] |
| Part | 消息或产物的内容块 | text / file / data |
| Artifact | 任务产出物 | name、parts[] |
parts[] 是 A2A 里最重要的设计:一条消息不是纯文本,而是若干内容块拼起来的,可以同时带一句说明、一个 PDF、一段结构化 JSON。这比"只传字符串"的表达力高一个量级。
3. 发现:一个固定路径的 JSON
{
"name": "report-agent",
"description": "把原始数据整理成可发布的周报",
"url": "https://agents.example.com/report",
"version": "1.2.0",
"capabilities": {"streaming": true, "pushNotifications": true},
"defaultInputModes": ["text/plain", "application/json"],
"defaultOutputModes": ["text/markdown", "application/pdf"],
"skills": [{
"id": "weekly-report",
"name": "周报生成",
"description": "输入指标 JSON,输出 Markdown 周报并附图表",
"tags": ["report", "markdown"],
"examples": ["把上周的 DAU 和留存整理成周报"]
}],
"securitySchemes": {"bearer": {"type": "http", "scheme": "bearer"}}
}
约定位置是 https://<host>/.well-known/agent-card.json(早期草案里叫 agent.json,读到 404 时两个都试)。发现机制是 A2A 最实用的部分:调用方不用读文档,拉一次卡片就知道对方能干什么、要什么鉴权、支不支持流式。
4. 建任务:message/send
{"jsonrpc":"2.0","id":1,"method":"message/send","params":{
"message":{"role":"user","parts":[{"kind":"text","text":"生成 2025-W44 周报"}],
"messageId":"m-001"},
"configuration":{"acceptedOutputModes":["text/markdown"]}}}
典型响应(示意,非实测输出):
{"jsonrpc":"2.0","id":1,"result":{
"id":"task-7f21",
"status":{"state":"working","timestamp":"2025-11-03T09:00:00Z"},
"artifacts":[],
"history":[{"role":"user","parts":[{"kind":"text","text":"生成 2025-W44 周报"}]}]}}
返回的是 Task 而不是结果。这是 A2A 和普通 HTTP API 最大的手感差异:你拿到的是一个 id,之后靠 tasks/get 轮询或靠流式订阅。
| 方法 | 用途 | 备注 |
|---|---|---|
message/send |
建任务或追加消息 | 同步等结果时它也可能直接返回 completed |
message/stream |
同上,但用 SSE 推事件 | 需要卡片里 streaming: true |
tasks/get |
查任务状态与产物 | 长任务的主要读取方式 |
tasks/cancel |
请求取消 | 是"请求",对端可以拒绝 |
tasks/pushNotificationConfig/set |
注册回调地址 | 需要 pushNotifications: true |
5. 状态机
| 状态 | 含义 | 谁会走到这里 |
|---|---|---|
submitted |
已接收,未开始 | 排队中 |
working |
处理中 | 最常见 |
input-required |
需要调用方补充信息 | 被调用 Agent 反问 |
completed |
完成,产物在 artifacts |
终态 |
failed |
失败,status.message 带原因 |
终态 |
canceled |
已取消 | 终态 |
rejected |
拒绝接单 | 终态 |
auth-required |
需要额外授权 | 半终态,补凭证后可继续 |
unknown |
对端自己也说不清 | 兜底 |
input-required 是整套协议的价值所在:它把"多轮澄清"变成了协议内的一等状态,而不是让调用方去猜"为什么对面不返回"。调用方看到它,应把 status.message 转给人或自己的模型,然后用 message/send 带同一个 taskId 追加消息。
流式下,这些状态变化以事件形式推过来:
event: TaskStatusUpdateEvent
data: {"id":"task-7f21","status":{"state":"working"},"final":false}
event: TaskArtifactUpdateEvent
data: {"id":"task-7f21","artifact":{"name":"weekly.md","parts":[{"kind":"text","text":"# 2025-W44..."}]},"append":true,"lastChunk":false}
event: TaskStatusUpdateEvent
data: {"id":"task-7f21","status":{"state":"completed"},"final":true}
append: true 表示这块内容要接在前一块后面,lastChunk: true 表示该产物传完了。大产物分块传是 A2A 相对普通 JSON 响应的另一个实用点。
6. 推送:长任务不该靠轮询
{"jsonrpc":"2.0","id":2,"method":"tasks/pushNotificationConfig/set","params":{
"taskId":"task-7f21",
"pushNotificationConfig":{
"url":"https://caller.example.com/a2a/callback",
"token":"callback-verify-token",
"authentication":{"schemes":["bearer"]}}}}
| 通道 | 适合 | 成本 |
|---|---|---|
轮询 tasks/get |
任务短、调用方无公网地址 | 简单,但延迟与请求量挂钩 |
| SSE 流 | 交互式、需要过程反馈 | 连接要保活,反代有超时 |
| Webhook 推送 | 分钟级以上的长任务 | 调用方必须暴露可回调地址 |
三者可以叠加:先建任务、拿 id,再按任务时长选轮询或回调。回调地址的 token 必须校验,否则任何人都能伪造"任务完成"通知打你的接口。
7. 与 MCP 的分工
| 你要做的事 | 该用 |
|---|---|
| 查数据库、跑命令、读文件 | MCP |
| 让另一个团队/厂商的 Agent 完成一段有自己判断的工作 | A2A |
| 自己的多个内部 Agent 协作 | 先看编排框架,未必需要协议 |
| 对外暴露能力给不特定调用方 | A2A(Agent Card + 标准方法) |
一句话:MCP 向下(资源与工具),A2A 向上(协作与委派)。 一个 A2A Agent 内部完全可以再用 MCP 挂一堆工具,层次不冲突。
8. 鉴权与企业网关
| 层次 | 做法 | 解决什么 |
|---|---|---|
| 传输 | HTTPS + mTLS(东西向) | 链路身份 |
| Agent 级 | Bearer / OAuth 2.0 | 谁在调用 |
| 任务级 | 任务所属租户 + 可见性 | 谁能看这个 Task |
| 网关 | 统一入口、限流、审计日志 | 横向治理 |
企业里常见的形态是"所有 Agent 都挂在同一个网关上",Agent Card 由网关代发,真实地址在内网。这样调用方只需要信任一个域名,被调用方的地址与扩缩容对调用方透明。
9. 排错
| 现象 | 原因 | 处理 |
|---|---|---|
拉 .well-known 404 |
路径写成了 agent.json 或未暴露 |
两个路径都提供,注意 Content-Type |
任务永远 submitted |
对端没有 worker 消费队列 | 用 tasks/get 看 status.timestamp 是否更新 |
调 message/send 一直不返回 |
对端把长任务做成同步 | 改用 message/stream 或先建任务后轮询 |
input-required 后调用方又建了新任务 |
没带 taskId |
追加消息必须带同一个 task id |
| 流式中途断 | 反代读超时 | 心跳 + 客户端重连后 tasks/get 补齐 |
| 回调收到伪造请求 | 没校验 token |
校验 token + 来源 IP + 幂等键 |
10. 边界:现实约束
| 现实 | 应对 |
|---|---|
| 规范仍在演进,字段会变 | 客户端按"缺字段就用默认值"写,别硬校验 |
| 跨厂商互通尚未普及 | 先在单厂商内部把协议跑通,再谈跨域 |
| 双方对"任务完成"定义不同 | 在 Agent Card 的 skills.description 里写清验收标准 |
| 长任务的对账困难 | 自己维护 taskId → 内部单号 的映射表 |
| 权限边界模糊 | 委派时只给最小权限凭证,别传长期 token |
落地顺序建议:先用自己的两个 Agent 走通 Agent Card → message/send → input-required → artifacts 这条链,再考虑接第三方。 一上来就做跨厂商互通,通常卡在鉴权和语义对齐上,而不是协议本身。
11. 自测
- A2A 与 MCP 的分界线用一句话怎么划?
- Agent Card 放在哪个路径,它至少要让调用方知道哪三件事?
input-required状态出现后,调用方正确的下一步动作是什么?message/send返回的是结果还是任务?这个设计为什么必要?- 三种推进通道各适合多长的任务?
- 为什么说回调地址的
token必须校验?