一批研究 COBOL 代码迁移的工程师发表论文,提出把 AI 锁进笼子。

标题很克制:《Agentic Method for Deterministic Validation of Legacy Code Migration》。他们设计了一个叫 Locksmith Loop 的系统——名字起得精准,「锁匠」,不是「大师」。不指望 AI 一次把活干好,要的是把 AI 限制在一个确定性的框架里,让它只做它擅长的事。
但真正有意思的不是这个结论。是他们为什么不得不这样做,以及这样做之后发生了什么。
三个模块,AI 只碰一个
Locksmith Loop 的架构分三层。
第一层是 Migrator。COBOL 源码进来,Java 目标代码出去。这一步不用 LLM,用的是确定性 AST 转换器——规则写死,COMPUTE 变 BigDecimal.add(),PERFORM VARYING 变 for 循环。每次跑,每次结果一样。TimByte 在 HN 上总结得精准:「他们把代码生成委托给确定性工具,模型只做输入 fuzzing——让它跟数据搏斗,别碰语法。」
第二层是 Witness Search。这是 AI 唯一出场的地方。LLM 不断构造输入用例,企图穿透程序的所有分支。每次跑完,覆盖率分析器会检查哪些分支还没被触达,然后 LLM 针对性地构造新输入。这个过程循环往复,直到覆盖率推不动为止。
第三层是 Oracle。COBOL 原版和 Java 迁移版同时跑同一个输入,输出必须逐位一致。任何偏差都视为迁移失败,回退修正。
为什么要这样设计?因为每一步都有明确的失败模式。

Migrator 是确定性的,所以翻译错误是可复现的、可追溯的。如果 COMPUTE 转 BigDecimal 的精度出了问题,你会知道是转换规则写错了,不是模型在某次推理中概率偏移了。
Oracle 是确定性的,所以验证结果是二元的——对就是对,错就是错。不存在「看起来差不多」的灰色地带。
Witness Search 是唯一非确定性的环节。但它的失败模式是可控的——最坏情况是没找到某个分支的触发输入,导致覆盖率不够。它不会产生错误代码,不会引入幻觉,不会在翻译环节制造 bug。
这就是 Locksmith 的设计哲学:把 AI 放在它失败后果最轻的地方。
当 AI 推不动了
Witness Search 不是万能的。跑到某个分支边界时,它可能会卡住。论文里管这种情况叫 Locked Paragraph——一个阻止深入探索的条件,AI 构造不出能穿透它的输入。

这是「锁匠」名字的另一层含义:系统不指望 AI 能打开所有锁。当 AI 推不动时,分析器会标记这个 Locked Paragraph,然后系统继续推进其他分支。最终覆盖率取决于 AI 能找到多少「钥匙」。
论文报告了三个测试案例的结果。两个开源 COBOL 程序,一个「内部生产级」程序。规模从 430 行到 4114 行。两个开源程序达到了「几乎完全覆盖」。生产级程序达到了 91.90% 的分支覆盖率。
91.90%。这个数字本身没有太多讨论价值,因为样本太小。真正有价值的是剩下那 8.1% 意味着什么。Locked Paragraph 是 AI 能力的边界——它找到了 91.90% 的钥匙,剩下 8.1% 的锁它打不开。这些锁在真实系统里,很可能对应着最复杂的业务逻辑、最深的嵌套条件、最古老的 corner case。
连 bug 一起搬,是设计目标
架构还有一个反直觉的设计:迁移后的 Java 必须和原 COBOL 行为完全一致,包括 bug。
aldente0630 在 Hacker News 上澄清了这一点:「保留 bug 是明确的设计目标。」但这不意味着论文在鼓励保留 bug——它只是承认了一个现实:在 legacy 迁移的世界里,这叫 bug-for-bug compatibility。那些 bug 已经在生产环境跑了二十年,下游系统依赖它们的行为。修了一个 bug,等于修了一个「别人以为正常」的功能。
Hyrum's Law 在这里完全适用:「对于有足够多用户的系统,任何可被观察到的行为——无论规范里写了还是没写——都会有人依赖它。」
但 bug-for-bug 有一个前提:Oracle 对比的是行为,不是代码。你保留了 bug 的行为,但你没有保留 bug 的可理解性。原来的 COBOL 老程序员知道那个 bug 为什么存在——可能是某次紧急修复留下的,可能是某个业务规则变更的遗迹。但 Java 程序员看到这段「故意保留 bug 行为」的代码时,他们只会看到一段看起来像是写错了的代码,没有任何上下文。
4KLOC 和 230KLOC
Hacker News 上的评论没有讨论架构设计,而是直接质疑了论文的规模。
pacaro 给出了真实世界的尺度:「IRS 一家就有大约 160 个 COBOL 程序,平均每个 23 万行代码。」
fock 的评论更具体,因为他试过:「我们用的代码里内嵌汇编,跨十几个文件、5 万行。这个程序用了某个预处理器,我们测试的所有 LLM 完全不知道它是什么——它们会编造它的功能。Gemini 2.5 甚至没注意到有预处理器存在。」
这指向了 Locksmith Loop 的真正边界。Migrator 是确定性 AST 转换器,但 AST 转换的前提是你能 parse 源码。如果源码里有预处理器宏、内嵌汇编、CICS 事务指令、JCL 批处理调度——这些不是 COBOL 语法的一部分,是环境的一部分。AST 转换器不认识它们,也不会编造它们(这是优点),但它会直接失败(这是问题)。
Zenst 补充了精度问题:「COBOL 以固定无舍入数学闻名。Java 的 0.1 + 0.2 等于 0.30000000000000004。除非全面使用 BigDecimal,否则迁移从一开始就是坏的。」论文用了 BigDecimal,这是对的。但金融系统的 COBOL 程序通常使用 packed-decimal(BCD 编码),其精度和舍入行为与 BigDecimal 并不完全等价。Oracle 的逐位对比会抓住这些差异,但修复它们意味着在 Migrator 里写越来越复杂的规则。
换句话说,Locksmith Loop 的确定性架构方向是对的,但它的工程复杂度会随系统规模非线性增长。从 4KLOC 到 40KLOC,不是工作量乘以 10,是你要处理的边缘情况、环境依赖、精度差异、非标准语法全部同时爆炸。
COBOL-in-Java
dragonwriter 在 HN 上点出了这个架构最深层的矛盾:「唯一现实的低错误率方案不是更复杂的 AI 用法,而是确定性转译。问题在于,这样得到的是 COBOL-in-Java——跑起来正确,但维护起来是噩梦。」
确定性转换的产物是:语法是 Java,灵魂是 COBOL。变量名、控制流、数据结构全部带着 1960 年代的味道。PERFORM VARYING 变成了 for 循环,但变量的命名方式、数据的组织逻辑、错误处理的分支结构,全部原样保留。
人类程序员做迁移时,会做一件 Migrator 不会做的事:重新理解系统的意图,然后用新语言的惯用方式重新表达。这引入了风险(理解可能出错),但这是唯一能产出可维护代码的方式。
Migrator 选择了零风险路径,但付出了可维护性的代价。你失去了 COBOL 老程序员,换来了 Java 程序员——他们看着这套代码,比老程序员还困惑。
matsemann 在 HN 上的第一句话就是这个意思:「以前至少还有几个 COBOL 老程序员懂这套系统。现在没人懂了。」你把一套没人懂的老系统,变成了一套没人懂的新系统。问题没有解决,只是换了个语言。
论文没说的东西
Locksmith Loop 的架构设计是对的。把 AI 限制在测试生成,把翻译和验证留给确定性代码——这可能是 AI 在严肃软件工程中唯一可靠的使用方式。
但它回避了一个更大的问题:迁移 COBOL 系统的真正瓶颈从来不是「翻译一段代码」,而是理解这个系统在做什么。二十年没人碰过的代码,没有文档,没有测试,原开发者已退休。你连它应该产生什么输出都不知道,你怎么验证 Oracle 的输出是对的?
论文假设 Oracle 是给定的——COBOL 原版能跑,能产生输出,用来和 Java 版对比。但在真实场景里,COBOL 系统可能已经跑不起来了,或者只能在一个无法复制的特定大型机环境里跑。Oracle 不是现成的,Oracle 本身就是一个需要重建的东西。
这不是 Locksmith Loop 的错。这是一个验证方法论文,不是迁移工程论文。但如果你正在认真考虑迁移 COBOL 系统,你需要知道的不是「AI 能做测试生成」,而是「你的 COBOL 程序还能跑吗?」
参考来源