← 工程跃迁 / 第 11 章
架构定位

工作流与编排

把复杂任务拆成可恢复的步骤,并通过明确合同协调执行单元。

如何拆解复杂任务,并协调步骤或多个 Agent?

概念校准

规划

它是什么
为多步骤、高成本或需要恢复的任务建立可检查、可更新的执行结构。
它不是什么
不是所有 Agent 的必备步骤,简单任务常常不需要独立规划器。

Agent

它是什么
围绕目标和状态,在约束下选择动作、执行、观察并验证结果的运行系统。
它不是什么
不是独立人格,也不是把模型放进无限循环。

验证与评测

它是什么
用测试、Schema、引用、状态和验收规则检查单次结果,并用数据集衡量系统表现。
它不是什么
不等于让模型对自己的输出再给一次意见;验证必须落到外部可检查证据。

Planner/Executor 与 Agentic 工作流

构建任务图、规划器、执行器、验证、重规划、预算和死循环防护。

本章只做一件事复杂需求被拆成可恢复任务图,失败时只重做受影响节点。
开始前,Forge 已经具备已完成 Module 10 的“状态、会话、记忆与上下文压缩”,其通过验收的 solution 是本章起点。
完成后,Forge 将能够把复杂需求拆成可恢复任务图,并在失败后局部重试。
卡住时的最小恢复点使用人工编写的三节点计划,先验证状态迁移。
下载本章实验仓库Python 3.12 · pytest · Pydantic · SQLite · deterministic Mock
阶段能力链
01用有向无环图表达任务依赖,并拒绝缺失依赖和循环02把目标、任务图和最终交付物绑定为可验证计划03只执行依赖已成功的任务,并记录每个任务的结构化结果04在任务成功前用结构、断言和证据验证执行结果05失败后只重置受影响子图,保留无关已完成任务的证据06在执行前预留步骤、Token 和成本预算,并拒绝超额消费07组合计划、执行、验证和预算,在失败时返回可恢复的局部状态
每一步只增加一种可验证能力;后一步建立在前一步已经通过的代码和测试上。

概念校准\n\n在开始操作代码前,先把本章涉及的概念放回正确的工程边界。\n\n
\n\n### 规划 planning\n\n- 它是什么: 为多步骤、高成本或需要恢复的任务建立可检查、可更新的执行结构,明确依赖、预算、状态和重新规划条件。\n- 它不是什么: 它不是所有 Agent 的必备阶段;简单低风险任务可能直接执行更快、更可靠,独立规划器必须证明价值。\n- 与相邻概念的关系: Agent Loop 可直接选择下一动作或执行计划节点;Verification 决定节点完成,State 与 Checkpoint 支持局部重试。\n- 在 HeatStack Forge 中的位置: Forge 将复杂请求转成带依赖、预算、验收和恢复点的任务图,执行器只领取可运行节点,失败后按证据局部重新规划。\n- 典型误用与修正: 误用是生成很长的静态计划并严格照做。修正方法是保持计划最小、状态可验证,并允许输入或工具变化时局部更新。\n\n### Agent agent\n\n- 它是什么: 围绕目标和当前状态,在约束下选择动作、执行、观察并验证结果的运行系统。核心运行环是:目标/状态 → 选择动作 → 执行工具 → 观察结果 → 验证 → 停止或重新规划。\n- 它不是什么: 它不是独立人格,也不是让模型无期限循环。模型只是决策组件,动作、权限、状态和停止条件由外部运行环境共同约束。\n- 与相邻概念的关系: Context 提供可见信息,Tool 提供动作,Harness 控制权限和副作用,Verification 判断结果,Planning 只在复杂任务需要时加入。\n- 在 HeatStack Forge 中的位置: Forge 读取任务合同与状态,选择一个受许可动作,记录工具结果,再用测试、Schema 或验收规则决定成功、失败、预算耗尽、重复动作、人工介入或重新规划。\n- 典型误用与修正: 误用是把“模型继续生成”当成进展。修正方法是让每轮产生可观察的状态变化,并为所有停止原因建立确定性检查。\n\n### 验证与评测 evaluation\n\n- 它是什么: Verification 用测试、Schema、引用、状态或验收规则检查一次结果;Evaluation 用数据集和指标衡量多次运行表现。\n- 它不是什么: 它不是让模型对自己的回答再给一次意见;通过与否必须落到外部可检查证据。\n- 与相邻概念的关系: Agent Loop 依赖 Verification 决定停止,RAG 需要分层评测,Planning 需要节点验收,Observability 提供运行记录。\n- 在 HeatStack Forge 中的位置: Forge 为单次运行保存合同测试与 evidence,再用固定场景集比较质量、延迟、成本、权限和恢复能力。\n- 典型误用与修正: 误用是把一个综合分数当成全部结论。修正方法是定义分层指标、失败样本和版本基线,并结合产品风险评审。\n\n

STAGE 01

用有向无环图表达任务依赖,并拒绝缺失依赖和循环

先看一个具体问题

当图中剩余任务都不可执行时,这是成功完成还是死锁?

你正在为 HeatStack 的规划器构建任务图模块,需要把复杂需求拆成可恢复的任务依赖结构。

当前测试要求 TaskGraph 在构造时拒绝缺失依赖和循环,并且 topological_order() 返回尊重依赖的元组。

一个常见的错误是:当 pending 非空但 ready 为空时,返回部分结果,导致循环图被当作成功完成。

你必须区分“所有任务已完成”和“剩余任务无法执行”这两种情况,后者必须抛出 ValueError

先做判断

topological_order() 中,如果 pending 非空但 ready 为空,你认为应该如何处理?

  • 返回当前已完成的顺序,因为这是部分拓扑排序
  • 抛出 ValueError 并提示图中存在循环
  • 跳过这些任务,继续处理其他任务
  • 返回空元组表示没有可执行任务

判断依据: 正确选项是抛出 ValueError。因为 pending 非空意味着还有任务未完成,而 ready 为空说明这些任务都依赖尚未完成的任务,这必然构成循环依赖,否则至少有一个任务可以执行。

它是什么

任务图的结构验证

TaskGraph 是一个有向无环图(DAG)的容器,它存储任务及其依赖关系,并提供拓扑排序。

拓扑排序是一种线性顺序,使得每个任务都出现在其所有依赖之后,这是规划执行顺序的基础。

构造函数会立即调用 topological_order() 来验证图是否合法,从而在创建时就拒绝循环和缺失依赖。

它不是什么

任务图不是执行引擎

TaskGraph 不执行任何任务,也不管理任务状态或预算,它只负责结构验证和排序。

它不处理任务失败后的重试或恢复,这些属于后续阶段的能力。

它不区分任务的优先级或成本,只关心依赖关系。

它与相邻概念的关系

与 Agent 循环的关系

Agent 循环可以直接选择下一个动作,也可以执行计划节点,而 TaskGraph 提供了计划节点的依赖顺序。

topological_order() 的输出可以作为执行顺序的参考,但实际执行仍需由外部运行时控制。

验证(Verification)决定节点是否完成,而 TaskGraph 只保证结构合法性,不参与验证。

本阶段的边界决定:pending 非空但 ready 为空时,必须抛出 ValueError,因为这是死锁而非完成;返回部分结果会掩盖循环依赖,导致后续执行无法进行。

STAGE 02

把目标、任务图和最终交付物绑定为可验证计划

先看一个具体问题

交付物必须是终态节点

在软件交付场景中,你负责实现 create_plan 函数,它需要把用户目标、任务图和最终交付物绑定成一个 Plan 对象。

当前测试 test_plan_binds_goal_to_a_terminal_deliverable 要求当交付物是终态节点(如 verify)时,返回的 plan.deliverable_task_id 必须等于该节点 ID,且拓扑顺序正确。

另一个测试 test_nonterminal_deliverable_is_rejected 要求当交付物是中间节点(如 code,它被 verify 依赖)时,必须抛出 ValueError 且错误消息包含 terminal

还有一个测试 test_unknown_deliverable_is_rejected 要求当交付物 ID 不在图中时,抛出 ValueError 且消息包含 missing

你的任务是创建 forge/workflow/planner.py,实现 Plan 数据类和 create_plan 函数,满足上述三个测试。

先做判断

在实现 create_plan 时,你认为检查交付物是否存在于图中就足够了吗?请选择最符合你预期的选项。

  • 是的,只要交付物 ID 在图中,计划就是有效的。
  • 不,还需要检查交付物是否被其他任务依赖,即是否为终态节点。
  • 不,还需要检查交付物是否在拓扑排序的第一个位置。
  • 不,还需要检查交付物是否与目标字符串相同。

判断依据: 正确选项是第二个。仅检查存在性会允许中间节点作为交付物,导致计划在验证任务之前就报告完成,违反测试 test_nonterminal_deliverable_is_rejected 的要求。

它是什么

终态节点与交付物

终态节点是指没有任何其他任务依赖它的任务,即它不出现在任何任务的 depends_on 列表中。

交付物必须是终态节点,因为最终交付物代表整个计划的完成,如果它还有后续任务,那么计划在交付物完成时并未真正完成。

create_plan 通过构建 TaskGraph 来验证图结构,然后检查交付物 ID 是否在图中,最后检查它是否在 depended_on 集合中。

它不是什么

不是仅存在性检查

仅检查交付物 ID 存在于图中是不够的,因为中间节点也存在,但它不是计划的终点。

交付物不是拓扑排序的第一个节点,而是最后一个节点,因为它是所有依赖链的终点。

交付物不要求与目标字符串相同,目标只是描述性文本,交付物是具体任务 ID。

它与相邻概念的关系

与 TaskGraph 和依赖的关系

TaskGraph 在构造时验证循环和缺失依赖,create_plan 利用它来获取任务集合和拓扑顺序。

depended_on 集合是从所有任务的 depends_on 中提取的,它表示哪些任务被其他任务依赖。

交付物必须不在 depended_on 中,这保证了它是终态节点,从而保证计划完成时所有前置任务都已执行。

本阶段的边界决定: create_plan 只负责验证和绑定,不执行任何任务;它不检查目标是否与任务内容匹配,也不检查任务是否可执行,这些属于执行阶段的责任。

STAGE 03

只执行依赖已成功的任务,并记录每个任务的结构化结果

先看一个具体问题

为什么依赖任务有结果记录不等于依赖任务成功?

在软件交付场景中,你有一个任务图:inspect 检查代码,code 修改代码,test 运行测试。code 依赖 inspecttest 依赖 code。如果 inspect 执行失败,code 就不应该运行。

当前 forge/workflow/executor.py 文件不存在,测试无法导入 ExecutorTaskResult。你需要创建这个文件,实现一个执行器,它只调度那些所有依赖任务状态为 succeeded 的任务。

关键决策是:判断一个依赖是否满足时,是只检查 results 字典里有没有这个任务的记录,还是必须检查该记录的状态是否为 succeeded?如果只检查记录存在,那么失败的依赖也会被当作满足,导致后续任务错误执行。

先做判断

Executor.ready 方法中,判断依赖是否满足时,以下哪种做法是正确的?

  • 只要 dep in self.results 就认为依赖满足,因为结果已经记录了。
  • 必须检查 self.results.get(dep).status == 'succeeded',只有成功才满足。
  • 依赖任务只要被调度过就算满足,不需要看结果。
  • 依赖任务是否满足由任务图决定,执行器不需要检查。

判断依据: 正确答案是第二个选项。依赖任务有结果记录只表示它执行过,但可能失败。只有状态为 succeeded 才表示依赖真正满足,后续任务才能安全执行。

它是什么

依赖感知执行器是什么

Executor 是一个调度器,它根据任务图中的依赖关系和已记录的任务结果,决定哪些任务可以立即运行。

ready 方法返回所有未执行且所有依赖都已成功的任务 ID 列表,并按字母顺序排序以保证确定性。

run_ready 方法调用 ready 获取就绪任务,依次执行它们的处理器,捕获异常并将结果记录为 TaskResult

它不是什么

依赖感知执行器不是什么

它不是任务图本身,任务图只定义任务和依赖关系,不负责调度。

它不验证任务输出的内容是否正确,只关心任务是否成功完成(没有抛出异常)。

它不管理预算或重试策略,只是简单地执行当前就绪的任务。

它与相邻概念的关系

与其他组件的关系

Executor 使用 TaskGraph 来获取任务和依赖信息,但不修改图结构。

TaskResult 是执行器记录每个任务结果的数据类,包含任务 ID、状态、输出和错误信息。

run_ready 依赖 ready 来确定哪些任务可以执行,而 ready 依赖 self.results 中已记录的任务状态。

本阶段的边界决定: 执行器只负责按依赖就绪规则调度任务并捕获处理器异常;不验证输出内容,不管理预算。

STAGE 04

在任务成功前用结构、断言和证据验证执行结果

先看一个具体问题

任务处理器正常返回,但结果可能无效

在软件交付场景中,执行器调用任务处理器后返回一个 TaskResult,但处理器没有抛异常并不代表输出满足业务要求。

例如,一个构建任务可能返回 {'tests': 'failed'},如果只检查异常,就会错误地把失败标记为成功。

本阶段需要引入 validate_output 函数,用 ResultContract 声明必需键和断言检查,确保只有通过验证的结果才能被视为成功。

先做判断

如果任务处理器返回 {'tests': 'failed'} 而没有抛出异常,执行器应该如何处理?

  • 直接标记为成功,因为处理器正常返回
  • 检查输出是否包含必需键,但不检查值
  • 使用合同中的断言检查输出,如果断言失败则标记为无效
  • 忽略输出内容,只记录日志

判断依据: 正确答案是使用合同中的断言检查输出。处理器正常返回只说明没有异常,但业务规则可能要求 tests 必须为 'passed'。合同中的 checks 元组包含可调用对象,每个检查返回布尔值,只有所有检查都通过且必需键都存在时,结果才有效。

它是什么

结果验证是外部合同检查

validate_output 接收任意输出对象和一个 ResultContract,返回一个 ValidationResult,其中包含 valid 布尔值和 errors 元组。

它首先检查输出是否为 dict,如果不是则立即返回无效,并附带错误 'output must be an object'

然后检查所有必需键是否存在,缺失的键会生成错误信息,最后遍历所有检查函数,任何返回 False 的检查都会添加 'check N failed' 错误。

它不是什么

结果验证不执行任务也不修改状态

validate_output 是一个纯函数,它只读取输出和合同,不调用任何任务处理器,也不改变任何全局状态或数据库。

它不负责重试或恢复,如果验证失败,调用方需要根据错误信息决定下一步动作。

它也不替代类型检查或运行时监控,只关注合同声明的结构和断言。

它与相邻概念的关系

验证与执行器的协作

执行器在任务处理器返回后调用 validate_output,根据 ValidationResult.valid 决定是否将任务标记为成功。

ResultContract 由规划器或调用方构造,包含 required_keyschecks,它们共同定义了任务输出的验收标准。

验证失败时,errors 元组提供了具体原因,便于日志记录和调试,也支持后续的局部重试。

本阶段的边界决定: 验证只检查输出是否符合合同,不关心任务如何执行;如果输出不是 dict,立即返回无效,因为后续的键检查和断言都假设输出是字典。

STAGE 05

失败后只重置受影响子图,保留无关已完成任务的证据

先看一个具体问题

失败后如何只重置受影响子图?

在软件交付场景中,任务图包含 inspect、code、test、publish 和 docs 五个任务,其中 code 依赖 inspect,test 依赖 code,publish 依赖 test,docs 也依赖 inspect。

当 code 任务失败时,直接依赖它的 test 需要重置,但 test 的后继 publish 也必须重置,因为 publish 依赖 test 的结果。

如果只重置直接依赖失败任务的后继,就会遗漏传递性后代,导致后续执行时使用过期的结果。

因此需要实现一个 replan 函数,它接收任务图、当前结果字典和失败任务 ID,返回需要重置的任务集合和可以保留的任务集合。

先做判断

在下面的任务图中,如果 code 失败,哪些任务应该被重置?

  • 只重置 code
  • 重置 code 和 test
  • 重置 code、test 和 publish
  • 重置 code、test、publish 和 docs

判断依据: 正确答案是重置 code、test 和 publish。因为 test 直接依赖 code,而 publish 依赖 test,所以 publish 也必须重置。docs 虽然依赖 inspect,但 inspect 没有失败,所以 docs 可以保留。

它是什么

局部重规划是什么

局部重规划是在任务图中识别出因某个任务失败而受影响的所有任务,包括直接依赖和传递依赖,并将它们标记为需要重置。

它通过从失败任务开始,反复查找依赖集合与已受影响集合有交集的任务,直到没有新任务加入为止。

最终返回两个有序元组:reset_task_ids 包含所有需要重置的任务 ID,preserved_task_ids 包含状态为 succeeded 且不在重置集合中的任务 ID。

它不是什么

局部重规划不是什么

它不是执行任务或修改结果字典,而只是计算哪些任务需要重置和哪些可以保留。

它不是只重置直接依赖失败任务的任务,而是必须包含所有传递性后代。

它不是对整个任务图进行全局重置,而是只针对受影响子图进行局部处理。

它与相邻概念的关系

与其他组件的关系

replan 函数依赖 TaskGraph 和 Task 来获取任务依赖关系,依赖 TaskResult 来获取任务状态。

它返回的 ReplanDecision 对象包含 reset_task_ids 和 preserved_task_ids,供执行器在重试时使用。

affected_subgraph 函数是 replan 的核心,它通过迭代计算受影响的任务集合。

本阶段的边界决定: replan 只计算受影响子图并分类 reset 和 preserved 集合;不执行任务,不修改 results 字典。

STAGE 06

在执行前预留步骤、Token 和成本预算,并拒绝超额消费

先看一个具体问题

为什么只检查步骤和成本不够?

在软件交付场景中,一个工作流可能包含多个任务,每个任务消耗步骤、Token 和成本。如果预算只检查步骤和成本,而忽略 Token,那么一个消耗大量 Token 的任务可能被错误地允许执行,导致超出 Token 预算。

例如,假设预算为 max_steps=3, max_tokens=100, max_cost=1.0。先执行一个任务消耗 (1, 40, 0.25),此时已用 Token 为 40。如果下一个任务消耗 (1, 70, 0.25),则总 Token 将达到 110,超过 100,但若只检查步骤和成本,该任务会被允许,从而违反预算约束。

因此,必须同时检查三个维度,确保任何维度超出预算时都拒绝执行,并且不修改任何使用量计数器。

先做判断

如果 WorkflowBudget 的 can_reserve 方法只检查步骤和成本,而忽略 Token,那么当已用 Token 为 40,尝试消耗 70 Token 时会发生什么?

  • 抛出 RuntimeError,因为 Token 超限
  • 不抛出异常,因为步骤和成本未超限
  • 抛出 ValueError,因为 Token 为负数
  • 修改使用量计数器,但记录警告

判断依据: 正确答案是“不抛出异常,因为步骤和成本未超限”。因为 can_reserve 只检查步骤和成本,而 Token 维度被忽略,所以即使 Token 超限,也不会触发 RuntimeError。这会导致预算防护失效。

它是什么

WorkflowBudget 是什么

WorkflowBudget 是一个数据类,用于跟踪工作流执行过程中三个资源维度的累计使用量:步骤数、Token 数和成本。

它提供 can_reserve 方法来判断是否可以预留指定的资源量,以及 consume 方法来实际消耗资源。

consume 方法在预留成功后才更新使用量计数器,确保原子性:要么全部更新,要么全部不更新。

它不是什么

WorkflowBudget 不是什么

WorkflowBudget 不负责决定重规划策略,也不执行任何任务。它只负责跟踪和检查资源使用情况。

它不是一个通用的资源管理器,而是专门针对工作流预算的三个维度设计的。

它不提供回滚机制,一旦资源被消耗,就无法撤销,因此必须在消耗前进行严格的检查。

它与相邻概念的关系

与其他组件的关系

WorkflowBudget 与 Executor 和 replan 模块协同工作。Executor 在执行任务前调用 budget.consume 来预留资源,如果抛出异常,则触发 replan 进行重新规划。

预算检查是执行流程中的一道防线,确保不会超出预设的资源限制。

can_reserve 方法使用 <= 比较,意味着当使用量恰好等于最大值时,仍然允许预留,这允许最后一个任务在预算恰好用完时执行。

本阶段的边界决定: 预算检查必须在任何资源消耗之前完成,并且任何维度超限都应阻止消耗。使用 <= 而不是 < 允许在预算恰好用完时执行最后一个任务,但负数使用量必须被拒绝,因为它表示无效的消耗。

STAGE 07

组合计划、执行、验证和预算,在失败时返回可恢复的局部状态

先看一个具体问题

任务处理器正常返回但验证不通过时,工作流应该继续执行后续任务还是立即返回可恢复状态?

在软件交付场景中,run_workflow 需要按拓扑顺序执行任务,并在每个任务完成后验证其输出是否符合契约。

如果 code 任务处理器返回了 &#123;'ok': False&#125;,但验证契约要求 value['ok'] is True,那么 validate_output 会返回 valid=False

此时工作流不能继续执行依赖任务 verify,因为 verify 的输入可能基于无效的 code 输出,继续执行会导致错误传播。

正确行为是立即调用 replan 并返回 status='needs_replan',同时提供需要重置的任务 ID 元组,以便上层进行局部重试。

先做判断

run_workflow 中,当 validate_output 返回 valid=False 时,以下哪种行为最符合可恢复工作流的设计?

  • 继续执行后续任务,因为处理器没有抛出异常,任务本身是成功的。
  • 立即停止执行,调用 replan 并返回 needs_replan 状态,避免无效输出影响依赖任务。
  • 忽略验证结果,将任务标记为成功并继续,最后统一处理失败。
  • 抛出异常终止整个工作流,让调用者决定如何处理。

判断依据: 正确选项是第二个。处理器正常返回只代表执行没有异常,不代表输出满足契约。验证失败意味着任务结果不可信,继续执行依赖任务会浪费预算并可能产生级联错误。立即返回 needs_replan 状态并携带重置任务列表,允许上层只重试失败部分,符合局部恢复原则。

它是什么

可恢复工作流的核心机制

run_workflow 是一个组合器,它按拓扑顺序调度任务,对每个任务执行、验证、消费预算,并在失败时返回可恢复状态。

它依赖 Executor 执行任务,依赖 validate_output 检查输出契约,依赖 replan 生成重置决策,依赖 WorkflowBudget 控制资源消耗。

当验证失败时,工作流立即停止,返回 WorkflowRun(status='needs_replan', completed=..., reset=...),其中 reset 包含需要重新执行的任务 ID。

它不是什么

可恢复工作流的边界

它不是简单的顺序执行器,不会忽略验证结果继续运行后续任务。

它不负责实现拓扑排序或重规划算法,而是调用已有的 Executor.readyreplan 公共 API。

它不处理任务内部异常(处理器抛异常)与验证失败完全相同:处理器异常通过 result.status == 'failed' 检测,验证失败通过 validation.valid == False 检测,两者都触发 replan 并返回 needs_replan,但 completed 元组在验证失败时会排除当前任务。

它与相邻概念的关系

与其他模块的关系

run_workflow 使用 Executor 获取就绪任务并执行,使用 validate_output 检查结果,使用 replan 生成重置决策,使用 WorkflowBudget 在每次执行前消费预算。

预算消费必须在 run_ready 之前,以确保即使任务执行失败或验证失败,预算也被正确扣除,防止无限重试耗尽资源。

completed 元组只包含状态为 succeeded 且通过验证的任务;验证失败的任务即使处理器成功,也不应出现在 completed 中。

本阶段的边界决定:validate_output 返回 valid=False 时,必须立即调用 replan 并返回 needs_replan,不能继续执行任何依赖任务;同时,当前失败任务应从 completed 中排除,但之前成功的任务保留。

完成本章

把复杂需求拆成可恢复任务图,并在失败后局部重试。

FORGE / LOCAL CHECK

本地实验自检

  1. 未开始
  2. 2阅读中
  3. 3实验已下载
  4. 4测试结果已读取
  5. 5本地自检通过

verification.json 只在当前浏览器中解析,不会上传。这里验证的是实验合同,不是服务器认证或第三方背书。

概念校准与一周复习

三道题检查你是否掌握了本章边界、交付证据和恢复方法。答案只保存在当前浏览器。

  1. 1哪一项最能证明你真正完成了「Planner/Executor 与 Agentic 工作流」?
  2. 2关于「规划」,哪一种理解最准确?
  3. 3实验卡住时,哪个动作是本章建议的最小恢复点?