夜雨聆风学习资料网

ARTICLE · 1222076

DeepSeek 说"一切皆插件",靠的是一个 2022 年的框架

DeepSeek 说"一切皆插件",靠的是一个 2022 年的框架

10 月 17 日,Harness 更新到 0.2.1-rc.2:适配新模型、接入自研搜索。两个月 25 万 star 的背后,是“连界面和 Agent 循环都能换”的架构;但“更强”目前还没有独立证据。

2026 年 10 月 20 日 · 科技解读 · 全文约 9,500 字 · 资料截至 2026-10-20

一、先从一个所有人都遇到过、但没人真正解决的麻烦说起

你在 VS Code 里装了个扩展,用着不顺手,禁用。VS Code 提示你:需要重启窗口。

这不是 VS Code 懒。它把成百上千个扩展的代码跑在同一个共享进程里(叫 extension host),扩展一旦被 activate 激活,它的注册项、事件监听、定时器就已经散布在进程各处了。VS Code 确实给了一个 deactivate 钩子,但那个钩子只在整个宿主进程退出时被调用一次——它不是用来"卸载某一个扩展"的,是用来"临终告别"的。

数字更说明问题。论文《A Programming Paradigm for Spatiotemporal Composability》统计了 VS Code 应用市场按安装量排的前 100 个扩展:87 个包含可执行代码,它们被禁用或卸载时都需要重启宿主;而声明了"依赖另一个非内置扩展"的,只有 7 个。(数据取自 2026 年 6 月 9 日的 Marketplace,论文作者自采。)

后半句的 7 很关键,但它不是"大家都很独立",而是这个扩展模型根本没给扩展之间留下正经的依赖通道。VS Code 让扩展通过一组固定的接口点(命令、视图、语言特性)向宿主"投稿",而不是彼此依赖。你想让 A 扩展用 B 扩展的能力?可以,vscode.extensions.getExtension(...).exports 拿到的对象类型是 any——没有任何契约。

于是我们得到一个奇怪的现状:软件行业最擅长"装",最不擅长"卸"。

这不是 VS Code 一家的问题。前端改一行代码要重启 dev server,React 组件卸载时你要记得在 useEffect 的返回值里退订,Java 的 Spring 容器关闭时统一销毁 bean——我们有一整套关于"初始化"的成熟工程习惯,关于"撤销"的习惯却几乎全靠自觉。

Cordis 想解决的,就是这件事。

二、Cordis 是什么,以及三个几乎所有人都搞错的地方

先说结论:Cordis 是一个 TypeScript 写的"插件元框架",它不提供 Web 路由、不提供数据库、不提供 UI,只提供一件事——组件怎么装、怎么卸、怎么互相依赖。

它自己的 tagline 是论文标题的直译:"Meta-Framework of Spatiotemporal Composability"(时空可组合性元框架)。

先看一眼它的仓库首页。请留意四个位置:① 右侧 About 栏的 tagline;② 星标数;③ 许可证;④ 右下角的 Releases 区域。

图注:① 仓库自我定位就是论文标题;② 9.3k star;③ MIT 许可;④ 总共只有 3 条 Release,最新一条是 4.0.0-rc.11。截图日期 2026 年 10 月 20 日。

看完这四处,你应该能得出两个判断:这是一个真实存在、可自由使用的项目;但它还没到 4.0 正式版——"Releases 3"这个数字不是省略,是全部。

在继续之前,得先拆掉三个很常见的误读。

误读一:这是 DeepSeek 2026 年新发布的框架。

不是。cordiverse/cordis 仓库 2022 年 5 月就建了,npm 上的 1.0.0 发布于 2022 年 5 月 20 日,3.0.0 发布于 2023 年 10 月——那时候 DeepSeek 还没现在这么出名。2026 年 8 月之所以突然被讨论,是因为两件事同时发生:论文挂上了 arXiv,以及 DeepSeek Harness 把它整个搬进了产品。

这个误读不是我编的。Hacker News 上就有人明确写道:"据我所查,Cordis 在他们把它和这个 harness 一起分享之前并不存在"——他还补了一句,"很可能是因为我没有用中文搜索"。这条评论是错的,但它恰好说明:一个 2022 年就开始的项目,在 2026 年是被当作"新东西"接收的。

误读二:它已经是正式版。

截至本文写作(2026 年 10 月 20 日),Cordis 的最新版本是 4.0.0-rc.11,发布 2026 年 10 月 14 日。GitHub 上总共只有 3 条 Release,全是候选版。README 里有一句原话:"The API is not yet stable and may change without notice."

误读三:这是 DeepSeek 的论文。

论文作者三位:Yifan Shi(北京大学 + DeepSeek-AI)、Wei Zhang(北京大学)、Tianyi Cui(DeepSeek-AI)。是产学合作,不是公司内部技术报告。

图注:论文 2026 年 8 月 26 日提交,作者单位标注为 (1) Peking University、(2) DeepSeek-AI;Comments 一栏写着"92 pages, 1 figure, 2 tables"。注意"92 页 / 1 张图"这个比例——它是一篇形式化论文,不是工程报告。

还有一条容易被忽略的:Cordis 有两条版本线。 上游开源仓库是 4.0.0-rc.11;DeepSeek Harness 里 vendor 的那份叫 @deepseek-ai/cordis,版本 4.0.5,记录的上游基线其实是 rc.7。这两个数字不能混着引用——后面讲到 DSH 时会再提一次。

三、第一个机制:每一次改动,都必须能倒着走

现在进入正题。

想象一个插件要做三件事:打开一个文件句柄、订阅一个事件、往全局注册一个命令。传统做法是,插件作者还要再写一段 uninstall,把这三件事反过来做一遍。忘了写,就是内存泄漏、幽灵监听器、重启才能恢复的诡异状态。

Cordis 的做法是:不给你"忘了写"的机会。

它的核心 API 只有一个,ctx.effect(callback)。你在这个回调里做任何对环境的改动,回调要返回一个函数——这个函数的作用是把这个改动撤销掉。

ctx.effect(() => {

constwatcher = fs.watch('config.yml', onReload)

return() => watcher.close()// 这就是"逆"

})

看起来像 React 的 useEffect 返回值?是,但有一个决定性的区别:Cordis 的 effect 可以写成生成器函数,一步步地申请资源、一步步地交出逆操作。

ctx.effect(function* () {

connect();yield() => disconnect()

subscribe();yield() => unsubscribe()

registerCommand();yield() => unregisterCommand()

})

运行时会把每一个 yield 出来的逆函数,按"后进先出"的顺序折成一个累加器。组件卸载时,执行这个累加器——最后申请的,最先释放。

这个设计的妙处在于中途失败也能回滚。我拿 rc.11 在本地跑了一遍:让插件在申请完前两个资源之后被强制卸载,实际输出是

acquire-1, acquire-2, release-2, release-1

顺序完全正确。再让它加载到一半抛异常,输出是 acquire, release——已经拿到的资源照样被还回去了。

论文把这个想法形式化了。它把"一次副作用"定义成一个函数:输入当前上下文,输出"新上下文 + 一个逆函数"(记作 Γ → Γ × (Γ → Γ))。论文证明了几件对工程有直接意义的事:追踪逆函数不会改变正向行为(Theorem 4);逐个追踪和一次性追踪复合结果是等价的(Theorem 5);一串逆函数按逆序执行,能把上下文精确恢复到起点(Theorem 16)。这些证明构成了"卸载"这件事的数学骨架。

但这里有个东西需要看得更仔细,因为它决定了整套机制的成色。

逆函数是插件作者提供的,运行时并不验证它真的能撤销。

论文 §5.1.1 写得非常直白:回调提供一个逆,"而这个逆确实撤销了它所伴随的副作用,是组件作者的义务,不是运行时验证的性质"。换句话说,Cordis 把"写对逆函数"从"要记得写清理代码"降级成了"要写对清理代码"——负担变轻了,但没有消失。

那么,什么才算"能撤销"?论文给了一条界线,叫系统边界。一个位置在边界之内,当且仅当系统能独占修改它、并能把它恢复原状——比如一块只有你在写的内存、一个私有目录里的临时文件。这种改动可以被追踪、被回滚。

而像"发一条消息""扣一笔款""写一个别人也在读的文件"这类操作,在边界之外。它们不能真的回滚,只能做两件事之一:要么延迟到确定要提交时才发出去(withholding),要么事后做补偿(compensation)——删掉刚创建的文件、退款。论文自己承认,补偿动作不满足论文定义的等价性,它的正确性需要另外证明。

这一句很重要,因为它是"热插拔"这个说法最常见的夸大来源。Cordis 能回滚的是进程内的上下文,不是外部世界。

下面这张图,是理解这一切的关键。它是论文全文唯一一张图。

图注:四个状态、八条迁移。O- 开头(O-Insert / O-Remove)是编排器从外部发起的动作;L- 开头(L-Begin / L-Iter / L-Finish / L-Divert / L-Leave / L-Unload)是系统自己推进的步骤。最左边的空心圆点表示"这个组件还不在注册表里"。

这张图要这样看:先找到 Inactive(不活动),它是最常见的起点。然后注意它有两条出路——L-Begin 走向 Reloading(正在装载),或者被 O-Remove 彻底移除。关键是 Reloading 这个状态的存在:它说明"装载"不是一瞬间的事,而是一个可以被观察、被中断、被回滚的过程。L-Divert 这条边就是"装到一半发现不对,转去卸载"。

再看 Active → Unloading → Inactive 这条路。注意 L-Leave 是进入 Unloading 的第一步——这一步的含义是"我已经不再对外提供服务了,但我还在释放资源"。这个顺序不是随便定的:它保证别的组件不会在你拆除的过程中还来找你办事。

看完这张图能得到的认识是:在 Cordis 眼里,一个插件不是一个开关,而是一台有明确档位的机器。论文的九条演算规则里,有八条就落在这张图的边上(剩下那条 O-Retire 作用于所有状态,画成自环会重复四次,所以省略了)。

一句话记住这一节:卸载不是删代码,是倒放录像。

四、第二个机制:依赖不是排队顺序,是入场条件

第二个问题更难。

假设你的插件需要一个数据库连接。传统做法是:启动脚本里先连数据库,再启动插件;插件里假设数据库已经在了。如果数据库没连上呢?插件要么报错,要么启动到一半崩掉。

Cordis 换了个模型:插件声明"我需要什么",而不是"我先启动"。

const plugin = {

inject:['database'],// 我需要 database 这个服务

apply(ctx){

ctx.database.query(...)//到这里,database 一定已经在了

}

}

如果你在 database 还不存在的时候装载这个插件,它不会报错——它会停在 PENDING 状态,什么都不做,安静地等。等到某个插件提供了 database,它自动激活,apply 开始执行。我实测过:缺依赖时状态是 PENDING,提供方一出现,状态变 ACTIVE,apply 恰好执行 1 次,不会重复。

反过来更精彩。当那个提供 database 的插件被卸载时,会发生什么?

不是"先撤数据库,再让用它的插件崩掉",而是:先把所有依赖它的插件停用、让它们各自完成清理,然后才撤掉自己的绑定。 实测的清理日志是:

consumer cleanup → provider cleanup

顺序恰好反过来了——依赖者先退场,提供者后撤。

论文把这件事叫做响应式 coeffect。术语不用记,机制是这样的:每个插件声明一组"我需要的 key"(inject);每当上下文发生变化,运行时就把变化后的状态和这个声明比对一次,分三类:从"不满足"变成"满足"是激活,从"满足"变成"不满足"是停用,其他都是无关。无关的变化什么都不做。

这里有一个非常聪明的细节。运行时判断"依赖是否变化"时,比的不是值,而是提供者是谁。论文的实现里,每个 fiber 记着一个 target——对每个声明的 key,记录"当前是哪个 fiber 在提供它"。所以哪怕新提供者提供的值一模一样,只要提供者换了,依赖者也会重新加载;反过来,如果同一个提供者在原地改了自己提供的对象,依赖者不会被打扰。

这套机制还顺手解决了两个常见事故:

循环依赖不会死锁,会双双停在 PENDING。 论文 §6.5 指出,这在并发系统里要靠运行时检测,但在 coeffect 模型里,光看声明就能预判——A 要 B 提供的 key,B 要 A 提供的 key,两者的满足条件永远不可能同时成立。我实测确认:A 和 B 都停在 PENDING,没有报错,没有崩溃,就是安静地不工作。

未声明的访问会被拒绝。 我在插件里访问一个没在 inject 里声明的属性,直接抛出 cannot get property "nonexistent" without inject。这意味着依赖声明同时是一份能力清单——论文 §6.3 把它类比为 capability-based security:一个组件能碰到什么,在它运行之前就已经知道了。

图注:官方文档把 Cordis 的核心概念归纳成五条。本文讲的是第 3 条(通过 inject 声明服务依赖)和第 5 条(注册是可逆的副作用);另外三条——插件是 Service、上下文是服务容器、类型化事件——是这套机制的承载结构。

这张图值得看的是官方自己怎么归纳。注意第 1 条:"插件是实现 Service 的对象",以及第 2 条:"上下文是服务的容器,一个服务占据一个稳定的 ctx."。这两条合起来解释了为什么第 3 条能成立:因为服务有稳定的名字,依赖才能用名字声明;而因为第 5 条成立——注册是可逆的副作用——服务才能在卸载时干净地消失。

官方文档开篇还有一句关键定性:"Cordis 是 DeepSeek Harness 底层以 vendor 方式引入的插件框架。" 这句话我们留到下一节展开。

一句话记住这一节:依赖不是排队顺序,是入场条件。

五、谁真的在用:一个用了四年,一个整个搬了进去

机制讲完了,接下来的问题是:这东西真的有人用吗?

两个证据,性质很不一样。

第一个是 Koishi。 一个 2019 年起的开源聊天机器人框架,跨平台、可装插件。

图注:论文 §5.3 原文。Koishi 是一个构建在 Cordis 之上的开源聊天机器人框架,四年间积累了 4000 多个社区插件;它的服务端与 Web 控制台是两个独立的 Cordis 应用。

论文这一段里,最值得注意的不是"4000+"这个数字,而是后面那句:Koishi 的 Web 控制台是第二个独立的 Cordis 应用——跑在浏览器里,组合的是浏览器的原语而不是服务端的。这证明这套抽象不绑定 Node 服务端,也不绑定聊天机器人这个领域。

4000 这个数字我试着交叉核对了一下:Koishi 官方插件市场的索引里约有 4781 个包,npm 上带 keywords:koishi 的包约 4351 个——同量级,对得上。

但要提醒一句:这是聊天机器人生态的插件数,不是 Cordis 生态的。而且论文脚注里写明,Koishi 目前用的是 Cordis v3,论文讲的是 v4。所以它证明的是"这套抽象能撑起一个四千插件量级的开放生态",不是"Cordis v4 生态繁荣"。

第二个是 DeepSeek Harness。

图注:① 254.7k star;② About 栏原文 "Everything is a Plugin.";③ topics 里直接列着 cordis;④ vendor 目录的最近提交信息:"release(vendor): cordis 4.0.5, cosmokit 1.8.6, group 1.0.5, hmr…"。

这张图的三处证据要连起来看。 第 ② 处是口号,第 ③ 处是标签,这两处都可能是"宣传"。真正硬的是第 ④ 处:vendor 目录的提交信息里明明白白写着 "cordis 4.0.5"。这说明 Cordis 不是 DSH 的并列依赖,而是被搬进仓库、纳入自己版本号的一部分。

这里的关键词就是 vendor。

DSH 没有在 package.json 里写一句 "cordis": "^4" 就完事。它把 Cordis 的源码复制进了自己的仓库,放在 vendor/ 目录下,改名成 @deepseek-ai/cordis,自己维护。仓库里的 vendor/README.md 解释了理由,用了三个词:auditable, patchable, pinned——可审计、可打补丁、可锁版本。

这不是一个随便的决定。它意味着 DSH 团队认为这层框架重要到不能交给上游的发布节奏,同时又不稳定到必须自己按住。

官方架构文档里有两句话,把这件事的性质说清楚了:

"Every part of the product is a plugin, including the model adapter, the tool registry, the session log, and the agent loop itself, so each is replaceable from configuration."

"There is no privileged core to patch: you extend dsh by mounting a plugin beside the others, and registrations are effects that unwind when their plugin unloads."

连 agent loop 本身都是插件。这句话的分量在于:它不是在说"我们支持插件",而是在说"我们没有一个需要打补丁的内核"。

但"可替换"是有代价的,而且代价被完整地记录了下来。

DSH 的 vendor/cordis-rc11-upgrade-plan.md 里,从 rc.7 升到 rc.11 这件事被拆成了 9 项(其中 PR 0 只是记录计划本身,其余 8 项才是实施),并且明确写着:升级 Core 的那一项"仍然是最大的一次必要兼容性变更"。vendor/README.md 里列着 24 条对上游代码的本地改动,每一条都写了理由——比如给 fiber 的生命周期加了三个防止重入的补丁,比如为了兼容构建链改了所有 TypeScript 的相对导入写法。

第三方也有独立印证。Hacker News 上一位用户(wren6991,2026-10-02)说了一句很到位的话:"'一切皆插件'的问题在于,当核心功能也是插件时,你想改点现有行为,还是得维护下游补丁。我目前有大约 25 个下游提交,和 0 个新插件。"

这个数字和 DSH 的 24 条本地改动是量级吻合的,但两者来源完全独立——一个是 DSH 自己的仓库文档,一个是第三方用户的随口一说。

这就是"一切皆插件"的真实成本:装配的自由度越高,维护的账越复杂。

六、证据的边界:它证明了什么,没证明什么

写到这里,最该谨慎的地方来了。

先看它证明了什么。论文的 §4 给了一套演算:九条规则、四个状态、一组关于系统行为的命题。其中对工程读者有意义的几条是:

· 恢复精确性(Theorem 68):卸载一个组件,只会把它自己造成的改动撤掉,不会波及别的组件。

· 顺序性(Theorem 70):一个组件只在它的依赖被提供时才进入装载;它的提供者要等它停用之后才撤自己的绑定。

· 解析一致性(Theorem 71):一次装载过程中,组件读到的依赖解析结果不会中途变化。

· 进展性(Theorem 73):系统不会卡死,最终会静下来。

· 合流性(Theorem 80):无论编排器以什么顺序下发装载和卸载,最终稳定下来的状态只由最终的配置决定。

这五条加起来,说的其实是同一件事:你随便怎么装、怎么卸,最后的结果和"从零开始按最终配置加载一遍"是一样的。 这是整个工作的核心承诺。

但这些证明的是演算,不是代码。

论文里没有任何机器验证——我全文检索过,没有 Coq、Lean、Agda、Isabelle。论文给的是纸笔证明。而"演算"和"TypeScript 实现"之间的关系,是一张对应表(Table 2),把理论符号和实现里的字段一一对上:Γ∞ 对应 ctx,fiber 对应 ⟨d,p,e,π,σ,τ,θ⟩,fiber.dispose 对应累加器 g……表本身是清楚的,但它不是证明。

论文也没有任何性能测量、没有用户研究、没有与替代方案的对照实验。作者在 §5.3 的"效度威胁"里自己写了这句话:

"What the case study establishes is thus an existence-and-adoption result rather than a quantitative one; measuring the abstraction's overhead and its effect on developer productivity against a baseline remains future work."

翻译过来:案例研究证明的是"这东西存在、且被采用了",不是一个量化的结论;测量它的开销和对开发效率的影响,是未来的工作。

我自己做了个小实验来补一点直觉(完整脚本和输出在素材包里):装载 10 / 50 / 100 / 200 / 500 个插件,到全部进入 ACTIVE 分别约 1.1 / 3.6 / 6.2 / 13.5 / 36.9 毫秒;在 200 个插件在场时替换其中一个,中位数约 0.137 毫秒。

但这组数字必须打上一个大大的折扣:我的插件全是空壳,只注册了一个空 effect,没有任何 I/O、没有真实业务逻辑。它测的是调度框架本身的开销,不能外推成"用 Cordis 就快"。论文自己也没做这类测量,我不打算替它宣称。

还有三个更实质的限制,需要贴着结论说清楚。

第一,逆函数不验证。 前面提过,运行时只负责收集和执行逆函数,不负责检查它是否真的撤销了副作用。写得不对,一样泄漏。Cordis 降低的是"忘记写"的概率,不是"写错"的概率。

第二,跨插件依赖的收益是有条件的。 Hacker News 上一位读过论文的开发者(badlogic,2026-08-13)提了一个很尖锐的观察:跨插件依赖这套机制"带着很多坑和限制",而且"大多数插件之间并没有依赖关系,我猜这是 90% 的情况"——在那种情况下,这套复杂系统"并不会给你带来多少好处"。这个判断和论文 §6.6 的自承能对上:coeffect 只按 key 名做名义链接,没有版本化、没有结构化兼容检查。两个包用同一个 key 名表示完全不同的接口,运行时会照单全收。论文承认这个问题,给了三种可能的方向(key 命名空间化、peer dependency、结构化兼容),然后说"设计一个统一的依赖模型仍然是未解问题"。

另一位用户(bjconlan,2026-08-18)说得更直接:这就是经典的 "dll hell" 问题——两个插件依赖同一个插件的不同版本,怎么办?论文的回答是:目前靠 npm 的 peer dependency,而 npm 的解析规则是"一个包只解析出一个版本"。

第三,热重载这件事上,Cordis 有一处明确更弱。 论文 §7.3 自己承认:webpack / Vite 的 HMR 会想办法把模块的内存状态往前迁移,而 Cordis 的做法是"撤销旧的、从干净状态重新应用新的"——组件自己的内存状态不会跨 reload 存活,除非你把它放到一个更长命的依赖里。所以别把"热重载"写成全面胜出,它在状态保持上是退了一步,换来的是不需要手写迁移逻辑。

最后,还有一个来自社区的、我认为必须正面回应的批评:这不就是 OSGi 吗?

Hacker News 上至少三位用户提出了这一点。一位说"听起来跟 Eclipse 的插件系统一模一样,每一代人重新发现一次";另一位说"关键在依赖求解器和命名空间隔离,而不是强迫所有客户端用同一个版本";还有一位读完全文后的结论是:"如果你不熟悉 OSGi、iPOJO、React 的 useEffect 这些东西(论文自己也提到了),它值得读;对其他人,略读就够了。"

这个批评有道理,但论文其实正面处理过。§7.4 把 OSGi 的 Declarative Services 和 iPOJO 称为"最接近的先例",然后指出了两处差异:它们的停用回调是手写的(资源安全依赖开发者自觉),而且是同步的(如果拆除需要与正在离场的依赖做异步交互,框架没有协议可以等待,只能阻塞在一个可能已经过期的引用上)。Cordis 的差异是:回滚的是累积的副作用,而且卸载是一个可以跑到完成、再响应下一次变化的异步过程。

需要说明的是,"OSGi 回调必须同步"是论文作者的批评,我没有去 OSGi 规范里逐字核实。所以更稳妥的表述是:Cordis 的贡献不在于发明了"服务随可用性启停",而在于把"回滚"和"异步拆除"这两件事补上了,并且给了一套可讨论的语义。

这个定位反而更准确:它不是发明了一个新品类,而是把一个二十年前就有雏形的想法,做成了现代 TypeScript 生态里可用的东西,并且第一次用形式化的语言说清楚了它到底保证了什么。

七、所以,这件事对你意味着什么

分三种人说。

如果你是写插件的开发者:Cordis 值得看的是 ctx.effect 的生成器写法。它把"申请资源"和"归还资源"放在同一段代码里、同一条执行路径上,这比"在 activate 里申请、在 deactivate 里归还"要难写错得多。MIT 许可,npm i cordis 就能试,不用先接受一整套世界观。

如果你在做 Agent 产品:DSH 的做法值得研究的不是它的功能,而是它的分层。profile 是一份命名的组合,bundle 是一组配置行加它们要挂载的代码,patch 按 id 覆盖某一行的配置——这三层叠起来,意味着"换掉模型适配器""换掉会话日志""换掉 agent loop"是配置操作,不是改代码。代价也摆在那儿:24 条本地改动、9 项升级计划。这是一笔需要提前算清楚的账。

如果你只是在看行业:这件事的信号是,模型公司的竞争正在从"模型分数"往"运行时"下沉。DeepSeek 不只发布了模型,它开源了一个 harness,而这个 harness 的地基是一个 2022 年就开始做的插件框架,并且为此配了一篇形式化论文。这在过去是学术界的动作,现在成了产品公司的动作。

至于 Cordis 本身,我的判断是:它证明了"可组合"这件事可以被认真对待,但还没有证明它值得所有人换过去。

值得关注的信号有三个:4.0 正式版什么时候发;除了 Koishi 和 DSH,还有没有第三方在生产里用;以及论文 §6.6 那个"版本兼容"的坑,会不会在真实生态里炸出来。

在这三件事有答案之前,"装得上、卸得干净"是一句可以验证的话;"所以它更好"还不是。

相关学习资料