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

- Name
- hpoenixf
一个报表系统已经能生成正文。现在想增加图表附件,AI 很快写好了画图函数。接上去才发现:页面要判断画图状态,保存接口要认识图表错误,定时任务要等待附件,所有报表还都增加了一次检查。
后来换一套画图库,这些地方又得一起改。最初那段函数不长,围绕它形成的约定却已经散到了整个系统。
这是一个教学模拟,业务和情节用于解释架构取舍,不对应 ETF Agent 的一次真实报表事故。它提出的问题很实际:新增功能的代码容易写,调用方增加的知识、正常任务付出的代价,以及将来的退出工作,同样要有人承担。
判断一项能力是否便宜,应该把这些工作一起算进去。
一、先看谁被迫知道了图表的内部规则
最初,为了尽快上线,开发者在几个入口各加一段判断,确实可能是合理选择。只有一个页面、一个调用方时,改动不多,也容易看清。
后来,报表增加了定时生成和重新下载。每个入口都复制了类似的规则:图表排队时等一等,渲染失败时重试,附件没准备好时整份报表暂时不可用。
再把画图代码移进一个新目录,问题也不会自动消失。页面和任务仍然知道它的内部状态;改变一次重试规则,依然要改好几个地方。
有没有封装好,要看调用者少知道了什么。
在这个例子里,先明确正文和图表的关系:正文是独立可用的结果,图表是可选附件。如果这就是产品承诺,调用方只需要拿到正文和附件结果,不必各自解释渲染器经历了几个阶段。图表模块处理它自己的准备、失败和清理,报表模块决定如何展示已经得到的结果。
反过来,如果某种报告必须包含法定图表才完整,就不能把它假装成可选项。边界来自真实承诺,不能为了让代码好看而改掉承诺。
下一次评审抽象时,可以直接问:更换这项实现,还需要哪些无关调用方跟着理解新规则?如果答案仍是一长串,移动文件只是开始。
二、一个附件失败,为什么整份报表消失了
假设正文已经生成,图表渲染器却不支持某个字体。最省事的写法是向上抛出异常,所有调用方都收到“报表失败”。
这让错误处理看起来统一,却扩大了故障影响。用户本来可以先阅读正文,现在连正文也没有了。
对于这里约定的可选图表,合适的结果应是:正文继续可读;附件位置说明图表暂不可用;后续重试只针对附件。保存失败仍然要如实报告,不能把“生成过正文”写成“保存成功”。
这也有明确边界。如果正文确实依赖未经授权的私人材料,不能删掉引用或去掉附件,就宣称正文可以公开。真正的权限和内容依赖仍须由原来负责它们的模块处理。
ETF Agent 的发布治理提供过相近的真实教训:发布模块能够保留独立合法正文,外围候选过滤和同批异常却仍可能提前把它丢掉。仅修发布函数不够,还要查实际入口有没有在到达它之前作出第二次拒绝。具体机制留在发布专文。
由此得到的行动很具体:遇到异常传播,沿调用链找出哪一处把“图表失败”改写成了“整份报表失败”,让拒绝停在它实际负责的效果范围内,而不是再增加一个总协调器。
三、可选功能,即使没有使用,也可能让所有任务变慢
继续看图表功能。为了统一流程,系统每次都创建画图任务,随后才检查用户是否需要附件。即使最终跳过画图,任务登记、状态查询和额外依赖也已经发生。
在 Agent 中,同类成本还可能出现在提示词和工具说明里:某个能力一次都没调用,它的定义仍可能随每轮请求输入模型。
因此,评估可选功能时,至少区分三种情况:
| 情况 | 应检查什么 |
|---|---|
| 功能尚未接入 | 原来的普通报表路径是什么 |
| 功能已接入,但本次不使用 | 是否仍新增任务、查询、上下文或等待 |
| 本次确实使用功能 | 得到什么结果,付出多少额外代价 |
在教学系统里,可以让选择图表的任务才进入渲染流程。普通正文报表继续走原有路径。是否减少了耗时,要用可比输入观察,不能只凭多了一个开关就宣布更快。
同样,也不能为了省输入而悄悄删掉本轮已准入的合法工具,或缺省略去必要保护。减少无关负担与缩减已经承诺的能力,是两个不同决定。
四、替换完成的标志,是旧东西不再要求你维护
现在要把旧画图库换成新库。一种容易出现的“升级”是:新增一层包装器,先试新库,失败再调旧库;两个返回格式都保留,页面继续判断两套状态。
这可能是必要的过渡,但如果没有结束条件,系统就要长期维护两套实现和中间的兼容规则。
在既有迁移任务中写清楚退出条件,比再建一套治理系统更有帮助。例如:新库已经覆盖仍在使用的图表类型;新任务不再调用旧库;仍需打开的历史文件可以读取;没有在途任务继续依赖旧格式。满足条件后,删除旧接线、旧依赖以及只服务旧库的状态判断。
历史图片是否继续保留,由原有保存和删除约定决定。删除旧渲染器,并不等于可以删除用户仍有权读取的文件。
还要查一眼当前说明和测试。若规范仍要求调用旧库、测试仍锁定旧状态名,下一位开发者或 Coding Agent 可能会把旧机制重新补回来。历史记录可以解释当时为何这样设计,现行规则则应指导当前实现。
五、可卸载、可替换、可删除,解决的是不同问题
DeepSeek Harness 的架构文档展示了另一种组织方式:以 Cordis 插件组合模型、工具和执行循环;生命周期文档说明,受框架管理的注册会随插件卸载撤销,框架外资源则需要声明清理动作。
这给我们的启发是提前考虑能力如何退出,而不是要求所有系统都改成插件。对报表例子,可以分别问三个问题:
- 停用时能否清理? 定时器、监听和临时资源是否还在运行?
- 更换时能否隔离? 调用方是否仍可使用稳定的结果约定?
- 退役时能否删除? 旧分支、旧依赖和过期要求是否真的能退出?
三个答案不会自动相同。清理定时器不能撤回已经发送的文件,更换实现也不能抹掉实际操作记录。没有真实替换需求时,也不必预建复杂的热插拔体系。
六、把“五不”写回一次普通评审
我们把这类取舍归纳为“五不”。它们适合变成评审里的具体问题:
| 原则 | 在图表案例里怎么检查 |
|---|---|
| 不因容易实现就新增机制 | 先说明原报表模块为什么不能局部解决问题 |
| 不让复杂度扩散 | 页面和定时任务是否还要解释渲染器的内部状态 |
| 不扩大局部否决权 | 可选附件失败,是否误伤独立可用的正文 |
| 不让普通任务持续付费 | 不要图表时,是否仍创建渲染任务或增加等待 |
| 不留下无法退出的能力 | 新实现接手后,哪些旧依赖和要求会被删除 |
不需要再为五行各建一个检查器。规则本身也有维护成本,放回已有设计和代码评审即可。
下一次接到“增加一个图表附件”,任务里可以写:明确正文与附件的交付关系;将渲染内部状态留在图表模块;从页面验证附件失败时的正文行为;比较不使用图表的普通路径;若替换旧库,列出仍在使用的入口和退出条件。
这已经足够让开发者开始工作,也让评审能判断设计有没有减少依赖。
代码生产得更快以后,值得追问的是:这次新增能力,有没有让下一次修改更容易? 能力集中在少数清楚的入口,失败影响准确,旧负担有机会退出,系统才更有可能持续升级。
延伸阅读与交流
- Publication:为什么回答需要独立发布系统:独立正文、附件和发布事实的具体机制。
- 局部改善不等于整体进步:怎样核对优化收益及额外代价。
- 测试也会过期:旧要求怎样从日常回归中退出。
- Agent 架构原则速查:本文对应能力引入与退出、新功能取舍和工程知识退役。
更多文章见 ETF Agent 技术阅读站 和 GitHub 公开文集。如果内容有所帮助,欢迎 Star 或提供反例交流。