Published on

代码越来越便宜,为什么系统却越来越难升级?

Authors
  • avatar
    Name
    hpoenixf
    Twitter

代码越来越便宜,为什么系统却越来越难升级?

从 DeepSeek Harness 的可插拔设计,到我们在 Agent 开发中逐渐形成的架构原则

一、从 0 到 1 越来越容易,从 1 到 A 却越来越难

AI Coding 正在改变软件开发的成本结构。

过去,实现一个新功能可能需要几天。现在,我们可以让 Coding Agent 快速创建模块、编写接口、补充测试,甚至完成一整套架构方案。

从 0 到 1,正在变得越来越容易。

但有趣的是,我们并没有因此觉得系统越来越容易维护。恰恰相反,随着功能越来越多,修改和升级系统反而开始变得困难。

因为真正麻烦的,往往不是从 0 到 1,而是从 1 变成 A,甚至从 A 再变回 0。

从 0 到 1,可以自由选择实现方式,历史包袱很少。先解决问题,跑通流程,就能交付一个可用版本。

从 1 到 A,就不同了。已有模块被其他代码依赖,接口成了契约,状态写入了数据库,测试固化了旧行为。修改一个看似简单的设计,可能需要协调许多已经存在的部分。

而从 A 再回到 0,可能更加困难。

假设我们曾经为了解决一个问题,增加了独立的状态管理器、校验模块或者专用执行流程。后来发现它们并没有必要,或者已有模块完全可以承担这些职责。

这时候能不能直接删除?

往往不能。

其他代码已经依赖它们的字段、状态、返回值甚至异常行为。它们可能不再承担重要工作,却已经成为系统的一部分。

最终,系统容易走上一条很熟悉的道路:

最初实现 A,发现 A 不够好,于是增加 B;为了兼容 A 和 B,又增加 Adapter;后来引入 C,却仍然保留着 A 和 B。

我们以为自己在升级架构,实际上可能只是在不断叠加新的结构。

这让我开始重新思考软件工程的三个阶段:

创造能力决定系统能发展多快,维护能力决定系统能存活多久,升级能力决定系统能走多远。

AI 降低的是代码生产成本,却不会自动消除代码进入系统后形成的依赖、兼容和维护成本。

一个功能一天就能写出来,却可能需要几周才能安全删除。这样的功能,真的便宜吗?

二、新增一个功能,其实也是新增一份长期承诺

我们过去很容易根据功能的实现成本判断它是否值得做。

一个需求只需要增加两百行代码、几个接口,看起来并没有多大代价。

但代码进入系统后,维护成本不再只由这两百行决定。

它可能增加新的持久状态,需要恢复和迁移;增加新的工具协议,需要模型学习如何使用;增加新的校验规则,需要所有调用方理解;增加新的异常分支,需要其他模块判断该继续还是停止。

更隐蔽的是,一项设计可能会逐渐变成其他开发者、测试代码乃至 Coding Agent 的默认前提。

代码可以删除,依赖它的假设却不会自动消失。

我们在 Agent 项目中就遇到过这种情况:旧的拒绝逻辑已经从实现中删除,但旧 Spec 仍然要求执行这些检查。后来再让 Coding Agent 根据规范修改代码,就可能把刚刚退役的机制重新实现出来。

因此,新增代码和新增复杂度不是一回事。

一个拥有清晰接口、能够隐藏大量内部实现的模块,即使代码很多,也可能让系统更简单。

相反,一个只有几十行的校验器,如果要求所有调用方理解它的特殊状态、错误码和处理顺序,反而可能引入很高的长期成本。

我越来越倾向于用另一个问题衡量架构复杂度:

未来要改变或者删除这个能力时,有多少无关模块必须跟着修改?

这比单纯比较代码行数、模块数量,更接近长期维护的实际代价。

三、DSH 给我们的启发:不仅要能增加,还要能替换和删除

研究 DeepSeek Harness(DSH) 时,它的可插拔架构给了我们很大启发。

DSH 基于 Cordis,将模型适配器、工具注册表、Agent Loop 等能力组织为插件。不同应用可以通过 Profile 和配置组合所需的能力,替换某个实现不一定需要修改整个系统。

更值得注意的是它的生命周期设计。

插件注册的服务、工具和事件监听,与插件实例绑定。通过 Cordis 管理的注册行为,可以在插件卸载时撤销;额外持有的资源,也可以通过 Effect 声明清理动作。

这意味着,插件体系不仅回答:

“怎样增加一项能力?”

还回答:

“如果这项能力不再需要,系统怎样撤销它的注册和资源?”

具体机制可以参考 DSH Architecture 和 Lifecycle and Effects。

不过,我们没有因此决定把自己的 Agent Runtime 全部改造成插件。

因为可卸载、可替换、可删除,其实是三个不同层次的能力。

可卸载,意味着当前运行中的注册和资源能够退出。

可替换,意味着消费者依赖稳定的公共契约,而不是某个具体实现。

可删除,则意味着一项能力退役后,原有的特殊分支、重复状态和兼容负担能够真正退出系统。

对于有持久化和外部副作用的 Agent,卸载代码并不能撤销已经发生的操作,也不能删除仍需合法读取的历史结果。

所以我们从 DSH 借鉴的,并不是“把所有函数都变成插件”,而是更底层的设计理念:

让独立能力尽可能在少量组合点引入、替换和撤退,不让它的内部复杂度扩散到整个系统。

这也改变了我们对架构升级的理解。

真正的升级,不是简单地用 B 包住 A,而是在保持必要能力的前提下,让 B 替代 A,并使 A 的维护负担最终退出。

四、从一次次教训中形成的“五不原则”

这些思考并不是我们一开始就完整建立的。

在开发 ETF Agent 的过程中,我们曾经非常积极地增加机制:需要更可靠的回答,就增加校验;担心研究不收敛,就增加控制;希望减少错误,就增加新的规则和状态。

每一项设计单独看,往往都有合理动机。

但真正投入使用后,我们发现一些新机制会制造新的失败路径,一些安全检查会意外阻断合法结果,一些重构只改变了代码位置,却没有改变职责归属。

于是,我们逐步把这些教训写进架构原则、Design Reflection 和 Code Review 规范,并将引入新复杂度的负面影响归纳为五个“不”。

第一不:不因为能够实现,就轻易新增机制

AI Coding 最大的诱惑之一,就是让增加功能变得太容易。

看到一个问题,我们很自然地想:再加一个 Manager、Validator、Reviewer 或者 Planner,是不是就能解决?

但新机制不仅有实现成本,也有后续的配置、维护、测试与退出成本。

我们曾经讨论过如何防止 Coding Agent 过度设计。完全可以再建设一个 Complexity Gate,引入独立评分器、审批记录和自动检查流程。

然而,为了防止过度设计,再设计一套复杂的治理系统,本身就值得质疑。

最后我们选择复用已有的 Design Reflection,只增加轻量提醒,而没有再创建独立的复杂度裁决引擎。

同样,研究收尾出现问题时,我们没有直接新增一个 Finalization Agent,而是先检查既有 Run Lifecycle 能否承担这个责任。

这让我们形成了一个习惯:

新增模块之前,先回答为什么不改、局部修复、复用已有能力或者调整组合仍然不够。

必要的复杂度当然应该引入。但不能因为新结构容易写出来,就忽略已有结构是否已经足够。

第二不:不让复杂度扩散到无关模块

一个好的模块,应该让调用方知道得更少,而不是更多。

我们曾经将 Publication 的相关逻辑从 Factory 中提取出来,但后续又发现,其他运行时模块仍然理解发布候选、修订和拒绝状态,甚至保留自己的判断。

这说明仅仅把代码移动到一个独立模块,并不代表封装已经完成。

后来我们继续收拢发布职责,删除外围的重复判断。

Run Termination 也类似。即使有了统一终结模块,Runtime 仍然维护着一份影子阶段状态,需要同步两边。最后我们选择深化原有模块,而不是再增加一层协调器。

这些经历让我们更重视一个问题:

如果修改某项规则,仍然需要找到多个调用方一起修改,那么这项知识真的被封装了吗?

真正有价值的抽象,不只是缩短调用代码,而是让复杂度集中在正确的位置。

我们也因此不再根据文件大小机械拆分模块。有时一个较大的深模块,比多个互相透传、彼此了解内部状态的浅模块更容易维护。

第三不:不让局部模块轻易获得全局否决权

这是我们付出过比较多工程代价才真正重视的一条原则。

为了保证结果可靠,我们曾经增加过不少校验。

但后来的真实研究与受控复现表明,一些检查逐渐获得了超出自身职责的权力。

例如,Publication 已经能够保留合法正文,外围的批次处理却可能因为另一块内容失败而抛错,导致整个结果被拒绝。

又例如,一次研究的读取额度已经耗尽,但模型请求和收尾额度仍然存在;局部工具拒绝却被传播为整项研究失败,让原本合法的收尾路径失去机会。

Stop Hook 的建议也曾能够否决完成,额外触发模型调用。

这些问题的共同点不是“错误处理不够完善”,而是:

负责局部效果的模块,开始决定整个任务的命运。

后来我们分别把正文处置权收回 Publication,把研究继续与终结权收回 Run Termination,删除外围重复的拒绝和终结判断。

但这不意味着取消必要的保护。真正的权限、隐私、内容身份、资金执行和未知副作用,仍然必须由相应的权威模块严格处理。

我们收回的是越界的决定权,而不是安全机制本身。

现在每次增加新的 throw、deny 或提前返回,都会多问一句:

它有权拒绝什么?为什么这个局部拒绝能够影响其他独立、合法的结果?

第四不:不让所有正常任务,为少数特殊情况持续付费

我们后来开始把一种容易被忽略的成本称为 Normal Path Tax——正常路径税。

例如,为复杂研究增加 Reviewer,可能确实有助于发现遗漏。

但如果每次简单问题也强制经过 Reviewer,即使它没有给出有用反馈,也可能增加模型请求、Token、延迟和失败点。

再比如,引用检查本来是为了增强回答可靠性,但如果错误引用必须触发模型修复后才能结束,合法正文也会被迫承担额外的请求成本。

有些变化甚至不需要真正触发:新增工具描述、Prompt 规则或全局检查,就可能改变模型的上下文负担和行为倾向。

所以我们逐渐学会区分:

一项能力能够工作,不代表它应该默认参与所有任务。

新增可选专家、Reviewer、诊断或研究增强时,除了证明功能有效,还需要考虑它在普通路径上有没有额外代价,以及是否值得按任务条件启用。

其中模型质量、成本和延迟的净收益,需要真实对照才能下结论;不能只因为模块成功执行,就宣称系统效率提高。

好的可选能力,应该尽量让真正使用它的任务承担成本,而不是让所有任务承担永久成本。

第五不:不引入没有退出条件的能力

这一条与 DSH 的启发直接相关,也来自我们反复删除旧机制的经历。

以前新增能力时,我们更关注它怎样接入系统、怎样通过测试。

现在还会提前思考:如果以后发现它不再必要,应该怎样退出?

例如发布治理并不只是让新的 Publication 校验通过。我们还删除了外围重复过滤、强制修复队列及部分旧规则,防止它们继续决定正文能否存在。

而且,仅删除生产代码仍然不够。我们还发现旧 Spec 可能要求未来的 Agent 重新实现退役规则,因此也必须处理那些不再成立的历史契约。

同样,对于插件和可选子代理,我们会考虑停用新增准入后,普通研究是否还能继续,已经接受的任务如何安全结算,历史结果是否仍可读取。

这里不要求所有模块都实现热插拔,也不主张为假想的替代者提前创建一大堆 Interface 和 Factory。

它要求的是:

如果一项能力具有独立演化的必要性,就尽量让它的引入、替换和撤退不影响无关模块。

可删除并不等于可以随意清空状态。真正已经发生的业务事实和用户已经获得的合法结果,仍然需要由原有责任方维护。

我们想删除的是不再需要的机制和依赖,而不是历史本身。

五、原则也会增加复杂度,所以不能把“五不”变成五个新 Gate

这里还有一个有趣的悖论。

为了避免过度设计,我们提出了五条原则。是不是又应该开发五个独立的检查器,确保所有新功能都符合要求?

如果真的这样做,我们很可能又走回了原来的道路。

因此,这些原则首先是架构设计与 Code Review 的判断方法,而不是五个新的 Runtime 控制模块。

我们通过已有的 Design Reflection 讨论事实、职责、故障、价值与退出,不为每个判断重新创建一份状态和审批记录。

同样,也不是所有决策都值得做 A/B 测试。

对于重复状态删除、权威收拢、确定性契约修复,我们可以依靠明确的架构不变量、代码审查和相关行为回归,验证实现是否符合设计。

对于 Prompt、Reviewer、Subagent 策略等依赖模型行为的变化,则需要根据不确定性、错误代价和潜在收益,决定是否进行定向测评或正式对照。

验证本身也有成本。

能通过确定性推理解决的问题,就不必强行进行统计实验;真正依赖经验结果的判断,也不能只靠直觉宣称有效。

这不是放松质量,而是让验证投入与问题本身相匹配。

还有一件事也必须承认:简化不是唯一目标。

必要的授权、持久化、来源绑定、资源记账和安全保护,可能确实需要复杂的实现。把它们为了减少代码而删除,并不意味着架构更优秀。

好的架构并不是功能最少、模块最少或代码最短,而是让每一份必要复杂度待在正确的位置,不让可选能力的代价无限扩散。

六、从创造更多,转向能够持续更新自己

回过头看,AI Coding 最明显的改变,是让我们更容易创造新东西。

但随着这种能力越来越充裕,我反而越来越觉得,优秀工程师真正需要加强的,是另外几种判断能力。

首先是判断什么不值得做。

并不是每一次失败都需要新机制,也不是每一个潜在需求都值得提前设计通用框架。

其次是判断什么应该保留。

必要的安全、状态和业务规则不能因为复杂就被删除。好的模块应该封装这些复杂度,而不是把它们推给所有调用方。

最后是判断什么应该退出。

当新设计已经承担旧设计的职责时,我们能否删除重复状态、无效规则和多余的兼容路径,而不是永远同时维护两套方案?

这些问题没有统一的答案,却越来越成为我们日常架构评审中的重要部分。

我们从 DSH 学到了能力的组合、替换与生命周期退出,又通过自己的 Publication、Run Termination、Extension Host 和设计治理实践,逐渐认识到:可插拔并不是目的,可持续演化才是目的。

而真正的架构升级,不是叠加新机制,而是在保持必要能力的前提下,用更合适的结构替代旧结构。


从 0 到 1,考验的是创造能力。

从 1 到 A,考验的是维护与演化能力。

从 A 再回到 0,考验的则是能否摆脱已经不再必要的历史复杂度。

AI 正在让第一种能力越来越便宜,但后两种能力,并不会随着代码生成速度提升而自动获得。

创造能力决定系统能发展多快,维护能力决定系统能存活多久,升级能力决定系统能走多远。

一个系统真正的长期竞争力,不在于它能够无限增加多少功能,而在于它能否不断更新自己,又不被历史复杂度拖垮。

我们不仅要有创造新系统的能力,也要有让旧系统体面退出的能力。

延伸阅读与交流

更多独立文章见 ETF Agent 技术阅读站 与 GitHub 公开文章仓库。如果内容有帮助,欢迎给文章仓库点个 Star。