乐于分享
好东西不私藏

3 分钟 AI 学院 | 插件卸载要重启进程,DeepSeek 和北大想从根上解决

3 分钟 AI 学院 | 插件卸载要重启进程,DeepSeek 和北大想从根上解决

插件能不能热插拔,是件工程里天天在做、却一直没人给过理论基础的事。

VSCode 里装了扩展想禁用,怎么办?大多数人的答案是重启整个编辑器。这不是 VSCode 偷懒,是整个插件系统家族的通病:extension host 进程没法在运行时把单个扩展的代码卸下来,一旦 activate 跑过,禁用就得连累所有扩展一起重启。

DeepSeek 和北京大学的一篇论文《A Programming Paradigm for Spatiotemporal Composability》想从根上把这个窟窿补上 [1]。作者 Yifan Shi、Wei Zhang 来自北大,Tianyi Cui 来自 DeepSeek-AI。他们把"动态组合"这件事重新放进 effect / coeffect 这套类型论语言里,给运行时来去的组件补一套它一直缺的扎实理论。下文除特别注明的 Cordis [2]、Koishi [3] 外,事实均出自该论文,不再逐句标 [1]。

先说清问题到底卡在哪

论文先把"动态组合"拆成两个正交的维度。

一个是时间可组合性:组件卸载时,它对共享环境干的那些活(分配的资源、注册的事件、改的状态)必须被完整、安全地撤回。静态世界里,这就是 RAII、bracket 那套词法作用域的活儿。

另一个是空间可组合性:组件得能声明、发现、并响应式地管理自己依赖的其他组件。静态世界里,这就是模块导入解析。

一旦组件在运行时来去,两件事都变难。副作用跨越了非词法的长生命周期;依赖也会在执行中冒出来、消失、改换身份。插件系统就是典型。

VSCode 的数据挺说明问题。论文统计了 VSCode Marketplace 安装量前 100 的扩展:87 个带可执行代码,卸载都得重启 host;只有 7 个声明了对非内置扩展的依赖 [1]。临时性缺陷:没法运行时卸载单个扩展代码;空间性缺陷:跨扩展交互靠 getExtension(...).exports,返回值是 any,没有类型契约。论文还顺手点了一句,这俩毛病不是 VSCode 独有,所有插件系统都这样,只是程度不同。

粗粒度兜底的代价也讲得直白:操作系统在进程粒度给你时间可组合性,容器编排器在服务粒度给你空间可组合性。大多数软件就这么凑合。但重启会丢光进程级状态(缓存、连接、算到一半的结果),重建要数秒到数分钟。粒度错配是核心痛点:现代系统在比进程/容器更细的粒度上组合,却只能在进程/容器边界兜底。

把类型论"升"到运行时

有意思的地方在这。

effect 系统和 coeffect 系统本来是类型论里描述副作用和依赖的两套静态工具。effect 描述"计算怎么改环境",coeffect 是它的对偶,描述"计算怎么依赖环境"——这俩恰好对应动态组合的两个维度。

但它们是静态的:effect 在词法固定作用域内跟踪,coeffect 注解在执行前就定好了。动态组合要的是,这些保证对运行时来去的组件、对持续演化的 context 仍然成立。没有词法作用域能框住部署后才加载的插件,没有编译期 context 能预判运行时配置涌现的依赖。

论文的转向很干脆:别再给静态类型系统加更多注解了,把 effect / coeffect 的概念结构实体化(reify),让运行时直接操作它们,动态地建立这些系统在静态时给出的保证。

可逆 effect:副作用能被运行时回滚

具体怎么做?时间维度上,论文搞出可逆 effect(revertible effects)。

核心构造是一个叫 effect context 的东西,可以理解成一个 pair:一边是当前 context 状态,另一边是个"累加器",即到目前为止所有副作用逆函数的复合。每次执行一个副作用,正向变换施加到状态上,逆函数复合进累加器。卸载组件时,按序应用累加器,环境就恢复了。

这里有个从单组件走向多组件的关键:独立性(independence)。逆函数可能要在"被后续其他 effect 移动过的状态"上运行——这正是从运行中系统撤回一个组件的情形。两个 effect 独立,意味着彼此的每个变换都满足交换律,且互不干扰对方产出的逆。在独立性下,逆函数可以按任意顺序应用都回到初态。

LIFO 只是其中一种排列,独立性买到的是"任意顺序",从而支持多个组件副作用的交错。这点很关键,因为现实里组件就是交错的,不是排队来的。

反应式 coeffect:依赖随环境变化而激活/停用

空间维度上,论文搞出反应式 coeffect(reactive coeffects)。

组件把自己需要的依赖声明成一个 specification。每当 context 变化,系统按 spec 把这次变化分类成三类:activating(依赖凑齐了,激活)、deactivating(依赖没了,停用)、neutral(不影响,啥也不干)。

最妙的协同在这:coeffect 的 set 操作,类型上恰好就是一个可逆 effect。也就是说,依赖注册本身就是副作用,副作用是可逆的。所以依赖的安装自动获得跟踪与恢复。这俩机制不是两套并行的东西,是一套东西的两个面。

统一成一个 context 范式

论文把承载 effect 和承载 coeffect 的两套 context 合并成单一递归类型,叫 context 范式。

这里有个工程上很实在的取舍。恢复保证本来断言的是状态相等,但物理状态没法原样恢复——free 不恢复 malloc 前的堆布局,生成的名字也不会被丢弃它的逆恢复。所以等式得"读到一个等价关系上":两个状态在"任何观测者都无法区分"时算相等。

观测者拿到的是 coeffect,每个自带等价。把这个等价"商掉",恰好买到了上面那个独立性所需的条件。论文证明:不同键上的操作天然独立;一个键是交换的,当它的值是"独立增删条目的表"——路由注册、事件监听是典型;有序链则不行。

这个取舍挺有工程味道:不是所有副作用都得严格可逆,只要在"观测者能区分"的粒度上可逆就够了。

演算的元理论:动态历史不留痕迹

论文把上面这些机制封装进 component 和 fiber,配了一套操作语义,然后用元理论把保证从单组件推广到一整个交错系统。

几个性质里,最有分量的是 confluence:无论系统经历过怎样一串激活和停用,它最终静息到的状态,正是"把最终活跃的组件按依赖序各装载一次、从不卸载"的静态装配产生的状态 [1]。

说白了,动态历史不留痕迹。不管你中途怎么折腾,最终态跟从零搭一遍是一样的。这是动态组合对"从零求值一致性"的类比,也是后面那个声明式 loader 可靠性的根基。

还有一个 progress 性质,前提是依赖图无环。环会让相关组件永远不活跃,但好处是这能从声明静态预测出来,加载时直接报错,不像并发系统里的死锁要等发生了才抓。

Cordis:不只是论文,是真跑起来的东西

这篇论文不光是理论,实现叫 Cordis,是个元框架 [2]。

注意"元框架"这个定位。它不绑定具体场景(不搞 web 路由、不搞 ORM、不搞 UI 渲染),只供应通用的动态组合语义。应用框架在它上面搭。

三层结构。核心库做 effect 跟踪和 coeffect 解析;声明式 loader 做配置调和和热模块替换;再上面是应用框架。

热模块替换(HMR)这块值得单独说。因为一个 fiber 已经界定了组件所有的副作用和依赖,模块如果本身是个组件,dispose 旧 fiber、实例化新 fiber,就能原地替换,不需要像 Webpack 或 Vite 那样让开发者标注接受边界。这算是把可逆 effect 的模式搬到了模块级。

Koishi:4000 个插件的验证

最有说服力的是 Koishi 这个案例。

Koishi 是个开源聊天机器人框架,建在 Cordis 上,四年累积了 4000 多个社区插件 [3],从 IM 适配器、数据库驱动到管理控制台和各种用户功能都有。

论文拿它验证了三件事。

表达力够:同一套模型,既跑服务端 bot,又跑 web console,框架本身只贡献领域词汇。

时间可组合性没认知开销:在控制台禁用插件,副作用原地撤回;开发时保存文件就触发 HMR,重应用编辑后的插件,别处的缓存和连接都还在。关键一句——"连卸载路径都不用写"。因为经 context 的副作用自动被跟踪、逆自动被组合,连新手作者都能拿到有序清理,不用自己写 uninstall 逻辑。

跨开放生态的空间可组合性:IM 适配器、数据库驱动、功能插件,往往是不同作者独立写的,只靠连接它们的那个 coeffect 协调。reactive coeffect 保持组装一致。

作者也老实承认,这是存在性和采纳性证据,不是定量对照。分离范式价值和 TypeScript 实现、Koishi 领域的优势,测开销和生产力影响,都留作 future work。

最值得想的一块:自演化 Agent harness

论文结尾指了个方向,跟当下 AI 这波关系最大:自演化 Agent harness

现在的 AI agent harness 要组合工具套件、执行环境、权限沙箱、会话状态、记忆系统、子 agent 工作流。未来的 harness 可能边服务请求,边生成并部署对自己组件的修改。这种持续、少人工监督的自我修改,正是动态组合。

论文把这种场景的痛点讲得很透。没有时间可组合性,每次自改都得全量重启、丢掉进程级累积状态,在自改频率下累积不可用很可观;更糟的是一次坏的自改可能把"用来恢复的那个进程本身"搞挂。没有空间可组合性,每个模块得自己探测所依赖模块的来去,naive 的代码替换会悄悄打断依赖者,循环依赖要 reload 时才暴露。

把 Cordis 用到这种场景,验证的就是两件事:快速组件替换下的完整恢复(时间),频繁拓扑变化下的依赖协调(空间)。论文说这能证明这个范式可作"可恢复、可协调、持续自演化"的 agent harness 基础。

当 agent 开始改写自己的运行时,"副作用可逆 + 依赖可反应"就从锦上添花变成刚需了。

它在前人里的位置

论文在相关工作里把自己摆得挺清楚。

最接近的时间可组合性前例分四类。有状态前向迁移那类(DSU、Erlang/OTP、webpack/Vite HMR)优雅地迁移内存状态,但需要手写迁移函数,且只能原地更新不能整体卸载。开发者手写恢复那类(OSGi、Eclipse、IntelliJ、VSCode 的卸载回调,Command 模式,saga,event sourcing),逆是不强制的责任,漏写就泄漏。论文专门提了 React 的 useEffect,说它最接近"把 effect 和 inverse 结构性配对",但短板在可组合性:hook 只能在组件顶层或另一个 hook 里调,不能放条件、循环、嵌套函数里,effect 体还不接受 async 或 iterator。所以 effect 没法从别的 effect 组装出来,也做不到和控制流交错,没法从中导出复合逆。

Cordis 不受这些限制:effect 是普通操作,自由组合、可异步,只要求每个原子 effect 手写一个逆,复合的逆由组合自动推导。

空间可组合性这边,最接近的是 OSGi 的 Declarative Services 和 iPOJO,论文说它们直接预示了 ctx.provide/ctx.get 模式。但它们的停用回调手写且同步,没法 await 异步 teardown。Cordis 的惰性 Unloading 状态机补的就是这个口。

一点判断

这篇论文没发明什么全新概念,effect、coeffect、依赖注入、热重载,单拎出来都不新。它做的是把"副作用可逆"和"依赖可反应"变成运行期的结构性保证,而不是开发者的自律。

插件生态和 Agent harness,表面看是两个领域,底层共享同一个问题:组件在运行时来去,环境得能干净地回收它,依赖得能优雅地重组。以前这件事靠重启进程兜底,靠开发者自觉写 deactivate。Cordis 想把它变成框架强制的、有形式化保证的事。

能不能从 Koishi 的聊天机器人场景,真正走到高频自演化的 Agent harness,还有距离。论文自己也没把这事做完,列成了未来验证方向。但方向值得认真对待:当 agent 越来越多地动自己的运行时,没有这层保证,每一次自我修改都是一次赌博。

参考资料

[1] Yifan Shi, Wei Zhang, Tianyi Cui. "A Programming Paradigm for Spatiotemporal Composability." Peking University / DeepSeek-AI, 2026.(本文核心事实均出自该论文;可按标题与作者检索公开 preprint。)[2] Cordis — 论文实现的时空可组合性元框架(核心库 + 声明式 loader + HMR)。https://github.com/cordisjs/cordis[3] Koishi — 开源聊天机器人应用框架,Cordis 之上的生产级实现与论文案例研究对象。https://koishi.chat