青雲的博客

Article

插件化的第五时代:契约迁移,而非契约消失

· 28 分钟阅读

摘要

如果只看表面,插件化像是在不断降低接入门槛:早期需要导出函数,后来需要 bundle、manifest 和服务注册,再后来只要把扩展放进 Marketplace。到了 Agent 时代,模型甚至可以根据一句自然语言描述,猜出某个工具大概能做什么,并生成调用它所需的胶水代码。

但插件系统真正的难题从来不是“能不能调用”。每一次把外部能力接进宿主,系统都必须重新回答四个问题:谁可以进入,能够看到什么,调用如何被理解,失败后怎样恢复。DLL、OSGi、VS Code 和 DeepSeek Harness 的差异,正好对应了这四个问题在不同阶段的扩大。

本文沿着这条历史线索,结合微软、OSGi、VS Code 的官方资料,以及 DSH dsh-v0.1.0-rc.7、提交 99f6f02fecdb7dff40c3fbc9470f5907c29f74ca 的源码和测试,讨论所谓“第五时代”究竟可能改变什么。我的判断是:AI 会把契约从开发者预先写好的接口,推向模型可理解的能力描述、运行时校验、权限策略和恢复证据;契约的数量和形态会变,契约本身不会消失。

关键词

插件系统;动态组合;运行时治理;DeepSeek Harness;Cordis;Agent Runtime

先说明本文的比较方式:DLL、OSGi、VS Code 和 DSH 并不是一条严格的产品继承链。本文把它们当作四个控制面样本,观察插件从装载代码、管理依赖、承载生态到进入 Agent 运行时之后,宿主需要负责的事情如何扩大。

从 DLL 到 DSH,插件系统控制面的扩大

从一个导出函数开始

把问题落到一个具体动作上:早期插件作者写完代码,把它编译成 DLL,然后告诉宿主:“这里有一个导出函数,名字叫 process。”宿主只需要在加载时或运行时找到这个符号,把 DLL 映射进自己的进程地址空间,再按照约定的调用方式执行。

微软对 DLL 的官方说明,重点就是动态链接、导出函数和装载时机。运行时链接可以按需加载模块,引用计数归零后再卸载模块。About Dynamic-Link Libraries 它回答了第一层问题:代码怎样进入进程,以及宿主怎样找到它。

那个时代的“插件契约”往往隐藏在头文件、导出符号和调用约定里。内存由谁分配,错误如何返回,线程是否安全,宿主和插件是否使用同一个运行库,这些事情很少由装载机制本身解释。插件只要能被链接,就可能在更深的地方破坏宿主。

这给后来的插件系统留下了一个持续存在的教训:动态装载不等于动态治理。越是接近宿主进程,接入越快,故障半径也越大。插件化的下一个问题自然出现了:当插件数量增加,并且插件之间也开始互相依赖,光有导出函数还够不够?

当插件开始互相依赖

OSGi 的出现,可以看作对这个问题的一次系统化回答。它把插件从一个可加载文件变成 bundle:bundle 有 manifest,有版本,有包依赖,也有自己的生命周期。宿主不只是找到一个入口,还要先解析模块之间能否共存。

OSGi Core 8 的 Module Layer 规定了 bundle、manifest、包导入导出和解析关系。Module Layer 解决的是“哪些模块可以被装进来”。Service Layer 再进一步,把服务注册、发现、绑定、动态变化和权限放进框架。Service Layer 中的服务可以在运行时出现或消失,但调用方仍然依赖接口、属性和版本语义。

生命周期层把安装、启动、停止、更新和卸载明确列成操作,并允许宿主监控这些变化。Life Cycle Layer 让“插件被替换”成为可观测事件,避免文件覆盖后留下未知状态。

这里有一个经常被忽略的转折。OSGi 的动态性比 DLL 强得多,但它并没有因此减少契约。相反,它把原本藏在调用约定里的内容拆成了更多显式信息:依赖版本、服务属性、权限、启动顺序和卸载行为。动态能力增加之后,宿主需要知道的事情也增加了。

VS Code 把插件变成了生态

VS Code 面对的又是另一种压力。扩展不再只是给一个程序增加几个函数,它们要贡献命令、语言服务、调试器、主题、视图和设置。用户会同时安装大量来自不同作者的扩展,编辑器还必须保持响应,不能因为某个扩展崩溃就一起退出。

因此,VS Code 把“插件”放进了一个产品化的宿主模型。官方文档区分 local、web 和 remote Extension Host,扩展可以运行在不同位置和不同运行时中;宿主还可以根据激活事件延迟加载扩展。Extension Host 处理的是隔离、部署位置和稳定性。

扩展作者则通过 package.json 声明入口、激活事件、引擎版本、publisher 和 Contribution Points。Extension Manifest 记录扩展是谁、适配什么版本以及何时启动;Contribution Points 用结构化 JSON 声明扩展要把哪些能力接入编辑器。

当扩展进入 Marketplace 或以 VSIX 分发时,版本兼容性、发布者身份、搜索、下架和升级也成了插件系统的一部分。市场看起来像生态设施,实际承担的是治理工作:宿主需要知道从哪里获得插件,用户需要知道谁发布了它,系统还要保留撤回和升级的路径。

到这里,插件化已经从“把代码装进来”变成“把不完全可信的第三方能力接入一个长期运行的产品”。契约不再只描述函数签名,还描述宿主边界、发布身份和用户可见的产品行为。

DSH 让插件进入 Agent 的运行时

DeepSeek Harness 把问题带进了 Agent 运行时。它面对的宿主不再是单一编辑器,而是一个同时包含模型适配器、工具、会话日志、UI 和 agent loop(代理循环)的系统。插件可以贡献服务、类型化事件和可逆 effect(副作用),不同部分通过 Cordis 组合起来。DSH Architecture

我在核对 DSH 源码时,起初也很容易把 “everything is a plugin” 理解成“任意代码都可以随时接入”。读到动态 runner 的实现和测试后,这个直觉需要修正。读者只需抓住两条路径:静态路径由 profile、bundle 等配置组合插件,服务通过稳定的 ctx key 暴露,插件用 inject 声明依赖;动态路径则由 definerun 控制。session log(会话日志)仍然是模型上下文的事实来源,文档要求凡是进入模型请求的内容都能够从日志重建。Session Log

DSH 确实提供了动态包路径。@deepseek-ai/dsh-cordis-host-runnerdefinerun 分开:定义阶段记录元数据并编译检查 host/client 两半(宿主端和浏览器端)的语法,真正的 effect 要到运行阶段才产生;浏览器端还需要经过页面的运行请求和人工允许流程。Host Runner README

这条动态路径仍有明确的边界:

  • 动态包定义存在进程内存中,DSH 重启后不会自动恢复。
  • 动态包带有 session ownership(会话归属),其他会话不能直接控制它。
  • stop 会释放 host half 的 fiber,并撤回浏览器半的运行状态。
  • vmTimeoutMs 只限制同步求值时间,异步主体采用合作式信任模型。
  • 文档明确声明 node:vm 不是安全边界,动态包应按接近 Bash 的权限来对待。

这组限制很有代表性。代码可以动态生成,生命周期和权限仍由宿主控制;执行可以暂时存在,事实记录和拒绝路径仍要明确。DSH 展示的是“动态代码加固定运行时边界”的过渡形态,距离零契约还有明显差距。

四个问题始终没有消失

沿着 DLL、OSGi、VS Code 和 DSH 往回看,插件系统一直在处理四个问题,只是问题的名字在变化。

先是准入:DLL 时代要决定加载哪个文件,OSGi 要解析 bundle,VS Code 要检查 publisher 和版本,DSH 还要加入会话归属、运行请求和人工批准。

接着是可见性:一个进程内的 DLL 几乎触碰宿主的全部地址空间;模块层和扩展宿主逐步加入包、进程、远程主机和权限边界;Agent 运行时还要决定哪些工具结果、历史消息和环境信息会进入模型上下文。

然后是能力解释:早期依赖导出符号和头文件,OSGi 依赖接口、服务属性和版本,VS Code 依赖 manifest 与 contribution points,DSH 则同时使用类型、事件、工具描述和模型可见的 session log。

最后是恢复:卸载 DLL 只释放模块,并不代表外部副作用已经撤销;OSGi 的 stop/update 需要处理依赖;VS Code 要保护编辑器主进程;DSH 则要区分工具是否已经执行、会话从哪个 checkpoint 继续,以及一个被中断的调用是否可能已经对外生效。

这四个问题决定了“契约”真正的含义。契约不只是一个接口文件,它是宿主对进入、权限、解释和恢复边界的可验证承诺。

插件宿主始终要回答的四个问题

第五时代:契约迁移到运行时

前四个阶段反复扩大了宿主需要管理的边界。本文暂把下一种可能的运行范式称为“第五时代”:插件作者主要发布能力意图、示例和约束,宿主借助模型完成能力发现、参数适配和执行编排;准入、权限、资源、状态和故障恢复则继续由宿主负责。

这意味着契约会迁移到运行时。过去,插件契约主要由函数签名、manifest、版本和注册接口组成;在这个假设中,模型可以把自然语言意图转换成候选调用,但宿主必须用 schema、策略、日志和 receipt(提交回执)把候选调用重新约束起来。以下讨论是基于前述事实的推演,不能当作 DSH 当前已经实现的能力。

模型最可能先降低的,是人类编写和维护胶水的成本;宿主同时要承担更多验证和治理工作。

这和 Skill 有什么不同?

第五时代一旦使用自然语言描述能力,读者很容易想到 Skill:两者都可能按需加载,也都把一部分约定交给模型理解。区别在于,Skill 主要回答“Agent 应该如何使用某种能力”,第五时代的插件系统还要回答“宿主如何把这种能力安全地接入运行时”。

维度第五时代的插件系统Skill
定位宿主运行时的能力扩展机制面向 Agent 的能力说明与工作流包
核心问题如何发现、加载、隔离、治理和恢复外部能力如何告诉模型何时使用、怎样使用某种能力
主要内容运行时代码、服务、权限、状态、生命周期SKILL.md、提示指令、示例、脚本和资源
执行主体宿主运行时、沙箱、远程节点或插件进程Agent 按指令调用已有工具,也可以调用附带脚本
生命周期通常需要安装、启动、停止、升级和回滚多数情况下按需加载,用完结束,不拥有完整运行时
契约形式能力描述、schema、权限、版本、事件和 receipt语义描述、触发条件、操作步骤和输入约束

Skill 与插件的职责分层

插件回答的是“系统增加了什么能力”。它通常包含运行时代码、服务、外部资源或工具入口,需要宿主处理注册、权限、生命周期、资源隔离和故障恢复。天气插件可能负责访问供应商 API、缓存数据、处理认证和限流;这些工作不能因为模型理解了几段说明就自动消失。

Skill 更接近一组面向模型的说明和工作流:什么时候触发,怎样拆解任务,调用哪些工具,输入输出应当遵守什么约束。一个天气 Skill 可以指导 Agent 识别城市、选择天气插件、统一温度单位并向用户解释数据时效,但真正访问天气服务的仍然是插件或其他工具。

Skill 可以成为插件的语义入口,插件也可以附带 Skill;前者主要改变 Agent 的决策和操作方式,后者改变宿主能够执行的运行时能力。进入执行阶段后,权限、版本、资源、状态和恢复仍属于插件运行时的责任。

语义发现先进入准入,适配器再进入热路径

一个自然语言能力说明,加上几个输入输出样例,可能帮助宿主发现插件,生成参数转换,甚至提出候选 schema。可“输入城市名,返回温度”仍然没有回答摄氏还是华氏、数据有效期多长、空值怎样处理、失败是否重试,以及一次调用是否产生计费或隐私副作用。自然语言适合做语义索引,执行语义仍需要结构化校验、权限声明和错误分类。

这条路径首先会遇到性能问题。语义检索、模型推理、动态代码编译和权限检查,都会叠加在原有调用上;如果插件被放到远程沙箱,网络往返和冷启动还会增加尾延迟。一次本来只需本地函数调用的操作,可能变成检索、推理、跨进程通信和工具执行的串联。

因此,运行时胶水更像编译器生成的中间表示,不宜成为每次调用的默认路径。宿主可以把检索放在调用前,缓存经过批准的适配器,为高频路径保留显式 schema 和预编译快速路径,并分别给模型推理与插件执行设置预算。评估时要看缓存命中率、冷启动时间和 p95、p99 延迟;否则接入成本只是被延期支付的性能债务。

适配器还要记录输入输出快照、插件版本、模型选择和生成依据。没有这些信息,同一个请求在不同时间可能得到不同转换逻辑,成功调用也无法证明下一次仍会使用相同的适配器。模型可以提出解释,宿主需要把最终执行版本固定下来。

预测提前了,解释和回滚不能省

内存增长、CPU 毛刺和失败率可以用来预测某个插件正在逼近风险边界,并在真正 OOM 之前迁移、限流或降级。这种能力适合先做隔离和资源调度,再把诊断上下文交给代码模型生成候选补丁。

直接修改线上源码则要谨慎。死循环、外部 API 变慢、锁竞争和输入规模突变,可能产生相似的观测曲线;自动插入的超时守卫既可能缓解问题,也可能改变业务语义。更可靠的流程是保存诊断证据,生成候选 diff,在影子环境和回放样本上验证,再由人或既定策略决定是否发布。

稳定性还取决于能否复现。宿主至少需要把模型版本、提示上下文、候选插件集合、适配器内容和插件提交哈希写入调用证据,并为已批准的适配器提供不可变版本。发生异常时,系统要切回上一个已验证的适配器,而不是临时再问一次模型。影子回放、金丝雀加载、熔断和降级仍然需要存在,只是测试对象从单一插件版本扩展成“模型、适配器和插件版本”的组合。

执行可以短命,资源和状态仍要结清

无状态计算可以从对象池借出资源,执行后立刻回收;跨主机调度也可能让插件看起来像在本地调用。但堆内存差分不能替代外部副作用的提交记录。写文件、发消息、创建订单和调用下游服务,都需要 receipt、幂等键、重试规则和恢复边界。

短命实例也不能自动限制子进程、文件句柄、网络连接、GPU 显存和外部服务配额。插件结束前打开的连接可能继续占用资源,远程调用也可能在宿主以为实例已销毁后才返回。每个插件都应有 CPU、内存、并发数、网络和下游配额;宿主要支持超时、取消、背压和强制终止,并区分合作式停止与进程级回收。DSH 文档已经明确指出 node:vm 不是安全边界。

动态拓扑还会让调试和重放更难。故障现场不再只是一份插件版本,系统还要知道当时加载了哪些依赖、模型看到了哪些上下文、适配器如何生成、请求被路由到哪台机器,以及外部副作用是否已经提交。session event、工具调用记录、checkpoint policy、receipt 和分布式 trace 共同构成恢复证据:从最后一个可验证边界继续,已经可能生效的外部调用不能简单重放。

分发更开放,测试和撤回更难

插件分发可能出现多种来源并存:Git 仓库和提交哈希可以提供可验证的来源,企业可以保留私有 registry,Sigstore 可以补充签名。但发现、兼容性、撤回、责任归属和恶意版本处理仍然需要索引与信任服务。代码放在 Git 上,不会自动产生生态治理。

用历史流量回放来考察新插件很有价值,但数据脱敏、输入覆盖率、外部依赖模拟和测试环境维护都需要成本。历史样本会遗漏新型输入、极端负载和插件组合故障;直接回放带副作用的生产请求,还可能制造第二个问题。更稳妥的做法是把回放限制在无副作用的影子环境,配合合成压力、故障注入和明确的退出标准。测试报告需要包含环境版本、样本范围、未覆盖路径和失败证据。

这些限制不推翻语义发现、动态适配或跨主机调度,却改变了它们进入生产的顺序:先用于检索和准入,再用于低风险路径,最后才考虑关键执行链。性能预算、稳定性证据、资源配额和可恢复边界,应该和能力描述一起成为插件契约的一部分。

动态插件运行时需要保留的四条硬边界

第五时代的契约迁移流程

结论:第五时代是契约迁移

DLL 让代码可以动态链接,OSGi 让模块和服务可以被管理,VS Code 让扩展进入多宿主和生态分发,DSH 则把动态组合带到了 Agent 的工具、模型、会话和 UI 之间。每一次能力扩张,都让契约覆盖更多边界。

AI 可能让插件作者只写一个函数和一段说明,宿主也可能即时生成适配代码、选择执行位置并预测资源风险。真正增加的工作,会落在函数之外:它能否被识别,能否被限制,出了问题能否解释,恢复时能否证明哪些副作用已经发生。

因此,我更愿意把第五时代称为“契约迁移”。标准不会凭空消失,它会从开发者提前写好的接口文件,迁移到能力描述、schema、权限、版本、事件、receipt、日志和回放样本组成的运行时证据中。模型降低了连接成本,也把验证责任推向宿主。

DSH 当前版本给出的答案很克制:动态包可以临时挂载,effect 可以回卷,模型可以通过工具探索运行时,但代码不会因为“能被模型理解”就自动获得持久化、权限和安全保证。第五时代能否成立,取决于这些边界是否清楚、可测量,并且能在插件出错后保留一条可恢复的路径。

参考资料

Keep Reading

相关文章

评论