我们花了两三周调试一个不该存在的功能

我们花了两三周调试一个不该存在的功能#

一个真实的翻车现场#

上个月,我们团队在做一个复刻经典游戏的项目。

有一天,测试报了一个 bug:「二段跳有时候会误触发」。

于是开始调。

改参数、加判断、打日志、对比帧数据……前前后后折腾了两三周。期间开了不少会,讨论 「误触发的边界条件到底是什么」「要不要加冷却时间」「阈值设成多少合适」。

直到有一天,有人突然问了一句:

「等一下,原版有二段跳吗?」

去查了原版资料。

原版没有二段跳。

我们按完整的工程流程,调试了一个根本不该存在的功能。


这不是「需求没对齐」#

你可能会说:「这不就是需求没写清楚吗?产品经理的锅。」

但问题是,我们不是在做一个新功能。我们是在「复刻」——理论上,「原版是什么样」 就是最清晰的需求。

那为什么还会出这种问题?

复盘之后,我们发现了一个更深层的原因:

这个项目一开始是个探索期的原型。

原型阶段,开发同学随手加了个二段跳——可能是为了测试手感,可能是觉得好玩。参数也 是随便填的,-15,没什么依据。

后来,方法论成熟了,我们想找个项目来验证流程。顺手就拿了这个原型。

问题就出在这个「顺手」上。

我们把原型的代码现状,当成了「既定事实」。

然后,所有后续工作——写测试、定协议、调参数——都是围绕着「让现有代码跑通」来做 的。

没有人问过:「这个代码本身是对的吗?」


我给这个现象起了个名字:公理污染#

公理污染(Axiom Contamination)

当实现先于设计意图存在时,代码的“现状”会被默认为正确,从而反向定义「什么是 对的」,导致后续工作在一个错误的前提上持续优化。

它和「技术债务」不一样。

技术债务是:我知道正确答案是什么,但为了赶进度,先写个烂的。

公理污染是:我不知道正确答案是什么,因为现有代码已经替我「定义」了正确答案。

技术债务像“欠债”。公理污染像“账本本身被写错”。


为什么这不只是「个别团队的问题」#

你可能会说:「你们团队流程不规范,大公司不会这样。」

我也这么想过。但后来看到了一个例子:

波音公司在他们的技术债务分类里,专门识别了一种债务类型,叫 Reified Prototyping

原型被直接演进为生产系统时产生的技术债务。问题在于,这些原型最初并没有按照生产级 的严谨程度来设计和构建。

这说明它不是“某个团队不专业”的偶发问题,而是原型演进路径上的系统性风险。

再想一个问题:

如果「先想清楚再动手」真的是行业共识,为什么技术债务是整个行业的系统性难题?

我的一个猜测是:这和“原型优先、快速迭代”的工作方式有关。


敏捷的一个结构性盲区#

敏捷宣言有一句话:

「Working software over comprehensive documentation」 (可工作的软件 优于 详尽的文档)

这句话的本意是:不要为了文档而文档,能跑的代码比 PPT 更有说服力。

但它被广泛解读成了:文档不重要,边做边想,快速迭代。

于是,「原型 → 迭代 → 产品」变成了默认路径。

验证都通过了,游戏一集成就崩:多 Agent 协作里一个看不见的盲区

验证全通过,我们一集成就崩了。

日志里没有任何异常。

(工程复盘:基于有限样本的复现与观察,结论未必可泛化。)

我们的多 Agent 协作系统有一套完整的验证协议:Agent 生产代码,下游 Agent 验证,验证通过后冻结产物。整条链路运行良好——事件日志里全是 pass,流程三轮稳定收敛。

然后我们把六个 Agent 的产物拼在一起运行,游戏直接崩溃了。

日志里没有任何异常。


问题#

我们研究的系统是一个多 Agent 协作平台。六个 Agent 各自负责一个模块(物理引擎、关卡数据、玩家控制、敌人行为、游戏状态、集成入口),通过一套结构化协议协作:

  1. PRODUCE – 每个 Agent 生产自己的代码模块
  2. VERIFY – 下游 Agent 验证上游的产物
  3. FREEZE – 系统根据验证结果决定是否冻结产物

所有协调事件写入一个共享事件日志。我们对这条日志做实时分析,计算协作健康度指标,检测失败模式。

在两次独立运行中,我们遇到了同一类致命问题:

第一次(Run 1): Agent B 实现了 loadLevel() 函数,返回关卡数据时做了"归一化"——只保留旗帜的 {x, y} 坐标,丢弃了 widthheight。Agent E 的 checkWin() 用 AABB 碰撞检测判断通关,需要 flag.widthflag.heightflag.widthundefinedflag.x + undefined 等于 NaN,碰撞判定永远为 false。结果:游戏可以运行,但永远无法通关。

第二次(Run 2): Agent E 实现了 getDifficulty() 函数,返回一个对象 { speedMultiplier: 1.0, senseMultiplier: 1.0 }——从设计角度看完全合理,对象比单个数字更具扩展性。Agent F 的 game.html 拿到返回值后执行 difficulty.toFixed(1) 显示难度。对象没有 .toFixed() 方法,TypeError 抛出,构造函数中断,游戏循环未启动。结果:打开页面只有蓝色天空背景,无任何游戏元素。