尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

多智能体协同开发实战:DeepAgents+MCP+A2A+Skills架构全解析

多智能体协同开发实战:DeepAgents+MCP+A2A+Skills架构全解析 多智能体系统这两年从论文里的概念一路杀到工程落地真正动手搭过一套能跑通全流程的集群之后你会发现最难的从来不是让一个Agent变聪明而是让一群Agent别互相打架。我最近用DeepAgents做编排底座配合MCP打通工具层、A2A打通Agent之间的通信层、Skills做能力封装完整跑通了一套多智能体协同开发流程。这套组合不是简单的技术堆叠而是各自解决了一个明确的工程痛点MCP解决Agent怎么用工具A2A解决Agent怎么找AgentSkills解决Agent怎么复用能力DeepAgents解决谁来管这些Agent。下面我把整套架构的设计思路、踩过的坑、以及可直接复现的实操步骤完整拆开讲。1. 为什么单Agent撑不住复杂任务从工具调用到集群协作的必然演进1.1 单Agent的能力天花板在哪里刚开始做Agent开发的时候大多数人的路径是一样的写一个prompt挂几个工具函数跑一个ReAct循环看起来什么都能干。但只要任务稍微复杂一点问题就全暴露出来了。我拿一个真实的场景举例——分析一份财报PDF提取关键财务指标生成可视化图表再写一份分析摘要。单Agent处理这个任务时上下文窗口里要同时塞进PDF解析逻辑、数据清洗规则、图表生成代码、文案撰写模板prompt会膨胀到几千token模型注意力被严重稀释最后每个环节都做得马马虎虎。更致命的是错误传播。单Agent的ReAct循环里如果第三步工具调用返回了脏数据第四步的推理就会基于错误前提继续往下走等到最后发现结果不对你根本不知道是哪一步出的问题。这就像一个人同时干五个岗位的活出了错连责任都分不清。还有一个隐性成本工具描述的token开销。当你给一个Agent挂20个工具时光是工具的schema描述就可能占掉2000-3000 token的上下文。这些token在每一轮推理中都要重复消耗成本高不说还会干扰模型对当前任务的判断——工具越多模型选错工具的概率反而越大。1.2 多智能体拆分的核心逻辑按职责边界而非按功能模块很多人第一次做多Agent拆分时习惯按功能模块切——一个Agent管数据库一个Agent管API一个Agent管前端。这种切法看起来清晰实际跑起来一团糟因为功能模块之间有大量交叉依赖Agent之间的通信会变成一张蜘蛛网。我后来总结出来的原则是按职责边界拆分而不是按功能模块拆分。什么叫职责边界就是每个Agent有独立的决策权和完整的输入输出契约。比如上面那个财报分析任务正确的拆法是解析Agent输入PDF路径输出结构化JSON数据不关心数据怎么用分析Agent输入结构化数据输出分析结论和图表配置不关心数据从哪来撰写Agent输入分析结论输出最终文案不关心分析怎么做的每个Agent只对自己的输入输出负责中间通过标准化的消息格式传递。这样拆分之后每个Agent的prompt可以精简到500 token以内工具数量控制在3-5个推理准确率大幅提升。1.3 DeepAgents在这个架构里扮演什么角色DeepAgents的核心价值是提供了一个有状态的编排层。普通的Agent框架是无状态的——每次调用都是全新的开始Agent不记得上一轮发生了什么。但多智能体协作天然需要状态谁在等谁的结果、哪个任务已经完成、哪个任务失败了需要重试。DeepAgents通过内置的状态管理和任务图机制解决了这个问题。你可以把它理解成一个项目经理它不干具体的活但负责分配任务、跟踪进度、处理异常。每个子Agent就是一个专员只负责自己那一块。这种分工带来的好处是编排逻辑和业务逻辑彻底解耦你可以单独优化某个Agent而不影响整体流程。提示不要一上来就上多Agent。先用单Agent跑通业务闭环确认prompt和工具都没问题之后再根据性能瓶颈决定要不要拆分。我见过太多项目在单Agent还没调好的时候就急着上集群结果调试成本翻了三倍。2. MCP协议把工具层从Agent代码里彻底剥离出来2.1 MCP到底解决了什么工程问题MCPModel Context Protocol刚出来的时候很多人觉得它就是个工具调用的标准化协议没什么新鲜的。但真正在多Agent场景下用过之后你会发现它的价值远不止标准化。在没有MCP之前每个Agent要调用工具都得在代码里硬编码工具的实现。这意味着如果你有5个Agent都需要查数据库你得在5个地方写5遍数据库连接代码。更麻烦的是当数据库连接参数变了你得改5个地方。这种耦合在单Agent时代还能忍到了多Agent时代就是灾难。MCP的做法是把工具实现从Agent代码里彻底抽出来变成一个独立的MCP Server。Agent通过标准协议跟Server通信不关心工具怎么实现的。这带来的直接好处是工具复用一个MCP Server可以被任意多个Agent调用独立部署工具更新不需要重新部署Agent权限隔离不同Agent可以连接不同的MCP Server实现细粒度权限控制2.2 MCP Server的三种传输模式与选型建议MCP Server目前支持三种传输模式选错了会直接影响性能和稳定性传输模式适用场景优点缺点stdio本地工具、开发调试零网络开销、启动简单无法跨机器、进程管理麻烦SSE远程工具、需要推送支持服务端主动推送连接不稳定、需要心跳保活Streamable HTTP生产环境、高并发稳定、支持流式、易扩展实现复杂度略高我的选型经验是开发阶段用stdio生产环境用Streamable HTTP。SSE虽然看起来很美但实际用下来连接断开的概率不低尤其是在网络抖动的时候重连逻辑写起来很烦。Streamable HTTP本质上是HTTP长连接分块传输稳定性和兼容性都好得多。2.3 手把手写一个可复用的MCP Server下面是一个用Python实现的MCP Server示例提供文件读取和SQL查询两个工具from mcp.server import Server from mcp.server.stdio import stdio_server from mcp.types import Tool, TextContent import sqlite3 import json app Server(data-tools) app.list_tools() async def list_tools(): return [ Tool( nameread_file, description读取指定路径的文件内容, inputSchema{ type: object, properties: { path: {type: string, description: 文件路径} }, required: [path] } ), Tool( namequery_sql, description执行SQL查询并返回JSON结果, inputSchema{ type: object, properties: { db_path: {type: string}, sql: {type: string} }, required: [db_path, sql] } ) ] app.call_tool() async def call_tool(name: str, arguments: dict): if name read_file: with open(arguments[path], r, encodingutf-8) as f: content f.read() return [TextContent(typetext, textcontent)] elif name query_sql: conn sqlite3.connect(arguments[db_path]) cursor conn.execute(arguments[sql]) rows cursor.fetchall() columns [d[0] for d in cursor.description] result [dict(zip(columns, row)) for row in rows] conn.close() return [TextContent(typetext, textjson.dumps(result, ensure_asciiFalse))] 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())这个Server写完之后任何支持MCP的Agent都可以直接调用这两个工具不需要写一行集成代码。2.4 MCP工具描述里的隐藏陷阱这里有一个我踩过的坑工具描述写得太模糊模型会乱选工具。比如你写查询数据模型不知道是查数据库还是查文件就会随机选一个。正确的写法是把工具的输入输出、适用场景、限制条件都写清楚。另一个坑是工具数量爆炸。我一开始把所有工具都挂到一个MCP Server上结果Agent的上下文里塞了30多个工具描述模型选择准确率直线下降。后来我按业务域拆成多个Server每个Server只挂5-8个工具Agent按需连接准确率立刻回升。注意MCP Server的工具列表是动态加载的每次Agent启动时都会拉取。如果你的工具列表很大启动延迟会很明显。建议对工具做分组按需加载。3. A2A协议让Agent之间能互相找到并对话3.1 A2A与MCP的本质区别很多人搞不清A2A和MCP的区别我用一句话概括MCP是Agent用工具A2A是Agent找Agent。MCP解决的是垂直问题——Agent怎么调用下层能力A2A解决的是水平问题——Agent之间怎么互相发现和协作。这个区别在架构上非常关键。MCP的连接是Agent到工具是单向的、主从的A2A的连接是Agent到Agent是双向的、对等的。一个Agent既可以是服务的提供方也可以是消费方。3.2 Agent CardA2A的名片机制A2A协议里最核心的概念是Agent Card。每个Agent都要暴露一张名片声明自己是谁、能干什么、怎么调用。这张名片通常是一个JSON文件放在/.well-known/agent.json路径下{ name: financial-analyzer, description: 财务数据分析Agent输入结构化财务数据输出分析结论, url: http://analyzer.internal:8080/a2a, version: 1.0.0, capabilities: { streaming: true, pushNotifications: false }, skills: [ { id: ratio-analysis, name: 财务比率分析, description: 计算流动比率、速动比率、资产负债率等核心指标, inputModes: [application/json], outputModes: [application/json] }, { id: trend-analysis, name: 趋势分析, description: 基于多期数据识别财务趋势, inputModes: [application/json], outputModes: [application/json] } ] }有了这张名片其他Agent就可以通过标准接口发现它、查询它的能力、发起调用。这就像给每个Agent发了一张工牌上面写清楚了岗位和职责。3.3 任务生命周期管理从submitted到completedA2A的任务是有状态的完整生命周期包括submitted→working→input-required→completed/failed/canceled。这个状态机是多Agent协作的基础。我实际用下来最容易出问题的是input-required状态。当Agent A调用Agent B但B发现信息不够需要A补充时B会返回input-required状态并附带问题。如果A没有正确处理这个状态任务就会卡死。我的做法是在编排层统一处理这个状态收到input-required后自动把问题路由回原始请求方拿到补充信息后重新发起调用。3.4 一个完整的A2A调用示例下面是一个Agent通过A2A协议调用另一个Agent的代码import httpx import asyncio class A2AClient: def __init__(self, agent_card_url: str): self.agent_card_url agent_card_url self.card None async def discover(self): async with httpx.AsyncClient() as client: resp await client.get(self.agent_card_url) self.card resp.json() return self.card async def send_task(self, skill_id: str, payload: dict): task { skillId: skill_id, messages: [ { role: user, parts: [{type: data, data: payload}] } ] } async with httpx.AsyncClient(timeout60) as client: resp await client.post( self.card[url], jsontask, headers{Content-Type: application/json} ) return resp.json() async def poll_until_done(self, task_id: str, interval: float 1.0): while True: async with httpx.AsyncClient() as client: resp await client.get(f{self.card[url]}/tasks/{task_id}) result resp.json() if result[status] in (completed, failed, canceled): return result await asyncio.sleep(interval) async def main(): client A2AClient(http://analyzer.internal:8080/.well-known/agent.json) await client.discover() task await client.send_task(ratio-analysis, {revenue: 1000000, cost: 600000}) result await client.poll_until_done(task[id]) print(result) asyncio.run(main())3.5 A2A的坑循环调用与死锁多Agent系统里最隐蔽的bug是循环调用。Agent A调用BB在处理过程中又调用了A如果A没有做幂等处理就会无限循环。我遇到过一次两个Agent互相调用了几百次token账单直接爆炸。防御手段有三个一是给每个任务加trace_id检测到同一个trace重复进入同一个Agent就拒绝二是设置最大调用深度超过阈值直接失败三是在Agent Card里声明依赖关系编排层提前做拓扑排序发现环就报警。4. Skills机制把Agent能力做成可插拔的技能包4.1 Skills和普通工具函数的区别Skills这个概念容易被误解成就是工具函数的另一种叫法。实际上它们的抽象层级不同工具函数是原子操作比如读文件、发HTTP请求Skills是完成特定任务的完整能力通常包含多个工具调用、一段prompt模板、以及特定的输出格式要求。举个例子生成周报这个Skill内部可能包含读取本周的git log、查询任务管理系统、调用LLM总结、按模板格式化输出。这些步骤对外是一个整体调用方只需要说帮我生成周报不需要关心内部怎么实现的。4.2 Skill的目录结构与元数据设计一个规范的Skill应该有自己的目录结构包含元数据、prompt模板、工具依赖声明skills/ weekly-report/ skill.yaml # 元数据 prompt.md # prompt模板 tools.json # 依赖的工具列表 examples/ # 示例输入输出 input.json output.jsonskill.yaml的内容name: weekly-report version: 1.0.0 description: 根据git提交记录和任务系统数据生成周报 author: platform-team inputs: - name: week_start type: string format: date required: true - name: week_end type: string format: date required: true outputs: - name: report type: markdown dependencies: mcp_servers: - git-tools - task-tracker a2a_agents: - summarizer这种结构的好处是Skill可以被版本化管理、可以被自动发现、依赖关系一目了然。当某个MCP Server升级时你可以快速定位哪些Skill受影响。4.3 Skill的动态加载与热更新生产环境里Skill的更新不应该导致整个系统重启。我的做法是用一个Skill Registry来管理所有SkillAgent启动时从Registry拉取Skill列表运行时按需加载。import importlib import yaml from pathlib import Path class SkillRegistry: def __init__(self, skills_dir: str): self.skills_dir Path(skills_dir) self.skills {} self._load_all() def _load_all(self): for skill_path in self.skills_dir.iterdir(): if not skill_path.is_dir(): continue meta_file skill_path / skill.yaml if not meta_file.exists(): continue with open(meta_file) as f: meta yaml.safe_load(f) self.skills[meta[name]] { meta: meta, path: skill_path, prompt: (skill_path / prompt.md).read_text(encodingutf-8) } def reload(self, skill_name: str): skill_path self.skills_dir / skill_name if not skill_path.exists(): raise ValueError(fSkill不存在: {skill_name}) with open(skill_path / skill.yaml) as f: meta yaml.safe_load(f) self.skills[skill_name] { meta: meta, path: skill_path, prompt: (skill_path / prompt.md).read_text(encodingutf-8) } def get(self, skill_name: str): return self.skills.get(skill_name) def list_skills(self): return [ {name: name, description: s[meta][description]} for name, s in self.skills.items() ]热更新的时候只需要调用reload(skill_name)不需要重启Agent进程。4.4 Skill组合用Skills编排复杂工作流单个Skill能力有限真正的威力在于组合。比如季度业务复盘这个任务可以拆成三个Skill串联>from deepagents import TaskGraph, AgentNode, SkillNode graph TaskGraph(namefinancial-report-pipeline) parse_node AgentNode( nameparser, agent_cardhttp://parser.internal/.well-known/agent.json, inputs[pdf_path], outputs[structured_data] ) analyze_node AgentNode( nameanalyzer, agent_cardhttp://analyzer.internal/.well-known/agent.json, inputs[structured_data], outputs[analysis_result] ) report_node SkillNode( namereport-gen, skillreport-generation, inputs[analysis_result], outputs[final_report] ) graph.add_node(parse_node) graph.add_node(analyze_node) graph.add_node(report_node) graph.add_edge(parser, analyzer, mapping{structured_data: structured_data}) graph.add_edge(analyzer, report-gen, mapping{analysis_result: analysis_result}) graph.set_entry(parser) graph.set_exit(report-gen)这种声明式的好处是整个流程一目了然哪个节点依赖哪个节点、数据怎么流转全都写在代码里。出问题的时候看一眼任务图就知道该查哪个节点。5.2 状态管理与断点续跑多Agent流程跑一半失败是常态。如果每次失败都从头开始成本受不了。DeepAgents的状态管理支持断点续跑每个节点完成后状态会被持久化失败重试时从最后一个成功的节点继续。实现上状态存储用Redis或者SQLite都行。关键是要给每个任务分配唯一的run_id所有节点的状态都挂在这个run_id下面。重试的时候带上同一个run_id编排层会自动跳过已完成的节点。from deepagents import StateStore store StateStore(backendredis, urlredis://localhost:6379/0) async def run_pipeline(graph, inputs, run_idNone): run_id run_id or generate_run_id() state await store.load(run_id) or {completed_nodes: [], data: {}} for node in graph.topological_order(): if node.name in state[completed_nodes]: continue node_inputs { k: state[data][v] for k, v in node.input_mapping.items() } try: result await node.execute(node_inputs) state[data].update(result) state[completed_nodes].append(node.name) await store.save(run_id, state) except Exception as e: state[last_error] str(e) await store.save(run_id, state) raise return state[data]5.3 并发控制与资源隔离多Agent集群跑起来之后资源竞争是必然的。我遇到过两个Agent同时调用同一个MCP Server把连接池打满导致第三个Agent超时。解决办法是在编排层做并发控制给每个MCP Server设置最大并发数超过就排队。DeepAgents支持在节点级别配置并发策略analyze_node AgentNode( nameanalyzer, agent_cardhttp://analyzer.internal/.well-known/agent.json, concurrency3, # 最多3个并发实例 timeout120, # 单次调用超时120秒 retry2, # 失败重试2次 retry_backoff5 # 重试间隔5秒 )资源隔离方面我建议给不同类型的Agent分配独立的资源池。比如LLM调用密集的Agent和IO密集的Agent分开部署避免互相影响。5.4 可观测性多Agent系统的黑匣子多Agent系统最难的是调试。一个请求经过5个Agent每个Agent又调用了若干工具出了问题你根本不知道是哪一环。我的做法是全链路追踪每个请求分配一个trace_id所有Agent和工具的调用日志都带上这个ID。关键指标要监控这几个指标含义告警阈值端到端延迟整个任务图完成时间P95 60s单节点延迟每个Agent的处理时间P95 15s工具调用失败率MCP工具调用失败比例 5%Agent调用失败率A2A调用失败比例 3%重试次数任务重试总次数单任务 3次这些指标用OpenTelemetry采集接到PrometheusGrafana上基本能覆盖大部分问题定位需求。6. 实战踩坑多智能体集群跑起来之后才会遇到的五个问题6.1 上下文污染Agent之间的信息串味多Agent协作时一个隐蔽的问题是上下文污染。Agent A把中间结果传给Agent BB在处理时又把A的中间结果带到了自己的输出里导致最终结果里混入了不该出现的内容。我遇到过一次解析Agent输出的JSON里带了一个_debug字段分析Agent没做过滤直接透传最后报告里出现了调试信息。解决办法是在Agent之间的数据契约里明确字段白名单只传递约定的字段其他一律丢弃。6.2 超时级联一个慢节点拖垮整条链路任务图里如果有一个节点特别慢整个链路都会被拖住。更糟的是如果这个慢节点还触发了上游的重试会造成雪崩。我的应对策略是分层超时每个节点有自己的超时时间编排层有全局超时。节点超时后立即失败不等待全局超时后整个任务标记为失败释放资源。同时给慢节点做降级方案比如分析Agent超时后直接返回原始数据让下游处理。6.3 版本漂移Agent Card更新导致的兼容性问题A2A协议里Agent Card是动态获取的。如果某个Agent升级了改了Skill的输入输出格式但调用方没有同步更新就会出错。我吃过这个亏分析Agent把输出字段从result改成了analysis撰写Agent还在找result直接报空指针。解决办法是版本锁定灰度发布。Agent Card里带版本号调用方在配置里锁定版本。升级时先发布新版本观察一段时间确认没问题再切换调用方的版本号。6.4 Token成本失控多Agent不等于多花钱很多人以为多Agent一定比单Agent贵其实不一定。关键在于上下文隔离。单Agent处理复杂任务时上下文会膨胀到几千token多Agent拆分后每个Agent的上下文只有几百token。总体算下来多Agent的token消耗反而可能更低。但如果拆分不当比如Agent之间传递了大量冗余数据或者Agent数量过多导致通信开销超过收益成本就会失控。我的经验是Agent数量控制在5-8个之间超过这个数就要重新审视拆分逻辑。6.5 测试困难怎么验证一个多Agent系统是对的单Agent的测试相对简单给定输入检查输出。多Agent系统的测试要复杂得多因为涉及多个Agent的交互。我的测试策略分三层单元测试每个Agent独立测试mock掉上下游依赖契约测试验证Agent之间的输入输出契约是否匹配端到端测试跑完整的任务图用固定的输入验证输出契约测试是最容易被忽略但最重要的。我写了一个小工具自动读取所有Agent Card检查上下游的字段映射是否一致提前发现不兼容问题。7. 从零搭一套多智能体集群可复现的落地路径7.1 环境准备与依赖清单先把基础环境搭起来。我用的技术栈是Python 3.11 DeepAgents MCP SDK httpxMCP Server用stdio模式本地跑A2A通信用HTTP。pip install deepagents mcp httpx pyyaml redis目录结构建议这样组织project/ agents/ # 各个Agent的实现 parser/ analyzer/ writer/ skills/ # Skill定义 weekly-report/ trend-analysis/ mcp_servers/ # MCP Server实现 >
返回列表