用 AI 搭流水线:把遗留代码迁移从数年压缩到数周

2026-06-12 55 预计阅读时间: 1 分钟
来源: infoq.com 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.

预计阅读时间:13 分钟

大型遗留系统的迁移,一直是工程团队最头疼的事——动辄数年、牵一发动全身、风险高到没人敢拍板。David Stein 在 ServiceTitan 的实践中,把这件事彻底换了一种打法:不靠少数架构师苦战,而是用 AI 把迁移拆成标准化工序,像工厂流水线一样大规模并行推进。关键在于两件事——任务分解的粒度要足够细、足够标准,以及验证回路必须程序化且刚性,不给 LLM 的幻觉留任何缝隙。

遗留迁移的真正瓶颈在哪

传统迁移的慢,不是因为代码多,而是因为每一步都依赖人的判断。架构师需要读懂旧系统、设计新结构、手动验证每一处改动——串行推进,瓶颈永远在少数人身上。更致命的是,人在高压下容易漏判,一次回归缺陷就可能让整个迁移倒退数周。

ServiceTitan 的核心洞察是:迁移本身可以像制造业一样工业化。不是让 AI 替你思考架构,而是让 AI 替你执行那些已经被定义清楚、可重复的搬砖动作。

流水线模式:把迁移拆成标准工序

"Assembly line" 模式的精髓是分解。把一个庞大的迁移目标,拆成大量同构的小任务:

  • 每个任务只做一件事:比如把一个旧框架的 Controller 改写成新框架的 Handler
  • 每个任务的输入输出格式固定:旧代码片段 → 新代码片段 + 元数据
  • 每个任务的验证规则固定:编译通过、单元测试通过、接口契约不变

这样一来,100 个同类任务可以同时扔给 100 个 LLM 调用并行处理,而不是排队等一个架构师逐个手改。

刚性验证回路:锁死幻觉的出口

LLM 最危险的不是生成慢,而是生成"看起来对但实际错"的代码。Stein 强调,必须用程序化的刚性验证回路来兜底:

  • 编译验证:生成的新代码必须能编译,零容忍语法错误
  • 测试验证:必须通过已有的单元测试,或至少通过自动生成的对照测试
  • 契约验证:接口签名、输入输出类型必须与旧系统严格一致
  • 差异验证:关键路径的行为差异必须被自动检测并标记

任何一步失败,该任务直接打回重做,不进入下一环节。这不是"建议性检查",而是硬性门禁——只有全部通过才算完成。

实战:搭建一条最小迁移流水线

下面用一个简化但可运行的示例,展示如何把上述思路落地。假设我们要把一批旧版 Django View 函数迁移到 FastAPI Handler。核心流程:任务定义 → LLM 生成 → 刚性验证 → 通过/打回。

# migration_pipeline.py — 最小可运行示例
# 前置依赖:pip install openai fastapi uvicorn pytest
# 假设你已有旧 Django 代码片段和对应的单元测试

import os, json, subprocess, textwrap
from openai import OpenAI
from pathlib import Path

client = OpenAI(api_key=os.getenv("OPENAI_API_KEY"))

# ---- 1. 定义标准任务格式 ----
TASK_SCHEMA = {
    "old_code": "str",       # 旧代码片段
    "old_tests": "str",      # 旧单元测试代码
    "target_framework": "str", # 目标框架名
    "interface_contract": "dict", # 接口契约:路径、方法、参数、返回类型
}

# ---- 2. LLM 生成环节 ----
SYSTEM_PROMPT = textwrap.dedent("""\
    你是一个代码迁移工程师。将给定的旧框架代码改写为目标框架代码。
    规则:
    - 保持接口契约完全一致(路径、HTTP方法、参数名、返回类型)
    - 只输出代码,不输出解释
    - 输出格式:{"new_code": "...", "new_tests": "..."}
""")

def generate(task: dict, retries: int = 3) -> dict:
    """调用 LLM 生成新代码,失败则重试"""
    for attempt in range(retries):
        resp = client.chat.completions.create(
            model="gpt-4o",
            messages=[
                {"role": "system", "content": SYSTEM_PROMPT},
                {"role": "user", "content": json.dumps(task, ensure_ascii=False)},
            ],
            temperature=0.1,  # 低温度减少随机性
        )
        try:
            result = json.loads(resp.choices[0].message.content)
            return result
        except json.JSONDecodeError:
            print(f"[attempt {attempt+1}] LLM 输出非合法 JSON,重试")
    raise RuntimeError("LLM 多次输出格式错误,任务终止")

# ---- 3. 刚性验证回路 ----

def validate_compilation(code: str, filename: str) -> bool:
    """验证生成的代码能否被 Python 编译"""
    try:
        compile(code, filename, "exec")
        return True
    except SyntaxError as e:
        print(f"编译失败: {e}")
        return False

def validate_tests(test_code: str, test_filename: str) -> bool:
    """将测试代码写入临时文件并运行 pytest"""
    tmp = Path(f"/tmp/migration_{test_filename}")
    tmp.write_text(test_code)
    result = subprocess.run(
        ["pytest", str(tmp), "-x", "--tb=short"],
        capture_output=True, text=True, timeout=30,
    )
    if result.returncode != 0:
        print(f"测试失败:\n{result.stdout}\n{result.stderr}")
        return False
    return True

def validate_contract(new_code: str, contract: dict) -> bool:
    """验证新代码的接口契约与旧系统一致(简化版:正则检查)"""
    import re
    path_ok = contract["path"] in new_code
    method_ok = contract["method"].lower() in new_code
    # 更严格的实现可以用 AST 解析,这里用正则做快速检查
    return path_ok and method_ok

# ---- 4. 流水线主循环 ----

def run_pipeline(task: dict, max_rounds: int = 5) -> dict:
    """完整流水线:生成 → 验证 → 打回重做 → 通过"""
    for round_num in range(max_rounds):
        print(f"\n=== 第 {round_num+1} 轮 ===")
        result = generate(task)
        new_code = result["new_code"]
        new_tests = result["new_tests"]

        # 刚性验证,全部通过才算完成
        checks = [
            ("编译", validate_compilation(new_code, "handler.py")),
            ("契约", validate_contract(new_code, task["interface_contract"])),
            ("测试", validate_tests(new_tests, "test_handler.py")),
        ]
        all_pass = all(passed for _, passed in checks)
        for name, passed in checks:
            status = "✅" if passed else "❌"
            print(f"  {name} 验证: {status}")

        if all_pass:
            print("\n🎉 任务通过所有验证,迁移完成")
            return result
        else:
            print("  打回重做...")

    raise RuntimeError(f"任务在 {max_rounds} 轮内未通过验证")

# ---- 5. 执行示例 ----
if __name__ == "__main__":
    # 这是一个示例任务,实际使用时替换为你的旧代码和契约
    sample_task = {
        "old_code": textwrap.dedent("""\
            from django.http import JsonResponse
            def get_user(request, user_id):
                user = User.objects.get(id=user_id)
                return JsonResponse({"id": user.id, "name": user.name})
        """),
        "old_tests": textwrap.dedent("""\
            import pytest
            def test_get_user(client):
                resp = client.get('/users/1/')
                assert resp.status_code == 200
                data = resp.json()
                assert data['id'] == 1
        """),
        "target_framework": "fastapi",
        "interface_contract": {
            "path": "/users/{user_id}",
            "method": "GET",
            "params": ["user_id"],
            "return_type": "dict",
        },
    }

    result = run_pipeline(sample_task)
    print("\n生成的新代码:\n", result["new_code"])

运行前需要设置 OPENAI_API_KEY 环境变量,并确保 pytest 可用。这个示例做了几件关键的事:

  1. 任务格式标准化——每个迁移任务都用同一套 schema 描述,LLM 的输入输出都被约束
  2. 低温度生成——temperature=0.1 大幅减少 LLM 的随机发散
  3. 三层刚性验证——编译、契约、测试,任何一层失败就打回,不给幻觉留后门
  4. 自动重试上限——最多 5 轮,防止无限循环

实际生产中,你还需要补充 AST 级别的契约校验、并行调度器(比如用 asyncio.gather 或任务队列)、以及人工审查环节。但核心骨架就是这个:标准化任务 → LLM 执行 → 刚性验证 → 通过/打回

并行调度:从一条线到一百条线

单条流水线跑通后,真正的加速来自并行。假设你有 300 个同类 Controller 要迁移,不需要排队:

import asyncio

async def run_pipeline_async(task: dict) -> dict:
    # 与 run_pipeline 逻辑相同,但用 async OpenAI client
    # 这里省略实现,核心是替换同步调用为异步
    pass

async def batch_migrate(tasks: list[dict], concurrency: int = 20):
    """并行调度多条流水线,控制并发数"""
    sem = asyncio.Semaphore(concurrency)
    async def guarded(task):
        async with sem:
            return await run_pipeline_async(task)
    results = await asyncio.gather(*[guarded(t) for t in tasks])
    return results

20 个并发意味着 300 个任务理论上可以在 15 批内完成——如果每个任务平均 2 分钟,整批迁移大约 30 分钟,而不是一个架构师苦干数周。

落地前的取舍与检查清单

这套模式不是万能药,有几个边界需要提前想清楚:

  • 任务同构性要求高:如果旧系统里混杂着各种风格、各种特殊逻辑,标准化分解本身就很困难。先对旧代码做分类和统计,同构比例低于 60% 时收益会打折
  • 验证回路的构建成本:编译和测试验证相对容易,但契约验证(尤其是行为等价性)可能需要你先花时间写对照测试或快照
  • LLM 的上下文窗口限制:单个任务的旧代码不能太长,否则 LLM 会丢失关键细节。控制在 2000 行以内比较稳妥
  • 人工审查不可省:流水线通过所有刚性验证后,仍然需要人工做最终审查——尤其是涉及业务语义的部分

检查清单:

  1. 旧代码是否已按类型/框架分组,同构任务占比是否足够高?
  2. 每类任务是否已有明确的接口契约文档?
  3. 是否已有可自动运行的单元测试覆盖旧代码核心路径?
  4. 验证回路是否覆盖编译、测试、契约三层?
  5. 并行调度的并发数是否根据 LLM API 限速做了调整?
  6. 是否预留了人工审查环节和回滚机制?

把迁移从"架构师苦战"变成"流水线量产",核心不是 AI 多聪明,而是你多严格。刚性验证回路越硬,AI 的产出就越可靠,迁移的速度就越快。


相关推荐