
我一直觉得多智能体系统在过去几年里一直处在“人人都在谈谁都不敢用”的状态。不是概念不行而是太散了有人只做消息转发有人只做工具回调有人把所有逻辑堆在一个巨大状态机里最后谁也编排不了谁根本谈不上集群。直到MCP、A2A、Skills这一套东西慢慢成熟加上DeepAgents这种面向编排的框架落地才真正有了把Agent变成“可编排、可互通、可扩展”的工程化路径。这篇文章就是我基于这套技术栈从零搭一个超级多智能体集群的完整记录包括架构思路、协议细节、实操步骤和我在真实项目中踩过的坑希望能给正在做Agent工程化的朋友一些参考。1. 整体设计与思路拆解从“单体智能”转向“集群智能”1.1 单体Agent到底卡在哪先聊聊为什么要从单体Agent迁移出来。很多人做Agent的第一版都是一个Agent接一堆工具LLM负责意图识别工具负责执行。这个模式在Demo阶段跑得通一旦进入生产环境就难受了工具越来越多上下文越来越长prompt怎么都调不干净不同工具之间的状态互相干扰A工具写了个临时文件B工具不知道直接撞车更别说团队协作你写你的Agent我写我的Agent互不通信最后只能用一个巨无霸网关硬塞到一起。我自己经历过最尴尬的一次一个Agent要处理“查询库存 → 生成采购单 → 发送审批”这条链路三个步骤拆成了三个独立Agent结果没有统一协议只能靠共享数据库外加轮询来“协作”哪天有个Agent挂了整条链就卡死排查半天才发现是数据库锁表。说白了单体Agent的问题不是智商不够是工程边界模糊。分工不清晰上下文不隔离扩展能力全靠堆prompt这条路走不长。1.2 为什么是DeepAgentsMCPA2ASkills这四个词组合这个组合不是拍脑袋拼在一起的它恰好把多智能体集群的四个核心问题都覆盖了Skills解决的是“能力怎么定义”把Agent的某项技能比如翻译、代码审查、数据分析封装成可复用、可加载的独立包而不是把逻辑写死在prompt里。MCPModel Context Protocol解决的是“Agent怎么用工具”用一个标准化协议把外部工具、数据源接入Agent相当于给Agent装了一个USB接口什么设备都能插。A2AAgent-to-Agent解决的是“Agent之间怎么通信”让不同的Agent能够互相发现、互相调用、互相协商而不是靠人写死调用链。DeepAgents解决的是“整个集群怎么编排”提供任务拆解、调度、状态管理、失败重试的框架层能力把上面三门技术组织成一个整体。你可以这么理解MCP是Agent的手和脚Skills是大脑里的经验A2A是嘴和耳朵DeepAgents是神经中枢。没有神经中枢手和嘴再多也是一盘散沙。1.3 我选择这套方案时对比过的其他路线做技术选型的时候我也认真评估过其他方向。一种是以LangChain为代表的“链式编排”它的思路是把Agent流程写成DAG图节点是工具调用边是数据流。优点是上手快缺点是无法处理动态任务——任务数量不确定、执行顺序不确定时DAG图根本画不出来。另一种是纯代码编排比如直接用Python写一个调度器把所有Agent抽象成函数来调这个方式可控性强但Agent之间的发现和协商机制全部要自己造轮子工作量太大。最后选了DeepAgents这套组合核心原因有三点首先是协议标准化MCP和A2A都有相对明确的规范社区生态也起来得快不用担心自己造的标准没人认其次是分层清晰Skills管能力、MCP管工具、A2A管通信、DeepAgents管编排各层职责不会重叠最后是演进成本低未来出现更好的LLM或者新的工具类型只需要替换对应层不会牵一发而动全身。2. MCP把工具变成Agent的“即插即用”能力2.1 MCP到底解决什么问题先看一个最朴素的需求我想要让我的Agent能查数据库能调内部API能读取某个文件夹里的文件。在没有MCP之前你需要针对每个数据源写一段特定的调用代码然后把这段代码的说明写进prompt里Agent通过“猜测”来决定要不要调用。这种方式的问题很明显代码写死了Agent换一个就废工具升级了prompt忘了同步Agent还在调用老接口更麻烦的是安全控制你能让Agent调用什么不能调用什么全部散落在prompt里根本没法审计。MCP的思路是把工具调用抽象成一个“客户端-服务器”模型。Agent这边是MCP Client负责发起工具调用请求工具那边是MCP Server负责接收请求、执行操作、返回结果。两者之间用统一的JSON-RPC格式通信所有的工具都以“resource、tool、prompt”三种原语暴露。Agent不需要知道工具背后的实现细节只需要知道“有个工具叫search_stock_price参数是code”就能直接调用。2.2 MCP的架构和工作流程我习惯把一个MCP系统的核心组件拆成三层来看传输层主要是HTTP和Stdio两种。HTTP适合跨网络调用比如Agent部署在一个服务上工具跑在另一台机器上Stdio适合本地进程通信比如直接把一个Python脚本作为MCP Server跑起来Agent通过标准输入输出和它交互。我在实际项目中本地调试用Stdio生产部署用HTTP各有各的优势。协议层遵循JSON-RPC 2.0规范。协议里有几个关键方法initialize负责握手交换版本号和能力tools/list返回该服务器支持的工具清单tools/call执行具体的工具调用resources/list和resources/read用来暴露非工具类的资源数据比如一个配置文件或者一段文档。服务层是具体实现。你的MCP Server可以承载一个工具也可以承载几十个工具它们共享一个生命周期。比如我写过一个小型MCP Server同时暴露了“读取数据库表结构”和“执行SQL查询”两个工具这样就避免了为每个工具各开一个进程的资源浪费。下面是一个用Python FastMCP快速实现MCP Server的实例这段代码我是在一个内部数据查询项目里实际用过的from fastmcp import FastMCP mcp FastMCP(namedata-query-server) mcp.tool() def query_stock(code: str, days: int 30) - list[dict]: 查询股票最近N天的行情数据 # 这里省略具体的数据获取逻辑 return [ {date: 2024-01-01, close: 12.5}, {date: 2024-01-02, close: 12.8}, ] if __name__ __main__: mcp.run(transporthttp, host0.0.0.0, port8000)核心逻辑就是把一个普通Python函数包装成ToolFastMCP负责处理协议层的事我只要关心函数实现。这个server跑起来之后任何支持MCP的Agent客户端都能连接到它然后通过tools/list看到query_stock这个工具进而调用。整个过程不需要客户端做任何专用适配。2.3 我在MCP接入过程中学到的几条经验MCP本身不复杂真正麻烦的是接入过程中的工程细节。我把几条踩过的坑整理一下版本的兼容性。MCP规范还在快速迭代不同SDK版本之间的协议握手行为不完全一致。遇到过FastMCP新版客户端连不上旧版远程Server的情况排查半天发现是initialize握手时对protocolVersion的校验太严格。后来统一固定版本才彻底解决。工具描述要写得足够具体。很多Agent框架会让LLM根据工具名和描述决定是否调用这个工具。如果你的描述写得模棱两可比如“获取数据”LLM根本不知道这个工具是干什么的自然就不会调用。我现在的习惯是每个工具描述至少两句话说明用途、适用场景和参数含义。超时和重试机制不能省。工具调用可能因为网络波动、后端故障等问题失败Agent侧必须有完善的超时和重试策略。不要把所有超时都设成同一个值写SQL的工具可能需要30秒查缓存的工具3秒就够了统一超时只会拖慢整体响应。3. A2A让Agent之间学会“说人话”通信3.1 A2A和MCP的本质区别很多人在刚接触A2A的时候会把它和MCP搞混。这里我多说两句MCP是Agent和工具之间的协议解决的是“能力获取”问题A2A是Agent和Agent之间的协议解决的是“协作与通信”问题。打个比方MCP是人和USB设备之间的接口标准而A2A是人与人之间共同使用的语言。两者处在不同的层次互相补充不冲突。A2A协议的核心思想是让Agent之间通过一种标准的“消息”和“任务”机制协作。所谓标准消息就是不管你用的是什么框架、什么模型只要你的Agent能理解和发送符合A2A规范的消息就能加入整个协作网络。这在很大程度上解决了多智能体系统“各自为政”的问题。从实现模式上看A2A有两种主流协作方式客户端-服务器模式一个Agent客户端向另一个Agent服务器发起请求请求中包含明确的“任务描述”服务器执行后返回结果。适合任务边界清晰的场景比如“帮我总结这篇文章”。点对点发布-订阅模式Agent之间通过共享的“黑板”或者消息总线来通信生产者发布消息消费者订阅感兴趣的消息。适合异步、事件驱动的场景比如“当库存低于阈值时通知采购Agent”。我在项目里主要用的是客户端-服务器模式因为现阶段的可编排性更强。DeepAgents可以把一个复杂任务拆解成多个子任务然后分发给不同的Agent每两个Agent之间的交互都是一次明确的请求-响应。3.2 A2A的协议原语和交互流程具体到实现层面A2A协议的交互流程一般是这样的客户端Agent先发送一个message/agent-card请求获取服务器Agent的“能力卡片”AgentCard这个卡片里描述了对端Agent能处理什么任务、支持哪些技能、使用什么认证方式。然后客户端发送任务请求task/send服务器创建任务并返回任务ID。之后客户端可以通过task/get轮询任务状态或者通过Webhook的方式接收异步结果。我在一个舆情分析集群里实际跑过这个流程有一个“采集Agent”负责抓取网页“分析Agent”负责情感分析“报告Agent”负责生成日报。采集Agent做完之后把抓取结果通过A2A的task/send发给分析Agent分析Agent识别到这是一批网页文本就自动执行情感分析完成后把结果再发给报告Agent。整个链路里没有任何人写死调用关系全部是动态发现、动态调用。A2A最吸引我的地方是它把“动态协商”变成了可能。原来写多Agent协作基本靠写死一个函数调用链现在有了A2AAgent之间可以根据能力卡片来决定谁来执行某个任务。比如有多个Agent都能做数据分析那个负载低、技能匹配度高的Agent会被优先选中。这个机制让整个集群有了自组织和自适应的味道。3.3 和MCP配合的典型架构实操中A2A和MCP在一个系统里同时出现是很常见的。我画过一个比较清晰的架构图文字版最底层是各种工具和数据源每个工具上都包了一层MCP Server中间层是一批“执行Agent”它们各自连接自己需要的MCP Server负责执行具体任务再往上一层是一个“协调Agent”它不直接连工具而是通过A2A协议与执行Agent通信派发任务、收集结果最顶层是DeepAgents引擎负责把用户请求拆解为任务图并调度协调Agent。这个层次的关键在于每一层只通过标准协议和相邻层通信层内实现完全自治。我可以在不影响其他层的情况下单独替换某个执行Agent的LLM模型或者单独给某个Agent增加MCP工具整个系统不需要做大的变更。4. Skills把可复用经验做成标准件4.1 Skills在Agent系统里扮演什么角色如果说MCP让Agent有了“即插即用”的工具那Skills就相当于给Agent注入“经验和作业流程”。举个例子单纯的MCP工具“search_code”可以让Agent搜索一段代码但搜索到代码之后怎么分析、怎么定位问题、怎么形成修复建议这整套分析流程就是一个Skills。我最早接触Skills是在Codex和Claude生态里后来在DeepAgents里进一步把它工程化了。Skills的本质是一组结构化的指令和模板通常包括技能描述、适用场景、执行步骤、输入输出规范、示例以及质量检查清单。当Agent发现自己面临的任务匹配某个Skills的描述时就会自动加载这个Skills按照里面的指导来执行。一个关于Skills的常见误解是“Skills就是给Agent加的prompt”。我认为Skills和prompt的关系就像“操作手册”和“一句话要求”的关系。Prompt告诉你“要做什么”Skills告诉你“怎么做、分几步做、每一步的标准是什么”。前者的效果不稳定后者规范了作业过程效果更可控。4.2 一份Skills内部长什么样我拆解一个我实际写过的“代码审查Skills”给你看它的存档结构大概是这样的code-review/ ├── SKILL.md # 主文件包含技能描述与执行步骤 └── templates/ ├── report.md # 输出报告的模板 └── rule-sets/ └── security.md # 安全审查规则集合SKILL.md的写法是Skills效果的核心。我第一次写Skills的时候把它当成笔记来写堆了很多形容词效果很差因为LLM看这种文档根本提取不到可执行步骤。后来我改成强制性结构SKILL.md分成了四个部分第一部分是触发条件明确什么情况下应该使用这个技能。比如“当用户请求中对代码的修改涉及文件写入、网络请求或SQL操作时必须启用安全审查技能”。第二部分是前置检查规定进入技能执行前必须收集哪些信息。比如“必须先获取代码文件路径、涉及的开发语言、已有的测试覆盖情况”。第三部分是执行步骤用编号列表写清楚每一步做什么。比如“第1步检查代码中是否存在硬编码密钥第2步检查SQL语句是否使用参数化查询第3步检查是否缺少输入验证”。第四部分是验收标准说明什么情况下该技能的产出是合格的。比如“报告必须包含风险等级、证据位置、修复建议三要素缺少任何一项即为不合格”。4.3 编写Skills需要避开的坑我在团队里推广Skills时发现最容易犯的错误是“贪多求全”。一个Skills想覆盖所有场景结果每个场景都讲得不够深最后LLM执行起来就是到处踩边界。现在我定了一条铁律一个Skills只解决一个具体问题。比如单独的“数据质量检查Skills”和单独的“敏感信息脱敏Skills”而不是混在一起搞一个“数据处理Skills”。另一个坑是Skills的更新维护跟不上业务变化。很多团队的Skills写完一次就扔在那里几个月后业务逻辑变了Skills还在用旧规则效果自然下降。我现在用DeepAgents内置的Skills注册表来管版本每次更新Skills就自动生成一个新版本号并且可以在调试面板里看到当前Agent实际加载的是哪个版本。这样排查问题时第一件事就是核对版本而不是重新梳理逻辑。5. 实操构建一个最小可运行的Agent集群5.1 环境准备与基础框架搭建现在进入正题我会把这套方案从理论转到实战带你搭一个最小可运行的“网页内容分析与摘要集群”。这个集群包含三个Agent一个负责抓取网页内容一个负责内容分析提炼一个负责生成结构化报告。三个Agent通过A2A通信分析Agent和报告Agent通过MCP连接外部工具。先说环境准备。我建议的运行时是Python 3.11以上Node.js 18以上也会用到具体看你要跑什么框架。我这边使用Python为主MCP Server用FastMCPA2A的通信基于一个轻量的HTTP服务DeepAgents作为编排层。首先安装核心依赖pip install fastmcp uvicorn pip install deepagents # 如果DeepAgents还在快速迭代建议直接用仓库源码安装DeepAgents的定位是编排框架它的核心是任务分解和调度。在DeepAgents里每个Agent被定义为一个“Worker”每个Worker可以拥有自己的Skills和工具访问权限。接下来我们把三个Worker定义出来。5.2 配置MCP Server与Agent的工具接入先写一个MCP Server提供“网页获取”和“文本清洗”两个工具from fastmcp import FastMCP mcp FastMCP(namecontent-tools) mcp.tool() def fetch_url(url: str, timeout: int 10) - str: 拉取指定URL的HTML正文内容返回纯文本 import requests from bs4 import BeautifulSoup resp requests.get(url, timeouttimeout) soup BeautifulSoup(resp.text, html.parser) return soup.get_text(separator\n, stripTrue) mcp.tool() def clean_text(raw_text: str, max_length: int 8000) - str: 清理文本去掉空行、压缩连续空格并限制最大长度 lines [line.strip() for line in raw_text.splitlines() if line.strip()] return .join(lines)[:max_length] if __name__ __main__: mcp.run(transporthttp, port8001)这里涉及到一个工具拆分细节很多人在Agent开发中喜欢把一步操作封装成一个工具结果Agent为了完成一件事要调好几个工具每个工具的上下文都要来回传浪费token还容易出错。我更推荐工具粒度适当放大比如上面这样抓取网页和清洗文本是两个独立工具但是把“最终需要什么”问清楚分析Agent只需要调用一次fetch_url返回的就是能直接使用的文本不需要想“我到底该用哪个工具”。在DeepAgents里注册这个MCP连接大概是这样一份配置from deepagents import Agent analysis_agent Agent( namecontent-analyzer, modelqwen-plus, mcp_servers[{url: http://localhost:8001}], skills[summarization-skill], )这样content-analyzer这个Worker就自动拥有了fetch_url和clean_text两个工具以及加载summarization-skill的能力。5.3 实现A2A互通让三个Agent协作起来三个Agent之间的A2A通信我用一套简单的HTTP包装来实现。核心思路是每个Agent都暴露一个/a2a端点接收符合A2A协议的任务请求。这里给出最小化实现from fastapi import FastAPI, Request import uvicorn app FastAPI() AGENT_HANDLERS {} def register_agent(name, handler): AGENT_HANDLERS[name] handler app.post(/a2a) async def a2a_endpoint(request: Request): payload await request.json() method payload.get(method) agent_name payload.get(agent) handler AGENT_HANDLERS.get(agent_name) if method task/send: return {result: await handler(payload[params])} elif method agent-card: return {skills: list(handler.skills), name: agent_name} return {error: unsupported method}我把采集Agent、分析Agent、报告Agent都注册到这个服务里。用户请求进来后DeepAgents会把任务拆成子任务并按照依赖关系依次分发先发给采集Agent采集Agent拿着任务里的URL列表去调fetch_url拿到纯文本内容后通过A2A的task/send发给分析Agent。分析Agent看到消息里的“content”字段就知道这是待处理的文本于是加载summarization-skill生成一段摘要。摘要在报告Agent里被组装成标准格式。这里有个关键的字段约定A2A通信时消息里必须有一个“任务类型”字段让接收方知道自己该做什么。我用的名字是task_type取值比如fetch_content、analyze_text、generate_report。如果接收方不认识这个task_type它会返回一个明确错误而不会硬着头皮处理然后给出一个无用结果。这个约定让整个集群的协作变得非常可预测。5.4 用DeepAgents编排一个完整任务流最后是DeepAgents的编排定义。我的实际项目里一个完整的任务流是这样的from deepagents import Orchestrator async def report_pipeline(input_data): orchestrator Orchestrator(agents[ fetch_agent, analysis_agent, report_agent, ]) task_graph { nodes: [ {id: fetch, agent: crawler, params: {urls: input_data[urls]}}, {id: analyze, agent: content-analyzer, depends_on: [fetch]}, {id: report, agent: report-builder, depends_on: [analyze]}, ] } return await orchestrator.run(task_graph)这种声明式任务图的好处是依赖关系一目了然而且编排器可以自动从“fetch”这类节点的输出中抽取数据填到“analyze”节点的输入里。我不需要自己写“fetch的结果拿到了就去调analyze”这种胶水代码DeepAgents会把状态保存和传递都处理好。这个最小集群搭建完成后我验证过一个真正的用例给它五个科技新闻的URL让它抓取、提炼要点、最后生成一张Markdown表格包含标题、核心观点、标签三个字段。整个过程大概耗时40秒没有人工干预三个Agent的协作符合预期。跑了十轮没有出现任务丢失的情况。5.5 生产环境部署的几个细节从Demo到生产有几件事必须做第一MCP Server和A2A服务都要用进程守护工具来托管比如systemd或supervisor否则半夜进程挂了没人知道。第二每个Agent都要有独立的日志文件并且日志里必须带请求ID和任务ID排查问题的时候靠这对ID就能串起整个链路。第三编排器的重试机制要配置好A2A调用失败时编排器要能够重新调度而不是直接报错。如果你要把这套集群部署到多台机器上A2A通信需要引入服务发现机制。最简单的方案是用内网DNS给每个Agent一个稳定的主机名再复杂一点可以用Consul或者etcd来做服务注册。我在一个小规模生产环境中用的是Nginx反向代理加路径路由把/a2a/crawler、/a2a/analyzer分别转发到不同的后端服务运维成本很低效果也稳定。6. 常见问题与排查实录6.1 MCP连接失败问题速查MCP相关的问题大头集中在连接和协议版本上汇总成一张速查表能帮你快速定位现象可能原因处理方法客户端初始化失败报“Protocol version mismatch”MCP SDK版本不一致统一客户端和服务端SDK版本重新构建Agent能连上MCP Server但看不到工具Server未执行tools/list注册检查Server日志确认工具装饰器是否生效工具调用超时后端执行时间过长按工具分别设置超时时间长耗时工具用异步返回调用工具时报参数校验错误工具参数Schema与客户端传参不一致用tools/list返回的参数定义重新生成调用代码这里要特别提醒MCP Server的日志非常值得看。我遇到过一个“工具偶尔能用偶尔不行”的问题看日志发现是Server内部的第三方接口限流根本不是协议问题。如果只盯着Agent侧排查永远找不到根因。6.2 A2A通信链路排查思路A2A通信失败时我一般遵循“从外到内”的排查顺序。先确认网络层Agent Agent之间的HTTP请求能不能通DNS解析正不正确。然后看协议层请求里的task_type字段接收方认不认消息格式是不是合法的JSON。最后深入到业务层接收方有没有加载对应的Skills输入的必填字段有没有缺。曾经遇到一个特别隐蔽的坑采集Agent成功抓到了网页发送给分析Agent的时候因为网页原文超过了我设的8000字符限制消息体被截断了。结果分析Agent那边解析JSON时报错整个链路中断。但这个错误只在特定的大页面出现小页面全部正常排查了半天才发现是消息体太大超出了HTTP代理默认的Body大小限制。后来统一在A2A网关层设置了更大的请求体限制并且在发送前对内容做压缩这个问题彻底解决。如果你在推进多智能体集群我的建议是先从最小闭环开始不要一上来就追求几十个Agent的大集群。先跑通两个Agent之间的MCP和A2A稳定之后再加第三个、第四个你会发现整个系统从1到10的扩展过程比从0到1要平滑得多。这套组合拳在一个明确的需求驱动下真的能把多智能体从“概念”变成“生产力”。最后分享一个我自己觉得非常有用的小习惯给每个Agent建一个“能力清单”文件里面记录了这个Agent当前注册了哪些MCP工具、哪些Skills、依赖哪些外部服务。每次调整Agent配置后更新这份清单并在编排器的测试面板里跑一遍冒烟用例。这个习惯已经在好几次系统重构时救了我因为配置一多人的记忆真的靠不住。这个内容后续还可以继续扩展的方向是把A2A通信从客户端-服务器模式延伸到发布-订阅模式让整个集群具备更强的事件驱动能力这样应对突发流量和异步任务时会更加从容。