DSH 核心设计为「万物皆插件、插件皆可逆」,通过数学形式化方法统一 Agent Loop 和 Memory 等不同插件的运行时语义。
DeepSeek Harness(DSH)的发布在社区引发了巨大轰动:主仓库上线仅两天,star 数便迅速突破 10 万,成为 GitHub 历史上最快达成 10 万 star 里程碑的项目(根据截至 2026-08-21 的实时 GitHub 数据,deepseek-ai/deepseek-harness 已超过 17 万 star,标记为 dsh-plugin 的插件仓库已超过 1 万)。与 DSH 发布一同出现的,还有一篇由 DeepSeek 与北京大学联合署名的论文——A Programming Paradigm for Spatiotemporal Composability(时空可组合性的编程范式)——该论文用数学形式化方法阐述了 DSH 底层插件运行时 Cordis 的设计原理。
DSH 的架构设计哲学可以归结为一句话:一切皆插件,且每个插件皆可逆。这真的是一种创新设计吗?社区的反应远未统一——有人兴奋,有人困惑,更有大量质疑的声音。最常见的反对意见包括:
"一切皆插件"是一种不必要的统一——Agent Loop 和 Memory 差异如此之大,将它们统一为同一种"插件"有什么意义?
"一切皆插件"并非 DSH 的发明。当今哪个 Agent 框架不是高度可配置的?Pi Agent 不也是插件化的吗?
"可逆插件"究竟是什么意思?副作用不可避免——怎么可能做到可逆?
为什么要动态更新插件?版本升级不就够了吗?
DSH 论文其实已经回答了这些问题,但它采用的是范畴论/函数式编程视角的形式化表述,普通开发者很难穿透数学符号去把握其真正含义。论文反复强调"可逆"(reversible)一词,这促使知乎上一些用户@我,询问这个概念与我提出的可逆计算理论是否相似。
本文从可逆计算理论的视角,用通俗的语言解释 Cordis 的设计原理,同时与之对比真正的可逆计算理论。具体结论如下:
Cordis 可以视为可逆计算理论在运行时结构空间中的一个具体应用——这里的"插件"不仅仅是功能模块,而是可以在数学上严格定义的增量单元。
然而,完全实现可逆性,需要辅以可逆计算理论在编译时结构空间已经形式化的一整套实践——DSH 的具体实现部分体现了这一点(声明式配置、运行时调和),但 Cordis 论文在这一层只提供了机械化的描述,并未将其形式化为增量代数。
Cordis 中的"可逆"并不意味着反向运行;而是指对运行时结构空间的变更可以撤销——如果将这些变更视为副作用,那么 Cordis 管理的并非所有副作用,而只是影响多个插件组合交互的一种特殊类型。
"一切皆插件"这一概念可以表达为如下公式:
App = Base + Plugin1 + Plugin2 + ...
也就是说,大量功能并非内置于基础产品中,而是通过插件来提供。在这个层面,DSH 的做法与 Pi Agent 类似——两者都可以视为一个提供最小功能集的核心,加上通过扩展提供的丰富功能。
在可逆计算理论中,上述做法对应于以下数学公式的一个具体实例化:
Structure = Base + Delta1 + Delta2 + ...
插件可以视为一个功能增量。这听起来像是一句废话——增量开发本来就是软件工程中早已稀松平常的概念。然而,本质的区别在于:在传统软件工程语境中,上述公式只是一个启发式描述,而 Cordis 论文与可逆计算理论的目标是将其变成一个真正的数学公式。
这涉及到以下问题:
参与运算的基本单元的具体结构形式是什么——即,一个 Plugin 的结构究竟是什么?
这个加号在数学上如何精确定义?
Base + Plugin1 + Plugin2 与 Base + Plugin2 + Plugin1 是否等价?
在不存在 Base 的情况下,Plugin1 + Plugin2 能否定义?它们运算的结果是什么?
Cordis 论文本质上就是要构建一个数学空间,然后在这个空间内定义对象之间的运算律,以精确回答上述问题。我们稍后会分析 Cordis 的详细形式化过程。首先,让我们考虑一个有趣的问题:"一切皆插件"是否包含 Base 本身?
Base + Plugin 中的 Base 显然不是 Plugin,所以很多程序员的本能反应是"一切皆插件"必定是夸大其词。但从可逆计算的视角看,这再自然不过:A = 0 + A——任何整体都可以视为零加上一个增量。因此,"一切皆增量"才是数学上精确的表述。
这里有一个微妙的点:增量是在一个支持增量运算的结构空间中定义的,所以"一切皆增量"适用于这个已存在的空间。构建这个空间需要相应的工作并付出代价,但这个基础空间并非增量运算的 Base。这类似于应用程序运行在操作系统之上——我们需要先构建操作系统作为使应用程序真正运行的基础空间,但我们通常不会将操作系统视为应用系统的 Base 部分。
在 Cordis 中,一切皆插件;当不存在插件时,整个系统没有业务可见的功能,对应于这个数学空间中的零元素。但这并不意味着绝对的虚无。这类似于物理世界中的真空。根据现代物理学,真空并非真正空无一物:它是量子场的基态,包含着隐藏结构——零点波动、QCD 凝聚态、拓扑构型——并具有非零能量密度(宇宙学常数)。在极端条件下,它甚至可以被极化或"击穿"(施温格粒子对产生)。
很多人一看到"可逆"(reversible)这个词,立刻想到的第一种解释就是:反向运行。另一些人从副作用的角度思考,认为插件在真实系统中执行不可避免地产生大量副作用——这些怎么可能被撤销和 undo?
这是对可逆计算的常见误解。实际上,可逆计算中的"可逆"既不是逐步反向执行,也不是对所有副作用的补偿性撤销。它指的是:发生在结构空间中的 Delta 变更可以是正向的,也可以是负向的。
让我先澄清一下"结构空间"这个术语。软件世界实际上包含两种本质不同的空间,它们共同构成了软件的"相空间":
编译时结构空间:由程序、模型、DSL 和配置构成的空间——这是软件"是什么"的层次;代码是在编译时被读取、合并和生成的结构化对象。
运行时结构空间:由进程状态、内存、依赖图和生命周期构成的空间——这是软件"在做什么"的层次;状态在运行时被创建、修改和销毁。
可逆计算理论明确指出:可逆性不需要全域性——它并不要求在整个相空间的每一层都实现可逆性。Nop 平台的实践证明了这一点:其主要的可逆性发生在编译时结构空间——所有 DSL 都支持增量合并(x-extends)和增量拆分(x-diff);增量运算发生在编译时和模型加载时,完全不触及运行时执行轨迹。相比之下,DSH 选择了一条互补的路径:在运行时空间实现可逆性——明确为每个 effect 配备一个逆函数,由运行时追踪并组合这些逆函数。区分了这两个空间之后,"可逆指的是结构空间中的 Delta 变更可正可负"这一陈述就获得了精确的含义——对 DSH 而言,这个"结构空间"特指运行时结构空间。
App = Base + (Plugin1, Plugin1 对运行时结构空间变更的逆) + (Plugin2, Plugin2 对运行时结构空间变更的逆) + ...
每个插件在激活时都会产生一个配对的撤销函数,其目的是逆转该插件在激活期间对运行时结构空间所做的更改。当插件被卸载时,这些撤销函数会按一定顺序执行。
在概念层面,这对应于:
Structure = Base + Delta1 + Delta2 - Delta2 + Delta3 = Base + Delta1 + Delta3
Cordis 论文证明了在安装插件 1、2、3 之后,如果卸载插件 2,结果在观察上等同于从头开始安装插件 1 和 3。
上述内容可能听起来很抽象;让我们来看一个具体的 DSH 插件示例。
// tool-stats.ts —— 一个会话统计插件
import { defineTool } from '@deepseek-ai/dsh-tools'
import type { Context } from '@deepseek-ai/cordis'
export const name = 'tool-stats'
export const inject = ['tools'] // 声明依赖:此插件仅在 tools 服务就绪后激活
export function apply(ctx: Context) {
// 1. 注册一个模型可以调用的工具
ctx.tools.register(defineTool({
name: 'stat_summary',
description: '输出本会话的调用统计信息',
parameters: {},
output: {
schema: { type: 'string' },
render: (_args, value) => [{ type: 'text', text: value }],
},
async execute() { return summarize() },
}))
// 2. 监听 LLM 请求失败事件(瀑布流模式;必须调用 next() 向下游委托)
ctx.on('agent/request-error', async (payload, next) => {
countFailure(payload)
return next()
})
// 3. 一个手动管理的资源:心跳定时器
ctx.effect(() => {
const timer = setInterval(heartbeat, 60_000)
return () => clearInterval(timer) // 配对的撤销函数
})
}
当插件被激活时(调用 apply),它做三件事:在工具注册表中注册一个工具、在事件表中注册一个监听器、启动一个定时器。表面上这似乎是三个普通的注册操作,但关键点在于——这三者都以 effect 的形式注册。这里的机制是:插件实例(fiber)直接将其自身的 effect 方法暴露为 ctx.effect(它们是同一个方法);像 ctx.on 和 ctx.tools.register 这样的注册 API 内部最终都归结到它——effect 体执行实际的注册操作并当场返回撤销函数;运行时自动将此 effect 记录在当前 fiber 的可处置列表中。作者无需手动注册任何东西——每次注册在完成时,都将其撤销函数交给运行时:
ctx.effect 和 fiber.effect 是同一个方法——fiber 通过 mixin 将其作为 ctx 的属性暴露出来。ctx 是插件接收的接口;fiber 是实际执行注册和清理的实体。layers.effect 是建立在 ctx.effect 之上的领域级封装,用于工具注册中心,负责"注册到全局层或当前作用域的专属层、自动回收空层、以及可见性变更通知",最终仍然归结到 ctx.effect。
运行时按顺序将这些撤销函数累积在 fiber 的可处置列表中。当插件被卸载时(fiber.dispose()),_unload() 会清除此列表并执行其中的撤销函数——在同一个 effect 内,按注册的反向顺序执行,像栈展开一样(disposables.splice(0).reverse())。于是:监听器被注销,工具从注册表中消失,定时器被清除。整个运行时结构空间恢复到安装前的状态——无需重启,无残留。
撤销函数的执行不仅发生在卸载时——作者可以在任何时候手动调用注册 API 返回的 disposer 来提前注销(例如 llm-retry 在自己的撤销逻辑中先手动注销监听器,然后中止并排空正在进行的重试);热重载(fiber.restart())和注入服务的撤回(依赖丢失触发 _refresh → 卸载并重新加载)同样先执行撤销函数再重新加载。可逆性不是"临终算账",而是一种随时可以相减的能力。
还有一个容易混淆的问题:撤销函数记录在谁的账本上?插件代码可以在单个进程内被多次实例化;每次 ctx.plugin() 挂载都会产生一个独立的 fiber——带有自己派生的 ctx 链、自己的可处置列表、以及自己的生命周期。DSH 中的每个 agent 作用域都是这样创建的:createScope 内部通过 ctx.plugin(scope) 挂载一个专属 fiber(dsh/packages/core/scope/src/index.ts);作用域内注册的所有内容(工具、护栏、监听器)都记录在这个 fiber 上,而 scope.dispose() 独立地清理它——"一份插件代码、多个实例"的隔离正是依赖于 fiber 的独立生命周期。相比之下,ctx.isolate(name) 只改变服务的存储槽(同名服务可以共存而不冲突),不会产生新的 fiber——在隔离子 ctx 上进行的注册仍然沿用原来的 fiber。因此 ctx 链(扩展原型链)同时决定了两件事:可见性(沿链读取哪个服务实现)和所有权(ctx.fiber 决定注册记录到哪个账本,以及级联卸载应用于哪个子树);服务配置覆盖(intercept)也沿此链继承。
用公式表达,安装此插件对应于:
App = Base + (Plugin, reverse of Plugin's changes to the runtime structural space)
= Base + Δ(tool + listener + timer) + Δ⁻¹(set of undo functions)
卸载就是应用 Δ⁻¹,等价于对运行时结构空间做一次减法:App = Base + Δ - Δ = Base。注意,被撤销的始终是结构,而不是业务数据:stat_summary 执行时读取的日志、countFailure 累积的计数——这些都不会被"撤销"。DSH 的可逆性针对的是运行时结构空间(谁注册了什么、谁在监听什么、谁占用了什么资源),而不是业务状态。这就是"Delta 可以是正或负"的精确含义:正负操作的对象是结构,不是状态。
一个细节:DSH 的"反向顺序"保证只在单个 effect 内部是严格的 LIFO;一个 fiber 的多个顶层 effect 在卸载时并发清理——它们的贡献相互独立,不需要排序。如果拆解序列有排序要求,官方指导是将它们写在同一个 effect 内,并在同一个撤销函数内按顺序 await 它们。
通过这个示例,论文的结论"安装插件 1、2、3 然后卸载插件 2 在观察上等同于从头安装插件 1 和 3"变得直观了:插件 2 的所有更改都记录在自己专属的 fiber 上,卸载时完全逆转;插件 1 和 3 的贡献相互独立——依赖通过 inject 显式声明,当插件 2 卸载时,依赖它的插件会被通知重新解析,不会留下悬空引用。因此,卸载后的运行时结构空间等同于"只安装插件 1 和 3"所能产生的结果——就像执行一组相互独立的语句 A; B; C,然后撤回 B,残留状态与直接执行 A; C 完全相同。区别在于 DSH 将"撤回 B 的效果"从口头承诺变成了运行时保证的、可验证的机制。
更进一步:既然安装和卸载都是对运行时结构空间的加法和减法,"动态插件更新"就成了自然的推论——对旧插件应用 Δ⁻¹,然后对新插件应用 Δ:
App = Base + Δ(old) - Δ(old) + Δ(new) = Base + Δ(new)
这就是 DSH 热更新(HMR)和运行时自我修改的整套机制秘密:无需重启,因为结构空间的更改本身就是可逆的。DSH 中这种模式的一个生产级实例是 llm-retry 插件(dsh/packages/llm/llm-retry/src/index.ts):它监听 agent/request-error 事件来实现重试策略,并通过 ctx.effect 注册"中止所有正在进行的重试并等待其排空"作为其撤销函数——与上面的示例骨架完全一致。
最后,有一点值得明确说明:在 DSH 中,这种"减法"之所以有效,并非因为运行时自主推断如何撤销,而是因为每个插件在安装时就写好了并交出了自己的撤销函数——"Δ 与其逆元相伴而生";逆元是插件作者在 apply 中写下的契约,而非运行时推导的属性。论文对这一点说得很清楚:运行时并不验证撤销函数是否正确(那是组件作者的义务);它保证的只是——你的撤销函数会被保存、会执行、且不会执行两次。
Pi Agent 同样是高度插件化的,所以如果"一切皆插件"仅仅意味着"框架支持许多扩展点",那这确实不是 DSH 的发明。两者真正的区别不在插件数量,而在一个问题:
扩展坐标是由核心预定义的,还是可以由插件通过 service/inject 关系持续生成?
"坐标"是理解 DSH 的关键术语。让我们先拆解它的含义。
3.1 ctx 是运行时结构空间的接口
DSH 的运行时结构空间由三部分组成:
共享坐标表。 整个运行时只有一张服务表(根 ctx 的 ReflectService;所有子 ctx 沿原型链共享它)。表中的每个槽位通过坐标对"服务名 + 隔离标签"定位;槽位记录实现值及其所属 fiber(dsh/vendor/cordis/src/reflect.ts)。
fiber 树。 每次 ctx.plugin() 挂载都会产生一个 fiber(插件实例)及其专属的派生 ctx(parent.extend({ fiber: this })),位于 dsh/vendor/cordis/src/fiber.ts。fiber 是结构空间的生命周期单元:依赖满足时激活、卸载时回收哪些资源,都由它决定。
ctx 链。 插件收到的 ctx 实际上是一个 Proxy——属性读取和写入被拦截并翻译为坐标查询(dsh/vendor/cordis/src/context.ts)。每个 ctx 都携带一个"标签视图"(服务名 → 隔离标签);当查询 ctx.database 时,当前 ctx 的视图首先将服务名翻译为表中的一个槽位;子 ctx 未修改的名称沿原型链继承父 ctx 的视图。一个直观的例子:父 Agent 的 ctx.shell 指向本地环境,而子 Agent 的 ctx.shell 指向受限沙箱——工具调用者调用的 key 相同,但解析出的实现不同。
因此,整个运行时结构空间可以看作多层 delta 叠加;ctx 是对外暴露的接口(Proxy 视图);fiber 是负责空间内动态生命周期管理的单元;"服务名 + 隔离标签 + 视图链"是坐标;注册项是坐标上的值。插件的所有操作——注册工具、订阅事件、提供服务——都是对某个坐标的写入,而"擦除此坐标上的值"的撤销函数当场交给运行时。工具注册表正是这样一个坐标表:每个工具占有一个以 name 为键的槽位——同层同名注册直接报错(duplicate error,NamedEntries.insert 位于 dsh/packages/core/scope/src/store.ts);跨层同名则近层遮蔽远层(dsh/packages/core/tools/src/index.ts)。事件以事件名为键;服务以服务名为键……"一切皆插件"统一的正是这样一个键值坐标空间。
3.2 ctx 链与 delta 层
与 Nop 的 delta 文件系统对比:同一资源路径在多层目录中各自存在;查找从高层向低层进行,取第一个命中——高层文件完全遮蔽低层文件。DSH 的 ctx 链:每个 ctx 贡献一个"服务名 → 隔离标签"的映射;子 ctx 未修改的名称沿原型链继承父 ctx 的映射,最终都指向同一张共享服务表——读取 ctx.database 沿链取最近的标签映射。两者都遵循"分层命名空间 + 最近优先"模式,只是物理布局不同:Nop 的层携带内容(文件活在层中,查找跨越层);DSH 的层携带路由(值活在共享表中,层只决定查哪个槽位)。x:extends 的语义骨架——"最具体层覆盖,其余保留为回退"——与原型链查找是同一回事。
然而,两者存在以下差异:
合并粒度。 x:extends 执行节点级合并:base 与 current 文件按子节点标识匹配;属性在属性级别覆盖;base 中未匹配的内容保留在结果中;特定节点也可通过 x:override="remove" 删除。DSH 的 provide 是整值遮蔽——子 ctx 与父同名的服务完全覆盖父实现;父实现在表中仍然存活,但在子视图完全不可见(没有操作可以"保留父实现的一部分")。DSH 的"保留与合并"只出现在值为数据时:拦截配置沿链按序合并(后代覆盖祖先,类似于 x:extends 的属性级覆盖);工具注册表跨层做并集组合(所有层的条目都可见,同名取最近遮蔽,类似于 delta 目录合并)。但当值是对象时(服务实现、监听器),合并不存在——两个闭包无法合并,只能被替换,或通过事件瀑布串成洋葱。
时间性。 Nop 的 delta 层在汇编时确定,除 nop-dyn 模块提供的租户隔离层外,一般不会动态变化。每个 DSH 层绑定到一个 fiber,可以在运行时创建、重启和卸载;卸载触发依赖方的协调。因此 ctx 链不仅是空间分层,也是时间生成:空间上决定坐标可见性;时间上决定生命周期所有权。论文所说的"时空可组合性",追溯到源代码,正是这两个正交维度:插件驻留在哪个 ctx/realm,能看到哪层服务;以及依赖关系何时被满足。