<?xml version="1.0" encoding="utf-8" standalone="yes"?><rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom"><channel><title>编程语言 · AI Times</title><link>https://blog.havenice.day/tag/%E7%BC%96%E7%A8%8B%E8%AF%AD%E8%A8%80/</link><description>AI 时代，中年人倔强的记录</description><language>zh-CN</language><lastBuildDate>Fri, 11 Sep 2026 10:38:05 +0800</lastBuildDate><atom:link href="https://blog.havenice.day/tag/%E7%BC%96%E7%A8%8B%E8%AF%AD%E8%A8%80/index.xml" rel="self" type="application/rss+xml"/><item><title>92 页论文，讲给普通开发者听：DeepSeek Harness 的插件内核为什么是对的</title><link>https://blog.havenice.day/2026/09/11/92-ye-lun-wen-jiang-gei-pu-tong-kai-fa-zhe-ting/</link><pubDate>Fri, 11 Sep 2026 10:38:05 +0800</pubDate><guid>https://blog.havenice.day/2026/09/11/92-ye-lun-wen-jiang-gei-pu-tong-kai-fa-zhe-ting/</guid><description>&lt;blockquote&gt;&#10;&lt;p&gt;精读对象：&lt;strong&gt;arXiv:2608.25512&lt;/strong&gt;《A Programming Paradigm for Spatiotemporal Composability》&lt;/p&gt;&#10;&lt;p&gt;作者：&lt;strong&gt;Yifan Shi&lt;/strong&gt;（北京大学 / DeepSeek-AI）、&lt;strong&gt;Wei Zhang&lt;/strong&gt;（北京大学）、&lt;strong&gt;Tianyi Cui&lt;/strong&gt;（DeepSeek-AI）&lt;/p&gt;&#10;&lt;p&gt;提交：2026-08-26 · 92 页 · 1 图 2 表 · cs.PL + cs.SE&lt;/p&gt;&#10;&lt;p&gt;前情提要：上一篇《DeepSeek Harness 精读》讲了 Cordis &lt;strong&gt;怎么用&lt;/strong&gt;，这一篇讲它&lt;strong&gt;为什么是对的&lt;/strong&gt;。&lt;/p&gt;&#10;&lt;/blockquote&gt;&#10;&lt;h2 id="0-一句话速览"&gt;0. 一句话速览&lt;/h2&gt;&#10;&lt;p&gt;&lt;strong&gt;这不是一篇 AI 论文，是一篇编程语言（PL）论文。&lt;/strong&gt; 它给 DeepSeek Harness 的插件内核 &lt;strong&gt;Cordis v4&lt;/strong&gt; 补上了完整的数学地基。&lt;/p&gt;&#10;&lt;p&gt;作者名单里的 &lt;strong&gt;Yifan Shi&lt;/strong&gt;，就是 Koishi / Cordis 的作者 &lt;strong&gt;Shigma&lt;/strong&gt; —— 现在在 DeepSeek。论文自己也交代了：Koishi 跑在 Cordis v3 上，论文呈现的是 v4，核心组合模型两版共享。&lt;/p&gt;&#10;&lt;p&gt;所以这不是「又一篇 Agent 论文」。它是一次&lt;strong&gt;工程实践的回头总结&lt;/strong&gt;：一个从 2019 年一路长起来的框架、被 4000+ 社区插件压测了四年，终于有人把「它为什么 work」讲清楚了。&lt;/p&gt;&#10;&lt;p&gt;三个你在 dsh 里天天见到的东西，论文都给了正式答案：&lt;/p&gt;&#10;&lt;table&gt;&#10;&lt;thead&gt;&#10;&lt;tr&gt;&#10;&lt;th&gt;你在 dsh 里看到的&lt;/th&gt;&#10;&lt;th&gt;论文里的名字&lt;/th&gt;&#10;&lt;th&gt;它证明了什么&lt;/th&gt;&#10;&lt;/tr&gt;&#10;&lt;/thead&gt;&#10;&lt;tbody&gt;&#10;&lt;tr&gt;&#10;&lt;td&gt;&lt;code&gt;ctx.effect()&lt;/code&gt; 返回 disposer，卸载自动撤销&lt;/td&gt;&#10;&lt;td&gt;可回滚效应&lt;br/&gt;Revertible Effects&lt;/td&gt;&#10;&lt;td&gt;卸载一个组件时，它撤回的&lt;strong&gt;只有它自己的&lt;/strong&gt;贡献&lt;/td&gt;&#10;&lt;/tr&gt;&#10;&lt;tr&gt;&#10;&lt;td&gt;&lt;code&gt;inject&lt;/code&gt; 声明依赖，服务就绪才启动&lt;/td&gt;&#10;&lt;td&gt;响应式余效应&lt;br/&gt;Reactive Coeffects&lt;/td&gt;&#10;&lt;td&gt;provider 撤回绑定&lt;strong&gt;之前&lt;/strong&gt;，所有依赖方一定已经停用&lt;/td&gt;&#10;&lt;/tr&gt;&#10;&lt;tr&gt;&#10;&lt;td&gt;&lt;code&gt;reload&lt;/code&gt; 不重启进程&lt;/td&gt;&#10;&lt;td&gt;合流定理&lt;br/&gt;Confluence&lt;/td&gt;&#10;&lt;td&gt;中途怎么折腾都无所谓，&lt;strong&gt;最终状态只取决于最终配置&lt;/strong&gt;&lt;/td&gt;&#10;&lt;/tr&gt;&#10;&lt;/tbody&gt;&#10;&lt;/table&gt;&#10;&lt;hr&gt;&#10;&lt;h2 id="1-问题动态组合为什么一直没有理论"&gt;1. 问题：动态组合为什么一直没有理论&lt;/h2&gt;&#10;&lt;p&gt;论文标题里那个拗口的词 —— &lt;strong&gt;时空可组合性（spatiotemporal composability）&lt;/strong&gt; —— 其实是把「运行时装卸组件」这件事拆成了两个正交的维度。&lt;/p&gt;&#10;&lt;p&gt;动态组合的两个正交维度&#10;时间可组合性关注卸载时能否完整撤销副作用，空间可组合性关注组件间依赖的声明与解析&lt;/p&gt;&#10;&lt;p&gt;动态组合&lt;/p&gt;&#10;&lt;p&gt;时间可组合性&#10;卸载时完整撤销副作用&lt;/p&gt;&#10;&lt;p&gt;空间可组合性&#10;依赖的声明与响应式解析&lt;/p&gt;&#10;&lt;p&gt;VSCode 前 100 扩展 · 87 个含代码&lt;/p&gt;&#10;&lt;p&gt;VSCode 前 100 扩展 · 仅 7 个声明依赖&lt;/p&gt;&#10;&lt;p&gt;&lt;strong&gt;时间可组合性&lt;/strong&gt;管的是：组件被卸载时，它对共享环境做的修改必须被完整、安全地撤销。每一次资源分配、事件注册、状态变更都要能被追踪和回收。&lt;/p&gt;&#10;&lt;p&gt;&lt;strong&gt;空间可组合性&lt;/strong&gt;管的是：组件必须能声明、发现、解析彼此的依赖，并在依赖出现或消失时协调各自的生命周期。&lt;/p&gt;&#10;&lt;p&gt;静态场景下这两件事都有现成答案：时间维度退化成词法作用域（RAII、bracket 模式），空间维度退化成模块导入解析。&lt;strong&gt;但一旦组件能在运行时来去，两边同时崩掉&lt;/strong&gt; —— 没有哪个词法作用域能框住一个部署后才加载的插件，也没有哪个编译期上下文能预测运行时配置里冒出来的依赖。&lt;/p&gt;&#10;&lt;h3 id="论文拿-vscode-开刀数据很扎心"&gt;论文拿 VSCode 开刀，数据很扎心&lt;/h3&gt;&#10;&lt;p&gt;插件系统是动态组合最典型的场景。论文挑了 VSCode 做样本，因为它最普及、也最经典：&lt;/p&gt;&#10;&lt;table&gt;&#10;&lt;thead&gt;&#10;&lt;tr&gt;&#10;&lt;th&gt;维度&lt;/th&gt;&#10;&lt;th&gt;VSCode 的现状&lt;/th&gt;&#10;&lt;/tr&gt;&#10;&lt;/thead&gt;&#10;&lt;tbody&gt;&#10;&lt;tr&gt;&#10;&lt;td&gt;&lt;strong&gt;时间&lt;/strong&gt;&lt;/td&gt;&#10;&lt;td&gt;所有扩展跑在同一个 extension host 进程里。&lt;strong&gt;安装量前 100 的扩展里 87 个含可执行代码&lt;/strong&gt;，禁用或卸载任何一个，都得重启整个 host，影响所有已加载扩展。&lt;/td&gt;&#10;&lt;/tr&gt;&#10;&lt;tr&gt;&#10;&lt;td&gt;&lt;strong&gt;空间&lt;/strong&gt;&lt;/td&gt;&#10;&lt;td&gt;虽然提供了 &lt;code&gt;extensionDependencies&lt;/code&gt;，但&lt;strong&gt;前 100 里只有 7 个&lt;/strong&gt;声明了非内置扩展依赖。&lt;code&gt;getExtension(...).exports&lt;/code&gt; 返回 &lt;code&gt;any&lt;/code&gt;，完全没有类型契约。&lt;/td&gt;&#10;&lt;/tr&gt;&#10;&lt;/tbody&gt;&#10;&lt;/table&gt;&#10;&lt;p&gt;论文对 &lt;code&gt;deactivate&lt;/code&gt; 钩子的批评特别值得记一笔：它不只是「不彻底」，而是&lt;strong&gt;把效果的销毁（&lt;code&gt;deactivate&lt;/code&gt;）和效果的创建（&lt;code&gt;activate&lt;/code&gt;）拆到了两个地方&lt;/strong&gt;，违反了关注的局部性 —— 于是「清理完整吗」这个问题，变成了一件无法验证的事。&lt;/p&gt;&#10;&lt;p&gt;顺带，论文也点了现有变通方案的本质：&lt;strong&gt;操作系统在「进程」粒度给了时间可组合性，容器编排器在「服务」粒度给了空间可组合性。&lt;/strong&gt; 代价是每次重启丢掉全部进程内状态（缓存、连接、部分计算），重建要几秒到几分钟；容器编排还&lt;strong&gt;无法表达共享地址空间内组件之间的依赖&lt;/strong&gt;，给本可以是本地函数调用的交互硬塞进一层网络开销。&lt;/p&gt;&#10;&lt;p&gt;论文给它起了个名字：&lt;strong&gt;粒度错配（granularity mismatch）&lt;/strong&gt;。现代系统越来越在更细的粒度上组合，而现有机制只在进程和容器的边界上工作。&lt;/p&gt;&#10;&lt;p&gt;对 AI agent 来说这事更致命。论文把「自进化 harness」列为头号动机，并指出最危险的一条：&lt;strong&gt;没有时间可组合性，一次错误的自我修改可能让恢复所需的那个进程本身瘫痪。&lt;/strong&gt;&lt;/p&gt;&#10;&lt;hr&gt;&#10;&lt;h2 id="2-时间维度让每个函数自带逆"&gt;2. 时间维度：让每个函数自带「逆」&lt;/h2&gt;&#10;&lt;p&gt;经典 effect system 和 coeffect system 都是&lt;strong&gt;静态工具&lt;/strong&gt;：效应在词法固定的作用域内被追踪、由编译期的 handler 处理；余效应注解针对执行前就确定的上下文验证。&lt;/p&gt;&#10;&lt;p&gt;论文的关键判断是：&lt;strong&gt;与其给静态类型系统继续加注解，不如把效应和余效应的概念结构「具体化」（reify），让运行时能直接操作它们。&lt;/strong&gt;&lt;/p&gt;&#10;&lt;p&gt;落到时间维度上，成果简单到有点意外：&lt;strong&gt;一个效应不是「一个函数」，而是「一个函数 + 它自己的逆」。&lt;/strong&gt; 类型写成这样：&lt;/p&gt;&#10;&lt;pre&gt;&lt;code&gt;e : Γ → Γ × (Γ → Γ)&#10; ↑ ↑&#10; 新状态 撤销这次变换的逆&#10;&lt;/code&gt;&lt;/pre&gt;&#10;&lt;p&gt;运行时把每一步返回的逆累积保存起来（论文叫它&lt;strong&gt;累加器&lt;/strong&gt;）。于是：&lt;/p&gt;&#10;&lt;p&gt;&lt;strong&gt;「卸载一个组件」就退化成「把累积的逆跑一遍」。&lt;/strong&gt;&lt;/p&gt;&#10;&lt;p&gt;可回滚效应：每个效应携带自己的逆&#10;组件加载时依次执行效应，每个效应返回新上下文和它自己的逆；逆按应用顺序累积进累加器，卸载时按 LIFO 逆序执行即可回到初始状态&lt;/p&gt;&#10;&lt;p&gt;状态 0&lt;/p&gt;&#10;&lt;p&gt;状态 1&lt;/p&gt;&#10;&lt;p&gt;状态 2&lt;/p&gt;&#10;&lt;p&gt;状态 3&lt;/p&gt;&#10;&lt;p&gt;效应 1&#10;效应 2&#10;效应 3&lt;/p&gt;&#10;&lt;p&gt;累加器 = 逆 1 ∘ 逆 2 ∘ 逆 3　（每个效应自带逆，按应用顺序累积）&lt;/p&gt;&#10;&lt;p&gt;卸载 = 把累加器跑一遍，逆序执行自动回到初始状态&lt;/p&gt;&#10;&lt;p&gt;最漂亮的一点是：&lt;strong&gt;LIFO 顺序是白送的&lt;/strong&gt;。逆按应用顺序累积、按相反顺序执行，正好每一次撤销都拿到「当初自己造成的那一个状态」。论文用一条定理把这个说死了（每步撤销都精确回到它自己的起点，且中间每个状态都满足可靠性不变量），不需要额外规则。&lt;/p&gt;&#10;&lt;p&gt;论文还点出一个我很喜欢的类比：&lt;strong&gt;效应迭代器本质上就是一个「具体化的定界续体」，也就是主流语言用 &lt;code&gt;yield&lt;/code&gt; 暴露的那个结构&lt;/strong&gt;。所以这套模型不是要发明什么新语言特性 —— 它直接落在语言已经给的 generator 上。&lt;/p&gt;&#10;&lt;h3 id="翻译成-cordis-的写法就是你在-dsh-里天天见的东西"&gt;翻译成 Cordis 的写法，就是你在 dsh 里天天见的东西&lt;/h3&gt;&#10;&lt;pre&gt;&lt;code&gt;ctx.effect(() =&amp;gt; {&#10; const onSignal = () =&amp;gt; shutdown()&#10; process.on('SIGINT', onSignal) // ← 正向：做一件事&#10; // 这个返回值就是论文里的「逆」&#10; return () =&amp;gt; process.off('SIGINT', onSignal)&#10;})&#10;&lt;/code&gt;&lt;/pre&gt;&#10;&lt;p&gt;你返回的那个 &lt;code&gt;() =&amp;gt; ...&lt;/code&gt;，在论文里是有严格类型的对象：&lt;strong&gt;运行时持有它，卸载时按 LIFO 跑它&lt;/strong&gt;。所以你不需要写 uninstall 路径，也不需要记得清理 —— 这不是「方便」，是&lt;strong&gt;有证明的结构性保证&lt;/strong&gt;。&lt;/p&gt;&#10;&lt;hr&gt;&#10;&lt;h2 id="3-空间维度依赖操作本身就是效应"&gt;3. 空间维度：依赖操作本身就是效应&lt;/h2&gt;&#10;&lt;p&gt;依赖这边，论文先用一张部分函数表来建模，也就是 &lt;code&gt;key → 值&lt;/code&gt; 的映射，叫&lt;strong&gt;余效应上下文&lt;/strong&gt;。用类型族保证每个 key 关联到具体的值类型，访问依赖时类型是安全的。&lt;/p&gt;&#10;&lt;p&gt;然后是全篇最「啊哈」的一步。注册一个依赖的操作签名长这样：&lt;/p&gt;&#10;&lt;pre&gt;&lt;code&gt;set(k, v) : Σ ⇀ Σ × (Σ ⇀ Σ)&#10; ↑ ↑&#10; 新状态 撤销这次注册的逆&#10;&lt;/code&gt;&lt;/pre&gt;&#10;&lt;p&gt;&lt;strong&gt;这个类型恰好就是第 2 节那个可回滚效应函数的类型。&lt;/strong&gt;&lt;/p&gt;&#10;&lt;p&gt;也就是说：&lt;strong&gt;注册一个依赖，本身就是一次可回滚效应&lt;/strong&gt;，它返回的逆就是「把 k 从表里删掉」。论文的结论是 —— 依赖注册的追踪和恢复，&lt;strong&gt;完全不需要新机制，直接从时间那一半白嫖过来&lt;/strong&gt;。原文把这称为两种机制之间的 synergy：&lt;em&gt;余效应操作就是效应，而效应是可回滚的。&lt;/em&gt;&lt;/p&gt;&#10;&lt;p&gt;在这之上才是「响应式」。每次上下文发生变化，都会被对照组件声明的依赖清单（specification）分类成三种之一：&lt;/p&gt;&#10;&lt;table&gt;&#10;&lt;thead&gt;&#10;&lt;tr&gt;&#10;&lt;th&gt;分类&lt;/th&gt;&#10;&lt;th&gt;含义&lt;/th&gt;&#10;&lt;th&gt;后果&lt;/th&gt;&#10;&lt;/tr&gt;&#10;&lt;/thead&gt;&#10;&lt;tbody&gt;&#10;&lt;tr&gt;&#10;&lt;td&gt;&lt;strong&gt;activating&lt;/strong&gt;&lt;/td&gt;&#10;&lt;td&gt;依赖从「不满足」变成「满足」&lt;/td&gt;&#10;&lt;td&gt;执行组件的效应（自动被追踪）&lt;/td&gt;&#10;&lt;/tr&gt;&#10;&lt;tr&gt;&#10;&lt;td&gt;&lt;strong&gt;deactivating&lt;/strong&gt;&lt;/td&gt;&#10;&lt;td&gt;依赖从「满足」变成「不满足」&lt;/td&gt;&#10;&lt;td&gt;跑累加器回滚（自动撤销）&lt;/td&gt;&#10;&lt;/tr&gt;&#10;&lt;tr&gt;&#10;&lt;td&gt;&lt;strong&gt;neutral&lt;/strong&gt;&lt;/td&gt;&#10;&lt;td&gt;满足性没变&lt;/td&gt;&#10;&lt;td&gt;什么都不做&lt;/td&gt;&#10;&lt;/tr&gt;&#10;&lt;/tbody&gt;&#10;&lt;/table&gt;&#10;&lt;p&gt;这就是为什么在 dsh / Cordis 里，插件&lt;strong&gt;不需要自己轮询依赖、也不需要自己接事件总线&lt;/strong&gt; —— 你 &lt;code&gt;inject&lt;/code&gt; 声明一下，系统在依赖满足性变化的瞬间驱动你的生命周期。&lt;/p&gt;&#10;&lt;p&gt;论文还顺手加了两个机制，都很实用：&lt;/p&gt;&#10;&lt;h3 id="隔离isolation同一个-key不同上下文解析到不同的值"&gt;隔离（isolation）：同一个 key，不同上下文解析到不同的值&lt;/h3&gt;&#10;&lt;p&gt;论文说它&lt;strong&gt;本质上实现了一个运行时的 ad-hoc 多态系统&lt;/strong&gt; —— 而且这种多态可以在运行时动态调整。多租户、测试环境、组件沙箱都用得上。比如同一份「数据库」依赖，给社区插件注入只读实例，给核心插件注入完整权限实例，而两份代码都不用改。&lt;/p&gt;&#10;&lt;h3 id="拦截interception给依赖访问挂横切元数据"&gt;拦截（interception）：给依赖访问挂横切元数据&lt;/h3&gt;&#10;&lt;p&gt;关键是它的合并规则是&lt;strong&gt;右偏的&lt;/strong&gt;：外层上下文可以覆盖组件自己的声明。这意味着&lt;strong&gt;外层可以约束一个组件「如何使用」某个依赖，而不修改那个组件的代码&lt;/strong&gt;。这个特性在权限控制里会变成主要卖点（第 6 节展开）。&lt;/p&gt;&#10;&lt;hr&gt;&#10;&lt;h2 id="4-最深刻的一点恢复不等于原样恢复"&gt;4. 最深刻的一点：恢复不等于「原样恢复」&lt;/h2&gt;&#10;&lt;p&gt;前面说「卸载把累加器跑一遍就回到初始状态」，论文很快就自己拆台了。&lt;/p&gt;&#10;&lt;p&gt;它举了两个例子：&lt;code&gt;free&lt;/code&gt; 把内存块还给分配器，但&lt;strong&gt;不会恢复&lt;/strong&gt; &lt;code&gt;malloc&lt;/code&gt; 之前堆的布局；一个「生成名」被丢弃后也恢复不了，因为下次创建会取一个新的。&lt;/p&gt;&#10;&lt;p&gt;物理状态根本没法原样恢复。所以论文做了一件很诚实的事：&lt;strong&gt;把「相等」降级成「观察等价」&lt;/strong&gt;。&lt;/p&gt;&#10;&lt;p&gt;定义是这样的：两个状态相关，当且仅当&lt;strong&gt;没有观察者能区分它们&lt;/strong&gt;。而「观察者」的定义才是精髓 —— &lt;strong&gt;观察一个值，就是跑它那个 key 提供的操作、读它们返回的结果。&lt;/strong&gt;&lt;/p&gt;&#10;&lt;p&gt;于是这个等价关系是被&lt;strong&gt;接口本身&lt;/strong&gt;生成的。推论非常实用：&lt;/p&gt;&#10;&lt;p&gt;接口发布什么决定了什么可回滚&#10;只发布必要结果的接口让变更不可观察，因而可交换可回滚；多发布一个结果就会暴露顺序差异，破坏可交换性&lt;/p&gt;&#10;&lt;p&gt;可交换 · 可回滚&#10;路由注册 / 事件监听器注册&#10;mmap — 可返回任意未用地址&#10;每次注册有自己的条目，顺序观察不到&lt;/p&gt;&#10;&lt;p&gt;不可交换 · 无法回滚&#10;中间件有序链&#10;open — 必须返回最低可用 fd&#10;多暴露一个结果，顺序差异就可见&lt;/p&gt;&#10;&lt;p&gt;接口少发布一个结果 → 观察等价更粗 → 更多变更变得不可观察&#10;接口设计因此可以直接当成可组合性设计来用&lt;/p&gt;&#10;&lt;p&gt;论文里那组 POSIX 对照我看了好几遍，实在太漂亮：&lt;/p&gt;&#10;&lt;ul&gt;&#10;&lt;li&gt;&lt;code&gt;mmap&lt;/code&gt; &lt;strong&gt;允许返回任何未使用的地址&lt;/strong&gt;，所以没有操作能观察到「用了哪个地址」，两次分配就是可交换的。&lt;/li&gt;&#10;&lt;li&gt;&lt;code&gt;open&lt;/code&gt; &lt;strong&gt;必须返回最低可用的文件描述符&lt;/strong&gt; —— 单这一条要求，就让两次描述符分配不再可交换。&lt;/li&gt;&#10;&lt;/ul&gt;&#10;&lt;p&gt;同一个内核里的两个系统调用，成败就取决于&lt;strong&gt;接口多发布了一个结果&lt;/strong&gt;。&lt;/p&gt;&#10;&lt;p&gt;论文还提到一个细节：关于「分配器发出的 handle」，如果它的接口不比较这些 handle，那么两个堆在「handle 重命名」下就是等价的 —— 这正是 &lt;strong&gt;CompCert&lt;/strong&gt; 关联一个程序及其编译结果的内存状态的方式。&lt;/p&gt;&#10;&lt;p&gt;这条洞察可以直接拿去用：&lt;strong&gt;如果你在设计一个可热插拔的接口，「少发布一个返回值」不只是减少耦合，它直接把更多变更变成了「不可观察」，从而变得可回滚。&lt;/strong&gt;&lt;/p&gt;&#10;&lt;hr&gt;&#10;&lt;h2 id="5-从单个组件抬到整个系统"&gt;5. 从单个组件，抬到整个系统&lt;/h2&gt;&#10;&lt;p&gt;单个组件能干净装卸还不够 —— 真实系统是一堆组件交错运行。论文为此搭了三层对象：&lt;/p&gt;&#10;&lt;table&gt;&#10;&lt;thead&gt;&#10;&lt;tr&gt;&#10;&lt;th&gt;概念&lt;/th&gt;&#10;&lt;th&gt;是什么&lt;/th&gt;&#10;&lt;/tr&gt;&#10;&lt;/thead&gt;&#10;&lt;tbody&gt;&#10;&lt;tr&gt;&#10;&lt;td&gt;&lt;strong&gt;组件（component）&lt;/strong&gt;&lt;/td&gt;&#10;&lt;td&gt;一个三元组：它需要的依赖 &lt;code&gt;d&lt;/code&gt;、它可能提供的 key &lt;code&gt;p&lt;/code&gt;、以及带见证的效应函数 &lt;code&gt;e&lt;/code&gt;&lt;/td&gt;&#10;&lt;/tr&gt;&#10;&lt;tr&gt;&#10;&lt;td&gt;&lt;strong&gt;纤程（fiber）&lt;/strong&gt;&lt;/td&gt;&#10;&lt;td&gt;组件的一次实例化，带自己的生命周期状态，和一份 &lt;strong&gt;committed view&lt;/strong&gt;（记录它激活时每个 key 由谁提供）。你在 Cordis 里看到的 &lt;code&gt;fiber&lt;/code&gt; 就是这个&lt;/td&gt;&#10;&lt;/tr&gt;&#10;&lt;tr&gt;&#10;&lt;td&gt;&lt;strong&gt;注册表（registry）&lt;/strong&gt;&lt;/td&gt;&#10;&lt;td&gt;所有纤程按名字挂载，父指针构成一棵树&lt;/td&gt;&#10;&lt;/tr&gt;&#10;&lt;/tbody&gt;&#10;&lt;/table&gt;&#10;&lt;p&gt;有个设计细节特别能体现作者的谨慎：&lt;strong&gt;整个依赖表是「派生」出来的，不是存储的&lt;/strong&gt; —— 它就是所有处于激活状态的纤程共同提供的东西。而且不同纤程的「可提供集合」&lt;strong&gt;必须不相交&lt;/strong&gt;，于是每个 key 恰好有一个 provider。&lt;/p&gt;&#10;&lt;p&gt;规则一共九条，分两类：&lt;strong&gt;编排规则&lt;/strong&gt;是外部能请求的动作（插入 / 退休 / 移除），&lt;strong&gt;生命周期规则&lt;/strong&gt;是只要前提成立就自发发生的步骤。&lt;/p&gt;&#10;&lt;p&gt;其中**「退休」（Retire）和「移除」（Remove）是刻意分开的**，理由很实在：退休是一个&lt;strong&gt;请求&lt;/strong&gt;，所以无条件；而一个已退休但仍处于激活态的纤程，&lt;strong&gt;必须先被停用才能移除&lt;/strong&gt; —— 提前移除会丢弃累加器，直接泄漏。&lt;/p&gt;&#10;&lt;p&gt;这套演算最终换来六个结果，我用大白话列一下：&lt;/p&gt;&#10;&lt;table&gt;&#10;&lt;thead&gt;&#10;&lt;tr&gt;&#10;&lt;th&gt;定理&lt;/th&gt;&#10;&lt;th&gt;大白话&lt;/th&gt;&#10;&lt;/tr&gt;&#10;&lt;/thead&gt;&#10;&lt;tbody&gt;&#10;&lt;tr&gt;&#10;&lt;td&gt;Preservation&lt;/td&gt;&#10;&lt;td&gt;系统不会「跑坏」：良构性在每一步之后都保持&lt;/td&gt;&#10;&lt;/tr&gt;&#10;&lt;tr&gt;&#10;&lt;td&gt;&lt;strong&gt;Recovery exactness&lt;/strong&gt;&lt;/td&gt;&#10;&lt;td&gt;任意交错之后跑某个纤程的累加器，&lt;strong&gt;它撤回的只有它自己的贡献&lt;/strong&gt;，别人的一丝不动&lt;/td&gt;&#10;&lt;/tr&gt;&#10;&lt;tr&gt;&#10;&lt;td&gt;&lt;strong&gt;Ordering&lt;/strong&gt;&lt;/td&gt;&#10;&lt;td&gt;纤程只在依赖就位的地方启动；provider 只在所有依赖方都停用之后，才撤回绑定&lt;/td&gt;&#10;&lt;/tr&gt;&#10;&lt;tr&gt;&#10;&lt;td&gt;Resolution coherence&lt;/td&gt;&#10;&lt;td&gt;一次过渡所依据的依赖解析，不会在它自己脚下移动&lt;/td&gt;&#10;&lt;/tr&gt;&#10;&lt;tr&gt;&#10;&lt;td&gt;&lt;strong&gt;Progress&lt;/strong&gt;&lt;/td&gt;&#10;&lt;td&gt;不会死锁，而且一定会终止 —— 每个最长的步骤序列都终结于「静止状态」&lt;/td&gt;&#10;&lt;/tr&gt;&#10;&lt;tr&gt;&#10;&lt;td&gt;&lt;strong&gt;Confluence&lt;/strong&gt;&lt;/td&gt;&#10;&lt;td&gt;无论中途怎么折腾、什么顺序，最终静止状态&lt;strong&gt;只取决于最终配置&lt;/strong&gt;&lt;/td&gt;&#10;&lt;/tr&gt;&#10;&lt;/tbody&gt;&#10;&lt;/table&gt;&#10;&lt;p&gt;前两个就是「时间可组合性」和「空间可组合性」的全局版本 —— 从「一个组件自己成立」升级成「任意交错的一堆组件之间都成立」。&lt;/p&gt;&#10;&lt;p&gt;而 &lt;strong&gt;Confluence 是最有工程价值的那个&lt;/strong&gt;，因为它直接回答了：&lt;strong&gt;「配置热更新为什么是对的？」&lt;/strong&gt; loader 在做增量调和的时候，中途怎么插入、怎么退休、以什么顺序执行，全都无所谓 —— 系统最终会停在「从零加载最终配置」本该在的位置。这就是 §5.2 那套实现的合法性来源。&lt;/p&gt;&#10;&lt;hr&gt;&#10;&lt;h2 id="6-落地理论--代码的对照"&gt;6. 落地：理论 ↔ 代码的对照&lt;/h2&gt;&#10;&lt;p&gt;Cordis 的实现分三层：&lt;strong&gt;核心库&lt;/strong&gt;（效应 / 余效应原语）→ &lt;strong&gt;组件加载器&lt;/strong&gt;（声明式配置、调和、热模块替换）→ &lt;strong&gt;应用框架&lt;/strong&gt;（Koishi 在第三层）。&lt;/p&gt;&#10;&lt;p&gt;论文的 Table 2 是一张理论到代码的对照表，几个有意思的映射：&lt;/p&gt;&#10;&lt;table&gt;&#10;&lt;thead&gt;&#10;&lt;tr&gt;&#10;&lt;th&gt;论文里的&lt;/th&gt;&#10;&lt;th&gt;Cordis 里的&lt;/th&gt;&#10;&lt;/tr&gt;&#10;&lt;/thead&gt;&#10;&lt;tbody&gt;&#10;&lt;tr&gt;&#10;&lt;td&gt;累加器&lt;/td&gt;&#10;&lt;td&gt;&lt;code&gt;fiber.dispose&lt;/code&gt;&lt;/td&gt;&#10;&lt;/tr&gt;&#10;&lt;tr&gt;&#10;&lt;td&gt;committed view&lt;/td&gt;&#10;&lt;td&gt;&lt;code&gt;fiber.committed&lt;/code&gt;&lt;/td&gt;&#10;&lt;/tr&gt;&#10;&lt;tr&gt;&#10;&lt;td&gt;插入 / 退休&lt;/td&gt;&#10;&lt;td&gt;&lt;code&gt;ctx.use&lt;/code&gt; 及其回调的逆&lt;/td&gt;&#10;&lt;/tr&gt;&#10;&lt;tr&gt;&#10;&lt;td&gt;生命周期状态&lt;/td&gt;&#10;&lt;td&gt;&lt;code&gt;fiber.state&lt;/code&gt;（LOADING 对应 Reloading）&lt;/td&gt;&#10;&lt;/tr&gt;&#10;&lt;/tbody&gt;&#10;&lt;/table&gt;&#10;&lt;h3 id="一个顺带做成的安全特性"&gt;一个顺带做成的安全特性&lt;/h3&gt;&#10;&lt;p&gt;访问依赖有两条路：&lt;code&gt;ctx.get(key)&lt;/code&gt; 是反射式查找，&lt;strong&gt;永远不失败&lt;/strong&gt;；而 &lt;code&gt;ctx[key]&lt;/code&gt; 走 TypeScript 的 Proxy，&lt;strong&gt;沿纤程链向上解析&lt;/strong&gt; ——&lt;/p&gt;&#10;&lt;ul&gt;&#10;&lt;li&gt;遇到第一个 committed view 里绑定了这个 key 的纤程 → 授权，返回&lt;/li&gt;&#10;&lt;li&gt;遇到一个&lt;strong&gt;声明了但还没加载&lt;/strong&gt;的纤程 → 访问失败&lt;/li&gt;&#10;&lt;li&gt;一路走到 root 都没人声明 → 直接拒绝&lt;/li&gt;&#10;&lt;/ul&gt;&#10;&lt;p&gt;这&lt;strong&gt;本身就是一层能力式访问控制&lt;/strong&gt;：组件只能访问它声明过的东西。而且因为声明是静态的，&lt;strong&gt;完整的权限集合在组件运行之前就已知&lt;/strong&gt; —— 编排器可以在加载的时候审查，而不是等到访问发生才发现。&lt;/p&gt;&#10;&lt;h3 id="koishi-的四年就是这套理论的对照组"&gt;Koishi 的四年，就是这套理论的对照组&lt;/h3&gt;&#10;&lt;ul&gt;&#10;&lt;li&gt;四年开发，积累了 &lt;strong&gt;4000+ 社区插件&lt;/strong&gt;（IM 适配器、数据库驱动、管理控制台、终端功能）&lt;/li&gt;&#10;&lt;li&gt;论文自己交代：Koishi 目前跑在 Cordis v3 上，论文里的 v4 精炼了效应/余效应语义并重写了 loader，核心组合模型两版共享&lt;/li&gt;&#10;&lt;li&gt;最有说服力的验证是**「时间可组合性没有认知开销」**：因为效应被自动追踪、逆被自动组合，&lt;strong&gt;一个没经验的插件作者也能拿到有序清理，完全不用写 uninstall 路径&lt;/strong&gt;&lt;/li&gt;&#10;&lt;/ul&gt;&#10;&lt;p&gt;论文说，这才补上了 VSCode 那种「靠每个作者自己勤勉」的缺失 —— 正确性从「每个人的责任心」变成了&lt;strong&gt;由抽象一次性结清&lt;/strong&gt;。&lt;/p&gt;&#10;&lt;p&gt;空间侧的验证同样实在：Koishi 的生态里有&lt;strong&gt;真实的依赖拓扑&lt;/strong&gt;。运行时切换存储后端，只会重新激活那些「解析结果真的变了」的依赖方；依赖暂时缺失的插件保持不活动、&lt;strong&gt;但不报错&lt;/strong&gt;。而关键在于，&lt;strong&gt;这一切跨独立作者成立&lt;/strong&gt; —— 插件和它的依赖通常由不同人写，除了连接它们的那个 key 之外，谁也不认识谁。&lt;/p&gt;&#10;&lt;hr&gt;&#10;&lt;h2 id="7-作者自己承认的局限"&gt;7. 作者自己承认的局限&lt;/h2&gt;&#10;&lt;p&gt;这部分我挺欣赏，写得非常实在，没有硬撑：&lt;/p&gt;&#10;&lt;table&gt;&#10;&lt;thead&gt;&#10;&lt;tr&gt;&#10;&lt;th&gt;局限&lt;/th&gt;&#10;&lt;th&gt;具体说明&lt;/th&gt;&#10;&lt;/tr&gt;&#10;&lt;/thead&gt;&#10;&lt;tbody&gt;&#10;&lt;tr&gt;&#10;&lt;td&gt;&lt;strong&gt;案例研究是「存在性」的，不是定量的&lt;/strong&gt;&lt;/td&gt;&#10;&lt;td&gt;只有 Koishi 一个生态、TypeScript 一种宿主语言，是观察性的，没有受控对照。抽象带来的&lt;strong&gt;开销、以及对开发效率的影响，论文没有测量&lt;/strong&gt;，明确留作未来工作&lt;/td&gt;&#10;&lt;/tr&gt;&#10;&lt;tr&gt;&#10;&lt;td&gt;&lt;strong&gt;「无环」是假设，不是结论&lt;/strong&gt;&lt;/td&gt;&#10;&lt;td&gt;Progress 和 Confluence 两条定理都建立在依赖优先关系无环的假设上。有环的后果只是相关组件永远不激活（好处是&lt;strong&gt;从声明就能提前预测&lt;/strong&gt;，不像并发死锁必须在运行时检测），但「拆环」会让集成组件的数量&lt;strong&gt;随组件数二次增长&lt;/strong&gt;&lt;/td&gt;&#10;&lt;/tr&gt;&#10;&lt;tr&gt;&#10;&lt;td&gt;&lt;strong&gt;版本问题完全开放&lt;/strong&gt;&lt;/td&gt;&#10;&lt;td&gt;形式模型只有&lt;strong&gt;按名字链接&lt;/strong&gt;，没有版本或结构化链接。于是有&lt;strong&gt;接口漂移&lt;/strong&gt;（provider 改了接口，消费者还声明同一个 key，依赖「满足」了但值不对）和 &lt;strong&gt;key 碰撞&lt;/strong&gt;（两个独立 provider 用同名 key 表示无关接口，消费者不做检查就接受）。Cordis 现在靠 peer dependencies 兜着，但这依赖大家遵守 semver，而包管理器通常只解析单一版本&lt;/td&gt;&#10;&lt;/tr&gt;&#10;&lt;tr&gt;&#10;&lt;td&gt;&lt;strong&gt;边界外的东西回不了滚&lt;/strong&gt;&lt;/td&gt;&#10;&lt;td&gt;论文把环境划成边界内 / 外。一次外部操作分两个阶段：&lt;strong&gt;获取&lt;/strong&gt;在边界内（装描述符、注册条目 —— 是可回滚效应），&lt;strong&gt;发射&lt;/strong&gt;跨到边界外（写出去的字节、发出去的消息 —— 表现为恒等变换）。事后要恢复只能靠&lt;strong&gt;扣留&lt;/strong&gt;（推迟发射，即经典的 output commit 问题）或&lt;strong&gt;补偿&lt;/strong&gt;（退款、删文件），而补偿「&lt;strong&gt;元理论不传递&lt;/strong&gt;」—— 交换性是针对观察等价证的，换成更粗的等价得重新证一遍&lt;/td&gt;&#10;&lt;/tr&gt;&#10;&lt;tr&gt;&#10;&lt;td&gt;&lt;strong&gt;沙箱需要语言之外的机制&lt;/strong&gt;&lt;/td&gt;&#10;&lt;td&gt;语言级的访问控制挡不住恶意组件（它能直接够到底层对象），必须靠软件故障隔离、独立运行时、沙箱进程或容器&lt;/td&gt;&#10;&lt;/tr&gt;&#10;&lt;/tbody&gt;&#10;&lt;/table&gt;&#10;&lt;hr&gt;&#10;&lt;h2 id="8-可借鉴清单"&gt;8. 可借鉴清单&lt;/h2&gt;&#10;&lt;h3 id="认知层"&gt;认知层&lt;/h3&gt;&#10;&lt;ol&gt;&#10;&lt;li&gt;&lt;strong&gt;「注册即副作用，卸载即撤销」值得当成一条设计纪律。&lt;/strong&gt; 不只是 Cordis 的做法，而是一个可以迁移的原则：任何「装上去」的东西，都应该在安装的那一刻就定义好「怎么拆下来」，并且让框架替你拆。凡是把清理代码写在另一个地方的设计，迟早会漏。&lt;/li&gt;&#10;&lt;li&gt;&lt;strong&gt;给「接口发布什么」这件事加一层思考。&lt;/strong&gt; 想让一个组件更容易被替换、被回滚，第一步不是加抽象，而是&lt;strong&gt;检查它的接口有没有发布不必要的返回值&lt;/strong&gt;。发布得越少，可观察的差异越少，能安全替换的实现就越多。POSIX 里 &lt;code&gt;mmap&lt;/code&gt; 和 &lt;code&gt;open&lt;/code&gt; 的对比是最好的教材。&lt;/li&gt;&#10;&lt;li&gt;&lt;strong&gt;依赖用「声明」而不是「查找」。&lt;/strong&gt; 组件说「我需要什么」，而不是「我去哪里找什么」。声明可以静态审查、可以推导加载顺序、可以在依赖消失时自动停用 —— 查找做不到这些。&lt;/li&gt;&#10;&lt;/ol&gt;&#10;&lt;h3 id="架构层"&gt;架构层&lt;/h3&gt;&#10;&lt;ol start="4"&gt;&#10;&lt;li&gt;&lt;strong&gt;把「撤销」做成一等公民，而不是异常路径。&lt;/strong&gt; 如果你的系统有任何形式的动态加载（插件、工具、技能、子 agent），先保证两件事：每次注册都返回一个 disposer，且框架在卸载时&lt;strong&gt;逆序&lt;/strong&gt;执行它们。&lt;/li&gt;&#10;&lt;li&gt;&lt;strong&gt;「退休」和「移除」分开。&lt;/strong&gt; 一个想下线的组件，应该先被标记、等它自然停用，再真正删除。直接删会丢掉它持有的撤销信息 —— 这是论文专门用一条规则强调的坑。&lt;/li&gt;&#10;&lt;li&gt;&lt;strong&gt;热更新要有「最终状态只取决于最终配置」这个性质。&lt;/strong&gt; 如果你的增量更新做出来的结果，和「拿最终配置重启一次」不一样，那这个增量就是不可信的。Cordis 用一条合流定理把这个性质钉死了，自研系统至少应该把它当成测试断言。&lt;/li&gt;&#10;&lt;li&gt;&lt;strong&gt;能力集合在加载前可知，比在访问时才拦截强得多。&lt;/strong&gt; 声明式的依赖清单天然就是一张权限表，可以在组件装载前审查。这是「声明式」相对「运行时反射」被低估的一个收益。&lt;/li&gt;&#10;&lt;/ol&gt;&#10;&lt;h3 id="可以直接去读的东西"&gt;可以直接去读的东西&lt;/h3&gt;&#10;&lt;ol start="8"&gt;&#10;&lt;li&gt;论文第 3.4.2 节关于「可交换性」的讨论，是全文最可迁移到日常接口设计的部分，配合 §6.6 一起读。&lt;/li&gt;&#10;&lt;li&gt;Cordis 源码只有 9 个模块（context / registry / fiber / events / service / reflect / logger / utils / index），&lt;code&gt;fiber.ts&lt;/code&gt; 是核心 —— 生命周期状态机加 effect 系统都在里面。&lt;/li&gt;&#10;&lt;li&gt;论文的 Table 2（理论↔实现对照表）值得单独保存，它是一张「读源码时该看哪里」的地图。&lt;/li&gt;&#10;&lt;/ol&gt;&#10;&lt;hr&gt;&#10;&lt;h2 id="9-风险与观望点"&gt;9. 风险与观望点&lt;/h2&gt;&#10;&lt;table&gt;&#10;&lt;thead&gt;&#10;&lt;tr&gt;&#10;&lt;th&gt;点&lt;/th&gt;&#10;&lt;th&gt;说明&lt;/th&gt;&#10;&lt;/tr&gt;&#10;&lt;/thead&gt;&#10;&lt;tbody&gt;&#10;&lt;tr&gt;&#10;&lt;td&gt;&lt;strong&gt;论文没有性能数据&lt;/strong&gt;&lt;/td&gt;&#10;&lt;td&gt;全文没有 benchmark。效应追踪和依赖解析的运行时开销是多少，只能自己压测&lt;/td&gt;&#10;&lt;/tr&gt;&#10;&lt;tr&gt;&#10;&lt;td&gt;&lt;strong&gt;单一生态验证&lt;/strong&gt;&lt;/td&gt;&#10;&lt;td&gt;所有实证来自 Koishi + TypeScript，范式本身和其他语言实现的优劣还没有横向对比&lt;/td&gt;&#10;&lt;/tr&gt;&#10;&lt;tr&gt;&#10;&lt;td&gt;&lt;strong&gt;「无环」需要自己保证&lt;/strong&gt;&lt;/td&gt;&#10;&lt;td&gt;它不是被证明的，是假设。你的组件设计得自己盯着别写出环&lt;/td&gt;&#10;&lt;/tr&gt;&#10;&lt;tr&gt;&#10;&lt;td&gt;&lt;strong&gt;版本语义还缺一块&lt;/strong&gt;&lt;/td&gt;&#10;&lt;td&gt;这是论文明确列为「开放问题」的部分。生态越大，接口漂移和 key 碰撞的代价越明显&lt;/td&gt;&#10;&lt;/tr&gt;&#10;&lt;/tbody&gt;&#10;&lt;/table&gt;&#10;&lt;hr&gt;&#10;&lt;h2 id="10-延伸阅读"&gt;10. 延伸阅读&lt;/h2&gt;&#10;&lt;ul&gt;&#10;&lt;li&gt;论文原文：&lt;code&gt;arxiv.org/abs/2608.25512&lt;/code&gt;（92 页，但 §3 和 §4 的定理陈述部分读下来就够了）&lt;/li&gt;&#10;&lt;li&gt;Cordis 源码：&lt;code&gt;github.com/cordiverse/cordis&lt;/code&gt;（MIT）&lt;/li&gt;&#10;&lt;li&gt;DeepSeek Harness 里 vendor 的版本：&lt;code&gt;vendor/cordis&lt;/code&gt;，发布为 &lt;code&gt;@deepseek-ai/cordis&lt;/code&gt;&lt;/li&gt;&#10;&lt;li&gt;官方插件入门：&lt;code&gt;docs/cordis-primer.zh.md&lt;/code&gt;&lt;/li&gt;&#10;&lt;li&gt;前文：《DeepSeek Harness 精读：一个「一切皆插件」的 Agent 运行时》&lt;/li&gt;&#10;&lt;/ul&gt;&#10;&lt;hr&gt;&#10;&lt;p&gt;**一句话总结：**它把 effect 和 coeffect 从编译期的类型注解，变成运行时可操作的一等对象，让「运行时装卸组件」第一次有了完整的理论保证 —— 而这一切，已经被一个 4000+ 插件的生产系统验证了四年。&lt;/p&gt;&#10;</description></item></channel></rss>