A2A协议实战:两个Agent怎么聊
摘要 :上篇写了 MCP 让 Agent 调工具,这篇写 A2A(Agent-to-Agent)让 Agent 调另一个 Agent。MCP 是 AI 世界的 REST API,A2A 是 AI 世界的微服务间通信。核心机制是一张"名片"------每个 Agent 对外暴露
/.well-known/agent.json,写上自己能干什么、怎么调。调用方先看名片再交互,不硬编码 API 路径。
上篇用 MCP 写了一个天气查询工具。Claude 想知道北京天气,调 get_weather,拿到结果,回答用户。整条链路是 Agent → 工具,单向调用。
但真实场景往往不是这样。你有一个 Agent 负责查天气,另一个 Agent 负责做决策------比如"今天适不适合打球"。查天气的 Agent 不管业务逻辑,做决策的 Agent 不管天气数据。它们需要互相通信。
问题来了:Agent 之间怎么聊?
A2A 是什么
Google 在 2025 年 4 月发布了 A2A(Agent-to-Agent)协议,目标很明确------建立一张"智能体互联网"(Internet of Agents),让不同厂商、不同框架开发的 Agent 能互相通信(来源:Google DeepMind 官方博客,2025年4月)。
用后端的话说,MCP 解决的是 Agent ↔ 工具 的通信,A2A 解决的是 Agent ↔ Agent 的通信。两个协议的定位互补:
scss
MCP A2A
Agent ──────► 工具 Agent ──────► Agent
(单向调用) (双向协作)
A2A 的核心机制非常简单:每张 Agent 对外暴露一张"名片"。
这个名字叫 Agent Card,路径固定为 /.well-known/agent.json。任何 Agent 想知道另一个 Agent 能干什么、怎么调,第一步就是去读这张名片。名片里写了什么?名字、能力描述、调用端点、输入格式、鉴权方式------看完这张名片,你就知道对方是干什么的、怎么跟它交互。
这跟你用了十年的微服务注册中心一个道理。Nacos 里每个服务注册自己的名字、IP、端口、健康状态,其他服务从注册中心拉列表,动态发现、动态调用。A2A 的名片机制就是 Agent 世界的服务发现------只是它不需要一个集中的注册中心,每张名片挂在自己服务下面就行。
架构长这样:
typescript
┌────────────────────────────┐
│ Agent Card (名片) │
│ /.well-known/agent.json │
│─────────────────────────── │
│ name: "weather-agent" │
│ description: "查天气" │
│ url: /api/tasks/weather │
│ input_schema: { │
│ city: string │
│ } │
│ auth: bearer_token │
└────────────┬──────────────┘
│
│ ① 先看名片:你是谁?怎么调?
┌──────────────────────────┼──────────────────────────┐
│ ▼ │
┌──────┴──────┐ ┌───────────────┐ ┌───────┴──────┐
│ BasketBall │ ② 提交 │ A2A Protocol │ ③ 处理 │ Weather │
│ Agent │ ───────► │ (任务式异步) │ ───────► │ Agent │
│ │ │ │ │ │
│ "能不能 │ ◄─────── │ JSON-RPC │ ◄─────── │ "晴天,26°C" │
│ 打球?" │ ④ 返回 │ │ 结果 │ │
└─────────────┘ └───────────────┘ └──────────────┘
重点看流程:BasketBall Agent 不硬编码 Weather Agent 的 API 地址。它只做一件事------先发一个 GET 请求到 /.well-known/agent.json,读出 Weather Agent 的调用端点和参数格式,然后再提交任务。Weather Agent 随便改路由、改端口,只要名片同步更新,BasketBall Agent 下次读名片就能自动适配。
不硬编码路径,靠名片动态发现------这就是 A2A 跟传统 API 调用的本质区别。
这个 /.well-known/ 路径前缀跟 Web 的 robots.txt 一样,是协议的硬约定。我第一次做的时候想换成 /api/agent-card.json,觉得路径爱叫什么叫什么。结果客户端 SDK 只认 /.well-known/,直接报 discovery failed。
两个 Agent 怎么聊:天气 + 打球
说设计,不贴完整代码。用上面的 Weather Agent + BasketBall Agent 把整条链路走通。
Weather Agent:暴露能力
Weather Agent 只干一件事:接收城市名,返回天气。它对外暴露两样东西------名片和任务端点。
名片 /.well-known/agent.json 长这样:
json
{
"name": "weather-agent",
"description": "查询指定城市的实时天气,返回温度、天气状况、风速",
"url": "http://localhost:8081",
"capabilities": {
"streaming": false
},
"skills": [
{
"id": "check_weather",
"name": "查天气",
"description": "输入城市名,返回该城市当前的温度、天气状况和风速",
"input_schema": {
"type": "object",
"properties": {
"city": {
"type": "string",
"description": "城市名称,如 北京、上海"
}
},
"required": ["city"]
}
}
]
}
input_schema 是关键。它是标准的 JSON Schema 格式,LLM 读到这个 Schema 之后能自动理解参数格式。但 Schema 写错了 LLM 不会告诉你------它直接给你一个它"理解"的参数。有一次我 Schema 里声明 {"type": "string"},代码里却传了嵌套 object,LLM 按 string 理解去构造请求,参数对不上,查了半天。另外 required 数组别漏------你写了三个参数但 required 只列两个,LLM 就当第三个是可选的,直接不传。
任务端点 /api/tasks/weather 负责接收请求、处理查询、返回结果。A2A 协议用任务式异步模型------调用方提交一个 task,服务方处理完返回结果。本质上跟你写的异步 REST 接口差不多,只是协议层面统一了 task 的状态机(submitted → processing → completed/failed)。
BasketBall Agent:读名片、做决策
BasketBall Agent 的逻辑分三步:
第一步,读名片。 它知道 Weather Agent 的地址,但不知道具体端点。所以先发一个 GET 请求到 http://weather-agent:8081/.well-known/agent.json,拿到上面的 JSON。读完它知道了三件事:这个 Agent 能查天气、调用端点是 /api/tasks/weather、需要的参数是 {"city": "string"}。
第二步,提交任务。 根据名片里的 input_schema,BasketBall Agent 构造请求:
json
{
"task": "check_weather",
"params": {
"city": "北京"
}
}
POST 到 http://weather-agent:8081/api/tasks/weather。Weather Agent 处理完返回 {"city": "北京", "temperature": 26, "condition": "晴", "wind_speed": 3}。
第三步,做决策。 BasketBall Agent 拿到天气数据,跑自己的逻辑:温度 26°C,晴天,风速 3 级------"能打球"。如果是暴雨,风速 10 级,它就返回"今天取消,改室内"。
整条链路不复杂,但关键点只有一个:BasketBall Agent 从头到尾没有硬编码 Weather Agent 的 API 路径。所有路由信息都来自那张名片。Weather Agent 升级改版把端点从 /api/tasks/weather 改成 /v2/tasks/weather-check,只要名片同步更新,BasketBall Agent 不用重新部署。
为什么不用 MCP
有人会问:MCP 不也能让 Agent 调工具吗?BasketBall Agent 直接用 MCP 调天气工具不就行了?
区别在粒度。MCP 是 Agent 调工具 ------查天气是一个工具函数,Agent 在推理链上决定要不要调它。A2A 是 Agent 调另一个 Agent------Weather Agent 是一个独立服务,有自己的推理能力、自己的状态管理、自己独立的生命周期。它可能不只是返回天气数据,还可能自己调用其他工具、做数据清洗、甚至根据天气推荐穿衣建议------这些是 BasketBall Agent 不需要关心的。
简单说:MCP 暴露的是函数,A2A 暴露的是能力。函数是无状态的工具调用,Agent 是有状态的协作方。
MCP vs A2A
| 维度 | MCP | A2A |
|---|---|---|
| 谁调谁 | Client/Claude → 工具服务 | Agent → Agent |
| 通信方向 | 单向(Agent 调工具,工具返回结果) | 双向(两个 Agent 可以互相调用) |
| 核心机制 | @tool 装饰器注册函数 |
Agent Card 名片动态发现 |
| 后端类比 | REST API(客户端调服务端) | 微服务间 RPC(服务调服务) |
| 触发方式 | LLM 推理链中决定调用 | Agent 业务逻辑中主动调用 |
| 状态管理 | 工具自身无状态 | Agent 可以维护对话状态和上下文 |
| 协议提出方 | Anthropic(2024年11月) | Google DeepMind(2025年4月) |
| 适用场景 | Agent 需要外部工具(数据库、API、搜索) | 多 Agent 协作(编排、委派、链式调用) |
一句话总结:Agent 接了 MCP 拿到工具,再通过 A2A 把它查到的结果传给另一个 Agent。 两个协议不是替代关系,是组合关系。
A2A 和 MCP 不是竞争对手。MCP 让一个 Agent 有了手------能调工具、拿数据。A2A 让多个 Agent 有了嘴------能把拿到的东西告诉彼此。手和嘴都有了,多 Agent 协作才不是 PPT 上的概念。
作者:唐悦玮 | 公众号同名
从后端出发,用 AI 拓展到全栈的工程师。