用 AI agent 现代化复杂的遗留代码
Mistral 帮一家欧洲能源运营商把 40,000 行 Fortran 77 迁移到 C++:先建数值一致性校验台,再用一百多个 agent 补文档,最后落定「人操作编码—测试—评审 agent 工作流」的中间路线,而不是完全自主。
中文
复制

遗留的科学代码库会积累几十年,而当初的作者离开之后,埋在代码里的知识就很难再复原。此外,使用没有活跃开发者生态的语言,也意味着放弃了在别人工作之上继续搭建的机会。Mistral 帮一家欧洲能源运营商把 40,000 行 Fortran 77 迁移到 C++,那是一个计算密集的水藏模拟器,既没有测试套件,也没有集中的文档。
遗留代码现代化不只是代码翻译
把语法从一种语言翻译到另一种,大体上已经解决了。让任何一个近期模型把一个不算太冷门的语言片段翻成另一种,几轮之内大概就能得到可接受的结果。但把一个完整系统从过程式语言迁移到面向对象的 C++,就需要做架构重构,这让任务变得不平凡。
Fortran 77 是在 1977 年标准化的,正如名字所示,用它写的代码直接反映着那些限制:没有模块,没有命名空间,没有结构化类型。状态住在 COMMON 块里,那是整个程序共享的全局内存。变量按名字首字母隐式定类型,所以一个拼错的名字会悄悄创建一个新变量,而不是报编译错误。
举一个简单但有说明性的例子:下面是一段一阶泰勒展开的实现。输入和输出都是 COMMON 块里的全局变量,而 IC 之所以是整数,只因为它的名字以 I 到 N 之间的字母开头。变量名相当隐晦,因为长度被限制在 6 个字符以内。
SUBROUTINE GASDEN
INCLUDE 'common.h'
DO 10 IC = 1, NCELL
10 RHOG(IC) = ROG(IV) + DROG(IV)*(P(IC)-PTAB(IV))
END
在 C++ 里,就可以用显式类型、写面向对象的代码,并且把值返回而不是写进全局变量:
double gasDensity(const GasProperties& gas, double pressure) {
size_t i = lookup(gas.pressure, pressure);
return gas.density[i] + gas.slope[i] * (pressure - gas.pressure[i]);
}
散落各处的 COMMON 数组变成了一个 GasProperties 参数,网格循环被移到了调用方,所以两边没有逐行的对应关系可供核对,这正是让验证这次迁移变难的原因。
这些结构性差异,加上必须集成 PetSc 这类现代科学计算框架,在开始迁移之前就引出了几个重要问题:
- 怎么证明迁移后的代码库在数值上与遗留版本一致
- 怎么把迁移拆成可控的块
- 怎么最好地利用自主 agent 来加速这个过程
迁移遗留代码之前先建一致性校验台
在把 agent 放出去之前,我们需要一个办法来证明两个代码库是一致的。这里的「一致」指的是输出的数值相等:既包括最终结果,也包括客户方水藏工程师指定的一组关键中间点。
我们加了这些东西:
- 让 Fortran 代码库能导出自身状态的子程序
- 一个把检查点载入 C++ 的测试框架
- 用来引导 agent 正确使用它们的 Skill.md 文件

在迁移工作流里,agent 成功地给 Fortran 代码库加上了转储状态快照的埋点,并用 C++ 测试框架验证已迁移模块的正确性。
先把校验台的这一部分建起来,对这个项目是净收益:它让长时间的 agent 运行更安全,而数值一致又是一个容易验证、也有说服力的论据,用来说明一段代码已经成功迁移。我们认为这应该是任何代码现代化项目的头几步之一。
下面的例子说明了这一点。我们先往 Fortran 代码里插了一行,转储 RHOG 变量的值(这次运行里是 42.71834),然后把同一个值当作参考检查点,用来测试迁移后的 C++ 模块。

用 AI agent 理解和记录遗留代码库
这个项目的文档散落在旧 PDF 和埋在 Fortran 代码里的注释中,而整个工作最大的附带收益之一,就是把这些文档整理清楚、并搬到代码旁边。
幸运的是,Fortran 这类过程式代码有一个方便的性质:整个程序可以画成一棵调用者—被调用者树。我们用一个自制的解析器解析代码库、生成这棵树,然后用 Vibe CLI 拉起一百多个 agent 来为它写文档。每个 agent 都能通过文档库和 Mistral OCR 把相关的 PDF 拉进来。
从树的叶子开始往上走,每个节点都会派生一个子 agent 来为它写文档,并向原始仓库开一个 PR。一个以 cron 定时循环运行的评审 agent 会寻找新开的 PR,评审它们,并在需要时安排修复任务。

用 AI agent 做代码现代化
第一次尝试时,我们给了 agent 完全自主权:每个 Fortran 子程序一个 agent,各自在一周内独立把自己的函数翻译成 C++。结果是能跑,但称不上代码现代化。COMMON 块一比一变成了全局结构体。GOTO 驱动的控制流原样保留,而不是被重构成循环或提前返回。它看起来像是把 Fortran 用 C++ 语法重打了一遍,而不是现代化过的代码。
第二次尝试里,我们改用「给 agent 结构、而不只是自主权」来应对:一个规划者、一个编码者、一个测试者和一个代码质量评审者,在每个模块上协同工作。代码质量比第一次明显提升。但源码的复杂度最终还是追上了 agent。它们会撞上一个 bug,试着修几次,然后卡住,而没有人能介入。
我们最终落在中间地带:由一个人操作一条由编码、测试、评审 agent 组成的工作流,一个模块一个模块地迁移。这保住了第二次尝试的代码质量,同时加了一个人工检查点,在 agent 卡住时把它们解开。下一节详细讲这条工作流。
为复杂代码迁移运行结构化的 AI agent 工作流
代码库有了文档、一致性校验台也就位之后,剩下有趣的部分就是调校:我们能给 agent 多少自主权,同时还能拿到可合并的代码。两个极端我们都试过,从完全自主的运行到密切监督的手动会话。下面这条结构化工作流,是这个用例里我们最终落定的方案。
与客户的水藏工程师一起,我们用那棵调用者—被调用者树找出彼此独立的模块,也就是规模可控的自包含子树(经验上小于约 10,000 行 Fortran)。每个模块都走同一条工作流:
- 生成目标 C++ 架构。
- 由一位水藏工程师评审它。
- 通过之后,把它拆成一个任务队列。
- 每个任务跑一条实现子工作流:规划 → 实现 → 测试 → 重复。
- 由人评审产出的 PR,并提出修改要求,直到它们被合并。
认识 AI 辅助遗留代码现代化的边界
第一个冲刺覆盖了核心功能:300,000 行里的 40,000 行。这个 Fortran 代码库是自包含、可运行的,这是有利的起始条件。那些依赖外部系统、缺少可运行基线,或者编码着任何地方都没有记载的物理模型的迁移,会带来本文没有讨论的额外挑战。
现代化复杂遗留系统的三条原则
这个项目里的三条经验,应该能沿用到任何大规模遗留迁移上。
- 在写迁移代码之前先建一致性校验台:数值一致是证明一个模块完成的最便宜、最有说服力的方式。
- 在依赖 agent 之前先把文档理清,因为你没法迁移没人读得懂的代码。
- 在这个规模上,带人工评审关卡的结构化工作流,胜过完全自主,也胜过纯手动的会话。
来源: Mistral AI← 返回首页