
单个 COBOL 文件把除了那条要紧事实之外的一切都告诉了 AI,所以我先建了地图。
引发这一切的那行 COBOL 只有三个词,而每一个都在骗我。
COMPUTE WS-RESULT = WS-WIRE-AMOUNT - TRN-LIMIT. 我当时在为一个中型银行的电汇子系统搭建演示用代码资产,而这正是位于名为WIRETXN的程序中的那行要命代码。它看起来像一年级学生都能移植的算术:从一个金额里减去一个限额,再写出结果。如果你把那单个文件交给任何一个现代模型,让它生成 Java,大约四秒内它就会给你干净、能编译、能通过单元测试的 Java。它会把TRN-LIMIT写成long。而在第一笔真实电汇时,它就会把损坏的字节写进生产数据库。
我之所以知道,是因为TRN-LIMIT并不是long。它是一个压缩十进制COMP-3字段,定义在三个文件之外,其运行时解释由一个完全不同的程序里设置的标志决定,而该标志又由凌晨两点运行的批处理作业排序。这一切均不可见于WIRETXN。那个包含危险COMPUTE的文件,并不包含任何使它危险的事实。
正是这个缺口,让我建造了CodeGraph,而这篇文章讲的是我在那条路上犯过的错。一开始我确信问题出在翻译质量。我错了。真正的问题是模型看不到它需要看到的东西,而我花了一段时间向自己证明:无论怎么「给它更多上下文」,都解决不了这个问题。
那行看起来安全、其实并不安全的 COMPUTE
我先手工映射了这次电汇改动,然后才敢信任任何工具去做,而一次正确迁移必须知道的事实恰好有九条。
其中三条住在WIRETXN里,对单文件阅读者来说是真正可见的。WIRETXN使用TRN-LIMIT于那条COMPUTE中,位于第 33 行。它导入了一本名为CBACCT的 copybook,仅按名称,在第 13 行。它发出一次UPDATE,针对 DB2 表ACCOUNTS,位于第 37 行。文本窗口工具能看到这三条。如果这些就是全部事实,朴素移植就会没问题。
另外六条才是伤人的。TRN-LIMIT被声明为PIC S9(9)V99 COMP-3,位于CBACCT.cpy第 11 行,这意味着压缩十进制,亦即BigDecimal在 Java 中,而绝不是long。紧挨着它下面,TRN-LIMIT-ALPHA REDEFINES TRN-LIMIT,把同一段六个字节叠在该字段上当作原始文本。第三个字段LIMIT-TYPE-FLAG在运行时决定这两种解释里哪一种是生效的。该标志由一个名为LIMITSET的程序写入,也由一个名为BATCHUPD的夜间批处理作业再次写入。而 JCL 作业NIGHTLY在 02:00 作为电汇作业的前置运行,这是整个资产里唯一记录了「设置标志」与「执行转账」之间顺序的地方。

六条事实。每一条都为真,每一条都是承重的,每一条从真正做计算的那个文件里在结构上都不可见。当我把它们这样排开时,让我不安的并不是朴素移植是错的。而是朴素移植根本无从知道自己错了。它读了被交给它的那一个文件,而那一个文件对六条要紧的事实沉默不语。
包含那行危险代码的文件,并不包含任何使它危险的事实。这不是翻译缺陷。这是披着翻译缺陷外衣的检索失败。
我为什么不再试图把上下文窗口做大
我的第一反应和眼下每个人的本能一样,我也想老实承认自己追过一阵:只要再多给模型一些就行。
这套推理听起来无懈可击。如果失败是因为模型只看到一个文件,那就连 copybook 一起喂给它。把触碰该标志的程序也喂进去。把 JCL 也喂进去。上下文窗口如今已经巨大,每个季度还在变大,所以答案显然是别再吝啬,把整片邻域代码一股脑倒进提示词。我真心以为这会奏效;对一个玩具例子,它勉强能行——因为当你已经知道该粘贴哪六个文件时,你其实已经手工解决了真正的问题。
裂缝就在这里。要把正确的上下文喂给模型,我首先得知道什么才是正确的上下文。而知道TRN-LIMIT的类型由写在BATCHUPD中的标志决定,并由 02:00 的 JCL 作业排序——这并不是靠把WIRETXN读得更用力就能挖出来的。那是你只有先沿着依赖图追踪完才能得到的东西。上下文窗口并不会告诉你该往上下文窗口里放什么。我一直在用答案本身去回答那个问题。
随后数字把这一点钉死了。这些银行实际运行的资产不是六个文件。它们是一百万到一千万行 COBOL,有时更多;全行业仍有 2200 亿行处于活跃生产(行业元分析,2025)。一次真实的电汇改动,其传递闭包可能是四十个文件,也可能是四百个。那永远塞不进上下文窗口——今天不行,三年后发布的模型版本也不行——因为资产比窗口涨得更快,而窗口本来就不是约束。约束是:在一千万行里知道这四十个文件是哪四十个,并证明你找到的是全部四十个,而不是三十八个。
更大的上下文窗口,是对我已不再追问的那个问题的更好答案。问题不是「模型能不能装下更多代码」,而是「哪段代码,以及你如何证明那就是全部」。
这一重框定,正是 CodeGraph 不是翻译器的全部理由。我刻意不做粘贴 COBOL、交回 Java 这种事。地图才是产品,而翻译是地图存在之后任何工具都能做的下游用例。我构建的是底下的理解层。在已交付的夹具上,该资产解析为一张 47 个节点、70 条边的类型化知识图谱;「一次改动的影响」是一次图遍历——改动触及的一切的传递闭包——每条边都带着它来自的file:line。这刻意是无聊的纯 Python 图工作,热路径上没有模型,因为我需要它的不是聪明。我需要它完整且可复现。同一夹具进,同一闭包出,每一次都如此。
我把它当作一条规则反复对自己说。智能体建议,代码裁决。演示里可选的语言层——用白话回答关于闭包的问题的那部分——默认关闭,并锁在密钥后面。价值并不依赖它。价值在于检索与证明,而这两者都不是模型能力。
朴素视图到底删掉了什么?
我特意在演示里做了一个开关,好看着那六条事实消失,因为在亲眼看见之前,我并没有完全相信这种失败。
勾选「朴素 AI 上下文视图」,图就会塌缩到单个源文件加上改动周围若干行的窗口——这正是文本窗口工具喂给模型的东西。原本显示 9/9 的面板掉到 3/9。文件内的三条事实保持点亮。另外六条变灰、沉默:COMP-3类型、REDEFINES叠加、控制标志、它的两个跨模块写入者,以及 02:00 的 JCL 前置。一条红色横幅用应用自己的话说出后果:只拿到三条可见事实时,模型会发出long TRN_LIMIT并损坏数据库。

我想在这里谨慎一些,因为这正是创始人容易夸大其词的地方。9 对 3 的结果测量于已交付的合成电汇夹具——我为这次演示手工撰写的资产,正是为了让真实依赖集已知,召回数字成为有标签的真测量,而不是感觉。它不是对你的 COBOL 的保证。朴素视图是模拟,不是实时 z/OS 流水线。图在内存中,底下是 SQLite,不是生产级图平台。我造了一家合成银行,因为我不能合乎伦理地向你展示一家真实银行,也因为已知真值是唯一诚实的方式,好说「图拿到全部九条,单文件拿到三条」。
但失败的形态并不是合成的,而这才是要紧的部分。那个COMP-3字段——类型在别处决定、标志由批处理作业设置、顺序只存在于 JCL——这些是四十年银行资产的寻常纹理,不是奇异边角案例。当大约 70% 到 80% 的大型机现代化项目未能达成目标(行业元分析,2025)时,我不再认为是因为翻译步骤差。翻译步骤没问题。它被喂的是一张裁掉了六条最重要事实的图。
没有证明,就不算数
我最自豪的功能,是那个承认自己做不到什么的功能;直到一次合规对话替我重新框定,我才真正体会到这一点。
工程师要的是正确的迁移。监管者要的是不同且更难的东西:证据。在 DORA 下,银行欠一份 ICT 资产清单。在 SOC-2 下,它欠变更控制回执。模型说「相信我,我找到了依赖」满足不了其中任何一项。他们需要完整性证明——工具实际能解析代码库多少的声明,以及更重要的,对它未能解析之物的诚实标注。于是我建了一道完整性门控。夹具里每一次PERFORM、CALL、COPY以及 DB2 引用都必须解析到图中的真实节点,否则标为「需要复核」。任何东西都不允许悄然消失。
在夹具上,该门控解析了 34 个引用中的 33 个,覆盖率 97.1%。它无法解析的那一个是名为DISPATCH的程序,它做一次动态CALL WS-PROGNAME——一个运行时计算的目标,任何静态解析器都跟不上,因为目标要等程序运行才知道。而正确做法不是去猜。而是举起一面旗,写着「这里需要人来看一眼」,并把它留在报告里。

那次被标注的DISPATCH调用是整次构建里我最爱的东西,我说真的。一个解析 97%、并确切告诉你哪 3% 它做不到的工具,比一个声称 100% 却藏着缺口的工具更有价值,因为那个隐藏的缺口,正是损坏电汇栖身之处。完整性门控产出可导出的「代码库拓扑与完整性报告」——一份 JSON 和一份可打印 HTML,含节点与边摘要、带file:line溯源的逐模块闭包、召回结果,以及带时间戳的标注项。那份产物才是重点。它是你可以交给监管者、下个季度再跑一遍、因为确定性而得到相同答案的东西。
我宁愿交付一个承认自身空洞的数字,也不要一个更圆滑却把它藏起来的数字。被标注的动态 CALL 不是演示的弱点。它就是演示。
这也是不会过时的那部分。一个完美模型——从不幻觉出一行 Java——仍然无法向监管者证明它检索到了哪些依赖。它仍然无法静态跟上一次运行时计算的CALL,因为那是代码的属性,不是阅读者的属性。溯源与完整性是你围绕模型构建的系统的属性,不是靠扩大规模就能解锁的能力。
你触碰事物的顺序
图最后给我的,是一件我起初甚至没打算建造的东西:一套安全的开工顺序。
一旦你有了完整的依赖拓扑,就可以按缠结程度给每个程序打分。我用一个朴素公式,把耦合对COMP-3陷阱、JCL 关键性与未解析调用加权,把夹具里的十四个程序排成绞杀榕提取顺序。风险最低的程序最先提取,上帝程序最后。在夹具上,AUDITLOG排在第 1 位,风险分为零,因为它没有耦合,也没有任何东西依赖它正确。那是安全的起点。WIRETXN程序——我们一直担心的那个——排在第 11 位,带着它那一个COMP-3陷阱与它的 JCL 关键性。DISPATCH带着未解析的动态调用,排在第 12 位。而ACCTMGR——一切倚靠的上帝程序——排在最后第 14 位,风险分 15。

我没料到自己会像现在这样在意排序。但这是同一课的第三次。你可以安全从哪里开始,是关于拓扑的事实,不是你在规划会上争论的意见。一支盯着百万行代码的团队,其实并不在争论如何翻译一个段落。他们无休止、昂贵地争论的是从哪里开始,以及先碰错东西会弄坏什么。那是一个图问题,而图每次运行都以同样的方式回答。
提取顺序、完整性门控、影响闭包——它们是从三个角度看的同一个对象。检索真正的切片,证明它是完整切片,再按风险给切片排序。这三样都不是翻译问题,也都不是更聪明的模型能解决的。
我不断回到的那个问题
我开始对每一个见到的 AI 现代化推销——包括我自己的——只问一个问题,而它悄悄成了我唯一信任的问题。
不是「它能不能写出好的 Java」,因为答案几乎总是能,而且几乎从来无关紧要。更难的问题是TRN-LIMIT那行教会我的:它能不能此刻证明检索到了哪些依赖,而这份证明能否扛住一个想让它失败的监管者。如果工具无法向我展示带file:line溯源的闭包,也无法诚实地告诉我它未能解析什么,那么输出看起来再流畅也无关紧要。那是带着好语法的猜测,而我亲眼见过正是那种猜测把long打在压缩十进制字段上,然后伸向数据库。
行业花了十年把翻译步骤做得更好,而 70% 到 80% 的项目仍未达成目标(行业元分析,2025),我认为那是因为风险从来就不在翻译步骤。风险住在拓扑里,住在六条看不见的事实里,住在凌晨两点设置的那个标志里。如果你想看图把那六条事实找回来、再诚实地标出它做不到的那一条,演示在这里:veriprajna.com/zh-Hans/demos/legacy-cobol-modernization。
如果你宁愿看它跑起来,而不是听我描述,这里是整件事端到端运行的样子。
我不再相信下一次模型发布会解锁这些迁移。更大的窗口能装下更多代码;它不知道是哪段代码,也无法证明它找到了全部。当我敲下解析器第一行时这就是真的,而我相信在我用来建造它的那个模型退役很久之后,这依然是真的。地图从来都是难的那部分。我们只是一直盯着翻译,因为那是我们知道怎么打分的部分。


