最近碰到一个很小的问题。
算法模块提供了一个 CalcLUTParams 接口,根据直方图计算图像增强参数。接口约定很简单:返回 0 表示成功,返回非 0 表示失败。
测试时发现,全黑图会返回 -5。
而业务上并不希望因此中断显示流程。对于这种场景,更合理的行为往往是退化为默认对角线曲线,让图像继续显示。
于是出现了一个很典型的问题。
到底应该:
- 算法模块内部直接返回默认参数;
- 还是客户端收到
-5后自行 fallback。
这个问题本身不大。
但有意思的是,它很容易继续往下延伸。
如果接口名叫 CalcLUTParams ,那么从直觉上看,它似乎应该负责"给出一组可用的 LUT 参数"。既然默认对角线参数本身就是合法结果,那么"全黑图返回默认参数"完全可以成为算法模块内部的领域规则。
这样调用方只需要得到结果,而不需要理解算法内部的"失败"细节(这并不是真正意义上的失败,而是算法内部正常的分支细节)。
一方面,客户端也并非没有自己的领域目标。
算法模块关心的是:是否成功计算出了有效增强曲线。
显示 SDK 关心的则是:图像是否还能继续显示。
这两者并不完全相同。
于是:
算法失败,并不一定意味着显示失败。
客户端收到错误码后,决定退化为默认参数,同样是合理的。
真正有意思的地方正在这里。
"全黑图返回默认曲线"这个规则,放在算法层是合理的,放在显示层似乎也合理。
问题开始从"如何 fallback",逐渐变成了"规则属于谁"。
复杂系统里,很多问题都会慢慢演化成这种形式。
最初只是一个局部决策。
后来会发现:
- 一部分 fallback 在算法层;
- 一部分 fallback 在 SDK;
- 一部分 fallback 在 Viewer;
- 一部分 fallback 在 UI。
每一次修改单独看都成立。
但系统会开始逐渐失去一种东西:稳定的规则中心。
这时会出现一种很微妙的状态。
系统里的行为并没有立刻错误。
相反,大部分时候它甚至还能正常工作。
只是:
- 相同场景在不同模块开始出现不同处理;
- 错误码的语义慢慢漂移;
- 调用方越来越依赖隐含约定;
- 系统行为越来越难以推导。
很多复杂系统后期令人不安的地方就在于这种"规则漂移"。
规则不再稳定地收敛在某个边界内,而是在系统中缓慢扩散。
一开始只是一个 fallback。
后来会变成大量局部合理、但彼此缺乏统一语义的补丁。
我越来越觉得,让规则持续收敛是复杂系统真正困难的部分。
这件事在 AI 编程时代可能会变得更加明显。
LLM 很擅长补局部缺口。
哪里失败,就补一个 fallback;哪里容易中断,就增加一个默认值。
这些修改从局部上下文看,往往都没有问题。
但模型天然缺少一种长期、稳定的系统边界感。
于是很容易产生一种状态:
局部越来越合理。
整体越来越漂移。
现在回头再看,最开始的问题其实只是:
if(ret != 0)
但很多复杂系统的问题,本来就是这样慢慢开始的。