1. 从一次接口联调说起:为什么协议选择会变成一场灾难
去年我参与了一个多智能体协作系统的搭建,团队里有人用 JSON-RPC 写了一个工具调用服务,有人用另一套自定义的 HTTP 接口做任务分发,还有人从某个开源项目里搬来了一套基于消息队列的通信层。三套东西各自跑得挺好,但一旦要把它们串起来,问题就炸了:工具调用服务返回的id字段是自增整数,任务分发那边期望的是 UUID 字符串;消息队列那套用的是完全不同的信封格式,连错误码的定义都对不上。我们花了整整一周时间写适配层,最后发现适配层本身的复杂度已经超过了业务逻辑。
这件事让我意识到一个很现实的问题:Agent 之间的通信协议之所以“这么多”,并不是因为大家喜欢重复造轮子,而是因为每个协议都在解决不同层面的问题。JSON-RPC 解决的是“怎么调用一个远程函数”,MCP 解决的是“怎么让模型发现和使用工具”,A2A 解决的是“多个 Agent 之间怎么互相委派任务”,ACP 解决的是“Agent 和外部系统之间怎么建立可靠的会话通道”。它们看起来都在做“通信”,但抽象层级、设计目标、适用场景完全不同。
这篇文章想做的事情很具体:把这几个协议放在同一张桌子上,从它们各自要解决的问题出发,拆解它们的设计取舍,然后给出在实际项目中选型和组合使用的判断依据。如果你正在搭建多 Agent 系统,或者正在纠结“到底该用哪个协议”,那这篇内容应该能帮你省下不少试错时间。
2. JSON-RPC:最容易被低估的通信底座
2.1 它到底解决了什么问题
JSON-RPC 的核心价值可以用一句话概括:让远程调用看起来像本地调用。它的设计极其克制,请求体只有四个字段——jsonrpc、method、params、id,响应体只有jsonrpc、result或error、id。没有复杂的头信息,没有强制的传输层绑定,你可以用 HTTP、WebSocket、甚至标准输入输出来承载它。
这种极简设计带来的好处是:实现成本极低。我试过用不到 200 行 Python 代码写一个完整的 JSON-RPC 服务端和客户端,包括错误处理和批量请求。对于 Agent 系统来说,这意味着你可以快速把任何一个函数暴露成远程可调用的工具,而不需要引入沉重的框架依赖。
但 JSON-RPC 的问题也恰恰在于它的“薄”。它只定义了消息格式,没有定义能力发现、没有定义会话管理、没有定义流式传输的标准方式。这些缺失的部分,正是后来 MCP、A2A 这些协议要补上的。
2.2 在 Agent 场景下的实际表现
在 Agent 系统里,JSON-RPC 最常见的用法是作为工具调用的传输层。比如一个 Agent 需要调用“查询天气”这个工具,它可以通过 JSON-RPC 发送:
{ "jsonrpc": "2.0", "method": "tools/call", "params": { "name": "get_weather", "arguments": {"city": "北京"} }, "id": 1 }服务端返回:
{ "jsonrpc": "2.0", "result": { "content": [{"type": "text", "text": "晴,25°C"}] }, "id": 1 }这个模式跑起来很稳,但有一个隐藏的坑:JSON-RPC 本身不规定method的命名规范。我见过有的团队用get_weather,有的用weather.get,有的用tool.weather.query。当你的 Agent 需要动态发现可用工具时,这种命名的不一致性会让自动发现变得非常困难。这也是为什么后来 MCP 要在 JSON-RPC 之上再定义一层标准化的方法名和参数结构。
另一个实际问题是错误处理。JSON-RPC 定义了标准的错误码范围(-32768 到 -32000 是保留的),但很多实现会自定义错误码,导致客户端无法统一处理。我的经验是:在 Agent 系统里,错误码应该尽量收敛到少数几个语义明确的类别,比如“工具不存在”“参数错误”“执行超时”“内部错误”,而不是每个工具都定义一套自己的错误码。
2.3 什么时候该用它,什么时候不该用
JSON-RPC 适合的场景很明确:点对点的、同步的、无状态的函数调用。如果你的 Agent 只需要调用几个固定的工具,不需要动态发现,不需要复杂的会话管理,那 JSON-RPC 是最省事的选择。
但如果你需要以下任何一项,JSON-RPC 就不够用了:
- 工具的动态发现和描述(需要 MCP 这样的能力层)
- 多 Agent 之间的任务委派和状态同步(需要 A2A 这样的协作层)
- 长连接的会话管理和流式输出(需要 ACP 这样的会话层)
一个实用的判断标准:如果你发现自己在 JSON-RPC 之上不断加自定义字段来传递元信息,那说明你需要的是一个更高层的协议,而不是继续在 JSON-RPC 上打补丁。
3. MCP:让模型真正“看见”工具的那一层
3.1 从“硬编码工具”到“动态发现”的转折
在 MCP 出现之前,大多数 Agent 系统是这样集成工具的:开发者在代码里写一个工具列表,每个工具对应一个函数,然后把这个列表塞进模型的上下文里。模型看到的是工具的名称和描述,但它不知道这些工具的参数 schema 是什么,也不知道调用后会返回什么格式的数据。
这种方式的痛点在于:工具和 Agent 是紧耦合的。每加一个新工具,就要改代码、重新部署。如果工具有几十个,上下文窗口很快就会被工具描述占满。
MCP 的核心贡献是定义了一套标准化的工具描述和调用协议。它基于 JSON-RPC,但规定了几个关键的方法:
tools/list:列出所有可用工具,包括名称、描述、参数 schematools/call:调用指定工具,传入参数,返回结构化结果resources/list和resources/read:读取外部资源prompts/list和prompts/get:获取预定义的提示模板
这套方法名的标准化,让 Agent 可以在运行时动态发现工具,而不需要把工具列表硬编码在提示词里。模型只需要知道“有一个tools/list方法可以调用”,然后根据返回的 schema 来决定怎么调用具体工具。
3.2 参数 schema 的设计细节
MCP 要求每个工具提供 JSON Schema 格式的参数描述。这个设计看起来简单,但实际用起来有很多讲究。我踩过的一个坑是:schema 的粒度太粗或太细都会出问题。
太粗的话,模型不知道每个参数的具体含义和约束,容易传错值。比如一个query参数,如果只写{"type": "string"},模型可能会传一个完整的自然语言句子,而工具期望的是一个结构化的查询表达式。
太细的话,schema 会变得非常冗长,占用大量上下文窗口。我见过一个工具的参数 schema 写了 200 多行,光是描述就占了上千个 token。
我的经验是:每个参数的描述控制在 1-2 句话,重点说明“这个参数影响什么”和“什么情况下该传什么值”。对于枚举类型的参数,把可选值列出来;对于可选参数,明确说明默认行为。这样模型既能理解,又不会浪费太多上下文。
3.3 MCP 的传输层选择
MCP 本身不绑定传输层,但最常见的两种是标准输入输出和 HTTP+SSE。标准输入输出适合本地工具进程,启动快、延迟低,但不利于远程调用。HTTP+SSE 适合远程服务,但需要处理连接管理和重连逻辑。
我在实际项目中的选择是:本地工具用标准输入输出,远程工具用 HTTP+SSE。这样既能保证本地调用的性能,又能支持分布式部署。需要注意的是,SSE 的连接在长时间空闲后可能会被中间层断开,所以客户端需要实现自动重连,并且在重连后重新拉取工具列表,以防服务端有更新。
3.4 MCP 解决不了的问题
MCP 的定位很清晰:它是 Agent 和工具之间的协议。但它不解决 Agent 和 Agent 之间的通信问题。如果你有两个 Agent,一个负责规划,一个负责执行,它们之间怎么传递任务、怎么同步状态、怎么处理冲突?这些是 MCP 不涉及的领域,也是 A2A 要解决的问题。
4. A2A:多 Agent 协作时的任务委派与状态同步
4.1 为什么 Agent 之间需要专门的协议
单 Agent 系统里,所有决策都在一个上下文里完成,不存在“通信”问题。但多 Agent 系统就不一样了:规划 Agent 需要把任务拆解后发给执行 Agent,执行 Agent 需要把结果返回给规划 Agent,可能还有审核 Agent 在中间做质量检查。这些 Agent 可能运行在不同的进程、不同的机器、甚至不同的组织里。
如果没有一个标准协议,每个多 Agent 系统都会发明自己的任务描述格式和状态同步机制。A2A 的目标就是把这层标准化,让不同来源的 Agent 能够互相协作。
4.2 任务生命周期管理
A2A 的核心概念是“任务”。一个任务有明确的生命周期:创建、分配、执行中、完成、失败、取消。每个状态转换都有对应的消息类型和确认机制。
这个设计解决了一个很实际的问题:当执行 Agent 挂掉时,规划 Agent 怎么知道任务失败了。在没有协议的情况下,通常靠超时来判断,但超时时间很难设——设短了容易误判,设长了浪费资源。A2A 通过显式的状态上报和心跳机制,让规划 Agent 能够及时感知执行 Agent 的状态。
我在实现时的一个经验是:心跳间隔和超时阈值需要根据任务类型动态调整。对于快速任务(比如查询数据库),心跳间隔可以设到秒级;对于长任务(比如训练模型),心跳间隔可以设到分钟级。关键是让规划 Agent 能够区分“执行 Agent 正在努力工作”和“执行 Agent 已经挂了”。
4.3 能力协商与任务匹配
A2A 还定义了 Agent 之间如何协商能力。一个 Agent 可以声明自己支持哪些技能、有哪些限制条件(比如最大并发数、支持的输入格式)。规划 Agent 在分配任务时,会根据这些声明来匹配最合适的执行 Agent。
这个机制在实际系统中非常有用。比如你有三个执行 Agent,一个擅长文本处理,一个擅长图像处理,一个擅长数据分析。当规划 Agent 收到一个“分析这份销售数据并生成图表”的任务时,它可以把任务拆成“数据分析”和“图表生成”两个子任务,分别分配给对应的 Agent。
但这里有一个坑:能力声明不能太细,否则匹配逻辑会变得非常复杂。我见过一个系统把能力声明细化到了“支持 CSV 格式的销售数据分析”这种程度,结果规划 Agent 需要维护一个巨大的匹配规则表。更好的做法是声明粗粒度的能力类别,然后在任务分配时通过参数来细化需求。
4.4 A2A 和 MCP 的关系
很多人会混淆 A2A 和 MCP,觉得它们都在做“通信”。但它们的抽象层级不同:MCP 是 Agent 和工具之间的协议,A2A 是 Agent 和 Agent 之间的协议。一个 Agent 可以通过 MCP 调用工具,同时通过 A2A 和其他 Agent 协作。两者是互补的,不是竞争的。
在实际架构中,我通常这样组合:每个 Agent 内部用 MCP 管理工具调用,Agent 之间用 A2A 管理任务委派。这样职责清晰,也便于独立演进。
5. ACP:会话层的可靠性与流式传输
5.1 会话管理的必要性
JSON-RPC 是无状态的,每次调用都是独立的。MCP 和 A2A 虽然引入了更多语义,但也没有定义完整的会话管理。ACP 要解决的就是这个问题:如何在一个长连接上管理多个会话,如何处理流式输出,如何保证消息的可靠投递。
在 Agent 场景下,会话管理的重要性体现在几个方面。首先,Agent 的推理过程可能是流式的,需要把中间结果实时推送给调用方。其次,一个 Agent 可能同时处理多个任务,每个任务需要独立的会话上下文。最后,网络可能不稳定,需要断线重连和消息补发机制。
5.2 流式传输的实现细节
ACP 的流式传输通常基于 SSE 或 WebSocket。SSE 的优点是简单、兼容性好,缺点是单向通信,客户端不能通过同一个连接发送消息。WebSocket 是双向的,但需要处理更复杂的连接状态管理。
我在实际项目中的选择是:如果只需要服务端推送,用 SSE;如果需要双向实时交互,用 WebSocket。对于大多数 Agent 场景,服务端推送已经足够了,因为客户端通常只是发起请求然后等待结果。
流式传输的一个关键设计是消息分片。一个大的响应可能需要分成多个片段发送,每个片段带上序号和是否结束的标志。客户端收到所有片段后拼接成完整响应。这个机制看起来简单,但实际实现时需要注意:片段的顺序必须保证,丢失的片段必须能检测到并请求重发。
5.3 断线重连与消息补发
ACP 的另一个重要能力是断线重连。当连接断开后,客户端应该能够重新连接,并且从上次断开的位置继续接收消息。这要求服务端维护一个消息缓冲区,记录每个会话已发送的消息序号。
我在实现时的一个经验是:消息缓冲区的大小需要根据业务场景来定。如果消息产生速度很快,缓冲区太小会导致客户端重连后丢失消息;缓冲区太大则会占用过多内存。一个实用的做法是:缓冲区只保留最近 N 条消息,同时记录一个“最早可用序号”,客户端重连时如果请求的序号早于这个值,就告知客户端需要重新初始化会话。
5.4 ACP 与其他协议的分工
ACP 通常不单独使用,而是作为传输层承载 MCP 或 A2A 的消息。比如你可以用 ACP 建立一个会话,然后在这个会话上发送 MCP 的工具调用请求,或者发送 A2A 的任务委派消息。这样分层的好处是:会话管理、流式传输、断线重连这些通用能力只需要实现一次,上层协议可以专注于自己的语义。
6. 协议选型的决策框架:从场景反推技术栈
6.1 先问三个问题
面对一个具体的 Agent 系统需求,我通常会先问三个问题:
- 通信的双方是谁:是 Agent 和工具,还是 Agent 和 Agent,还是 Agent 和外部系统?
- 通信的模式是什么:是同步的请求-响应,还是异步的任务委派,还是流式的持续推送?
- 可靠性的要求有多高:能接受消息丢失吗?需要断线重连吗?需要消息顺序保证吗?
这三个问题的答案,基本就能确定需要哪些协议。
6.2 典型场景的协议组合
| 场景 | 推荐协议组合 | 理由 |
|---|---|---|
| 单 Agent 调用本地工具 | JSON-RPC 或 MCP(标准输入输出) | 简单、低延迟、无需网络 |
| 单 Agent 调用远程工具 | MCP(HTTP+SSE) | 标准化工具发现和调用 |
| 多 Agent 协作完成复杂任务 | A2A + MCP | A2A 负责任务委派,MCP 负责工具调用 |
| 需要流式输出的交互式 Agent | ACP + MCP | ACP 管理会话和流式传输,MCP 管理工具 |
| 跨组织的 Agent 协作 | A2A + ACP | A2A 定义协作语义,ACP 保证传输可靠 |
这个表格不是绝对的,但可以作为一个起点。实际选型时还需要考虑团队的技术栈、运维成本、生态兼容性等因素。
6.3 一个常见的架构误区
我见过不少团队在搭建多 Agent 系统时,试图用一个协议解决所有问题。比如有人试图用 A2A 来管理工具调用,结果发现 A2A 的任务模型对于简单的函数调用来说太重了。也有人试图用 MCP 来做 Agent 之间的协作,结果发现 MCP 没有定义任务状态同步的机制。
正确的做法是分层组合:底层用 ACP 或 JSON-RPC 做传输,中间用 MCP 做工具能力层,上层用 A2A 做协作层。每一层只解决自己范围内的问题,层与层之间通过标准接口交互。
6.4 迁移和兼容性的考虑
如果你已经有一个基于 JSON-RPC 的系统,想逐步迁移到 MCP,我的建议是:先保持 JSON-RPC 的传输层不变,在方法名和参数结构上向 MCP 靠拢。比如把get_weather改成tools/call加上name: "get_weather"的参数结构。这样可以在不改变传输层的情况下,逐步引入 MCP 的语义。
对于 A2A 的引入,通常需要更大的改动,因为任务模型和状态管理是全新的。我的经验是:先从一两个 Agent 之间的协作开始试点,跑通后再逐步扩大范围。不要一开始就试图把所有 Agent 都改成 A2A,那样风险太大。
7. 实操中的坑与经验:那些文档里不会写的东西
7.1 协议版本兼容性
JSON-RPC 有 1.0 和 2.0 两个版本,差异不小。1.0 的id可以是字符串或数字,2.0 要求必须是字符串或数字或 null。1.0 支持位置参数,2.0 推荐命名参数。如果你的系统需要同时对接不同版本的客户端,建议在服务端做版本检测和适配。
MCP 和 A2A 也在不断演进,新版本可能会增加字段或改变语义。我的做法是:在消息中带上协议版本号,服务端根据版本号选择对应的处理逻辑。这样可以在升级协议时保持向后兼容。
7.2 错误处理的统一
不同协议对错误的定义不同。JSON-RPC 有标准的错误码,MCP 在 JSON-RPC 基础上增加了工具执行错误的语义,A2A 有任务失败的状态码。在一个混合系统中,需要把这些错误统一映射到一个内部的错误模型,否则上层逻辑会变得非常混乱。
我通常定义一个内部的错误类型枚举,比如TOOL_NOT_FOUND、INVALID_PARAMS、EXECUTION_TIMEOUT、TASK_FAILED,然后在各个协议的适配层做映射。这样上层代码只需要处理内部错误类型,不需要关心底层用的是哪个协议。
7.3 性能与开销的权衡
每引入一层协议,就会增加一层序列化和反序列化的开销。JSON-RPC 的序列化开销很小,MCP 因为要传输工具 schema,开销会大一些。A2A 的任务消息通常比较大,因为要包含任务描述、状态、结果等信息。
在高并发场景下,这些开销会累积。我的经验是:对于性能敏感的内部调用,可以直接用 JSON-RPC 跳过 MCP 的 schema 发现步骤;对于需要动态发现工具的场景,才走完整的 MCP 流程。A2A 的消息可以做一些压缩,比如只传输变化的字段而不是完整状态。
7.4 调试和可观测性
多协议系统的调试是一个挑战。一个请求可能经过 JSON-RPC 传输、MCP 工具调用、A2A 任务委派等多个环节,出问题时很难定位是哪一层的问题。
我的做法是:在每个协议层都加上统一的 trace ID,并且在日志中记录完整的消息内容。这样可以通过 trace ID 把整个链路的日志串起来。另外,对于流式传输,要记录每个片段的序号和时间戳,方便分析延迟和丢包情况。
一个实用的技巧:在开发阶段,可以把所有协议的消息都打印到控制台,用不同的颜色区分不同的协议层。这样一眼就能看出消息的流转路径。
7.5 安全边界
不同协议的安全模型不同。JSON-RPC 通常依赖传输层的安全机制,MCP 需要处理工具调用的权限控制,A2A 需要处理 Agent 之间的身份认证和授权。在一个混合系统中,需要确保每个协议层的安全措施都到位,不能有短板。
我的建议是:在网关层做统一的认证和授权,然后把身份信息透传到各个协议层。这样每个协议层不需要重复实现认证逻辑,只需要根据透传的身份信息做细粒度的权限判断。
8. 写在最后:协议是手段,不是目的
折腾了这么多协议之后,我最大的体会是:协议的选择应该由场景驱动,而不是由技术偏好驱动。JSON-RPC 简单直接,MCP 标准化了工具发现,A2A 解决了多 Agent 协作,ACP 补上了会话管理。它们各自有明确的适用边界,没有哪个协议是“万能”的。
在实际项目中,我通常会从最简单的方案开始:如果 JSON-RPC 够用,就不引入 MCP;如果单 Agent 够用,就不引入 A2A。只有当场景确实需要更高级的能力时,才引入对应的协议。这样既能保持系统的简洁性,又能避免过度设计。
另外,协议只是通信的约定,真正决定系统质量的是消息的设计、错误处理、可观测性这些“脏活累活”。我见过太多团队在协议选型上争论不休,却在消息格式和错误处理上草草了事,结果系统上线后问题不断。把精力花在这些基础工作上,比纠结用哪个协议更有价值。