
1. 从零搭建自动化工作流 Agent 的整体设计思路1.1 为什么单 Agent 不够用多智能体编排才是正解很多人第一次接触 Agent 开发脑子里想的都是我做一个超级 Agent把所有工具都挂上去让它自己决定调哪个。我最早也是这么干的结果踩了一堆坑工具一多模型选工具的准确率断崖式下跌一个环节出错整条链路全崩想加个新能力得把整个提示词重写一遍。后来我把这套东西拆成多个专职 Agent用编排层串起来稳定性直接上了一个台阶。这就是多智能体编排的核心价值把复杂任务拆成若干职责单一的 Agent每个 Agent 只干一件事通过一个编排器Orchestrator决定谁先谁后、谁调用谁、数据怎么流转。打个生活化的比方单 Agent 像一个什么都会一点的杂工多智能体像一个分工明确的施工队——水电工、木工、油漆工各司其职工头负责排期和验收。在这个案例里我们要做的是一条自动化工作流用户丢进来一个模糊需求系统自动完成理解需求 → 拆解任务 → 调用工具 → 校验结果 → 汇总输出的全流程。这条链路里至少涉及三类角色负责规划和调度的编排 Agent、负责执行具体动作的执行 Agent、负责质量把关的校验 Agent。1.2 编排层选型为什么我把 MCP 作为工具接入标准热词里反复出现MCP这不是偶然。MCPModel Context Protocol本质上是一套让模型和外部工具、数据源对话的标准协议。在没有它之前每接一个工具就要写一套适配代码工具 A 的调用格式和工具 B 完全不一样维护成本极高。MCP 把这些统一成资源Resource 工具Tool 提示Prompt三件套Agent 只要按协议说话就能对接任何实现了 MCP 的服务。我选 MCP 作为工具接入标准的理由很直接解耦工具的实现和 Agent 的调用逻辑彻底分开换工具不用改 Agent 代码。可复用一个 MCP Server 可以被多个 Agent 同时挂载比如文件读写服务编排 Agent 和执行 Agent 都能用。可观测所有工具调用都走统一协议日志和追踪好做排查问题方便。提示MCP 不是银弹。如果你的工具就两三个、调用逻辑极其简单硬上 MCP 反而增加复杂度。它真正的价值在工具数量多、需要跨 Agent 共享的场景。1.3 整体架构分层一张图讲清数据怎么流我把整个系统分成四层从上到下依次是层级职责关键组件交互层接收用户输入、展示结果命令行 / Web 接口编排层任务拆解、路由、状态管理编排 Agent 工作流引擎执行层调用工具、完成具体动作执行 Agent 集群工具层提供原子能力MCP Server 集合数据流向是用户输入 → 编排层解析成任务图 → 按依赖关系分发给执行层 → 执行层通过 MCP 调工具 → 结果回传编排层 → 校验 Agent 检查 → 汇总输出。这个分层的好处是每一层都能单独测试和替换。我调试的时候经常把执行层换成 mock专门验证编排逻辑对不对效率高很多。2. 核心细节解析与实操要点2.1 任务拆解把模糊需求变成可执行的任务图编排 Agent 最核心的能力是任务拆解。用户说帮我整理这份销售数据并生成周报这句话里藏着好几个子任务读取数据文件、清洗异常值、按维度聚合、生成图表、套用周报模板、输出文档。编排 Agent 要做的就是把这句话翻译成一张有向无环图DAG。我用的拆解策略是先粗后细两轮拆解第一轮让模型输出高层任务列表比如[读取数据, 分析数据, 生成报告]。第二轮针对每个高层任务再问一次这个任务需要哪些具体步骤、依赖哪些前置任务的输出。两轮拆解比一次性拆到底准确率高不少因为模型在单次输出里塞太多步骤容易漏。拆解结果我用一个 JSON 结构承载每个节点包含id、description、depends_on、agent_type、tool_hint五个字段。depends_on决定执行顺序agent_type决定派给哪个执行 Agenttool_hint是给执行 Agent 的提示告诉它大概该用哪类工具。{ task_id: t3, description: 按地区维度聚合销售额, depends_on: [t1, t2], agent_type: data_analyst, tool_hint: 使用数据处理类工具输出结构化表格 }注意depends_on一定要做环检测。我遇到过模型拆出 A 依赖 B、B 又依赖 A 的情况工作流直接死锁。加一个拓扑排序校验发现环就回退让模型重拆。2.2 状态管理工作流跑一半崩了怎么办自动化工作流最怕的就是跑到一半挂了前面全白干。我的做法是给每个任务节点维护一个状态机状态取值有pending、running、success、failed、retrying五种。每完成一个节点就把状态和输出持久化到本地我用的是 SQLite轻量够用。这样带来两个好处一是断点续跑崩了之后从最后一个success的节点继续不用从头来二是可观测随时能查每个节点卡在哪、耗时多久、重试了几次。状态流转的规则我定得很死pending→running调度器分配资源后触发。running→success执行 Agent 返回且校验通过。running→failed执行 Agent 报错或超时。failed→retrying满足重试条件非致命错误、重试次数未超上限。retrying→running重新调度。重试策略我用的是指数退避第一次等 2 秒第二次 4 秒第三次 8 秒最多重试 3 次。为什么不用固定间隔因为很多失败是下游服务瞬时抖动导致的固定间隔容易在同一个抖动窗口里反复撞墙。2.3 工具调用MCP 接入的三个关键参数通过 MCP 挂载工具时有三个参数必须配好否则很容易出问题超时时间timeout默认值往往偏短网络类工具建议设到 30 秒以上本地计算类可以短一点。我吃过亏一个文件解析工具因为文件大默认 10 秒超时一直失败排查半天才发现是超时问题。重试次数max_retries工具级别的重试和任务级别的重试是两回事。幂等的读操作可以多试几次写操作要谨慎避免重复写入。并发上限concurrency_limit同一个 MCP Server 被多个 Agent 同时调用时要限制并发防止把下游打挂。配置示例YAML 格式实际项目里我用配置文件管理mcp_servers: file_service: command: python -m mcp_file_server timeout: 30 max_retries: 2 concurrency_limit: 5 data_service: command: python -m mcp_data_server timeout: 60 max_retries: 1 concurrency_limit: 3提示concurrency_limit不是越大越好。我实测下来下游是数据库时并发超过 5 就容易出现连接池耗尽。宁可让工作流慢一点也别把下游搞崩。2.4 校验 Agent别让错误结果一路流到底很多人做工作流只关注跑通忽略了跑对。我在每个关键节点后面挂了一个校验 Agent它的职责是检查上游输出是否符合预期格式和业务规则。比如数据聚合节点输出后校验 Agent 会检查行数是否合理、有没有空值、数值范围是否异常。校验不通过怎么办两条路一是打回重做让执行 Agent 带着校验反馈重新跑一次二是标记降级如果重试仍失败就在结果里标注此部分数据存疑让最终输出带上警告而不是直接崩掉。我倾向于第二种作为兜底。因为自动化工作流的价值在于尽量给出可用结果而不是一有问题就罢工。当然涉及资金、安全等敏感场景另说那种必须严格失败。3. 实操过程与核心环节实现3.1 环境准备与依赖安装先把基础环境搭起来。我用的是 Python 3.11主要依赖三个库负责 Agent 逻辑的框架、负责 MCP 通信的客户端、负责工作流调度的引擎。python -m venv venv source venv/bin/activate pip install mcp-client workflow-engine pydantic这里pydantic是用来做数据校验的任务图的每个节点、每个工具调用的入参出参我都用 Pydantic 模型定义好处是类型安全、报错清晰。别小看这一步我早期用裸字典传数据一个字段名拼错能查半天。3.2 编排 Agent 的实现从提示词到代码编排 Agent 的核心是一段系统提示词加一个解析函数。系统提示词我反复打磨过很多版最终定下来的结构是先说明角色你是任务规划专家再给出输出格式要求严格 JSON最后给两三个 few-shot 示例。提示词里我特别强调了一点只输出 JSON不要有任何解释性文字。因为模型很爱在 JSON 前后加好的我来帮你拆解这种废话解析的时候会失败。加了这条约束后解析成功率从 70% 提到了 95% 以上。解析函数负责把模型输出转成任务图对象并做环检测import json from pydantic import BaseModel class TaskNode(BaseModel): task_id: str description: str depends_on: list[str] agent_type: str tool_hint: str def parse_task_graph(raw_output: str) - list[TaskNode]: data json.loads(raw_output) nodes [TaskNode(**item) for item in data[tasks]] if has_cycle(nodes): raise ValueError(任务图存在环需要重新拆解) return nodes def has_cycle(nodes: list[TaskNode]) - bool: graph {n.task_id: n.depends_on for n in nodes} visited, stack set(), set() def dfs(node): if node in stack: return True if node in visited: return False stack.add(node) for dep in graph.get(node, []): if dfs(dep): return True stack.discard(node) visited.add(node) return False return any(dfs(n) for n in graph)3.3 调度器实现拓扑排序 并发执行调度器的逻辑是先对任务图做拓扑排序得到执行顺序然后把没有依赖或依赖已完成的节点放进就绪队列并发执行每完成一个节点就检查有没有新节点变成就绪状态。并发我用的是asyncio因为工具调用基本都是 IO 密集型异步比多线程更合适。并发度我设了个上限默认 4防止一次性把下游打爆。import asyncio async def run_workflow(nodes: list[TaskNode], max_concurrency: int 4): completed set() semaphore asyncio.Semaphore(max_concurrency) async def run_node(node): async with semaphore: result await execute_agent(node) completed.add(node.task_id) return result while len(completed) len(nodes): ready [n for n in nodes if n.task_id not in completed and all(d in completed for d in n.depends_on)] if not ready: raise RuntimeError(没有就绪节点可能存在死锁) await asyncio.gather(*[run_node(n) for n in ready])这段代码有个细节while循环每轮重新计算就绪节点而不是一次性算完。因为节点是动态完成的必须每轮刷新。我第一版写成一次性计算结果并发节点全挤在一起依赖关系完全失效。3.4 执行 Agent 与 MCP 工具的对接执行 Agent 拿到任务后先根据tool_hint从 MCP Server 列表里筛选可用工具再让模型决定具体调哪个、传什么参数。这里我用的是工具描述注入的方式把筛选出的工具的名称、描述、参数 schema 拼进提示词模型输出工具调用请求我再解析执行。工具调用的返回结果统一封装成一个结构包含success、data、error三个字段。这样上层不用关心具体工具返回什么格式统一处理就行。async def call_mcp_tool(server, tool_name, params): try: result await server.call_tool(tool_name, params) return {success: True, data: result, error: None} except Exception as e: return {success: False, data: None, error: str(e)}注意工具返回的data可能非常大比如读了一个大文件直接塞进下一轮提示词会爆 token。我的做法是加一层摘要超过阈值的内容先让模型压缩成摘要再往下传。3.5 完整跑通一次从输入到输出的现场记录我拿一个真实场景测了一遍输入读取 sales.csv按地区统计销售额生成一份 Markdown 周报。编排 Agent 拆出 5 个节点读取文件、清洗数据、按地区聚合、生成图表描述、套模板输出。调度器拓扑排序后前两个节点无依赖并发跑聚合节点等前两个完成后面依次串行。实测耗时读取 1.2 秒清洗 0.8 秒聚合 2.1 秒图表描述 3.5 秒模板输出 1.8 秒加上调度开销总共约 11 秒。中间清洗节点第一次因为空值处理规则没匹配上失败了重试一次后成功重试等待 2 秒。最终输出的周报结构完整数据准确。校验 Agent 检查了聚合结果的地区数量和原始数据一致通过。4. 常见问题与排查技巧实录4.1 任务拆解不准模型把简单任务拆复杂了这是最常见的坑。用户说发个邮件模型能拆出确定收件人、撰写主题、撰写正文、检查附件、发送五步。步骤太细会导致调度开销大、失败点变多。我的解法是在提示词里加一条约束如果一个任务用单个工具调用就能完成就不要拆。同时给一个反例告诉模型发送邮件应该是一个节点而不是五个。加了这条之后拆解粒度明显合理了。4.2 工具调用参数错误模型编造不存在的参数模型有时候会幻觉出工具不支持的参数。比如工具只接受path模型传了file_path。这种错误在 MCP 层会被拒绝但报错信息往往不直观。我的做法是在执行 Agent 里加一层参数校验拿到模型输出的参数后先和工具的 schema 比对发现多余或缺失的参数就带着 schema 重新问一次模型。这样能把大部分参数错误在调用前拦下来。4.3 工作流卡死依赖关系没更新前面提过调度器必须每轮重新计算就绪节点。除此之外还有一个隐蔽的坑节点执行成功但状态没写回。我遇到过异步任务抛异常被吞掉的情况节点实际没完成但调度器以为它在跑一直等。后来我加了超时机制任何节点运行超过阈值就强制标记失败并触发重试。4.4 常见问题速查表问题现象可能原因排查方向解决手段任务图解析失败模型输出带解释文字检查原始输出提示词强制纯 JSON工作流死锁依赖成环打印任务图加环检测回退重拆工具调用超时超时阈值太短看工具耗时日志调大 timeout结果一路错到底缺校验环节检查节点输出挂校验 Agent并发把下游打挂并发上限过高看下游负载调低 concurrency_limit重试风暴固定间隔重试看重试时间戳改指数退避4.5 几个我踩过的独家坑第一个坑别在编排 Agent 里塞业务逻辑。我一开始图省事把如果销售额为负就标记异常这种规则写进了编排提示词结果模型时灵时不灵。后来把这类规则全部下沉到校验 Agent用代码实现稳定多了。编排 Agent 只负责调度不碰业务判断。第二个坑日志要带 task_id。多智能体并发跑的时候日志混在一起根本没法看。我给每条日志都带上task_id和agent_type排查问题时按 task_id 一过滤整条链路清清楚楚。第三个坑MCP Server 要独立进程。我早期把 MCP Server 和主程序跑在同一个进程里一个工具崩溃直接把整个工作流带崩。改成独立进程后工具崩了只是那个调用失败主流程还能重试或降级。第四个坑提示词里的示例要覆盖边界情况。我给的 few-shot 示例一开始都是正常场景结果模型遇到数据为空这种边界就乱拆。后来我在示例里专门加了一个空数据场景模型处理边界的能力明显提升。这套自动化工作流 Agent 我从最初的单 Agent 版本迭代到现在前后改了七八版最大的体会就是编排的稳定性不来自模型多聪明而来自工程约束多严密。把状态管好、把校验做足、把重试和降级设计清楚比换一个更强的模型管用得多。后续如果要把这套东西扩展到更多场景我的建议是先把工具层做厚——MCP Server 越丰富编排层能玩的花样就越多而编排逻辑本身几乎不用改。