Agent 迁移遗留代码,真正值钱的不是翻译能力
40,000 行 Fortran 77,没测试,文档散在旧 PDF 和代码注释里。Mistral 把它迁到 C++ 后,最让我停下来看的不是 Agent 跑了多少,也不是模型写 C++ 有多快。
而是他们先给老系统插了导出状态的能力,再让新系统逐点对账。
这顺序一点都不性感。但它决定了这件事是一次可控的现代化,还是一次把风险换了个语言继续埋下去。
9 月 9 日,Mistral 复盘了一个欧洲能源运营商的项目。对象是一个物理计算很重的储层模拟器,首个 sprint 完成了 30 万行代码中的 4 万行。团队试过让 Agent 按子程序自由翻译,也试过 planner、coder、tester、reviewer 全部自动协作,最后留下来的方案反而没那么炫。
人负责卡住关键决策,Agent 负责把模块化流水线跑快。
我一直觉得,遗留系统改造最危险的一句话是「能跑就行」。今天能跑,不等于行为没变。对一个把计算结果交给业务决策的系统,真正的问题不是 C++ 编译器接不接受代码,而是某个边界条件下的数值偏差,会不会在三个月后变成一笔谁也解释不了的成本。
这篇不准备替 Mistral 做产品宣传。我想借这个案例聊一件和 Java、Spring 这类企业系统更近的事。Agent 让重写代码变便宜之后,验证、定位和业务签字会成为新的瓶颈。团队如果还把迁移理解成「把 A 语言改成 B 语言」,迟早会被自己的速度反噬。
老代码最难迁的,从来不是语法
很多人看到 Fortran 77,会先想到过时的语法。确实,它的约束很强。没有 modules、namespaces 和结构化类型,状态常常放进跨程序共享的 COMMON blocks。变量还可以按首字母隐式推断类型,一个拼错的变量名未必报错,而是悄悄变成新变量。
这种代码放到大模型面前,表面看简直是送分题。给一段函数,要求改成 C++ 或 Java,模型大多能很快给出像样的答案。
但函数片段不是系统。
举个 Mistral 文中那个缩小后的例子。Fortran 子程序在循环里给 RHOG 写值,输入、输出和中间状态都来自全局 COMMON block。迁到 C++ 后,合理的做法不是做一个更大一点的全局 struct,而是把气体性质收成一个显式参数,把网格循环挪到调用方,函数返回计算结果。
代码看着干净了,可验证难度立刻上来了。
旧函数靠写全局数组完成工作,新函数靠参数和返回值完成工作,两边已经没有逐行对应关系。你无法靠 diff 说「每行都翻过去了」。更麻烦的是,架构重构恰恰是迁移要争取的收益。为了方便验收而强迫一比一照抄,最后只会得到一份披着 C++ 外衣的 Fortran。
我见过 Java 项目里很像的场景。一个十年前的 Spring MVC 服务把上下文塞进 ThreadLocal,支付状态写进静态单例,DAO 又隐式依赖事务传播。你让 Agent 把它迁到 Spring Boot 3、WebFlux 或者新的领域模型,它可以改出一堆更现代的类。
可订单是否重复扣款,超时重试是否保持幂等,异常时补偿任务有没有漏掉,这些从来不住在语法树里。
它们藏在调用顺序、共享状态、运维脚本和老同事脑子里。
所以,遗留改造至少有三层,不要混在一起谈。
第一层是语法可转换。类型、控制流、库调用改得过去,编译能过。
第二层是行为等价。给定同样输入,新旧系统的输出、关键中间状态和副作用要在约定范围内一致。
第三层才是业务验收。新设计保留了业务规则,同时真的降低了后续维护成本。
Agent 在第一层已经很能打。第二层需要工程护栏。第三层必须有懂业务的人承担责任。
这不是保守,是分工。
先搭对账台,再允许 Agent 动刀
Mistral 案例里最成熟的决定,是迁移前搭了一套 parity harness。原系统可以导出状态,新系统加载关键 checkpoint 做测试,储层工程师还指定了必须观察的中间点。
这里的关键不是「有测试」。而是测试的对象从最终结果往前走了一步。
只比最终结果,常常太晚。一个数值求解器最后的总产量接近,不代表中间压力场、边界单元或迭代收敛过程没漂。对于 Web 服务也一样,接口返回 200 不代表事件没有重复投递,缓存没有穿透,审计记录没有丢。
你得提前约定哪些状态可以成为证人。
Mistral 文中给了一个很直观的数字。团队先在 Fortran 里导出 RHOG 的值,某次运行得到 42.71834,再用这个值做 C++ 模块的参考 checkpoint。它朴素得像手工记账,但这种朴素的东西能把一次漫长的 Agent run 变成可判定任务。
我自己的判断是,迁移项目第一周最该做的代码,通常不该是目标语言实现,而该是观测和回放。
如果你手里是 Java 或 Spring 系统,可以从这份最小清单开始。
- 固定一批有代表性的生产脱敏输入,不只放正常路径,也放历史 bug、边界日期、重试和降级场景
- 记录输入、输出、关键领域事件和必要的中间快照,字段要有版本号
- 对每类结果定义比较策略,金额用精确相等,时间和浮点计算写明容差,集合明确排序规则
- 把外部依赖换成可重放的 stub 或录制响应,别让第三方接口把对账搞成随机事件
- 把差异报告做成 CI 的一等产物,让 reviewer 看到具体字段,而不是只看一句 failed
这里有个容易被忽略的坑。很多团队一听「对比」就把旧系统奉为真理。老系统的输出并不总是正确,它可能带着多年前的 bug。比较框架不该把旧值直接封为金标准,而要允许领域专家把某些差异标为已知缺陷或有意修正,并留下理由。
否则,你会把历史债务自动化复制一遍。
这块需要注意一下。Agent 很擅长把规则执行得一致,却不会自然知道哪条老规则本来就不该保留。验证链要同时保留「是否一致」和「为何允许不一致」两种记录。
全自动翻译为什么会失败得很体面
Mistral 第一次尝试很有代表性。他们让每个 Agent 负责一个 Fortran 子程序,自主翻成 C++,跑了一周。结果功能能工作,但 COMMON blocks 被一对一换成全局 structs,GOTO 控制流也大体保留。
这不叫现代化,只是换皮。
很多团队会觉得这已经很不错了。毕竟旧语言退出了,招聘容易了,IDE 和 CI/CD 也好配了。站在交付压力下,这个选择完全能理解。系统太大、预算有限,谁都不想先花两个月讨论理想架构。
但问题在于,迁移后的第一年才是账单真正来的时候。全局状态、隐式约束和无边界模块还在,只不过懂它们的人从 Fortran 老兵变成了 C++ 新人。你没有买到演进能力,只买到一段过渡期。
Mistral 的第二次尝试更进一步。每个模块由 planner、coder、tester、code quality reviewer 组成工作流,代码质量确实更好。但复杂性最后还是把 Agent 困住了。它遇到 bug,试几种修复方式,然后原地打转,没有人能判断是需求缺失、模型卡住,还是原系统本身就有矛盾。
我当时看到这里,反而觉得这比「全自动成功」更有价值。它把一个常被忽略的事实摆出来了。
多 Agent 不是无限叠加的聪明。
如果信息不完整,四个角色只是在更快地围绕同一个误解协作。planner 会给出很像样的计划,coder 会忠实实现,reviewer 也可能只是在同一份错误上下文里挑格式问题。流程看起来特别完整,真正缺的是能把模糊业务描述落到可验证约束的人。
所以人不该被塞在每次编码之前逐行审批,也不该只在事故后背锅。人的位置应该放在高信息密度的关口。
例如,模块边界怎么切,哪些差异可接受,历史行为哪些必须废弃,发现不一致时是继续试还是回到架构设计。这些决定一旦错了,Agent 跑得越久,错误扩散得越快。
模块边界,比 Agent 数量更值得花时间
Mistral 最终选了中间路线。他们先从 caller-callee tree 找相对独立的子树,再和储层工程师确认模块边界。经验上,一个模块控制在大约一万行 Fortran 以下,然后依次完成目标架构、领域评审、任务拆分、计划、实现、测试和 PR 审查。
这里没有神秘提示词,更多是工程常识。
你想想看,一个 Agent 面对 30 万行系统时,最容易犯的错不是不会写某个函数,而是不知道某个变量的真实生命周期。它会看到几十个调用点、几个隐式约定和一堆名字模糊的数组,然后补出一个貌似合理的局部答案。局部答案一多,系统边界就碎了。
切成可验证的模块,实际是在给 Agent 降低上下文噪声,也是在给人降低审查成本。
对企业后端来说,一个好的迁移单元通常不是按 package 或文件数量切,而是按可独立回放的业务闭环切。比如「计费试算」比「pricing 包」更像一个单元。「库存预占并生成领域事件」比「controller 到 repository 的整条调用链」更适合对账。
我会用四个问题判断它能不能交给 Agent。
- 能不能列出明确输入和输出
- 能不能在不访问全系统的情况下回放
- 能不能找到一位对结果有解释权的业务 owner
- 出现差异时,能不能在半天内缩到有限范围
四个都答不上来,就别急着让 Agent 写。先做代码考古。
Mistral 也没有跳过这一步。他们用自定义解析器画调用树,再用一百多个 Agent 从叶子节点向上补文档,每个节点提交 PR,另一个 reviewer Agent 用 cron 轮询新 PR 并安排修复。这个做法很适合 Agent 的长处。文档提取、交叉引用、把旧 PDF 和注释放到一起,都是可并行、可复查的工作。
但我不建议照着「一百多个」这个数字抄作业。团队还没建好任务队列、配额治理和 PR 规则时,十个 Agent 也足够制造一次高质量混乱。先让一个模块从文档到对账、从实现到合并完整走通,再放大并发,比较稳。
把 Agent 当成迁移流水线的一段,不是总包商
这类项目真正可复用的架构,应该是一条可回滚的变更链。
1 | 旧系统可运行样本 |
每一段都有失败出口。样本跑不起来,先修可观测性。模块说不清,先补文档。差异过大,回到任务拆分或架构。业务 owner 不认可,不能靠多跑几轮 prompt 把它压过去。
这比「给 Agent 一个仓库,让它重构」慢吗?前两周往往更慢。要建数据集、补日志、整理文档、约人评审,过程并不轻松。
可一旦进入批量迁移,速度会反过来。因为每次失败都能定位,每个通过的模块都有证据,后续 Agent 也可以沿用同一套 Skill、测试约束和 PR 模板。你不是在赌一次大迁移,而是在生产可验证的小变更。
这和 LangGraph 里设计 Agent workflow 很像。模型节点不是系统,状态、路由、工具权限、失败重试和人工中断点加起来才是系统。把迁移交给 Agent 也是一样。模型强一点当然有帮助,但决定可靠性的,往往是图外面的那圈约束。
我有时候觉得,AI 时代最容易贬值的是「把一段代码写出来」的时间。最难贬值的是判断它该不该写、写完怎么证明、出问题谁能做决定。
这也是技术负责人要更早介入验证设计的原因。不是为了把所有代码审核一遍,而是为了让团队不会在错误的地方自动化。
可以明天就开始的一个小实验
如果你的团队正维护一套不敢动的 Java 或 Spring 服务,不必等到拿到一个大模型预算才开始。挑一个有明确输入输出的老模块,用一天做一个小实验。
第一步,找三到十个真实脱敏样本,把老版本运行结果和关键事件保存下来。
第二步,让 Agent 只做两件事。先读代码和历史资料,产出调用链、隐式依赖和待确认问题。再按你给出的目标边界生成迁移计划。先别让它直接改主分支。
第三步,让它实现一个窄改动,并把新旧差异写成机器可读报告。报告里不要只有通过或失败,要有输入标识、字段路径、旧值、新值、容差和关联 commit。
第四步,找真正懂业务的人花半小时审差异。通过的模块才进下一批,解释不清的差异直接回退到文档和边界阶段。
这套动作听起来不够酷,但它会给你一个很诚实的答案。你的系统到底适不适合被 Agent 加速,卡住你的到底是代码量、测试债、领域知识,还是组织里的决策链。
Mistral 的案例有它的有利条件。旧 Fortran 系统是自包含且可运行的,首个 sprint 只覆盖了 30 万行中的 4 万行,外部系统依赖和无文档物理规则带来的难题并没有完全展开。把它包装成「Agent 已经解决遗留迁移」并不诚实。
不过,这个案例仍然给出了比模型能力榜单更有用的提醒。
当 Agent 终于能把代码改得很快,别急着把油门踩到底。先把仪表盘、刹车和方向盘装上。
资料
- Mistral,2026 年 9 月 9 日,《Modernizing complex legacy code with AI agents》
https://mistral.ai/news/legacy-code-modernization/ - 本文中的项目规模、工作流和限制说明均来自上述 Mistral 一手复盘。文中关于 Java、Spring 和 LangGraph 的做法是基于该案例的工程推演,不代表 Mistral 的主张。




