最近在推进一个企业级多智能体项目,核心方案是用DeepAgents 21.3搭建一套基于MCP和A2A双协议的业务集群。在此之前我也拿LangChain、AutoGen这些框架试过水,但真正到生产环境,才发现“多个智能体怎么稳定协作”才是最大的坎。这篇就把这次从架构设计到落地部署的完整过程摊开来聊,重点说说MCP和A2A为什么必须一起用、怎么用,以及在集群场景下哪些坑是文档里不会告诉你的。适合正在评估或已经上手多智能体开发的架构师、后端工程师和AI应用开发者参考。
1. 双协议的分工逻辑:为什么一个MCP还不够
1.1 MCP解决的是“智能体不会干活”的问题
MCP(Model Context Protocol)本质上是一个工具接入协议。智能体本身没有手,没有眼睛,它想查数据库、调接口、发消息,总不能每个系统都单独写一套连接代码。MCP的定位就是统一这层连接:智能体通过MCP Client连接MCP Server,MCP Server背后再对接具体的业务系统。
用生活的话说,MCP像是给智能体装了一排标准USB接口。不管你是接U盘(数据库)、接键盘(API)、接摄像头(文件扫描),只要对方实现了MCP Server,插上就能用。我在项目里用FastMCP快速封装了订单查询、库存校验、物流轨迹三个工具,每个工具就是一两百行Python代码,跑起来非常干净。
但MCP有个边界:它解决的是“智能体到工具”的纵向连接。它不关心两个智能体之间怎么说话,谁来启动谁、任务怎么流转、结果怎么回传。这就好比每个员工都配上了一个工具箱,但同事之间怎么分工协作,工具箱解决不了。
1.2 A2A解决的是“智能体之间怎么配合”的问题
A2A(Agent-to-Agent,智能体间通信协议)补上的正是这个横向协作的缺口。A2A协议定义了智能体之间的一套标准通信语言:每个智能体发布一份Agent Card(能力名片),标注自己会干什么、怎么调用;调用方通过Task(任务)对象发起协作,然后跟踪任务状态,收取产物。
如果MCP是USB接口,那A2A更像是商务谈判的“标准合同模板”。大家不用临时约定怎么报价、怎么交付,按同一套模板走,签完字各自干活就行了。在这个项目里,调度方通过A2A把“查询订单OT2178并核实物流异常”这个任务发给订单智能体,订单智能体再通过MCP去查数据库,查完把结果按照A2A的Message格式推回来,整个过程双方不需要知道对方的内部实现,只需要知道Agent Card长什么样。
1.3 DeepAgents 21.3如何融合两套协议
DeepAgents 21.3让我觉得顺手的点,在于它把这两套协议放在了同一个运行时里统一管理。你不用自己同时维护MCP SDK和A2A SDK两套会话逻辑,框架提供了一套Agent Runtime,既能注册MCP工具,又能发布和调用A2A能力端。
我在21.3里的实际用法是这样的:所有“干活”类的技能全部走MCP接入,比如查数据库、调ERP、发通知;所有“协作”类的流程全部走A2A编排,比如派工单、进度汇报、结果汇总。这样分了两层之后,逻辑非常清晰:底层是工具能力,上层是协作能力,互不干扰,出了问题也好排查。
2. 架构设计:多智能体集群的四层结构
2.1 模块划分:注册中心、调度层、执行层、工具层
我把这套集群的架构拆成了四层,从上到下分别是:
- 注册与路由层:维护所有智能体的Agent Card,负责发现能力、分发请求。
- 编排调度层:接收顶层业务请求,拆解成子任务,通过A2A分发给对应智能体。
- 智能体执行层:一个个具体的业务智能体,负责各自领域内的判断和处理。
- 工具接入层:通过MCP Server统一接入企业内部的数据库、API、文件系统等。
这套结构最大的好处是每一层都可以独立扩展。加了新业务,只需要在注册中心挂一个新的Agent Card,在工具层挂新的MCP Server,调度层几乎不用改。我在做售后工单系统时,先上了订单、库存、物流三个智能体,后来要接CRM,就是新增一个MCP Server加一个Agent Card的事,调度层逻辑一行没动。
2.2 任务拆解与状态机流转
A2A协议里的Task是有生命周期的。从submitted(已提交)到working(执行中),再到succeeded或failed,整个流转过程调度层心知肚明。我在设计任务流转时,参考了有限状态机的思路,把每一步的状态转换都显式记录下来,并落到日志里。
有一点必须提醒:任务状态一定不能让智能体自己随便改,所有状态更新要经过统一的任务管理模块。我之前图省事让各智能体自行上报状态,结果出现两个智能体同时把自己标成succeeded的情况,排查了半天,最后硬改成统一管理状态才稳定下来。
顺带说一句,A2A的Artifact(产物)机制特别好用。任务执行完,智能体把结果写成结构化的Artifact返回,调度层不需要再解析一堆自然语言,直接取JSON字段做后续处理就行。我在工单系统里就让每个智能体返回统一的Artifact格式,字段包括result_code、message、data三个部分,后面写代码省了不知道多少事。
2.3 通信边界:什么时候用MCP,什么时候用A2A
我在项目里总结了一套判断依据:
- 如果是“智能体要操作外部系统”,走MCP。比如查数据库、调库存API、读取配置文件。
- 如果是“智能体要让另一个智能体干一件事”,走A2A。比如让物流智能体查一下运单,让工单智能体生成处理意见。
两者偶尔会交叉,比如订单智能体收到A2A请求后,内部再通过MCP去查数据库。这种交叉是正常的,实际场景中几乎不可避免。关键是要把“对外协作”和“对内工具调用”分清楚,标注在接口设计文档里。
2.4 生活化类比:一个成熟团队是这样运转的
我打个比方帮大家理解这套架构。假设你开了一家电商公司,接到一个大促活动:
- 老板(调度层)拿到任务后拆成“备货、上线、客服、物流”几块。
- 各部门主管(智能体)接到任务后,分给自己的团队去执行。
- 团队里的执行员工(MCP工具调用)要去仓库查库存、要调系统改价格,这些操作就是调用MCP Server。
老板和主管之间怎么沟通——任务怎么分配、怎么验收、多久汇报一次——这就是A2A做的事。而主管让自己手下去查数据、改系统,这又是MCP做的事。两套机制各有各的位置,谁也替代不了谁。
3. 实操:从零搭建一套基于MCP与A2A的订单售后集群
3.1 环境准备与依赖安装
我这里用的是DeepAgents 21.3,需要Python 3.11及以上版本。建议用虚拟环境隔离依赖,别直接裸装到系统Python里。
python3 -m venv deepagents-env source deepagents-env/bin/activate pip install deepagents==21.3.0 fastmcp a2a-sdk装完后验证一下版本:
deepagents --version如果输出21.3.0或21.3.x,就说明环境没问题。依赖装好之后别急着写代码,先把目录结构规划好。我的建议是:
project/ ├── agents/ # 各业务智能体定义 ├── tools/ # MCP工具封装 ├── registry/ # Agent Card注册信息 ├── runtime/ # 运行时启动配置 └── logs/ # 运行日志目录结构看着小,后面集群一扩展,好处就出来了。别嫌麻烦,我刚开始就是懒得理结构,堆了两个星期后自己都找不到文件在哪。
3.2 用FastMCP封装第一个企业工具
先说一个简单的场景:订单智能体需要一个“按订单号查询订单”的工具。实现MCP Server的代码并不复杂:
from fastmcp import FastMCP import sqlite3 mcp = FastMCP("order-tools") @mcp.tool() def query_order(order_id: str) -> dict: """根据订单ID查询订单基础信息""" conn = sqlite3.connect("orders.db") cur = conn.cursor() cur.execute( "SELECT order_id, status, amount, create_time FROM orders WHERE order_id = ?", (order_id,) ) row = cur.fetchone() conn.close() if not row: return {"found": False} return { "found": True, "order_id": row[0], "status": row[1], "amount": row[2], "create_time": row[3] } @mcp.tool() def update_order_status(order_id: str, status: str) -> bool: """更新订单状态""" # 实际项目中这里要加权限校验和审计日志 conn = sqlite3.connect("orders.db") cur = conn.cursor() cur.execute( "UPDATE orders SET status = ? WHERE order_id = ?", (status, order_id) ) conn.commit() conn.close() return True if __name__ == "__main__": mcp.run(transport="streamable-http")注意这里的transport参数,我用的21.3推荐方式是streamable-http,比旧的SSE更稳,尤其适合集群环境下多个智能体同时发请求的场景。旧版本的SSE连接是长连接,在代理层容易出问题,streamable-http改成短请求加流式响应后,对负载均衡友好多了。
启动这个MCP Server:
python tools/order_mcp.py启动后会在本机开一个HTTP服务,默认端口8000。为了在DeepAgents里能连上,还需要在配置里注册这个工具端点。
3.3 在DeepAgents中注册MCP工具
DeepAgents 21.3把工具注册写成了配置,很方便。在配置文件里加上:
mcp_servers: order-tools: transport: streamable-http url: http://localhost:8000/mcp然后在智能体定义里引用:
from deepagents import Agent order_agent = Agent( name="order_agent", description="负责订单查询与状态更新", instructions="你是订单域智能体,用户询问订单信息时,调用order-tools工具获取数据。", mcp_servers=["order-tools"] )这里有一个经验:工具的描述信息(docstring)一定要写清楚。大语言模型是通过描述来决定要不要调用工具的,描述写得模糊,模型就可能不调用或者乱调用。比如你写“查询订单”,模型不明白这个工具到底能查什么字段,就会犹豫。我一般会写成“根据订单ID查询订单基础信息,包括订单状态、金额、创建时间,用于订单售后场景”。
3.4 发布A2A Agent Card
接下来要让订单智能体能被其他智能体通过网络调用。A2A协议要求每个智能体发布一份Agent Card,格式是JSON。我写的示例是这样的:
agent_card = { "name": "order_agent", "description": "订单域智能体,负责订单信息查询、状态变更、退换货审核。", "url": "http://order-agent.internal:8100/", "version": "1.0.0", "capabilities": { "skills": [ { "id": "query_order", "name": "查询订单", "description": "根据订单ID返回订单状态、金额、时间等信息" }, { "id": "update_status", "name": "更新订单状态", "description": "修改订单当前状态,如pending、shipped、completed" } ] } }发布Agent Card有两种方式,一种是把Card以JSON文件形式放到注册中心,另一种是提供一个Card端点让注册中心来拉取。我在生产环境用的是后者,因为更新能力时不需要重启注册中心。
# a2a_card_server.py from a2a import A2ACardServer from a2a.types import AgentCard app = A2ACardServer( card=AgentCard(**agent_card), handler=order_agent ) if __name__ == "__main__": app.run(host="0.0.0.0", port=8100)启动后,注册中心通过GET请求这个端点的/.well-known/agent-card.json就能发现订单智能体。
3.5 调度层:通过A2A编排一个售后工单流程
现在到了关键环节:我们有一个工单智能体,收到用户一个售后请求:“订单OT2178显示已经签收但我没收到,查一下怎么回事。”
工单智能体自己解决不了,它需要调用订单智能体查单,还需要调用物流智能体查物流。实现代码如下:
from a2a import A2AClient, Task, Message, TaskState async def process_after_sale(ticket_content: str): # 1. 发现订单智能体 order_client = A2AClient("http://order-agent.internal:8100/") # 2. 创建任务并下发 task = Task( input=Message( content=f"查询订单状态,订单号为OT2178" ) ) order_task = await order_client.send_task(task) # 3. 轮询任务状态 while True: task_status = await order_client.get_task(order_task.id) if task_status.status in (TaskState.SUCCEEDED, TaskState.FAILED): break await asyncio.sleep(1) # 轮询间隔1秒 # 4. 解析返回结果 if task_status.status == TaskState.SUCCEEDED: artifacts = task_status.artifacts order_data = json.loads(artifacts[0].data) # 根据订单状态决定下一步处理逻辑 return order_data这一段是整个集群的核心——任务下发和状态轮询。这里有个重要设计决策:用轮询还是用推送。
A2A协议实际上支持两种模式,但生产环境我建议优先用轮询。为什么?因为推送模式需要在智能体间建立长连接,集群规模一大,连接数就成了瓶颈,而且断线重连逻辑非常麻烦。轮询虽然实时性稍微差一点,但实现简单、稳定性高,1秒间隔对大多数业务场景来说已经足够。
3.6 容器化部署与集群启动
本地跑通了,下一步就是部署到服务器。我用Docker Compose来编排所有服务,每个智能体独立成一个容器。
version: "3.8" services: registry: image: deepagents-registry:21.3 ports: - "8080:8080" environment: - AUTO_DISCOVER=true order-agent: build: ./agents/order ports: - "8100:8100" environment: - MCP_CONFIG=/app/mcp_config.yaml logistics-agent: build: ./agents/logistics ports: - "8101:8100" environment: - MCP_CONFIG=/app/mcp_config.yaml ticket-agent: build: ./agents/ticket ports: - "8102:8100" environment: - REGISTRY_URL=http://registry:8080 nginx: image: nginx:stable ports: - "443:443" volumes: - ./nginx/conf.d:/etc/nginx/conf.d容器化部署的核心是网络规划。我在这个项目里把智能体间通信都改成了容器DNS名,比如order-agent、logistics-agent,而不是IP地址。这么做的好处是,只要容器重启后注册到同一个DNS,调度层无需修改任何配置。这点在Kubernetes环境里尤其重要,因为Pod的IP是动态的,用DNS名才能保证Agent Card里的url始终有效。
3.7 服务发现与注册中心配置
注册中心是集群的“通讯录”。DeepAgents 21.3的注册中心支持两种发现方式:
- 主动注册:智能体启动时向注册中心POST自己的Agent Card。
- 被动发现:注册中心定时抓取已知端点的Agent Card。
我建议两种都开。主动注册保证新上线的智能体第一时间被发现,被动发现则兜底处理注册丢失的情况。我在生产环境里配了每30秒做一次被动发现,配合主动注册的双保险,基本没出现过智能体“失联”的情况。
注册中心启动后,可以在Dashboard上直观看到所有在线智能体的状态、能力列表、最近调用情况。对于运维排查,这比一个个看容器日志高效太多。
4. 生产环境必须处理的四个问题
4.1 认证与权限隔离
多智能体集群上线后,首先要解决的是安全问题。智能体之间互调的接口如果裸奔在企业内网,一旦某个智能体被攻击,攻击者可能通过它横向调用其他智能体。
我采用的方案是内部mTLS认证,在Nginx层统一做客户端证书校验,智能体之间的所有接口都走Nginx代理。同时,MCP工具建议做操作层面的权限控制,比如查询工具可以放开,但更新订单状态必须校验调用方身份。
4.2 超时、重试与幂等控制
智能体调用和普通API有一个本质区别:大模型推理时间不确定,调用链一长,超时阈值很难定。
我的经验是把超时分三层:
- 网络连接超时:5秒,走不到就快速失败。
- 单次任务超时:120秒,智能体单次推理加工具调用应该在这个范围内完成。
- 整体流程超时:600秒,整个多智能体协作流程的上限。
针对重试,我吸取过一次教训。刚开始没有幂等控制,调度层对失败的A2A任务直接重发,结果订单状态被重复更新了两次,一个“已完成”的订单被改回了“处理中”,用户那边直接炸了。
后来我在Agent Card的每个Skill上都加了幂等键机制,调度层重试时必须携带相同的task_id,执行层收到重复task_id直接返回上次结果。这个改动看着简单,但解决的是生产环境最要命的重复执行问题。
4.3 日志追踪与链路监控
多智能体调用链比单体应用复杂得多,一个售后请求可能穿过工单、订单、物流三个智能体,中间还穿插多次MCP工具调用。出了问题,如果没有链路追踪,排查耗时可以按天算。
DeepAgents 21.3内置了OpenTelemetry集成,我在部署时给每个智能体都加了trace_id透传。思路很简单:调度层创建任务时生成一个trace_id,通过A2A的Message header传给下一跳,MCP工具调用时再透传到数据库查询层。这样日志系统里按trace_id一搜,整条链路都出来了。
注意:链路追踪不是等出了问题才想起要加的,最好在第一天设计接口时就统一带上trace_id字段,不然后期补,成本高到你想哭。
4.4 弹性伸缩与会话状态维护
智能体容器和普通服务一样,是可以水平扩展的。订单智能体如果成了瓶颈,就把它扩到5个副本,前面加一层负载均衡。A2A的Agent Card地址应该指向负载均衡器,而不是具体实例。
但这里有个大坑:多智能体会话状态。智能体协作过程中,一个任务往往需要多轮交互,而容器是无状态的。如果你做的是无状态任务(查询、计算),随便扩;但如果是多轮对话或长流程审批,就需要把会话状态放到Redis或数据库里,不能存在智能体进程内存中。
我在这上面栽过一次跟头:调度层的重试请求因为负载均衡策略打到了另一个副本,导致前一轮对话上下文丢失,结果智能体回了一句“我不记得你刚才问过什么”。后来我把会话状态统一存到Redis,智能体每次交互前先从Redis拉取上下文,才算彻底解决。
5. 常见问题与排查技巧实录
5.1 MCP Server连接闪断,工具调用经常失败
症状:智能体先是正常调用了几个工具,执行到一半突然报工具调用超时。
排查方向:
- 先看MCP Server进程的log,确认是主动退出还是异常崩溃。
- 检查MCP Server所在机器有没有把连接数打满,streamable-http模式下每个请求会新建连接,高并发时文件描述符很容易被占满。
- 检查nginx的proxy_read_timeout配置,默认60秒,如果MCP工具执行超过60秒,连接会被nginx掐断,而智能体这边拿到的报错往往非常隐晦。
我当时的问题就是nginx超时设置太短,订单查询工具要查的数据量大,SQL执行了80多秒,nginx在60秒时把连接断了。解决方案是把proxy_read_timeout调到了300秒,同时对慢查询做了优化。
5.2 注册中心发现不了新上线的智能体
症状:启动了一个新的智能体容器,注册中心Dashboard看不到它,调度层调度时也找不到。
排查方向:
- 先确认Agent Card端点能访问:curl http://agent-ip:port/.well-known/agent-card.json
- 如果curl能通,检查注册中心的主动发现间隔配置,可能30秒还没到。
- 如果curl不通,大概率是容器网络问题,Docker Compose环境下检查服务名是否在同一个网络里。
这个问题的典型原因是我把智能体容器放到了默认bridge网络,而注册中心在另一个网络,两边网络不通。解决方案是在docker-compose.yml里统一声明一个自定义网络,所有服务都挂上去。
5.3 多智能体出现循环调用,谁也不让谁
症状:调度层发现两个智能体互相给彼此发任务,形成A调用B、B又调用A的循环,一直不结束。
排查方向:
- 给每个A2A任务加最大跳数限制,我默认设5跳,超过直接抛异常终止。
- 给每个任务加过期时间,超过过期时间强制失败。
- 检查Agent Card的Skill描述,确认没有歧义,避免智能体误解“该由谁来负责”从而导致相互推诿。
5.4 问题速查表
| 现象 | 可能原因 | 快速解决方案 |
|---|---|---|
| MCP工具调用超时 | nginx/服务端超时配置过短 | 调大proxy_read_timeout,优化工具执行逻辑 |
| A2A任务一直pending | 执行智能体崩溃或无响应 | 登录执行节点查看日志,确认进程存活,增加健康检查 |
| 两个智能体循环调用 | 编排逻辑存在循环依赖 | 设定最大跳数限制和任务过期时间 |
| 工具返回结果乱码 | 编码不统一 | 统一使用UTF-8,MCP Server端显式声明content-type |
| 集群扩容后任务重复执行 | 缺少幂等机制 | 增加task_id幂等去重,重复请求直接返回上次结果 |
| 智能体找不到对应工具 | 工具注册配置错误 | 检查MCP配置文件里url和transport是否匹配实际服务 |
5.5 排查思路总结
多智能体集群出问题时,我的排查顺序一般是:先看A2A链路,确认任务状态卡在哪个智能体;再看MCP日志,确认工具调用是否失败;最后看大模型本身,确认是不是理解出了问题。
前两类问题占了九成以上,解决思路也都比较标准化。真正困难的是第三类——模型理解错乱,比如把查库存理解成创建订单。这类问题没有银弹,只能优化提示词、加强工具描述的清晰度,同时在高危操作上加人工审核环节。
6. 关于21.3版本的那点事
DeepAgents版本号里的21.3,核心是对MCP和A2A的运行时融合。相比早期版本需要自己拼装两套SDK,21.3把双协议的管理统一到了Agent Runtime里,配置和运维负担都小了不少。
我也试过之前流行的纯LangGraph方案做多智能体编排,那套方法的DSL很强大,但图结构一复杂,调试起来非常痛苦。DeepAgents胜在把“通信协议”标准化了——智能体之间不依赖框架内部的数据结构,而是走A2A这种开放协议。这意味着以后集群里甚至可以混入用其他框架开发的智能体,只要它实现了A2A标准,就能接入协作。这种开放性对长期演进的系统来说很关键。
另一个让我觉得顺手的地方是21.3版本对配置的简化。所有MCP工具、Agent Card、任务超时、重试策略都收敛到一份核心配置文件里,改完配置滚动重启即可,不用再像以前那样改代码发版。
回到最初的问题:企业级多智能体集群到底应该怎么搭?我的答案很明确——用MCP把工具能力标准化,用A2A把协作流程标准化。一个好的多智能体系统不是模型越强越好,而是协议设计得足够清爽,让每个智能体都知道自己该干什么、找谁配合、怎么交接。DeepAgents 21.3刚好把这两件事都做进去了。如果大家正在搭建自己的多智能体集群,在动手之前,先把MCP和A2A的分工边界想清楚,肯定能少走不少弯路。