去年下半年开始,我把公司内部那些各自为战的 Agent 项目往“一个真正的集群”上面靠。靠完以后回头看,最核心的变化不是模型换得多强了,而是三样东西补齐了:MCP把工具接入统一了,A2A让 Agent 之间能正经地打招呼、交接任务,Skills则把反复调教出来的能力沉淀成了可复用的模块。这套组合,就是“超级多智能体”的基本盘——可编排、可互通、可扩展。这三个词听着抽象,拆开落地其实非常具体:MCP 解决“Agent 怎么用工具”,A2A 解决“Agent 怎么找 Agent 干活”,Skills 解决“Agent 的能力怎么被复用”,最后再用一个编排内核把它们串起来。这篇文章我就按这个顺序,把每一层都讲透,给出一套可以直接照着搭的方案。
1. 先把“超级多智能体”这句话拆开:三个协议各管哪一层
很多人一听到“多智能体”就以为是把一堆 Agent 进程拉起来,让它们自由对话。这个理解错得挺远。真正的多智能体集群,核心不在“多”,而在“有序”。要有序,就必须先回答三个问题:工具从哪来、别人怎么找我、我沉淀下来的能力怎么复用。MCP、A2A、Skills 恰好分别回答了这三个问题。
1.1 单 Agent 的瓶颈:不是模型不够强,是结构不可扩展
先说单 Agent 的问题。一个 Agent 把目标、上下文、工具列表全部塞进提示词里,模型自己决定调哪个工具、怎么调。这种方式跑 Demo 没问题,一旦上生产就会撞墙:工具越来越多,提示词越来越长,模型选择工具的准确率开始下降;某个工具的输入输出格式调整了,你得去翻提示词里所有相关描述;想让另一个 Agent 复用这套能力,只能把整段提示词复制过去,之后两边各自维护,改一处漏一处。
我管这种状态叫“提示词的循环”,你改提示词、跑一次、看结果、再改提示词,Agent 只是一个被提示词驱动的函数,谈不上编排。真正需要多智能体架构的场景,是同一个系统里存在多种角色、多个职责边界、多种外部依赖,比如客服入口、订单查询、翻译、内容审核、回复生成,每个角色有独立的上下文和工作流,彼此还需要交换中间结果。这种情况下,单 Agent 的“一把梭”结构是撑不住的。
1.2 三个协议的分工:工具层、沟通层、能力层
我把这套集群分成三层,每一层都有对应的协议载体:
- 工具层用 MCP(Model Context Protocol):负责把外部工具标准化接入。Agent 不需要关心工具是 Java 写的还是 Python 写的,也不需要关心它在哪台机器上,只要通过 MCP 暴露能力,Agent 就能像插 U 盘一样把它用起来。
- 沟通层用 A2A(Agent2Agent):负责 Agent 之间的对话规则。包括怎么声明自己的能力、怎么发起一个任务、怎么同步任务状态、怎么交付结果。
- 能力层用 Skills:负责把“提示词 + 处理流程 + 辅助脚本”打包成一个可安装、可版本化、可共享的技能包。Skills 本身不一定走网络协议,更像是一个按需加载的本地能力库。
举一个生活化的类比:MCP 是统一插座和插头规格,A2A 是两个同事之间约定好的工作交接流程,Skills 是每个人身上的专业技能包。没有统一插头,每接一个新设备都得改线路;没有交接流程,两个同事只能靠默契,一换人就全乱;没有技能包,一个人的经验永远只存在他脑子里。
1.3 “可编排、可互通、可扩展”的最终检验标准
这三个词不能停留在概念层面,每条都得有可以验收的标准。
可编排意味着:任意一个任务丢进系统,你能说清楚它由谁处理、用了哪些工具、按什么顺序执行、当前卡在哪个环节。可互通意味着:新加入一个 Agent,不需要改已有 Agent 的代码,它对外声明自己会什么,别人就能找到它并给它派活。可扩展意味着:想给系统增加一个新能力,不用改主程序,塞一个 MCP Server 或一个 Skills 包进去即可,模型侧可以通过能力发现机制自动感知。
后面所有章节都在围绕这三条标准展开。先讲 MCP。
2. MCP:工具接入的“USB 接口”,从写死代码到即插即用
MCP 刚出来那阵子,不少人问我同一个问题:MCP 到底是软件协议还是硬件协议,类似于 USB、PCIe 那种吗?答案是:它是纯软件协议,但它的设计思想确实是照着硬件接口协议抄的作业。USB 解决了不同外设接入电脑的乱象,MCP 解决的是不同工具接入 Agent 的乱象。
2.1 MCP 的架构模型:Agent 是主机,工具是外围设备
没有 MCP 之前,每接一个工具,你都要为 Agent 写一份适配代码。接 GitHub 写一套,接数据库写一套,接内部订单系统再写一套。最痛苦的是这些对接逻辑散落在各个 Agent 的代码里,A 项目里写了一版,B 项目里又复制了一版,两边对接方式稍微不一样,后面维护起来就是灾难。
MCP 把架构改成了标准的客户端-服务器模型。Agent 那一侧是 MCP Client,工具那一侧是 MCP Server。Agent 通过 MCP 协议发现 Server 上注册了哪些工具、每个工具的参数结构是什么样的,然后像调用本地函数一样调用远程工具。所有对接细节被收进 MCP Server 内部,Agent 侧不再感知工具的具体实现。
MCP 协议里三块核心能力:
- Tools(工具):可执行的函数,Agent 按需调用。比如
query_order_status(order_id)。 - Resources(资源):可读取的数据对象,类似 REST API 里的资源。比如
order://{order_id}。 - Prompts(提示模板):可复用的提示词模板,供 Agent 按场景加载。
传输层最常见的是 stdio 和 HTTP。stdio 适合本地进程调试,HTTP 适合远程部署,后面讲部署时会详细说这条路怎么选。
2.2 写一个真实可跑的 MCP Server
直接上代码。下面这个用 Python FastMCP 写的 MCP Server,暴露了一个订单查询工具和一个订单详情资源:
from fastmcp import FastMCP mcp = FastMCP("order-tools") @mcp.tool() def query_order_status(order_id: str) -> str: """查询订单当前状态,返回状态描述文本。""" # 这里替换成真实的订单服务调用 return f"订单 {order_id} 当前状态:已发货,物流单号 SF1234567890" @mcp.resource("order://{order_id}") def get_order_detail(order_id: str) -> str: """订单详情资源,返回订单基础信息。""" return f"订单 {order_id} 详情:商品A x1,收货地址:..., 下单时间:..." if __name__ == "__main__": mcp.run(transport="streamable-http")把这段代码跑起来,一个 MCP Server 就诞生了。然后用 MCP Client 连接它,Agent 就能看到query_order_status和order_detail两个能力入口。不同版本的 FastMCP API 略有差异,以官方文档为准,但结构就是这样一个:注册函数、声明入参和描述、跑服务。
这里有个特别容易被忽略的点:工具描述决定了模型会不会用它。模型靠description判断这个工具是干什么的、什么时候调用,描述写得含糊,比如“查询信息”,模型很可能在错误场景下调用它或者在正确场景下漏掉它。写描述要像写 API 文档一样,明确触发条件和输入输出。我见过不少团队在 MCP Server 上花了很多功夫,最后效果不行,一查发现工具描述全是“查询订单”“获取数据”这类词不达意的句子。
2.3 从“能用”到“扛得住”:并发、超时与可观测性
很多人关心“AI Agent 怎么扛并发”,这个问题的答案可能和直觉相反:并发压力根本不在模型推理那边,而在工具侧和调度侧。模型推理服务有独立的限流机制,Agent 应用层能做的优化空间不大;真正容易被冲垮的是 MCP Server——每个工具调用都是一次真实的业务操作,可能是查数据库、调外部 API、执行一个本地脚本。所以并发问题,首先要解决的是 MCP Server 的稳定性。
我的实践原则有这么几条:
- MCP Server 无状态化。不要把会话状态或者临时数据存在 Server 进程里,否则水平扩展时状态对不上,调用直接乱套。需要状态就放 Redis 这类外部存储。
- 工具调用必须是幂等的,至少查询类工具要支持重复调用。因为 Agent 在一次任务里完全可能重复调用同一个工具,如果每次调用都有副作用,结果就是脏数据满天飞。
- 超时和熔断必须做。模型侧等待工具返回是有耐心上限的,一个工具 30 秒不返回,整个任务就卡死了。给每个工具调用设置明确超时,外部依赖异常时快速失败,不要让请求挂在连接池里空等。
- 优先用 HTTP 传输,容器编排下别用 stdio。stdio 是一个进程对应一个连接,容器里想水平扩展非常别扭;用 HTTP 可以把 MCP Server 当成普通服务挂在网关后,拉几个副本就拉几个副本。
可观测性这块我会在第 5 章集中展开,这里先埋一个点:每个工具调用都要记录入参摘要、出参摘要、耗时和错误码,这是后面做任务追踪的基础。
3. A2A:Agent 之间真正意义上的“对话协议”,而不是互相扔 JSON
MCP 解决的是 Agent 和工具之间的连接。那 Agent 和 Agent 之间呢?最朴素的方案是:把对方也当成一个 MCP 工具来调用。这样做 Demo 确实可行,但一旦场景复杂起来就会暴露问题。
3.1 为什么不能直接把下游 Agent 当作 MCP 工具来调
如果只把一个 Agent 包成一个 MCP 工具,调用方和接收方之间的关系是请求-响应式的:调用方传参数,接收方计算完返回结果。这套模型有一个致命缺陷——没有任务生命周期。接收方干到一半需要用户补充信息怎么办?干了两分钟没干完,调用方怎么知道进度?中途发现任务没法完成,怎么取消?这些在 MCP 的工具模型里都没有原生的表达方式。
Agent 之间的协作,本质上是一个长期运行的过程。比如内容生成 Agent 把一篇文档丢给翻译 Agent,翻译 Agent 可能要跑几分钟,期间进度在变,还可能中途卡住要确认术语表。这种场景需要的不只是“传一个函数进去拿到返回值”,而是一个任务对象:有唯一 ID,有状态流转,有进度通知,有结果交付。A2A 协议补的正是这一块。
3.2 AgentCard:让每个 Agent 自己讲清楚“我会什么、怎么找我”
A2A 协议里有个核心概念叫 AgentCard,本质是一份 JSON 格式的能力声明,每个 Agent 把它放在一个固定 URL 上对外暴露。别的 Agent 想看你会什么,就 GET 一下这个地址。
{ "name": "translator-agent", "description": "中英互译 Agent,支持 Markdown 文档翻译和术语表定制", "url": "https://agent.example.com/a2a", "version": "1.0.0", "capabilities": { "streaming": true, "pushNotifications": true, "stateTransitionLogging": true }, "skills": [ "translate::zh2en", "translate::en2zh" ], "defaultInputModes": ["text/markdown", "text/plain"], "defaultOutputModes": ["text/markdown", "text/plain"] }这份卡片的信息量很大:name和description用于能力发现;skills字段声明它掌握哪些技能包;capabilities声明它支不支持流式输出、支不支持回调通知。上游 Agent 拿到这份卡片,就能判断“这个任务能不能交给它”,不需要提前硬编码对方的存在。
我见过很多团队做 Agent 发现时用“配置文件里写死下游地址”的方案。那只能叫静态路由,不是互通。互通的意思是:新 Agent 上线,只要把自己的 AgentCard 注册到目录里,其他 Agent 就能通过能力搜索找到它,然后把匹配的任务委托过去。如果你用了 Spring AI 那套 Java 技术栈,A2A 也有对应的适配模块,把 Agent 类写好,框架能自动生成并发布 AgentCard,省去手动维护 JSON 的麻烦。
3.3 Task 生命周期:一次翻译委托的实际消息流
A2A 里任务叫做 Task,核心状态包括submitted、working、input-required、completed、failed、canceled。一次翻译委托的完整流程是这样的:
- 内容生成 Agent 给翻译 Agent 发一个任务,消息体里带 Task ID、源文本、目标语言。
- 翻译 Agent 立即返回
working状态,并附带当前进度,比如“正在处理第三章”。 - 翻译过程中如果想确认术语,它可以返回
input-required,把问题抛给上游。 - 翻译完成后,返回
completed,并把翻译结果作为 artifact 附在消息里。
{ "id": "task-1234", "sender": "content-writer-agent", "recipients": ["translator-agent"], "status": { "state": "working", "progress": 50, "message": "正在翻译第三章" }, "parts": [ { "kind": "artifact", "artifact": { "name": "translated_doc.md", "contents": "# 第三章..." } } ] }远程调用时,A2A 通常走 HTTP + 流式响应或者 webhook 回调。不管走哪种方式,核心都是这个任务状态机。我强烈建议所有 Agent 间协作都显式建模这个状态机,而不是让上游 Agent 阻塞地等待一个长连接。阻塞等一个可能跑几分钟的任务,一旦网络抖动整个链路就断了。
3.4 和 MCP 的边界:什么该走 MCP,什么该走 A2A
判断标准很直接:如果调用方需要的是一个明确的数据或者一个短操作,比如查个订单、拉个天气、给某个系统写一条记录,走 MCP。如果调用方需要把一个完整任务委托出去,还要追踪状态、拿中间进度、处理中途交互,走 A2A。
我自己习惯用一句话来卡边界:MCP 像你在命令行里敲一条指令,A2A 像你给同事发了一条工单。指令式的调用,适合工具;工单式的协作,适合 Agent。把这条边界划清楚,后面做编排的时候就不会纠结路由逻辑了。
4. Skills:把“调教出来的能力”变成可复用的技能包
协议把工具和 Agent 串起来了,但还有一个问题没解决:那些没法用网络接口表达的能力怎么办?比如代码审查的检查清单、前端组件的生成规范、文档翻译的术语表策略。这些东西本质上是一套“提示词 + 处理流程 + 辅助脚本”,过去只能写进某个 Agent 的提示词里,换一个 Agent 就要复制一遍。Skills 解决的正是这个“能力复用”问题。
4.1 Skills 跟 Prompt、Tool 的区别
很多初学者分不清三者的关系,我打个比方:
- Prompt 是一段话,告诉 Agent “你要注意什么”,但 Agent 记不记得住、执行不执行全凭运气。
- Tool 是一个函数,输入输出严格定义,适合确定性操作,但没法承载复杂流程。
- Skills 是一个能力包,里面既有指令性的 SKILL.md(相当于说明书),又有处理流程、检查清单、辅助脚本,Agent 在需要时加载它,按里面定义的步骤去做。
Skills 相比 Prompt 最大的优势是结构化。一份 SKILL.md 有 YAML 格式的元信息,声明这个技能的名字、描述、期望输入输出,正文部分把处理流程拆成一二三四步。模型加载后不是“参考一下”,而是“照着流程执行”。
4.2 写一个“前端开发 Skills”的能力包
前端开发类 Skills 现在特别多,正好拿它举例。假设你要做一个技能包,让 Agent 负责生成新组件代码,同时保证代码风格和可访问性达标。目录结构大致是这样:
frontend-dev/ ├── SKILL.md └── scripts/ ├── check_a11y.py └── generate_component.pySKILL.md 的核心内容:
--- name: frontend-dev description: 负责 Vue/React 组件生成与代码审查,强制检查可访问性和样式规范。 inputs: - request: 用户要求的组件描述,包含功能点和技术栈 - styleGuide: 可选的团队样式规范文件路径 outputs: - component_code: 生成的组件代码文件 - review_report.md: 代码审查报告 version: 1.1.0 --- ## 处理流程 1. 解析用户请求,确认技术栈(React/Vue)和组件类型。 2. 从团队规范目录加载 styleGuide(如果提供)。 3. 生成组件代码,代码内必须包含 aria 属性。 4. 调用 scripts/check_a11y.py 检查可访问性,不通过则修改。 5. 输出组件代码并生成 review_report.md。这份 SKILL.md 看起来简单,但里面有三个设计细节很关键:
- 输入输出写清楚。模型拿到技能包后知道自己该给什么、该产出什么,不会答非所问。
- 强制检查脚本。Skills 不能只靠提示词约束,最好自带可执行的校验脚本,把“检查可访问性”这种话变成实际跑的代码。
- 版本号。团队里多人维护技能包时,版本号能避免模型加载到过期版本。
类似地,“代码审查 Skills”“文档翻译 Skills”“论文写作 Skills”都能按这个模式拆:声明适用场景,定义输入输出,把处理流程拆成步骤,配上可执行脚本。Skills 的最佳实践不是“写一条高质量的提示词”,而是“设计一个有交付标准的流水线”。
4.3 Skills 目录怎么管理:版本、发现、权限
在单机工具里,Skills 一般放在某个约定目录下,像 Codex、Claude Code 这类编码 Agent 已经把这个机制做成一等公民了,开发者把自己的 skills 目录放进去,Agent 会自动读取。但到了多 Agent 集群场景,Skills 不能只靠“每个 Agent 本地放一份”,要有统一管理:
- 统一注册中心。所有技能包集中存放,Agent 启动或任务需要时按名称拉取。相关技能包支持从本地目录、对象存储或 Git 仓库加载。
- 按需加载。不要把所有 SKILL.md 塞进你的主提示词,模型上下文不够用,也会产生“工具选择困难”。任务匹配到某个技能后,再把对应技能包内容注入上下文。
- 权限分级。不是所有技能包都应该被任何 Agent 执行,包含写数据库、发通知这类敏感操作的技能包要限制可调用的 Agent,否则一个入口 Agent 被提示词注入攻击,整个集群的能力都可能被滥用。
5. DeepAgents 编排层:从任务分解到状态机的一整套调度内核
到这里,工具、通信、能力三块积木都已经有了。那谁来决定怎么组合?这就是编排层(DeepAgents 编排内核)的工作。我理解的 DeepAgents,核心不是某个具体框架,而是一套调度设计路线:把任务分解、Agent 路由、工具调用、A2A 委托、状态追踪、结果汇聚统一成一个可解释的调度循环。下面拆开讲。
5.1 编排器到底在编排什么:任务计划与路由决策
编排器的输入是用户目标,输出是一个完整的任务链路。比如用户说“帮我处理一下这个英文订单投诉”,编排器要回答几个问题:
- 这个任务需要哪些 Agent 参与?(客服入口 Agent、翻译 Agent、订单查询 Worker)
- 每个 Agent 需要用哪些工具?(MCP 订单工具、A2A 翻译任务)
- 执行顺序是什么?(先翻译,再查订单,最后生成回复)
- 某个 Agent 失败了怎么办?(重试、降级、换 Agent)
实际操作中,我不建议把所有编排逻辑都交给模型做“自由发挥”。模型自由度太高,链路就不可控,出问题根本没法复盘。我习惯用“规则 + 模型”的混合模式:先用规则做粗路由,根据任务类型和 Agent 的能力声明把任务分给对应的 Agent;再用模型做细编排,让模型在粗路由的框架内决定调用哪些工具、生成什么内容。
5.2 调度循环的最小实现:状态机、工具调用与 A2A 委托
核心调度循环听起来很高大上,拆到底就是一个 while 循环加一个状态机。大概是这样:
from enum import Enum, auto class TaskState(Enum): PENDING = auto() DISPATCHED = auto() RUNNING = auto() WAITING_A2A = auto() COMPLETED = auto() FAILED = auto() CANCELLED = auto() class Orchestrator: def __init__(self, registry, mcp_client, a2a_client, skill_registry): self.registry = registry # Agent 注册表 self.mcp_client = mcp_client # MCP 客户端 self.a2a_client = a2a_client # A2A 客户端 self.skill_registry = skill_registry # 技能注册表 def run_task(self, task): agent = self.registry.discover(task) # 路由:找到负责的 Agent tools = self.mcp_client.list_tools(agent.mcp_server) skills = self.skill_registry.get(agent.required_skills) while task.state != TaskState.COMPLETED: response = model.call( system=agent.prompt, tools=tools, skills=skills, history=task.trace, ) for action in response.actions: if action.type == "tool_call": result = self.mcp_client.invoke(action.name, action.args) task.trace.append(("tool", action.name, result)) elif action.type == "a2a_delegate": subtask = self.a2a_client.delegate(action.target_agent, action.params) task.state = TaskState.WAITING_A2A self.a2a_client.wait_subtask(subtask.id) task.trace.append(("a2a", action.target_agent, subtask.result)) elif action.type == "complete": task.output = action.output task.state = TaskState.COMPLETED这个伪代码已经把编排内核的骨架画出来了。现实项目里会比这个复杂,比如要支持多个子任务并行、要接消息队列做异步调度、要处理重试和超时,但骨架就是这三件事的循环:模型决策、执行动作、记录轨迹。
有个经验值得单独说一下:子任务执行不要用阻塞等待。上面的代码里wait_subtask是阻塞的,新手容易这么写,但一旦下游 Agent 处理时间超过链路容忍度,整个编排循环就卡住了。更稳的做法是把等待做成事件驱动:子任务完成时通过回调或者轮询把结果写入任务队列,编排器空闲时继续消费。这样编排器永远在处理“当前能推进的事”,而不是干等一个慢任务。
5.3 可观测性设计:把三块协议串成一条追踪链
没有可观测性的多 Agent 集群,等于在工地摸黑施工,出问题连哪个环节断的都不知道。我吃过这个亏:早期系统没有统一追踪,一个跨三个 Agent 的任务失败了,只能看到一堆日志碎片,定位花了一个下午。后来我强制所有环节都带trace_id,每个动作都落一条结构化日志,问题定位时间从小时级压到了分钟级。
具体在每个环节要记录什么,我整理了一张表:
| 环节 | 必须记录的信息 | 建议保留时间 |
|---|---|---|
| MCP 工具调用 | 工具名、入参摘要、出参摘要、耗时、错误码 | 30 天 |
| A2A 委托 | 任务 ID、发送方、接收方、状态流转、每次消息的 part 类型 | 90 天 |
| Skills 加载 | 技能名、版本、加载时间、关键输出文件 | 30 天 |
| 模型调用 | 模型名、输入 token 数、输出 token 数、耗时、是否重试 | 30 天 |
还要强调一点:日志摘要要做脱敏。入参摘要不等于入参全量打印,订单号、姓名、地址这些字段要么截断要么打码,不然可观测性做成了数据泄露,那就得不偿失了。
6. 组装一台超级集群:一个跨 Agent 订单售后任务的完整链路
前面四章分别讲了零件,现在我们来把整台机器装起来。用一个我实际搭过的“订单售后”场景来走一遍端到端链路,你会看到 MCP、A2A、Skills、编排器是怎么协作的。
6.1 集群拓扑:从入口到工具的四层角色划分
这个集群由几个独立服务组成:
- 客户入口 Agent(Gateway Agent):接收用户提交的投诉工单,判断工单需要哪些处理。它自己是编排器直接管理的 Agent。
- 翻译 Agent(Translator Agent):负责中英文转换,通过 A2A 被入口 Agent 委托任务。
- 订单查询 Worker(Order Worker):通过 MCP Server 连接订单系统和物流系统,处理订单状态查询。
- 回复生成 Agent(Reply Agent):聚合所有中间结果,生成给用户的最终回复。
- 编排器(Orchestrator):串联所有角色,维护每个任务的统一状态机。
- MCP Server 集群(订单/物流/工单):每个业务系统一个 MCP Server,独立部署。
6.2 走一遍链路:从用户投诉到自动回复的全过程
假设用户提交了一条中英混合的投诉:“我的订单 #2024001 收到两周了还没发货,Please help.”
第一步:入口 Agent 接收工单,拆解任务。编排器给这个任务分配一个trace_id=TRACE-001,入口 Agent 发现文本里包含英文和订单号,判断需要两件事:查订单状态、翻译用户描述。
第二步:委托翻译 Agent(A2A 链路)。入口 Agent 通过 A2A 给翻译 Agent 发一个任务,内容是“翻译这段投诉文本,保留订单号不动”,翻译 Agent 返回working,业务方无需干等。这段时间里,入口 Agent 可以同步去查订单状态。
第三步:查询订单状态(MCP 链路)。订单 Worker 通过 MCP Server 调用query_order_status(order_id="2024001"),拿到订单状态“已支付,待发货”。这一步的入参、耗时、出参都会以trace_id=TRACE-001写入日志。
第四步:聚合结果,生成回复。翻译 Agent 返回翻译文本,订单 Worker 返回订单状态,回复生成 Agent 按“道歉 + 说明订单状态 + 给出预计处理时间”的回复模板生成最终答复,交付给用户。
整个链路里,编排器始终握着每个子任务的状态:入口 Agent 正在做什么、翻译任务到哪一步了、MCP 工具调用成功没有。任何一个环节出问题,都能通过trace_id把整条链路的日志捞出来。
6.3 部署形态与技术栈参考
部署的时候,我建议把职责拆到进程级别,不要搞一个超级进程把所有逻辑塞进去:
- 编排器:独立服务,负责调度和状态管理,可以水平扩展,但要注意状态存储统一放 Redis。
- MCP Server:每个业务域一个独立服务,挂在网关后,用 HTTP 传输对外暴露。
- Agent 服务:按角色拆成独立部署单元,可以共享同一份大模型 API,但上下文隔离。
- Skills 目录:放对象存储或 Git 仓库,Agent 启动时按需拉取,本地做缓存。
技术栈上,Python 生态用 FastAPI + FastMCP + A2A SDK(不同语言的 A2A 库逐步成熟了);Java 生态可以用 Spring AI 的 A2A 模块快速生成 AgentCard,MCP Server SDK 也有官方 Java 版。选择的原则是“团队熟哪个用哪个”,协议是语言无关的,不要被某一栈绑死。
6.4 一个真实故障的排查全程:A2A 回调地址指向 127.0.0.1
这套架构上线后我遇到过一个非常典型的故障,值得单独拿出来复盘。现象是多 Agent 协作任务大部分超时,报错信息只有一行“callback connection refused”。查日志发现,翻译 Agent 完成翻译后要回传completed消息给上游 Agent,但回调地址写成了http://127.0.0.1:8000/a2a/callback。
根因分析:翻译 Agent 部署在另一个容器里,这个容器里的 “127.0.0.1” 是它自己,不是上游 Agent(入口 Agent 所在容器)的地址。由于我没有建立服务发现机制,A2A 回调地址被写死,跨容器环境直接失联。
修复方案:把回调地址改成一个可解析的服务名(例如http://gateway-agent:8000/a2a/callback),或者统一走网关转发。同时,在 A2A 消息结构里强制校验“回调地址是不是服务注册中心里的合法地址”,从配置上杜绝写死 IP。
这个故障的价值在于:A2A 协议本身解决的是 Agent 之间的消息格式和任务状态,但网络层的服务发现与地址解析是你自己去解决的,协议不负责帮你找到对方。集群里所有 Agent 的对外地址,都应该是注册中心里的逻辑名,而不是某个容器或机器的 IP。
7. 落地两年后的复盘:哪些设计值得坚持,哪些坑早晚会踩
跟多智能体集群打了两年交道,有一些原则我现在看是值得坚持的,也有一些坑是团队大概率会遇到的,总结在这里,算是给同行的一些参考。
7.1 三条值得坚持的工程原则
第一条,协议先行,而不是 Agent 先行。很多人立项第一件事是“我要开发一个 Agent”,结果 Agent 写完了才发现没法跟已有的系统对接。正确的顺序是先定好 MCP 工具边界、A2A 通信规则、Skills 仓库规范,再去开发具体的 Agent。协议是骨架,Agent 是挂在上面的肉。
第二条,先跑通一条端到端链路,再加花活。我见过太多团队一上来就搭五六个 Agent,结果链路迟迟没跑通,成了一个庞大的玩具。我的建议是:先一个编排器加两个 Agent 加一个 MCP 工具,把“用户提问 -> 工具调用 -> 结果生成”这条主链路跑顺了,再加并行、再加深度编排。复杂度应该由业务需求驱动,不是由架构冲动驱动。
第三条,可观测性从第一天就做,不然后补全是地狱。多 Agent 的链路过一次会跨好几个服务,想之后再加追踪会非常痛苦,因为涉及大量历史代码改造。从第一个任务开始就带上trace_id,把每个环节的日志结构化落地,后面排障节省的时间远超写日志的成本。
7.2 早晚会踩的四个坑
- MCP Server 进程成为性能瓶颈。工具调用越来越频繁,MCP Server 单实例撑不住,但你没有及时做水平扩展。判断标志:工具调用 P99 延迟升高、日志里反复出现超时。解决方法是把工具服务拆成可独立扩容的部署单元。
- A2A 循环委托导致的对话风暴。下游 Agent 遇到处理不了的问题又委托给别的 Agent,被委托方再委托回来,形成无限循环,白白消耗 token。一定要在编排器里配任务深度上限和循环检测,超过预设层数直接置失败。
- Skills 版本过期。模型读了旧版技能包,按旧流程干活,输出风格和内容跟团队当下规范不一致。统一技能注册中心并加版本锁,是治这个问题最有效的办法。
- 并发压力全堆在模型推理 API 上。为了“扛并发”拼命给一个大模型 API 加并发,结果限流更严重,成本也上去了。正确的思路是让编排器做并发控制,不同任务尽量复用工具结果(比如同一订单的查询),减少重复的模型往返调用。
7.3 给刚起步团队的具体建议
如果你所在团队准备上手这套架构,我的建议是别急着把体系铺满。先选一个业务边界清晰、重复性高的场景,比如“工单分类 + 自动回复生成”或者“代码审查 + 规范检查”,把 MCP、A2A、Skills 都小规模用起来,跑通一两个真实流程后,再逐步覆盖更多场景。
技术选型上也不要过度设计。如果你现在只是单机、单体应用、一个模型一把梭能解决问题,那就没必要上多 Agent;只有当任务流程确实需要多个职责分明的角色协作、并且这个协作关系会长期存在时,这套架构的投入才划算。
7.4 我最后想单独提的一件事
如果你去追各种“超级多智能体”的概念,会发现本质上没有互不相容的新东西,它们是在不同抽象层级解决不同问题:MCP 管工具,A2A 管协作,Skills 管能力复用,编排器管调度。这个分层理念,值得任何一个准备做多 Agent 系统的团队认真参考。我现在可以在这套架构上加新的 Agent,基本上只做三件事:写一个 AgentCard、配一组 MCP 工具、塞一个技能包,注册进来就能被调度,这才是“可扩展”三个字真正落地时的样子。