Agent 驱动的代码迁移:从规则编码到分批验证的工程实践
一、大规模代码迁移的成本黑洞
Python 2 升 3,框架跨大版本升级,语言整体切换。这类代码迁移,单个改动不难,难在数量。几万处调用点,人工逐个改,按人月计。改漏一处,上线就是线上故障。
纯人工迁移慢且不可控。纯靠 LLM 一把梭,误改语义、漏改边界,回归测试一塌糊涂。两个极端都失败,根子在没有结构化的迁移规则与验证链路。Agent 介入迁移,不是替人写,是把迁移拆成可编排的子任务。
识别、改写、验证,每步可审计、可回退、可分批。本文讨论这条链路的设计,并给出规则引擎骨架。
二、Agent 在迁移链路中的角色:识别/改写/验证
迁移是一条流水线,Agent 在每个环节扮演不同角色。识别阶段。扫描代码,定位需要改写的模式。比如 print x 到 print(x),unicode 到 str。
Agent 把模式匹配与语义分析结合,产出待改清单。比纯正则准,比人工快。改写阶段。按规则对每个命中点做转换。
规则是可版本化的,迁移本身就是代码资产。Agent 负责执行,不负责"创造",创造性的改写必须人审。验证阶段。改完跑测试,对比迁移前后的行为。
测试覆盖到的地方自动验,覆盖不到的地方标红人工确认。Agent 产出变更报告与置信度,人做最终裁决。关键设计是分批迁移。不是一次改全仓库,而是按模块、按文件粒度分批。
每批独立验证、独立合并、独立回滚。把爆炸半径压到最小。
置信度是自动改写的门槛。低置信度的命中点必须人工预审,不能让 Agent 自作主张。
三、迁移规则引擎骨架实现
下面用 Python 实现一个迁移规则引擎。规则用 AST 模式匹配 + 转换函数定义。支持 dry-run 预览、分批执行、变更报告。
import ast
import astor
import hashlib
from dataclasses import dataclass, field
from pathlib import Path
from typing import Callable, Optional
@dataclass
class MigrationRule:
"""单条迁移规则:名称、匹配谓词、转换函数、置信度策略。
为什么把置信度放规则里:不同规则的可靠性差异大,
print 改写几乎 100% 安全,元类改写风险高。
规则自带置信度,路由器据此决定自动改还是人审。
"""
name: str
matcher: Callable[[ast.AST], bool]
transformer: Callable[[ast.AST], ast.AST]
auto_confidence: float # 自动改写置信度阈值
description: str = ""
@dataclass
class HitRecord:
"""单次命中记录:定位、规则、改写前后、是否自动应用。
why 记录前后快照:变更报告与代码审查都要可追溯,
回滚时也能精确还原。
"""
file: str
line: int
rule: str
before: str
after: str
applied: bool
confidence: float
class MigrationEngine:
"""迁移引擎:加载规则、扫描文件、分批改写、产出报告。
为什么基于 AST 而非正则:正则改代码极易误伤字符串与注释,
AST 保证只在语法节点上操作,且能拿到行号定位。
"""
def __init__(self, rules: list[MigrationRule],
auto_threshold: float = 0.9) -> None:
self.rules = rules
# 全局自动阈值,规则自身的阈值取较高者,保守优先
self.auto_threshold = auto_threshold
def scan_file(self, path: Path) -> list[HitRecord]:
"""扫描单个文件,返回所有命中,不改写。
why 扫描与改写分离:先全量扫描产出清单,
便于人工预览与分批决策,避免边扫边改难以回滚。
"""
try:
source = path.read_text(encoding="utf-8")
except UnicodeDecodeError as e:
# 二进制或非 UTF-8 文件跳过,记录为无法处理
return [HitRecord(str(path), 0, "encoding",
"", "", False, 0.0)]
try:
tree = ast.parse(source, filename=str(path))
except SyntaxError:
# 语法错误文件不迁,先让人修编译问题
return [HitRecord(str(path), 0, "syntax",
"", "", False, 0.0)]
hits: list[HitRecord] = []
for node in ast.walk(tree):
for rule in self.rules:
if not rule.matcher(node):
continue
before = astor.to_source(node).strip()
try:
new_node = rule.transformer(node)
after = astor.to_source(new_node).strip()
except Exception as e:
# 转换失败记录为低置信,不中断整批扫描
hits.append(HitRecord(
str(path), getattr(node, "lineno", 0),
rule.name, before, f"TRANSFORM_ERROR: {e}",
False, 0.0,
))
continue
hits.append(HitRecord(
str(path), getattr(node, "lineno", 0),
rule.name, before, after,
False, rule.auto_confidence,
))
return hits
def apply_batch(self, path: Path,
hits: list[HitRecord]) -> list[HitRecord]:
"""对单文件应用本批命中,返回应用结果。
仅应用置信度达阈值的命中,其余标记为待人审。
"""
source = path.read_text(encoding="utf-8")
tree = ast.parse(source, filename=str(path))
applied = 0
for node in ast.walk(tree):
for rule in self.rules:
if not rule.matcher(node):
continue
# 达阈值自动改,保守优先
if rule.auto_confidence < self.auto_threshold:
continue
try:
rule.transformer(node)
applied += 1
except Exception:
# 单点失败不影响其他点,最终报告会暴露
continue
if applied == 0:
return hits
# 生成新源码,带备份哈希便于回滚校验
new_source = astor.to_source(tree)
backup_hash = hashlib.sha1(source.encode()).hexdigest()[:8]
backup_path = path.with_suffix(
f".bak_{backup_hash}{path.suffix}"
)
# 备份原文件,回滚时按哈希校验,防止误覆盖
backup_path.write_text(source, encoding="utf-8")
path.write_text(new_source, encoding="utf-8")
for h in hits:
if h.confidence >= self.auto_threshold:
h.applied = True
return hits
# 示例规则:Python2 print 语句改函数调用
def _is_print_stmt(node: ast.AST) -> bool:
# Python3 解析器不会产生 print 语句节点,
# 这里用 ast.Name 指向 print 当函数用的旧模式近似
return (isinstance(node, ast.Expr)
and isinstance(node.value, ast.Name)
and node.value.id == "print")
def _noop_transform(node: ast.AST) -> ast.AST:
# 真实规则做语义改写,此处仅作骨架占位
return node
PRINT_RULE = MigrationRule(
name="print_to_function",
matcher=_is_print_stmt,
transformer=_noop_transform,
auto_confidence=0.95,
description="print 语句迁移为函数调用",
)
if __name__ == "__main__":
engine = MigrationEngine([PRINT_RULE], auto_threshold=0.9)
target = Path("legacy.py")
hits = engine.scan_file(target)
for h in hits:
status = "自动" if h.confidence >= 0.9 else "人审"
print(f"{h.file}:{h.line} {h.rule} [{status}]")
生产系统会接 LLM 做复杂语义改写。但 LLM 的产出仍走这套规则的置信度与验证链路,不直接落盘。规则负责结构,LLM 负责语义,人负责裁决。
四、自动化迁移的信任边界:误改与漏改
Agent 迁移提效,但信任边界要划清。
误改风险。规则匹配过宽,改到不该改的地方。比如把字符串里的 print 也改了,或改写了测试用例里的反面示例。AST 比正则安全,但动态特性仍能骗过它。
漏改风险。规则覆盖不全,迁移后残留旧模式。残留比误改更隐蔽,测试通过但语义已偏。漏改靠规则集完备性,靠迁移后全量扫描兜底。
语义等价难保证。语法改对了,行为可能变了。print >> sys.stderr 改成 print(file=sys.stderr),缓冲行为可能不同。验证依赖测试覆盖率,覆盖不到的盲区是定时炸弹。
不可逆迁移的代价。有些迁移不可逆,改了就回不去。必须分批、可回滚、有备份。全仓一把梭等于赌博。
适用边界。Agent 迁移适合模式清晰、测试完备的代码库。动态语言、元编程密集、无测试的代码,自动迁移风险极高。一个常被忽视的点是"迁移的不可逆检查"。
每条规则应标注是否可逆,不可逆规则强制要求更高置信度与人工签字,避免事后发现改错却无法回退。另一个实践要点是"规则版本与代码版本对齐":迁移规则本身要纳入版本管理,与被迁移代码的 commit 对应,出问题时能精确复现"哪条规则在哪次迁移改了什么"。最后,人工 review 的抽样比例要随置信度动态调整,高置信度规则抽 5%,低置信度规则全量人审,用有限的人力盯住真正的风险点,而非均匀稀释审查力度。
结论
大规模代码迁移,靠 Agent 把流程拆成识别、改写、验证三段。机制上规则可版本化、置信度门槛分级、分批执行可回滚。工程上 AST 保证结构安全,LLM 辅助语义,人裁决灰区。落地路线:先把迁移模式编码为规则集;再 dry-run 全量扫描产出清单;按模块分批执行并跑回归测试;低置信度命中人工预审。迁移能自动化,但信任要分级。




