任务编排与工作流引擎:给工坊排班的人
一条 Agent 循环把"思考—行动—观察"走得很顺,前提是任务连续、能一步接一步推进。可现实里的复杂任务常带着两个这条循环装不下的特征。一是它不可原子:"评估一篇论文"听上去是一件事,拆开却是取文本、分析内容、核对引用、检查格式、汇总报告好几步,彼此还有先后;硬塞进一条循环,模型要在同一段上下文里同时记住所有中间产物,很快顾此失彼。二是它不该串行:内容分析和引用分析互不依赖,本可以同时做,挤在一条循环里一步步跑,既慢又浪费。
编排层就是站在循环之上的那个角色。它不替运行时决定"这一步说什么",只回答四个更大的问题:哪些任务、按什么顺序、交给谁、出错了怎么办。 它像工坊里的排班人——不比钟表匠更懂齿轮,也不比石匠更懂石头,但她懂顺序、依赖、责任和恢复。当 Agent 走出 demo、面对长任务和真实副作用,决定它可靠与否的瓶颈,已经从模型本身转移到了包裹模型的这层工程上。
TL;DR
编排层管理跨循环、跨任务、跨智能体的全局调度。它用 DAG 表达依赖与并行,用拓扑排序找出执行顺序,用有限状态机表达分支、审批与重试,用角色专化破解单智能体的上下文退化和自评偏差,用可靠通信把执行单元连起来。五个词记住它:分解、定序、分工、通信、恢复。
一、编排层处在运行时之上,是另一条边界
运行时引擎管的是一条循环内部的"下一步做什么":识别模型请求、推进思考—行动—观察。编排层管的是这条循环之外、之上的全局调度。两层的边界要画清楚,否则复杂任务会退化成一团互相纠缠的逻辑:
用户目标
→ 编排层(Orchestration):拆任务、排顺序、分角色、定恢复 —— 决定"全局怎么走"
→ 运行时(Runtime):一条循环内识别请求、推进下一步 —— 决定"这一步做什么"
→ 工具层 → 外部系统:文件、命令、网络、数据库
值得把背景说透:编排不是另起炉灶的新发明。Anthropic 把 LLM 系统分成两类——工作流(用预定义代码路径编排模型与工具,要的是可预测)和智能体(让模型动态指导自身流程,要的是灵活),并归纳出五种可组合模式:提示链、路由、并行化、编排者-工作者、评估者-优化者。编排层做的事,本质就是把这几种原本活在"单次调用的代码结构"里的模式,放大成"跨循环、跨智能体的系统结构"。DAG 的并行分层就是并行化,状态机能自然容纳提示链和路由,多智能体那两根支柱正是编排者-工作者和评估者-优化者。
二、任务分解:把大问题画成一张 DAG
任务分解的根本目标,是把一个不可原子的大问题拆成若干能独立执行的小任务,再用一张有向无环图把依赖画出来:节点是任务,箭头是依赖,箭头指向谁,谁就得等前面先完成。DAG 是恰当的抽象,因为它同时保证三件事:
- 无环:不存在循环依赖,任务一定跑得完,不会互相死等。
- 可拓扑排序:总能找到一个执行顺序,让每个任务开跑时它依赖的东西都已就绪。
- 有并行潜力:彼此没有直接依赖的节点可以同批执行。
拿贯穿全文的例子——评估论文质量——画成图,调度器一眼就能看出哪几件事能挤在同一批里跑:
┌──→ 内容分析 ──┐
文本预处理 ──┼──→ 引用分析 ──┼──→ 综合评估 ──→ 生成报告
└──→ 格式审查 ──┘
(串行根) (三件事并行) (汇聚点:必须等三件全完成)
每个节点不只是一句任务描述,它要带上执行所需的全部约束,而这些约束应当在画图阶段就定好,而不是出错了再临时拍脑袋:
@dataclass
class Task:
id: str # 形如 父任务#类型#序号,可层层嵌套,便于定位与审计
kind: str # local_bash / local_agent / workflow ...(见第四节)
deps: list[str] # 依赖的任务 id;箭头指向谁,谁就得先完成
state: str = "pending" # pending / running / completed / failed / killed
max_retries: int = 0 # 失败重试上限
timeout_s: int = 600 # 超时上限
on_fail: str = "fail_fast" # fail_fast 还是 continue(放过它继续跑其余任务)
isolate_ctx: bool = False # 是否给它一个干净的上下文窗口
画图时有两个决策直接决定系统好不好用。其一是粒度:切太粗,本可并行的部分被困在一个节点里;切太细,调度开销反而超过任务本身。一个好用的经验区间是让单个任务预期执行十秒到十分钟——低于一秒倾向合并,高于十分钟倾向继续拆。其二是分清数据依赖和控制依赖:两者都画成箭头,但"我需要你的输出"要传数据,"我只需要你先完成"只需传一个完成信号。区分清楚,直接影响后面的通信成本。
三、调度:拓扑排序与并行分层
图画好后怎么决定执行顺序?答案是拓扑排序,经典做法是 Kahn 算法。它顺手就能把任务切成一层层的并行组:
统计每个任务的入度(被多少前驱指着)
→ 入度为 0 的任务进就绪队列,组成第 0 层(可同时开跑)
→ 每完成一个,把它指向的任务入度 -1
→ 入度减到 0 的任务进入下一层
→ 反复,直到所有任务分完层
这里有个容易翻车的细节:分层时只能把"依赖已在前几层全部完成"的任务放进同一层,否则会把本该串行的任务误排进并行批次,让下游读到还没生成的中间结果。另外,开跑前一定要先验证整张图——所有依赖都存在、没有环、没有谁被孤立在外。一张没验证过的 DAG,跑到一半才发现悬空依赖,比根本没并行更糟。
四、七种任务类型与上下文隔离
光有依赖图还不够,还得决定每个节点用什么方式执行。一种实用的归纳是七种 Task 类型,本质是为不同任务匹配不同的执行环境和隔离级别:
| Task 类型 | 跑在哪里 | 典型场景 | 隔离上下文 |
|---|---|---|---|
| local_bash | 本地 shell | 系统命令、文件操作 | 否 |
| local_agent | 本地子智能体 | 代码分析、生成 | 是 |
| remote_agent | 远程 Harness | 分布式计算 | 是 |
| in_process_teammate | 进程内子智能体 | 轻量分工 | 是 |
| workflow | 工作流引擎 | 复杂多步流程 | 部分 |
| monitor_mcp | MCP 监听 | 长期监控 | 是 |
| dream | 后台异步任务 | 非关键路径并发 | 是 |
记这张表的关键不是背七个名字,而是抓住两个维度:任务跑在哪里(本地/远程/进程内),以及要不要给它一个干净的上下文。需要隔离的,往往是那些会产生大量中间信息、却只需把结论交回来的工作。
为什么"隔离上下文"值得专门设计,用一组数字最清楚。一个子智能体可以用干净的上下文窗口去探索,过程中烧掉几万个 token,最后只把一两千 token 的精炼摘要交回主智能体。这种"用 token 换覆盖面"的分工收益相当可观:Anthropic 的多智能体研究系统相比单智能体提升了约九成,而在 BrowseComp 这类评测里,token 用量本身能解释约八成的性能方差。换句话说,任务分解不只是为了并行跑得快,更是为了保护主智能体的上下文不被细节淹没——这一点和记忆子系统里"证据求全、上下文求精"是同一条原则的两面。
每个任务从生到死会经过五种状态,调度器据此决定下一步:pending(已定义、依赖未满足)→ running(依赖凑齐,开跑)→ completed(成功,输出供下游用)。出错是 failed,由策略判断是否重试回到 running;人工中止或超时是 killed,可重新入队回到 pending。这套状态机看着简单,却是后面容错和通知调度的地基。
五、工作流引擎:用状态机描述复杂流程
DAG 擅长表达"谁先谁后",但它表达不了"验证失败走另一条路""失败重试三次""执行前先等审批"这类带分支和循环的逻辑。要描述这些,需要更强的抽象:有限状态机。它由四部分组成——有限个状态(待验证、执行中、完成)、触发转移的事件(批准、出错、超时)、规定"某状态遇某事件去往哪"的转移函数、以及明确的初始态与终止态。
定义状态机有两种取向,对照着看最清楚:
| 维度 | 代码优先 | 声明式(如 YAML 引擎) |
|---|---|---|
| 怎么定义 | 用代码写状态和转移 | 用 YAML 描述,无需编码 |
| 转移条件 | 任意代码表达式 | 受约束的模板表达式 |
| 灵活性 | 高,能表达复杂逻辑 | 受语法约束,但更规整 |
| 可审计性 | 取决于代码风格 | 天然可版本控制、可 diff |
| 适合谁 | 工程师 | 也面向非技术人员 |
声明式引擎通常强调四个特性:声明式(把"流程长什么样"和"怎么跑"分开)、确定性执行(相同输入保证相同路径,对调试和审计极关键)、副作用暂停、可恢复可审计。
这其中最值得记住的设计,是给每个动作打上**"有没有副作用"**的标记。纯计算(验证、分类、读取)直接执行,结果进下一轮;有副作用的动作(写文件、改数据库、调危险 API)先挂起进入等待审批,外部批准后才真正提交:
验证失败
验证 ─────────────────→ 拒绝 ──→ ●
│ 验证通过
▼ 有副作用 人工批准
执行 ──────────────────→ 等待审批 ──────────→ 已提交 ──→ ●
│ 纯计算,无需审批 │ 人工驳回
└────────→ 已提交 ──→ ● └──────────→ 拒绝 ──→ ●
这其实是运行时里"候选输出 vs 已提交状态"那条原则落到了编排层。背后还有一条更普适的判断:模型变强,意味着可以更信任它去"生成",但不能因此就信任它去"提交"。 模型越聪明,"替模型擦屁股"型的控制(重试、输出清洗)越可以删;但"状态完整性、提交边界、审计"型的控制要保留甚至加强,因为它们防的是不可逆的外部副作用,这跟模型聪不聪明无关。把哪一步是提交边界明确画进流程图,远比让审批逻辑散落在代码各处安全。
实现这套机制有个细节最容易翻车:执行动作前必须先跳过已经执行过的动作,否则人工批准后再次启动流程,会把同一个副作用动作重新挂起,永远转移不到下一个状态。最后,工作流要有检查点——保存当前状态、上下文和执行历史,让进程崩溃后能从最近一个"已验证的最小状态"续跑,而不是从头重跑一遍可能带副作用的步骤。
六、多智能体编排:拆职责,破自评偏差
把所有事交给一个智能体从头干到尾,简单任务够用,复杂任务会撞上两堵墙。一堵是上下文退化:窗口被填满后模型表现下滑,有些模型还会在接近上限时草草收尾、回避复杂推理。另一堵是自评偏差:智能体倾向高估自己的产出,让它给自己打分往往偏松,给不出真正批判性的反馈。
解法是职能分离加对抗性反馈。多智能体协作有四种常见结构,选型时记住代价就够:
| 模式 | 结构 | 适用 | 主要代价 |
|---|---|---|---|
| 网络 | 智能体平等、点对点 | 去中心化讨论 | 通信复杂,一致性难保 |
| 主管 | 一个主管管多个 Worker | 明确分工、质量把控 | 主管易成瓶颈 |
| 层级 | 多层,上层规划下层执行 | 大规模复杂分解 | 通信延迟、实现复杂 |
| 并行 | 流水线式逐段处理 | 流式数据、多步验证 | 严格顺序,难回溯 |
最有价值的工程模式,是规划-生成-评估这个三角色专化的对抗循环。规划者把模糊需求展开成带验收标准的分步计划;生成者按计划逐项实现,专注执行;评估者拿多维标准给结果打分、列问题、提改进:
规划者 ──计划 + 验收标准──→ 生成者 ──产出──→ 评估者
▲ │ 按设计质量/原创性/
│ │ 完成度等维度打分
└──── 打回(低于阈值,带反馈) ─┘
达标 或 用尽迭代(通常 2-3 轮) ──→ 交付
这正是评估者-优化者模式。它解决的核心痛点恰恰是单智能体的自评偏差——把评估交给一个独立角色,就从结构上绕开了"自己给自己打高分"。一组对比数据能说明这种分工的价值(具体数字依实验设置,看趋势即可):
| 指标 | 单智能体(约 20 分钟, $9) | 多智能体专化(约 6 小时, $200) |
|---|---|---|
| 功能完整性 | 核心功能损坏 | 完整实现 |
| 代码质量 | 一次性脚本 | 接近生产级 |
| 迭代次数 | 0 | 3–4 次 |
| 总体可用性 | 不足 20% | 超过 90% |
代价写在表头里:多智能体更慢、更贵,适合质量比速度重要的任务,不是越多越好。并发跑的时候最怕上下文互相污染,对策是分层上下文:每个子智能体有自己的本地变量空间,只读地继承父上下文,要把结果传回父级必须显式提交某几个变量,而不是默认共享。这样既隔开各自工作区,又留出一条受控的数据通道。
七、通信:系统的血管
智能体之间怎么交换信息,直接决定系统的可靠性、可扩展性和延迟。两种基本范式各有取舍:
| 维度 | 消息传递 | 共享内存 |
|---|---|---|
| 耦合度 | 松,互不需知道内部状态 | 紧,需要锁等同步 |
| 延迟 | 较高 | 低 |
| 可扩展性 | 易加新智能体,分布式友好 | 难分布式,易数据竞争 |
| 顺序保证 | 困难,需额外机制 | 强 |
| 适用 | 分布式、异步、系统级隔离 | 单进程、低延迟、关系紧密 |
一条实用默认规则:优先消息传递,因为它更安全、更易扩展;只有确实需要低延迟且智能体关系紧密时,才退回共享内存。实践中常常混用——任务之间用结构化通知显式传状态和结果,工作者之间用一块带版本号和访问日志的共享 Scratchpad 协同,跨进程走双向 HTTP 流。任务通知里有个值得单独记的细节:消息体带一个"后续任务列表"字段,列出当前任务完成后现在可以开跑的下游任务。有了它,调度就从"轮询所有任务看谁能跑"变成"事件驱动地唤醒下游",效率高得多。
不管用哪种范式,可靠通信都要守住三条底线:
- 可靠性靠三件套配套:至少一次送达 + 幂等性 + 确认机制。正因为有重试,就必然出现重复消息,所以必须靠幂等键兜底,缺一不可。
- 顺序保证:必须按序处理的消息,按 key 分组进顺序队列(FIFO),同一个任务的状态更新不能乱序。
- 背压控制:消费者追不上时,把压力反馈给发送方让它减速或暂停,而不是让队列无限膨胀直到吃光内存。永远不能假设消费者足够快。
这些听起来像消息系统常识,但在 Agent 编排里更要命——一次乱序、重复或丢失的中间结果,会让后续模型基于错误状态继续推理,错误就此被放大。
八、常见误区
编排层的坑,多半来自"看起来对,其实没想清楚责任落在哪一层":
- 把并发当默认性能开关。 能不能并发取决于依赖、副作用和资源竞争,不取决于性能冲动。写同一文件的两个任务,不能只因为"都能 async"就同时跑。
- 多智能体越多越好。 它更慢更贵,只在质量、覆盖面、独立评审比速度重要时才划算。简单任务用单智能体就够。
- DAG 画好就能跑。 没验证环、悬空依赖、孤立节点的图,会跑到一半才崩。验证图比画图更不能省。
- 副作用暂停 = 在代码里加几个审批分支。 审批逻辑散落在代码各处,等于没有提交边界。它必须作为状态明确画进流程图,才挡得住将来新增的调用入口。
- 成功率达标 = 编排健康。 同一个成功率背后,可能是疯狂重试、走了危险路径、钻了测试漏洞。不看轨迹,分不清"按计划完成"和"侥幸成功"。
- 定义了状态机 = 用上了状态机。 主循环若还在直接调度任务、绕过引擎,那状态机里的审批、检查点、审计就只是旁路代码。要追踪真实调用链,而不是看设计图。
最小心智模型
如果只留五个词:分解、定序、分工、通信、恢复。 分解把不可原子的大任务画成 DAG;定序用拓扑排序找出合法顺序与并行分层;分工用状态机表达分支审批、用角色专化破解自评偏差;通信用消息传递可靠地连接执行单元;恢复用检查点和提交边界让流程可重放、可回滚。
衡量编排做得好不好,不看"拆了多少任务",而看这几个问题有没有明确答案:
- 任务粒度是否落在合理区间,数据依赖和控制依赖分清了吗?
- DAG 是否验证过无环、无悬空依赖?哪些任务需要隔离上下文、只回传摘要?
- 每个有副作用的动作是否都标了提交边界、走了审批?相同输入是否保证相同路径?
- 是否用独立评估者绕开了自评偏差?并发子智能体的上下文隔离、结果显式提交了吗?
- 通信是否配齐了幂等、顺序、背压?进程崩溃后能否从检查点恢复?
编排不是"把任务拆得更碎",而是"把控制责任放到正确的层级"。运行时管单点执行,编排管全局调度——守住这条边界,复杂任务才不会退化成一团互相纠缠的逻辑。能干脆回答上面这些问题的编排引擎,才算把模型的推理能力,真正驯成了可重放、可恢复、可审计的可交付系统。