这篇论文的核心是在回答一个很实际的问题:

一个正在运行的软件系统,能不能安全地动态加载、卸载、替换组件,并且自动处理组件之间不断变化的依赖关系?

论文提出了一套统一的理论和框架,称为“时空可组合性”(spatiotemporal composability),并实现为 TypeScript 元框架 Cordis。论文全文 88 页,作者来自北京大学和 DeepSeek-AI。paper.pdf

1. 它要解决什么问题?

传统软件组合主要是静态的:

  • 编译时导入模块;
  • 启动时加载插件;
  • 运行期间基本不改变结构。

但现代系统越来越需要动态组合:

  • 插件实时启停;
  • 热模块替换;
  • 在线切换数据库或服务实现;
  • AI Agent 自己修改工具和执行环境;
  • 多组件系统在不中断服务的情况下升级。

难点有两个方向。

时间维度:卸载时能否彻底撤销?

组件加载后可能做了很多事情:

  • 注册事件监听器;
  • 添加路由;
  • 修改配置;
  • 创建定时器;
  • 分配资源;
  • 注册服务;
  • 启动子组件。

如果只是调用一个 unload()deactivate(),开发者很容易漏掉某些清理逻辑。

论文把这个问题称为“时间可组合性”:

组件被移除后,它对共享环境造成的影响应当被完整撤销。

空间维度:依赖变化时能否自动响应?

例如:

  • A 提供数据库;
  • B 依赖数据库;
  • C 依赖 B;
  • 数据库实现被替换或暂时消失。

系统需要自动决定:

  • 谁可以激活;
  • 谁必须停用;
  • 依赖恢复后谁重新加载;
  • 如何避免读取不存在的服务;
  • 如何处理依赖顺序和循环依赖。

论文把这个问题称为“空间可组合性”:

组件能够声明依赖,运行时能够发现、解析,并对依赖变化做出反应。

2. 最重要的两个概念

论文把经典编程语言理论中的 effects 和 coeffects,从“静态类型分析”提升成了运行时机制。

可以用一句简单的话理解:

概念它描述什么
Effect组件对环境做了什么
Coeffect组件需要环境提供什么

2.1 可逆 effect:每个副作用都带一个逆操作

论文把普通副作用抽象成:

effect : Context -> (NewContext, Inverse)

也就是说,一个操作不仅修改上下文,还要返回一个可以撤销它的函数。

例如:

注册事件监听器
=> 返回“注销该监听器”的逆操作

添加服务
=> 返回“删除该服务”的逆操作

创建资源
=> 返回“释放该资源”的逆操作

运行时会把所有逆操作累积起来。组件卸载时,系统自动按正确顺序执行这些逆操作。

关键点在于:清理逻辑不再是一个与初始化分离的、容易遗漏的 deactivate() 函数,而是和每个副作用绑定在一起。

论文还讨论了多个 effect 交错执行时的独立性。如果两个组件的操作满足一定的交换条件,那么即使它们的加载、卸载顺序不同,最终恢复结果仍然一致。

2.2 响应式 coeffect:依赖满足状态变化会触发生命周期

组件可以声明依赖集合:

B requires { database, logger }

运行时根据上下文判断:

  • 依赖从不满足变成满足:activating
  • 依赖从满足变成不满足:deactivating
  • 依赖状态没有改变:neutral

于是组件生命周期可以自动驱动:

依赖出现 -> 激活组件
依赖消失 -> 停用组件
依赖恢复 -> 重新激活

这比普通依赖注入更动态。传统 DI 通常只在初始化时注入一次;这里依赖绑定本身是持续反应式的。

3. 两者如何统一?

论文提出一个统一的 Context:

Context = 当前状态
        + 用于撤销 effect 的累加器
        + 依赖/服务表

所有组件与环境的交互都通过这个 Context 完成。

这样做有几个好处:

  1. 组件的副作用都能被追踪;
  2. 依赖注册本身也是一种可逆 effect;
  3. 依赖变化可以触发生命周期变化;
  4. 组件可以嵌套,形成树状控制结构;
  5. 加载、卸载、替换都能在统一模型中描述。

论文还引入了“观察等价”(observational equivalence):

不要求物理内存或内部表示完全恢复,只要求外部可观察行为恢复。

这很重要。例如:

  • mallocfree 不会让堆布局逐字节回到原样;
  • 删除一个生成的名字后,下一次生成的名字可能不同;
  • 但从组件可以观察到的行为来看,它们可以被认为等价。

4. 组件模型和形式化证明

论文将组件建模为:

Component = 依赖声明
          + 提供声明
          + 带逆操作的 effect

组件的实例称为 fiber,每个 fiber 有生命周期状态,例如:

Inactive
Reloading
Active
Unloading
Failed

运行时维护:

  • 组件的父子关系;
  • 当前提供的服务;
  • 组件激活时看到的依赖版本;
  • 已积累的逆操作;
  • 组件是否正在退出或重新加载。

论文形式化证明了几个性质:

Preservation

运行时步骤不会破坏系统状态的类型和结构约束。

Temporal composability

组件卸载后,它的贡献可以被撤销;在满足相应条件时,其他组件的状态不会被破坏。

Spatial composability

依赖只有在满足时才会激活;提供者撤销后,依赖者会被正确停用或重新解析。

Progress

在无循环依赖、操作能够终止等条件下,系统最终可以到达稳定状态。

Confluence

在组件 effect 相互独立、组件完整安装其声明服务且没有失败时,不同的生命周期调度顺序可以收敛到等价的最终状态。

这相当于说:

最终系统状态主要由目标配置和依赖关系决定,而不应依赖调度器恰好采用了哪一种顺序。

5. Cordis 实现了什么?

论文不是纯理论工作,还实现了 Cordis。

主要功能包括:

Core Library

提供:

  • effect tracking;
  • effect inverse;
  • ctx.set() / ctx.get()
  • 组件注册;
  • 组件生命周期管理;
  • Proxy 形式的上下文访问。

Declarative Component Loader

可以用声明式配置描述系统:

组件 URL
父子关系
依赖隔离
拦截策略
组件配置
是否禁用

配置变化后,loader 会做增量协调,而不是重启整个系统。

例如:

  • 修改组件 ID 或 URL:重建;
  • 修改 isolate:重新分配作用域;
  • 修改 intercept:原地更新;
  • 修改 config:交给组件做增量更新;
  • 修改 disabled:卸载或重新加载。

Hot Module Replacement

HMR 的流程大致是:

  1. 找出受影响的模块;
  2. 判断哪些模块可以热替换;
  3. 失效相关模块缓存;
  4. 撤销旧组件的 effect;
  5. 加载新组件;
  6. 如果加载失败,恢复旧模块和旧组件。

这比很多传统 HMR 机制更“整体化”,因为组件的所有副作用都由生命周期统一管理。

Koishi 案例

论文以 Koishi 作为生产案例:

  • 超过 4000 个社区插件;
  • 插件来自不同作者;
  • 涵盖聊天平台适配器、数据库、管理后台等;
  • 支持插件动态停用、替换和依赖重新解析。

论文声称,Koishi 证明了这套模型至少能够支撑一个真实的大型插件生态。

6. 这篇论文真正有价值的地方

我认为它最有价值的不是某个具体 API,而是提出了一种清晰的统一视角:

动态组件系统的核心,不只是“加载代码”,而是管理组件对环境的全部贡献,以及它对环境的全部需求。

传统插件系统往往把这些问题拆散处理:

  • 初始化函数负责加载;
  • 卸载函数负责清理;
  • DI 容器负责注入;
  • 配置系统负责重载;
  • HMR 系统负责替换;
  • 容器编排系统负责服务依赖。

这篇论文试图把它们统一为:

组件 = 可撤销的环境变更 + 可响应的环境依赖

这个抽象对于以下场景尤其有启发:

  • 插件化 IDE;
  • Agent 工具运行时;
  • 多租户服务;
  • 在线升级;
  • 规则引擎;
  • 游戏 Mod 系统;
  • 浏览器扩展;
  • 微内核式应用;
  • 自演化软件。

7. 它的局限和需要谨慎看待的地方

7.1 “可撤销”不是任何副作用都能实现

论文明确承认系统边界问题。

对于以下操作,通常不能真正恢复原状:

  • 向外部网络发送消息;
  • 给用户发邮件;
  • 写入其他进程共享的文件;
  • 已经完成的支付;
  • 已被外部观察到的输出;
  • 不受本系统独占控制的共享状态。

这类操作只能:

  • 延迟提交;
  • 记录并补偿;
  • 采用业务层面的反向操作。

所以它保证的不是“宇宙级回滚”,而是:

在系统能够独占控制、并且能够提供逆操作的边界内,实现可靠撤销。

7.2 逆操作仍然需要开发者提供

论文将多个原子 effect 的逆自动组合起来,但每个原子操作仍然需要提供逆。

例如:

注册监听器 -> 开发者必须提供注销监听器
创建文件 -> 必须知道如何删除或恢复

它减少了遗漏和顺序错误,却没有消除“逆操作如何定义”的根本困难。

7.3 形式化保证依赖较强条件

全局 confluence 等性质需要假设:

  • effect 之间满足独立性;
  • 依赖图无环;
  • 组件完整安装其声明的服务;
  • 没有失败或失败处理满足特定条件;
  • 生命周期操作可以最终完成。

现实系统中,这些条件并不总是成立。

7.4 依赖类型和版本问题还没有彻底解决

当前模型主要依赖 key:

"database" -> 某个值

这会产生:

  • key 冲突;
  • 接口漂移;
  • 不同版本提供相同 key;
  • 编译时类型与运行时对象不一致。

论文讨论了命名空间、peer dependency、结构兼容性等方案,但明确把统一解决方案留作开放问题。

7.5 实验验证还不够量化

Koishi 案例说明它“能用”,但还没有充分回答:

  • 运行时开销是多少?
  • effect tracking 增加多少内存?
  • 依赖重新解析的复杂度如何增长?
  • 与 OSGi、普通 DI、现有 HMR 的性能差多少?
  • 是否真的提高了开发者生产力?
  • 在大量并发、频繁热替换下是否稳定?

论文自己也承认,案例是单一生态、单一语言、观察性证据,不是严格的对照实验。

7.6 安全性不能只靠 Context

依赖声明和拦截机制可以实现一定程度的能力控制,但如果组件代码在同一个 JavaScript 进程里运行,它仍可能直接访问宿主对象。

真正运行不可信代码,仍然需要:

  • WebAssembly;
  • 独立进程;
  • 沙箱;
  • 软件故障隔离;
  • 容器或虚拟机。

8. 对 AI Agent 的意义

论文特别提到“自演化 Agent harness”。

这套模型可以用于:

Agent 当前运行中
    ↓
加载新工具或新策略
    ↓
新组件声明依赖
    ↓
依赖满足后激活
    ↓
发现错误或版本不兼容
    ↓
卸载组件并撤销其副作用
    ↓
继续保留原有会话、缓存和其他工具

它解决的是传统 Agent 自修改中的两个危险:

  1. 修改失败只能整体重启;
  2. 修改了一个模块,却不知道哪些依赖者已经失效。

不过,真正的 Agent 自修改还需要额外解决:

  • 非确定性;
  • 长期状态迁移;
  • 外部副作用;
  • 权限升级;
  • 恶意或错误生成代码;
  • 并发中的安全更新;
  • 数据库 schema 迁移。

因此,这篇论文提供的是一个很有潜力的底层组合机制,而不是完整的“安全自演化 Agent”方案。

总体评价

我的判断是:

这是一篇偏编程语言理论和运行时架构的论文,核心贡献是把“副作用撤销”和“动态依赖响应”统一成一个可形式化、可实现的组件模型。

它最强的地方:

  • 问题定义清楚;
  • effects 和 coeffects 的结合有理论动机;
  • 对卸载顺序、依赖顺序、异步、失败和收敛进行了较完整的形式化;
  • 有 Cordis 和 Koishi 作为实际实现支撑。

它最弱的地方:

  • 很多保证依赖理想化假设;
  • 对不可逆外部副作用的处理仍需业务补偿;
  • 缺少系统化性能和生产力评估;
  • 依赖版本、兼容性和安全边界仍未完全解决。

如果用一句话概括:

论文把“插件加载/卸载”从工程惯例提升成了一个有状态、可撤销、依赖响应式的运行时组合理论;它很适合作为动态插件系统和 Agent 运行时的设计基础,但距离普适、低成本、完全安全的动态软件更新平台,还有一段工程距离。