未来确定却算不准?有限观察者视角下的工程应对策略
未来是确定的但对有限的观察者而言它并不一定能够被完全计算。这句话听起来有点玄但它几乎每天都在工程现场发生。比如你曾经遇到过这样的问题程序没有任何随机逻辑输入数据也一样可为什么两次运行的结果就是不一样或者反过来一个算法理论上应该复现历史轨迹但换了台机器、换了个并发调度顺序、换了一次依赖版本升级结果就漂移了。这不是玄学也不是代码“不讲道理”而是确定性和可计算性被混为一谈了。古代天文学用二十八星宿来划分天空把连续的天球切成二十八个区域是一种用有限框架去逼近无限现实的计算方案。它背后的思路和今天我们用有限状态机建模系统、用离散网格模拟物理场、用概率分布近似真实世界并没有本质差别。这篇博客我真正想聊的是为什么“未来是确定的”不等于“未来可以被完全计算”以及当我们接受这个现实之后工程上应该怎么设计系统、怎么训练模型、怎么做预测、怎么留容错空间。1. 为什么“未来是确定的”和“未来可以被计算”是两回事1.1 拉普拉斯妖和二十八星宿两种“计算未来”的古老冲动物理学史上有一个著名的思想实验叫拉普拉斯妖。它假设有一个智能体如果能够知道某一时刻宇宙中所有粒子的位置和动量并且拥有足够的计算能力那么它就能推算出整个宇宙的过去和未来。这个设想非常符合人类对确定性的直觉既然物理定律是确定的输入也是确定的那输出也应该确定。但拉普拉斯妖有一个致命前提它需要知道“所有粒子的位置和动量”而且需要“足够的计算能力”。这两个条件在真实世界里都不成立。二十八星宿则提供了另一个维度的参考。古人没有能力计算每颗星的完整轨道他们做的事情是把环绕天球赤道附近的恒星划分成二十八个天区并以此为坐标来追踪月亮、太阳和行星的运动。这不是对天空的精确复刻而是对天空的一种有损压缩。它牺牲了细节换来了可操作性。从工程角度理解二十八星宿本质上是一套状态空间划分方案。古人无法观测整个连续的天球所以他们把连续空间离散化为二十八个状态再通过观察这些状态的变化来推断节气和时间。这比拉普拉斯妖的“完全计算”粗糙得多但它可以落地。这两个例子放在一起刚好代表了两种预测思路一种是“如果我掌握了全部信息我就能算准一切”另一种是“我承认信息有限所以我先建立一个新的可选框架再用可观测状态去逼近现实”。现代工程和科学基本都走在第二条路上。1.2 三体问题严格确定却无法解析求解你不需要真的运行一个宇宙模拟器也能感受到确定性和可计算性之间的裂缝。经典力学里的三体问题就是典型。两个天体互相绕转牛顿给出的万有引力公式可以精确求解这是二体问题。一旦加入第三个天体方程仍然完全确定每一个时刻每个天体受到的引力都清清楚楚只要给定初始位置、速度和质量理论上未来所有时刻的轨迹都是唯一的。但问题在于这个系统无法得到一个闭式解。也就是说你不存在一个简单的公式可以把时间 t 直接代进去一次性算出每个天体在 t 时刻的位置。你只能用数值方法一步一步地积分每推进一步都会引入舍入误差。这个误差不会消失它会在系统的非线性作用下被放大。这就是确定的未来和可计算的未来之间的分界线。数学上三体问题的未来是确定的工程上你只能计算一个有限精度的近似。你增加步长精度可以推迟误差发散的时间但你无法完全消除它。并且当系统对初始条件敏感时哪怕只差一个极小的小数点位置长期演化结果可能完全不同。同一个道理也出现在开放世界游戏物理引擎、分子动力学模拟和航天器轨道规划里。这不是理论空谈而是任何做数值计算的人都会遇到的问题。1.3 确定性程序并不等于可预测程序有人会说物理系统对初始条件敏感我可以理解但我自己写的程序总该是确定性的吧。同一个输入、同一个函数、同一个逻辑结果还能跑偏不成答案是单个函数可以是确定性的但一个复杂系统的整体行为不等于单个函数的简单叠加。我在工作中经常遇到这样的场景代码里并没有random.random()这样的语句但两次运行的结果不一样。最后排查下来可能来自几个并不起眼的地方字典的遍历顺序在不同 Python 版本或不同哈希种子下可能不同并发任务执行顺序受到线程调度影响最终累积顺序不同浮点数在 CPU 指令重排或不同硬件上可能出现细微差异外部服务返回的字段顺序变了导致后续逻辑走了不同分支依赖库升级后内部排序算法变了输出顺序跟着变。这些情况里每个环节单独看都是确定性的但组装成一个完整系统后整体结果却依赖于一组不可控的细节。你拥有了一段“确定性的代码”但你并不拥有“可预测的系统”。所以从工程视角看要把一个系统做成可预测的通常需要额外做三件事明确种子和随机源的管理方式固定排序策略把关键输出结果做成可回放的快照。否则即使物理世界没有不确定性你的系统也会表现得像一个不确定系统。2. 有限观察者的三个约束分辨率、顺序、历史2.1 观测分辨率的极限浮点数、离散化和状态压缩我们观察世界不是直接观察世界本身而是通过一层测量和编码。测量精度是有限的编码格式也是有限的。最典型的就是浮点数。IEEE 754 标准下的浮点数并不是实数它只是实数的一个离散子集。两个看起来几乎相等的值可能在末位尾数上不同某些看似精确计算的式子在浮点运算下会得到一个很不精确的结果。比如计算0.1 0.2很多语言里得到的不是正好等于0.3而是一个近似值。对单个表达式来说这个误差微不足道但如果误差在循环里反复累积最终结果可能相差很大。数值计算里还有一个概念叫条件数。它衡量的是输入发生微小扰动时输出会发生多大变化。条件数小系统稳定微小扰动只会带来微小差异条件数大系统病态很小的输入误差会被放大成很大的输出误差。对有限观察者来说真正的问题不是“误差存在”而是“误差经过系统后会被放大到什么程度”。这决定了我们是否需要把结果当成区间而不是点来看待也决定了我们是否需要给系统增加校验和回归测试。2.2 顺序效应同样的步骤不同的结果有限观察者在观测一个动态系统时通常会采用“采样”的方式。我们无法同时看到所有状态所以我们只能按一定顺序获取状态快照。问题在于一个系统的最终状态往往和中间步骤的先后顺序高度相关。数据库事务是一个经典例子。多个事务并发执行时不同的执行顺序会导致不同的最终结果。数据库通过锁、隔离级别和事务日志来约束顺序以保证结果可控。但很多普通程序并没有这个级别的约束。我看过不少因为“顺序”而翻车的问题批量导入数据时先执行 A 再执行 B和先执行 B 再执行 A结果完全不同定时任务和用户请求同时修改同一条缓存记录结果取决于谁后写入日志处理器里事件堆积导致处理顺序和产生顺序不一致下游统计出现偏差。这些问题的根源都是我们只能按顺序观测和处理状态但对一个并发系统来说全局顺序本身可能是不确定的。要想让结果可预测有时候不是靠算法更聪明而是靠引入明确的顺序约束比如队列、版本号、乐观锁、时间戳排序。2.3 有限历史你以为的输入只是一个投影更隐蔽的一个约束是我们永远只能看到系统的“当前状态”而不知道它是如何演化到这一步的。很多系统是路径依赖的同样的当前状态可能来自完全不同的历史路径而这些历史路径会影响未来行为。举个例子。数据库中有两条记录一条是用户手动创建后又修改了三次另一条是批量任务一次性生成的。从当前时间点看这两条记录的字段值可能完全一样但它们的创建时间、修改版本、触发日志都不同。如果后续逻辑只读取当前快照那么这两条记录就被视为等价但一旦需要回滚、审计或者重新计算补偿历史信息就变得关键。这和二十八星宿的观察模式很像。假设你只观察一个星宿当前处于哪个天区你很难判断它是刚进入这个天区还是已经停留了很久。相同的位置观测值对应着不同的运动阶段。你必须有历史观测序列才能做更好的预测。工程上做事件溯源系统的人对这个体会尤其深。事件溯源不是保存当前状态而是保存完整的事件流当前状态可以从事件流重新计算出来。这个模式之所以强大正是因为它意识到了未来可能由历史路径影响不是由当前快照单独决定。如果只保存快照你实际上丢失了大量用于预测未来的信息。3. 面对“算不准”工程上应该怎么办3.1 从点预测到区间预测承认误差的存在很多系统在设计初期就犯了一个错误要求输出一个精确值。预测明天的销售量要求给出一个精确到个位数的数字预测用户点击率要求给出一个精确的百分比预测任务执行时间要求报一个精确到秒的时间点。现实是只要系统对初始条件敏感、存在测量误差、存在外部扰动精确的点预测就会失效。与其追求一个永远无法保证准确的点不如设计成区间预测。我一般在设计预测类系统时会做三个层次的处理先输出点估计但同时给出置信区间如果置信区间太宽就降低预测范围或者延长预测周期把“预测错了也安全”当成核心指标而不是把“预测得准”当成核心指标。这个思路在金融风控、电商备货、容量规划、天气预报里都很常见。比如天气预报不会告诉你“明天下午 3 点 14 分下雨”它只会给你一个概率和降水时段。这种表达不是因为气象学家不够聪明而是因为精确到一个时间点的预测在物理上不稳定。3.2 用重试、回退和异常处理来吸收不确定性另一个减少“预测失败影响”的方法不是在模型层追求更准而是在执行层增加弹性。比如一个分布式任务调度系统某个节点执行任务失败了。你不知道它是真的处理不了还是因为网络抖动导致结果没有返回。如果你坚持一次成功系统就会陷入阻塞如果你引入重试和幂等设计系统就有机会自我修复。一个常见的实践是为每个任务分配唯一任务 ID执行前记录任务状态返回结果时校验任务是否已经被处理过失败后按退避策略重试而不是激进地重试多次重试失败后进入待人工处理的队列。这套机制并没有让“未来变得确定”它只是设计了一套应对不确定未来的策略。它假设未来可能出现失败、超时、乱序和重复执行然后用工程手段把这些异常吸收掉。这种思路和天文学上的多路径验证也有相似之处。古人不会只依赖某一个星宿的位置来判断节气而是综合多个恒星、月亮位置和长期历法记录。多条线索互相校验即使某个观测出错整体判断也不至于崩溃。3.3 在线重新校准与其预测未来不如持续观测未来说到长期预测就不得不提“校准”这个概念。一个模型离线训练时表现很好上线后效果却变差这往往不是因为模型代码写错了而是因为现实世界的分布发生了漂移。以固定的模型去应对不断变化的数据分布天然就是不可靠的。所以很多稳定运行的预测系统都采用“短周期预测 在线校准”的模式定期收集最新观测数据比较模型预测值和实际值根据偏差调整模型参数或重新训练把预测周期控制在系统仍然有效的范围内。天气预报之所以能做到几天内可信是因为它每天不断用新的观测数据重新初始化模型而不是靠一次初始条件算到底。工业界做销量预测也是类似。你不用一年前训练一个模型然后希望它永远准确你每个月、每周甚至每天都做增量更新。工程上这等于承认长期完全预测是不可能的但短期预测加持续纠偏可以做到够用。3.4 二十八星宿的模式状态划分、容错、经验修正回过头看二十八星宿你会发现它其实已经包含了一套完整的“有限观察者工程方案”它没有试图计算每一颗星的完整轨道而是把天球划分成有限个状态区域它通过观测月亮在星宿间的位置变化建立周期性时间框架它不断通过实际观测修正历法而不是死守一套数学公式它保留了大量经验积累用来弥补理论模型的不足。这套方法在今天的软件架构里其实很容易找到对应物用有限状态机建模复杂流程用指标监控和告警替代完整模拟用手工补丁和经验文档弥补自动化系统的盲区用定期复盘替换一劳永逸的最优方案。从这个角度说“未来是确定的但不能完全计算”并不是一种消极判断而是要求我们改变策略从追求“完美预测”变成建设“有弹性、可观测、能修正”的系统。4. 面向有限观察者的预测工作流一套可落地的四步法4.1 第一步区分“确定性输入”和“可重建状态”当你需要做一个预测或推断系统时第一步不是上模型而是盘点你的输入和状态哪些输入是真正确定性的比如配置参数、版本号、固定业务规则哪些输入是外部观测数据比如网络请求、传感器数据、用户行为日志哪些状态可以从历史数据中重建哪些状态只能实时获取哪些状态丢失后无法恢复会被记录为不可逆决策。如果你发现系统依赖大量无法重建的实时状态就要提前设计审计日志以便事后重建因果链。否则后续所有“预测失败”的排查都会像在黑暗里找钥匙。4.2 第二步做扰动测试评估“小输入变化”对结果的影响很多系统失败不是因为输入错了而是因为输入只对了一部分。一个重要工程建议是主动做扰动测试。把某个字段的值小幅修改看输出变化多少随机丢弃少量数据看模型结果是否剧烈变化并发环境下打乱执行顺序看最终一致性是否符合预期在不同 CPU 架构或运行环境下运行测试用例看是否存在浮点稳定性问题。如果结果对外部细微变化异常敏感就说明该系统正处在混沌边缘。这时候你需要的不是增加模型复杂度而是增加输入约束固定特征顺序、明确排序规则、增加数据校验。4.3 第三步用短期预测加持续校准替代一次性长期预测如果业务上必须做长期预测我的建议是先拆解找出哪些部分在短期内是稳定的把长期预测拆成多个短期预测的接力为每个阶段设置观测点在观测点做实际值与预测值的比较然后校正下一阶段参数。这种方法比单一模型做长期预测要可靠得多。它更适合作为一个流程而不是一个模型。4.4 第四步设计失败路径而不是只设计成功路径当预测失败、系统失效、任务超时时系统到底该怎么做这是决定一个系统能否长期稳定运行的关键。一套完整的失败路径至少包括超时阈值重试次数和退避策略熔断条件降级方案人工介入通道日志和追踪信息。如果系统只实现了“正确路径”一旦预测偏差超过容忍范围整个链路就会雪崩。把失败路径当成一等公民来设计才是真正把“有限观察者”的认知落实到工程实践。5. 这套观念在数字技术里的现实落点5.1 对大模型和生成式 AI 的意义概率输出不是缺陷现在很多人用大模型做内容生成、代码补全、结构化输出。一个最常见的问题就是同样的提示词为什么两次生成的结果不一样有时候差异很大有时候结构不稳定。从本文的视角看语言模型本质上就是一个对海量语料进行统计建模的系统。它的输出是概率性的因为它建模的不是某个确定答案而是一个答案在整个历史文本中的条件概率分布。再加上采样过程中的随机种子、温度参数、批次调度最终输出天然具有非确定性。对工程师来说正确的处理方式不是生气地要求模型“给我完全一样的输出”而是要做后处理约束设置合理的 temperature 参数固定随机种子如果平台支持用结构化输出模板约束生成格式对关键内容做二次校验对重要结果做多轮生成投票。这些操作的目标不是消除不确定性而是把不确定性控制在一个可接受范围内。这和传统软件工程里的“错误处理”思路完全一致。5.2 对可预测基础设施的再思考很多时候我们要求基础设施具备确定性不是因为未来真的确定而是因为只有建立起可重复性才能定位问题。构建要可重复测试要可重复部署要可重复日志要可追踪。这看起来和“未来不可完全计算”矛盾其实并不矛盾。我们要做的是在源头管理随机性比如固定种子、集中管理随机数在过程中保留可观测性比如结构化日志、trace ID在结果上提供可回放性比如快照恢复、事件溯源在模式上区分“确定性算法”和“确定性系统部署”。一个系统能不能从崩溃中恢复往往不取决于它的预测能力而取决于它对自身历史的重建能力。这比追求完全准确的预测更实际也更重要。5.3 适用边界这套思维解决什么问题不解决什么问题需要讲清楚的是这篇文章讨论的“有限观察者”思维适合以下场景面对长期、复杂、多层嵌套的预测问题系统对初始条件敏感的数值计算依赖大量外部数据的在线系统需要做容量规划、风险预警、供应链计划等任务的场景。它不适合被当作“放弃精确性”的借口。如果业务上有一个明确的、稳定范围内的计算任务比如“给定税后收入和税前收入计算税额”你应该用精确公式而不是用概率模型和区间估计。确定性可计算的问题仍然应该精确计算。本文强调的只是当系统超出有限观察者的计算能力时不要假装自己还能消除不确定性。5.4 几千年后回看二十八星宿我们仍然在同一个框架里回头看二十八星宿它做了一次非常漂亮的工程选择它用一个有限的、离散的、可观测的框架去逼近连续运动的星空。这套框架有边界有误差也有修正机制但它足够好用所以沿用了上千年。现代软件工程中我们其实也在做类似的事我们把系统切分成模块把时间切分成批次把连续数据转换成离散特征用状态机管理复杂流程。我们没有能力在一个整体中精确推演所有可能性但我们有能力构建一个足够好的框架在这个框架里决策是可行的错误是可追踪的未来是可适应的。所以当再有人说“未来是确定的”这句话时我会提醒自己确定只是世界的一条底牌我没有能力看到整副牌。但这不意味着我只能被动等待。我可以把观察做得更细一点把模型做得更稳一点把失败处理做得更周全一点把历史记录保留得更完整一点。这才是“有限的观察者”在确定性世界里的正确姿态不追求全知但追求可用不幻想零误差但设计出可靠的误差处理路径不试图一步算到很久以后而是用持续观测、持续校准、持续迭代把未知一步步转化为已知。这也正是从二十八星宿到现代数值模拟人们在面对复杂系统时始终在做的事情不是靠神谕而是靠框架。