ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

MCP与A2A协议实战:从零搭建智能体协作基础设施

MCP与A2A协议实战:从零搭建智能体协作基础设施 1. 从两个缩写说起MCP 和 A2A 到底在解决什么问题第一次看到 MCP 和 A2A 这两个词放在一起很多人会懵。MCP 在硬件圈是微控制器引脚定义在工业圈是某种控制协议在 AI 圈却是另一回事。A2A 也一样做通信的会想到端到端做架构的会想到应用对应用。但把这两个词放进同一个标题里指向就很明确了——它们说的是 AI 应用层正在形成的两套协作协议Model Context Protocol和Agent-to-Agent Protocol。我最早接触 MCP 是在给一个内部知识库做检索增强的时候。当时每个模型、每个工具、每个数据源都要写一套适配代码换一个模型就得重写一遍。MCP 出现之后这件事的逻辑变了模型不再直接对接工具而是通过一个标准协议去发现和调用外部能力。A2A 则更进一步解决的是多个智能体之间怎么互相委托任务、怎么交换中间结果、怎么协商能力边界的问题。这两个协议放在一起实践本质上是在搭一套“智能体协作的基础设施”。MCP 负责让单个智能体能够接入外部世界A2A 负责让多个智能体能够互相配合。一个管纵向的能力扩展一个管横向的协作编排。适合谁来参考如果你正在做 AI 应用集成、智能体编排、或者想把现有系统接入大模型能力这套东西值得花时间吃透。如果你只是偶尔调个 API 做个 demo那可以先了解概念不必急着上手。我写这篇东西的出发点很简单网上讲 MCP 的文章不少讲 A2A 的也有但把两者放在一起、从工程落地角度讲清楚“怎么接、怎么调、怎么排错”的内容很少。大部分要么停留在概念介绍要么只给一个最简单的 hello world。我踩过的坑、调过的参数、遇到的报错在这里尽量都写出来。2. 协议设计的底层逻辑为什么不是直接调 API2.1 MCP 的核心思路把“能力”变成可发现、可调用的资源在没有 MCP 之前一个 AI 应用要接入外部工具通常是这样做的在代码里硬编码工具的名称、参数格式、调用方式。比如要查数据库就写一个query_database函数要读文件就写一个read_file函数。模型通过 function calling 来触发这些函数。这种做法的问题在于每接一个新工具就要改一次代码、重新部署一次服务。工具的描述信息散落在各个地方模型不知道有哪些工具可用也不知道每个工具的具体参数要求。更麻烦的是当工具有几十个上百个的时候提示词里根本放不下所有工具的描述。MCP 的做法是把工具、资源、提示词这三类东西统一抽象成“能力”通过一个标准协议暴露出来。客户端启动时先向服务端请求能力列表服务端返回一个结构化的清单包含每个能力的名称、描述、参数 schema。客户端把这个清单转换成模型能理解的格式模型决定调用哪个能力之后客户端再通过协议发起实际调用。这个设计的关键在于解耦。工具的实现和模型的调用之间隔了一层协议工具变了不需要改模型侧的代码模型换了也不需要改工具侧的代码。我实测下来一个中等规模的 MCP 服务端暴露二三十个工具客户端启动时的能力发现耗时在 200 毫秒以内完全可接受。2.2 A2A 的核心思路让智能体之间能“对话”而不是“硬编码调用”A2A 要解决的问题不太一样。假设你有三个智能体一个负责查资料一个负责写代码一个负责做审核。如果没有 A2A你需要在编排层写死先调查资料智能体把结果传给写代码智能体再把结果传给审核智能体。任何一个智能体的接口变了编排层就要改。A2A 的思路是让智能体自己描述自己能做什么、需要什么输入、产出什么输出。编排层不需要知道具体实现只需要根据任务需求去发现合适的智能体然后发起协作请求。智能体之间可以互相委托子任务也可以并行执行后汇总结果。这里有一个容易混淆的点MCP 和 A2A 不是替代关系。MCP 解决的是智能体与工具之间的连接A2A 解决的是智能体与智能体之间的连接。一个智能体可以通过 MCP 接入数据库和文件系统同时通过 A2A 与其他智能体协作。两者可以同时使用互不冲突。2.3 为什么选 JSON-RPC 和 Streamable HTTP 作为传输层MCP 和 A2A 在传输层都选择了 JSON-RPC 2.0 作为消息格式底层传输可以用 stdio、HTTP、或者 Streamable HTTP。JSON-RPC 的好处是结构简单、人类可读、调试方便。一个典型的请求长这样{ jsonrpc: 2.0, id: 1, method: tools/call, params: { name: query_database, arguments: { sql: SELECT * FROM users LIMIT 10 } } }Streamable HTTP 是在普通 HTTP 基础上支持流式响应。为什么需要流式因为有些工具调用耗时很长比如跑一个数据分析任务可能要几十秒。如果等全部完成再返回客户端会超时用户体验也很差。Streamable HTTP 允许服务端先返回一个“任务已接受”的响应然后通过同一个连接持续推送进度和最终结果。我一开始觉得流式不是必须的直到有一次接了一个代码执行工具跑一个测试用例要 40 多秒普通 HTTP 请求直接超时了。换成 Streamable HTTP 之后客户端可以实时看到执行日志体验完全不一样。3. 环境搭建与基础配置从零开始跑通第一个 MCP 服务3.1 工具选型与版本确认动手之前先把工具链定下来。我用的组合是运行时Node.js 20 LTS 或 Python 3.11MCP SDK官方提供的modelcontextprotocol/sdkNode或mcpPythonA2A SDK目前主流实现是a2a-sdkPython 和 TypeScript 都有调试工具MCP Inspector官方提供的可视化调试界面传输层开发阶段用 stdio生产环境用 Streamable HTTP版本这块要注意MCP 的 SDK 迭代很快不同版本之间的 API 有差异。我建议锁定一个已知稳定的版本不要盲目追新。比如 Node SDK 的 1.0.x 和 1.1.x 在工具注册的写法上就有区别。Python SDK 相对稳定一些但也要注意mcp包和mcp-sdk包不是同一个东西装错了会报模块找不到。提示安装之前先确认你的 Node 版本。MCP SDK 要求 Node 18 以上但实测 Node 20 LTS 最稳。Node 22 在某些流式场景下有兼容性问题建议避开。3.2 最小可运行 MCP 服务端的搭建先写一个最简单的 MCP 服务端暴露一个计算器工具。用 Python 写from mcp.server import Server from mcp.server.stdio import stdio_server from mcp.types import Tool, TextContent app Server(demo-server) app.list_tools() async def list_tools(): return [ Tool( nameadd, description计算两个数字的和, inputSchema{ type: object, properties: { a: {type: number, description: 第一个加数}, b: {type: number, description: 第二个加数} }, required: [a, b] } ) ] app.call_tool() async def call_tool(name: str, arguments: dict): if name add: result arguments[a] arguments[b] return [TextContent(typetext, textstr(result))] raise ValueError(f未知工具: {name}) async def main(): async with stdio_server() as (read, write): await app.run(read, write, app.create_initialization_options()) if __name__ __main__: import asyncio asyncio.run(main())这段代码做了三件事声明服务端名称、注册工具列表、实现工具调用逻辑。list_tools返回的inputSchema是 JSON Schema 格式客户端会把它转换成模型能理解的函数描述。跑起来之后用 MCP Inspector 连接npx modelcontextprotocol/inspector python server.pyInspector 会打开一个网页界面左边是能力列表右边是调用面板。你可以手动填参数、点调用、看返回结果。这个工具在调试阶段非常有用比直接看日志高效得多。3.3 客户端接入与能力发现流程客户端这边核心是三步建立连接、获取能力列表、调用能力。用 Node 写一个最小客户端import { Client } from modelcontextprotocol/sdk/client/index.js; import { StdioClientTransport } from modelcontextprotocol/sdk/client/stdio.js; const transport new StdioClientTransport({ command: python, args: [server.py] }); const client new Client({ name: demo-client, version: 1.0.0 }); await client.connect(transport); const tools await client.listTools(); console.log(可用工具:, tools.tools.map(t t.name)); const result await client.callTool({ name: add, arguments: { a: 3, b: 5 } }); console.log(调用结果:, result.content[0].text);这里有一个细节listTools返回的工具列表里每个工具都有name、description、inputSchema。客户端需要把这个列表转换成模型能理解的格式。如果是 OpenAI 的 function calling就转成tools数组如果是 Anthropic 的 tool use就转成对应的格式。转换逻辑不复杂但要注意inputSchema里的required字段要正确映射否则模型可能漏传参数。我踩过的一个坑是工具描述写得太简略模型不知道什么时候该调用。比如一个工具叫search描述只写“搜索”模型根本不知道搜什么、什么时候搜。后来改成“根据关键词搜索内部知识库返回最相关的文档片段”调用准确率明显提升。4. A2A 协议实践多智能体协作的编排与通信4.1 Agent Card 的设计与能力声明A2A 的核心概念是 Agent Card可以理解成智能体的“名片”。每个智能体启动时暴露一个 Agent Card描述自己叫什么、能做什么、需要什么输入、产出什么输出。一个典型的 Agent Card 长这样{ name: code-reviewer, description: 代码审核智能体检查代码规范、潜在 bug 和安全问题, version: 1.0.0, capabilities: { streaming: true, pushNotifications: false }, skills: [ { id: review-python, name: Python 代码审核, description: 审核 Python 代码返回问题列表和改进建议, inputModes: [text], outputModes: [text, json] } ], endpoint: http://localhost:8080/a2a }设计 Agent Card 的时候skills的粒度很关键。太粗了编排层不知道具体能做什么太细了卡片会变得很臃肿。我的经验是按“任务类型”来划分一个 skill 对应一类可独立完成的任务。比如“代码审核”是一个 skill“生成测试用例”是另一个 skill不要混在一起。capabilities里的streaming字段决定这个智能体是否支持流式响应。如果支持编排层可以在任务执行过程中收到进度更新如果不支持就只能等最终结果。我建议只要任务可能超过 10 秒就开启 streaming。4.2 任务委托与结果回传的完整流程A2A 的任务流程大致是这样的编排层发现合适的智能体发送任务请求智能体接受任务后开始执行执行过程中可以推送进度完成后返回结果。如果任务需要子任务智能体可以再向其他智能体发起委托。用 Python 写一个简单的任务委托示例import httpx async def delegate_task(agent_endpoint: str, task: dict): async with httpx.AsyncClient() as client: response await client.post( f{agent_endpoint}/tasks, json{ taskId: task[id], skillId: task[skill], input: task[input], streaming: True }, timeout60.0 ) return response.json()服务端收到任务后先返回一个taskId和状态accepted然后通过 SSE 或 WebSocket 推送进度。客户端根据taskId来关联进度和最终结果。这里有一个容易忽略的点任务幂等性。如果网络抖动导致客户端重试服务端不能重复执行同一个任务。我的做法是在服务端维护一个任务状态表收到重复taskId时直接返回已有状态不重新执行。4.3 多智能体编排的三种典型模式实际用下来多智能体编排主要有三种模式串行流水线智能体 A 的输出作为智能体 B 的输入B 的输出再传给 C。适合有明确先后依赖的任务比如“查资料 → 写初稿 → 审核修改”。并行扇出编排层把同一个任务拆成多个子任务同时发给多个智能体最后汇总结果。适合可以并行处理的场景比如同时让三个智能体从不同角度分析同一份数据。层级委托一个主智能体负责拆解任务把子任务委托给下层智能体下层智能体还可以继续往下委托。适合复杂任务但要注意控制委托深度避免无限递归。我实测下来串行流水线最稳定调试也最简单。并行扇出对结果汇总的逻辑要求比较高如果多个智能体的输出格式不一致汇总会很麻烦。层级委托最灵活但也最容易出问题建议先用前两种模式跑通再考虑层级委托。5. 实操中的常见问题与排查技巧5.1 连接建立失败与超时排查MCP 和 A2A 的调试过程中最常见的问题就是连接建立失败。表现是客户端报connection refused或timeout。排查思路按这个顺序来先确认服务端是否真的在监听。用netstat -tlnp | grep 端口号看端口有没有被占用。如果服务端用的是 stdio 传输那就不涉及端口要检查启动命令是否正确、工作目录是否对。再确认传输层配置是否匹配。客户端配的是 HTTP服务端开的是 stdio那肯定连不上。MCP 的传输层配置在客户端和服务端要一致这个在配置文件里很容易写错。最后看超时设置。Streamable HTTP 的默认超时可能只有 30 秒如果工具执行时间超过这个值连接会被断开。我一般把超时设成 120 秒同时在服务端加心跳保活。注意如果用的是 Docker 部署容器内的 localhost 和宿主机的 localhost 不是一回事。客户端连localhost:8080可能连的是客户端容器自己的 8080而不是服务端容器的。要用 Docker 网络里的服务名或者宿主机 IP。5.2 工具调用参数不匹配的典型报错参数不匹配是第二高频的问题。报错信息通常是Invalid params或Missing required parameter。原因一般有三个一是inputSchema定义和实际调用不一致。比如 schema 里写的是a和b调用时传的是num1和num2。这个在手动调试时容易发现但在模型自动调用时模型是根据 schema 生成参数的如果 schema 描述不清模型可能生成错误的参数名。二是类型不匹配。schema 里写的是number模型传了字符串3。JSON-RPC 对类型比较严格3和3不是一回事。解决办法是在服务端做类型转换或者在 schema 里把类型放宽。三是嵌套对象处理。如果参数是一个嵌套对象schema 要写清楚每一层的结构。我见过一个案例schema 里只写了type: object没有定义properties模型完全不知道这个对象里该放什么。5.3 流式响应中断与重连策略流式响应中断的表现是客户端收到了一部分进度更新然后连接突然断了最终结果没收到。原因可能是网络抖动、服务端超时、或者客户端处理速度跟不上导致缓冲区溢出。我的处理策略是客户端维护一个lastEventId重连时带上这个 ID服务端从断点继续推送。服务端这边任务状态要持久化不能因为连接断了就把任务丢了。重连后根据taskId查询任务状态如果还在执行就继续推送如果已完成就直接返回结果。还有一个细节SSE 连接默认会在 30 秒无数据后断开。如果任务执行时间很长但中间没有进度更新连接会被误断。解决办法是服务端定期发送心跳事件哪怕没有实际进度也要发一个空事件保持连接。5.4 常见问题速查表问题现象可能原因排查方法解决方式连接被拒绝服务端未启动或端口不对检查进程和端口监听启动服务端确认端口配置调用超时工具执行时间过长查看服务端日志增大超时启用流式响应参数校验失败schema 与实际调用不一致对比 schema 和请求体修正 schema 或调用参数流式中断网络抖动或心跳缺失查看连接断开时间点加心跳实现断点续传工具找不到能力列表未刷新重新调用 listTools重启客户端或手动刷新结果格式错误输出未按约定格式检查返回内容结构统一输出格式加校验6. 从能跑到好用性能优化与生产化建议6.1 能力发现的缓存策略每次客户端启动都去拉一遍完整的能力列表在工具数量多的时候会比较慢。我的做法是在客户端本地缓存能力列表设置一个合理的过期时间比如 5 分钟。过期后异步刷新不阻塞主流程。如果服务端的能力有变更可以通过一个轻量的通知机制告诉客户端客户端收到通知后再主动刷新。缓存的时候要注意版本号。服务端的能力列表带一个version字段客户端缓存里也存这个版本号。刷新时先请求版本号如果版本号没变就直接用缓存变了再拉完整列表。这样大部分情况下只需要一次轻量请求。6.2 并发调用的限流与隔离多个工具同时调用时如果不做限流服务端可能被压垮。我在服务端加了一个简单的信号量控制同时执行的工具调用不超过 N 个N 根据服务端的 CPU 和内存来定。超出的请求排队等待而不是直接拒绝。隔离方面不同类型的工具要分开处理。比如数据库查询和文件读写一个慢查询可能拖垮整个服务。我的做法是给每类工具分配独立的线程池或进程池互不影响。如果某个工具连续失败自动熔断一段时间避免雪崩。6.3 日志、监控与问题回溯生产环境一定要有完整的日志。我记录的字段包括请求 ID、工具名称、参数摘要、开始时间、结束时间、执行结果状态、错误信息。参数摘要不要记完整参数避免敏感信息泄露只记关键字段和参数个数。监控方面重点关注三个指标调用成功率、平均耗时、P99 耗时。成功率低于 95% 就要告警P99 耗时突然升高也要关注。我遇到过一次 P99 从 200 毫秒飙到 5 秒查下来是某个工具在特定参数下会触发全表扫描加了索引之后恢复正常。问题回溯的时候请求 ID 是关键。客户端生成的请求 ID 要透传到服务端服务端的日志里带上这个 ID这样从客户端到服务端的完整链路都能串起来。6.4 安全边界与权限控制MCP 和 A2A 都是开放协议默认没有认证机制。生产环境必须加认证。我的做法是在传输层加 API Key 或者 OAuth每个客户端有独立的凭证服务端根据凭证判断权限。权限控制要细到工具级别。不是所有客户端都能调用所有工具。比如一个只读的客户端不应该能调用删除数据的工具。我在服务端的工具注册表里加了一个requiredScopes字段调用时检查客户端凭证是否包含对应的 scope。还有一个容易忽略的点工具的参数要做输入校验。模型生成的参数不一定安全可能包含注入攻击的 payload。所有参数在进入实际执行逻辑之前都要做类型检查、长度限制、特殊字符过滤。7. 我踩过的坑与实操心得第一个坑是工具描述写得太技术化。我一开始把工具描述写成给开发看的文档比如“执行 SQL 查询并返回结果集”。模型看到这个描述不知道什么时候该用、什么时候不该用。后来改成“当用户需要查询数据库中的结构化数据时使用输入标准 SQL 语句返回查询结果”调用准确率提升了很多。模型需要的是“使用场景”而不是“技术实现”。第二个坑是忽略流式响应的背压问题。服务端推送速度太快客户端处理不过来缓冲区满了之后连接被强制断开。解决办法是在客户端加一个处理队列收到事件后先入队后台慢慢消费。服务端这边根据客户端的消费速度动态调整推送频率。第三个坑是A2A 任务没有设置合理的超时。一个智能体委托任务给另一个智能体如果被委托方卡住了委托方会一直等。后来我在每个任务上加了三层超时单次请求超时、任务总超时、心跳超时。任何一层超时都触发任务终止和资源回收。第四个坑是能力列表的版本管理混乱。服务端加了新工具但没更新版本号客户端一直用旧缓存导致新工具不可用。后来强制要求每次能力变更必须递增版本号客户端启动时先比对版本号不一致就强制刷新。第五个坑是错误信息不够具体。服务端返回Internal error客户端完全不知道发生了什么。后来改成返回结构化的错误信息包含错误码、错误描述、可能的解决建议。比如TOOL_TIMEOUT: 工具执行超时建议增大超时时间或优化工具实现。这样排查问题快很多。最后分享一个小技巧调试 MCP 和 A2A 的时候用一个中间代理把所有的请求和响应都记下来。我用的是一个简单的 HTTP 代理把所有流量转发到日志文件。这样出问题的时候可以完整回放整个交互过程比看零散的日志高效得多。这个代理在开发阶段帮我省了很多时间尤其是排查那些偶发的、难以复现的问题。这套东西目前还在快速演进协议本身也在迭代。我的建议是先把核心流程跑通理解清楚 MCP 的能力发现机制和 A2A 的任务委托模型然后再根据实际需求去扩展。不要一上来就追求大而全的架构从一个小工具、一个小智能体开始跑通了再往上加。
返回列表