Workflow 可不是 多叫几个 Agent

今天要学习的是 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 每次请求模型时,会把工具名称、参数结构和脚本约定一起提供给模型。模型由此知道可以提交 metascriptargs

这也说明 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 以 errormax-tokensrefusal 等原因结束,或者没有产生要求的结构化结果时,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 的 nullparallel()pipeline() 捕获到它们后会重新抛出,整条 Workflow 最终得到:

js 复制代码
{
  stopReason: 'error',
  value: null,
  error: '具体错误信息',
}

宿主随后开始 abort 和 dispose 仍在启动或运行的兄弟子 Agent。引擎的 run.result 本身不会 reject,而是始终 resolve 成 completederrorcancelled 之一,这样宿主能统一完成生命周期记录和资源清理。


小炎: 用户点击取消以后,到底发生了什么?已经做过的事情会自动撤销吗?

大老师: 取消会停止控制流并清理运行资源,但不会自动回滚外部状态。

取消大致经过下面的过程:

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

使用它之前,应当回答六个问题:

  1. 任务是否真的需要许多 Agent?
  2. 每个子任务是否能用独立 prompt 描述清楚?
  3. 阶段之间的数据是否有明确 JSON/schema?
  4. 单项失败是否允许继续,是否重试?
  5. 并行写入同一工作区是否会冲突?
  6. 进程崩溃或任务重跑时,副作用是否安全?

如果这些问题没有答案,Workflow 只会把模糊的计划放大并并发执行。它真正带来的设计认识是:多 Agent 系统的核心难题不是"能启动多少 Agent",而是如何把依赖、数据、失败和副作用变成明确规则。

相关推荐
京东云开发者2 小时前
数据涅槃:AI 赋能的深度加工与价值重塑
agent
Erishen2 小时前
tsm-hub:把 LLM、Tools、MCP、Skills 收进一个统一网关
架构·开源·agent
bamboo_ye2 小时前
从“记住对话”到可信上下文:伴AI 的统一快照、可追溯 Wiki 与 Passkey 治理
agent
半糖程序员2 小时前
从零构建 Agent(8):让 Agent 调用工具
agent
武子康2 小时前
两台机器跑 vLLM,什么时候才值得引入 Ray?
人工智能·llm·agent
桃西西呀2 小时前
Agent说做完了,其实什么都没改:静默失败原因拆解
人工智能·llm·agent
吴佳浩11 小时前
Function Calling 为什么不够用?深入拆解 MCP 标准协议的设计哲学
agent·ai编程·mcp
吴佳浩12 小时前
从零手写一个生产级 MCP Server:鉴权、流式传输与状态管理
agent·ai编程·mcp
看浪的路人12 小时前
第5讲:Agent 决策链路可视化——让 Agent 的思考过程透明化
agent