Skip to content

Orchestrator 与成本治理

本课交付结果

你将交付 Orchestrator.runPlan(tasks, budget):先拒绝重复 ID、无效成本和写路径冲突;再按 planner → coder → tester、同角色 ID 顺序运行;每项执行前比较剩余预算并聚合实际成本。

岗位问题

多 Agent 增加的首先是协调成本。两个角色同时写 src/a.ts 会互相覆盖;估算已超预算还启动下一角色,会在“知道付不起”的情况下继续花费。Orchestrator 的核心是治理,不是尽可能多地并行。

前置检查

前置知识快照

先完成受限 Subagent:

bash
pnpm --filter @learn-traeai/coding-agent-bootcamp verify:lesson -- 34

每个任务声明 role、estimatedCost 和 writePaths;执行结果返回 actual cost。

第 34 课证明单个 Subagent 合同不会越权,本课处理合同之间的关系。多个各自安全的任务仍可能同时写同一文件、总预算超限、结果身份串线或因输入顺序产生不同报告。Orchestrator 是 admission controller、scheduler 和 ledger,不是把 Promise.all 包一层。

教学角色固定 planner、coder、tester,稳定顺序表达信息依赖:计划先产生边界,编码基于计划,测试验证实现。同角色按 task id 排序,避免调用者数组顺序影响执行与报告。真实项目可以使用 DAG,但每条边、可并行批次和稳定 tie-break 都需显式。

预检必须扫描完整计划后才启动第一个任务。重复 ID、非法 estimate、writePaths 冲突或父预算无效都属于计划错误;若边跑边发现,前几个 child 已花费、写文件,系统无法声称“计划被拒绝”。zero-call spy 是这条不变量的直接证据。

原理拆解

mermaid
flowchart TD
  A[接收全部任务与预算] --> B[校验 budget/ID/cost/path]
  B --> C[建立 path→owner 冲突表]
  C --> D[按 role 与 ID 稳定排序]
  D --> E{actual + next estimate <= budget?}
  E -->|否| F[停止为 budget_exceeded]
  E -->|是| G[运行受限 Subagent]
  G --> H[验证 result id 与实际 usage]
  H --> I[追加结果并累计 ledger]
  I --> E

预检阶段建立 path → owner 表,任何重复写路径在 run 调用前失败。排序后逐项判断 totalActual + next.estimatedCost <= budget;不满足就返回 budget_exceeded,未启动该任务。执行后累计 actual 并验证 result ID。

代码实验

打开本课 Lab

bash
pnpm --dir bootcamps/coding-agent/labs/35-orchestrator-cost/starter test
pnpm --dir bootcamps/coding-agent/labs/35-orchestrator-cost/solution test

Starter 应声明 实现编排预算预检;Solution 应通过 5 项测试。

失败实现

ts
async function runUnsafe(tasks: OrchestratorTask[], budget: number) {
  const results = await Promise.all(tasks.map(run));
  const total = tasks.reduce((sum, task) => sum + task.estimatedCost, 0);
  return { status: total > budget ? "budget_exceeded" : "completed", results };
}

它先花完钱再宣布超预算,使用 estimate 冒充实际成本,未检查路径冲突和结果 ID,无界并发也会争抢资源。结果顺序可能来自输入或完成时点;即使最终 status 正确,治理已经失败,因为不可逆动作都发生了。

预检先建立唯一所有权:

ts
const ids = new Set<string>();
const owners = new Map<string, string>();
for (const task of tasks) {
  if (!task.id || ids.has(task.id)) throw new Error(`任务 ID 重复:${task.id}`);
  ids.add(task.id);
  if (!Number.isFinite(task.estimatedCost) || task.estimatedCost < 0) {
    throw new Error(`任务成本无效:${task.id}`);
  }
  for (const path of task.writePaths) {
    const owner = owners.get(path);
    if (owner) throw new Error(`写路径冲突:${path} (${owner}, ${task.id})`);
    owners.set(path, task.id);
  }
}

关键实现讲解

预算 10,planner 估算/实际 3,coder 估算 8:coder 在执行前停止,因为 3 + 8 > 10。路径冲突测试同时断言 runner 从未调用,证明预检没有隐藏副作用。

总 budget 是硬上限,必须为有限非负数。零预算可以运行 estimate 与实际成本均为零的纯本地任务,但生产还要考虑 token/时间等多维资源;NaN、Infinity 和负数直接拒绝。预算单位必须明确,美元、token 和抽象点不能混在一个 number 中。

task id 在计划内唯一,并用于 result、Trace、checkpoint 和用户展示的关联。空 id 或重复 id 在预检失败;不能让后者覆盖 Map。角色来自有限枚举,未知 role 不应被放到排序末尾猜测执行。自由显示标题与稳定 id 分离。

estimatedCost 是 admission 上界或保守估算,不是最终账单。估算可来自历史 P95、任务复杂度和模型配置;低估会在执行后突破硬预算,高估会提前拒绝本可完成任务。系统同时监控误差,按 role/model 校准,但不能为了提高完成率故意把估算调低。

writePaths 在任务规划时声明为规范相对路径或受限 glob。先拒绝绝对、点点与 workspace 外路径,再计算集合交集。Lab 用完全相同字符串检测,便于看清所有权;生产还要识别 srcsrc/a.ts、大小写折叠、symlink、glob 重叠和 rename 的冲突。

只检查写写冲突是最小版本。读写也可能影响正确性:tester 读取 coder 将修改的文件应在 coder 之后,而两个只读任务可并行。构建 conflict graph:写写与无依赖读写形成边,明确依赖决定方向;图有环说明计划不可调度,执行前返回诊断。

声明 writePaths 可能不准确,执行后要用实际 changedPaths 复核。总项目允许两个任务运行后发现都改 src/shared.ts,返回 completed_with_conflicts 而不是自动合并。更严格策略在隔离 worktree 中运行,每个 child 产出 patch,Orchestrator 验证路径后按顺序应用;冲突时停止并请求重新规划。

自动合并不能只按文本无冲突判断。两个 patch 修改不同代码行却改变同一语义,测试与 reviewer 仍需验证。Orchestrator 的职责是发现并呈现冲突,不假装理解所有语义。合并结果进入新 checkpoint,后续 tester 针对合并后的统一 workspace 运行。

稳定排序使用 role rank planner=0、coder=1、tester=2,再用 id locale 或 ASCII 规则。不要依赖 JavaScript sort 在 comparator 返回零时的原输入顺序,因为输入本身可能来自不稳定 Map/文件系统。若同 role 可并行,也先稳定分批,结果最终按这个规范顺序聚合。

顺序版更容易建立基线:每项执行前检查 totalActual + estimatedCost <= budget,符合才调用。估算超过剩余时未启动该任务,返回已完成结果和 actual total。用户可以增加预算或重新规划,不需要回滚未发生动作。

ts
for (const task of ordered) {
  if (totalCost + task.estimatedCost > budget) {
    return { status: "budget_exceeded", results, totalCost };
  }
  const result = await deps.run(cloneTask(task));
  validateResult(task, result);
  results.push({ ...result });
  totalCost += result.cost;
  if (totalCost > budget) {
    return { status: "budget_exceeded", results, totalCost };
  }
}

执行后仍检查 actual,因为 estimate 不是保证。若 actual 单项使总额超过 budget,当前任务已经发生,报告保留结果与超支事实,并停止后续。要真正防硬超支,Subagent 内部还必须以剩余预算设置 Provider/tool limit,在执行中取消;Orchestrator 的外层检查不能追回已消费费用。

多维 ledger 分别累计 input/output tokens、toolCalls、duration、costUsd,admission 对下一任务每个 estimate 与剩余量比较。一个任务可能美元够但 toolCalls 不够,同样拒绝。duration 并行时总墙钟与各任务 duration 和不同,报告同时提供 elapsed 与 resource sum,不能混用。

result id 必须等于 task id,cost 有限非负,value/result schema 合法。返回对象复制后存入 results,避免 executor 后续修改共享引用。实际 changedPaths、usage 和 status 也验证;一个恶意或出错 child 不能用负 cost 给总账充值,或冒用另一个任务覆盖结果。

失败策略要在计划中声明。planner 失败通常停止所有依赖;一个独立 reviewer 失败可能继续但最终 status partial;安全策略失败立即 cancel 全部。课程顺序 Runner 遇异常直接抛出,生产 Orchestrator 捕获稳定 child status,根据 DAG 与 policy 标记 skipped/cancelled,绝不把空结果当完成。

父 AbortSignal 在每项开始前检查,并传给 SubagentRunner。取消时停止启动新任务,级联所有 active child,等待资源回收,再返回 cancelled 与已完成 results。不能先返回 UI 再让 child 后台写文件;structured concurrency 要求父结束前子已结束或明确移交给持久任务管理器。

并行只用于 conflict graph 中无边、资源预算可同时预留的批次。启动前从 ledger reserve 每个 estimate,全部结束后用 actual 结算并释放差额;若不预留,两个任务都看到同一剩余预算而共同超支。并发上限与 Provider rate limit 同时约束,结果仍按稳定 task order。

预留遇到 actual 超 estimate 时从未分配余量扣除;不足则取消尚未开始/可取消任务并报告 estimate miss。已经并发执行的 sibling 可能也消费,硬成本上限需要 Provider 级共享 token bucket 原子扣减。单进程 number 累加不能在分布式 worker 间保证一致。

planner 的计划不是自动可信。Orchestrator 验证 task 数上限、role、dependency、path、permission、context 与预算,拒绝自循环和超大 fan-out。Planner 只能提出候选合同,宿主 admission 决定是否执行。模型在 prompt 里写“这些任务不会冲突”不替代 path graph 证据。

任务粒度影响协调开销。太大无法隔离路径与验收,太小重复上下文、握手和合并成本。好的任务拥有单一目标、窄读写集合、独立 completion criteria,并能在预算内完成。评测统计每任务固定开销和失败率,发现拆分收益拐点。

planner 也消耗预算,不能当免费前处理。计划可能被拒绝,但模型 token 已花费,应进入 ledger;重新规划基于剩余量。可用便宜模型生成草案,再用规则验证;连续无效计划达到上限后向用户澄清,不无限自我规划。

实际成本来自 Provider usage 与版本化 price table,工具/MCP 也可有费用。只累加模型估算会漏掉外部 API、构建分钟和子进程资源。报告按 task/role/provider/tool 分摊,价格版本可重算;审批界面显示 estimate 区间与最坏上限,不承诺虚假精确值。

Checkpoint 在每项结算后原子保存 ordered plan hash、next index、results、ledger 与 workspace hash。恢复时验证代码/计划/插件/Skill identity,completed task 不重跑;进行中任务按副作用规则处理。若仅保存 index 不保存实际结果与成本,重启会重复花费或错误计算剩余。

计划 hash 规范化任务 ID、role、dependencies、context hashes、permissions、budgets 和 writePaths,排除生成时间。输入顺序不同但语义计划相同应得到相同 hash;任务内容改变必须不同。报告链接 parent/child Trace,能从总成本下钻到每次模型和工具调用。

ts
it("rejects conflicts before starting any task", async () => {
  const run = vi.fn();
  await expect(orchestrator(run).runPlan([
    task("a", "coder", 1, ["src/a.ts"]),
    task("b", "tester", 1, ["src/a.ts"]),
  ], 10)).rejects.toThrow("路径冲突");
  expect(run).not.toHaveBeenCalled();
});

测试还需验证乱序角色变 planner/coder/tester,同角色 b/a 变 a/b;estimate 三加八、budget 十只执行 planner;actual 二而 estimate 三时 total 是二;result id 错、负 cost 和重复 ID 全失败。生产添加 glob overlap、并行 reservation、父取消与 checkpoint 恢复。

可观测指标包括计划数、预检拒绝、任务/角色分布、估算误差、实际总成本、budget stop、冲突、取消、并发和重规划。task id 不做高基标签;详情进入脱敏 Trace。估算长期系统性偏低触发校准,冲突多说明 planner 未学会划分所有权。

用户体验要在执行前展示计划、预计成本和可能写路径,高风险计划请求确认。运行中显示已花/剩余、当前角色和可取消状态;预算停止后保留已完成证据并给出增加预算、缩小范围或继续单项的选择。不要自动购买更多预算或悄悄降级安全验证。

完整验收输入 tester、planner、coder 乱序,得到稳定角色次序;同角色 b/a 得 a/b;合法计划聚合 actual;下一 estimate 超剩余时 zero-call;冲突路径在首任务前 zero-call。总项目再运行真实 Subagent,验证秘密不传、权限收缩、实际 usage ledger 与冲突报告。

从固定角色顺序升级 DAG 时,每个任务声明 dependsOn,预检验证引用存在、无自依赖、无环。Kahn 拓扑排序每轮从入度零集合按 role/id 稳定取任务;失败任务的后继标 skipped,而无关分支可按 policy 继续。只按 planner/coder/tester 能覆盖课程流水线,DAG 才表达多个文件域和 reviewer 分支。

依赖不仅传顺序,还定义可见结果。coder 只接收其 dependsOn planner 的规范 output hash,不能读取所有兄弟对话;tester 接收已合并 workspace 与 coder evidence。Orchestrator 通过 Context Builder 生成这些片段,保持第 34 课的数据最小化。没有依赖边的任务不能偷偷消费另一个结果。

计划变更需要重新预检。运行中用户修改范围、planner 新增任务或某 child 请求额外路径时,不把任务直接插队;暂停启动新任务,基于当前 checkpoint 和剩余 budget 构造新规范计划,重新做 ID、DAG、权限、路径和资源检查。已经完成结果只有合同 identity 兼容才复用。

estimate 可以由历史同 role、模型、上下文大小和工具集合训练简单预测器,但预测器也需版本与评测。返回 P50/P95 区间,admission 使用保守分位,UI 展示范围;冷启动用规则上界。实际误差进入反馈,异常任务不立即污染模型,防止一次故障把以后估算推高。

预算还应预留失败和收尾。把百分之百额度分给功能 child,会没有空间跑最终测试、生成报告或回滚;计划设置 validationReserve 与 emergencyReserve,普通任务不能消费。只有用户明确批准才释放预留,安全检查 reserve 永不被 coder 借用。

成本优化不能牺牲完成证据。为了省 token 跳过 tester 可能让任务表面 completed,却没有验证;Orchestrator 的 completion policy 指定必须角色和 mustPass task。预算不足以覆盖必需链时在执行前拒绝整个计划或请求调整,而不是先做 coder 再发现付不起测试。

角色级熔断防止同类失败放大。连续 planner 输出无效图、coder 越界或 tester 基础设施错误达到阈值,停止新同角色任务,保存已完成证据并选择 fallback。熔断原因进入 status,恢复需配置改变、冷却或人工确认,不无限重试消耗预算。

公平与优先级在多计划服务中同样重要。每个用户/项目有独立 quota,scheduler 采用加权公平队列;单个大计划不能占满所有 worker。安全修复可提高优先级但仍受硬权限和预算。租户 ID 进入内部调度键,不泄露给其他计划或高基监控标签。

分布式 worker 需要租约。Orchestrator 原子领取 task,lease 到期前续约;worker 失联后不能立即在另处重跑有副作用任务,先检查幂等 key、workspace snapshot 与 Trace。result 提交带 task/attempt/lease token,过期 worker 的晚结果被拒绝,防止双写 ledger。

账本是追加事件而非只改 total number:RESERVED、STARTED、USAGE_REPORTED、SETTLED、RELEASED、ADJUSTED 每条带 sequence 和 task。崩溃恢复重放得到同一余额,重复 usage event 用 idempotency key 去重。最终 total 是投影,审计可以解释每一笔费用来源。

Provider usage 常在流结束后才完整返回,网络中断可能 unknown。unknown 不能当零;先按 reserve 扣住,后台对账后结算,多余释放。长时间无法对账标 uncertainty,阻止使用可能已经消耗的额度。财务账单与本地估计差异超过阈值触发告警。

路径 ownership 可按组件层级声明,例如 packages/core/**,但过宽声明会串行所有任务。Planner 用 Repo Context 找最小预期集合,coder 实际需要新增路径时先暂停申请扩展;Orchestrator 重新检查与其他 active/reserved owner 冲突,获准后更新合同。不能先写再补申报。

对生成文件与 lockfile 要有特殊政策。多个任务可能间接改同一个 lock,即使各自 source path 不重叠;把共享派生产物映射到逻辑资源锁,例如 dependency-graph。最终统一由一个 integration task 重新生成,可降低冲突。格式化全仓库也属于广域写,不应藏在局部 coder 中。

测试资源同样有所有权:固定端口、数据库 schema、浏览器 profile、缓存和环境变量。resourceClaims 进入预检,冲突任务串行或分配独立命名空间。只检查文件路径会漏掉最常见的并行 flaky 来源。资源分配结果进入 child contract,不让任务自行猜端口。

合并阶段应用 patch 前验证 baseHash。若 workspace 已被前一任务改变且 patch 基于旧基线,使用三方合并或拒绝重新运行,不能盲 apply。每次成功合并生成新 checkpoint hash,tester 绑定该 hash;报告能证明测试对应最终代码,而不是某个分支副本。

最终 reviewer 可以检查任务之间的接口而非重复所有工作:计划是否满足用户需求、Diff 是否超范围、测试是否覆盖、Trace/ledger 是否完整。reviewer 默认只读,发现问题返回新 task proposal,由 Orchestrator 重新 admission;它不能直接修代码绕过 ownership。

发布、部署和外部消息是高影响 task,永远不因前序成功自动获权。计划可包含 proposed deploy,但执行前需要单独审批、环境锁、发布预算与回滚条件。Orchestrator 的“completed”只说明当前获批任务完成,不扩大用户最初授权范围。

评测多 Agent 不只看最终成功率,也看冲突率、计划拒绝率、estimate error、协调 token、空闲等待和恢复次数。与单 Agent 比较相同 benchmark,确认复杂任务收益覆盖协调成本。若 Orchestrator 频繁退化为串行且上下文重复,可能不值得多角色。

混沌测试随机让 child 超时、返回错 id、负 usage、改越界路径、worker exit 或 checkpoint 写失败。无论注入点,ledger 不重复扣款,未获 owner 的 patch 不合并,父取消后无 active child,恢复得到同一已完成集合。正常单元测试无法替代这类跨组件不变量。

运维 runbook 在 budget_exceeded 时先冻结新任务,核对 reserve/actual/unknown,再展示剩余必需链;在 conflict 时保留各 patch 和 baseHash,不手工覆盖;在 child 卡住时取消并等待回收。所有人工调整写 ADJUSTED 账本事件和理由,不直接改 JSON total。

Week 7 的最终项目演练可以让 planner 只读生成计划,coder 限定路径实现,tester 只执行验证:三者 context、permission、budget 单调受限,extensions Trace 关联父子;估算不足时 tester 不启动,路径冲突时任何 child 都不启动。它把前五课从独立 API 连接成一个可治理扩展系统。

成熟 Orchestrator 的核心承诺有四条:计划错误零副作用,执行顺序可复现,每笔实际资源可解释,任何取消或冲突都停止扩大影响。多 Agent 的价值不是角色数量,而是在这些承诺下把可独立验证的工作安全组合起来。

运行与验证

bash
pnpm --filter @learn-traeai/coding-agent-bootcamp verify:lesson -- 35
pnpm --filter @coding-agent/extensions test

乱序输入 tester/planner/coder 的实际执行顺序必须稳定为 planner/coder/tester,同角色 b/a 稳定为 a/b。

真实运行输出

text
✓ tests/lab.test.ts (5 tests)
Test Files  1 passed (1)
Tests       5 passed (5)

项目验证还应展示 extensions orchestrator 的多维 ledger、parent cancellation、实际 changedPaths 冲突与 Week 7 smoke。绿色测试之外保存计划 hash 和 zero-call 预检证据。

常见失败与排查

故障案例 1

症状:第二个 child 执行后才发现同写 src/a.ts,已有 patch 被覆盖或测试基于混合状态。

根因:未在完整计划上建立 write ownership,或只比较完全相同字符串而遗漏目录/glob 重叠。

定位:打印规范 write set 与 conflict graph,断言冲突计划 runner 调用数为零;执行后对比 actual Diff。

修复:全量预检规范路径和写写/读写边;无冲突才调度,实际 changedPaths 再独立验证。

故障案例 2

症状:报告 totalCost 等于估算而账单不同,或预算十仍启动 estimate 八的 coder。

根因:用 estimate 结算,下一任务判断发生在 run 之后,actual 超估未停止后续。

定位:让 estimate 三、actual 二,再组合剩余边界,比较 run spy、ledger 与 Provider usage。

修复:estimate 只做执行前准入,actual 验证后入账;超估触发停止,底层共享预算落实硬上限。

故障案例 3

症状:相同语义计划因输入数组不同改变角色/结果次序,恢复或报告 hash 不一致。

根因:保留输入顺序、同角色没有 ID tie-break,或并发按完成顺序 push。

定位:原序、反序与随机序运行固定 executor,比较执行顺序、规范 plan 与报告 hash。

修复:角色 rank 加 ID 稳定排序;并发结果按规范 task order 收集,checkpoint 绑定规范 plan hash。

课后作业

加入读写集合冲突、并行无冲突批次、全局 token/时间预算和失败取消;比较串行与受控并行的成本、延迟和合并冲突率。

验收 Rubric

维度通过标准常见扣分
预检ID/路径/成本在执行前验证边跑边发现
顺序角色与 ID 稳定依赖输入顺序
预算下一任务前判断超支后才停止
聚合使用实际成本与结果副本只记录估算

总项目增量

总项目包:@coding-agent/extensions

总项目路径:packages/extensions/src/orchestrator.ts

总项目验证命令:pnpm --filter @coding-agent/extensions test

Week 7 完成扩展与多角色治理。Week 8 将产品化 CLI、Provider、端到端 Bug Fix、回归门禁与作品集证据。

延伸阅读

  • Multi-agent scheduling 与 conflict graph。
  • Cost-aware planning。
  • Preflight admission control。

方案对比与工程取舍

完全串行延迟较高,却让依赖、workspace 和预算最容易证明,适合首个正确版本;无界并行最快但冲突和超支不可控;基于 conflict graph 的固定并发池在安全任务间取得平衡。先以串行报告作为语义基线,再证明并发结果等价。

静态 writePaths 可在执行前拒绝,代价是模型或作者可能声明不准;完全依赖事后 Diff 准确却发现太晚。两者组合最合理:声明用于 admission,隔离 worktree 与 actual Diff 用于验证。高风险任务声明过宽也会减少并行,应通过反馈优化 planner。

硬停止保护预算,可能留下“计划完成百分之八十”的部分结果;自动超支追求完成却破坏用户控制。Orchestrator 保留已验证结果与恢复点,让用户决定增资或缩小剩余任务。费用治理应像权限审批一样是显式产品行为。

下一课衔接

Week 7 已完成 Skill、Plugin、MCP、Subagent 与 Orchestrator 的扩展治理。Week 8 会把这些能力装进可发布产品:CLI UX、真实 Provider、端到端 Bug Fix、评测回归门禁与作品集证据。所有发布动作继续受本课计划、预算和 Trace 约束。

从零实现 Mini Code Agent Runtime