注册并分享邀请链接,可获得视频播放与邀请奖励。

Jason Meng (@desenmeng) “我支持内部尽可能开放,外部有意识地限制。平台应该有自己的判断,而不是把所有可能性” — TopicDigg

Jason Meng 的个人资料封面
Jason Meng 的头像
Jason Meng
@desenmeng
加入 June 2011
0 正在关注    0 粉丝
我支持内部尽可能开放,外部有意识地限制。平台应该有自己的判断,而不是把所有可能性原样交给插件作者和用户。 “一切皆插件”作为内部架构原则,我非常认同。通用引擎最怕的就是不断被具体产品的需求侵蚀:今天加一个特例,明天再补一层判断,最后虽然还叫通用引擎,实际上已经只能服务当前这个产品。其他人想做不同的东西,只能 fork,然后各自维护一套。 所以 DSH 希望把 Agent Loop、Session、Tool Runtime、LLM、Persistence、Sandbox 甚至 UI 都变成可以重新组合的部分,这个方向是有价值的。Harness 还处在快速变化期,现在也没有哪套实现已经被证明是最终答案。底层如果太早写死,后面想换一种上下文组织方式、Session 模型或者 Agent Loop,都会很难。 但我不认为,由此就能推出“所有内部逻辑都应该成为第三方扩展点”。 把 Pi、VS Code 和 DSH 放在一起看,区别其实不在于谁有插件冲突。Pi Extension 一样可以注册同名 Tool、修改 Prompt、拦截同一个事件;VS Code 里也一直有格式化器、快捷键和语言服务互相影响的问题。只要多个扩展可以同时运行,命名、顺序、优先级和兼容性就一定会出现。 真正的区别,是插件可以改到哪一层,以及平台有没有提前规定这些变化怎么组合。 VS Code 的做法是,核心仍然由平台控制,第三方主要通过 Contribution Points 和稳定接口增加能力。某个扩展点允许多个 Provider 时,宿主决定结果怎么合并;某个能力只能有一个实现时,就让用户选择;名字必须唯一的地方,就在注册时处理。它当然不能消灭冲突,但它尽量把冲突限制在已经定义好的范围里。 Pi 更开放一些。Extension 可以注册 Tool 和 Command、修改上下文、监听或拦截 Agent 的运行过程,甚至通过同名注册覆盖内置 Tool。但它仍然有一个相对固定的 coding-agent core。Agent Loop、Session Runtime 等部分主要由宿主持有,所以 Extension 大多还是围绕同一个内核运行。 DSH 再往下走了一层。它不是在固定内核周围增加能力,而是让插件共同组装内核本身。Session、Agent Loop、Tool Runtime、Persistence、Sandbox 都可能来自不同插件。 这种设计的自由度确实更高,但需要处理的问题也不再只是“两个 Tool 重名”。 DSH 实际上同时存在几套身份:npm/module instance、Cordis service 或 Symbol、loader entry、Tool/Command/Route/Prompt 的名字,以及 host/preset/agent/session 的 Scope。同一个插件在配置里看起来只装了一次,不代表运行时一定只有一个 module instance;module instance 相同,也不代表 registry、loader 和 Scope 对它的理解一致。再加上 reload 和 generation 切换,问题还会沿着时间继续展开。 这不是纯理论问题。至少在 rc.6 的社区运行时报告里,已经出现过双实例导致 Symbol 分裂、重复 loader entry、process-global registry 冲突、late provider 工具重名,以及旧 generation 没有完全 dispose 等情况。这里需要强调,这些是社区报告,不等于维护者已经确认了同一个根因;但它们足以说明,DSH 要处理的是一整套运行时身份和生命周期问题,不只是普通的插件重名。 Cordis 能很好地处理 activation、依赖等待、effect disposal 和 Scope visibility,但这些机制本身并不会决定:重复注册应该 reject、replace、merge 还是 shadow;谁拥有一个名字;两个实现谁优先;安装失败以后怎么回滚;旧 generation 什么时候才算真正退出。 这些选择最后还是要由平台来做。 Effect 也是一样。它能把依赖和副作用表达得更清楚,让资源释放、重试、并发和 TestClock 变得更容易测试。这是很大的价值,我完全认同。 但能够静态知道一段代码访问了文件、网络或者数据库,并不等于系统已经知道两个插件发生冲突时应该听谁的。知道“发生了什么”和决定“应该怎么办”,是两件事情。 外部操作尤其明显。消息已经发送、文件已经被读取、支付已经完成、货已经发出,这些事情不会因为插件 dispose 就自动消失。框架可以让它们更容易追踪,也可以强迫开发者把补偿逻辑写出来,但具体是重试、补偿、对账还是人工处理,仍然是产品选择。 所以“不 fork 主干”当然是个很好的目标,但它不会让不同需求之间的分歧消失。以前这些分歧存在于代码分支里,插件化以后,它们会存在于 Profile、Bundle、Scope、插件版本和加载顺序的不同组合里。 这通常仍然比长期维护多个 fork 更好,但维护成本只是换了位置。以前处理 merge 和升级,现在处理依赖、身份、生命周期、状态兼容和组合测试。 AI Coding 可以降低写 Adapter、迁移接口和补兼容层的成本,却不会自动决定两个插件冲突时哪一种行为才符合产品意图。代码越来越容易生成以后,平台自己的判断反而会变得更重要。 这也是我为什么更支持“内部开放,外部限制”。 平台不应该只是一个中性的插件容器。它首先应该知道自己要做成什么产品,然后决定哪些变化可以交给第三方,哪些行为必须保持一致。开放一个稳定接口,不只是让别人能够调用它,也意味着平台准备长期维护它的语义、升级方式和失败处理。 如果平台对什么都不做判断,默认所有逻辑都能被第三方改写,那么冲突出现以后,用户只能自己判断应该装哪个插件、谁覆盖谁、哪个版本能一起工作。这个结果不是更开放,而是平台把集成工作转交给了用户。 我的选择会很明确: 内部实现可以尽量模块化,Agent Loop、Session、Persistence、Sandbox 都应该有替换空间。这样核心团队自己做实验、切换实现或者服务不同产品时,不需要不断侵入底层代码。 但面向普通第三方时,应该只开放平台已经想清楚的部分。Tool、模型 Provider、Command、Prompt、上下文来源和 UI Contribution,可以较早形成稳定接口;Agent Loop、Session、Sandbox、Persistence 仍然可以允许替换,但先放在 experimental 或 proposed 层,不急着承诺跨版本兼容。 真正需要完全控制 Runtime 的团队,可以进入更底层的 distribution 层,自己固定插件版本,并测试 install、boot、reload、dispose 和 resume。到了这一层,他做的已经不只是写一个插件,而是在维护自己的发行版,自然也应该承担对应的集成责任。 所以我不反对“一切皆插件”。我反对的是把内部可替换性直接等同于外部无限扩展。 内部越开放,平台越有空间继续演化;外部越克制,第三方越容易知道哪些东西真的可以依赖。这两件事并不矛盾。 一个好的平台不只是提供可能性,也要敢于做选择。什么应该允许扩展,什么应该保持统一,什么现在还没想清楚、只能作为实验接口存在——这些判断本身,就是平台应该提供的价值。
显示更多
我可以讲清楚这种启发具体是什么。 所谓 “一切皆插件” 至少有三四种不同的架构含义,很多人会将其误解为类似 Webpack 和 Pi 中的 hook 模式,或 VSCode 中在主创团队决策下开放给第三方的插件场景。 然而我对 DSH 插件架构要解决的问题的理解,其实还是来源于当年的 AFFiNE。 那时,虽然 AFFiNE 在产品功能上相当于 OpenNotion,但我们非常希望架构要完全可定制,理想中任意需求最好都能不 fork 我们主干就能由第三方实现(请就这句话联想一下 harness 场景)。因为在我们的融资叙事里,AFFiNE 的长期愿景是成为一个通用的应用开发底座。AFFiNE 内部我负责的 BlockSuite 编辑引擎也遇到同样的问题。满足短期需求的产品功能快速侵入通用引擎,会让技术团队对通用性愿景逐渐失去信心。 因此,当时 @EEEEYHN 设计实现了 Affine Framework,而 @mirone_saul 在 BlockSuite 中也做了类似的工作。其基本思路是将 DI 和 rxjs 结合来管理业务对象的生命周期,从而只要基于这套架构的逻辑,都是原理上能运行时动态替换的——很高兴看到今天 DeepSeek 甚至就此给出了数学证明! 重复一遍,这种插件模式的重点在于运行时任意扩展的理论可能性。其精髓不同于 VSCode 那样人工决定“什么能被第三方扩展”,而是任意逻辑都默认能被第三方扩展。 这项工作不存在于 AFFiNE 的公开文档中,因为项目后期我们已经无暇顾及开发者业务的叙事。且这种架构也会让代码显著地更为晦涩,在古法编程时代会拖慢开发效率。有兴趣的朋友欢迎用 codex 去考古。 并且,我也不觉得我有资格声称“DSH 的理念早已被我们掌握”,因为 DSH 是第一个做到在 day one 就将我们未竟的理念贯彻为项目中一等公民的产品。这种技术方向上的产品化可能性,就是对我最大的启发。 在离开 AFFiNE 加入 Lovart 后,我注意到了 Prisma 作者 Johannes 对 @EffectTS_ 的称赞。如果你将手搓 DSH 插件架构的思路做到极致,那么基本上你就会重新得到 Effect。只要把它的 layer 理解成 provider, 把 tag 理解成 service key 就够了。 充分 Effect 化的代码里没有 setTimeout,能用 TestClock 做确定性模拟测试,从而将 E2E 墙钟时间压缩到单测时间。我认为这对提高软件可验证性有极大价值,甚至单独约过会向 Melvin 老板和 CTO 介绍过这项技术,然而也确实感觉没人能听懂我在说什么…… 现在回头看,我觉得 AFFiNE 有点像是个 General Magic 式的组织,它确实尝试过在一个宏大理想的 umbrella 下汇聚很多领域内最优秀的人各自的小理想。 在 AFFiNE,我有幸光明正大地烧投资人的钱,专门去研究通用编辑引擎如何从第一性原理出发设计架构。这些 know how 并不是局外人“那样肯定搞不成”可以一笔带过的。 GM 最后没有跑出来,但安卓、iPod、iPhone、Flash,其实都是技术条件成熟后 GM 团队当年理想的开枝散叶。AFFiNE 团队的核心成员去了 Manus、VoidZero 等地方,我也感觉是在追寻自己多年前的影子。像 PocketJS 能不能成为新一代的 Flash 呢? 现在像是来到了十年前前端框架大乱斗风起云涌的年代,在这个技术奇点加速开蹬吧!
显示更多