把大型机上的 COBOL 迁移到 Java,最危险的结果并不是编译失败,而是迁移成功了:代码能运行,测试也能通过,但原系统里那些没人敢碰的边界行为、舍入规则和历史 bug 被悄悄复制到了新系统。
一批研究 COBOL 迁移的工程师提出了一个克制的方向,论文题为《Agentic Method for Deterministic Validation of Legacy Code Migration》,并设计了名为 Locksmith Loop 的系统。这个名字很准确:它不是把 AI 当成“大师”,而是像锁匠一样,用确定性的锁、钥匙和检查点限制 AI 的活动范围。
真正的问题不是“能不能生成 Java”
传统迁移工具通常把任务描述成语法转换:读取 COBOL,生成 Java,再处理编译错误。可是遗留系统的行为并不只写在语法里,还分散在以下位置:
- 隐含的小数位和舍入规则;
PIC定义与字段截断行为;- 空格、零值、负数和溢出处理;
- 文件读写顺序与批处理边界;
- 上游系统传入的异常数据;
- 经过多年运行后形成的、甚至不符合业务文档的实际行为。
因此,迁移目标不应只是“生成一份看起来合理的 Java”,而应是回答一个更严格的问题:在同一组输入下,新系统是否保留了旧系统可观察到的行为?
这里还存在一个不太舒服的事实:如果旧系统本身有 bug,而这个 bug 已经被其他程序依赖,那么修复它可能反而构成一次业务变更。迁移阶段需要区分“保持兼容”和“主动修复”,不能让 AI 擅自替团队做决定。
Locksmith Loop 的核心:缩小 AI 的自由度
从摘要给出的方向看,Locksmith Loop 的价值不在于让 AI 更自由地修改代码,而在于把它放入一个确定性的验证循环中。一个可实践的循环可以拆成几步:
- AI 根据受控输入生成或修改 Java 迁移代码。
- 固定测试夹具同时运行旧实现和新实现。
- 比较返回值、输出文件、错误码以及其他可观察结果。
- 发现差异后,只把结构化的差异报告交回 AI。
- AI 提交下一次小范围修改。
- 由确定性的构建、测试和规则检查决定是否接受结果。
关键点是,AI 不应拥有“最终正确”的解释权。它可以提出补丁,可以分析差异,也可以生成测试,但验收标准必须来自可重复执行的程序。
这样的设计也能降低一个常见风险:AI 为了让测试通过,重写测试、删除断言,或者用更宽松的逻辑掩盖差异。测试夹具、比较器、构建脚本和验收规则应当属于 AI 无法自行修改的边界。
可以这样实践:用行为快照锁住迁移结果
下面是一个可以直接运行的最小示例。它不依赖 COBOL 编译器,假设旧系统已经被封装成一个命令行程序,并以 JSON 输出可观察结果;新系统则是 Java 程序。示例用 Python 驱动两者,比较规范化后的结果。
运行前需要把 legacy-cobol 和 migrated-java 替换成真实的旧、新实现命令。若暂时没有 Java 程序,可以先让两个命令指向不同的测试替身,验证验证器本身。
#!/usr/bin/env python3
import json
import subprocess
import sys
from pathlib import Path
CASES = [
{"account": "A001", "amount": "0010.50", "rate": "0.075"},
{"account": "A002", "amount": "0000.00", "rate": "0.075"},
{"account": "A003", "amount": "-0001.25", "rate": "0.075"},
]
def run(command, case):
completed = subprocess.run(
[*command, json.dumps(case)],
text=True,
capture_output=True,
check=False,
)
if completed.returncode != 0:
return {
"status": "process_error",
"exit_code": completed.returncode,
"stderr": completed.stderr.strip(),
}
try:
return json.loads(completed.stdout)
except json.JSONDecodeError as exc:
return {"status": "invalid_json", "error": str(exc)}
def normalize(result):
# 只比较双方约定的可观察字段,避免时间戳等非确定字段制造噪声。
return {
"status": result.get("status"),
"balance": result.get("balance"),
"error_code": result.get("error_code"),
}
def main():
legacy = ["./legacy-cobol"]
migrated = ["java", "-jar", "./migrated-java.jar"]
failures = []
for index, case in enumerate(CASES, start=1):
old = normalize(run(legacy, case))
new = normalize(run(migrated, case))
if old != new:
failures.append({"case": index, "input": case, "legacy": old, "java": new})
if failures:
print(json.dumps({"passed": False, "differences": failures}, indent=2))
return 1
print(json.dumps({"passed": True, "cases": len(CASES)}, indent=2))
return 0
if __name__ == "__main__":
sys.exit(main())
在 CI 中可以把它作为迁移门禁:
python3 validate_migration.py
更完整的实现还应把以下内容纳入比较范围:批处理生成的文件、记录顺序、错误码、精度与舍入结果,以及旧系统在异常输入下的输出。对非确定字段要显式做规范化,而不是简单地忽略所有差异。
“连 bug 也迁移”并不等于不修 bug
行为等价和业务正确是两条不同的轴。
如果迁移的目标是替换运行平台,第一阶段通常应优先保证可观察行为一致,包括已知缺陷。这样做的好处是问题定位清楚:一旦输出改变,可以把原因归到迁移,而不是同时归到“迁移加修复”。
但这不意味着缺陷永远不能修。更稳妥的做法是把修复拆成显式变更:
- 在差异报告中记录原行为与目标行为;
- 由业务负责人确认该差异是否属于预期修复;
- 为修复建立独立测试;
- 在迁移兼容测试和新业务规则测试之间分层管理。
这样,AI 负责加速探索和生成补丁,人类负责决定哪些历史行为仍然是合同,哪些行为已经可以终止。
落地时的检查清单
采用这类 AI 辅助迁移流程时,可以从以下边界开始:
- 固定输入夹具,避免 AI 通过改变测试数据来降低差异率;
- 固定旧系统和新系统的启动命令,记录版本与环境;
- 只比较明确的可观察结果,并对时间戳、随机数等字段做受控规范化;
- 限制 AI 每次修改的文件范围和补丁大小;
- 禁止 AI 修改验收脚本、基准输出和核心断言;
- 保存每一轮输入、补丁、测试结果和差异报告;
- 把“兼容旧行为”和“修复历史缺陷”拆成不同的变更类型;
- 对金额、日期、编码、负数和溢出等高风险边界增加专门用例。
COBOL 到 Java 的迁移当然可以借助 AI,但可靠性不来自模型的一次性发挥。更可控的路线,是把 AI 变成循环中的一个受限参与者,再用确定性的测试和比较器决定代码是否真的完成了迁移。这样即使旧系统的 bug 被带到了新系统,团队也至少知道它被带过去了,并且能决定何时、为什么把它移除。