CORDISDSH 的心脏第 02 章 · 问题全书目录术语表读论文原文 ↗
第二章 · 问题

换个灯泡,
为什么要拉整栋楼的电闸

你一定见过「请重启以完成卸载」。这一章讲它为什么存在、代价有多大、到了 agent 时代为什么会变成灾难,以及论文把它拆成的两道考题。

约 6 分钟 · 上一章 01 由来 · 下一章 03 一页纸

题图一个灯泡,一个电闸。

「请重启以完成卸载」

论文开篇拿 VSCode 当例子,因为它是用的人最多的可扩展编辑器之一。VSCode 的所有扩展跑在同一个共享进程里,叫扩展宿主。装扩展可以随时装,卸扩展却没法单独卸:扩展的代码一旦跑起来,想停用或卸载,就得把整个宿主重启一遍,所有扩展陪着一起重来。

有多常见?论文数了安装量前 100 的扩展:87 个带可执行代码,卸载时都得重启 p.5。剩下 13 个是主题、快捷键这类纯声明的东西,才能随手删。

VSCode 不是没给扩展留「收尾」的口子。它有个 deactivate 钩子,但只在整个宿主关闭时才调用,做不到运行中单独卸载 p.5。还有一个更隐蔽的毛病:装的时候做了什么写在一处,撤的时候该撤什么写在另一处。两份清单靠作者自己对齐,漏一行就是一个清不掉的残留。首页那个「幽灵任务」,就是这么来的。

插件之间,几乎不打交道

另一个数字更有意思:前 100 个扩展里,只有 7 个声明了对别的扩展的依赖 p.5。

不是扩展们不想合作,是平台没给好的通道。扩展主要往宿主固定的几个口子上挂东西:命令、视图、语言功能。真要互相调用,拿到的是一个没有类型的对象,对方改了接口,你这边直到出错才知道。结果就是:插件之间要么不打交道,要么提心吊胆地打交道。

论文说,这两个毛病不是 VSCode 独有的,几乎所有插件系统都有,只是程度不同。

土办法:断电重来

大部分软件对付这两个毛病,用的是现成的粗办法。某个模块出了问题,重启进程;模块之间有依赖,干脆拆成两个服务,交给容器编排去管。

能用,但很贵。每次重启都丢掉进程里攒下的东西:缓存、连接、做到一半的计算,重建一次要几秒到几分钟 p.6。为了重启期间不停服,还得多养几份副本顶着。拆成服务呢,本来一次函数调用就能办的事,变成了一次网络请求。

论文的诊断是一句话:粒度不对。重启管的是「进程」,编排管的是「服务」,可现代软件真正的组装单位,是比它们细得多的「组件」。

演示 2.1 · 可以点升级一个插件:整机重启 vs 原地换
    一个进程 · 小周的机器人
    天气插件在岗
    版本 v1
    提醒插件在岗
    待发提醒 3 条
    聊天连接在岗
    已在线 12:00
    缓存在岗
    已缓存 248 条
    旁白小周的机器人跑在一个进程里,里面有四块东西各忙各的。她要把天气插件从 v1 升到 v2。按「开始」,先看它正常跑起来。
    按主按钮一拍一拍往下走

    到了 agent 这里,问题放大一千倍

    上面这些,在人写插件的世界里还能忍:一天卸几次插件,重启就重启吧。可 agent harness 不一样,它可能一边服务用户,一边给自己改零件,而且改的频率高、几乎没人盯着。

    论文把后果说得很直白。拆不干净,每次自我修改都要整机重启,攒下的状态全丢;改得一频繁,停机时间越积越多,手头的任务被一遍遍打断。更糟的是,一次改坏的自我修改,可能连负责修复的那个进程也一起弄瘫 p.5。

    接不稳呢?每个模块都得自己盯着它依赖的模块:什么时候出现、什么时候消失、什么时候换了一个。用土办法直接替换代码,还可能悄悄弄坏下游,或者绕出一个循环依赖,直到下次重载才炸出来。

    两道考题:时间与空间

    论文把「边跑边换零件」需要的能力,拆成两个互相独立的维度 p.4:

    第一道 · 时间

    拆得干净

    一个组件被拿走时,它对共享环境做过的每一个改动,都要完整、安全地撤回。

    本书用回执来想:做一件事,留一张「怎么撤回」的回执。

    第二道 · 空间

    接得稳

    组件之间谁依赖谁,要能声明、能被找到;依赖一变,谁该上岗、谁该歇着,自动安排好。

    本书用插座来想:进门先说清楚,自己要插哪几个插座。

    为什么说这两道题新?在老式的静态程序里,它们都早有答案:一段代码执行完,变量自动收走,这是「拆得干净」;文件开头写 import,编译器帮你连好,这是「接得稳」。麻烦在于,组件一旦能在运行中来来去去,这两个老答案就都不够用了。

    带走这一句

    边跑边换零件的软件,要过两道考题:拆得干净(时间),接得稳(空间)。

    请帮我检查这个项目,回答两个问题:
    1. 时间:哪些代码在运行中注册了东西(定时器、事件监听、命令、全局状态),却没有对应的撤销?列出位置。
    2. 空间:哪些模块依赖别的模块,却没有声明出来,而是直接假设对方已经存在?列出位置。
    先只列清单,不要改代码。