DeepSeek新论文讲了什么:插件和Agent如何不停机更换组件

设想这样一个场景。
一个长期运行的 AI Agent 正在处理任务。此时,系统发现检索组件效果不好,于是生成了一个新版本,准备在不中断当前会话的情况下替换旧组件。
代码加载成功并不代表替换完成。旧组件注册过事件监听,打开过连接,写入过共享状态;其他组件还依赖它提供的能力。直接覆盖代码,可能留下幽灵监听和陈旧引用。重启整个进程虽然干净,却会丢掉会话、缓存和正在执行的任务。
这正是论文 A Programming Paradigm for Spatiotemporal Composability 想解决的问题。
论文草稿发布于 2026 年 8 月 13 日,作者 Yifan Shi、Wei Zhang 和 Tianyi Cui 来自北京大学与 DeepSeek-AI。仓库将其标注为仍在持续修订的预印本,具体定义与结论应以最新版本为准。它没有提出新的大模型,也不是一篇 Agent Benchmark,而是在尝试建立一种新的编程范式:让软件组件在运行时加入、退出和重新组合时,系统仍然能够完整回滚副作用,并自动协调依赖关系。
论文把这两个目标称为:
- 时间可组合性(temporal composability):组件退出后,它对共享环境造成的修改能够被完整撤销。
- 空间可组合性(spatial composability):组件可以声明依赖,系统能随着依赖的出现、消失和替换自动调整组件生命周期。
这两个词看起来抽象,背后讨论的却是插件系统、热更新和自进化 Agent 都绕不开的工程问题。
动态加载不是动态组合
传统软件的组合大多在编译期确定。函数调用、模块导入和类继承一旦构建完成,运行期间不会频繁改变。
插件系统把组件加载推迟到了运行时,但"能加载"只解决了一半问题。真正困难的是卸载。
论文以 VS Code 扩展为例。作者统计了安装量最高的 100 个扩展,其中 87 个包含可执行代码。这些扩展一旦执行 activate,停用或卸载时仍然需要重启整个 Extension Host。deactivate 更接近进程关闭前的善后回调,并不能让某一个扩展在宿主持续运行时被完整移除。
依赖管理也存在类似问题。前 100 个扩展中,只有 7 个声明了对非内置扩展的依赖。扩展之间可以通过 exports 交换能力,但返回值缺少可靠的结构契约,宿主也不会根据依赖变化自动安排它们的启停。
因此,下面三件事完全不同:
text
加载一段新代码
让新代码开始工作
让旧代码像从未出现过一样退出
多数热更新方案主要处理前两件事。这篇论文关心第三件事,以及旧组件退出时依赖它的组件应该怎么办。
重启进程为什么只是粗粒度解法
操作系统其实已经提供了某种时间可组合性:结束一个进程,操作系统会回收它的内存、文件描述符和其他资源。容器编排器也提供了某种空间可组合性:服务消失后,流量和依赖可以被重新调度。
问题在于粒度不匹配。
如果只有一个插件需要更新,重启整个进程会同时丢掉无关组件的缓存、连接和中间计算。为了维持可用性,系统还要准备额外副本。把每个插件拆成独立服务则会引入部署、通信和序列化成本,本来可以是一次本地函数调用的交互被迫跨越网络。
论文的判断是:副作用和依赖关系应该在组件所在的粒度上被管理,而不是一遇到问题就退回进程或容器边界。
时间维度:副作用必须带着撤销方法
论文先处理组件退出后的清理问题。
普通副作用可以理解成一次上下文变换:
text
Γ → Γ
组件注册一个事件、写入一个服务、创建一项资源,都会让运行环境从状态 Γ 变成新状态。
论文把它改写成:
text
Γ → Γ × (Γ → Γ)
一次操作不仅返回修改后的上下文,还要返回一个逆操作。运行时不需要事后猜测怎样清理,而是在创建副作用时就获得清理方法。
用接近 TypeScript 的形式表达,大致是这样:
ts
ctx.effect(() => {
const listener = registerListener(handleMessage);
return () => {
unregisterListener(listener);
};
});
重点不是多写一个回调,而是所有会改变上下文的操作都经过同一个 ctx.effect。运行时收集这些逆操作,并以与创建相反的顺序执行。
如果组件依次完成:
text
创建数据库连接 → 注册消息处理器 → 启动定时任务
退出时就应该:
text
停止定时任务 → 注销消息处理器 → 关闭数据库连接
这和调用栈展开、RAII 或 try/finally 的直觉相近,区别在于组件的生命周期可能持续数小时,甚至跨越多轮异步任务。它没有一个固定的词法作用域,清理信息必须由运行时长期保存。
可逆不等于随时可以乱撤销
论文在这里比常见的 dispose 模式多走了一步。
假设组件 A 和组件 B 都修改了共享环境:
text
初始状态 → A 的修改 → B 的修改
如果先卸载 A,它的逆操作不能顺手抹掉 B 后来写入的状态。否则每个组件单独看都能回滚,放在一起却无法组合。
这要求不同组件的效果具有某种独立性。论文没有把独立性简单定义为"修改不同字段",而是引入观察等价:只要组件通过自己声明的依赖观察环境时,两个执行顺序产生的结果不可区分,就可以把它们视为等价。
这个设计把副作用和依赖联系了起来。系统只有知道一个组件能观察哪些环境内容,才有可能判断其他效果是否真正影响了它。
时间可组合性因此不是"每个操作都有 undo"这么简单。它还要求:
- 逆操作确实能恢复相应修改。
- 多个逆操作按正确顺序组合。
- 撤销一个组件时,不破坏仍然存活的组件。
空间维度:组件不再主动寻找依赖
第二个问题是依赖变化。
传统依赖注入通常发生在应用启动阶段。容器找到数据库、日志或缓存实现,把它们传给消费者,之后默认这些依赖一直存在。
动态系统里,提供者可能在运行中出现、退出或被另一个实例替换。让每个消费者自己监听这些变化,会迅速产生大量临时状态和边界条件。
论文借用了 coeffect 概念。effect 描述"程序对环境做了什么",coeffect 描述"程序需要环境提供什么"。本文将它译作"余效",也可以把它理解成组件的环境需求声明。
一个组件不直接寻找数据库实例,而是声明:
text
inject = { database, logger }
运行时每次发现上下文变化,都会重新检查这组需求,并把结果分成三类:
- 激活:原来缺少依赖,现在全部满足,组件可以加载。
- 停用:原来依赖完整,现在某个提供者消失,组件必须退出。
- 中性变化:上下文发生变化,但组件需要的依赖没有改变。
这是一种组件级响应式系统。Vue 或 SolidJS 的响应式机制会在一个值改变时重新计算相关表达式;Cordis 则在服务依赖改变时重新安排相关组件的生命周期。
提供者退出时,为什么要先停掉消费者
动态依赖最容易出错的时刻,是提供者退出。
假设报表组件依赖数据库组件。数据库准备卸载时,如果先关闭连接,再通知报表组件退出,报表组件的清理逻辑可能还要使用数据库保存进度,此时依赖已经不存在。
论文给出的顺序是:
text
数据库组件进入 UNLOADING
↓
它立即停止"提供"数据库能力
↓
依赖数据库的组件收到变化并开始退出
↓
等待所有消费者完成清理
↓
数据库组件执行自己的逆操作
也就是说,提供者在真正释放资源以前,先从依赖图中宣布离场。消费者仍能在自己的清理阶段读取已经提交的旧依赖,直到退出完成。之后提供者才关闭资源。
这个顺序解决的是动态组件系统中的经典悬空引用问题:服务虽然正在退出,但不能在消费者完成善后前提前消失。
异步加载期间依赖又变了怎么办
真实组件很少能同步完成加载。它可能要建立连接、读取配置或等待外部服务。加载进行到一半时,依赖可能已经被替换。
Cordis 使用一种"惯性"状态机处理这个问题:一次加载或卸载开始后,当前转换先运行到可安全停下的位置,再根据最新目标决定下一步。
论文中的生命周期可以简化为:
text
INACTIVE
↓ 依赖满足
LOADING
↓ 加载完成且目标未变化
ACTIVE
↓ 依赖失效
UNLOADING
↓ 清理完成
INACTIVE
如果 LOADING 过程中依赖发生变化,系统不会让两个生命周期转换并发修改同一个组件。它会停止后续迭代,保留已经积累的逆操作,完成回滚,再按照最新依赖重新加载。
这比简单的"取消 Promise"更可靠。Promise 被取消不代表前面已经执行的资源分配自动消失,真正需要处理的是部分完成后的回滚。
Context为什么是整篇论文的中心
论文最终把 effect 的运行环境与 coeffect 的依赖环境合并成同一种 Context。
在这个模型里,Context 同时承担四项职责:
| 职责 | 解决的问题 |
|---|---|
| 记录效果 | 组件做过哪些可撤销修改 |
| 累积逆操作 | 组件退出时如何恢复 |
| 保存能力 | 当前环境提供哪些依赖 |
| 通知依赖变化 | 哪些组件应该激活或停用 |
这也是论文称其为"编程范式"而不仅是插件框架的原因。组件不再直接面对一个全局可变世界,而是在自己的 Context 中读取被声明的能力,通过 Context 执行可追踪的修改。
Context 还支持两种重要操作:
- **隔离(isolation)**改变一个依赖键解析到哪个 realm。例如两个插件都请求
database,但可以被定向到不同数据库实例。 - **拦截(interception)**不改变依赖指向谁,而是改变怎样调用它。例如为某个社区插件提供只读数据库能力,或者为调用增加日志与权限检查。
隔离解决"拿到哪个服务",拦截解决"怎样使用服务"。两者都能在运行时调整。
Cordis如何把理论变成代码
论文实现了 TypeScript 元框架 Cordis。它不提供路由、ORM 或聊天机器人等具体能力,只负责动态组合语义。
实现分成三层:
- 核心库提供效果跟踪、依赖解析和组件生命周期。
- 组件加载器把声明式配置转换成组件实例,并进行增量协调。
- 应用框架在上面定义自己的领域能力,Koishi 就是其中的生产案例。
核心 API 可以压缩成几个动作:
| API | 作用 |
|---|---|
ctx.effect(callback) |
执行修改并记录逆操作 |
ctx.set(key, value) |
向上下文提供一项能力 |
ctx.get(key) |
读取已声明并解析完成的能力 |
ctx.use(component, config) |
在当前上下文实例化组件 |
ctx.isolate(key, realm) |
改变某项依赖的解析空间 |
ctx.intercept(key, metadata) |
调整依赖的访问方式 |
这里最关键的工程约束是:所有上下文修改都必须经过 ctx.effect。
如果组件绕过 Context,直接修改全局变量、原生对象或外部系统,运行时就看不到这次效果,也无法保证卸载时恢复。论文的理论保证只覆盖系统边界以内、能够被独占修改并恢复的位置。
热更新不再依赖组件作者声明接受边界
Cordis 的组件加载器还实现了热模块替换。
一般 HMR 方案需要开发者标记哪些模块接受热更新。Cordis 的做法不同:组件本身已经圈定了效果和依赖,因此替换组件可以复用同一套卸载与重新加载机制。
论文把更新分成三步:
- 根据模块导入关系,把变更模块分成可热替换和需要完整重启的集合。
- 找出依赖树经过变更模块的组件实例。
- 备份模块缓存,卸载旧组件并导入新组件;任何一步失败,就恢复缓存并重新实例化旧版本。
第三步很重要。热更新不是简单重新执行一次 import,而是一个事务:要么相关组件全部切换到新版本,要么恢复旧版本,系统不能停在一半新、一半旧的状态。
4000个插件证明了什么,又没有证明什么
论文用 Koishi 作为案例。Koishi 是建立在 Cordis 上的聊天机器人框架,经过四年发展,已经形成超过 4000 个社区插件的生态。服务端插件会组合消息平台适配器、数据库驱动和业务功能;Web 控制台本身又是另一个运行在浏览器里的 Cordis 应用。
这个案例至少说明两件事:
- 这套抽象足以支撑一个真实、长期运行的插件生态,而不只是形式化模型。
- 同一套组合机制可以用于服务端和浏览器,说明它不依赖聊天机器人这一特定领域。
但论文也主动限制了结论。
首先,Koishi 当前使用的是 Cordis v3,论文描述的是重新细化效果、余效和加载器语义的 v4。两者共享核心模型,但不能把生产使用规模直接当成 v4 全部机制已经得到同等验证。
其次,案例来自单一语言、单一生态,属于存在性和采用情况证明,而不是与其他架构的对照实验。论文没有给出热更新耗时、运行时开销、故障率或开发效率等量化结果。
所以,"4000 个插件"证明这种方向能够落地,却没有证明它在所有指标上优于容器、进程隔离或其他插件架构。
这篇论文和自进化Agent到底有什么关系
论文在开头把 self-evolving agent harnesses 作为核心动机之一,这也是它受到关注的重要原因。
未来的 Agent Harness 可能动态增加工具、修改记忆模块、重写权限策略,甚至生成并替换自己的组件。每一次自我修改都可以看成动态组合:
text
生成新组件
↓
检查它需要哪些能力
↓
加载并记录全部效果
↓
依赖变化时协调上下游
↓
出现错误时完整撤销
如果没有时间可组合性,Agent 每改一次自身都可能需要重启,当前任务和内存状态随之丢失。若没有空间可组合性,新组件可能拿不到正确依赖,或者在替换服务时悄悄破坏其他模块。
Cordis 提供的模型确实适合作为这类系统的基础设施:组件声明自己需要什么,所有修改带着逆操作,运行时负责依赖协调和失败回滚。
不过必须强调:论文尚未在自进化 Agent 上完成验证。
作者在结论中把它列为未来研究方向。当前生产案例是 Koishi 插件生态,不是一个持续改写自身 Harness 的 Agent。把这篇论文直接说成"DeepSeek 已经实现可自我进化的 Agent"并不准确。
三个仍然没有解决的问题
论文最后的讨论部分比结论更值得看,因为它清楚交代了理论边界。
1. 逆操作的正确性仍由作者负责
ctx.effect 能确保逆操作被记录、按顺序执行并且最多执行一次,却不能证明开发者提供的逆操作真的恢复了原状态。
如果创建文件后返回的清理函数删错路径,运行时仍会忠实执行这个错误操作。形式化系统依赖一个前提:组件作者提供的 inverse 是有效见证。
2. 外部世界并不总能回滚
发送过的邮件、已经扣除的款项、被第三方消费的消息,都不能像内存字段一样简单撤销。
论文用"系统边界"限定保证范围。只有系统能独占修改并恢复的位置,才属于可逆 Context。边界外的动作需要幂等、补偿事务或外部协议配合,不能因为包了一层 ctx.effect 就自动获得可逆性。
3. 不可信代码仍然需要沙箱
Context 可以控制组件通过声明依赖访问哪些能力,却挡不住恶意组件绕过 API,直接接触宿主运行时对象。
论文明确指出,不可信组件需要语言运行时、独立进程、软件故障隔离、WebAssembly 或容器提供真正的执行边界。依赖注入和访问拦截可以缩小能力,但不能替代沙箱。
此外,依赖键的版本和类型兼容仍是开放问题。不同包可能使用同名 key 表示完全不同的接口;提供者升级后,消费者也可能继续拿到名字相同但行为不兼容的对象。Cordis 当前借助 peer dependencies 缓解问题,还没有一个完全语言无关的统一方案。
如何读这篇88页的论文
这篇论文形式化内容很多,从效果与余效的范畴论背景,一直推到保存性、进展性、合流性和时空可组合性定理。工程读者不必从第一页顺序啃完。
更高效的阅读路线是:
text
第 1 节:先理解两个可组合性维度
↓
第 3.1、3.2 节:理解可逆效果与反应式余效
↓
第 5.1 节:把公式映射到 ctx.effect / ctx.set / ctx.use
↓
第 5.2 节:看配置协调与事务式 HMR
↓
第 5.3、6 节:核对生产证据和适用边界
↓
需要验证理论时,再回看第 4 节证明
阅读英文原稿时,我把 PDF 放进 PDFTranslator 做成双语对照,主要用来保持公式、算法编号、表格和章节引用的位置关系。涉及 effect、coeffect、inverse、fiber 和 realm 的段落仍保留英文术语核对,避免中文译名掩盖它们在形式化定义中的差异。
最后
这篇论文最重要的贡献,不是又造了一个热更新工具,而是把动态组件系统中两个长期依靠工程经验处理的问题放进了同一套模型:
text
组件做过的事情,必须能够按顺序撤销。
组件需要的东西,必须能够随环境变化重新解析。
可逆效果负责时间,反应式余效负责空间,统一 Context 把二者连接起来,组件生命周期再把局部保证扩展到整个依赖图。
对于普通业务系统,进程和容器仍然是更简单、更可靠的隔离边界。对于需要大量插件、长期保持进程状态、频繁热替换模块,尤其是未来可能持续修改自身的 Agent Harness,这套范式提出了一个值得认真研究的方向。
它距离"自进化软件基础设施"还有验证和工具链上的空白,但至少把问题说清楚了:真正困难的从来不是把新代码放进去,而是让旧代码干净地离开,并让周围组件知道世界已经变了。
参考资料
- Yifan Shi, Wei Zhang, Tianyi Cui, A Programming Paradigm for Spatiotemporal Composability, draft of August 13, 2026。