ByteNoteByteNote
DeepSeek Harness 设置子系统设计解析
字

字节笔记本

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

DeepSeek Harness 设置子系统设计解析

API中转
¥120

DeepSeek Harness(简称 dsh)是 DeepSeek 开源的智能体框架,走「一切皆插件」的路线,底层由 Cordis 框架驱动。这样的插件生态里,配置天然是分层的:框架和插件怎么组合,写在组合配置里;用户在界面上改的那些偏好,则交给独立的设置子系统。本文依据其官方文档,拆解这个子系统的设计。

用户设置按 namespace 切分

设置子系统(dsh-settings 包)持有一份按 namespace 分节的用户文档。每个已注册的 namespace 都会解析出一个最终值,顺序固定为三层:先是 schema 默认值,然后是注册方在组合期给出的 base 层,最后才是用户分节。用户只覆盖自己改过的字段,其余字段沿 base 与 schema 默认值逐层回退。

存储与消费是分开的。dsh-settings-file 这类提供方只负责存储原始文档,并把外部编辑推送进来;消费方插件注册 schema 之后,读取或观察解析后的值。组合配置仍留在 cordis.yml,namespace 只承载用户可编辑的子集,两条配置通道互不越界。

dsh-settings 三层解析示意

注册即 effect

namespace 是用户文档中一个归插件所有的分节的标识。它是品牌化 id,构造时校验小写 kebab-case 语法,避免与包或进程之间传递的其他 id 混用。

注册通过 register 完成:把 schemastery schema 绑定到调用方插件 fiber 上的 namespace,重复注册同一 namespace 会直接报错。注册本身是一个 effect:dispose 该 fiber,namespace 及其观察者一并移除,不留下悬挂的监听。

注册选项里有三件东西值得展开。

其一是 base,组合层的取值,解析时位于用户层之下,相当于插件作者在组合期写下的推荐值。

其二是 applies,标注 owner 的生效时机,取值 live 或 restart,默认 live。它是给配置界面看的提示而非机制:声明为 restart 的 owner 只是从不 watch,其值在构造期读取一次,配置界面据此给待生效的变更加上标记。

其三是 validate,一个可选钩子,用来校验 schema 表达不了的约束,比如跨字段要求,或一个字段的有效性取决于另一个字段。它在 schema 接纳该值之后运行,所以看到的默认值与 base 和 owner 实际看到的完全一致。钩子抛错时,产生该值的写入被拒绝,调用方在 update、replace 或 mutate 时就得到失败,而不是把一个会让 owner 静默失效的值存进文档。dsh-llm-pi-ai 插件就用它在自己无法服务的提供方 profile 落盘之前,于写入处直接拒绝。

失败的后果分两种时机。注册完成之后,一条没过 validate 的已存分节会让 namespace 保留上一个可用值并发出告警,与 schema 校验失败的表现完全一致,外部编辑过的文档因此不会搁浅正在运行的 owner;而注册那一刻还没有上一个可用值,一条已经坏掉的存档分节会让注册本身直接失败。

Owner scope:合并、替换与串行写入

register 返回一个面向 owner 的 scope 句柄,提供四个操作。

get 返回当前解析值,也就是三层合成后的深冻结快照。watch 注册观察回调:同一回调的各次调用异步执行、一次一个、按提交顺序排列;回调里的拒绝会被包含起来,像同步抛错一样记录日志。disposer 返回之后不再开始新的调用,已经开始的那次仍会跑完,服务释放会等它结束。

update 接收稀疏 patch,只合并进用户分节,绝不进 base。replace 整体替换分节,这是删除与重置的唯一路径:替换中缺席的键重新继承 base 与 schema 默认值,replace 传空对象等于全部重置。两个接口都只接受 JSON 兼容数据,遇到无法序列化的值会带着路径拒绝,而且发生在任何持久化之前。同一 namespace 的写入按调用顺序串行执行。

describe 与机密脱敏

describe 为配置界面序列化每个已注册 namespace,产出描述符:schema 的 toJSON 结构驱动表单渲染,解析值填充表单;分离给出的 base 与 user 两层,让表单能按字段是否出现在 user 层来标注「用户已覆盖」,也知道重置该回到什么状态。

关键在脱敏。describe 支持 redactSecrets 选项,文档明确要求每个对外传输接口都必须传入:开启后从 value、base、user 三层剥离所有 role('secret') 字段,并在描述符里枚举它们的位置,形如 path 加 set 的槽位。页面因此能渲染只写输入框,而永远收不到机密值本身。

dsh-settings 写入与读取路径示意

这份脱敏描述符带出一个直接推论:只持有它的调用方无法安全重建整个分节,因为重建出的文档里,所有从未下线的机密字段都不在场。若允许它整体 replace,每次保存都会静默删掉这些机密。于是删除改以路径操作传递:set 在指定路径写入,unset 在指定路径删除。调用方点名自己要动的字段,无需重述整个分节,也删不掉自己从未见过的字段。配套的 mutate 按顺序应用这组路径编辑,写入到达队列前沿时才套用到当前分节,后一个操作能看到前一个操作的结果。

revision:乐观并发控制

每个描述符还携带一个针对原始分节的单调递增 revision。写入时把它作为 expectedRevision 送回,命名空间已经越过这个版本时,写入被拒绝并给出冲突错误,而不是覆盖先落地的写入。配置界面因此不必加锁,也能避免拿旧表单盖掉别人的修改。

两条事件,两种读者

每次提交的变更,无论是进程内写入还是提供方观察到的外部编辑,都在新值成为权威值之后发出 settings/updated 事件,带上 namespace、新值、旧值与来源,source 区分 update 与 provider 两条入口路径。解析值深相等时绝不发出,观察者不会被无意义的抖动打扰。

配置界面听的是另一条 settings/document-updated:原始用户分节一旦变化就发,无论解析值有没有变。同一个值从继承变成覆盖,或者自己持有的 revision 已过期,这类只有配置界面在乎的事,由这条事件负责通知。

监听失败的处理也有讲究:同步抛出与异步拒绝都会被包含并记录;唯独 INVARIANT 编码的失败例外,它在所有监听器跑完之后重新抛出,而这次重抛只会从同步监听器到达派发方,所以在这条事件上做不变式检查的监听器不能写成 async 函数。

小结

这套设计可以压成五句话:解析走 schema 默认值、base、用户层的三层回退;注册绑定 fiber 生命周期,validate 把跨字段坏值挡在写入处;update 只做合并,replace 专职删除与重置;机密以脱敏描述符加路径编辑的方式不落出进程;revision 用乐观并发挡住过期写入。想给 dsh 写插件的开发者,读懂这一层,配置相关的接缝就都有了着落。仓库地址:github.com/deepseek-ai/deepseek-harness。

相关文章

分享: