一句话结论
这篇论文想解决一件事:让组件像乐高一样在运行时随插随拔,而且拔走后世界自动恢复原状、别的组件自动感知------并用效应/余效应理论给了这套能力一个形式化基础,还做成了能用的框架 Cordis(Koishi 的底座)。
为什么和你有关
你熟悉的三个场景,全在论文射程内:
- React:useEffect 的 cleanup、fiber、reconciliation------论文术语和你每天写的东西同源;
- Vite / webpack HMR:改代码不重启页面------论文把 HMR 做成了组件级通用机制;
- Agent harness:论文第 1.2.2 节和结论直接点名"自演化智能体驾驭框架"------AI 在不停机的前提下持续生成、替换自己运行时的组件,这正是你 local-model-harness 那一类系统往前走会撞上的问题。
1. 要解决的问题:动态组合
静态组合 :import、函数调用、类继承------编译时就定死,跑起来不变。
动态组合:组件在运行时随时加载、卸载、换配置。插件系统和自演化 agent 框架都需要。
现状的难处,论文用 VSCode 举例,非常具体:
- 时间问题 :VSCode 扩展装在共享的"扩展宿主"进程里,装容易,卸不掉------activate 跑过之后想禁用就得重启整个宿主,所有扩展陪葬。deactivate 钩子只是关机回调,不是"活体摘除"。Top 100 扩展里 87 个带代码,全都躲不掉重启。
- 空间问题 :VSCode 有 extensionDependencies,但 Top 100 里只有 7 个用了。扩展之间没有类型化的依赖契约,
exports是 any,互相依赖基本靠野路子。
现实中的变通方案是粗粒度重启:模块有问题?重启进程。服务依赖?交给 K8s。代价:每次重启丢掉全部进程内状态(缓存、连接、算一半的东西),还要冗余副本兜底。
论文的核心主张:重启是因为缺少组件级的两个能力,而不是因为组件级做不到。
2. 两个维度:时间和空间
| 维度 | 一句话 | 要求 |
|---|---|---|
| 时间可组合性 | 组件被拔掉后,它对世界做过的所有修改都能完全撤销 | 每一次资源分配、事件注册、状态修改都要被跟踪,卸载时有序回收 |
| 空间可组合性 | 组件声明"我需要什么",系统负责解析、供给、撤回 | 维护依赖拓扑,依赖变化时协调相关组件的生命周期 |
静态场景下这两个维度退化为你认识的东西:时间 ≈ 词法作用域 / RAII / useEffect cleanup;空间 ≈ import 解析 / DI。动态场景难在:效应长期存活没有作用域边界,依赖随运行时出现、消失、换身份。
3. 术语翻译机
| 论文术语 | 你已经认识的东西 |
|---|---|
| 效应(effect) | 程序对外部世界的改动:改全局状态、写文件、发请求 |
| 余效应(coeffect) | 反方向:程序向世界提的要求------要什么资源、权限、服务 |
| 可逆效应 | useEffect(() => { 副作用; return 撤销函数 }),但撤销不是开发者自觉,而是运行时强制跟踪、自动组合 |
| 响应式余效应 | 声明式依赖 + 依赖变化驱动 mount/unmount,像响应式版的 DI 容器 |
| 效应上下文 ∂Γ | (当前状态, 撤销清单)------撤销清单是所有逆操作的复合,一执行就回滚到初始 |
| 纤维 fiber | 组件 = 图纸,fiber = 建起来运转的那间房(带生命周期状态的实例) |
| 目标视图 + 静止 | 每个 fiber 有个"它该处于的状态",全员达标 = 静止。就是 K8s/React 的声明式对齐(reconciliation)思路 |
| 观测等价 | 不要求物理状态原样复原(free 恢复不了原堆布局),只要求任何观察者分不出差别 |
| 上下文类型 Γ^∞ | 状态 + 撤销清单 + 依赖表揉成一个一等公民对象 ctx,可以递归嵌套(父上下文管子上下文) |
4. 两个核心机制(直觉版)
4.1 可逆效应:每个副作用必须交出撤销函数
关键设计:效应函数返回的不只是新状态,还带一个逆操作 (类型直觉:ctx → (新ctx, 撤销函数))。运行时把每个逆操作按"扭曲组合"累进撤销清单------正向按顺序复合,逆向按反序复合(先装的后拆)。
对应关系一目了然:
kotlin
React useEffect: 装 = effect body,拆 = return 的 cleanup,框架按 LIFO 调
Cordis: 装 = effect 执行,拆 = 返回的逆操作,运行时自动累进清单
差别在两点:
- React 的 cleanup 靠开发者手写且只管组件卸载;Cordis 的逆操作是结构性保证------你不交出撤销函数,这个效应就不成立;
- 论文证了独立性定理:如果各组件的效应互不干扰,撤销不必严格 LIFO,任意顺序拆都行。这才允许多个组件的效应交织在一个系统里、从运行中的系统单独摘掉某一个。
4.2 响应式余效应:依赖声明 + 变化通知
组件先声明"我需要哪些键"(余效应规范)。上下文每次变化,系统对照规范把变化分类成三种通知:
- 激活:依赖从"没齐"变"齐了" → 执行组件效应(开始跟踪)
- 停用:依赖从"齐"变"没齐" → 跑撤销清单
- 中性:没影响
不变量:依赖齐了才激活,缺了就停用------不是"先用着,缺了再报错"。
再加两个进阶机制:
- 隔离(isolate):同一个依赖键在不同上下文绑定不同值------多租户、测试环境、沙箱;
- 拦截(intercept):不改依赖的值,给依赖访问附加横切行为------AOP 的味道。
4.3 合在一起:上下文范式
一个 ctx 同时携带状态、撤销清单、依赖表,所有操作都经过它中介。论文把它定位在两个极端之间:
- 纯函数式的显式状态传递(State 单子):可追踪但调用链每个函数都要手传状态,累;
- 命令式的隐式修改(React useEffect 偷偷注册、Spring getBean 全局乱取):好用但不可追踪。
上下文范式 = 函数式的可追踪性 + 命令式的易用性:效应和余效应都通过显式 ctx 参数中介,每个操作都能追到"哪个组件、在哪个上下文、干的"。
5. 从组件到系统:演算与元理论
组件 = 三元组(我需要什么 d,我提供什么 p,激活时干的事 + 撤销 e)。系统 = 一堆 fiber 组成的注册表 + 一个编排器。五条基础规则 + 扩展规则处理四种现实:撤回顺序 (消费者拆自己时还得用正在被撤的依赖,所以供给必须等消费者拆完才撤)、多步激活 、异步 (Future:迭代在状态 A 起飞、在状态 B 着陆)、失败(装到一半失败,已执行的效应也必须恢复,不能悬空)。
元理论五条,每条翻译成人话:
| 定理 | 人话 |
|---|---|
| 保持性 | 无论走哪条规则,系统结构不会被搞坏 |
| 恢复精确性 | 组件拆掉后,世界(观测等价意义下)回到它来之前的样子 |
| 排序 | 依赖解析一旦确定就不变;提供者一定比消费者活得久;绑定值保持稳定 |
| 进展 | 无死锁,且一定会到达静止态(给了步数上界) |
| 合流性 | 加载顺序不影响终态------同样的编排意图,怎么交错执行,最后收敛到同一个状态 |
合流性是声明式加载器的理论靠山:因为终态只取决于最终配置,加载器才敢做增量协调而不是推倒重建,编排者也才不需要安排加载顺序。
6. 落地:Cordis 与 Koishi
Cordis 是 TypeScript 实现的元框架(框架的框架),三层:
arduino
核心库 ctx.effect / ctx.use / ctx.set / ctx.get / ctx.isolate / ctx.intercept
↓
加载器 声明式配置(条目:id/url/config/disabled)+ 增量协调 + HMR
↓
应用框架 Koishi(聊天机器人)等------领域词汇由应用框架提供
API 直觉版:
scss
// 效应:所有对 ctx 的改动都走 ctx.effect,自动跟踪、自动回收
ctx.effect(() => {
const server = listen(port)
return () => server.close() // 交出逆操作,卸载时自动执行
})
// 组件:声明依赖 inject,激活时 apply
ctx.use(MyPlugin, config)
// MyPlugin.inject = ['database', 'platform'] ← 余效应规范
// 依赖齐了才激活;database 被换掉时,只有真正用它的插件被重新激活
Koishi 是什么
Koishi(koishi.chat)是一个跨平台聊天机器人框架,也是 Cordis 之上最大的生产系统:
- 干什么的:写一个机器人,同时接入 QQ、Telegram、Discord 等聊天平台,支持多账号、跨平台数据互通;自带可视化控制台和在线插件市场,零基础也能几分钟拉起一个 bot
- 名字来源 :东方 Project 角色古明地恋(Komeiji Koishi)------一个会做出无意识举动的角色,隐喻聊天机器人。作者 Shigma(施一帆)就是 Koishi 的创造者,这篇论文相当于把他维护 Koishi 多年的架构经验形式化
- 技术栈:完全 TypeScript,类型支持顶级,核心功能有单测覆盖;插件保存即热重载------官方原话"如同前端开发一样丝滑顺畅"(这就是论文里 HMR 的现实出处)
- 生态:四年迭代,数千个官方 + 社区插件(论文写 4000+,官网写 3000+,口径略有差异),从平台适配、数据库到具体业务功能全覆盖
理解这一点对读论文很关键:Cordis 的理论不是先验设计,而是从一个跑了好几年、承载数千插件的生产系统里提炼出来的,Koishi 在论文里扮演"实验证据"的角色。
Koishi 是生产级案例:4 年、4000+ 社区插件。对比 VSCode 的三个事实:
- VSCode 卸扩展要重启宿主;Koishi 从控制台禁用插件,效应就地撤回,其他插件无感;
- VSCode 扩展间依赖几乎不存在;Koishi 生态是真正的依赖拓扑------IM 适配器供平台、数据库驱动供存储、功能插件按需声明消费;
- HMR:文件一保存,变更的插件就地替换,缓存和别处的活动连接都保住。同一套模型还跑在 Koishi 的 Web 控制台(浏览器端)------服务端原语换成浏览器原语,组合语义不变。
7. 值得记住的讨论点
- 循环依赖不是死锁:在这套模型里,依赖成环的组件永远处于"未激活"------看依赖声明就能静态预知,不像并发死锁要等它发生。实践中大多数环可以拆细粒度组件解开。
- 系统边界:逆操作能撤到什么程度取决于边界。边界内 = 系统独占可恢复;边界外(比如已经发出去的网络消息)分获取/发送两阶段处理。余效应可以把边界往外推------把外部资源收编成受控通道。
- 访问控制免费获得:组件只能访问自己声明过的依赖(代理中介),这本身就是一种 capability-based 沙箱基础。
- 键冲突/接口漂移:靠命名空间、peer dependencies、结构兼容性三件套,和 npm 生态同一套思路。
8. 和你最相关的部分:自演化 Agent Harness
论文 1.2.2 节描绘的场景,就是 agent 工程的下一站:harness 一边对外服务,一边让 AI 持续生成并替换自己的组件,几乎无人监督。每次自修改都是一次动态组合。没有这两个能力会怎样:
- 没有时间可组合性 → 每次自修改都要重启,进程内累积的会话、缓存、任务状态全丢;高频修改下不可用时间累积惊人,任务反复被打断;最糟的是一次坏修改可能把"用来恢复的进程本身"也搞坏。
- 没有空间可组合性 → 每个模块自己土法检测依赖变化;粗暴的代码替换会悄悄弄坏依赖方,或引入重载时才暴露的循环依赖。
对照你现在做的 harness 工作:工具/扩展的热插拔、会话状态在组件替换中的存续、坏扩展的安全摘除------这些问题这篇论文给了正式的名字和一套有定理支撑的解法。即使不直接用 Cordis,"每个效应必须交出逆操作 + 依赖声明驱动生命周期"这两条设计原则可以直接借进任何 harness 设计。 论文结论也把自演化 harness 列为未来验证方向------这个领域还没有统治者。
9. 局限与提醒
- 数学保证建立在"逆操作写得对"之上------撤销函数本身有 bug,理论救不了你;
- 观测等价是理想化判据,工程里"恢复干净"仍需要测试兜底;
- Cordis/Koishi 生态主要在聊天机器人领域,直接搬到你的 harness 需要自己评估适配成本;
- 论文对 agent 场景只给了动机论证,没有实际实现------这既是局限,也是机会。
元信息
- Cordis 仓库:github.com/cordiverse/...
- 论文仓库:github.com/cordiverse/...