
EP01 做家庭日历,往核心加了 LLM Pool 回退逻辑和设计令牌(design token)扫描。EP02 做教育工具,往核心加了多租户 schema 处理和 OAuth provider 路由。两个产品做完,核心流水线从 800 行长到 2000 行——每一行都有道理,但凑在一起就是一团绞在一起的意大利面。
改核心的恐惧很具体:每次在约束工程(harness)里加一段领域逻辑,都担心会影响其他流程。教育工具的 migration 检查和家庭日历的设计令牌扫描共享同一个启动钩子(startup hook)——改一个,另一个有可能断。而且这种耦合是隐式的:代码里没有一行写着”这两个功能有依赖关系”,你只有在改坏之后才发现它们共享了同一个执行路径。隐式耦合比显式依赖难处理得多——你无法通过搜索 import 来找到它。
EP04 看了 Cline 的 3764 行编排器,心安了一些:上帝文件在编排器场景下是可接受的。但 OPC 的膨胀方向不对——Cline 的 3764 行是编排逻辑,OPC 的 2000 行里有三分之一是领域逻辑。区别在于变化频率:编排逻辑在架构稳定后很少改,但领域逻辑随每个新产品不断变化。把高频变化的代码和低频变化的代码混在一个文件里,意味着每次改领域逻辑都要承担误伤编排逻辑的风险。编排器可以大,但它不该装领域知识。
OPC 同时扮演两个角色:通用的多角色任务编排引擎,和特定领域工作流的载体。 两个角色的代码混在一起,是时候拆了。拆的方向不是重写——是画一条线,让两个角色各自演进而互不干扰。
问题的本质是产品层面的:每做一个新产品,它的特殊逻辑就污染工具核心。 两个产品还能忍,第三个产品进来时改核心的恐惧会大于做产品的兴奋。这是一个典型的技术债务加速度问题:不是线性增长,而是每增加一个产品,现有产品之间的交叉影响面就多出 N-1 条。三个产品就有三对潜在冲突,四个产品就有六对。下面三个方案都是在解这个问题。
三个方案
方案 A:核心 + 插件系统。标准做法——定义插件接口,外部代码实现接口,运行时动态加载。EP04 看到的 LobeHub(100 行写一个 Piece)和 Activepieces(30 行加 SDK 学习成本)都是这条路。但插件系统的维护成本太高:生命周期管理、版本兼容、API 稳定性保证、发现和注册机制。对只有一个用户的工具来说,这些基础设施全是浪费。你为了解耦付出的成本,比耦合本身还高——这不是简化,是搬迁复杂度。
方案 B:单体加配置层。核心不拆,但用配置文件控制哪些功能开启。简单,但治标不治本——配置越加越多,最终变成另一种形式的膨胀。配置层的问题是它改变了激活方式,但没改变代码组织。所有领域逻辑还是在同一个文件里,只是被 if-else 包裹。到第五个产品时,你的配置文件会比核心代码还难读。
方案 C:核心开源 + 私有扩展。核心只做编排,领域逻辑写成独立的扩展模块。扩展不是插件——不需要动态加载、版本管理、注册表那套。它只是在固定的接触点上和核心对话。
最终走了 B+C 混合体:核心保持开源,编排逻辑留在一个文件里。流程定义分内置和扩展两类,唯一耦合点是流程定义规范和五个固定的钩子接口。选择固定五个钩子而不是开放式的事件总线,是因为钩子数量越少,合约越容易维护——扩展作者不需要理解事件拓扑就能写出正确的代码。
能力合约:两边互不认识
核心和扩展之间的解耦靠能力合约(capability contract)实现。S1E04 讲过早期版本——“关节是扩展点,骨头中间不能挂肌肉”。v0.5 把它正式化。
设计原则:两边不直接认识对方,只认识一个共享词汇表。 流程节点声明”我需要 visual-consistency-check 这种能力”,扩展声明”我提供 visual-consistency-check”。交集非空就触发,否则静默跳过。加一个新流程模板声明需要某种能力,所有提供这个能力的扩展自动在上面运行,不用改一行扩展代码。反过来也成立:加一个新扩展声明提供某种能力,所有需要这个能力的流程模板自动受益,不用改一行流程定义。双向解耦——任何一方的变化都不会强制另一方改动。
这解决了最核心的问题:加一个新产品不需要改核心。 产品的领域逻辑写成扩展,声明自己提供的能力。核心只负责在合适的时机问”谁能做这件事”,然后把活分出去。如果没有任何扩展响应,流程正常继续——不报错,不中断,只是那个能力点没有被行使。这意味着你可以安全地移除一个扩展,流水线不会因此崩溃。
五种钩子
扩展通过五种钩子切入流水线的不同阶段:
启动时验证前置条件——检查依赖是否就位、配置是否完整。如果前置条件不满足,扩展主动退出,不拖慢流水线。
分发前注入上下文——在审查者收到任务之前,向提示词注入领域特定的评分标准。比如 Design Intelligence 在这个阶段注入设计令牌和视觉一致性评分维度。
审查时提交机械化审查发现——扩展自己做一轮机械检查(不靠大语言模型),把结果作为额外的审查发现提交给门禁。机械检查和 LLM 审查分开的原因是确定性:正则匹配、schema 校验、命名规范扫描这些检查的结果是二值的(通过或不通过),不需要 LLM 的概率判断,也不应该受到 prompt 漂移的影响。
执行时跑副作用——截图、性能测量、数据收集。这些操作有副作用,所以放在独立阶段而非审查阶段。把副作用和审查分开还有一个实际原因:审查阶段的结果会影响门禁判断(通过或拒绝),但副作用的执行结果不应该影响门禁。截图失败不是代码质量问题——不能因为截图服务超时就阻塞代码合入。
完成后生成输出文件——汇总报告、可视化、归档。
五种钩子覆盖了流水线的全生命周期。扩展只通过钩子和能力声明与核心交互,不碰核心的执行逻辑。 这条规则没有例外——如果一个扩展需要读取或修改核心的内部状态,说明它不应该是扩展,而应该合并进核心。划这条线的原因是:一旦允许扩展访问核心内部状态,核心的任何重构都会变成一次扩展的全面审计。合约的价值正是通过限制接触面来保障双方的独立演进。
熔断器和那个没修好的 bug
扩展跑在流水线里,一个坏的扩展可能拖死整个流程。逐扩展熔断器(circuit breaker)是安全网:连续 3 次失败就在本次流程中被禁用,任何一次成功重置计数器。
v0.5 发布后第二天就出了 bug:熔断器状态只在内存里,每次 CLI 调用都重置。 OPC 每个 tick 是一次 CLI 调用——一个反复崩溃的扩展,每次 tick 都拿到三次新机会。熔断器形同虚设。这意味着一个在启动阶段就崩溃的扩展可以无限重试,每次都浪费启动时间,拖慢整条流水线但永远不会被禁用。
这个 bug 的本质是”无状态 CLI 和有状态机制”之间的矛盾。CLI 天然无状态——启动、执行、退出。熔断器需要跨调用记住失败次数。v0.5.1 把熔断状态持久化到 JSON 文件,按扩展 ID 和流程 ID 索引。修好之后回头看,这是一个本该在设计时就预见到的问题——但做的时候脑子里只有”熔断器要连续计数”,忘了”连续”在无状态 CLI 里不是免费的。这个经验适用于所有 CLI 工具中的有状态机制:如果你的逻辑依赖”上一次运行的结果”,你必须显式设计持久化层,否则每次启动都是失忆的。
从 v0.5 到 v0.8
四轮 hardening。每一轮不是加新功能,而是加清晰度。
v0.5 首发扩展接口。能力合约、五种钩子、熔断器。
v0.5.1 修了 7 个缺陷。最关键的是熔断器持久化。其次是能力匹配的前缀回退:visual-consistency-check-v2 能匹配到声明了 visual-consistency-check 的扩展。前缀回退让能力声明有了版本演进的空间——新版扩展可以细化能力名称,同时兼容旧流程模板。
v0.6 五个生产扩展同时挂载:设计规范检查(design-lint)、视觉评估(visual-eval)、知识回忆(memex-recall)、变更集审查(git-changeset-review)、会话日志提取(session-logex)。五个扩展共存是对能力合约的真实压力测试——它们的能力声明不能冲突,钩子执行顺序不能互相干扰。一个扩展在审查阶段提交的发现不能覆盖另一个扩展的发现;两个扩展都声明”启动时验证”不能因为一个失败就阻止另一个运行。这些边界条件在单扩展测试里根本不会出现。
v0.7 验证了”外人”能不能只看文档写出一个完整扩展。7800 字的 extension-authoring.md,一个 121 行的起步模板,一个 70 行的参考扩展——后者是一个”外人”智能体只读文档写出来的,零次查看核心源码。
能写,但暴露了 5 个开发者体验缺陷。严重度到 emoji 的映射表埋在文档中段,新手找不到。ctx.task 可能是 string 也可能是 object,文档没说——这类类型歧义在动态语言里是常见陷阱,但文档里不标注就是作者的失职。能力名字的合法列表在哪?——需要一个注册表。
这里有一个需要面对的问题:这个”外人”是另一个 AI 智能体,不是人类开发者。AI 有无限耐心,不会在第三个文档坑之后关掉浏览器。如果测试对象换成人类开发者,缺陷数量可能翻倍。用 AI 测试 AI 工具的文档,是闭环内验证——必要条件,不是充分条件。
v0.8 关闭遗留摩擦项,新增运行手册(runbook)机制。到 v0.8 为止,258 个测试全绿。从 v0.5 到 v0.8 没有加一个新的用户可见功能——全部精力花在让已有功能的行为可预测、边界条件有覆盖、失败模式有保护。这段时间没有任何产品产出的压力。但回头看,这四轮 hardening 的投入回报比可能是整个 S2 最高的——后面做的每一个新扩展都不需要调试框架本身。
约定优于配置
EP04 看到 LangChain 67.7k 行核心的代价——标准越通用,核心越重。OPC 选了反面:约定优于配置(convention over configuration),不做抽象层。
没有 Runnable 接口,没有通用流水线 SDK,没有插件 API 版本管理。流程定义就是一个 JSON schema 加一组约定。扩展的钩子签名是固定的五种,不支持自定义钩子类型。
这限制了灵活性——无法发明新的钩子点,无法在流水线中间插入自定义阶段。但换来的是:核心代码量小(相对 LangChain 少一个数量级),新手看 extension-authoring.md 就能写扩展,不用先理解一个抽象类型系统。约定的代价是更高的纪律要求——你不能绕过约定做”聪明”的事。但这个代价在单人工具场景下几乎为零,因为约定的制定者和执行者是同一个人。
EP04 的插件困难三角(开发者体验、安全、发现性)也帮助锁定了位置。OPC 是单人工具,扩展作者就是用户自己。所以开发者体验优先,安全靠信任边界(信任边界 = 用户边界),发现性暂不做(就一个用户,不需要市场)。这个取舍在多人场景下大概率会翻车——第二个用户出现的那天,你得补上沙箱隔离、权限模型、API 版本管理——但对只有一个用户的工具来说,不值得为假设的第二个用户提前付安全成本。提前为不存在的用户做抽象,是用今天的工程时间赌明天的需求——YAGNI 在这里完全适用。
扩展系统让核心不再胖。核心的职责从”编排 + 领域”收缩到”只编排”,行数回到可控范围。但 EP06 会发现,有些审查能力不是加一个代码层面的扩展就能解决的——当问题出在视觉层面,代码审查者看不到。
基础设施工作的本质:修好的时候没人鼓掌,没修的时候所有人骂。
硅基团队 S2: 在实战中进化工具链 ← S2E04: 动手之前先看别人踩过的坑 | S2E06: 三个审查者没一个看出颜色不对 →