![]()
![]()
Mistral 帮助一家欧洲能源运营商将 40,000 行 Fortran 77 代码迁移到 C++。了解这一过程是如何完成的,以及值得借鉴的经验教训。
遗留科学代码库历经数十年不断累积,当最初的作者离开后,代码中蕴含的知识便难以恢复。此外,使用没有活跃开发者生态的语言,意味着错失在他人工作成果之上继续构建的机会。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, NCELL10 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 等现代科学计算框架的要求,在迁移开始之前就引发了一些重要问题:
如何证明迁移后的代码库在数值上与旧代码一致
如何将迁移拆分成可管理的多个部分
如何最好地利用自主智能体来加速这一过程
在迁移旧代码之前构建一个对等性测试框架。
在放开智能体之前,我们需要一种方法来证明两套代码库是一致的。这里的“一致”指的是输出在数值上完全相等——既包括最终结果,也包括油藏工程师在客户端标记的一组关键中间点。
我们增加了:
用于导出 Fortran 代码库状态的子程序
一个用于将检查点加载到 C++ 中的测试框架
Skill.md 文件,用于引导智能体正确使用这些工具
在迁移工作流中,智能体成功地对 Fortran 代码库进行了插桩以转储状态快照,并使用 C++ 测试框架验证了迁移模块的正确性。
先构建这部分测试平台对项目来说是一项净收益投资:它让长时间的智能体运行更加安全,而且数值一致性是一个易于验证且极具说服力的论据,能够证明一段代码已成功迁移。我们认为这应该是任何代码现代化项目的首要步骤之一。
下面的示例说明了这一点。我们首先在 Fortran 代码中插入了一行,用于转储 RHOG 变量的值(本次运行中为 42.71834),然后在测试迁移后的 C++ 模块时,将该值用作参考检查点。
使用 AI 智能体来理解和记录遗留代码库。
该项目的文档散落在旧版 PDF 和 Fortran 代码中埋藏的注释里,而整个工作最大的附带成果之一,就是将这些文档整理对齐,并移到代码旁边。
幸运的是,像 Fortran 这样的过程式代码有一个便利特性:整个程序可以绘制成一棵单一的调用者-被调用者树。我们通过自定义解析器解析代码库生成这棵树,然后使用 Vibe CLI 派生出上百个智能体来为其编写文档。每个智能体都可以通过文档库和 Mistral OCR 拉取相关的 PDF。
从树的叶子节点开始向上推进,每个节点都会派生一个子智能体来为其编写文档,并向原始仓库发起一个 PR。一个审查智能体按 cron 计划循环运行,查找新开的 PR,进行审查并在必要时安排修复任务。
利用 AI 智能体实现代码现代化。
在第一次尝试中,我们给了智能体完全的自主权:每个 Fortran 子程序对应一个智能体,各自在一周内独立地将其函数翻译成 C++。结果虽然可用,但称不上代码现代化。COMMON 块变成了一对一的全局结构体。GOTO 驱动的控制流原封不动,没有被重构为循环或提前返回。这看起来像是用 C++ 语法重打的 Fortran,而不是现代化的代码。
第二次尝试中,我们通过给智能体赋予结构而非仅仅自主性来解决这个问题:一个规划者、一个编码者、一个测试者和一个代码质量审查者协同处理每个模块。代码质量相比第一次尝试有了显著提升。但源代码的复杂度最终还是让智能体难以招架。它们会遇到一个 bug,尝试几次修复后便停滞不前,而当时没有人可以介入干预。
我们最终找到了一个折中方案:由人类操作一个由编码者、测试者和审查者智能体组成的工作流,逐模块迁移代码库。这既保留了第二次尝试的代码质量,又增加了人工检查点,以便在智能体卡住时为其解围。下一节将详细介绍这一工作流。
运行结构化的 AI 智能体工作流,以完成复杂的代码迁移。
在代码库完成文档化、对等性测试框架就绪之后,剩下的有趣部分就是调优:我们究竟能给智能体多大自主权,同时仍能产出可合并的代码。我们尝试了两个极端,从完全自主运行到严密监督的人工会话。下面的结构化工作流就是我们针对这一用例最终确定的方案。
我们与客户的油藏工程师合作,利用调用者-被调用者关系树来识别独立模块——即规模可控的自包含子树(根据经验,少于约 10,000 行 Fortran 代码)。每个模块都经过相同的工作流:
生成目标 C++ 架构。
与油藏工程师一起审查该架构。
获批后,将其拆分为一个任务队列。
为每个任务运行一个实现子工作流:规划 → 实现 → 测试 → 重复。
由人类审查生成的 PR,并提出修改意见,直到合并为止。
认识到 AI 辅助遗留代码现代化的局限性。
第一个冲刺周期覆盖了核心功能:30 万行代码中的 4 万行。该 Fortran 代码库是自包含且可运行的,这是一个有利的起始条件。依赖外部系统、缺乏可运行基线或包含未在任何文档中记录的物理知识的迁移,会带来本文未讨论的额外挑战。
应用三项原则来现代化复杂的遗留系统。
这个项目的三条经验应该适用于任何大型遗留系统迁移。
在编写迁移代码之前先构建一致性测试框架——数值一致性是证明模块已完成的最廉价、最有说服力的方式。
在依赖智能体之前先把文档整理好,因为你无法迁移没人能读懂的代码。
在这种规模下,带有审查关卡的结构化工作流胜过完全自主和纯手动操作。
我们正在招聘!
Mistral 的应用 AI 团队由一群工程师组成,他们围绕 Mistral 模型和企业级平台构建全栈解决方案。我们交付高价值、面向特定领域的解决方案,以攻克一些世界上最棘手的问题。
如果你希望参与这类工作,欢迎申请加入我们的团队。
![]()
![]()
Mistral helped a European energy operator migrate 40,000 lines of Fortran 77 to C++. Learn how it was done, and the lessons to carry forward.
Legacy scientific codebases accumulate over decades, and when original authors leave, the knowledge embedded in the code becomes hard to recover. Moreover, using languages with no active developer ecosystem means missing out on the opportunity to build on top of others’ work. Mistral helped a European energy operator migrate 40,000 lines of Fortran 77 to C++, a physics-intensive reservoir simulator with no test suite and no centralized documentation.
Moving beyond code translation in legacy code modernization.
Translating syntax from one language to another is a largely solved task. Asking any recent model to translate a snippet from a non-completely-obscure language to another, will likely converge to an acceptable outcome in few iterations. However, migrating a full system from a procedural language to object-oriented C++ creates the need for architectural refactors that make the task non-trivial.
Fortran 77 was standardized in 1977, as the name may suggest, and code written in it reflects those constraints directly: no modules, no namespaces, no structured types. State lives in COMMON blocks—global memory shared across the entire program. Variables are implicitly typed by their first letter, so a misspelled name silently creates a new variable instead of raising a compiler error.
For a simple but illustrative example, take the following implementation of a first-degree Taylor expansion. Inputs and outputs are globals in a COMMON block, and IC is an integer only because its name starts with a letter between I and N. Variable names are quite cryptic, as their length is capped to 6 characters.
SUBROUTINE GASDEN INCLUDE 'common.h' DO 10 IC = 1, NCELL10 RHOG(IC) = ROG(IV) + DROG(IV)*(P(IC)-PTAB(IV)) END
In C++, one will be able to leverage explicit types, write object-oriented code, and return a value returned instead of a global write:
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]);}
The scattered COMMON arrays become a single GasProperties parameter and the grid loop moved out to the caller, so there's no line-for-line correspondence to check, which is what makes verifying the migration hard.
These structural differences, plus the requirement to integrate modern scientific computing frameworks such as PetSc, raised a few important questions even before starting the migration:
How to prove the migrated codebase matches the legacy one numerically
How to split the migration into manageable chunks
How to best use autonomous agents to speed up the process
Building a parity harness before migrating legacy code.
Before letting agents loose, we needed a way to prove the two codebases agreed. Here, "agreement" meant numerical equality of the outputs—both the final results and a set of critical intermediate points flagged by the client's reservoir engineers.
We added:
subroutines allowing to export the state of the Fortran codebase
a test framework to load the checkpoints into C++
Skill.md files to steer agents into using them correctly
In the migration workflows, agents successfully instrumented the Fortran codebase to dump state snapshots and used the C++ test framework to verify correctness of migrated modules.
Building this part of the harness first was a net-positive investment for the project: it made long agent runs safer, and numerical parity is an easy-to-verify and compelling argument to show a piece of code has been successfully migrated. We believe this should be one of the first steps for any code modernization engagement.
The example below illustrates this. We first inserted a line into the Fortran code to dump the value of the RHOG variable (42.71834 in this run), and then used that same value as the reference checkpoint when testing the migrated C++ module.
Using AI agents to understand and document legacy codebases.
The project’s documentation was scattered across old PDFs and comments buried in the Fortran itself, and one of the largest side-wins of the whole effort was reconciling this documentation and moving it next to the code.
Luckily, procedural code like Fortran has a convenient property: the whole program can be drawn as a single caller-callee tree. We generated that tree by parsing the codebase with a custom parser, then used Vibe CLI to spawn over a hundred agents to document it. Each agent could pull in the relevant PDFs through document libraries and Mistral OCR.
Starting from the leaves of the tree and working upward, each node spawned a subagent to document it and open a PR to the original repository. A reviewer agent running in a loop on a cron schedule looked for newly opened PRs, reviewing them and scheduling fix tasks when necessary.
Utilizing the AI agents for code modernization.
On our first attempt, we gave agents full autonomy: one agent per Fortran subroutine, each translating its function to C++ independently over the course of a week. The result was functional, but it couldn't be called code modernization. COMMON blocks became global structs, one-to-one. GOTO-driven control flow stayed intact instead of being restructured into loops or early returns. It looked like Fortran retyped in C++ syntax rather than modernized code.
In the second attempt, we addressed this by giving agents structure instead of just autonomy: a planner, a coder, a tester and a code quality reviewer working together on each module. Code quality improved substantially over the first attempt. But the source code's complexity caught up with the agents eventually. They would hit a bug, attempt a few fixes, and stall, with no one available to intervene.
We landed on a middle ground: a human operating a workflow of coder, tester, and reviewer agents, migrating the codebase module by module. This preserved the code quality of the second attempt while adding a human checkpoint to unblock agents when they got stuck. The next section covers this workflow in detail.
Running structured AI agent workflows for complex code migration.
With the codebase documented and the parity harness in place, the remaining fun bit was tuning how much autonomy we could hand the agents while still getting mergeable code out. We tried both extremes, from fully autonomous runs to closely supervised manual sessions. The structured workflow below is where we landed for this use case.
Working with the client's reservoir engineers, we used the caller-callee tree to identify independent modules—self-contained subtrees of manageable size (empirically, less than ~10’000 lines of Fortran). Each module ran through the same workflow:
Generate the target C++ architecture.
Review it with a reservoir engineer.
On approval, break it into a task queue.
Run an implementation sub-workflow per task: plan → implement → test → repeat.
A human reviews the resulting PRs and requests changes until they merge.
Recognizing the limits of AI assisted legacy code modernization.
The first sprint covered core functionality: 40,000 of 300,000 lines. The Fortran codebase was self-contained and runnable, which is a favorable starting condition. Migrations that depend on external systems, lack a runnable baseline, or encode physics documented nowhere would bring additional challenges not discussed in this post.
Applying three principles for modernizing complex legacy systems.
Three lessons from this project should carry over to any large legacy migration.
Build the parity harness before you write migration code—numerical agreement is the cheapest, most convincing proof that a module is done.
Get the documentation in order before you lean on the agents, because you can't migrate code nobody can read.
And at this scale, structured workflows with human review gates beat both full autonomy and hand-driven manual sessions.
We are hiring!
Mistral's Applied AI team is a group of engineers building full-stack solutions around Mistral models and Enterprise platform. We ship high-stakes, domain-specific solution to solve some of the world’s hardest problems.
If this is the kind of work you want to be involved in, apply to join our team.