---
title: Cordis 论文解读:插件系统的时空可组合性
published_at: 2026-08-14
language: zh
---
# Cordis 论文解读:插件系统的时空可组合性
插件系统真正困难的地方,不是“把功能拆成插件”。这个动作本身很简单:一个插件注册命令,另一个插件提供数据库,第三个插件监听消息。
困难出现在运行时。
一个插件可以被启用,也可以被禁用;可以被热更新,也可以因为依赖缺失而暂时停用。它加载时注册过事件监听、定时器、服务、命令和后台任务;它卸载时,这些东西应该被完整撤回。它依赖的服务可能晚一点才出现,也可能运行中被替换或消失。依赖变化以后,哪些插件要重新启动,哪些插件要停下来,不能靠每个插件自己临时判断。
Cordis 论文讨论的就是这个问题。
这篇论文题为 **A Programming Paradigm for Spatiotemporal Composability**。它试图给动态插件系统建立一套更一般的理论:当软件由一组运行时组件组成时,系统如何保证这些组件既能干净退出,又能稳定协作。
论文把这个目标叫做:**spatiotemporal composability**,可以译作“时空可组合性”。
## 动态组合比静态组合难在哪里
传统软件组合大多是静态的。
```text
函数调用
模块 import
类继承
编译期链接
```
这些关系通常在程序启动前就确定了。模块 A 依赖模块 B,编译器或运行时加载器可以在固定时刻解析它。
插件系统不一样。插件可能在程序已经运行很久以后才被加载,也可能在用户点击“禁用”以后被卸载。更复杂的系统,比如 agent harness,还可能在运行中生成新工具、替换组件、改变权限和执行环境。
这类系统面对的是动态组合:
```text
组件运行时加入
组件运行时退出
组件运行时替换
依赖运行时出现或消失
系统状态不能因为这些变化被随意丢弃
```
最粗暴的解决方案是重启进程。插件坏了,就重启宿主;依赖关系变了,就重启服务;状态不干净,就重新来过。
这当然有效,但粒度太粗。重启会丢缓存、连接、内存状态和进行中的任务。对于插件系统、开发工具、机器人框架、长期运行的 agent harness 来说,越来越多组合都发生在进程内部,只靠进程或容器级别的重启,已经不够细。
论文因此提出:动态组合至少有两个正交维度。
```text
时间维度:组件走了,它留下的副作用能不能撤回?
空间维度:组件之间的依赖能不能声明、解析并响应变化?
```
这就是“时空可组合性”的来源。
## 时间维度:组件卸载后,副作用能不能撤回
时间维度叫 **temporal composability**。
它关心的是:一个组件被移除时,它对共享环境做过的修改能否完整、安全地撤回。
一个插件加载时可能做很多事:
```text
注册事件监听
创建定时器
提供服务
添加命令
打开连接
启动后台任务
修改上下文状态
```
如果插件卸载时这些副作用没有清理干净,就会出现常见问题:
```text
旧监听还在响应
热更新后逻辑执行两遍
服务残留
定时器泄漏
连接没有关闭
状态污染后续插件
```
很多框架把清理责任交给插件作者:加载时写 `activate`,卸载时写 `deactivate` 或 `dispose`。这有一个结构性缺陷:**创建副作用和清理副作用分离了**。
作者在一个地方注册监听,在另一个地方记得取消监听;在一个地方启动任务,在另一个地方记得停止任务。只要漏掉一处,系统就不干净。
Cordis 论文把这个问题抽象为 effects。
Effect 描述的是:组件对环境做了什么修改。
论文进一步要求:动态组件里的 effect 应该是 **revertible effect**,也就是可撤回副作用。每次对 Context 的修改,都要同时带上一个 inverse,也就是反向操作。
可以这样理解:
```text
做一件事时,同时登记怎么撤回它。
```
注册监听时,登记取消监听;提供服务时,登记移除服务;创建资源时,登记释放资源。组件卸载时,Cordis 按顺序执行这些反向操作。
这就是 Cordis 生命周期机制背后的理论含义:
```text
Lifecycle 清理 = Revertible Effects = 时间可组合性
```
## 空间维度:组件之间的依赖能不能响应变化
空间维度叫 **spatial composability**。
它关心的是:组件之间的依赖关系能否被结构化声明、解析,并在环境变化时自动更新。
比如一个机器人系统里:
```text
数据库插件提供 database
适配器插件提供 bot
命令插件依赖 bot
用户插件依赖 database
AI 插件可能同时依赖 bot、database、http、logger
```
如果依赖靠每个插件自己写判断,就会变成一堆临时逻辑:
```text
如果 database 存在就启动
否则等待
如果 database 消失就停用
如果 provider 换了就重载
如果多个依赖同时变化就自己处理顺序
```
Cordis 的做法是让插件声明自己需要什么:
```ts
ctx.inject(['database'], (ctx) => {
// database 可用后执行
})
```
另一个插件提供服务:
```ts
ctx.provide('database', db)
```
当 `database` 出现、消失或换成另一个 provider 时,Cordis 负责通知依赖它的组件,重新计算这些组件的状态。
论文把这种“组件对环境的需求”称为 coeffects。
如果 effect 是:
```text
我改变了环境什么?
```
那么 coeffect 是:
```text
我需要环境提供什么?
```
Cordis 把 coeffect 做成运行时机制:组件声明依赖,Context 变化时,运行时根据依赖规格判断这个组件应该激活、停用还是保持不变。
这就是:
```text
Inject / Service / Dependency = Reactive Coeffects = 空间可组合性
```
## Context 为什么是核心
理解这篇论文后,Cordis 里的 `Context` 就不只是一个方便传来传去的对象了。
工程上看,`ctx` 可以做这些事:
```ts
ctx.plugin(myPlugin)
ctx.provide('database', db)
ctx.inject(['database'], callback)
ctx.on('message', handler)
ctx.emit('message', session)
```
理论上看,Context 同时承担两类职责:
```text
记录组件对环境做出的修改
记录组件对环境提出的依赖
```
也就是说:
```text
Context = effect context + coeffect context
```
它既是副作用追踪器,也是依赖解析器;既负责组件如何进入系统,也负责组件如何退出系统。
这也是论文把 Cordis 称为 **meta-framework** 的原因。Cordis 不规定你是在写 Web 应用、机器人、编辑器还是 agent harness。它只提供一个更底层的运行时语义:组件如何动态组合。
## Component 和 Fiber:组件在运行时的形态
论文里,一个 component 不是普通函数。它至少包含三类信息:
```text
它需要哪些依赖
它会提供哪些能力
它运行时会产生哪些 effect
```
在 Cordis 实现里,一个组件被实例化后会形成一个 **fiber**。
可以把 fiber 理解成:
> 某个组件挂载到某个 Context 后形成的运行时实例。
它有自己的生命周期状态,例如:
```text
LOADING
ACTIVE
UNLOADING
INACTIVE
FAILED
```
依赖满足时,fiber 可以从 inactive 进入 loading,再进入 active。依赖消失时,fiber 进入 unloading,执行副作用撤回,最后回到 inactive。
这个状态机很重要,因为动态组合不是瞬间完成的。加载可能是异步的,卸载也可能要等待清理完成。Cordis 要处理这些中间状态,避免依赖变化和卸载过程互相踩踏。
论文用更形式化的方式证明:如果单个组件的 effect 和 coeffect 满足要求,那么由多个交错运行组件组成的系统,也能保持时空可组合性。
## Cordis 如何落地这套理论
论文把理论和实现对应起来。落到 Cordis 里,关键机制有几层。
第一层是 `ctx.effect`。
所有会改变 Context 的操作都应该通过它完成。`ctx.effect` 运行一个 callback,并收集 callback 返回或产生的 inverse。卸载时,这些 inverse 会按 LIFO 顺序执行。
```text
后产生的副作用先撤回
先产生的副作用后撤回
```
这符合资源嵌套的直觉。
第二层是 coeffect store。
服务提供本身也是一种可撤回 effect。提供 `database` 时,Context 里出现一个 binding;撤回 provider 时,这个 binding 被移除,并通知依赖它的组件。
第三层是 reactive notification。
每次 Context 中某个 key 变化,Cordis 会检查哪些 fiber 声明依赖了这个 key,并重新计算它们的目标状态。
```text
依赖满足 → 启动或保持 active
依赖消失 → 卸载或保持 inactive
依赖变化 → 按需要重载
```
第四层是 component loader。
loader 处理更上层的工程问题:声明式配置、配置差异协调、热模块替换。Koishi 控制台里启停插件、开发时保存后热更新插件,背后就需要这种能力。
## Koishi 证明了什么
论文把 Koishi 作为 case study。
Koishi 是一个聊天机器人框架,有大量社区插件。论文提到 Koishi 生态有超过 4000 个社区插件。虽然论文讨论的是 Cordis v4,而 Koishi 当时使用的是 Cordis v3,但两者共享核心组合模型。
Koishi 说明 Cordis 不是纯理论模型,而是在真实插件生态里有落地价值。
它证明了两件事。
第一,插件可以在不重启整个宿主的情况下被禁用、卸载和热更新。Cordis 通过 effect tracking 把插件产生的上下文副作用集中追踪,卸载时自动撤回。
第二,插件之间可以形成真实依赖拓扑。数据库插件、平台适配器、功能插件、控制台插件都可以通过服务和依赖连接起来。依赖变化时,Cordis 只重新影响相关插件,而不是重启整个系统。
这和很多传统插件系统不同。论文以 VSCode 为例指出,VSCode 扩展通常贡献到宿主提供的固定扩展点,而不是广泛依赖彼此;禁用带可执行代码的扩展往往需要重启 extension host。Cordis 想提供的是更细粒度的动态组合。
## 这套理论对 agent harness 为什么重要
论文里还有一个值得注意的方向:self-evolving agent harness。
今天的 agent harness 已经不只是“调用几个工具”。它可能包含:
```text
工具系统
权限系统
记忆系统
执行沙箱
子 agent 编排
持久化状态
用户接口
自动化任务
```
更进一步,未来 agent 可能会在运行中修改自己的 harness:生成新工具,替换组件,调整权限,增加子 agent,改变执行环境。
这时,动态组合就不再只是插件市场里的工程便利,而是系统可靠性的前提。
如果没有时间可组合性,agent 的一次错误自我修改可能留下污染,甚至破坏恢复路径。
如果没有空间可组合性,新组件依赖谁、谁依赖它、替换后影响哪些模块,都只能靠临时代码判断。
Cordis 论文的意义在这里变大了:它不仅解释 Koishi 这样的插件生态,也提供了一种面向可恢复、可协调、可持续演化 agent harness 的运行时模型。
## 边界:不是所有副作用都能撤回
这篇论文并不是说所有事情都能被 Cordis 神奇撤销。
有些副作用发生在系统边界内,例如:
```text
注册事件监听
添加服务绑定
创建定时器
保存上下文内部状态
打开由系统掌控的资源句柄
```
这些可以通过 inverse 清理。
但有些副作用跨出了系统边界:
```text
发送网络请求
写入外部数据库
给用户发出消息
调用支付接口
让外部系统执行动作
```
这些通常不能被严格撤回。最多只能做 compensation,也就是补偿操作,比如删除刚创建的记录、退款、发送一条更正消息。
补偿不是数学意义上的 inverse。它只能把外部世界恢复到某种业务上可接受的等价状态。
所以 Cordis 的保证主要覆盖 Context 管得住的部分。外部世界的副作用仍然需要业务设计处理。
## 边界:运行时不证明 inverse 一定正确
还有一个重要限制:Cordis 可以要求 effect 带上 inverse,也可以负责记录和执行 inverse,但它不能自动证明 inverse 一定正确。
如果插件作者写错了清理函数,运行时无法凭空知道。
Cordis 的价值在于把副作用创建和副作用清理放到同一个结构里,并由框架统一追踪。它减少遗漏,改善局部性,但不是形式化验证器。
这点很重要。论文提供的是一种运行时组合范式,而不是“只要用了 Cordis 就永远不会泄漏”的保证。
## 总结
这篇论文给 Cordis 提供了一个比“插件框架”更深的解释。
Cordis 解决的不是单个 API 怎么设计,而是动态组件系统的两个根问题:
```text
时间上:组件退出后,它留下的副作用能不能撤回?
空间上:组件之间的依赖能不能随环境变化自动协调?
```
对应到论文的语言:
```text
Temporal composability → Revertible Effects
Spatial composability → Reactive Coeffects
Unified Context → 把两者放进同一个运行时环境
Component / Fiber → 让组件生命周期围绕依赖和副作用运行
```
对应到 Cordis 的工程语言:
```text
Lifecycle 清理背后是可撤回 effect
Service / Inject 背后是响应式 coeffect
Context 是两者统一的运行时载体
Plugin / Component 是动态组合单元
```
一句话概括:
> Cordis 的核心不是“把插件加载进来”,而是让插件在运行时既能有秩序地协作,也能有秩序地退出。论文把这种能力概括为时空可组合性。