
字节笔记本
2026年10月6日 · 约 9 分钟读完
DeepSeek Harness 存储子系统解析
DeepSeek Harness(dsh)是 DeepSeek AI 在 GitHub 上开源的智能体框架,采用一切皆插件的架构,由 Cordis 插件框架驱动,仓库以 MIT 许可证发布,目前处于开发者预览阶段。它的设计文档按子系统逐一成文,本文梳理其中的存储子系统:它持久保存一切不属于会话事件日志的数据,会话日志本身由独立的持久化子系统负责,两者通过清晰的能力接缝分工。整套设计可以概括为一句话:介质归后端,语义归数据形式,枢纽不碰 IO。

三层结构:后端、枢纽与数据形式
存储子系统不是一个整体,而是按能力接缝拆成三个包。最底下是后端:dsh-storage-json 注册为 json 后端,把每个 unit 以原子方式整文件重发布为一份人类可读的文件;dsh-storage-sqlite 注册为 sqlite 后端,在单个数据库中每行存储一份文档,适合频繁更新的数据。中间是枢纽 dsh-storage,对产品暴露为 ctx.storage,它维护一张名称到后端的注册表,同时是数据形式的挂载点。最上层是数据形式 dsh-storage-domain,暴露为 ctx.storageDomain,它是后端约定的唯一消费方,也为其他一切代码提供类型化 API。
这套分层的关键约束是:枢纽自身不做任何 IO。哪个后端服务哪个消费方,由消费方自己的配置决定,也就是领域层的路由表,而不是枢纽的全局选择;产品包则绝不直接触碰后端。
枢纽:ctx.storage
枢纽是汇合点,不是存储本体。ctx.storage.backend 是一张名称到后端的表,多个后端可以并排保持挂载。register(name, backend) 返回一个 disposer;重复注册同名或查找未知名称都会抛出 StorageError。dispose 只注销名称,关闭后端仍是拥有它的插件在注销之后自行完成的事。每个后端插件还会发布一个仅用于生命周期的服务键,数据形式提供方注入它,使自身的激活不会与后端注册发生竞态。
数据形式以一张可合并扩展的键 map 挂到枢纽上:mount(form, facility) 是一个 effect,返回的 disposer 负责卸载,对同一键的第二次挂载会报 duplicate-mount;form(form) 解析已挂载的 facility,在拥有插件加载之前会报 form-not-mounted,组合方应据此安排插件顺序,而不是静默推迟。领域层把自己的 facility 合并进这张表,因此 ctx.storage.domain 与 ctx.storageDomain 是同一个对象。
后端约定:一个介质,可选能力组
每个后端拥有恰好一个介质,可以是一棵文件树的根目录,也可以是一个数据库文件,并提供可选的操作组,目前 kv 是唯一一组。KvFacet.open(descriptor) 打开一个具名 unit,描述符携带名称、格式版本、表名清单,以及是否存在全局单例 slot;返回的 KvUnit 提供 loadAll、putRecord、deleteRecord、setGlobal 和 close。unit 名与表名必须匹配 UNIT_NAME_RE,保证既能安全用作文件名,也能安全用作 SQL 标识符片段;记录键是任意字符串,绝不进入文件路径。
一致性条款写得非常明确:unit 不对并发写入做串行化,顺序由调用方负责,但每次单独调用在介质上都是原子的,且 resolve 之后即已持久。介质上记录的版本不一致时拒绝 version-mismatch;无法按该 unit 解析的介质拒绝 malformed-medium。这里能看出一个鲜明的预发布立场:不做迁移。backend.ts 是逐条款的规范性约定,tests/contract.ts 中的共享一致性套件会针对每个后端检查每项条款。
声明领域:spec 是唯一事实来源
领域由拥有包用 spec 对象声明一次,它是该领域的身份、布局和记录 schema 的单一来源:
interface DomainSpec {
/** 领域名;必须匹配 UNIT_NAME_RE(同时充当后端 unit 名) */
readonly name: string
/** 格式版本;介质上的版本不一致时在打开时拒绝 */
readonly version: number
/** 可选的全局单例 slot */
readonly global?: DomainGlobalSpec<unknown>
/** 按表名组织的表声明;每个名字必须匹配 UNIT_NAME_RE */
readonly tables: Record<string, DomainTableSpec>
}schema 用 zod 编写,z.infer 让消费方类型无需重复声明。defineDomain 在拥有方模块加载时、任何介质被触碰之前就明确报错:领域名或表名不合法、版本不是非负整数、global schema 接受 null,这些都会抛出。原因在于 null 是介质上的从未写入哨兵值,可空的 global 一旦存储就无法往返还原。domainTable(schema) 声明的表键是只存在于编译期的 phantom 类型,通常配合品牌化 id 使用;descriptorOf(spec) 则投影出面向后端的 unit 描述符。
打开领域:严格顺序,响亮失败
DomainFacility.open(spec) 按严格顺序执行,每一步失败都让整个调用失败:拒绝已打开或仍在关闭中的名称(already-open),解析路由(backend-not-found),要求后端具备 kv facet(facet-unsupported),打开 unit(后端的 version-mismatch 与 malformed-medium 原样透传),最后按 spec 的 zod schema 校验每条已存储记录和 global(invalid-record,附带出错的表与键)。
路由是领域插件的配置,不属于枢纽:backend 指定必填的默认路由,routes 可以按领域名逐个覆盖。返回的句柄由调用方拥有并用 close() 释放,通常注册为插件自己的 effect disposer;插件卸载时仍处于打开状态的领域由 facility 兜底关闭,已关闭领域的名称要等拆除完全结束才释放出来供重新打开。get(name) 是无类型的诊断查找,closeAll() 则是卸载路径。
一次写入的生命周期

打开的领域里,读取是同步的,直接来自权威的内存态:表句柄暴露 get、entries、keys、size,global 句柄在第一次 set 将 slot 物化到介质之前,一直返回 spec 的 initial。每次写入,无论是 put、delete、update 还是 global.set,都排在同一条逐领域写链上,顺序固定:先在后端完成持久化,再更新内存,最后发出 domain/changed 事件。后端写入被拒时内存原样不动,因此读取绝不会偏离介质。
几个细节值得记住。update 在写链 slot 上是一次原子的读改写,键缺失时报 missing-key;delete 一个不存在的键直接 resolve 为 false,不产生写入也不产生事件。返回的记录就是存储的对象本身,不是副本,必须经 put 或 update 整体替换,绝不要就地修改。
domain/changed 严格发生在后端确认持久之后,顺序遵循该领域的写链。put 事件在 value 中携带新快照,绝不携带旧值,需要做差异比较的消费方要自行保留上一份快照;deleted 则是不带值的墓碑。该事件是通知,不是事务参与者:发出时提交点已经过去,同步监听器抛出的异常会被兜住并记录一条警告,不会让已经持久的写入被拒绝。目前该事件仅限进程内,跨进程的变更推送是一项已记录的限制。
小结
这套设计的取舍相当自觉:预发布阶段不做介质迁移,宁可响亮失败;枢纽不做 IO,把介质与语义彻底分离;读取走内存、写入走单链,让一致性问题在结构上消失。对于想在插件化框架上组织持久化能力,或者想研究智能体框架如何管理状态的读者来说,这份逐条款成文的后端约定是很好的范本。源码位于 deepseek-harness 仓库的 packages/storage 目录,文档以 MIT 许可发布。



