Published on

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

Authors
  • avatar
    Name
    hpoenixf
    Twitter

一个报表系统已经能生成正文。现在想增加图表附件,AI 很快写好了画图函数。接上去才发现:页面要判断画图状态,保存接口要认识图表错误,定时任务要等待附件,所有报表还都增加了一次检查。

后来换一套画图库,这些地方又得一起改。最初那段函数不长,围绕它形成的约定却已经散到了整个系统。

这是一个教学模拟,业务和情节用于解释架构取舍,不对应 ETF Agent 的一次真实报表事故。它提出的问题很实际:新增功能的代码容易写,调用方增加的知识、正常任务付出的代价,以及将来的退出工作,同样要有人承担。

判断一项能力是否便宜,应该把这些工作一起算进去。

一、先看谁被迫知道了图表的内部规则

最初,为了尽快上线,开发者在几个入口各加一段判断,确实可能是合理选择。只有一个页面、一个调用方时,改动不多,也容易看清。

后来,报表增加了定时生成和重新下载。每个入口都复制了类似的规则:图表排队时等一等,渲染失败时重试,附件没准备好时整份报表暂时不可用。

再把画图代码移进一个新目录,问题也不会自动消失。页面和任务仍然知道它的内部状态;改变一次重试规则,依然要改好几个地方。

有没有封装好,要看调用者少知道了什么。

在这个例子里,先明确正文和图表的关系:正文是独立可用的结果,图表是可选附件。如果这就是产品承诺,调用方只需要拿到正文和附件结果,不必各自解释渲染器经历了几个阶段。图表模块处理它自己的准备、失败和清理,报表模块决定如何展示已经得到的结果。

反过来,如果某种报告必须包含法定图表才完整,就不能把它假装成可选项。边界来自真实承诺,不能为了让代码好看而改掉承诺。

下一次评审抽象时,可以直接问:更换这项实现,还需要哪些无关调用方跟着理解新规则?如果答案仍是一长串,移动文件只是开始。

二、一个附件失败,为什么整份报表消失了

假设正文已经生成,图表渲染器却不支持某个字体。最省事的写法是向上抛出异常,所有调用方都收到“报表失败”。

这让错误处理看起来统一,却扩大了故障影响。用户本来可以先阅读正文,现在连正文也没有了。

对于这里约定的可选图表,合适的结果应是:正文继续可读;附件位置说明图表暂不可用;后续重试只针对附件。保存失败仍然要如实报告,不能把“生成过正文”写成“保存成功”。

这也有明确边界。如果正文确实依赖未经授权的私人材料,不能删掉引用或去掉附件,就宣称正文可以公开。真正的权限和内容依赖仍须由原来负责它们的模块处理。

ETF Agent 的发布治理提供过相近的真实教训:发布模块能够保留独立合法正文,外围候选过滤和同批异常却仍可能提前把它丢掉。仅修发布函数不够,还要查实际入口有没有在到达它之前作出第二次拒绝。具体机制留在发布专文。

由此得到的行动很具体:遇到异常传播,沿调用链找出哪一处把“图表失败”改写成了“整份报表失败”,让拒绝停在它实际负责的效果范围内,而不是再增加一个总协调器。

三、可选功能,即使没有使用,也可能让所有任务变慢

继续看图表功能。为了统一流程,系统每次都创建画图任务,随后才检查用户是否需要附件。即使最终跳过画图,任务登记、状态查询和额外依赖也已经发生。

在 Agent 中,同类成本还可能出现在提示词和工具说明里:某个能力一次都没调用,它的定义仍可能随每轮请求输入模型。

因此,评估可选功能时,至少区分三种情况:

情况应检查什么
功能尚未接入原来的普通报表路径是什么
功能已接入,但本次不使用是否仍新增任务、查询、上下文或等待
本次确实使用功能得到什么结果,付出多少额外代价

在教学系统里,可以让选择图表的任务才进入渲染流程。普通正文报表继续走原有路径。是否减少了耗时,要用可比输入观察,不能只凭多了一个开关就宣布更快。

同样,也不能为了省输入而悄悄删掉本轮已准入的合法工具,或缺省略去必要保护。减少无关负担与缩减已经承诺的能力,是两个不同决定。

四、替换完成的标志,是旧东西不再要求你维护

现在要把旧画图库换成新库。一种容易出现的“升级”是:新增一层包装器,先试新库,失败再调旧库;两个返回格式都保留,页面继续判断两套状态。

这可能是必要的过渡,但如果没有结束条件,系统就要长期维护两套实现和中间的兼容规则。

在既有迁移任务中写清楚退出条件,比再建一套治理系统更有帮助。例如:新库已经覆盖仍在使用的图表类型;新任务不再调用旧库;仍需打开的历史文件可以读取;没有在途任务继续依赖旧格式。满足条件后,删除旧接线、旧依赖以及只服务旧库的状态判断。

历史图片是否继续保留,由原有保存和删除约定决定。删除旧渲染器,并不等于可以删除用户仍有权读取的文件。

还要查一眼当前说明和测试。若规范仍要求调用旧库、测试仍锁定旧状态名,下一位开发者或 Coding Agent 可能会把旧机制重新补回来。历史记录可以解释当时为何这样设计,现行规则则应指导当前实现。

五、可卸载、可替换、可删除,解决的是不同问题

DeepSeek Harness 的架构文档展示了另一种组织方式:以 Cordis 插件组合模型、工具和执行循环;生命周期文档说明,受框架管理的注册会随插件卸载撤销,框架外资源则需要声明清理动作。

这给我们的启发是提前考虑能力如何退出,而不是要求所有系统都改成插件。对报表例子,可以分别问三个问题:

  • 停用时能否清理? 定时器、监听和临时资源是否还在运行?
  • 更换时能否隔离? 调用方是否仍可使用稳定的结果约定?
  • 退役时能否删除? 旧分支、旧依赖和过期要求是否真的能退出?

三个答案不会自动相同。清理定时器不能撤回已经发送的文件,更换实现也不能抹掉实际操作记录。没有真实替换需求时,也不必预建复杂的热插拔体系。

六、把“五不”写回一次普通评审

我们把这类取舍归纳为“五不”。它们适合变成评审里的具体问题:

原则在图表案例里怎么检查
不因容易实现就新增机制先说明原报表模块为什么不能局部解决问题
不让复杂度扩散页面和定时任务是否还要解释渲染器的内部状态
不扩大局部否决权可选附件失败,是否误伤独立可用的正文
不让普通任务持续付费不要图表时,是否仍创建渲染任务或增加等待
不留下无法退出的能力新实现接手后,哪些旧依赖和要求会被删除

不需要再为五行各建一个检查器。规则本身也有维护成本,放回已有设计和代码评审即可。

下一次接到“增加一个图表附件”,任务里可以写:明确正文与附件的交付关系;将渲染内部状态留在图表模块;从页面验证附件失败时的正文行为;比较不使用图表的普通路径;若替换旧库,列出仍在使用的入口和退出条件。

这已经足够让开发者开始工作,也让评审能判断设计有没有减少依赖。

代码生产得更快以后,值得追问的是:这次新增能力,有没有让下一次修改更容易? 能力集中在少数清楚的入口,失败影响准确,旧负担有机会退出,系统才更有可能持续升级。

延伸阅读与交流

更多文章见 ETF Agent 技术阅读站 和 GitHub 公开文集。如果内容有所帮助,欢迎 Star 或提供反例交流。