专题篇章 · 拆开 DeepSeek Harness

workflow / schedule / plan / todo:编排原语的取舍

四种编排原语各管什么,为什么没做成一个大而全

本页解决的问题

先给结论

「workflow / schedule / plan / todo:编排原语的取舍」要解决的关键问题是什么?

四种编排原语各管什么,为什么没做成一个大而全

判断标准

让这个结论先证明自己值得留下。 把这一页当成决策工具,而不是需要背下来的定义。把概念连到一个真实任务、一个可观察结果,以及一个能改变你判断的失败上。

下一步

写下一个问题:试完这个方法后,你能用什么证据回答它?

常见误区

结论听起来很完整,却没有检查最关键的假设。

课程目标读完你能说清三件事:DSH 把多步编排拆成的四个原语各自的适用边界,workflow 管执行、schedule 管时间、plan 管协作姿态、todo 管进度展示;模型写脚本与框架状态机这两种编排思路在四个原语上怎么分工;以及为什么这四样东西刻意没有合成一个统一的任务系统。
交互演示 · 原语选择器

四个真实场景,每个场景选一个原语。选错也没关系,判词会讲清楚为什么。点「播放」可以看自动讲解,自己点卡片可以随时抢答。

场景 1 / 4
等待第一个场景。
演示为教学化归纳,各原语的边界事实分别出自 docs/subsystems/workflow.zh.mdschedule.zh.mdplan.zh.mdpackages/todo/tool-todo/README.zh.md,核对日期 2026-08-13。
逻辑拆解 · 四个原语各管一摊

先给结论:这四个原语没有共享一个任务引擎,它们连持久化形态都不一样。workflow 是一次性的:模型写一段 JS 脚本,引擎在 node:worker_threads 的 vm 里执行,脚本里的 agent() 打回宿主起子 Agent,跑完只留结果与展示记录,逻辑本身不落成持久状态机。schedule 是持久的:create、dispatch、delete 都是 schedule/change 会话事件,回放日志就能重建全部提醒状态。plan 更轻,就是一个 plan/mode 布尔事件的日志折叠。todo 是快照:每次 todo_write 整表替换,UI 靠投影渲染最新一份。

大纲里那个问题「多步编排应该是模型写脚本还是框架状态机」,DSH 的回答是两个都要,但分工明确。执行编排交给模型写脚本,因为编排逻辑千变万化,框架预设不完;时间、姿态、展示交给框架状态机,因为这三样需要跨轮次甚至跨重启的确定性,模型的脚本给不了。workflow 文档自己说了,它的 meta 字段词汇与 Claude Code 的 dynamic workflows 对齐(workflow.zh.md 第 41、49 行),思路同源,落点不同。

还有一条容易忽略的纪律。workflow 脚本里拼错一个 agent() 选项,抛的是 fatal: true 的 WorkflowError,parallel() 组合器对它直接重抛、终止整个脚本;只有子 Agent 真实的运行失败才映射成逐项的 null(workflow.zh.md 第 116 行)。写错代码和运行失败是两类错误,混在一起脚本就没法调了。

workflow · 执行编排 模型写 JS 脚本 · worker + vm 执行 agent() 回宿主起子 Agent · 一次性,不留状态机 谁拿方向盘:模型 schedule · 时间 schedule/change 事件持久化 · 只在本会话内交付 错过的间隔合并成一次 · followup 不打断当前轮 谁拿方向盘:框架状态机 plan · 协作姿态 plan/mode 日志事件的折叠 · 激活时注入指引段落 软性指引,硬限制归沙箱与审批 · 退出过人机评审 谁拿方向盘:框架状态机 todo · 进度展示 todo_write 整表替换 · todo/write 事件 + 投影渲染 给人看的,不驱动执行 · 单一所有者,子 Agent 不共享 谁拿方向盘:框架状态机 四个原语 · 四种持久化形态 · 都是可选能力,agent loop 不依赖任何一个 合成一个大而全的任务系统,四种生命周期就得强行共享一套状态,谁都说不清自己是什么
教学化结构图:节点与连线用于解释源码关系,内容经过课程化整理。
plan 不是权限

plan mode 是软性指引:激活时往系统提示词里加一段 plan:policy,工具目录一个不变(为了请求缓存稳定)。真正拦住写操作的是沙箱和审批,两者都不读 plan 状态,要分别配。

schedule 不出会话

提醒只以 followup 轮次回到原会话,没有推送、没有外部通知通道,冷会话不干活。交付语义是至少一次:准入后、落 dispatch 前崩溃,恢复会重复一次提醒。

todo 不驱动执行

todo_write 是纯展示状态:整表替换、落日志、投影给 UI。没有部分更新、没有回读工具、没有稳定 id。把它当任务引擎用,是对这个原语最常见的误读。

关键证据 · 错过合并与 pending 切换

先看 schedule 的固定速率决策,这是本课唯一值得整段看的代码:会话离线错过了 N 个到期时点,恢复后不逐个补发,一次除法直接算出最新一次到期,再把记录推进到未来。不枚举、不回放、不积压:

packages/schedule/schedule/src/domain.ts第 536 至 543 行节选
  const steps = Math.floor((acceptedAt - target) / interval)
  const occurrence = target + steps * interval
  /* v8 ignore next -- bounded operands and a quotient-derived product stay safe. */
  if (!Number.isSafeInteger(occurrence) || occurrence < target || occurrence > acceptedAt) {
    throw new ScheduleLogError('every occurrence arithmetic must stay within the accepted interval')
  }
  const occurrenceAt = new Date(occurrence).toISOString()
  const next = occurrence + interval
源码快照说明:依据本地仓库 deepseek-harness-master,核对文件 packages/schedule/schedule/src/domain.ts,核对日期 2026-08-13。代码块保留源码原文。

第二条边界是 plan mode 的生效时机,逻辑用文字讲。用户在模型流式输出时点了切换,插件不立刻写日志,选择先挂在进程内存的 pending 里,等下一个轮内 pre-step 边界才动手。顺序讲究得很:监听器先 await next() 问下游这一步收不收,下游拒绝、信号已取消或者没有 pending,都原样放行;三关都过了才把选择追加进日志。追加万一失败,只记一条 warn 日志然后放行这一步,绝不因为一次姿态切换失败就阻塞整个轮次。这也回答了崩溃语义:pending 只活在进程内存,切换还没落日志时崩溃,重启后 plan mode 维持切换前的状态。

出处:packages/plan/plan-mode/src/index.ts 第 205 至 218 行的 agent/pre-step 监听器,核对日期 2026-08-13。

todo 那条最有态度的设计不用贴代码:allowParallelInProgress 是必填配置,schema 里写的是 z.boolean().required(),没有默认值(packages/todo/tool-todo/src/index.ts 第 41 至 43 行)。允不允许多个任务同时进行中,取决于这个部署跑不跑并发子 Agent,工具自己观测不到,所以强制部署方表态。设成 false 后,模型多标一个进行中就吃 Error: invalid todos: at most one task may be in_progress(第 107 至 109 行)。

横向对比 · 统一 Task 框架 vs 四个独立原语

Claude Code 走的是聚合路线:七种异步工作(shell 命令、本地子 Agent、远程 Agent、Teammate、工作流、MCP 监控、记忆整合)统一挂在一个 Task 框架下,共享 registerTask、updateTaskState、kill 一套生命周期(书稿 study/chapters/06-task-system.md 第 27 至 47 行引 tasks/types.ts)。DSH 相反,subagent 文档明确写着可继续路径「不会创建 Task,也不会创建承载中间结果的包装层」,四个编排原语更是各有各的持久化形态。聚合换来统一的进度 UI 和管理入口,拆分换来每个原语能把自己的语义说到底,比如 schedule 的错过合并、plan 的 pending 切换,塞进统一框架里都得妥协。

todo 这个小工具上的分歧最能看出两家的脾气。Claude Code 的 TodoWrite 在提示词里硬编码了纪律:「Exactly ONE task must be in_progress at any time (not less, not more)」,条目还要求 content 加 activeForm 双形态,执行中显示进行时文案(书稿 study/chapters/14-all-prompts.md 第 1243 至 1293 行引 TodoWriteTool/prompt.ts 原文)。DSH 把同一条纪律做成了必填的部署配置:跑并发子 Agent 的组合选 true,单线程纪律选 false,选了 false 就由代码拒绝而非提示词劝告;条目形状刻意最小,只有 content 和三态 status。一个用提示词约束模型,一个用 schema 约束部署,然后让代码执行。

课堂练习
01

推演两条边界

其一:模型正在流式输出一大段方案,用户此刻点了「进入 plan mode」,这个选择什么时候真正写进日志、什么时候开始影响模型请求?如果这一轮结束前进程崩了,重启后 plan mode 是开还是关?(提示:pending 只存在于进程内存。)其二:一条 every_seconds: 3600 的提醒,会话离线 5 小时后恢复,恢复瞬间会触发几次提醒、下一次目标定在哪?用本课第一段源码里的 steps 算式手推一遍。

Takeaway:执行编排交给模型写脚本,时间、姿态、展示交给框架状态机,四个原语四种持久化形态,谁也不冒充谁。挑原语时先问一句:这件事的状态需要活多久?活一次 run 的用 workflow,活到会话重启之后的用 schedule 和 plan,只是给人看的用 todo。

「交互演示 · 原语选择器」的能力藏在每次交接里

「四个真实场景,每个场景选一个原语。选错也没关系,判词会讲清楚为什么。点「播放」可以看自动讲解,自己点卡片可以随时抢答」说明,Agent 的表现不只由模型决定。模型、上下文、工具、状态、权限和人之间的每次交接,都会改变任务能否继续以及出了问题能否恢复。

先写清状态,再增加能力

从「先给结论:这四个原语没有共享一个任务引擎,它们连持久化形态都不一样。workflow 是一次性的:模型写一段 JS 脚本,引擎在 node:worker_threads 的 vm 里执行,脚本里的 agent() 打回宿主起子 Agent,跑完只留结果与展示记录,逻辑本身不落成持久状态机。schedule 是持久的:create、dispatch、delet…」出发,可以把流程拆成起始状态、下一步动作、工具返回、状态更新和停止条件。这样调试时找的是第一处丢失信息或权限的位置,而不是笼统地说“模型变笨了”。

成功路径不能代表系统可靠

用「其一:模型正在流式输出一大段方案,用户此刻点了「进入 plan mode」,这个选择什么时候真正写进日志、什么时候开始影响模型请求?如果这一轮结束前进程崩了,重启后 plan mode 是开还是关?(提示:pending 只存在于进程内存。)其二:一条 every_seconds: 3600 的提醒,会话离线 5 小时后恢复,恢复瞬间会触发几次提醒、下一次目…」重放一次成功和一次失败,记录每一轮真正传入的上下文、工具结果和负责人;只要第二个人能复述这条链,系统才有可维护性。

从「交互演示 · 原语选择器」走到「逻辑拆解 · 四个原语各管一摊」

「交互演示 · 原语选择器」先把问题落在「四个真实场景,每个场景选一个原语。选错也没关系,判词会讲清楚为什么。点「播放」可以看自动讲解,自己点卡片可以随时抢答」上;到了「逻辑拆解 · 四个原语各管一摊」,讨论继续推进到「先给结论:这四个原语没有共享一个任务引擎,它们连持久化形态都不一样。workflow 是一次性的:模型写一段 JS 脚本,引擎在 node:worker_threads 的 vm 里执行,脚本里的 agent() 打回宿主起子 Agent,跑完只留结果与展示记录,逻辑本身不落成持久状态机。schedule 是持久的:create、dispatch、delete 都是 schedule/change 会话事件,回放日志…」。两段连起来,重点就不只是记住一个结论,而是看清它成立所依赖的条件。

把这条判断带到下一个场景

分析 Agent 时,沿着状态、动作、工具结果和下一步的顺序走一遍;每次交接都要能说明信息从哪里来、由谁确认、失败时停在哪里。

  • 「交互演示 · 原语选择器」:四个真实场景,每个场景选一个原语。选错也没关系,判词会讲清楚为什么。点「播放」可以看自动讲解,自己点卡片可以随时抢答
  • 「逻辑拆解 · 四个原语各管一摊」:先给结论:这四个原语没有共享一个任务引擎,它们连持久化形态都不一样。workflow 是一次性的:模型写一段 JS 脚本,引擎在 node:worker_threads 的 vm 里执行,脚本里的 agent() 打回宿主起子 Agent,跑完只留结果与展示记录,逻辑本身不落成持久状态机。schedule 是持久的:create、dispatch、delete 都是 schedule/change 会话事件,回放日志…
  • 「最后的要点」:todo_write 是纯展示状态:整表替换、落日志、投影给 UI。没有部分更新、没有回读工具、没有稳定 id。把它当任务引擎用,是对这个原语最常见的误读

最后的「最后的要点」把讨论落到「todo_write 是纯展示状态:整表替换、落日志、投影给 UI。没有部分更新、没有回读工具、没有稳定 id。把它当任务引擎用,是对这个原语最常见的误读」。回看这条线索时,最值得保留的是:当输入、规模或风险改变,哪些判断需要重新做一遍。

标记为已学完 阅读进度会自动记录
← 上一篇下一篇 →

继续阅读

同一条线上的下一篇。

文章讨论

读到这里,留下一个判断。

把刚想明白的地方、还没想通的问题,留给下一位一起学习的人。

正在讨论 workflow / schedule / plan / todo:编排原语的取舍 拆开 DeepSeek Harness
3条讨论文章讨论 · 与共学社区同步
在共学社区查看
AM
Asha Morgan内容编辑
观点实践记录

我把这篇文章里的一个判断改写成了今天可以验证的小实验。比记住结论更有用的是,知道下一步要观察什么。

文章讨论7 有帮助
LH
Lin Harper独立开发者
观点观点

读完以后我先回头找它成立的条件,而不是直接把方法搬进项目。这个顺序让后面的取舍清楚很多。

文章讨论5 有帮助
KM
Kiki Moore产品运营
问题问题

如果把这个判断放到真实工作里,最先需要补的约束是什么?我想知道从阅读到第一次实践之间,哪一步最值得先做。

文章讨论4 有帮助