四万行 Fortran 77 写的储层模拟器被迁到了 C++,操盘方是 Mistral,客户是一家欧洲能源运营商,整套迁移方法与经验在 2026 年 9 月上旬被公开复盘。我在 4sapi.com 专门聊大模型 API 接入与工程落地,这一期就把这个案例背后的打法逐层拆开。
一、没人敢动的四万行:痛点到底在哪
能源行业的老系统有个共同气质:没人说得清它全部的行为,但所有人都离不开它。储层模拟器就是典型——油藏怎么渗流、压力怎么衰减、注采怎么安排,全靠这类程序算了几十年。这次的主体是一套 40000 行的 Fortran 77 代码,服务一家欧洲能源运营商,操盘迁移的是 Mistral,方法与经验在 2026 年 9 月上旬被公开复盘,我把它当成一个难得的工程样本来对待。
痛点至少有三条。第一,没人敢动。代码能跑,生产在用,改一行可能引发连锁反应,出事故的责任没人担得起,于是"能跑就别碰"成了默认文化,系统就这样带着疑问活了下去。第二,重写风险高。从零重写一个数值程序,最怕的不是写不出来,而是写出来的东西和旧版"差不多但不完全一样"——浮点求和的次序、边界条件的处理、物理单位的换算,任何一处细小差异都可能让几十年的计算口径对不上,而发现差异的时刻往往已经在生产环境里。第三,人才断层。Fortran 77 是上世纪的方言,固定格式、反直觉的语法,年轻工程师不愿接手,会写的人陆续退休,系统却还要再跑很多年。
三条痛点叠在一起,就是典型的"高风险、高价值、没人干"局面,也恰恰是 Agent 最该进场的地方。
二、为什么这是 Agent 落地确定性最高的场景之一
Agent 落地最怕的不是模型不够强,而是"对错说不清"。聊天、写文案这类开放任务,评价全凭主观感受,模型再好也没法验收。遗留代码迁移恰好相反,它几乎是确定性最高的一类场景。
第一,行为可验证。旧程序就是活的规格说明书:给定输入,输出是什么,跑了三十年,白纸黑字。迁移质量不靠感觉,靠对比——新代码跑同样的输入,结果对齐旧版,就是及格。验收标准从"看起来对"变成"算得一样",这一步把主观题变成了客观题。
第二,数据私有。核心代码是运营商的命根子,不能随便外流。这类需求可以通过私有化部署或者合规的数据协议来满足,模型只在受控环境里接触代码,数据主权握在客户手里。相比之下,那些必须把业务数据明文发出去的场景,落地阻力天然大得多。
第三,付费意愿强。四万行核心计算代码,人工迁移意味着以年计的工时和说不清的预算,业务方有真实且迫切的付费动机。对比那些"有趣但没人掏钱"的 Agent 演示,迁移场景的价值链非常清晰:省下的是真金白银的工程成本,换来的是系统续命。
可验证、可私有、可收费,三个条件同时满足,这就是我判断它确定性高的依据。
三、迁移流水线速览:六步走
方法论本身并不神秘,核心思路是把"一次性的大冒险"拆成"一串可验证的小步骤"。整条流水线长这样:
旧代码盘点 → 黄金基准 → 模块拆解 → Agent 逐模块迁移 → 等价验证 → 集成验收
[旧代码盘点] 盘点 40000 行 Fortran 77,标出 COMMON 块、入口子程序与隐式声明热点
↓
[黄金基准] 固定输入、对齐旧版输出,先让"对错"有裁判
↓
[模块拆解] 画模块依赖图,按拓扑顺序切成可独立验证的小任务
↓
[Agent 逐模块迁移] 纯计算子任务批量交给 Agent,接口边界留给人工
↓
[等价验证] 每个模块过一遍黄金基准,偏差超容差就打回重做
↓
[集成验收] 全量拼装、整体回归,通过后再做 C++ 侧现代重构
每一步的产出都是下一步的输入:盘点产出代码清单与风险地图,基准产出验收判据,拆解产出子任务序列,迁移产出 C++ 代码,验证产出比对报告,验收产出可上线的系统。任何一步不达标,流水线就地停下,绝不带病前进。后面几节把其中最关键的几步单独展开。
四、黄金基准:先解决"对错由谁说了算"
整条流水线里,黄金基准是地基,值得单独讲。
做法不复杂:挑选一组有代表性的输入——常规工况、极端边界、历史出过问题的情况——固定下来,喂给旧版 Fortran 程序,把输出原样存档,这就是"黄金输出"。此后所有迁移工作都被这一把尺子衡量:新版 C++ 跑同样的输入,输出必须和黄金对齐。
两个细节决定这套机制的成色。一是时机:基准必须在迁移动手之前建好,用旧程序当裁判;一旦旧环境被破坏,裁判就没了,争议再也断不清。二是浮点比对方式:数值程序不能拿相等号硬比,单精度还是双精度、求和的先后次序、编译器的优化级别,都可能让结果在最后几位小数上抖动,所以要按物理量纲设容差,容差内算通过,超了就打回。
基准集还要按模块分块建。整体跑一个大用例,只能证明"大概率没崩";按模块建基准,才能在等价验证阶段精确定位到"哪个模块、哪个函数、哪组输入"出了偏差。定位粒度差一个量级,排查成本就差一个量级,这笔账怎么算都划算。
五、Fortran 77 难啃在哪:固定格式、COMMON 块与隐式类型
为什么这类代码人工不愿碰、Agent 也容易翻车?因为 Fortran 77 的老习惯处处反现代。
固定格式是第一道坎。代码写在哪一列都有讲究:第 1 到 5 列留给语句标号,第 6 列是续行标记,语句从第 7 列开始,第 72 列之后的内容直接被编译器无视。空格不重要、列位置才重要,这种格式对现代解析工具和从没接触过它的模型都不友好,复制粘贴时少个空格都可能出乱子。
COMMON 块是第二道坎,也是最深的一道。它本质是全局共享内存:不同子程序按约定的顺序和类型,去同一段内存里取数。这意味着成百上千个子程序之间存在隐式耦合——改一个 COMMON 块的布局,所有引用它的地方全部受影响,而编译器一声不吭。更麻烦的是 EQUIVALENCE 这类老技巧,让同一块内存在不同子程序里被解释成不同类型,等价翻译时必须逐个确认,不能想当然。
隐式类型是第三道坎。变量不声明也能用,按名字首字母猜类型:I 到 N 开头是整数,其余默认实数。好处是写得快,坏处是拼错一个变量名,编译器安静地把它当成一个新变量,程序照跑,结果悄悄变错,这种 bug 藏几十年都不奇怪。
再算上满天飞的 GOTO、算术 IF、从 1 开始的数组下标、按列优先的内存布局、默认单精度的浮点运算,这些特征单看都不难,叠在四万行代码里就是雷区。所以迁移的第一步不是翻译,而是把这些隐式约定全部显式化,写进文档,喂给后续所有环节。
六、模块拆解与逐模块迁移:Agent 干活,人守闸口
雷区要切块排。先给代码画依赖图:谁是叶子模块、谁依赖谁、COMMON 块把哪些子程序绑在了一起。有了依赖图,按拓扑顺序从叶子节点开始切,每一块是几百行级别、可以独立编译、独立验证的子任务,块与块之间靠冻结的接口约定衔接。
分工原则很简单:可验证的纯计算子任务交给 Agent,架构决策和接口边界留给人。
Agent 擅长的是机械且量大的活:把一段逻辑清晰、接口明确的 Fortran 子程序翻译成语义等价的 C++。这类任务输入输出明确、有黄金基准兜底,模型自由发挥的空间被压到最小,产出可以机器验收。一个子任务一次调用,批量化推进,进度一目了然。
人守的闸口设在接口边界。函数签名、数据结构布局、物理单位约定、COMMON 块的等价方案——这些决策一旦错了,下游全错,而且单模块测试还发现不了,必须由人拍板后冻结成"接口约定",随子任务一起喂给 Agent,并在指令里写明禁止改动。审核时也不必逐行通读,重点抓三样:接口是否守约、数值处理有没有小动作、自检备注里有没有含糊其辞。
失败处理同样重要:某个模块跑不过基准,就停下整条流水线,修好再走。带病模块混进下游,污染的是所有依赖它的后续模块,排查成本指数级上升。宁可慢一个模块,不可错一条链。
七、C++ 侧的现代重构:不是翻译完就结束
等价验证全绿,只说明 C++ 版本"算得和旧版一样",离"值得再跑三十年"还差一步现代化。
重构分两类。一类是安全与可维护性:用 RAII 管理资源生命周期,把裸指针换成智能指针,动态数组换成标准容器,补上 const 正确性,把散落各处的隐式全局状态收进明确的结构体——这些改动不碰数值逻辑,风险低、收益长期。再配上单元测试和编译器自带的消毒器工具,把新代码的质量基线立起来。
另一类是绝对不能碰的:浮点运算的次序与精度。随手"优化"一个求和顺序、把单精度悄悄升成双精度,都可能让结果对不上黄金基准。所以现代重构必须踩着等价验证走——每完成一步重构就重跑基准,确认数值行为纹丝不动,才算改得对。
先等价、再现代,两步分开走、分开验,等于把"翻译正确"和"工程质量"两件事解耦。混在一起做,出了偏差分不清是翻译错了还是重构改的;分开做,责任一目了然。
八、接入教程:用 OpenAI 兼容接口编排迁移任务
方法论讲完,落到接入层面。迁移这类长任务,接大模型 API 的姿势和聊天机器人完全不同:不追求一次对话聊完,而是按子任务分批调用,每批只喂当前模块需要的最小上下文,diff 与测试结果落盘,方便追溯与续跑。
下面这份 Python 骨架用 OpenAI 兼容 SDK 风格写成,可以直接套在前面讲的流水线上:
import os
import json
import difflib
import subprocess
from pathlib import Path
from openai import OpenAI
# OpenAI 兼容客户端:base_url 指向中转站,密钥只从环境变量读取
client = OpenAI(
base_url="https://4sapi.com/v1",
api_key=os.environ["MIGRATE_API_KEY"],
)
MODEL = os.environ["MIGRATE_MODEL"] # 迁移用的模型名,同样走环境变量
OUT = Path("migration_run")
for sub in ("cpp", "diffs", "reports"):
(OUT / sub).mkdir(parents=True, exist_ok=True)
SYSTEM = (
"角色:Fortran 77 到 C++ 的迁移工程师。"
"规则:语义严格等价,不做任何发挥;保持原有运算次序与精度;"
"遵守给定的接口约定,禁止改动函数签名;"
"只输出 JSON,字段为 cpp_code 与 self_check_notes。"
)
def call_agent(module):
"""按子任务喂片段:只给当前模块、接口约定与基准要点,不给全库。"""
user_prompt = (
f"子任务:{module['name']}\n"
f"接口约定(已冻结,禁止改动):\n{module['interface']}\n"
f"黄金基准要点:{module['golden_hints']}\n"
f"Fortran 77 源码片段:\n{module['fortran']}\n"
)
resp = client.chat.completions.create(
model=MODEL,
temperature=0, # 迁移要确定性,温度压到 0
messages=[
{"role": "system", "content": SYSTEM},
{"role": "user", "content": user_prompt},
],
)
return json.loads(resp.choices[0].message.content)
def run_golden_tests(name):
"""跑该模块的黄金基准,返回退出码与输出尾部。"""
proc = subprocess.run(
["python", "tools/run_golden.py", "--module", name],
capture_output=True, text=True,
)
return {"exit_code": proc.returncode, "tail": proc.stdout[-2000:]}
def migrate_batch(modules):
for module in modules:
name = module["name"]
cpp_path = OUT / "cpp" / f"{name}.cpp"
old_text = cpp_path.read_text(encoding="utf-8") if cpp_path.exists() else ""
result = call_agent(module)
cpp_path.write_text(result["cpp_code"], encoding="utf-8")
# diff 落盘:新旧两版逐行对比,人工审核闸口快速过目
diff_path = OUT / "diffs" / f"{name}.diff"
diff_path.write_text("\n".join(difflib.unified_diff(
old_text.splitlines(), result["cpp_code"].splitlines(),
fromfile=f"{name}.prev", tofile=f"{name}.new", lineterm="",
)), encoding="utf-8")
# 测试结果落盘:不过就立即停,等人工介入,不让坏模块流进下游
tests = run_golden_tests(name)
report = {
"module": name,
"self_check_notes": result["self_check_notes"],
"tests": tests,
"passed": tests["exit_code"] == 0,
}
(OUT / "reports" / f"{name}.json").write_text(
json.dumps(report, ensure_ascii=False, indent=2), encoding="utf-8"
)
if not report["passed"]:
print(f"[halt] {name} 未通过黄金基准,等待人工审核")
break
if __name__ == "__main__":
# subtasks.json 由模块依赖图拆解生成,已按拓扑排序,分批喂给 Agent
subtasks = json.loads(Path("subtasks.json").read_text(encoding="utf-8"))
migrate_batch(subtasks)
几个接入要点单独说一下。
上下文要抠着给。四万行代码绝不能整包塞进 prompt,每次调用只带当前模块源码、冻结的接口约定和基准要点,系统提示词里写死输出格式与纪律。喂得越少,模型跑偏空间越小,token 账也越干净。
温度压到 0。迁移要的是确定性,不是创造力,采样参数越保守越好,同一段代码重复调用,结果应该基本一致,否则等价验证会变成抽签。
产物全部落盘。生成的 C++、与上一版的 diff、黄金基准的测试报告,每个模块一份写进目录。流水线中断了,翻报告就知道走到了哪、断在了哪,从断点续跑,不用从头再来。
九、人工迁移 vs Agent 辅助迁移:分工对照表
把两种模式在各环节的分工与风险放在一张表里对照着看:
| 环节 | 人工迁移 | Agent 辅助迁移 | 主要风险与对策 |
|---|---|---|---|
| 旧代码盘点 | 老工程师通读加回忆,周期以月计 | 工具扫描加 Agent 辅助梳理调用关系,人工抽查关键路径 | 隐式规则漏标;对策是盘点结果交叉核对 |
| 黄金基准 | 手工挑用例、肉眼比对输出 | 固定输入自动跑旧版存档,脚本化比对加容差 | 覆盖不足;对策是按模块补齐边界用例 |
| 模块拆解 | 凭经验划块,粒度忽大忽小 | 依赖图驱动、拓扑排序,粒度均匀可控 | 切块破坏语义;对策是接口边界人工确认 |
| 逐模块翻译 | 逐行手译,以年计的工时 | 按子任务批量调用,单模块小时级产出 | 模型自由发挥;对策是温度压 0、约定写死 |
| 等价验证 | 抽样验证,靠信心兜底 | 每模块全量过黄金基准,机器判定 | 容差设错;对策是按物理量纲分级设容差 |
| 集成验收 | 边拼边修,问题集中爆发 | 按拓扑序增量集成,每步带回归 | 单模块全绿整体崩;对策是保留全量用例 |
| 文档沉淀 | 人走了知识就跟着走 | 过程记录、diff、报告全部落盘 | 记录无人看;对策是纳入审核闸口必读项 |
一句话概括:人从"翻译工"变成"裁判和架构师",产出没少,风险反倒更集中、更可控。
十、避坑清单:十条血泪经验
结合通用工程常识,把最容易翻车的地方列成 checklist,动手前逐条过一遍:
- 先建黄金基准,再写第一行 C++,顺序不能反
- 接口约定(函数签名、数据布局、单位)冻结之后才开始迁移
- 每个子任务附带它依赖的接口约定,绝不让 Agent 猜全局状态
- 浮点比对用容差,严禁直接判等
- 温度压到 0,同一段代码两次生成的结果必须基本一致
- 人工审核闸口设在模块边界,抓接口与数值纪律,不逐行通读
- diff 与测试报告逐模块落盘,全程可追溯、可回滚
- 任一模块验证不过就停下整条流水线,禁止带病前进
- 大 COMMON 块先设计好等价的数据结构,再开始翻译
- 单模块全绿不等于结束,集成回归必须全量跑完
清单不长,每一条背后都是一类真实的事故模式。迁移工程里死掉的项目,多数不是死于不会写代码,而是死于顺序错了、约定没冻住、验证偷了懒。
十一、成本与风险:token 账与代码隐私
长任务的 token 账要提前算。四万行代码切成上百个子任务,每个子任务一次调用,再乘上重试与迭代,调用量轻松上四位数。控制成本的关键不在压单价,而在压每次调用的上下文:系统提示词固定复用,模块源码按需喂入,接口约定只发相关片段,历史对话绝不回灌——迁移任务天然无状态,是最适合"薄上下文"调用的场景。建议给每个子任务单独记一笔 token 账,哪个模块花了多少一目了然,出现异常消耗立刻能定位到具体环节。
模型选型也可以分层:样板化的简单翻译用性价比档,数值核心、COMMON 块重灾区用能力档,按子任务路由模型,整体成本能压下来一大截。中转站侧再做一层负载均衡与重试策略,长任务的稳定性就有底了。
风险侧最重的是代码隐私。能源行业的核心代码是战略资产,接外部 API 前必须想清楚三件事:数据出不出域——走私有化部署还是合规协议下的云端调用,要在合同和架构两个层面同时落实;发送内容有没有脱敏——注释里的内网地址、真实井位数据、密钥常量,喂给模型之前清干净;全过程有没有留痕——每次调用的时间、内容摘要、产出物落盘归档,出了问题可以审计回溯。
合规红线也要提前划清:接 API 走正规渠道、按量付费、签好数据协议,不做任何绕过限制的动作。迁移提速靠的是方法论扎实,不是野路子。
十二、总结
这一期复盘了一个少见的高确定性 Agent 落地案例:Mistral 帮一家欧洲能源运营商,把 40000 行 Fortran 77 的储层模拟器迁到 C++,方法与经验在 2026 年 9 月上旬公开。我顺着通用工程常识,把整条路径拆成六步流水线,讲了黄金基准为什么是地基、Fortran 77 难啃在哪、Agent 与人怎么分工、C++ 侧怎么重构,附上了 OpenAI 兼容接口的编排骨架、人工与 Agent 的分工对照表、避坑 checklist,以及 token 账与代码隐私这两本账。核心结论一句话:遗留代码迁移之所以是 Agent 最稳的落地场景,在于它把"对错"从主观感受变成了机器可判定的比对。
这期就写到这里,更多接入实践都沉淀在 4sapi.com。对这套迁移打法有补充、有疑问,或者踩过类似的坑,欢迎在评论区留下想法。