
今天要学习的是 DeepSeek Harness 的 Workflow。我们不从 API 清单出发,而是追踪一个真实问题:当任务大到需要许多子 Agent 时,谁来安排它们的顺序、并发、数据传递和失败处理?理解这条主线之后,agent()、parallel()、pipeline()、取消与恢复才会落在同一张图上。
小炎: Workflow 不就是同时调用很多个 Subagent 吗?为什么还要单独发明一个概念?
大老师: 不完全是。Subagent 解决的是"找另一个执行者完成一项任务",Workflow 解决的是"许多执行者应该怎样协作"。
直接调用 Subagent 时,父模型通常一边执行一边规划:
text
父模型 → 委派 A → 阅读 A 的结果 → 决定是否委派 B → 再汇总
Workflow 则让父模型先写出一段完整的 JavaScript 编排脚本,再交给运行时执行:
text
父模型 → 生成编排脚本 → Workflow 引擎执行并协调多个子 Agent → 返回最终 JSON
所以最简洁的区别是:
text
Subagent = 一个独立执行者
Workflow = 创建并协调许多执行者的临时程序
小炎: "模型写脚本"是什么意思?这个脚本是开发者提前放在仓库里的吗?
大老师: DeepSeek Harness 当前这条普通 Workflow 路径里,脚本通常不是开发者预先写好的,而是模型针对本次任务临时生成的。
假设用户说:
使用 Workflow 并行审查二十个模块,再让另一个 Agent 汇总高风险模块。
模型可能产生类似下面的工具调用内容:
js
phase('检查模块')
const reviews = await parallel(
args.modules.map((name) => () =>
agent(`检查 ${name} 的测试设计`, { label: name })
),
)
phase('汇总结论')
const summary = await agent(
`根据这些结果识别高风险模块:${JSON.stringify(reviews)}`,
{ label: '汇总' },
)
return { reviews, summary }
模型不是凭空知道这些函数。当前 bundle/profile 必须先装配 Workflow service、worker-thread provider 和模型可见的 workflow 工具。Harness 每次请求模型时,会把工具名称、参数结构和脚本约定一起提供给模型。模型由此知道可以提交 meta、script 和 args。
这也说明 Workflow 不是 DeepSeek 模型 API 自带的魔法。模型负责生成脚本,DeepSeek Harness 的插件负责校验和执行。换一个能正确使用同一工具协议的模型,Harness 仍然可以提供 Workflow;只调用裸模型 API,则不会自动获得它。
小炎: 脚本在哪里执行?子 Agent 也在那个地方运行吗?
大老师: 编排脚本运行在 Node.js Worker Thread 中,但真正的子 Agent 仍由宿主侧启动。
text
父 Agent
│ 提交 workflow 工具调用
▼
Workflow 宿主
│ 创建 Worker Thread
▼
编排脚本执行 agent(...)
│ 通过结构化消息请求宿主
▼
宿主创建真正的子 Agent + 独立 Session
│ 返回子结果
▼
Worker 中的脚本继续组合数据
Worker Thread 的主要目的,是把编排脚本和 Harness 主事件循环隔离开。即使脚本有大量同步计算,父 Agent、Session 写入和其它工具也不至于一起被阻塞;取消后还可以最终终止 Worker。
但 Worker Thread 和 node:vm 不是安全沙箱。它们解决的是调度和故障隔离,不负责提供操作系统级权限控制。真正的文件、命令和网络权限,仍由工具层以及底层沙箱机制决定。
小炎: 既然父模型也能一个个调用 Subagent,为什么还需要让模型先写程序?
大老师: 因为大规模任务里,调度本身会变成问题。
让父模型逐步安排二十个模块,意味着它要不断经历"调用---等待---阅读---再规划"。所有中间结果也会进入父上下文。Workflow 把循环、并发、阶段依赖和中间变量交给程序保存,父模型只需等待脚本最后返回的结果。
它适合以下情况:
- 对一个列表执行大量同类任务;
- 多项工作可以并行;
- 每个对象要经过检查、复核、汇总等多个阶段;
- 需要统一控制并发数、总 Agent 数和条目数;
- 父上下文只需要最终结果,不需要容纳所有中间对话。
如果只有一两个边界清楚的任务,直接调用 Subagent 更简单。DeepSeek Harness 的工具指引也要求:只有用户明确要求 Workflow,或者明确要求大型多 Agent 编排时才使用。当前实现是前台运行,父 Agent 会等待整条 Workflow 完成。
小炎: 多个子 Agent 会共享记忆吗?第二阶段怎么知道第一阶段发现了什么?
大老师: 默认不共享对话记忆。Workflow 中每次 agent() 都是一次独立运行,有自己的 Session,只看到传给它的 prompt。
第一阶段的结果要通过脚本变量显式交给第二阶段:
js
const finding = await agent('检查鉴权模块,返回证据')
const verdict = await agent(
`复核下面的发现:${JSON.stringify(finding)}`,
)
return { finding, verdict }
多个 Agent 通常可以看到同一个工作区,所以一个 Agent 写入文件后,另一个可能读到变化。但那是共享外部状态,不是共享记忆;它也意味着并行修改同一文件可能冲突。
可靠的阶段传递最好使用 schema,要求子 Agent 返回经过校验的 JSON。否则下一阶段只能猜测自由文本结构。Workflow 最终的 return 也必须是普通、可序列化的 JSON 数据。
小炎: 那 parallel() 和 pipeline() 到底有什么区别?
大老师: parallel() 表达独立任务的并发,pipeline() 表达每个对象的阶段依赖。
parallel() 可以近似理解为:
js
const results = await Promise.all([
taskA(),
taskB(),
taskC(),
])
这些任务一起启动,调用者等待全部结束。A 的结果不会自动传给 B。
pipeline(items, stage1, stage2) 则表示每个 item 都要依次经过多个阶段:
js
const results = await pipeline(
['auth.ts', 'payment.ts', 'ui.ts'],
async (_previous, file) =>
agent(`检查 ${file}`),
async (findings, file) =>
agent(`复核 ${file}:${JSON.stringify(findings)}`),
)
第二阶段的第一个参数是上一阶段的返回值,同时还能拿到原始 item 和索引。
小炎: 可是每个文件都是"检查完再复核",这不就是串行吗?
大老师: 对单个文件来说是串行的;对整个文件集合来说不是完全串行。最准确的心智模型是:
js
Promise.all(
items.map(item => 串行执行 stage1 → stage2 → stage3)
)
假设 A 检查需要 2 秒、复核需要 5 秒;B 检查需要 5 秒、复核需要 1 秒。
完全串行需要:
text
A检查 2秒 → A复核 5秒 → B检查 5秒 → B复核 1秒
总计 13 秒
如果阶段之间存在全局屏障,要等 A、B 都检查完才开始复核:
text
A检查 ─┐
├─ 等到第5秒 → A复核、B复核
B检查 ─┘
总计约 10 秒
pipeline 不设这个跨 item 的阶段屏障:
text
时间:0 2 5 6 7
A: 检查 ─→ 复核 ─────────→ 完成
B: 检查 ─────────→ 复核 → 完成
A 在第 2 秒完成检查后立即进入复核,不必等待 B。最终 pipeline() 仍要等所有 item 完成才返回,但各 item 可以独立向下一阶段流动。
当然,所有 agent() 还受 maxConcurrentAgents 限制。如果上限是 1,即使写了 pipeline(),实际子 Agent 也只能逐个运行。
小炎: 如果某个子 Agent 失败并返回 null,最终结果不是也坏了吗?
大老师: 会受影响。关键是区分"编排脚本执行完成"和"业务结果完整"。
普通子 Agent 以 error、max-tokens、refusal 等原因结束,或者没有产生要求的结构化结果时,agent() 通常返回 null。如果脚本直接返回数组,可能得到:
js
{
stopReason: 'completed',
value: [
{ file: 'auth.ts', issues: [] },
null,
{ file: 'ui.ts', issues: [] },
],
}
这里的 completed 只表示脚本正常运行到了 return,不表示三个模块都成功。
还有一个容易漏掉的细节:null 是正常返回值,不是异常。在 pipeline() 里,下一阶段仍会收到它。如果不检查,脚本甚至可能让下一个 Agent 去"复核 null"。
js
async (findings, file) => {
if (findings === null) {
throw new Error(`${file} 没有检查结果`)
}
return agent(`复核 ${file}:${JSON.stringify(findings)}`)
}
在 pipeline() 内抛出的普通异常只会让当前 item 变成 null,并跳过它剩余的阶段;其它 item 继续运行。这是一种批处理容错,而不是结果正确性保证。
小炎: 所以失败策略必须一开始就定义好吗?
大老师: 自动处理所需的规则必须在执行前明确,但你不一定要亲自写脚本。
Harness 只定义底层机制:
text
普通子任务失败 → null
普通 stage 异常 → 当前 item 为 null
框架级致命错误 → 整条 Workflow 终止
外部取消 → 停止调度并清理资源
任务本身必须定义业务策略:
- 19/20 成功是否可以接受?
- 失败后重试几次?
- 最终是否保留失败项及其身份?
- 哪些关键模块失败时必须让整体失败?
你可以直接用自然语言告诉模型:
每个模块失败后重试一次;仍失败则保留模块名和失败状态。普通模块允许部分成功,但支付模块没有有效结果时,整个 Workflow 必须失败。
模型再把规则编码进临时脚本。如果这是反复执行的固定业务流程,就不应每次依赖模型临时猜测,而应该把规则沉淀为模板、插件或开发者定义的工作流。
不要简单使用 .filter(Boolean) 把失败项删掉。那样虽然数组看起来干净,却会丢失"哪个模块失败了"。更清楚的结果是:
json
[
{ "item": "auth.ts", "ok": true, "value": {} },
{ "item": "payment.ts", "ok": false, "error": "no result" }
]
小炎: 什么错误会让整条 Workflow 真正失败?
大老师: 脚本语法错误、hook 参数错误、不支持的 schema、超过条目或 Agent 上限、provider 无法启动、子 Agent 结果通道损坏、最终结果无法序列化等,属于框架级致命错误。
这些问题意味着执行基础已经不可靠,不能伪装成某个 item 的 null。parallel() 和 pipeline() 捕获到它们后会重新抛出,整条 Workflow 最终得到:
js
{
stopReason: 'error',
value: null,
error: '具体错误信息',
}
宿主随后开始 abort 和 dispose 仍在启动或运行的兄弟子 Agent。引擎的 run.result 本身不会 reject,而是始终 resolve 成 completed、error 或 cancelled 之一,这样宿主能统一完成生命周期记录和资源清理。
小炎: 用户点击取消以后,到底发生了什么?已经做过的事情会自动撤销吗?
大老师: 取消会停止控制流并清理运行资源,但不会自动回滚外部状态。
取消大致经过下面的过程:
text
1. 宿主记录取消原因,第一次取消生效
2. 向 Worker Thread 发送 Cancel
3. abort 正在启动和已经运行的子 Agent共享的信号
4. 拒绝等待并发槽位的 agent() 调用
5. 后续 agent/parallel/pipeline/phase/log 调用抛出 CANCELLED
6. 请求子 Agent dispose,并等待它们协作退出
7. 超过宽限时间仍未结束,补齐 cancelled 事件并终止 Worker
这三个概念不能混在一起:
text
取消:不要再开始新的工作,并尽量停止正在进行的工作
清理:释放 Worker、子 Agent、句柄等运行资源
回滚:把已经改变的业务状态恢复到执行前
如果某个 Agent 已经修改了 auth.ts,之后用户才取消,文件修改仍然存在。已经发送的网络请求也不能自动"撤回"。Workflow 引擎不知道每种副作用应该如何反向操作,因此它不是事务引擎。
真正需要回滚时,必须另外设计:前面阶段只读分析,最后统一写入;把修改隔离在独立 worktree,成功后才接纳;让写操作支持幂等重跑;或者为远程副作用定义补偿操作。
小炎: Session 不是记录了 Workflow 的开始、子 Agent 和结束吗?进程崩溃后不能从中间继续?
大老师: 当前不能。这里要区分观察日志和执行检查点。
父 Session 会记录 Workflow 的开始、成员开始与结束、运行结束,原始工具调用里也保留模型生成的脚本。因此 UI 可以回放"运行过什么"和"哪些成员完成了"。
但脚本局部变量、Promise 状态、parallel() 或 pipeline() 走到哪里、哪些中间值已经返回,并没有被保存成可恢复状态。如果二十个模块完成十二个后进程崩溃,新进程可以展示那十二个成员的历史,却不能自动从第十三个继续。
text
可回放的观察日志 ≠ 可恢复的执行状态机
真正可恢复的持久工作流,还需要保存执行游标、每个节点的输入输出、重试次数、幂等规则和副作用状态。DeepSeek Harness 当前的 Workflow 更适合一次性大规模编排,而不是跨进程长期运行的 durable workflow。
小炎: 那它和其它 Agent 框架里的 Workflow,最根本的区别是什么?
大老师: 不要只问"有没有并行和流水线",更应该问四件事:谁写计划、什么时候写、状态是否持久、由什么运行时执行。
text
开发者预定义 Workflow
稳定、可测试、适合反复运行的产品流程
模型临时生成 Workflow
灵活、适合一次性复杂任务,但脚本和策略更不确定
父模型逐步调用 Subagent
直观灵活,但调度往返多,中间结果占父上下文
DeepSeek Harness 当前普通 Workflow 的突出特点,是模型为当前任务临时写 JavaScript,由插件化运行时在 Worker Thread 中执行,再通过 Subagent seam 启动实际执行者。它借鉴了动态 Workflow 思路,但目前只支持前台收集,也没有脚本检查点和中途恢复。
因此它既不是传统的固定业务工作流,也不是一个"更强就应该总用"的工具。它是在任务规模足够大、结构足够清楚时,把模型的调度计划变成一段可以检查和执行的程序。
总结:Workflow 的价值不在并发,而在显式化调度
DeepSeek Harness Workflow 的完整链路可以压缩成一句话:
text
模型生成编排脚本
→ Worker 执行顺序、并发和阶段规则
→ 宿主启动独立子 Agent
→ 脚本显式传递和汇总结果
→ 父 Agent只接收最终 JSON
使用它之前,应当回答六个问题:
- 任务是否真的需要许多 Agent?
- 每个子任务是否能用独立 prompt 描述清楚?
- 阶段之间的数据是否有明确 JSON/schema?
- 单项失败是否允许继续,是否重试?
- 并行写入同一工作区是否会冲突?
- 进程崩溃或任务重跑时,副作用是否安全?
如果这些问题没有答案,Workflow 只会把模糊的计划放大并并发执行。它真正带来的设计认识是:多 Agent 系统的核心难题不是"能启动多少 Agent",而是如何把依赖、数据、失败和副作用变成明确规则。