「JS全栈AI Agent学习」八、A2A 协议完全指南:理解 Agent 协作体系

2 阅读17分钟

📌 系列简介:「JS全栈AI Agent学习」系统学习 21 个 Agent 设计模式,篇数随学习进度持续更新。

📖 原书地址adp.xindoo.xyz

前端转 JS 全栈,正在学 AI,理解难免有偏差,欢迎批评指正 ~


🗺️ 系列导航

主题
第一篇提示链 · 路由 · 并行化
第二篇反思 · 工具使用 · 规划
第三篇多智能体 · 记忆管理 · 学习适应
第四篇MCP:给AI工具世界造一个USB接口
第五篇目标设定与监控 · 异常处理与恢复
第六篇Human-in-the-Loop 设计
第七篇深入理解 RAG(检索增强生成)技术
本篇A2A 协议完全指南:理解 Agent 协作体系

写在前面

学到多 Agent 系统这里,有一个问题一直没想清楚:

多个 Agent 之间怎么协作?

不是"调用"——调用是单向的,一个 Agent 叫另一个干活。 而是"协作"——两个 Agent 各有分工,互相传递信息,共同完成一件事。

这中间需要一套协议,就像人与人之间需要语言一样。

A2A(Agent to Agent)协议,就是为了解决这个问题而生的。

这篇是学习笔记,记录了从零推导 A2A 协议的完整过程。 不只是"是什么",更重要的是"为什么这样设计"。


目录

  1. A2A 是什么,解决什么问题
  2. Agent Card:Agent 的自我介绍
  3. 通信机制:异步、流式与任务派发
  4. 任务生命周期
  5. 调度中心设计:优先级与上下文切换
  6. 任务去重与缓存
  7. 事件驱动:Plan A/B/C 动态切换
  8. 语义鸿沟:误差是如何累积的
  9. A2A vs MCP:两个协议的分工
  10. 安全与鉴权:Agent 凭什么信任对方
  11. 环形依赖死锁:僵局如何打破
  12. Human-in-the-loop:什么时候该让人介入
  13. 系统哲学:接受误差,设计反馈

1. A2A 是什么,解决什么问题

从一个场景开始

想象你要规划一次旅行,你告诉一个 AI 助手:"帮我安排下周去三亚的行程。"

这件事,一个 Agent 做不完:

  • 要查机票 → 机票 Agent
  • 要订酒店 → 酒店 Agent
  • 要查天气 → 天气 Agent
  • 要整合行程 → 行程规划 Agent

这些 Agent 之间需要协作,需要传递信息,需要知道彼此的能力边界。

A2A(Agent to Agent)协议,就是为了解决这个问题而生的:

让不同的 Agent 能够互相发现、互相理解、互相协作。

A2A 解决的核心问题

问题A2A 的解法
Agent 之间怎么知道对方能做什么?Agent Card 自我描述
Agent 之间怎么传递任务?标准化任务协议
任务执行中途怎么同步状态?流式响应 + 事件机制
怎么保证安全,防止伪造?OAuth 2.0 双向鉴权

2. Agent Card:Agent 的自我介绍

你怎么知道一家店能提供什么服务?

你走进一家餐厅,门口有菜单,上面写着:

  • 我们提供:川菜、粤菜
  • 营业时间:11:00 - 22:00
  • 支持:堂食、外卖、预约

Agent Card 就是 Agent 的菜单。

没有这张菜单,Agent 再强大也是孤岛——能力藏在里面,没人知道,也没人能调用。

Agent Card 的标准位置

每个 Agent 都在固定路径暴露自己的 Card:

https://your-agent.com/.well-known/agent.json

这个路径是约定俗成的,就像每个网站的 robots.txt 一样,调用方知道去哪里找。

Agent Card 的内容

{
  "name": "机票查询 Agent",
  "description": "负责查询国内外航班信息",
  "version": "1.0.0",
  "capabilities": {
    "streaming": true,
    "pushNotifications": true
  },
  "skills": [
    {
      "id": "search_flight",
      "name": "查询航班",
      "inputModes": ["text"],
      "outputModes": ["text", "structured_data"]
    }
  ],
  "authentication": {
    "type": "oauth2",
    "authorizationUrl": "https://auth.example.com/oauth/authorize"
  }
}

3. 通信机制:异步、流式与任务派发

为什么不能用同步请求?

普通的 HTTP 请求是同步的:

你问 → 等待 → 收到答案

但 Agent 的任务往往很长:查航班可能要 30 秒,生成报告可能要 5 分钟。

如果同步等待,连接会超时,用户体验会很差。

这个问题在做简历押题系统的时候就踩过——AI 生成要好几分钟,同步等待根本不现实,后来改成 SSE 流式推送才解决。

A2A 的解法:异步 + 流式

你发任务 → 立刻收到 task_id(不用等结果)
              ↓
         Agent 在后台执行
              ↓
         执行过程中,持续推送进度
              ↓
         完成后,推送最终结果

这就是流式响应(Streaming):不是等全部做完再给你,而是做一点给你一点。

就像你在餐厅点了一桌菜,厨师不会等所有菜都做完再上,而是做好一道上一道。

任务派发的标准结构

{
  "id": "task_20240401_001",
  "skill": "search_flight",
  "input": {
    "from": "北京",
    "to": "三亚",
    "date": "2024-04-08"
  },
  "callback": "https://scheduler.example.com/callback",
  "timeout": 60
}

4. 任务生命周期

一个任务的完整旅程

submitted(已提交)
    ↓
working(执行中)
    ↓
    ├── input-required(需要更多信息)→ 等待输入 → 回到 working
    ├── paused(暂停)→ 等待恢复 → 回到 working
    └── completed(完成)
         或 failed(失败)
         或 canceled(取消)

每个状态的含义

状态含义下一步
submitted任务已收到,排队中→ working
working正在执行→ 各种结果状态
input-requiredAgent 需要更多信息才能继续→ 等人回答 → working
paused主动暂停,等待外部条件→ 条件满足 → working
completed成功完成终态
failed执行失败终态(可重试)
canceled被取消终态

为什么需要 input-required 这个状态?

因为 Agent 不是万能的。有时候任务进行到一半,发现信息不够:

"你让我查机票,但你没说几号回程,我查不了往返票。"

这时候 Agent 不应该瞎猜,也不应该直接失败,而是主动暂停,等待补充信息

知道自己不知道,比假装知道更重要。这是一种负责任的设计。


5. 调度中心设计:优先级与上下文切换

调度中心是什么?

在多 Agent 系统里,通常有一个调度中心(Orchestrator),负责:

  • 接收用户任务
  • 拆解成子任务
  • 分发给各个 Agent
  • 汇总结果

就像一个项目经理,自己不写代码,但负责协调所有人。

优先级抢占

当多个任务同时存在,调度中心需要决定谁先执行:

任务队列:
  [低优先级] 生成月报          等待中...
  [中优先级] 查询本周数据       执行中...
  [高优先级] 紧急:客户投诉处理  ← 新来的!

处理:
  暂停"查询本周数据"
  保存它的执行状态(快照)
  先处理"客户投诉"
  投诉处理完,恢复"查询本周数据"

上下文快照(Context Snapshot)

暂停一个任务,不是直接丢掉,而是保存现场

{
  "task_id": "task_weekly_data",
  "status": "paused",
  "snapshot": {
    "progress": "已查询3个部门,还剩2个",
    "partial_results": { "部门A": 120, "部门B": 98, "部门C": 156 },
    "next_step": "查询部门D"
  }
}

恢复的时候,从快照继续,不用从头开始。

这个设计和操作系统的进程调度本质上是同一件事:保存现场,切换,恢复现场。 底层的逻辑,跨越了层次,是相通的。


6. 任务去重与缓存

问题:重复任务怎么办?

在分布式系统里,同一个任务可能被发送多次:

  • 网络超时,客户端重试
  • 用户手抖,点了两次
  • 系统故障,任务重放

如果每次都重新执行,会造成:

  • 资源浪费
  • 结果不一致
  • 甚至重复付款、重复发邮件等严重问题

解法一:Singleton Task(单例任务)

同一个任务 ID,只执行一次。
第二次收到相同 ID,直接返回第一次的结果。
// 第一次请求
{ "id": "task_flight_001", "skill": "search_flight", ... }
→ 开始执行

// 第二次请求(相同 ID)
{ "id": "task_flight_001", "skill": "search_flight", ... }
→ 检测到已存在,直接返回第一次的结果

解法二:Result Cache(结果缓存)

对于相同输入的任务,缓存结果:

查询:北京 → 三亚,2024-04-08
  第一次:调用航班 API,耗时 3 秒,缓存结果,有效期 10 分钟
  第二次:直接返回缓存,耗时 0.01 秒
  第三次:直接返回缓存,耗时 0.01 秒
  10 分钟后:缓存过期,重新查询

幂等性:设计的基本原则

幂等性:同一个操作,执行一次和执行多次,结果相同。

这是分布式系统设计的黄金原则。

前端开发里也有这个概念——同一个按钮,点一次和点十次,结果应该一样。 从前端到后端到多 Agent 系统,这个原则从来没变过。


7. 事件驱动:Plan A/B/C 动态切换

静态计划的局限

传统的任务执行是线性的:

步骤1 → 步骤2 → 步骤3 → 完成

但现实世界是动态的,计划赶不上变化。

事件驱动的思路

不是"按步骤执行",而是"根据事件响应":

初始计划(Plan A):
  查机票 → 订酒店 → 安排接送

事件触发:
  机票查询结果 → 目标日期无票!

切换 Plan B:
  改签日期 → 重新查机票 → 继续订酒店

事件触发:
  改签日期也无票!

切换 Plan C:
  改变出发城市 → 查其他机场 → 重新规划

条件触发的实现

{
  "task_id": "travel_plan",
  "plan": "A",
  "on_event": {
    "flight_not_found": {
      "action": "switch_plan",
      "target": "B",
      "params": { "adjust": "date", "range": "±3days" }
    },
    "all_dates_full": {
      "action": "switch_plan",
      "target": "C",
      "params": { "adjust": "departure_city" }
    }
  }
}

系统的智慧,在于知道什么时候该变——不是死守计划,而是在事件触发时做出正确的响应。


8. 语义鸿沟:误差是如何累积的

一个看似简单的任务

"帮我安排一个舒适的旅行。"

这句话里,有多少个不确定性?

"舒适"   → 你的舒适 ≠ 我理解的舒适
             商务舱?还是经济舱靠窗?
             五星酒店?还是精品民宿?

"旅行"   → 几天?几个人?预算多少?
             国内还是国外?

"安排"   → 只查信息?还是直接预订?
             需要我做决定吗?

每一个词,都携带着不确定性。

误差累积的数学

假设每一步理解的准确率是 90%:

1个Agent理解:  90%
2个Agent串联:  90% × 90% = 81%
3个Agent串联:  90% × 90% × 90% = 72.9%
4个Agent串联:  90% × 90% × 90% × 90% = 65.6%

每多一层转发,误差就多累积一次。 这就是语义鸿沟——不是一次大的误解,而是无数次小偏差的叠加。

看到 65.6% 这个数字的时候,停了一下。 四个 Agent 串联,准确率已经不到七成——这不是理论上的担忧,是真实会发生的事。

解法:在每一层做语义对齐

不好的设计:
  用户 → Agent A → Agent B → Agent C → 执行
  (每层只传递结果,不传递意图)

好的设计:
  用户 → Agent A → Agent B → Agent C → 执行
  (每层都携带原始意图 + 当前理解 + 置信度)
{
  "original_intent": "安排一个舒适的旅行",
  "current_interpretation": "预算15000元,3天2夜,商务出行风格",
  "confidence": 0.78,
  "assumptions": [
    "舒适 = 商务级别",
    "旅行 = 国内短途",
    "安排 = 提供方案,不直接预订"
  ]
}

当置信度低于阈值,自动触发 Human-in-the-loop 确认。


9. A2A vs MCP:两个协议的分工

一个场景,两个协议

天气 Agent 需要做两件事:

  • 调用天气 API 查询数据
  • 把结果告诉旅行规划 Agent

这两件事,是不同协议负责的。

分工边界

旅行规划 Agent
      │
      │  "帮我查三亚明天的天气"
      │  ────── A2A ──────────────→
      │
天气 Agent 收到任务
      │
      │  调用天气API工具
      │  ────── MCP ──────────────→  天气 API
      │                                  │
      │  ←───── MCP ────────────────  返回数据
      │
      │  "明天晴,28℃,适合出行"
      │  ←───── A2A ───────────────
      │
旅行规划 Agent 收到结果
协议全称管的是关系方向
A2AAgent to AgentAgent ↔ Agent 通信对等协作双向
MCPModel Context ProtocolAgent → 工具调用调用使用单向

一句话记住区别

MCP:Agent 的"手"  — 伸出去拿东西用的
A2A:Agent 的"嘴"  — 和其他 Agent 说话协作的

MCP 是 Anthropic 提出的,专门负责 LLM/Agent 如何调用外部工具。 A2A 是 Google 提出的,专门负责 Agent 之间的协作通信。 两个协议不是竞争关系,而是互补关系。

一开始觉得两个协议有点重叠,后来想清楚了:调用工具和同伴协作,本来就是两件不同的事,当然要分开设计。各司其职,才能各得其位。


10. 安全与鉴权:Agent 凭什么信任对方

问题:没有鉴权会怎样?

有一个"转账 Agent"
任何人发一条消息:
"帮我把账户里的钱全转走"
它就执行了……

这显然不行。

A2A 的鉴权机制:OAuth 2.0 + JWT

Step 1:Agent Card 声明鉴权方式
  "我支持 OAuth 2.0,
   你要调用我,先去这个地址拿 Token"

Step 2:调用方获取 Token
  调度中心 → Auth Server → 拿到 JWT

  JWT 里包含:
    who:   我是调度中心
    what:  我有权调用机票Agent的查询接口
    when:  这个Token 1小时后过期

Step 3:带 Token 发请求
  Header: Authorization: Bearer eyJhbGci...

Step 4:被调用方验证
  ✅ Token 有效 + 有权限 → 执行任务
  ❌ Token 过期           → 拒绝,要求重新授权
  ❌ Token 伪造           → 拒绝,告警

A2A 特有的威胁:Agent 身份伪造

普通网站鉴权是人 → 系统,A2A 鉴权是Agent → Agent

这带来一个新威胁:

恶意 Agent 伪装成"调度中心",发指令给其他 Agent,执行恶意任务。

解法是双向验证

普通鉴权(单向):调用方证明自己是谁
A2A 鉴权(双向):调用方证明自己是谁 + 验证对方是谁

就像你打电话给银行:

  • 银行验证你是不是本人 ✅
  • 你也验证接电话的是不是真银行 ✅
  • 防钓鱼,防伪装

11. 环形依赖死锁:僵局如何打破

死锁场景

机票 Agent A:
  "我要等酒店 Agent B 确认目的地,才能查航班"
  等待中...

酒店 Agent B:
  "我要等机票 Agent A 确认出发城市,才能查酒店"
  等待中...

结果:
  ABBAABBA → ∞

这就像两个吵架的情侣,心里都想着"如果他先开口,我就原谅他",结果谁都没开口,僵局永远持续。

解法一:TTL 超时机制

task = {
    "id": "task_001",
    "timeout": 30,         # 最多等 30 秒
    "max_retries": 3,      # 最多重试 3 次
    "on_timeout": "cancel" # 超时就取消,主动上报
}

关键:超时后必须有明确的退出状态。 不是"继续等",不是"静默失败", 而是主动上报,让调度中心知道,重新规划。

知道在哪里停下来,才能稳定——这是打破僵局的前提。

解法二:依赖图检测(死锁预防)

在任务派发之前,先画出依赖关系:

A → 依赖 → B
B → 依赖 → A   ← ⚠️ 检测到环!

调度中心发现成环,直接拒绝派发。

就像出门前先看地图,发现是死胡同就不走,而不是走进去再掉头。

事前预防 > 事后处理。

解法三:外部介入

AB 超过阈值
    ↓
系统自动升级:发出 Human-in-the-loop 信号
    ↓
人工介入:
  "A 你先用上海作为默认出发城市"
  "B 你先用三亚作为默认目的地"
    ↓
死锁打破,任务继续

12. Human-in-the-loop:什么时候该让人介入

四种必须让人介入的情况

情况一:权限/能力边界

Agent 遇到了自己无权或无法处理的事,继续走下去 = 越权或偏离预期。

例:查文档时发现需要更高权限
例:直接查不到,需要用另一种方式,但这种方式可能偏离原始目标
→ 暂停,申请授权或确认方向

情况二:死锁/僵局

系统自己解不开,必须引入外部条件。

(见上一章节)

情况三:高风险不可逆操作

Agent 准备执行:
  - 删除数据
  - 转账付款
  - 发送邮件给 100 个客户
  - 部署上线

这些操作做了就很难撤回,错了代价极大。
→ 不管 Agent 多"确定",都必须人工确认。

就像银行大额转账,系统再智能,也会弹出: "您确定要转账 ¥50,000 吗?"

情况四:置信度低于阈值

查航班      → 置信度 99%  → Agent 自己做主
翻译文件    → 置信度 95%  → Agent 自己做主
法律条款解读 → 置信度 60% → ⚠️ 需要人工复核
医疗诊断建议 → 置信度 72% → ⚠️ 必须人工确认

不是"不会"才请示,是"不够确定"就请示。 有时候说"我不确定,建议人工核实",才是负责任的表现。

介入也是成本:找到平衡点

介入太少 → Agent 乱来,出错没人管
介入太多 → 每步都问人,还不如不用 Agent

Human-in-the-loop 的设计核心,不是"什么时候让人介入",而是:

让人在最值得介入的地方介入。

低价值重复的事  →  Agent 全自动
高风险关键节点  →  人来拍板
出了意外的边界  →  人来兜底

13. 系统哲学:接受误差,设计反馈

工程的本质不是消灭误差,而是管理误差

在 A2A 这样的多 Agent 系统里,误差是不可避免的:

  • 语义理解有偏差
  • 工具调用有失败
  • 网络传输有延迟
  • Agent 判断有偏差

追求零误差,是一个危险的幻觉。

真正的工程智慧,是:

接受误差的存在
    ↓
设计反馈回路
    ↓
让系统能够感知误差
    ↓
让系统能够自我修正

反馈回路的设计

执行 → 观察结果 → 与预期对比 → 发现偏差 → 修正 → 再执行
  ↑___________________________________________________|

这个回路越短,系统越健壮。


总结

章节核心一句话
A2A 是什么让 Agent 之间能够发现、理解、协作
Agent CardAgent 的菜单,声明自己能做什么
通信机制异步非阻塞,做一点给一点
任务生命周期每个状态都有意义,终态必须明确
调度中心识时务,懂优先级,会保存现场
去重与缓存幂等性是分布式系统的黄金原则
事件驱动计划赶不上变化,系统要能动态切换
语义鸿沟每层转发都会累积误差,要携带意图
A2A vs MCPA2A 是嘴,MCP 是手,各司其职
安全鉴权双向验证,防伪装,防越权
环形死锁超时退出,预防检测,外部介入
Human-in-the-loop让人在最值得介入的地方介入
系统哲学接受误差,设计反馈,动态中守住不变的原则

写在最后

学完这一章,停下来想了一下。

A2A 协议里有一个细节让我印象很深—— 任务状态里有一个 input-required,不是失败,不是继续,是主动暂停,等待补充

Agent 知道自己不知道,所以停下来问。

这让我想到《大学》里的一句话,"知止而后有定"。 知道在哪里停下来,才能稳定。

系统设计里的超时退出、Human-in-the-loop、置信度阈值—— 本质上都是在回答同一个问题:什么时候该停,什么时候该问人。

不是越自动越好,而是知道自己的边界在哪里。 Agent 如此,人也如此。


昇哥 · 2026年4月 学 AI Agent 系列途中,把想清楚的事写下来