一张视觉隐喻图:扁平的文本文档与从遗留代码中生长出的、丰富且相互关联的图结构形成对比,聚焦 COBOL 到 Java 迁移场景。
Artificial IntelligenceSoftware EngineeringTechnology

AI 完美翻译了 30 年的 COBOL 代码,然后它搞崩了数据库

Ashutosh SinghalAshutosh Singhal2026年2月5日16 min

那是一个周二的晚上,我盯着一段毫无意义的堆栈跟踪。

我们当时正与一个金融服务团队合作,尝试将一个核心交易模块从 COBOL 迁移到 Java。AI 已经完成了它的工作——或者我们以为如此。生成的 Java 代码干净、结构良好,编译时没有一个错误。单元测试全部通过。电话会议上的每个人都持谨慎乐观的态度。然后他们把它部署到测试环境,第一笔电汇就损坏了数据库。

这个 bug 不在 Java 里。Java 代码是语法上完美无缺的。这个 bug 藏在 AI 从未看到的地方。

一个名为 TRN-LIMIT 的变量——它并未定义在 AI 所翻译的源文件中,而是定义在执行链中早了数千行的一个 COPYBOOK 里——包含一个 REDEFINES 子句。这是一种 COBOL 结构,即同一个内存地址会根据一个在完全不同的模块中设置的标志,被解释为两种不同的数据类型。AI 把 TRN-LIMIT 看作一个简单的数值字段。但它不是。它是一个伪装成整数的紧缩十进制数(packed decimal),具体取决于运行时上下文。AI 幻想出了一个标准定义,于是 Java 应用程序把损坏的二进制数据写入了数据库的一列。

那天晚上,我和团队坐在会议室里剖析问题出在哪里,我意识到了一件将重塑我们在 Veriprajna 所构建的一切的事情:AI 失败,不是因为它愚蠢。它失败,是因为它是盲的。

那个1.52万亿美元、却没人愿意谈的问题

这就是2025年全球经济令人不安的现实:43%的银行系统仍运行在 COBOL 上,而这些系统处理着全部 ATM 交易的95%。运营《财富》500强企业的软件呢?其中大约70%是二十多年前编写的。仅在美国,技术债务就已膨胀到估计1.52万亿美元。

而编写这些代码的人正在退休。不是「也许某天会退休」——他们现在就在离开,带走了数十年积累的机构知识。与此同时,80%的联邦 IT 预算用于维持遗留系统的运转,只剩下勉强20%用于任何新事物。

我曾隔着桌子与一些 CTO 面对面,他们描述自己现代化处境的方式,就像你描述一栋地基正在崩塌的房子:你知道你需要修它,你知道拖延只会让它更糟,但每一个尝试过的承包商都只是让事情变得更昂贵,却没有真正解决问题。

数据印证了这一点。70%到80%的遗留系统现代化项目未能达成目标。而这还是在生成式 AI 进入这个领域之前的情况。

为什么大家都以为 GPT 能解决这个问题?

我理解。我真的理解。当 GPT-4 发布时,软件咨询市场进入了狂热状态。突然之间每家公司都有了一款「COBOL 迁移加速器」——而如果你掀开引擎盖看一看,它不过是套在基础模型外面的一层薄薄的封装。粘贴进你的 COBOL 段落,得到一个 Java 方法。魔法。

我和我的联合创始人花了数周时间评估这些工具。我们会把来自客户环境的真实遗留代码喂给它们,然后检查输出。语法几乎总是正确的。代码能编译。然后它就会以极难诊断的方式失败,因为代码的形态看起来是对的,即便其含义是错的。

最危险的 bug 不是让你的系统崩溃的那个。而是在任何人注意到之前,悄悄损坏你数据长达六个月的那个。

这个问题是架构性的,归根结底在于大语言模型处理信息的方式。LLM 使用一种注意力机制来权衡输入中不同部分的重要性。现代模型号称拥有高达一百万 token 的上下文窗口。但研究已经证明了一种被称为「迷失于中间」(Lost in the Middle)效应的现象:LLM 表现出一条 U 形性能曲线,能很好地回忆起提示词开头和结尾的信息,但对处于中间的任何内容,其表现会显著退化。

在一个现代化项目中,单个 COBOL 程序可能长达数千行,引用的 copybook 本身也可能长达数千行。如果 MAX-TRANSACTION-LIMIT 的定义恰好位于那段庞大上下文的中间,AI 在统计学上很可能会漏掉它。而当它漏掉某样东西时,它不会停下来发问。它会产生幻觉。它会虚构一个看似合理的定义,然后继续往下走。

当你把代码当成文本对待时,会发生什么?

一张并排对比图,展示标准向量 RAG 检索如何漏掉关键的代码依赖,与基于图的检索如何跨文件追溯逻辑连接。

这就是我看到整个「AI 封装」生态系统所犯的核心错误,也是我早期与一位潜在投资者反复争论的论点。他看着我们的方法——为代码仓库构建知识图谱——说:「为什么不干脆用更大的上下文窗口?GPT-5 会解决这个问题的。」

我在笔记本电脑上调出一个 COBOL 程序。「帮我找到 ACCOUNT-BALANCE 的定义,」我说。

他搜索了这个文件。找不到。因为它不在那个文件里。它在一个 copybook 里,通过第47行的一条语句被引入,而那个 copybook 本身又引用了一个由完全不同的团队维护的共享数据部(data division)。

「现在想象你是一个 LLM,」我说。「你正在做一次向量相似度搜索,寻找与『支付处理』相关的代码。你会找到五个提到『支付』这个词的代码块。你会完全错过那个名为 GlobalVarDef.cbl 的文件——它定义了支付逻辑所使用的税率——因为那个文件在任何地方都从未提到『支付』这个词。」

标准的检索增强生成(RAG)——大多数 AI 编码工具用来为 LLM 补充知识的技术——是基于文本相似度来检索上下文的。它把代码转换成向量,然后寻找相似的向量。这对 FAQ 聊天机器人来说效果极佳。但对代码而言,它是灾难性地不够用。

代码不是自然语言。「猫坐在垫子上」这句话,无论你五十页之前读了什么,含义大致都一样。但 x = y + 1毫无意义,除非你知道 xy 的定义、类型和当前状态——而它们可能定义在另一个文件、另一个模块中,或者继承自某个父类。

我在我们研究的交互式版本中深入探讨了这个结构性问题。简短的说法是:软件不是文本。软件是一张图。

我们停止打造更好封装的那个夜晚

有那么一个时刻——我记得很清楚——我的团队正在争论我们的架构。我们面前有两条路。第一条路:构建一个更聪明的 RAG 流水线。更好的分块、更好的嵌入、更好的提示词。在封装这条路上不断迭代,直到它足够好用。第二条路:彻底抛弃基于文本的范式,把代码当作它真正的样子来对待——一个由逻辑构成的关系系统。

第一条路更快。第一条路是投资者能理解的。第一条路已经有十几个竞争对手在证明市场需求。

我的首席工程师走到白板前,把一个 COBOL 程序画成了一张图。节点代表变量、函数、copybook、数据库表。边代表 CALLSREADSUPDATES_TABLEIMPORTS_COPYBOOK。然后她追踪了一条依赖链:模块 A 调用模块 B,模块 B 修改变量 X,而变量 X 又被位于完全不同目录中的模块 C 读取。

「让向量搜索去找出那条链,」她说。

没人能找到。

那就是我们下定决心去构建如今我们称之为仓库感知知识图谱(Repository-Aware Knowledge Graph)的那个夜晚——一个统一的图数据库,它将代码的静态结构(抽象语法树、调用图)与业务逻辑的语义含义(文档、注释、变量意图)结合在一起。我们不打算构建一个更好的翻译器。我们要构建一张地图。

你如何把三十年的 COBOL 变成一张地图?

一张四阶段流水线图,展示知识图谱的构建过程:结构化解析、实体/关系抽取、跨仓库符号解析,以及传递闭包计算。

这个过程有四个阶段,我就不给你讲实现细节了——你可以在我们完整的技术深度剖析中找到那些细节。但概念很重要,因为它们解释了为什么这种方法能在封装失败的地方奏效。

第一,我们对代码进行结构化解析,而非文本化解析。 标准的 RAG 流水线使用「朴素切分」——它们每500个 token 就切一刀,常常把一个函数的签名与其函数体割裂开。我们使用像 Tree-sitter 这样的解析器来生成抽象语法树,它尊重代码的逻辑边界。一个函数被当作一个完整的逻辑单元来对待,而不是一段随意的文本跨度。

第二,我们抽取实体和关系。 类、段落、变量、数据库表、API 端点——这些成为节点。它们之间的边——CALLSUPDATES_TABLEDEFINES_VARIABLE——成为连接组织。现在我们可以查询这张图:「给我看每一个更新 CUSTOMER-ID 字段的段落。」精确结果,即时呈现。用 grep 试试看。

第三——这里开始变得有趣——我们在整个仓库范围内解析符号。 一个标准解析器会把文件 A 中的 ACCT-NUM 和文件 B 中的 ACCT-NUM 看作两个不同的字符串。我们的系统判定两者都指向某个共享 copybook 中的同一条目,并将它们合并成一个单一节点。我们还把文档与代码合并:如果一份 PDF 需求文档描述了「User API」,而代码中包含一个名为 UserAPI 的类,系统就会把意图与实现连接起来。

第四,我们计算传递闭包。 还记得那次银行故障吗?A 依赖 B,B 依赖 C,而 AI 看到了 A 却漏掉了 C。我们的图会深度遍历——从 A 到 B 到 C——以识别每个变量的根定义。当 AI 为模块 A 生成代码时,它会从模块 C 导入正确的定义,哪怕模块 C 位于完全不同的目录或仓库中。

为什么标准 RAG 在代码迁移中会失败?

人们总是对此提出反驳。「RAG 用在代码上没问题啊,」他们会说。「用更好的嵌入就行了。」

让我给你三个向量相似度搜索彻底崩溃的场景:

一位开发者把 Account 重命名为 Acct。语义相似度下降了,尽管逻辑完全相同。一个名为 FNC-001 的函数执行利息计算,但不含任何注释——搜索「利息计算」永远也找不到它。而最常见的失败是:向量 RAG 检索到了一个提到「支付」的单元测试和一段 UI 注释,却漏掉了核心业务逻辑,因为变量名与查询词不匹配。

基于图的检索不问「哪些文本看起来相似?」它问「哪些东西在逻辑上是相连的?」——而这个区别,就是能编译的代码与真正能工作的代码之间的区别。

我们称之为 GraphRAG 的东西,是在结构上运作,而非在相似度上运作。当有人问「重构支付逻辑」时,系统用向量搜索来找到入口点——比如说 ProcessPayment 段落。但接下来,它不会就此停下,而是遍历图的边。它通过 CALLS 边拉入子例程,通过 READS 边拉入变量定义,通过 INCLUDES 边拉入 copybook。这些相连的部分在文本上或许并不相似,但在逻辑上密不可分。

研究表明,在多跳推理——连接被数个步骤分隔开的事实——方面,GraphRAG 显著优于向量 RAG。在软件中,几乎每一个严重的 bug 都是多跳推理的失败。如果我修改了模块 A 中的利率逻辑,模块 Z 中的哪些报表界面会崩溃?向量 RAG 回答不了这个问题。而图可以,因为它会遍历将它们连接起来的那条函数调用链。

GOTO 问题(或者说:为什么 COBOL 会让 AI 幻想出循环)

一张图,展示 COBOL 的 GOTO 跳转如何被映射为控制流图中的边,然后被模式匹配为恰当的 Java 控制结构(循环、条件语句、返回语句)。

我想跟你讲一个几乎击垮我们的具体技术挑战,因为它说明了为什么这项工作比人们所以为的要难得多。

COBOL 有一个 GOTO 语句。Java 没有。GOTO 允许程序执行跳转到任何地方——向前、向后、跳进另一个代码块的中间。它制造出每一位计算机科学教授都警告你要提防的「意大利面条式代码」。翻译 GOTO 不是一个语法问题。它是一个拓扑问题。

我们看着三款不同的商业 AI 工具尝试翻译一个大量使用 GOTO 的 COBOL 模块。一款生成了一个会在生产环境中导致 StackOverflowError 的递归函数调用。另一款产出了一个没有退出条件的 while(true) 循环。第三款——我个人最喜欢的——干脆凭空发明了一段原始代码中根本不存在的控制流。它看起来合情合理。它完全是错的。

我们的方法:把 GOTO 的目标映射为控制流图中的边。然后在图上使用模式识别。一个跳回到较早标签的 GOTO?那是一个循环。一个跳过某个代码块的 GOTO?那是一个条件语句。一个跳向退出段落的 GOTO?那是一个返回语句。在图结构的引导下,AI 把这些跳转重构为 while 循环、if/else 代码块,或 break/continue 语句。

没有图,AI 是在猜测。有了图,它是在做工程。

聊天机器人与智能体之间的区别

我们不构建聊天机器人。我需要把这一点讲清楚,因为市场上充斥着让你「与你的代码库聊天」的工具,而它们并不是一回事。

聊天机器人接收你的问题,发送给 GPT-4,然后返回它给回来的任何东西。如果输出是错的,你就得手动调试。这就是市面上每一款 AI 封装的工作流程。

我们所部署的,是能够规划、执行并自我纠正的自主智能体。该智能体分析目标 COBOL 文件的 AST,识别依赖关系,查询知识图谱,生成 Java 代码,然后在沙箱中编译它。如果编译器抛出一个错误——「找不到变量」——智能体会读取该错误,向图查询缺失的依赖,然后重新生成。接着它运行从原始 COBOL 执行轨迹派生出的单元测试,以验证行为等价性。

这个「编译—修复」循环把验证的负担从人转移到了系统。但是——这一点在受监管的行业里极其重要——知识图谱提供了完全的可解释性。开发者可以清楚地看到 AI 做出每一个决策的原因:「AI 导入了 com.bank.logic,因为它发现了对 COPYBOOK-X 的一个依赖。」而不是「相信我,我是 AI。」而是:这就是这段逻辑的引证链。

在银行业,每一行代码都必须是可审计的。你不能部署一个「大概」做对了的黑盒子。你需要一个能展示其工作过程的系统。

那死代码怎么办?

有一件事让我很意外:遗留系统里充满了无人再使用的代码。旧的促销活动、已下架的产品、1997年的调试例程。基于文本的 AI 会把给它的一切都迁移过去——它无法区分活代码和死代码。

我们的调用图会识别出不可达的节点——那些没有入边的段落或文件,意味着没有任何东西调用它们。我们在迁移开始之前就把这些死代码标记出来以待删除。根据我们的经验,这通常能把代码库缩减20-30%。这不是一个微不足道的优化。这是消除了四分之一的工作量和四分之一的攻击面。

「更大的上下文窗口难道不能解决这个问题吗?」

我仍然不断收到这个问题。人们的假设是:如果 GPT-5 或 Claude 4 能够处理一千万个 token,「迷失于中间」的问题就会消失。

它不会消失。原因如下。

即便注意力退化有所改善——它确实会改善——你仍然是在做文本检索。你仍然是在搜索相似的字符串,而不是在遍历逻辑连接。如果你需要的那个变量定义在一个与你正在翻译的文件毫无共同关键词的文件里,那么一个百万 token 的上下文窗口也帮不了你。问题不在于窗口的大小。问题在于这扇窗户看的是错误的东西

我听到的另一个反对意见是:「知识图谱构建起来很昂贵。」确实如此。解析整个仓库、解析符号、计算传递闭包——这是一笔可观的前期投入。但请想想另一种选择。人工迁移一个大型 COBOL 系统要花费数千万美元,并且需要数年时间。基于封装的 AI 迁移前期成本更低,但会源源不断地产生由幻觉引发的 bug,需要昂贵的人工调试。基于图的方法搭建成本更高,但返工成本却大幅降低。麦肯锡的数据表明,生成式 AI 能把编码任务减少50%,但前提是正确地部署。与标准 AI 工具相比,我们已经看到了2到3倍的开发者生产力提升,具体原因就在于开发者不再花数小时去寻找某个变量究竟在哪里定义。

地图就是资产

这是我希望自己在一开始就明白的道理:知识图谱不仅仅是一个迁移工具。它是一项永久的资产。

一旦你的代码库以图的形式存在,它就会一直是你系统的一个鲜活表示。随着新的 Java 代码不断演进,图也随之更新。你会得到永远保持最新的自动化文档。你会得到架构漂移检测——如果新代码违反了你所定义的模块化规则,系统会向你发出警报。你会得到按需的影响分析:「如果我修改这个方法,什么会崩掉?」

现代化不是一次性事件。它是一个生命周期。那些把它当作一个项目——有开始日期和结束日期——来对待的组织,正是五年后又回到起点、淹没在新一代技术债务中的那些组织。

代码不是文本

我反复回到的这个教训——来自那个盯着堆栈跟踪的周二夜晚的教训——看似简单得具有欺骗性:代码不是文本,而把代码当成文本对待的工具,会产出看起来正确、行为却错误的结果。

整个「AI 封装」经济,是建立在一个范畴错误之上的。它假定,因为 LLM 在处理语言方面异常出色,它们就必定在处理代码方面异常出色。但代码不是语言。代码是一张图——一个由依赖、数据流和状态变化构成的、密集而相互关联的系统,它同时存在于多个维度之中。试图用基于文本的工具去把它现代化,就好比只靠一份街道名称清单、却没有地图来在一座城市中导航。你会「迷失于中间」。

我们构建了那张地图。而它确实奏效——不是因为我们比那些构建基础模型的团队更聪明,而是因为我们问了一个不同的问题。他们问的是:「我们怎样让 AI 更好地理解文本?」我们问的是:「如果问题根本就不在文本呢?」

遗留系统现代化的未来,不是一个更大的语言模型。而是一个以软件真正运作的方式——作为结构,而非字符串——来理解软件的系统。

这就是我们在 Veriprajna 押下的赌注。每一天,都有又一个组织发现,它们由 AI 生成的 Java 编译得漂漂亮亮,运行起来却灾难性地失败。每一天,语法翻译与语义理解之间的鸿沟,其被忽视的代价都在变得更高。那些弥合这道鸿沟的组织,将不只是把它们的代码现代化。它们将终于真正理解自己的代码——其中许多组织还是有史以来第一次。

Related Research

Also Published On