赞助商LobeHubLobeHub了解更多
ddshfind
GitHub

第 2 课:动机示例:VSCode 插件与 AI 智能体

一句话版:插件系统普遍「装上容易、拆下难」——VSCode 想停用一个插件得重启整个进程,想依赖另一个插件又没有类型保证;而未来的 AI 智能体恰恰会在运行时不断自我修改,因此最需要「装得上、拆得下、互不破坏」的能力,这正是 Cordis 论文要解决的问题。

1. 时间限制:装上容易,拆下却要重启整个进程

先做一个思想实验。你的手机可以随时随地安装新 App,但假如你想「只把某款正在运行的 App 从系统里摘掉」,让它彻底停下来、退出内存,系统却告诉你:对不起,你得重启整个手机,其他 App 也跟着一起重启。是不是很荒谬?

VSCode 的插件系统,就是这个荒谬的现状。论文用它作为「插件系统」的代表:

  • VSCode 把所有扩展都装进同一个「扩展宿主」(extension host)共享进程里运行;
  • 扩展可以动态安装——运行时装上,没问题;
  • 但宿主没有「在运行时卸载单个扩展代码」的机制
  • 因此,一旦某个扩展的 activate 函数执行完毕,想停用或卸载它,只能重启整个宿主进程,所有已加载的扩展都会受影响。

是不是所有扩展都这么麻烦?不是,关键在于有没有代码

扩展类型典型例子能否单独移除
纯声明式(不含代码)主题、快捷键绑定、代码片段✅ 可以,自由移除
含可执行代码语言服务、调试器、自动补全❌ 不行,移除要重启整个宿主

数据有多吓人?按安装量排名前 100 的扩展中,有 87 个含有可执行代码。也就是说,绝大多数常用扩展想「拆」都拆不利索。

deactivate 钩子:只是「临终回调」,不是卸载

VSCode 不是提供了 deactivate 钩子吗?听起来像「卸载回调」?别被名字骗了:

  • 只是在宿主进程已经开始终止时被调用的「优雅关闭回调」——相当于写遗嘱,而不是实时移除;
  • 更糟的是,它把效应的释放(写在 deactivate 里)和效应的创建(写在 activate 里)拆成了两处,违背了关注点局部性(locality of concern),也让「完整清理」难以验证。

💡 什么是「关注点局部性」?借书还书的例子:activate 是「借书时登记」,deactivate 是「还书时登记」。如果借书登记和还书登记分属两个完全独立的系统,你很难验证「每一本借出去的书都还回来了」。deactivate 把「创建」和「销毁」拆开,清理是否完整就成了一个猜谜。

2. 空间限制:插件之间「几乎不互相依赖」

时间上拆不利索,那空间上呢?插件和插件之间能不能好好「互相配合」?论文的答案是:VSCode 几乎没有为「扩展依赖扩展」提供安全且结构化的方式

VSCode 确实提供了一个叫 extensionDependencies 的字段,用来声明扩展之间的依赖。但看看实际使用情况:

按安装量排名前 100 的扩展中,只有 7 个会通过 extensionDependencies 声明对非内置扩展的依赖。

为什么这么稀少?论文给了两个原因:

  1. 扩展 API 的形态决定了扩展「各自为政」:这个 API 只暴露命令、视图、语言功能等固定的表层扩展点。扩展通过扩展点向宿主「贡献功能」,而不是互相依赖——所以扩展间依赖很少出现。
  2. 扩展间交互没有结构化契约:VSCode 通过 vscode.extensions.getExtension(...).exports 向其他扩展暴露某个扩展的功能,但返回值没有类型(默认为 any)——依赖方根本无法依靠「经过类型检查的接口」。

🏢 打个比方:公寓里的住户(扩展)只跟物业(宿主)打交道。想借用隔壁的东西?只能通过物业递纸条(exports),而且纸条的内容没有任何人检查格式(没有类型检查)。想抄邻居的作业,邻居只给你一张没头没尾的字条,你只能祈祷格式对得上。

小结:VSCode 引导扩展使用一组固定的、由宿主提供的扩展点,却没有为「扩展彼此依赖」提供安全且结构化的方式。

⚠️ 顺带一提,这两项限制并非 VSCode 独有——它们在各种插件系统里都会出现,只是程度不同。所以这不是某个产品的小毛病,而是整个「插件架构」的通病,也是论文要动刀的地方。

3. 为什么自演化 AI 智能体更需要它?

有人会说:VSCode 插件的问题,忍一忍不就过去了?忍不过去,因为 AI 智能体时代把这个痛点放大了无数倍。

现代 AI 智能体依赖运行时智能体框架:组合各类工具套件和执行环境、管理权限与沙箱、维持会话状态与持久化、提供上下文管理与记忆、编排子智能体和多智能体工作流、向用户与自动化系统提供接口。而未来的智能体框架可能会在持续处理请求的同时,生成并部署对自身组件的修改——也就是「自演化」。模型合成的可复用工具,就是「组件级自修改」的一种范围更窄的先行形态。

关键来了:每一次这样的自我修改,本身都是一次动态组合实例——装上一个新组件,或者拆下一个旧组件。而且这些修改持续发生、只有有限乃至完全没有人工监督,所以动态可组合性「不可或缺」。缺了会怎样?

缺少后果
时间可组合性每次自修改都迫使整个进程重启,并丢弃进程本地积累的全部状态;以这样的频率发生时,累积的不可用时间相当可观,执行中的任务一再中断;更糟:有缺陷的自修改可能使「恢复所必需的进程」本身失效
空间可组合性每个模块都必须自己检测所依赖模块的变化,并在这些模块出现、消失或改变身份时作出适应,只能靠临时拼凑的手段;更糟:朴素的代码替换策略可能悄无声息地破坏依赖方,或者引入只在重新加载时才暴露的循环依赖

⚠️ 注意这两处「更糟」:时间维度上,连「用来恢复系统的进程」都可能被改坏——等于把救命稻草也烧了;空间维度上,破坏是「悄无声息」的,等重新加载时才暴露问题,那时已经晚了。对一个长期无人值守的智能体来说,这两种「更糟」都是灾难。

4. 粗粒度变通方案:能凑合,但代价高昂

你可能会问:这么明显的问题,为什么一直没人解决?论文给出的回答很诚实:因为操作系统和容器编排器已经提供了一种「粗粒度」替代方案,让大家凑合着用了很久

  • 终止并重启进程 → 可以在整个地址空间的粒度上实现时间可组合性;
  • 容器编排 → 可以在整个服务的粒度上实现空间可组合性。

在实践中,大多数软件确实就靠这两招兜底:模块行为异常就重启进程,服务依赖就交给编排器管理。

然而,这套变通方案代价高昂:

  • 时间维度:每次重启都会丢弃进程本地积累的全部状态——缓存、连接、部分完成的计算统统归零;重建这些状态需要数秒乃至数分钟。想在这期间维持可用性?只能开冗余副本,用资源开销弥补「无法恢复单个组件」的问题
  • 空间维度:容器级编排无法表达共享同一地址空间的组件之间的依赖;本来一个本地函数调用就能完成的交互,被迫承担网络开销。
传统做法换 1 个插件 → 重启整个宿主插件A插件B插件C插件D全部一起停 ❌Cordis 做法换 1 个插件 → 只拆它自己插件A插件B插件C插件D只停 1 个,其他继续 ✅

粒度不匹配:想换一个组件,传统做法却要推倒整个进程

这就是粒度不匹配:重启和编排都在进程、容器的边界上运作,但现代系统越来越多地在更细的层次上进行组合。我们需要一种组合式抽象,让它能在与组件本身相同的层次上管理效应和依赖——组件级的「装上、拆下、不互相破坏」。这正是论文接下来要建立的形式化基础。

关键点回顾

这一课的信息量不小,记住这五句话就够了:

  1. 时间限制:VSCode 所有扩展共享一个「扩展宿主」进程,没有运行时卸载单个扩展的机制;按安装量前 100 的扩展中 87 个含代码,移除就要重启整个宿主。
  2. deactivate 不是卸载:它只是宿主进程终止时的「临终回调」;还把释放与创建拆成两处,违背关注点局部性,完整清理难以验证。
  3. 空间限制:扩展只向宿主贡献功能,几乎不互相依赖(前 100 只有 7 个声明依赖);即便用 exports 暴露功能,返回值也是 any,没有类型化契约。
  4. 自演化智能体:每次自修改都是一次动态组合;缺时间可组合性会频繁重启丢状态、连恢复进程都可能被改坏;缺空间可组合性则模块各自拼凑、破坏悄无声息。
  5. 变通方案粒度不匹配:重启进程、容器编排在进程/容器边界运作,代价是丢状态、冗余副本、网络开销,无法在组件自身的层次上管理效应和依赖。

🚀 下一课预告:动机已经清楚了——我们需要「组件级的装上与拆下」。第 3 课我们将回顾论文的三项核心贡献(可回退效应、反应式余效应、组件生命周期模型),并认识贯穿全篇的类型判断记号。

自测题 · 动机示例

完成作答后点击「提交答案」,可以查看对错与解析。

1. 为什么在 VSCode 中停用或卸载「含代码的扩展」必须重启整个扩展宿主进程?
2. 关于 VSCode 的 deactivate 钩子,下列说法正确的是?
3. 为什么 VSCode 扩展之间几乎不互相依赖?(前 100 名中只有 7 个声明对非内置扩展的依赖)
4. 为什么「重启进程、容器编排」这类粗粒度变通方案,满足不了自演化智能体框架的需求?