1. 复杂 Agent 工作流到底难在哪
做 Agent 系统的人,十个里有九个最后都会撞上同一堵墙:单 Agent 跑个 Demo 很惊艳,一旦把任务复杂度拉上去,整个系统就开始胡言乱语、反复横跳、成本失控。这不是模型能力不够,而是工作流设计没跟上。
我前后搭过几套不同规模的 Agent 系统,从最简单的单轮工具调用,到十几个 Agent 协同处理长链路任务,踩过的坑基本能写一本小册子。复杂 Agent 工作流的核心矛盾其实就三个:并行怎么拆、协调怎么做、效果怎么评。这三个问题不解决,Agent 系统永远停留在玩具阶段。
这篇文章面向的是已经写过基础 Agent、准备把系统往生产级别推的开发者。我会把并行架构的设计逻辑、协调机制的具体实现、评估优化的落地方法拆开讲,每个环节都配上我实际用过的方案和参数。读完你应该能直接照着搭一套能跑复杂任务的 Agent 工作流,而不是停留在“调个 API 就完事”的阶段。
先说一个基本判断:复杂 Agent 工作流的本质是分布式任务调度 + 状态管理 + 质量闭环三件事的组合。把它当成一个微型操作系统来设计,思路会清晰很多。下面按这个逻辑逐层展开。
2. 并行架构设计:把任务拆对是成功的一半
2.1 为什么必须并行,串行到底卡在哪
很多人第一反应是让 Agent 一步步串行执行:先查资料,再分析,再写报告。小任务没问题,但一旦任务链路超过五步,串行的三个致命问题就暴露了。
第一是延迟叠加。假设每个子任务平均耗时 3 秒,十个子任务串起来就是 30 秒,用户早就跑了。第二是错误累积。串行链路里任何一步出错,后面全废,而且错误会沿着上下文一路污染下去。第三是上下文膨胀。每一步的输出都塞进上下文,到后面 token 消耗爆炸,模型注意力被稀释,质量断崖式下跌。
并行的价值就在于把这三点同时缓解:多个独立子任务同时跑,总延迟取决于最慢的那个而不是总和;单个分支失败可以重试而不影响其他分支;每个分支只持有自己需要的上下文,主流程只做汇总。
但并行不是无脑拆。我见过有人把任务拆成二十个并行分支,结果协调开销比串行还大。拆分的粒度需要根据任务的实际依赖关系来定。
2.2 任务依赖图:先画图再写代码
动手写并行逻辑之前,我强烈建议先把任务依赖关系画成一张有向无环图(DAG)。这不是形式主义,而是能帮你提前发现三类问题:循环依赖、孤立节点、关键路径过长。
具体做法是列出所有子任务,标注每个任务的输入来自哪些任务的输出。比如一个“竞品分析报告”任务,可以拆成:
- 任务 A:抓取竞品基础信息(无依赖)
- 任务 B:抓取竞品定价策略(无依赖)
- 任务 C:抓取竞品用户评价(无依赖)
- 任务 D:汇总 A/B/C 的输出做对比分析(依赖 A、B、C)
- 任务 E:基于 D 生成报告(依赖 D)
这张图里 A、B、C 可以并行,D 是汇聚点,E 是终点。关键路径是 A/B/C 中最慢的那个加上 D 和 E。如果你发现关键路径太长,就要考虑把 D 或 E 进一步拆分。
提示:依赖图里如果出现某个节点被超过三个下游依赖,说明这个节点是个瓶颈,考虑把它拆成多个更细的输出,或者提前缓存它的结果。
2.3 并行执行的三种模式与选型
实际落地时,并行执行有三种常见模式,各有适用场景。
模式一:扇出-扇入(Fan-out/Fan-in)。主流程把任务分发给多个并行分支,等所有分支完成后汇总。适合子任务之间完全独立、需要全量结果的场景,比如多源信息采集。实现上用asyncio.gather或类似的并发原语即可。
模式二:流水线(Pipeline)。任务分成多个阶段,每个阶段内部并行,阶段之间串行。适合有明确阶段划分的场景,比如“采集 → 清洗 → 分析 → 生成”。这种模式的关键是阶段之间的缓冲区设计,避免上游生产太快下游消费不过来。
模式三:竞速(Race)。同一个任务发给多个 Agent,谁先给出合格结果就用谁的。适合对延迟极度敏感、且单次成功率不高的场景。成本会翻倍,但延迟能压到最低。
选型判断标准很简单:看子任务之间有没有数据依赖。完全独立用扇出-扇入,有阶段依赖用流水线,对延迟敏感且能接受成本用竞速。我实际项目里用得最多的是扇出-扇入,因为大部分复杂任务都能拆成相对独立的子任务。
2.4 并发度控制:别让并行变成灾难
并行度不是越高越好。我踩过最惨的一次坑是把并发度设成 50,结果下游的 API 直接限流,一半请求失败,重试又把配额打满,整个系统雪崩。
并发度需要根据三个约束来确定:下游服务的速率限制、本地资源(内存/连接数)、成本预算。我的经验值是,对于调用外部 API 的 Agent,并发度控制在 5 到 10 之间比较稳妥;如果是本地模型推理,根据显存和批处理能力来定,通常 4 到 8。
实现上一定要加信号量(Semaphore)控制并发数,而不是无脑gather所有任务。下面是一个典型的并发控制写法:
import asyncio async def run_with_limit(tasks, max_concurrency=8): semaphore = asyncio.Semaphore(max_concurrency) async def wrapped(task): async with semaphore: return await task return await asyncio.gather(*[wrapped(t) for t in tasks])这个模式看起来简单,但它是整个并行架构的地基。没有它,你的系统在压力下必然崩。
3. 协调机制:让多个 Agent 不打架
3.1 协调的本质是状态管理
并行跑起来之后,下一个问题就是协调。很多人把协调理解成“让 Agent 之间通信”,这个理解太浅了。协调的本质是共享状态的管理——多个 Agent 读写同一份任务状态,怎么保证不冲突、不丢失、不重复。
我见过最典型的翻车场景:两个 Agent 同时判断某个子任务“未完成”,于是都去执行,结果做了两遍,还产生了冲突的输出。这就是状态管理没做好。
协调机制的设计要回答三个问题:状态存在哪、谁能改、改了怎么通知。下面逐个说。
3.2 中心化 vs 去中心化协调
协调架构分两大流派,各有取舍。
中心化协调:有一个 Orchestrator(编排器)统一管理任务分配和状态。所有 Agent 向 Orchestrator 汇报,由它决定下一步。优点是状态一致性强、逻辑清晰、容易调试;缺点是 Orchestrator 是单点,任务量大时可能成为瓶颈。
去中心化协调:Agent 之间直接通信,通过共享状态或消息队列协调。优点是扩展性好、没有单点;缺点是状态一致性难保证,调试起来像破案。
我的建议是:任务规模在几十个 Agent 以内,一律用中心化。去中心化的复杂度只有在超大规模下才划算,而绝大多数项目根本到不了那个规模。中心化 Orchestrator 用状态机实现,清晰又可靠。
3.3 用状态机管住整个工作流
Orchestrator 的核心是一个状态机。每个任务有明确的状态:pending、running、completed、failed、retrying。状态转移必须满足预定义的规则,非法转移直接拒绝。
这样做的好处是,任何时刻你都能通过查询状态知道系统在哪一步,出问题能精确定位。我习惯把状态机持久化到数据库或 Redis,这样即使进程重启,任务也能从断点恢复。
一个简化版的状态转移表:
| 当前状态 | 允许转移到 | 触发条件 |
|---|---|---|
| pending | running | 调度器分配资源 |
| running | completed | 任务成功返回 |
| running | failed | 任务报错或超时 |
| failed | retrying | 重试次数未超限 |
| retrying | running | 重新调度 |
| failed | dead | 重试次数超限 |
这张表看起来朴素,但它能挡住 90% 的状态混乱问题。任何不在表里的转移,代码里直接抛异常,逼你面对设计缺陷。
3.4 Agent 之间的通信协议设计
Agent 之间传递消息,格式必须严格约定。我推荐用结构化的 JSON,包含这几个字段:task_id、sender、receiver、payload、timestamp、status。不要用自然语言直接传,那样下游解析起来会疯掉。
消息传递有两种模式:同步请求-响应和异步事件。同步适合需要立即拿到结果的场景,异步适合通知类消息。实际系统里两者混用,但要注意异步消息的顺序问题——如果下游依赖消息顺序,必须加序列号。
注意:Agent 之间的消息一定要做大小限制。我遇到过某个 Agent 把整个网页内容塞进消息传给下游,直接把消息队列撑爆。单个消息建议控制在 100KB 以内,超出的内容走对象存储,消息里只传引用。
3.5 冲突处理与幂等性
并行系统里冲突不可避免。两个 Agent 可能同时想修改同一个资源,或者重复执行同一个任务。解决办法有两个:加锁和幂等。
加锁简单粗暴,用分布式锁(比如基于 Redis)保证同一时刻只有一个 Agent 能操作某个资源。但锁的粒度要控制好,太粗会拖慢系统,太细容易死锁。
幂等更优雅:让每个操作无论执行多少次,结果都一样。实现方式是给每个任务分配唯一 ID,执行前先检查这个 ID 是否已经处理过。我倾向于幂等优先,锁作为兜底。因为幂等不阻塞并发,扩展性更好。
3.6 超时、重试与降级策略
协调机制里最容易被忽视的是异常处理。Agent 卡住不返回怎么办?下游服务挂了怎么办?这些必须提前设计。
超时策略:每个任务设置硬超时,超过就标记失败。超时时间根据任务类型定,简单查询 10 秒,复杂分析 60 秒,别一刀切。
重试策略:失败任务自动重试,但要加指数退避,避免雪崩。重试次数一般 2 到 3 次,再多说明是系统性问题,重试也没用。
降级策略:关键路径上的任务失败时,要有备选方案。比如主模型调用失败,降级到备用模型;实时数据拿不到,降级到缓存数据。降级不是妥协,是保证系统可用的必要手段。
4. 评估优化:没有度量就没有改进
4.1 为什么 Agent 评估比模型评估更难
传统模型评估有明确的指标:准确率、召回率、F1。但 Agent 评估难得多,因为 Agent 的输出是多步骤、多分支、带工具调用的,最终结果对不对只是一方面,过程合不合理同样重要。
我见过太多团队只看最终输出,结果 Agent 用错误的方式碰巧得到了正确答案,下次就翻车。评估必须覆盖三个维度:结果质量、过程合理性、资源消耗。
4.2 结果质量评估:自动 + 人工双轨
结果质量评估分两层。第一层是自动评估,用规则或模型打分。规则适合有明确对错的场景,比如数据抽取是否完整、格式是否合规。模型打分适合主观性强的场景,比如报告质量、文案水平。
自动评估的关键是评估标准要具体。不要写“评估回答质量”,要拆成“事实准确性、逻辑连贯性、信息完整度”三个子项,每项 1 到 5 分。标准越细,评估越稳定。
第二层是人工评估,抽样检查。自动评估再完善也有盲区,人工抽查能发现系统性问题。我通常按 5% 到 10% 的比例抽样,重点看自动评估得分高和低的两个极端,中间地带可以少看。
4.3 过程评估:追踪每一步的决策
过程评估是 Agent 特有的。要记录每个 Agent 的每一步决策:调用了什么工具、传了什么参数、得到了什么结果、为什么选择这个分支。这些数据是优化的金矿。
实现上,给每个 Agent 加一个决策日志,结构化记录每一步。日志要包含step_id、agent_id、action、input、output、reasoning、duration。有了这些,你就能回答“为什么这个任务花了 30 秒”“为什么这个分支失败了”这类问题。
过程评估的核心指标是步骤效率和决策准确率。步骤效率看有没有冗余操作,决策准确率看 Agent 选的分支对不对。这两个指标能直接指导优化。
4.4 资源消耗评估:成本必须可控
Agent 系统的成本很容易失控,尤其是并行之后。评估必须包含资源维度:token 消耗、API 调用次数、总耗时、并发峰值。
我习惯给每个任务算一个成本画像:平均 token 数、平均调用次数、P95 耗时。有了画像,就能识别哪些任务成本异常,针对性优化。比如某个任务 token 消耗是同类任务的三倍,多半是上下文没控制好。
4.5 基于评估的迭代优化闭环
评估不是终点,优化才是。完整的闭环是:评估发现问题 → 定位根因 → 调整设计 → 重新评估。
常见的优化方向有三个。第一是提示词优化,如果某个 Agent 决策准确率低,先看提示词是不是有歧义。第二是拆分粒度调整,如果某个任务步骤效率低,可能是拆得太粗或太细。第三是模型选型调整,简单任务用小模型,复杂任务用大模型,别一律用最贵的。
这个闭环要跑起来,关键是评估要自动化、可重复。每次改动后能快速跑一遍评估,看指标有没有提升。手动评估的迭代速度根本跟不上。
5. 实操落地:从零搭一套可用的工作流
5.1 整体架构与技术选型
把前面三块拼起来,一套完整的复杂 Agent 工作流包含四层:任务编排层、Agent 执行层、状态存储层、评估监控层。
任务编排层负责解析任务、构建依赖图、调度执行。我一般用 Python 的asyncio加一个轻量状态机,不引入太重的框架,因为 Agent 场景变化快,框架的抽象反而碍事。
Agent 执行层是各个具体的 Agent,每个 Agent 封装一个能力,输入输出标准化。状态存储层用 Redis 存运行时状态,用数据库存历史记录。评估监控层独立部署,异步消费执行日志。
技术选型上,我的原则是能用简单方案就不上复杂框架。很多团队一上来就上重型编排框架,结果学习成本高、调试困难,最后还不如自己写两百行代码来得快。
5.2 关键代码结构
一个可复用的工作流骨架大概长这样:
class WorkflowOrchestrator: def __init__(self, dag, state_store, max_concurrency=8): self.dag = dag self.state_store = state_store self.semaphore = asyncio.Semaphore(max_concurrency) async def execute(self, task_id): ready_tasks = self.dag.get_ready_tasks(task_id) while ready_tasks: results = await self._run_batch(ready_tasks) self._update_state(results) ready_tasks = self.dag.get_ready_tasks(task_id) return self.state_store.get_result(task_id) async def _run_batch(self, tasks): async def run_one(task): async with self.semaphore: return await self._execute_with_retry(task) return await asyncio.gather(*[run_one(t) for t in tasks])这个骨架的核心是get_ready_tasks——它根据依赖图找出当前所有依赖已满足的任务,批量执行,然后更新状态,再找下一批。整个流程自动处理了并行和依赖。
5.3 参数配置与调优记录
实际调优时,有几个参数需要反复试。我把自己的经验值列出来供参考。
| 参数 | 初始值 | 调优范围 | 说明 |
|---|---|---|---|
| 最大并发度 | 8 | 4-16 | 根据下游限流调整 |
| 单任务超时 | 30s | 10-120s | 按任务类型分级 |
| 最大重试次数 | 2 | 1-3 | 超过说明系统性问题 |
| 重试退避基数 | 1s | 0.5-2s | 指数退避的底数 |
| 上下文窗口占比 | 60% | 40-70% | 留出空间给工具输出 |
这些值不是拍脑袋定的,是实际跑出来的。比如并发度,我从 16 降到 8 是因为发现 16 时下游 API 错误率明显上升。超时时间按任务类型分了三档,简单查询 10 秒,中等分析 30 秒,复杂生成 60 秒。
5.4 一个完整的任务执行实录
拿一个“多源信息汇总分析”任务举例,走一遍完整流程。
任务进来,编排层先解析成依赖图:三个采集任务并行,一个分析任务依赖采集,一个生成任务依赖分析。状态初始化,所有任务pending。
调度器发现三个采集任务就绪,并发执行。每个采集 Agent 调用对应工具,返回结构化数据。假设其中一个采集任务超时失败,状态标记failed,触发重试。重试成功后,三个采集任务都completed。
分析任务就绪,执行。分析 Agent 拿到三份采集结果,做对比分析,输出结构化结论。生成任务就绪,基于分析结论生成最终报告。
整个过程中,每一步的状态变化、耗时、token 消耗都记录到评估层。任务结束后,评估层自动打分,生成一份执行报告。如果得分低于阈值,标记出来供人工复查。
这套流程跑下来,一个原本需要串行几分钟的任务,并行后压缩到几十秒,而且每一步都可追溯、可优化。
6. 常见问题与排查技巧实录
6.1 并行任务互相污染上下文
这是最常见的坑。多个并行 Agent 共享了同一份上下文,A 的输出被 B 读到了,导致 B 的判断出错。
根因是上下文没有隔离。解决办法是给每个并行分支分配独立的上下文空间,只在汇聚点合并。实现上,每个 Agent 执行时传入一个上下文副本,而不是引用。
提示:如果确实需要共享部分信息,用显式的共享区,而不是让所有 Agent 读同一份大上下文。共享区只放必要的公共信息,比如任务目标、全局约束。
6.2 汇聚点成为性能瓶颈
扇出-扇入模式里,汇聚点要等所有分支完成。如果某个分支特别慢,整个任务就被拖住。
排查方法是看各分支的耗时分布。如果 P95 和 P50 差距很大,说明有长尾分支。解决办法有两个:给慢分支单独设超时,超时就降级;或者把汇聚逻辑改成流式,来一个处理一个,不等全部。
6.3 重试导致重复副作用
任务失败重试时,如果任务有副作用(比如写数据库、发消息),重试会造成重复。
根因是没做幂等。解决办法是给每个任务分配唯一 ID,副作用操作前先检查 ID 是否已处理。对于无法幂等的操作,用补偿机制,重试前先回滚上一次的副作用。
6.4 评估指标虚高
自动评估得分很高,但实际效果差。这通常是评估标准太宽松,或者评估模型被“讨好”了。
排查方法是人工抽查自动评估的高分样本,看是不是真的合格。如果是评估标准问题,细化标准;如果是评估模型问题,换一个更严格的评估模型,或者用多个模型交叉验证。
6.5 成本突然飙升
某天发现 token 消耗翻倍,但任务量没变。这通常是某个环节的上下文膨胀了。
排查方法是看每个任务的 token 画像,找出异常的任务。常见原因是工具返回结果没截断,或者历史消息没清理。解决办法是给工具输出设大小上限,定期清理不再需要的历史消息。
6.6 问题速查表
| 现象 | 可能原因 | 排查方向 | 解决方向 |
|---|---|---|---|
| 输出质量不稳定 | 上下文污染 | 检查并行分支上下文隔离 | 独立上下文 + 显式共享区 |
| 任务耗时波动大 | 长尾分支 | 看分支耗时分布 | 超时降级或流式汇聚 |
| 重复副作用 | 缺幂等 | 检查重试逻辑 | 唯一 ID + 幂等检查 |
| 评估虚高 | 标准太松 | 人工抽查高分样本 | 细化标准 + 多模型交叉 |
| 成本飙升 | 上下文膨胀 | 看 token 画像 | 截断工具输出 + 清理历史 |
| 系统雪崩 | 并发过高 | 看下游错误率 | 降并发 + 指数退避 |
这张表是我实际排查时总结的,覆盖了八成以上的常见问题。遇到新问题先对照这张表,能省不少时间。
7. 一些踩坑后的个人体会
搭复杂 Agent 工作流这几年,最大的体会是别追求一步到位。我见过太多团队一上来就设计一个完美架构,结果复杂度爆炸,半年都跑不起来。正确的做法是先跑通最小闭环:单 Agent 加简单串行,能出结果就行。然后逐步加并行、加协调、加评估,每加一层都确保系统还能跑。
第二个体会是评估要趁早。很多人觉得系统还没成型,评估以后再说。结果系统越做越大,问题越积越多,最后根本不知道从哪优化。评估框架应该在第一个 Agent 跑通时就搭起来,哪怕只评估最简单的指标。
第三个体会是日志比什么都重要。Agent 系统的调试难度远超普通程序,因为决策过程是黑盒。没有详细的决策日志,出问题只能靠猜。我现在的习惯是,任何 Agent 的每一步决策都记日志,宁可多存点数据,也别在排查时抓瞎。
最后一个,成本意识要贯穿始终。Agent 系统的成本是隐性的,跑起来不觉得,月底账单出来吓一跳。每个任务都要有成本画像,异常及时告警。省下来的钱,够你多跑很多实验。
这套东西没有银弹,都是在实际项目里一点点磨出来的。上面写的方案和参数,你可以直接拿去用,但一定要根据自己的场景调整。照搬参数是最容易踩的坑,因为每个系统的下游服务、任务类型、成本预算都不一样。先跑起来,再优化,比什么都强。