永远不要让 AI 写 fallback

这四个字,原版里没有

最近在清理复刻版客户端里写死的中文字,翻出来一件事:一件药师长袍的说明框里,有一行写着「职业限制」。看着完全正常——中文、位置、字号、颜色,都挑不出毛病,甚至比原版还讲究一点。

但原版那一行只有「限制」两个字。多出来的那两个字,在原版整张九千多行的数据表里根本不存在,是代码里编出来的。它就这么在屏幕上待了整整一个月,没有任何人、任何测试发现过。

这个项目第 1 篇讲过,一句话版本:把一款 2003 年上线、早就停运的韩国网游客户端,一个人复刻一遍,标准只有一条——跟原版一模一样。复刻到物品说明框的时候,AI 帮了个「忙」——写渲染逻辑的时候,顺手给每个查表调用都加了中文默认值,万一查不到,总得显示点什么,不能空着嘛。

听起来很贴心。结果这批默认值里至少有三条是错的,而且错得谁都看不出来(包括我自己,不然也不会让它活一个月)。

为什么一个中文字都不能写死

这三条错误到底有多严重,得先看原版是怎么管文字的。

原版客户端的所有界面文字——菜单、按钮、物品说明、技能描述,玩家在屏幕上读到的一切中文——一个字都不嵌在代码里,全部存在一张外部数据表里:一种叫 SOX 的格式,编号 601,一共 9,122 行。

这不是推测,是数出来的。我把物品说明框用到的全部 34 个标签,按原版的中文编码形态,在原版程序里做了一次全字节扫描,一次都没命中(具体怎么扫的就不展开了)。原版确实一个字面量都没嵌。

含义很直接:换掉那张表,就换掉整个游戏的语言。所以复刻版只要在代码里写死一个中文字,就等于在这条链路上打了个洞——那个字从此脱离数据表的控制,原版改了它也不会跟着改。

说明框里每一行在数据表里的真实来源:前四行都在表里,唯独「职业限制」查无此词

原版在 2003 年做对了一件事

那原版查不到字符串的时候怎么办?它的做法值得单独说。

原版在每一处标签调用点都传了一个「允许用默认值」的参数。查不到的时候,返回一个 ASCII 字面量 [String not found],然后——那一行照样画出来。不跳过,不隐藏,不替换成别的词。

一行 [String not found] 出现在中文界面上,丑是丑,但谁路过都看得出来这里不对。也就是说,原版自己选择了让失败显眼。

2003 年那批人是对的。

复刻版后来照抄了这个行为,另外加了一条:同一个键只警告一次。因为这些查表函数跑在渲染路径上,每帧都会调,不加限制的日志会把控制台刷爆。

三条活了一个月的默认值

回头看那批默认值。时间线是这样的:

日期 事件
2026-06-26 物品说明框落地,代码里带着一批中文默认值
2026-07-22 小地图上一条英文默认值被发现,修掉
2026-07-28 全部中文默认值连根拔掉,并做全字节扫描验证

拔的时候逐条对了一遍表里的真实数据,发现它们早就跟表对不上了。三条最典型:

  1. 「职业限制」这四个字,在整张 9,122 行的表里根本不存在——原版只有「限制」。
  2. 376,表里是「防御 」,结尾带一个空格——代码里没有,排版会差一格。上面那张图里就有这一行,跟「限制」同一个框。
  3. 382,表里是「生命」,代码里写的是「体质」——词错了。

同一件装备,同一个位置。左边是代码里编的「职业限制」,右边是数据表里真正的「限制」

第一条最能说明问题。它不是抄错了,是 AI 编了一个看起来很合理的词。拆开看每个字都通顺,放进说明框的上下文里更通顺——「职业限制」听着甚至比「限制」还专业一点。但原版表里就是没有。

第三条得多说一句,因为它是另一种情况:那个属性原版根本没启用,所以那一行从来没上过屏。错了一个月,但连被看见的机会都没有——兜底把一段死代码里的错误也一起藏住了。

同一次清理还对着原版逐条核了全部 3,101 行物品数据,顺手删掉的中文字面量还有「限制」、「力量」、「智力」、「敏捷」和「意志」这些。每一个单独看都像是对的。

为什么一个都没被发现

这是整件事真正值得讲的部分。

因为它们看起来全是对的。

屏幕上显示「职业限制」——是中文,位置对,字号对,颜色对,跟上面「防御」「躲闪」那几行排在一起,毫无违和。没有报错,没有乱码,没有问号,没有 [String not found]。它和正确的「限制」摆在一起,你也说不出哪个更像真的。

唯一的区别是:一个来自原版的数据表,一个是代码里编的。而这个区别,看屏幕是看不出来的。

那一整批默认值里,唯一一次被及早抓到的,是小地图上那条。它被抓到,只是因为那条默认值恰好是英文——一堆中文界面里冒出一行英文,一眼就看出来了。

换句话说:抓到它的不是测试,不是审查,是运气——那条默认值恰好用错了语言。

这就是 fallback 在还原度项目里真正可怕的地方。它不会让程序崩溃——崩溃反而是好事,一崩你就知道哪里出了问题。它做的是另一件事:用一个看起来合理的假答案,悄悄顶替真答案,而且顶得严丝合缝。

这不是「AI 写错了代码」

到这里我们很容易得出一个结论:AI 不靠谱,写代码随便乱加东西。

But……

「查不到就给个默认值」这种做法,在绝大多数项目里都是个好习惯,是防御性编程的标准动作,也是训练数据里被反复奖励的写法。模型这么写,不是它的错,是它在照着通识办事,而且办得相当标准。

而问题就在于,这个项目的目标跟通识恰好相反:

  • 普通项目:别让用户看见错误 → fallback 是对的
  • 还原度项目:必须让我看见每一处不一致 → fallback 是灾难

模型不知道这件事,因为我一开始没把它写进项目规范。规范里写了代码风格、命名约定、提交格式,唯独没写「这个项目里错误必须显眼」这一条。所以准确的说法是:不是 AI 写错了代码,是我没告诉它这个项目的成功标准是什么。

一刀切也是错的

故事讲到这,顺手立一条「永远别写 fallback」的规矩,看起来顺理成章。

但这也是错的。换个地方就不成立了。

原版的中文是 GBK 那套双字节老编码,进到复刻版里得转成 UTF-8。转换的时候总会碰上一两个不认识的字节——可能是数据本来就有点脏,也可能是我这边哪里读偏了(二十多年前的数据,有点脾气很正常)。

这时候转换器有两条路可走。一条是猜:不认识就替换成一个替代字符,剩下的照转,让流程走下去。另一条是拒:整段不认,直接报错,一个字都不吐出来。

绝大多数编码库默认走第一条。听着也合理——总不能因为一个字就把整件事停掉。

问题是这个替代字符会跟着数据走。玩家名字里混进一个替代字符,界面上还是一串像模像样的中文;可这个名字要是被写进数据库,那就永久变成这样了。下次读出来它就是「真的」——跟前面那个「体质」一模一样:看着完全正常,实际是编出来的。

所以这次做了两个转换器。

给人看的那个,保留宽容。 界面上错一个字,你一眼就看见了,而且不该为了一个字把整个界面炸掉。

要写下来的那个,必须严格。 落库、持久化、发到网络上——这些地方错了会永久留下,还长得像真的。所以它的规矩是一句话:拒绝,并且永不替换。 碰到不认识的东西就返回错误、输出为空——宁可这一步整个失败,也不许一段半对半错的字符串溜出去。

场景 该用哪种 为什么
界面显示 宽容 错了肉眼看得见,不该为一个字炸掉整个界面
落库、持久化、协议边界 严格 错了会永久留下,而且看起来像真的

所以这篇真正的判断不是「别兜底」,是「在错误必须被看见的地方别兜底」。同一个项目里两种都有,关键是各自用在什么地方。

两条能带走的方法

方法一:怎么证明兜底根本不需要。 不要靠推理,去数。那 34 个标签的全字节扫描,一次都没命中,「原版一个字面量都没嵌」就从推测变成了事实,兜底也就失去了全部理由。推广开来:删掉一个防御性分支之前,先想办法证明它永远不会被合法地触发。证不出来,说明你其实不了解这段代码——那才是真问题。

方法二:怎么验证 AI 说「我做完了」。 那张编码表是生成的,不是手写的,生成器跟表一起进了版本库——理由写在提交信息里:宁可要一个可复现的生成器,也不要一个没人能重新推导出来的产物。验证方式,提交信息里的原话是「不采信交付方的说法,独立验证」:

  1. 重跑生成器,看输出能不能逐字节复现那张表;
  2. 数数:正向表条目数必须正好等于理论值 23,940;
  3. 拿一个完全独立的实现,跑 6,000 组随机的首尾字节组合逐一比对——非法的组合也必须一致,都得判错。

整套测试当时 203/203 全绿。要注意第三条里那 6,000 组是抽样,不是穷举,它不能证明整张表全对——但它是一个 AI 没参与过的独立参照,这就够了。推广开来:AI 说做完了、测过了,就找一个它没参与的东西去对答案。重跑、计数、跟另一个实现比,三样里至少做一样。

后来立下的规矩

清理完那次之后,项目规范里多了几条硬规矩:

  • 任何用户可见的字,一律从数据表来,永远不许写字面量;
  • 绝不允许 str_label(key, "中文") 这种带兜底的写法——兜底跟真数据同一种语言,等于让错误永久隐身;
  • 查不到就渲染 [String not found],照原版的行为,不替换;
  • 只有非本地化的脚手架可以是字面量:标记标签、格式串、资源路径、调试输出。

至于那行「体质」,已经在代码里被原地正法了。不过从那以后,我看自己界面上每一个完全正常的中文字,都忍不住要多想一层——这大概就是这个项目送给我的职业病。🐶

几条能直接用的

  • 别让 AI 写兜底,除非错了看得见。 默认值的「长相」必须跟真数据不一样——换个语言、加个前缀都行。错的时候一眼认不出来,兜底就是把错误变成永久隐身。
  • 删防御分支之前,先证明它不会被合法触发。 别靠推理,去数、去扫,拿到证据再动手。证不出来,说明你还不懂这段代码。
  • AI 说「做完了」,用它没参与的东西去验。 重跑生成器、数条目、拿一个独立实现对随机样本,三样里至少做一样。第 1 篇《五个月,一个人,把一款 2003 年的韩国网游客户端重写了一遍》里说过,这个项目最贵的不是让 AI 写代码,是建立一套能持续发现它判断错误的机制——这一条就是那套机制的日常动作。
  • 给 AI 立规矩,写清成功标准,别只写代码风格。「这个项目里错误必须显眼」这种话不写进规范,模型就只会照通识办事——而你的项目,通识可能是反的。