☰
A2A与MCP协议深度解析:Agent协作与工具调用实战指南
2026/10/22 6:21:21 网站建设 项目流程

简介:这份PPT课件系统解析A2A协议与MCP协议,面向AI应用开发、多智能体系统设计及技术选型相关从业者,帮助读者厘清两种协议在Agent协作与外部资源连接上的分工。资源包内包含1个PPTX文件,容量约2.42MB,结构完整,从协议基础概述、技术架构对比到功能特性差异、应用场景分析、性能优化方向与发展前景展望均有涉及。PPT通过分层结构、Agent卡与USB-C集线器等比喻,直观解释A2A与MCP的核心交互流程,并对比了传输效率、安全机制和节点兼容性上的差异。已有332人学习,内容结合招聘、科研模拟实验、工业物联网等实际案例,展示两种协议如何互补以降低多Agent系统集成成本,支撑高效、开放、可扩展的AI生态建设。对于希望快速掌握AI协同标准化进展的学习者,这是一份信息密度较高的入门与进阶参考资料。

1. A2A协议与MCP协议到底是什么:先把“连接”和“协作”分开

AI Agent跑到生产环境后,最先卡住的往往不是模型推理效果,而是它摸不到外部工具、也喊不动别的Agent。MCP协议解决的是“模型怎么调用函数、读数据”这层连接问题,A2A协议解决的是“多个Agent之间怎么互相派活、回传结果”的协作问题。这两件事经常被放在一起讨论,但它们的层次完全不同。有人以为A2A会取代MCP,实际上一套完整Agent系统通常两个都要用。本文不打算做PPT复述,而是按我自己的落地经验,把MCP的最小服务端、A2A的AgentCard与任务状态机拆开讲清楚,再给出选型边界和联调踩坑记录。适合正在做Agent平台、多智能体系统的开发者阅读,新手能跟着跑通第一个示例,熟手可以直接跳去避坑章节对照自己的问题。

2. 先拆MCP:模型、工具与数据之间的“标准插座”

2.1 MCP的角色边界:host、client、server三层各干什么

MCP的全称是Model Context Protocol,核心思路是定义一套标准接口,让大模型应用通过客户端连接工具服务端。这里涉及三个角色:host是发起方,通常是聊天应用或Agent运行时;client是host内部维护的连接组件,负责按协议发请求;server是能力提供方,可以是天气查询、数据库连接器、企业内网API封装。

这套分层的直接收益是:模型应用不再需要为每个工具写定制适配器。工具方实现一次server,就能被所有支持MCP的host复用。反过来,host换一个工具也只需要改server配置,不用动业务代码。MCP的消息格式基于JSON-RPC 2.0,所有交互都规整成jsonrpc、method、params、id四个字段,这为跨语言实现打下了基础。

常见误用是有人把MCP当成一个“工具注册中心”来用,往里面塞业务编排逻辑。实际上MCP不关心业务如何组织,它只负责一次工具调用的完整往返。谁接、怎么接、接几次,那是上层Agent编排的事。

2.2 用Python搭一个最小MCP服务端:快速验证工具调用

我一般先用官方Python SDK里的FastMCP封装搭最小服务端,它把JSON-RPC编解码、握手流程都封装好了,一行代码就可以把普通函数暴露成工具。

from mcp.server.fastmcp import FastMCP # 创建服务端实例,名字会出现在客户端的工具命名空间里 mcp = FastMCP("pm-tools") @mcp.tool() def query_stock(sku: str) -> str: """按SKU查询库存余量""" # 真实项目中这里会接数据库或WMS系统接口 return f"{sku} 库存余量: 326 件" if __name__ == "__main__": mcp.run()

逻辑说明:装饰器mcp.tool()会把query_stock注册成MCP工具。客户端发起tools/list请求时,协议返回该工具的名字、描述和输入参数Schema。mcp.run()默认走stdio transport,适合本地调试,也适合被IDE或Agent运行时拉起。

参数说明:工具名默认取函数名,描述取函数docstring,入参Schema由类型注解自动生成。sku: str会产出JSON Schema的string类型,客户端根据这个Schema做入参校验。返回值可以是字符串或字典,字典会自动转成结构化内容块,便于客户端区分文本和结构化数据。

2.3 MCP消息流:initialize、tools/list、tools/call一个都不能少

MCP客户端的请求顺序是固定的:先发initialize握手,交换协议版本和各自支持的能力;握手完成后发tools/list拉取工具清单;真正要执行时发tools/call传入工具名和参数。跳过握手直接调工具,服务端会返回“Not initialized”错误,这是第一个常见的联调翻车点。

一个典型的客户端调用用Python SDK写出来长这样:

from mcp import ClientSession from mcp.client.streamable_http import streamablehttp_client # 连接远程MCP服务端,走HTTP transport async with streamablehttp_client("http://localhost:8000/mcp") as session: async with ClientSession(session) as client: # 第一步先握手,交换能力 await client.initialize() # 拉取当前服务端暴露的全部工具 tools = await client.list_tools() print([t.name for t in tools]) # 调用工具,参数必须与Schema匹配 result = await client.call_tool("query_stock", {"sku": "SKU-10086"}) print(result.content)

逻辑说明:streamablehttp_client建立的是HTTP+SSE长连接,适合跨网络的远程调用;ClientSession负责维护请求ID和响应映射。initialize()完成协议握手,list_tools()获取工具清单,call_tool()执行实际调用。三次调用缺一不可,否则可能遇到初始化错误或工具不存在的问题。

参数说明:call_tool的第二参数是字典,必须和工具声明里的入参名一致。这里传入{"sku": "SKU-10086"}对应服务端函数的sku参数。如果服务端函数有多入参,字典里应包含全部必填key。

2.4 Transport怎么选:本地进程优先stdio,跨服务走Streamable HTTP

MCP的传输层有stdio和Streamable HTTP两种主流方式,选择直接影响部署形态。

维度stdioStreamable HTTP
通信方式父子进程标准输入输出HTTP请求 + SSE响应流
适用场景本地调试、单机Agent跨服务、微服务、远程工具
生命周期随宿主进程启停独立服务,可水平扩展
认证能力无内置认证支持OAuth等HTTP认证
推荐场景开发期快速验证生产环境多团队共享

我一般建议:你在本地写demo或者给单个Agent挂工具时用stdio,省掉端口和鉴权成本;但工具要开放给多个Agent或团队成员共用时,立刻切Streamable HTTP,否则每个客户端都要拉起一个子进程,资源开销会很难看。

3. 再拆A2A:Agent与Agent之间的业务对话协议

3.1 A2A出现的原因:MCP解决不了的“互操作”问题

MCP把模型和工具打通了,但企业里往往有多个Agent:订单Agent、物流Agent、财务Agent可能由不同团队用不同技术栈开发,它们之间的沟通没有公共标准。A2A(Agent-to-Agent)协议要解决的就是这一层:每个Agent对外公布自己会什么、怎么访问、支持哪些能力,其他Agent通过这些信息派发任务、接收结果。

A2A的核心不是把Agent做成单体,而是让异构Agent保持自治,通过标准协议协同。它定义了三种角色:client发起任务,remote agent接收并处理任务,aggregator作为中间协调者转发请求。一个Agent可以同时是某些场景的client,又是另一些场景的remote agent。

之前有同事把A2A比作“Agent界的HTTP协议”,这个类比基本准确。它不关心Agent内部用什么模型、什么框架,只关心Agent之间怎么描述能力、传递消息、推进任务。

3.2 AgentCard:Agent能力的“主页”

A2A的发现机制是AgentCard。每个Agent在/.well-known/agent.json路径发布一份JSON描述文件,其他Agent先读它,才知道该往哪里发任务、这个Agent会做什么。

{ "name": "order-agent", "description": "负责订单查询、物流跟踪和异常上报", "url": "https://agent.example.com/order", "version": "1.2", "capabilities": { "pushNotifications": false, "stateTransitionHistory": false }, "skills": [ { "id": "track_order", "name": "订单物流追踪", "description": "根据订单号查询当前物流状态", "tags": ["order", "logistics"] }, { "id": "return_order", "name": "退换货处理", "description": "发起退换货流程并返回售后单号", "tags": ["after-sale"] } ] }

逻辑说明:name和description是Agent的基础身份信息,客户端据此展示能力摘要。url是核心字段,所有任务请求消息要POST到这个地址。capabilities声明运行特性:pushNotifications表示是否支持服务端主动推送状态更新,stateTransitionHistory表示是否记录状态变更历史。skills数组描述可执行的具体能力,类似MCP的工具清单。

参数说明:tags用于能力检索和路由,命名建议用业务域通用词,否则跨团队发现时会搜不到。version建议跟随接口变更递增,客户端可以缓存AgentCard,版本变化时重新拉取。

3.3 Task状态机与一次跨Agent调用:状态推进的每一步都有意义

A2A把一次协作抽象为Task。Task的生命周期是固定的:submitted表示任务已受理;working表示Agent正在处理;可能中间出现input-required表示需要更多信息;正常结束进入completed,附带artifacts结果集;异常或主动取消则进入failed或canceled。客户端必须按这个状态机处理流转,不能把input-required当失败。

一次最简单的A2A调用流程是这样的:客户端向AgentCard里的url发送tasks/send请求,附带任务描述;remote agent响应一个Task对象,初始状态为submitted;处理完成后再次查询或通过推送拿到终态。如果Agent不支持推送,客户端要主动轮询tasks/get。

状态机设计的好处是天然适配长耗时任务。不像普通HTTP接口必须立即返回结果,A2A允许任务在后台跑几分钟,客户端通过轮询感知进度。

3.4 实现一个最小A2A接收端:用FastAPI暴露任务端点

不用完整SDK,用FastAPI写一个最小接收端就能理解A2A的消息结构。

from fastapi import FastAPI from pydantic import BaseModel app = FastAPI() class TextPart(BaseModel): text: str class Message(BaseModel): role: str parts: list[TextPart] class TaskParams(BaseModel): message: Message class TaskRequest(BaseModel): jsonrpc: str id: str method: str params: TaskParams @app.post("/order") async def handle_task(req: TaskRequest): if req.method == "tasks/send": # 简单示例:直接把请求文本回显为完成结果 return { "jsonrpc": "2.0", "id": req.id, "result": { "task": { "id": "task-001", "status": "completed", "artifacts": [ { "parts": [ {"text": "已收到你的请求,处理完成"} ] } ] } } } # 不支持的方法返回JSON-RPC错误码 return { "jsonrpc": "2.0", "id": req.id, "error": {"code": -32601, "message": "method not found"} }

逻辑说明:请求体是标准的JSON-RPC 2.0结构,method取值tasks/send表示新任务。服务端返回的result.task.status决定状态流转,这里直接返回completed并附上artifacts。真实项目中,收到任务后应先返回submitted或working,异步处理完成后客户端再轮询tasks/get拿终态。

参数说明:id字段由客户端生成,服务端响应时必须原样回传。role可取user或agent,表示消息来源。parts是消息内容数组,除了text还支持file和data类型,对应文件路径和二进制数据。

4. 选型与配合:MCP不是A2A的下位替代

4.1 一张表看清MCP和A2A的边界

很多人纠结“二选一”,实际上两者的设计目标完全不冲突。列一张对比表最直观:

维度MCPA2A
解决的问题模型到工具/数据的连接Agent与Agent的协作
消息结构JSON-RPC 2.0JSON-RPC 2.0(REST封装)
核心角色host / client / serverclient / remote agent / aggregator
发现机制客户端配置文件AgentCard(/.well-known/agent.json)
任务模型单次工具调用,同步返回Task状态机,支持长时异步
典型场景查询库存、搜索知识库、调API订单Agent向物流Agent派单
是否关心Agent内部实现不关心不关心

MCP是“能力接入”层,解决单个Agent怎么拿到外部数据的问题;A2A是“协作编排”层,解决多个Agent怎么分工的问题。两者可以独立存在,也可以叠加使用。

4.2 一个典型的三层架构:调度层、Agent层、工具层

实际生产中,我见过比较顺的Agent平台长这样:最上层是调度层,由工作流引擎或编排服务负责拆解用户意图、决定派给哪个Agent;中间是Agent层,每个Agent有自己负责的业务域和独立的A2A端点;底层是工具层,所有数据库、API、文档库通过MCP server接入。

每个Agent内部既可能是A2A的remote agent,又是MCP的host。它对外通过A2A接收其他Agent派来的任务,对内通过MCP调用自己域内的工具。这个结构最大的好处是:每个Agent的替换、升级都不会影响跨Agent协作,只要保持A2A端点和AgentCard不变即可。

4.3 什么时候只上MCP,什么时候必须上A2A

如果你的系统只有一个Agent,它的工具需要接数据库、查接口,那只需要MCP就够了,A2A是多余的。但如果系统里有两个以上独立部署、独立开发的Agent,并且它们需要互相派发任务,就必须引入A2A。判断标准不是Agent数量,而是“是否存在跨进程、跨团队的能力委托”。同进程内的函数调用不需要协议,跨服务才需要。

我一般先画一张协作图:把Agent画成节点,把工具和数据画成外部依赖。节点和外部依赖之间标MCP,节点和节点之间标A2A。这张图画完,需要什么协议就一目了然了。

4.4 两者的配合误区:不要在A2A消息里塞MCP调用细节

常见的错误是在A2A的text部分里直接写“请调用query_stock('SKU-10086')”。这样等于把工具名硬编码到消息里,接收方Agent一旦MCP工具列表变化,消息就失效了。正确做法是A2A只传递业务意图,比如“查询SKU-10086的库存余量并返回可用数”,接收方Agent自己决定用哪个MCP工具。

这里有一个血泪教训:早期我们为了省事,让下游Agent直接执行上游传来的工具名,结果上游重构工具名后,整条链路悄悄断裂,没人发现。后来统一改成“业务语义传递、工具选择下沉”后,稳定性明显改善。

5. 避坑排查:A2A与MCP联调最容易翻车的五个细节

5.1 MCP工具参数校验失败:类型注解和实际入参不一致

现象:客户端调用工具时报Input validation error,或返回“additional properties not allowed”。

原因:函数声明了sku: str,调用时传了{"sku": 10086},整数被直接透传;或者函数用了dict做入参类型,导致SDK无法生成精确Schema,客户端传了多余字段。

解决:所有工具入参都用具体类型注解,复杂结构用Pydantic模型,不要用裸dict。调用方严格按Schema传参。举例:库存查询入参若包含仓库维度,就定义class StockQuery(BaseModel): sku: str; warehouse: str,类型清晰后校验问题立刻减少。

5.2 AgentCard返回404:路径或URL配置不一致

现象:客户端发现Agent时访问https://agent.example.com/.well-known/agent.json得到404,但浏览器直接访问Agent主页正常。

原因:服务端路由没配/.well-known/agent.json,或者AgentCard里的url字段写的是页面地址而不是任务端点。

解决:先确认两件事:AgentCard一定要放在站点的/.well-known/agent.json固定路径;url字段一定要指向接受tasks/send的POST端点。用curl验证最直接:curl -s https://agent.example.com/.well-known/agent.json | jq '.url',拿到结果再对比POST端点是否可访问。

5.3 Task一直停在working状态:轮询策略和结果回传不完整

现象:A2A任务提交后,客户端轮询tasks/get一直返回working,但Agent日志显示业务早就处理完了。

原因:Agent内部业务完成后忘了把Task状态更新为completed,或更新时没带artifacts导致状态机判定数据不完整。另一个常见原因是轮询间隔设置太短,上游还没落库就查询到了旧状态。

解决:Agent侧把状态更新放到finally块里执行,确保任务结束时必然更新状态;客户端轮询间隔建议不低于2秒,并为tasks/get加上请求序号,防止读到乱序响应。这个坑排查成本高,建议提前写好状态机转换的单元测试。

5.4 MCP的SSE流被网关缓冲,事件一直推不下来

现象:本地直连MCP服务端一切正常,部署到服务器后客户端长时间收不到工具返回,偶尔几分钟后才全部到达。

原因:SSE依赖持续连接推送事件,Nginx等网关默认开启缓冲,把事件攒到一定大小才刷给客户端,导致流式响应变成“一次性倒出来”。

解决:反向代理配置里关闭对MCP路径的缓冲,关键参数是proxy_buffering off;,同时把proxy_read_timeout调大。这个坑在Kubernetes内部署时尤其隐蔽,因为中间多了一层Ingress转发,很容易让人误以为是代码问题,查半天才发现是网关配置。

5.5 A2A与MCP的认证混用:两种协议不要共用一个鉴权中间件

现象:系统同时接入MCP和A2A后,A2A的AgentCard能发现,但tasks/send频繁报401;单独测MCP工具调用则正常。

原因:MCP的Streamable HTTP认证和A2A的Authorization头设计不一致,网关用同一套Bearer Token校验两个路径,下游Agent只认自己体系的token,于是互相误伤。

解决:路由上把两个协议分开挂到不同前缀,比如/mcp/*和/agent/*,中间件分别配置。认证逻辑也要分:MCP走其标准OAuth flow,A2A按AgentCard声明的认证方式校验。这条做好了能省掉大量联调白噪音。

6. 联调验证:写一个跨Agent任务的并发压测脚本思路

协议都跑通后,下一步是验证两个协议叠加时的稳定性。我习惯构造一个最小联调场景:Agent A是订单客服入口,收到用户消息后通过A2A把“查询物流”任务派给Agent B;Agent B内部通过MCP调用物流查询工具,拿到结果后经A2A回传。这个场景覆盖了A2A的发现、发送、轮询和MCP的工具调用整条链路。

压测脚本的核心思路是统计两个指标:跨Agent任务的端到端延迟分布,以及MCP工具独立调用延迟的对比。重点观察当并发数升高时,端到端延迟的P95是否明显偏离MCP工具调用的P95,如果偏离过大,说明耗时卡在A2A轮询等待上。

我最后一条教训是:写压测脚本时一定不要只断言HTTP状态码是200,要断言task.status == "completed"以及artifacts里是否真的拿到了文本内容。真实系统里出现过Agent返回200但任务结果是错误信息的情况,状态码掩盖了协作层的异常。把断言写严格,这份脚本才能长期沉淀为回归用例。希望帮到你。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询