规则漂移

最近碰到一个很小的问题。

算法模块提供了一个 CalcLUTParams 接口,根据直方图计算图像增强参数。接口约定很简单:返回 0 表示成功,返回非 0 表示失败。

测试时发现,全黑图会返回 -5

而业务上并不希望因此中断显示流程。对于这种场景,更合理的行为往往是退化为默认对角线曲线,让图像继续显示。

于是出现了一个很典型的问题。

到底应该:

  • 算法模块内部直接返回默认参数;
  • 还是客户端收到 -5 后自行 fallback。

这个问题本身不大。

但有意思的是,它很容易继续往下延伸。

如果接口名叫 CalcLUTParams ,那么从直觉上看,它似乎应该负责"给出一组可用的 LUT 参数"。既然默认对角线参数本身就是合法结果,那么"全黑图返回默认参数"完全可以成为算法模块内部的领域规则。

这样调用方只需要得到结果,而不需要理解算法内部的"失败"细节(这并不是真正意义上的失败,而是算法内部正常的分支细节)。

一方面,客户端也并非没有自己的领域目标。

算法模块关心的是:是否成功计算出了有效增强曲线。

显示 SDK 关心的则是:图像是否还能继续显示。

这两者并不完全相同。

于是:

算法失败,并不一定意味着显示失败。

客户端收到错误码后,决定退化为默认参数,同样是合理的。

真正有意思的地方正在这里。

"全黑图返回默认曲线"这个规则,放在算法层是合理的,放在显示层似乎也合理。

问题开始从"如何 fallback",逐渐变成了"规则属于谁"。

复杂系统里,很多问题都会慢慢演化成这种形式。

最初只是一个局部决策。

后来会发现:

  • 一部分 fallback 在算法层;
  • 一部分 fallback 在 SDK;
  • 一部分 fallback 在 Viewer;
  • 一部分 fallback 在 UI。

每一次修改单独看都成立。

但系统会开始逐渐失去一种东西:稳定的规则中心。

这时会出现一种很微妙的状态。

系统里的行为并没有立刻错误。

相反,大部分时候它甚至还能正常工作。

只是:

  • 相同场景在不同模块开始出现不同处理;
  • 错误码的语义慢慢漂移;
  • 调用方越来越依赖隐含约定;
  • 系统行为越来越难以推导。

很多复杂系统后期令人不安的地方就在于这种"规则漂移"。

规则不再稳定地收敛在某个边界内,而是在系统中缓慢扩散。

一开始只是一个 fallback。

后来会变成大量局部合理、但彼此缺乏统一语义的补丁。

我越来越觉得,让规则持续收敛是复杂系统真正困难的部分。

这件事在 AI 编程时代可能会变得更加明显。

LLM 很擅长补局部缺口。

哪里失败,就补一个 fallback;哪里容易中断,就增加一个默认值。

这些修改从局部上下文看,往往都没有问题。

但模型天然缺少一种长期、稳定的系统边界感。

于是很容易产生一种状态:

局部越来越合理。

整体越来越漂移。

现在回头再看,最开始的问题其实只是:

复制代码
if(ret != 0)

但很多复杂系统的问题,本来就是这样慢慢开始的。