ByteNoteByteNote
DeepSeek Harness 工作流子系统设计详解
字

字节笔记本

2026年10月6日 · 约 7 分钟读完

DeepSeek Harness 工作流子系统设计详解

API中转
¥120

DeepSeek 开源的 agent 框架 DeepSeek Harness 主张一切皆插件,模型在这个框架里不只能一步步调用工具,还能自己写一段编排脚本,交给引擎代为执行,脚本内部可以批量启动并行子代理。承担这件事的就是工作流(workflow)子系统。本文基于该项目的开发者文档,拆解这条能力接缝的接口设计与背后的取舍。

DeepSeek Harness 工作流子系统架构:消费方、服务合同、worker 引擎与只读事件

工作流是一项可选能力

工作流与子代理一样,是一项可选能力,不属于 agent 主循环,因此它的类型和操作定义在独立的服务合同里,核心不依赖它。与 bash 执行类似,每个上下文只允许一个引擎实现,没有命名提供方注册表;第二个引擎想接入,只能通过插件配置替换第一个,而不是与之同时运行。这个约束换来的是确定性:同一个会话里,编排语义永远只有一种解释。

分工上是三个角色:面向模型的消费方把工具调用转成启动请求;服务合同定义引擎必须满足的行为;默认提供方是一个基于 node:worker_threads 的引擎,每次运行分配一个 worker,脚本在其中的 vm 上下文里执行。想要别的执行方式,实现同一份合同即可替换。

启动一次运行:请求、身份与结果

调用方提交的启动请求包含脚本文本、身份块 meta、可选输入 args、发起代理 parent 和取消信号。脚本允许顶层 await,结尾 return 一个 JSON 值作为最终产出。这里有几条值得注意的纪律。

第一,参数是数据,不是代码。meta 与 args 是纯 JSON 数据,引擎用 schema 校验 meta,无效数据在任何工作开始前就明确报错拒绝;引擎绝不会通过对脚本文本求值来获取参数,从根上堵住了注入面。第二,parent 必填,脚本启动的每个子代理都归属它,工作目录、谱系与深度经由子代理接缝传递。第三,meta 的字段词汇与 Claude Code 动态工作流的 meta 块保持一致:name 用 kebab-case,description 一句话说明用途,可选的 whenToUse 说明适用场景;phases 声明仅用于进度展示,phase() 调用按标题与之匹配,供观察者分组使用,不暗示任何执行结构。

运行的结果对象同样讲究。value 是脚本的物化返回值,纯宿主域 JSON 数据,脚本没有返回值时为 null,仅在 stopReason 为 completed 时有意义。stopReason 是封闭联合类型:completed、cancelled、error 三选一,消费方可以穷举处理;非 completed 的原因在 error 字段携带失败信息,消费方把它映射为 isError 工具结果,而不是把部分输出当成功上报。结果里还有一个 agentsStarted 计数,记录整个生命周期被接受的 agent() 调用数;在强制终止路径上,脚本队列里尚未发布的调用无从得知,此时退化为宿主侧观测计数。

一次工作流运行的生命周期:正常路径、取消岔路与 fatal 岔路

运行句柄:result 永不 reject

脚本执行期间,消费方持有活跃运行句柄,它集中体现了这个子系统对并发失控的防御。

一是 result 承诺永不拒绝:脚本失败会兑现为 stopReason 等于 error 的结果,等待结果的调用方总能拿到终态,而不是捕获异常。二是取消有界:运行被取消后,即使脚本本身永不结算,结果也会在引擎规定的有界宽限期内被强制结算为 cancelled,随后 worker 线程引擎终止脚本所在的 worker,等待方不会无限期挂起。三是 dispose 必须调用:它会按需取消、等待有界结算并等子代理完全停稳,幂等,不因脚本卡死而挂起。

失败纪律:fatal 错误不许被吞掉

脚本内部的钩子误用,比如错误参数、未知或延迟的 agent() 选项、schema 超出结构化输出子集、超出代理上限、接缝启动失败、取消,都会抛出 fatal 标记为真的 WorkflowError。parallel 与 pipeline 组合器对这类错误直接重新抛出,而不是把该项映射为 null。理由很朴素:一个拼错的选项必须明确报错并终止脚本,绝不能消融成看似普通的子代理失败。逐项的 null 语义只保留给真正的子运行失败和阶段内的普通脚本错误。

只读事件与可校验的会话记录

六类 workflow 事件(start、phase、log、agent-start、agent-end、end)是仅供观察的 emit。每个载荷以运行信息快照开头,而非活跃句柄,订阅者拿不到 cancel 与 dispose;end 事件刻意省略结果值,观察结果的监听器不会拿到调用方结果的可变别名。每次 emit 对每个监听器隔离:订阅者抛出的异常只记日志、不传播,也不影响后续监听器,每个监听器收到的是自己的载荷克隆。

持久化侧,顶层消费方把展示事实投影到调用它的父会话,同时不改变执行所有权:运行被接受后先写 run-start,成员以运行 ID 加序号配对,结果取得且 dispose 完全停稳后才写 run-end;嵌套调用不写记录;第一次落盘失败即禁用本运行后续写入,日志要么为空,要么是合法的连续前缀。一个独立校验器在实时提交前和会话加载时校验同一协议,日志尾部缺少成员 end 被视为有效的中断证据而非损坏。界面再把四类事件折叠成一个会话节点,锚定在发起工具节点之后,缺失终点显示为已中断。

三个值得借鉴的设计决策

回看整套设计,有三处对任何想给 agent 加编排能力的团队都有参考价值。

其一,数据与代码分离。meta 与 args 是先校验后执行的纯 JSON,脚本只是文本,引擎从不求值取参,注入面被压到最小。其二,失败显式化。fatal 与普通失败分级处理,组合器不吞类型错误,null 语义收窄到真正的子运行失败,排障时不用猜哪一层出了问题。其三,观察与执行分离。事件只给快照,控制句柄只给所有者,会话日志可校验、可中断、可续读,界面展示不会反过来影响执行。

项目以 MIT 协议在 GitHub 开源(github.com/deepseek-ai/deepseek-harness),正处于开发者预览阶段,接口可能破坏性变更,但这份工作流合同已经能看出它对可控编排的完整思考。

相关文章

分享: