2026-08-15Tao

软件组件如何安全地即插即拔:读懂“时空可组合性”

这篇 88 页预印本把动态软件组合拆成时间与空间两个问题,并用可回滚副作用、响应式依赖和统一上下文解释组件如何在运行中安全装载、卸载与重连。

目录共 13 节
  1. 一个插件为什么很难拆干净
  2. 时间与空间,是两种不同的难题
  3. 可回滚副作用:每次操作都带一张回收凭证
  4. 响应式环境依赖:服务变了,组件跟着调整
  5. Context 是整套机制的总账本
  6. 四个生命周期状态,把异步与失败纳入模型
  7. 论文证明了哪些性质
  8. 这些证明带着哪些前提
  9. Cordis 如何把理论放进代码
  10. Koishi 提供了现实样本,也暴露了证据边界
  11. 安全边界也要单独看
  12. 为什么自进化 Agent 会需要这套思路
  13. 可拔出,才让动态组合闭合

原始论文《A Programming Paradigm for Spatiotemporal Composability》

2026 年 8 月 13 日预印本草稿 · 88 页 · 作者:Yifan Shi、Wei Zhang、Tianyi Cui

作者单位:北京大学、DeepSeek-AI · 实现:Cordis · 案例:Koishi

论文仓库明确提示,这份预印本仍在持续修订,具体内容可能发生较大变化。

软件里,装上一个组件通常很容易,完整拿掉它却很难。

一个插件启动后,可能注册事件监听器、定时器和路由,打开端口,建立数据库连接,还会向其他插件暴露服务。卸载时只要漏掉一项,系统就会带着残留状态继续运行。最省事的处理方式是重启整个进程,让操作系统统一回收资源。

这个办法会连同缓存、连接和正在进行的任务一起清掉。一个插件的问题,最后由整个进程承担代价。

《A Programming Paradigm for Spatiotemporal Composability》试图把这件事从工程惯例变成一套可以描述、实现和证明的编程范式。论文提出两个问题:组件离开时,能否完整撤回自己造成的变化;组件依赖的服务出现、消失或更换时,系统能否自动重新协调。

作者把前一个问题称为时间可组合性,把后一个问题称为空间可组合性。两个方向合在一起,构成论文所说的“时空可组合性”。

一个插件为什么很难拆干净

论文用 Visual Studio Code 扩展系统说明现有做法的局限。

按照论文的分析,VS Code 的扩展运行在共享的 extension host 进程里。扩展可以动态安装,单个扩展执行过 activate 后,却无法在同一个进程里完整卸载代码和副作用。禁用或卸载扩展需要重启扩展宿主,deactivate 更接近进程关闭前的善后回调。

论文还统计了 2026 年 6 月 9 日安装量最高的 100 个扩展,其中只有 7 个通过 extensionDependencies 声明了对非内置扩展的依赖。这个数字来自作者的市场调查,适合用来理解问题形态,不能当成跨平台的独立基准。

插件系统常把功能限制在宿主预先设计好的命令、视图和语言服务接口上。插件之间需要协作时,依赖关系容易散落在运行时代码里,类型和生命周期约束也很弱。

进程和容器提供了一种粗粒度答案。进程退出后,操作系统会回收内存、文件描述符和端口;容器编排器可以按照服务依赖重新启动实例。它们处理的是进程和服务,无法直接表达同一地址空间里几十个组件之间的资源归属与依赖关系。

论文要解决的,是更细的一层:只拔掉出问题的组件,其余组件继续运行。

时间与空间,是两种不同的难题

时间可组合性关注组件留下了什么。

组件加载时修改共享环境,卸载时要把这些修改撤回。事件监听器需要取消,定时器需要停止,端口需要关闭,子组件需要退休,注册表里的条目也要移除。顺序还不能错。后创建的资源常常依赖先创建的资源,所以回收通常要按相反顺序进行。

空间可组合性关注组件需要什么。

一个业务插件可能依赖数据库服务和聊天平台适配器。数据库尚未出现时,插件不应先启动再等待报错;数据库被替换时,插件要停止使用旧实例,完成清理,再连接新实例;数据库准备退出时,还要等使用它的插件先结束。

静态程序在编译时解析模块导入,函数作用域也会限定资源寿命。动态系统里的组件随时到来和离开,依赖图与资源寿命都在运行时变化。原有的静态边界已经不够用。

可回滚副作用:每次操作都带一张回收凭证

论文给 effect 增加了一个运行时约束:一次副作用不能只返回新状态,还要同时返回能撤回这次变化的函数。

它的形状可以简化为:

当前上下文 → 新上下文 + 回收函数

注册监听器时,回收函数负责移除监听器;打开服务器时,回收函数负责关闭服务器;覆盖配置时,回收函数要记住旧值,并在卸载时恢复。

这里的关键细节是,回收函数可以根据操作发生时的状态生成。覆盖配置之前是什么值,只有执行到那一步才知道。每张回收凭证因此记录了当时真正需要恢复的内容。

运行时把这些函数逐个收集起来。组件执行 A、B、C 三个操作,卸载时就按照 C、B、A 的顺序回收。这个后进先出的次序会自然保留资源之间的嵌套关系。

组件作者只需为原子操作给出回收方法,组合操作的回收顺序由运行时自动得到。

论文把这种结构称为 revertible effects,也就是可回滚副作用。它比单独写一个 deactivate 更容易核对,因为创建资源和回收资源发生在同一处,运行时也知道每项副作用属于哪个组件。

响应式环境依赖:服务变了,组件跟着调整

另一半来自 coeffect。effect 描述程序对环境做了什么,coeffect 描述程序需要环境提供什么。

论文把依赖存进一个带类型的键值上下文。组件先声明自己需要哪些键,例如 databaserouterlogger。运行时检查这些键是否都由活动组件提供,并把每次上下文变化分成三类:

  1. 原来缺少依赖,现在齐全,组件进入激活流程。
  2. 原来依赖齐全,现在有一项消失,组件进入停用流程。
  3. 满足状态没有变化,当前组件保持不动。

依赖关系因此从“出错时才发现”变成组件生命周期的输入。

数据库组件退出时,运行时先把它标记为不再向新组件提供服务。已经连接它的业务组件仍保留原来的 committed view,也就是激活时确认的依赖视图。业务组件可以在清理阶段继续使用旧数据库连接,把事务、连接或缓存妥善交还。等所有依赖者完成停用,数据库组件才真正执行自己的回收函数。

提供者先停止对外可见,依赖者先完成清理,提供者最后释放资源。

这条顺序解决了动态依赖里很容易被忽略的问题:依赖消失之后,依赖者的善后代码往往仍需要它。

论文还给出两种扩展。Isolation 让同一个依赖键在不同上下文里解析到不同实例,可用于多租户、测试和局部隔离。Interception 会在依赖访问时合并元数据,可用来传递路径限制、只读权限或其他调用策略。

Context 是整套机制的总账本

论文最终把 effect 与 coeffect 放进同一个递归 context。

每个组件都有自己的子 context。组件通过 context 注册资源、读取依赖、创建子组件。父 context 收集子组件产生的副作用,子组件离开时只回收属于自己的部分。组件层层嵌套后,会形成一棵可以逐层装载和卸载的树。

这很像一套分户总账。每个组件产生的资源有明确归属,每项依赖有明确提供者,每次变更都经过同一个入口。运行时才能回答三个问题:谁改了状态,谁依赖这项服务,谁应该先退出。

论文对“恢复原状”的定义也更贴近真实机器。内存经过申请和释放后,堆的字节布局未必与之前完全相同;一个临时名字被丢弃后,下次生成的名字也可能变化。论文采用可观察等价:只要 context 暴露的操作无法区分两个状态,就把它们视为等价。

恢复目标因此是外部可观察行为回到同一状态,无需追求物理表示逐字节复原。

四个生命周期状态,把异步与失败纳入模型

真实组件的加载和卸载都需要时间。数据库连接可能等待网络,模块导入可能失败,加载到一半时依赖也可能已经更换。

论文先从 Inactive 和 Active 两个状态出发,再加入 Reloading 与 Unloading,形成四态生命周期:

  • Inactive:组件没有运行,也没有对外提供依赖。
  • Reloading:组件正在逐步安装副作用。
  • Active:组件已经完成加载,依赖视图已经提交。
  • Unloading:组件停止对外提供服务,正在等待依赖者退出并回收资源。

一次加载可以被拆成多个迭代步骤。每一步完成后,运行时都会检查目标依赖是否仍然与开始时一致。依赖中途变化,已经完成的步骤会被回滚。

异步操作已经发出去时,运行时允许它先返回,再立刻进入卸载。这样可以避免组件在半加载状态下对外提供服务。某一步失败时,系统会回收此前积累的副作用,把错误记录在这个 fiber 上,其他兄弟组件仍然运行。

论文证明了哪些性质

88 页篇幅里,真正重的部分是一套动态组合演算。组件被表示为三部分:需要的依赖、可能提供的依赖、带回收函数的效果。组件的一次运行实例称为 fiber,每个 fiber 保存自己的生命周期、依赖视图和回收函数。

作者在这套模型上证明了几类性质。

1. 状态约束会被持续保留

任何合法步骤执行后,注册表仍保持良好结构。父子关系有效,同一个依赖键不会出现两个未隔离的提供者,活动依赖者引用的提供者也仍处于可用生命周期内。

2. 一个组件退出后,只撤回自己的贡献

当不同组件的操作满足独立性条件时,一个 fiber 的回收函数可以穿过其他组件后来产生的变化,删掉自己的部分,同时保留其他组件的结果。

3. 依赖者的生命周期被包含在提供者之内

依赖者只会在提供者已经活动时开始加载,并会在提供者真正回收资源之前完成卸载。一次加载期间使用的依赖解析也保持一致,避免同一次过渡同时读到旧提供者和新提供者。

4. 系统最终能够安静下来

依赖先后关系无环、组件数量有限、每次加载步骤有上界时,等待依赖者退出的守卫不会永久卡住。所有生命周期步骤最终到达 quiescent state,也就是没有组件还需要继续转换的稳定状态。

5. 最终状态与中间路径无关

在更严格的条件下,无论独立组件的加载和卸载怎样交错,只要最终配置相同,稳定后的系统状态就与按照依赖顺序从头装载一次相同。论文把它称为 confluence。

这项结论可以用一句工程语言概括:系统记得最终要有哪些组件,不必永远背着中间折腾过的历史。

这些证明带着哪些前提

形式证明没有给任意 JavaScript 程序自动套上一层安全外壳。它依赖几项明确纪律。

所有共享变化都要经过 context。组件绕过 context 修改全局变量、直接调用未封装 API,运行时就看不见这些副作用,也无法回收。

回收函数必须正确。Cordis 能收集并调用回收函数,当前实现不会证明它真的恢复了对应操作。这个责任仍由基础 API 或组件作者承担。

不同组件的效果需要满足独立性。简单地说,一个组件的操作和回收不应改变另一个组件的操作结果。不同依赖键通常天然独立;有顺序意义的中间件链、共享计数器或地址分配器则需要额外设计。

依赖先后关系需要无环。A 等 B,B 又等 A,两者会一直停在未激活状态。论文建议把双向交互拆成更小的核心组件和连接组件,代价是组件数量与配置复杂度上升。

最终状态的合流结论还排除了失败 fiber,并要求组件完整提供自己声明的键。外部世界里的发射行为也不在普通回滚范围内。关闭文件句柄可以撤回一次资源占用,已经写入共享文件的字节、发出的网络消息或完成的扣款通常无法直接抹掉。它们需要延迟提交或业务补偿。

Cordis 如何把理论放进代码

论文给出的实现叫 Cordis。它不绑定 Web、数据库或聊天机器人等具体领域,只提供动态组件组合的底层语义。

ctx.effect(callback) 驱动一个效果回调,并收集每一步返回的回收函数。ctx.setctx.get 提供和读取依赖,ctx.use 创建子组件。Isolation 与 interception 分别调整依赖解析范围和访问策略。

组件加载器把声明式配置转换成 fiber。配置变化后,它只重建受影响的条目。热模块替换会先备份模块缓存,卸载旧 fiber,再导入新模块;任一导入失败,缓存和旧组件会被恢复,避免系统停在只换了一半的状态。

公开的 Cordis 仓库把自己描述为“时空可组合性的元框架”,同时明确提醒项目仍在积极开发,API 尚不稳定。论文里的 Cordis v4 因此更接近正在形成的参考实现,还不是已经冻结的工业标准。

Koishi 提供了现实样本,也暴露了证据边界

论文选择开源聊天机器人框架 Koishi 做案例。服务器里的聊天平台适配器、数据库驱动、管理控制台和业务功能都以插件存在;浏览器端控制台本身又是另一套 Cordis 应用。

论文写作时把这个生态概括为经过四年发展、拥有超过 4000 个社区插件。Koishi 官方插件市场近期显示 3952 个可用于 v4 的插件,两个数字的时间与统计口径略有差异。它们共同说明,这套模式已经承载了规模较大的开放插件生态。

Koishi 可以从控制台停用插件,也支持开发时热重载。插件通过 context 注册的命令、监听器和服务会在退出时被回收,依赖它们的插件也会随提供者变化重新协调。

论文脚注保留了一个关键限定:当前 Koishi 使用 Cordis v3,论文展示的是重新设计后的 Cordis v4。 Koishi 的长期运行经验支持两代共享的核心组合思路,无法单独验证 v4 的每条新语义和定理对应。

作者也把案例研究定义为存在性与采用度证据。样本只有一个生态、一种宿主语言,观察结果没有与其他架构做受控对照。运行时开销、恢复延迟和开发者生产力仍缺少定量测量。

安全边界也要单独看

依赖声明可以限制组件通过 context proxy 访问哪些服务,interception 还可以附加只读路径、数据库权限等策略。这提供了一种能力式访问控制。

恶意组件仍可能直接接触宿主运行时,绕过 context。论文明确把不可信代码的隔离交给外部沙箱,例如独立进程、单独语言运行时、WebAssembly 或容器。Cordis 管理组件组合与资源归属,本身不构成恶意代码沙箱。

跨组件接口的版本兼容也没有完全解决。当前模型主要按键名连接提供者和使用者。独立开发的插件可能遇到同名键冲突,或接口已经变化、键名却没变。Cordis 当前借助 npm peer dependency 缓解版本问题,结构兼容和行为契约仍是开放课题。

为什么自进化 Agent 会需要这套思路

论文把 self-evolving agent harness 作为重要动机。

Agent harness 同时管理工具、权限、沙箱、记忆、上下文、子 Agent 与持久状态。未来的 Agent 如果能修改自己的工具和运行模块,每次改动都会触发动态组合问题。直接重启会中断任务并丢掉进程内状态;直接替换代码又可能留下旧副作用,或悄悄破坏依赖它的模块。

时空可组合性给出一套可能的底座:旧组件能撤回自己的影响,新组件只有在依赖齐全后才启动,依赖者会在提供者离开前完成清理,失败更新还能回到稳定状态。

这部分目前仍是未来验证方向。论文没有报告 Cordis 已经运行在持续自我修改的 Agent harness 中。它提出了适用理由,也留下了最关键的实验。

可拔出,才让动态组合闭合

这篇论文把“可组合”从能拆成模块、能装上插件,推进到三个更严格的结果:组件造成的变化可以撤回,依赖变化可以重新协调,最终状态可以摆脱中间路径。

它的价值既来自统一视角,也来自对边界的诚实处理。context 看不见的副作用不会自动消失,外部发射无法靠普通 inverse 撤回,错误回收函数仍会出错,恶意代码仍需要沙箱,定量收益也尚未建立。

Cordis 展示了一条可运行的实现路径,Koishi 说明核心思路能够支撑真实插件生态。接下来需要验证的,是 Cordis v4 本身的性能和工程成本,以及这套范式能否在更复杂的宿主语言、跨进程系统和自进化 Agent 中保持同样的保证。

软件组件长期被比作积木。论文补上了积木比喻里经常缺失的一半:装上去以后,还要能够安全拿下来。

发布自
atlasnote-editorial
发布日期
2026-08-15
标签
programming-languagessoftware-architecturepluginscordis