把 AI 关进笼子:遗留 COBOL 迁移为何要连 bug 一起验证

2026-08-03 58 预计阅读时间: 1 分钟
来源: oschina.net AI 摘要 Original link

Disclaimer: This article is an AI-assisted summary. Read it together with the original source when precision matters. The summary may omit context, version differences, or edge cases and is not official documentation.

预计阅读时间:10 分钟

把大型机上的 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 更自由地修改代码,而在于把它放入一个确定性的验证循环中。一个可实践的循环可以拆成几步:

  1. AI 根据受控输入生成或修改 Java 迁移代码。
  2. 固定测试夹具同时运行旧实现和新实现。
  3. 比较返回值、输出文件、错误码以及其他可观察结果。
  4. 发现差异后,只把结构化的差异报告交回 AI。
  5. AI 提交下一次小范围修改。
  6. 由确定性的构建、测试和规则检查决定是否接受结果。

关键点是,AI 不应拥有“最终正确”的解释权。它可以提出补丁,可以分析差异,也可以生成测试,但验收标准必须来自可重复执行的程序。

这样的设计也能降低一个常见风险:AI 为了让测试通过,重写测试、删除断言,或者用更宽松的逻辑掩盖差异。测试夹具、比较器、构建脚本和验收规则应当属于 AI 无法自行修改的边界。

可以这样实践:用行为快照锁住迁移结果

下面是一个可以直接运行的最小示例。它不依赖 COBOL 编译器,假设旧系统已经被封装成一个命令行程序,并以 JSON 输出可观察结果;新系统则是 Java 程序。示例用 Python 驱动两者,比较规范化后的结果。

运行前需要把 legacy-cobolmigrated-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 被带到了新系统,团队也至少知道它被带过去了,并且能决定何时、为什么把它移除。


相关推荐