1. 为什么 Agent 通信协议突然“百花齐放”
过去一年里,只要你在做 AI Agent 相关的开发,几乎绕不开一个困惑:同样是让两个智能体互相说话,怎么一会儿是 JSON-RPC,一会儿是 MCP,转头又冒出 A2A、ACP?更让人头大的是,每个协议背后都有一拨人在推,文档写得都挺像那么回事,但真到选型落地的时候,你会发现它们解决的问题压根不在一个层面上。
我自己是从最原始的“手搓 HTTP 接口”开始做 Agent 协作的。最早那会儿,两个 Agent 要交换信息,无非就是约定一个 URL,POST 一段 JSON 过去,对面解析完再返回一段 JSON。简单粗暴,能跑就行。但项目一复杂,问题就来了:工具怎么暴露给模型?上下文怎么在多个 Agent 之间传递?一个 Agent 调用另一个 Agent 的能力时,怎么描述“我能干什么”?这些事靠裸 HTTP 加自定义 JSON 结构,维护成本高得离谱。
于是协议开始分层。JSON-RPC 是底座,它只规定“怎么调用一个远程方法”,不关心你调用的是工具、是模型还是另一个 Agent。MCP(Model Context Protocol)解决的是“模型怎么发现和调用外部工具与数据源”,它把工具、资源、提示词模板标准化,让模型侧不用为每个工具写一套适配。A2A(Agent-to-Agent)则更进一步,解决的是“Agent 之间怎么互相发现、协商、委派任务”,它假设双方都是有一定自主性的智能体,而不是单纯的函数。ACP(Agent Communication Protocol)走的是另一条路,强调消息传递的可靠性和会话状态管理,更像是一个面向 Agent 的消息中间层。
这篇文章不打算给你背协议规范,而是想把这几个协议为什么会长成这样、各自卡在哪个位置、实际选型时怎么权衡讲清楚。如果你正在做多 Agent 系统、工具调用平台,或者只是好奇“我到底该用哪个”,那这篇应该能帮你省下不少翻文档的时间。
2. 从 JSON-RPC 说起:所有 Agent 通信的“最小公约数”
2.1 JSON-RPC 到底规定了什么,没规定什么
JSON-RPC 本质上就是一个极简的远程调用约定。一个请求长这样:
{ "jsonrpc": "2.0", "method": "tools/call", "params": { "name": "search_web", "arguments": { "query": "agent protocol" } }, "id": 1 }响应就是:
{ "jsonrpc": "2.0", "result": { "content": "..." }, "id": 1 }它只干了三件事:方法名、参数、请求 ID。至于 method 叫什么、params 里放什么、result 怎么解析,全靠双方自己约定。这就是它最大的优点,也是最大的坑。
优点在于极度灵活。你可以用它调一个本地函数,也可以调一个远程服务,甚至可以用它传文件流(base64 塞进去就行)。缺点在于,没有语义层。当你的 Agent 要调用一个工具时,它怎么知道有哪些工具可用?每个工具需要什么参数?参数类型是什么?这些 JSON-RPC 一概不管。你得自己再写一套描述文件,或者硬编码在代码里。
我早期做过一个多 Agent 协作的小系统,当时就是用 JSON-RPC 做底层,上面自己定义了一套tool_list、tool_describe、tool_call的方法名。跑是能跑,但每接一个新工具,就要改一次描述逻辑,而且不同 Agent 之间的描述格式还不统一。那段时间我最大的感受就是:JSON-RPC 是很好的传输层,但它不是协议层。
2.2 为什么 Agent 场景下 JSON-RPC 不够用
Agent 和传统微服务最大的区别在于:调用方本身是概率性的。传统微服务里,A 服务调 B 服务,接口是确定的,参数是确定的,返回也是确定的。但 Agent 调用工具时,它是在“理解”工具描述之后,自己决定要不要调、怎么调、传什么参数。这就要求工具描述本身必须是机器可读、语义清晰的。
JSON-RPC 没有能力表达“这个工具是干什么的”“参数有什么约束”“返回结果是什么结构”。你当然可以在 params 里塞一个 JSON Schema,但那只是把问题往后推了一层:谁来生成这个 Schema?谁来保证不同 Agent 对同一个 Schema 的理解一致?
另一个问题是能力发现。在多 Agent 系统里,一个 Agent 需要知道其他 Agent 能做什么。JSON-RPC 没有服务发现机制,你得自己维护一个注册中心,或者硬编码地址列表。这在 Agent 数量少的时候还行,一旦超过十个,维护成本就指数级上升。
所以 JSON-RPC 的定位很明确:它是通信的底座,不是 Agent 协作的完整方案。MCP、A2A、ACP 这些协议,本质上都是在 JSON-RPC(或类似传输层)之上,补上了语义层、发现层和会话层。
3. MCP:让模型“看见”工具的那一层
3.1 MCP 解决的核心问题:工具描述的标准化
MCP 最核心的贡献,是把“工具”这个东西抽象成了模型可以理解的标准结构。一个 MCP Server 暴露出来的工具,长这样:
{ "name": "get_weather", "description": "查询指定城市的当前天气", "inputSchema": { "type": "object", "properties": { "city": { "type": "string", "description": "城市名称" } }, "required": ["city"] } }模型看到这段描述,就知道有个叫get_weather的工具,需要传一个city参数。这看起来简单,但它是整个工具调用链路能跑通的前提。没有这个标准,每个模型厂商都要为每个工具写一套适配,工具开发者也要为每个模型写一套接口。
MCP 还定义了资源(Resources)和提示词模板(Prompts)。资源让模型可以读取外部数据,比如文件、数据库记录;提示词模板让模型可以复用预定义的指令。这三者加起来,构成了模型与外部世界交互的完整接口。
3.2 MCP 的传输层选择与实操细节
MCP 本身不强制传输层,但最常见的实现是stdio和HTTP+SSE。stdio 适合本地工具,比如你写一个 Python 脚本,通过标准输入输出和模型通信。HTTP+SSE 适合远程工具,模型通过 HTTP 发请求,工具通过 SSE 推事件。
我实测下来,stdio 的延迟最低,但调试麻烦,因为你看不到网络包。HTTP+SSE 调试方便,但要注意 SSE 的连接保持和重连逻辑。如果你用 Python 写 MCP Server,官方 SDK 已经帮你处理了大部分细节:
from mcp.server import Server from mcp.server.stdio import stdio_server app = Server("my-tool-server") @app.list_tools() async def list_tools(): return [ { "name": "search", "description": "搜索网页", "inputSchema": { "type": "object", "properties": {"query": {"type": "string"}}, "required": ["query"] } } ] @app.call_tool() async def call_tool(name, arguments): if name == "search": return {"content": [{"type": "text", "text": "搜索结果..."}]}这段代码的关键在于list_tools和call_tool两个钩子。模型侧会先调list_tools拿到工具列表,然后根据用户意图决定调哪个,再通过call_tool执行。整个过程对模型来说是透明的,它不需要知道工具是本地脚本还是远程服务。
注意:MCP 的
inputSchema必须用 JSON Schema 描述,而且 description 字段要写得足够清晰。我踩过的坑是,description 写得太模糊,模型会反复调同一个工具但传错参数。后来我把每个参数的 description 都写成“什么时候该传这个值”,调用准确率明显提升。
3.3 MCP 的边界:它不解决 Agent 之间的协作
MCP 的定位很清晰:它是模型与工具之间的协议。但它不解决 Agent 与 Agent 之间的通信。比如你有两个 Agent,一个负责规划,一个负责执行,它们怎么互相传任务?MCP 没有定义这个。你当然可以让规划 Agent 把执行 Agent 当成一个“工具”来调用,但这会丢失很多语义:执行 Agent 的自主性、任务状态的同步、多轮协商的过程。
这就是 A2A 要解决的问题。
4. A2A:当 Agent 开始互相“对话”
4.1 A2A 的核心抽象:Agent Card 与任务生命周期
A2A 最关键的创新是Agent Card。每个 Agent 对外暴露一张卡片,描述自己是谁、能做什么、怎么联系:
{ "name": "research-agent", "description": "擅长文献检索和摘要", "capabilities": ["search", "summarize"], "endpoint": "https://example.com/a2a", "authentication": { "type": "bearer" } }这张卡片让其他 Agent 可以“发现”它。发现之后,A2A 定义了一套任务生命周期:提交任务、接受任务、更新状态、返回结果。这比简单的“调用-返回”要复杂得多,因为 Agent 之间的任务往往不是一次性的,而是需要多轮交互。
比如规划 Agent 给研究 Agent 发一个任务:“帮我查一下最近三个月关于 Agent 协议的论文,总结成表格。”研究 Agent 可能先返回“已接受”,然后过一会儿返回“正在检索”,再返回“找到 15 篇,正在摘要”,最后返回结果。这个过程中,规划 Agent 可以随时查询状态,甚至取消任务。
A2A 的传输层通常也是 HTTP+JSON-RPC,但它在上层定义了更丰富的消息类型:Task、TaskStatus、Artifact、Message。这些结构让 Agent 之间的交互有了明确的语义。
4.2 A2A 与 MCP 的关系:互补而非竞争
很多人第一次看到 A2A 和 MCP 会懵:这俩不是都在讲 Agent 通信吗?其实它们解决的是不同层面的问题。
| 维度 | MCP | A2A |
|---|---|---|
| 通信对象 | 模型 ↔ 工具 | Agent ↔ Agent |
| 核心抽象 | Tool、Resource、Prompt | Agent Card、Task、Artifact |
| 交互模式 | 请求-响应 | 任务生命周期、多轮协商 |
| 自主性 | 工具无自主性 | Agent 有自主性 |
| 典型场景 | 模型调用搜索、数据库 | 多 Agent 协作完成复杂任务 |
我自己的做法是:底层用 MCP 接工具,上层用 A2A 做 Agent 协作。规划 Agent 通过 A2A 把任务委派给执行 Agent,执行 Agent 内部再用 MCP 调用具体工具。这样分层之后,每层职责清晰,调试也方便。
实操心得:A2A 的 Agent Card 一定要版本化。我遇到过 Agent 升级后能力变了,但卡片没更新,导致其他 Agent 还在按旧能力发任务。后来我在卡片里加了
version字段,并且要求每次能力变更必须递增版本号,问题就解决了。
4.3 A2A 的落地难点:状态同步与超时处理
A2A 最麻烦的地方在于状态同步。因为 Agent 任务是异步的,规划 Agent 发出任务后,不能一直阻塞等待。它需要轮询或者订阅状态更新。A2A 支持 SSE 推送,但 SSE 连接不稳定,断线重连后状态可能不一致。
我的做法是:状态查询接口必须幂等,并且每次返回都带一个last_updated时间戳。规划 Agent 本地维护一个任务状态缓存,收到推送时更新,断线重连后先拉一次全量状态,再继续订阅。这样即使推送丢了,也能通过轮询兜底。
超时处理也很关键。A2A 任务可能跑很久,规划 Agent 不能无限等。我一般会设置三级超时:接受超时(30 秒内没返回“已接受”就重试)、心跳超时(5 分钟没状态更新就查询一次)、总超时(30 分钟没结果就取消)。这三层超时加上重试策略,基本能覆盖大部分异常情况。
5. ACP:被低估的“消息中间层”
5.1 ACP 的定位:可靠消息传递与会话管理
ACP 的知名度不如 MCP 和 A2A,但它在某些场景下更实用。ACP 的核心是消息队列 + 会话状态。它不关心消息内容是工具调用还是 Agent 任务,它只保证消息可靠送达,并且维护会话的上下文。
你可以把 ACP 理解成 Agent 世界的消息总线。一个 Agent 发消息到某个主题,其他 Agent 订阅这个主题。消息可以持久化,可以重放,可以保证顺序。这对于需要高可靠性的场景很重要,比如金融、医疗领域的 Agent 协作。
ACP 的消息格式通常包含:会话 ID、发送者、接收者、消息类型、负载、时间戳。会话 ID 让多轮对话可以关联起来,消息类型让接收者知道怎么解析负载。
5.2 ACP 与 A2A 的选型对比
A2A 和 ACP 都能做 Agent 间通信,但侧重点不同。
| 维度 | A2A | ACP |
|---|---|---|
| 核心关注 | 任务委派与生命周期 | 消息可靠传递与会话 |
| 发现机制 | Agent Card | 主题订阅 |
| 交互模式 | 任务导向 | 消息导向 |
| 状态管理 | 任务状态机 | 会话上下文 |
| 适用场景 | 多 Agent 协作 | 事件驱动、高可靠 |
| 复杂度 | 较高 | 中等 |
我的经验是:如果你的 Agent 协作是任务驱动的,选 A2A;如果是事件驱动的,选 ACP。比如一个监控 Agent 发现异常,发消息给处理 Agent,处理 Agent 再发消息给通知 Agent,这种链式反应用 ACP 更自然。而如果是“帮我写一份报告”这种任务,拆解成多个子任务委派出去,用 A2A 更合适。
5.3 ACP 的实操要点:消息幂等与死信队列
ACP 落地时,消息幂等是必须处理的。因为消息可能重复投递,接收 Agent 必须能识别重复消息。我一般会在消息里加一个message_id,接收方维护一个已处理 ID 的集合,重复的直接丢弃。
死信队列也很重要。如果一条消息处理失败多次,不能一直重试,要丢到死信队列里人工介入。我见过一个系统因为死信队列没配,一条坏消息把整个 Agent 集群堵死了。后来加了死信队列,并且设置了最大重试次数,问题才解决。
注意:ACP 的会话状态不能无限增长。我一般会设置会话过期时间,比如 24 小时没活动的会话自动清理。否则内存会被慢慢吃光。
6. 协议选型实战:我如何为一个多 Agent 系统做决策
6.1 需求拆解:先搞清楚你要解决什么问题
选协议之前,先问自己三个问题:
- 通信双方是谁?模型和工具?Agent 和 Agent?还是 Agent 和消息总线?
- 交互模式是什么?请求-响应?任务委派?事件驱动?
- 可靠性要求多高?允许丢消息吗?需要持久化吗?
这三个问题的答案,基本能锁定协议范围。比如:
- 模型调工具 → MCP
- Agent 委派任务 → A2A
- Agent 事件通知 → ACP
- 底层传输 → JSON-RPC 或 HTTP
6.2 一个真实项目的协议组合方案
我之前做过一个多 Agent 研究助手系统,架构是这样的:
- 工具层:用 MCP 接入了搜索、PDF 解析、数据库查询三个工具。
- Agent 层:规划 Agent、检索 Agent、摘要 Agent、审核 Agent。
- 协作层:规划 Agent 通过 A2A 把任务委派给其他 Agent。
- 通知层:审核 Agent 发现问题时,通过 ACP 发消息给规划 Agent。
这个组合跑下来,最大的感受是分层清晰。MCP 让工具接入标准化,A2A 让任务委派有状态可查,ACP 让异常通知不丢。如果全用 JSON-RPC 硬搓,代码量至少翻三倍,而且维护起来很痛苦。
6.3 选型检查清单
| 检查项 | 问题 | 推荐协议 |
|---|---|---|
| 通信对象 | 模型调工具 | MCP |
| 通信对象 | Agent 调 Agent | A2A |
| 通信对象 | 事件广播 | ACP |
| 交互模式 | 请求-响应 | JSON-RPC |
| 交互模式 | 任务生命周期 | A2A |
| 交互模式 | 发布-订阅 | ACP |
| 可靠性 | 允许丢消息 | JSON-RPC |
| 可靠性 | 不允许丢消息 | ACP |
| 状态管理 | 无状态 | JSON-RPC |
| 状态管理 | 有状态会话 | ACP |
| 状态管理 | 任务状态机 | A2A |
这张表不是绝对的,但能帮你快速缩小范围。实际选型时,往往是组合使用,而不是二选一。
7. 常见问题与排查技巧实录
7.1 协议兼容性问题
问题:MCP Server 返回的工具描述,模型解析不了。
排查:先检查inputSchema是否符合 JSON Schema 规范。我遇到过required字段写成字符串而不是数组,导致模型解析失败。另外,description 里不要用特殊字符,有些模型对 Unicode 支持不好。
解决:用 JSON Schema 校验工具先验证一遍,确保格式正确。description 尽量用英文或简单中文,避免生僻字。
7.2 A2A 任务状态不一致
问题:规划 Agent 认为任务还在进行,执行 Agent 已经完成了。
排查:检查状态更新是否丢失。A2A 的 SSE 推送在弱网环境下容易丢。
解决:加轮询兜底。规划 Agent 每隔一段时间主动查询一次任务状态,以查询结果为准。同时,执行 Agent 的状态更新要带版本号,避免旧状态覆盖新状态。
7.3 ACP 消息重复消费
问题:同一个消息被处理了两次,导致重复操作。
排查:检查消息是否有唯一 ID,接收方是否做了幂等处理。
解决:消息里加message_id,接收方用 Redis 或本地缓存记录已处理的 ID,设置合理的过期时间。处理前先查 ID 是否存在,存在就跳过。
7.4 协议选型速查表
| 问题现象 | 可能原因 | 排查方向 | 解决方案 |
|---|---|---|---|
| 模型调错工具 | 工具描述模糊 | 检查 description | 写清楚使用场景 |
| 任务状态不同步 | SSE 丢推送 | 检查网络和重连 | 加轮询兜底 |
| 消息重复处理 | 无幂等设计 | 检查消息 ID | 加去重逻辑 |
| 会话内存泄漏 | 会话未过期 | 检查过期策略 | 设置 TTL |
| 工具调用超时 | 工具执行慢 | 检查工具性能 | 加超时和重试 |
| Agent 发现失败 | Agent Card 未更新 | 检查卡片版本 | 版本化卡片 |
最后分享一个小技巧:不管用哪个协议,日志一定要打全。我一般会在每个协议层都加一个 trace_id,从模型发起调用到工具返回结果,全链路可追踪。这样出问题时,能快速定位是哪一层挂了。踩过几次坑之后,我发现排查时间至少缩短了一半。
这个领域还在快速演进,协议本身也在迭代。我的建议是:先跑通最小闭环,再逐步引入协议分层。不要一上来就追求大而全,那样很容易被协议细节淹没。先用 JSON-RPC 把链路跑通,然后根据实际痛点引入 MCP、A2A 或 ACP,这样每一步都有明确的收益。