接手过一套年久失修的遗留系统的人,大概都体会过这种无力感:核心模块上千行,函数调用关系像一团乱麻,文档早就和代码脱节,谁都不敢轻易动里面的逻辑。我去年在某跨平台系统的重构中就是这么个局面。传统做法是召集几个人,排期几个月,靠人肉通读代码一层层扒。后来我换了一条路——把重构拆成一条带护栏的自动化流水线,让 AI Agent 在受控轨道里做分析、改码、验证、提交,我负责任务切分和最终把关。
整套东西跑通之后,我沉淀出了一套可复用的工具集合,也就是这次要聊的“自动化代码重构 Agent Harness”。它解决的核心问题是:怎么让编码 Agent 在不失控的前提下完成大规模重构。适合谁参考?给那些想用 AI 辅助重构、但又不放心裸调模型直接改代码的开发者,也适合需要在多个仓库里反复执行同类重构的团队。这篇实战总结会把设计思路、模块拆解、完整实操流程和踩坑记录都过一遍,尽量说透。
1. 重构 Harness 的整体设计思路
1.1 为什么需要 Harness,而不是裸调模型
很多人第一次尝试用 AI 做重构,都是把整个仓库喂给模型,然后让它“帮我把代码改得更干净”。实际跑一次就知道,这条路走不通。我自己踩过的坑就有三类:
第一,上下文长度限制。一个中型仓库动辄几万行代码,远超过模型能一次处理的范围。模型只能“看到”一部分代码,改出来的逻辑往往前后不一致,经常丢掉某些边界分支。
第二,修改范围不可控。模型在生成代码时,会自作主张地“优化”一些没让它动的文件。我见过它顺手改了配置文件、改写了注释、甚至调整了测试代码的格式,这些无关改动在 code review 时会消耗大量人工精力,在自动化流水线里更会造成不可预期的级联问题。
第三,缺少验证闭环。裸调模型只产出代码,不保证代码能编译、能通过测试。传统重构必须靠编译器和测试套件兜底,把这两件事交给模型自己判断并不现实。
所以我把 Harness 定位成一个带护栏的流水线系统:Agent 不是直接“改文件”,而是被约束在一个任务框架里,只能产出差异补丁(diff),所有 diff 必须经过最小化检查、语法验证、构建测试、人工审批四道关卡,确认没问题之后才真正落地。你可以理解成——给一个很聪明但没经验的新人配一套标准工作流程和安全带,而不是任由他自己发挥。
1.2 核心架构与数据流
整个 Harness 分四层:
- 任务建模层:把大型重构目标拆成可独立验证的子任务,每个子任务有明确的文件范围、约束条件和验收标准。
- 上下文准备层:基于调用图和符号索引,对仓库信息做裁剪,只把本次重构真正涉及的那部分代码片段、函数签名和依赖关系交给 Agent。
- 执行与沙箱层:Agent 在隔离环境里生成 diff,Harness 对 diff 做路径白名单校验、语法检查和影响面分析,全部通过后才进入验证阶段。
- 验证与回滚层:自动执行单元测试、类型检查、静态分析,完成后生成审计日志,人工复核通过后再合入主干,任何一步失败都可以快速回滚。
数据流大概是这样的:git 基线 diff → 语义索引 → 任务切片 → 上下文构造 → Agent 推理 → diff 生成 → 最小化过滤 → 构建与测试 → 审批合入。每一步的输出都会写入审计日志,方便事后回溯。
1.3 关键技术选型
- 语义分析部分用了 tree-sitter 解析代码结构,把函数、类、调用关系提取成符号索引,比正则表达式稳定得多,能照顾到注释、字符串里的假代码。
- 上下文裁剪的核心是调用图。我先建立文件、函数两级调用图,然后从目标文件出发,求受影响的闭包,再按深度阈值截断,保证 Agent 拿到的上下文足够但不臃肿。
- 沙箱环境用容器做隔离,和宿主机共享只读的仓库镜像,写操作只发生在构建产物目录里。Agent 没有直接写原仓库的权限,所有变更必须通过 diff 形式交回宿主侧处理。
2. 核心模块拆解与实现要点
2.1 任务建模与切片
这是最容易偷懒、也最不能偷懒的一步。切片质量直接决定重构成功率。一个任务卡片至少要包含目标描述、涉及文件、硬性约束、验证命令和期望产出,我通常用 JSON 格式管理:
{ "task_id": "TASK-0042", "objective": "将 PaymentService.charge() 从同步阻塞改为异步回调", "target_files": [ "src/services/payment_service.py", "src/models/payment.py" ], "constraints": [ "不得修改对外 API 签名", "新增回调接口需兼容旧版本", "所有异常路径需有结构化日志记录" ], "validation": [ "python -m pytest tests/test_payment_service.py", "python -m mypy src/services/payment_service.py" ] }切片颗粒度需要权衡:太粗,Agent 处理不过来,上下文会超出上限,结果也难验证;太细,任务数量膨胀,流程开销大,还可能把原本紧密耦合的改动切得七零八落,导致中间状态编译不过。我的经验是以“一个可独立验证的行为变更”为最小单位。比如支付流程改造可以切成“新增异步回调接口”“迁移核心支付逻辑”“更新外部调用方”三个子任务,而不是把整个支付模块一次丢给 Agent。
2.2 上下文准备与压缩
给模型喂的上下文,是决定输出质量的第一要素。我的原则是“宁缺毋滥但必须相关”。具体做法是:
- 先用 tree-sitter 提取目标文件里所有函数和类的签名、类型注解、整体注释。
- 再构建调用图,从任务目标出发,把直接调用方、被调用方和同文件内兄弟函数纳入上下文。
- 对间接关联函数不做全文展开,只输出它们的接口签名和一行摘要,让 Agent 知道存在这些依赖,但不需要关心实现细节。
- 最后把所有内容压缩成结构化的 Markdown 上下文块,按“目标文件全文 -> 直接依赖签名 -> 间接参考”的顺序排列,并在开头单独标注本任务的硬性约束。
压缩这一步非常关键。我试过把整个调用闭包全部塞给模型,结果上下文超过窗口上限,被截断后模型反而开始“编造”不存在的依赖。后来改成按深度截断,只保留两层调用深度,效果立刻上来了。
2.3 差异生成与安全校验
我让 Agent 只负责生成 diff,不允许直接产出完整文件。这有三个好处:第一,diff 天然自带最小化约束,模型不容易发散;第二,diff 便于做逐行审查和回滚;第三,diff 可以直接和基线做合并,避免整个文件覆盖造成的隐性变更。
Agent 生成的 diff 要经过一个“清洗管道”,主要做四件事:
- 路径白名单检查:diff 里出现的文件路径必须落在任务卡片的 target_files 范围内,否则直接丢弃该 hunk,并记录审计告警。
- 语法检查:用 compiler 或 parser 对修改后的文件做语法解析,语法不对直接打回。
- 依赖变化分析:对比修改前后文件导入的模块和调用的外部接口,识别出新增依赖或移除依赖,确认是否超出任务允许范围。
- 最小化校验:对于每个 hunk,检查它是否和任务目标语义相关。我这里用一个轻量的文本相似度模型,把 hunk 内容和任务 objective 做匹配,相似度过低的 hunk 视为无关改动,进入人工复核队列。
这个管道一开始只做了白名单检查,后来发现不够,模型偶尔会在允许的文件里塞进一些“顺手优化”的无关改动,所以加上了语义相关性校验。加了之后,diff 的“干净度”从最初的 65% 提升到 91% 以上。
2.4 审批与回滚机制
自动化不等于无人值守。我的流水线在最后合入之前,永远保留一道人工审批卡点。Harness 会自动生成一份变更说明,包含改动文件清单、新增函数、删除函数、核心逻辑变更摘要、测试覆盖结果和技术债提示。我只需要在代码托管平台的合并请求页面上浏览 diff,确认没有问题后合入。
回滚机制分两级:第一级是任务级回滚,验证失败的任务直接丢弃 diff,不进入合入流程;第二级是合入后回滚,万一测试环境没覆盖到的问题暴露了,可以基于 git 历史一键回退到上一个稳定提交。实际操作中,我还会把每次重构的基线快照存成一个 tag,回滚时直接指向这个 tag,比手动找 commit 稳得多。
3. 实操:从零搭建一套可运行的 Harness
3.1 环境准备与配置模板
先列一下我的基础环境,供参考:
- 开发语言:Python 3.10+
- 代码分析:tree-sitter、tree-sitter-language-pack
- 调度框架:纯 Python asyncio,没有引入重量级任务队列
- 模型侧:通过统一的 LLM 客户端接口接入,方便切换模型
- 构建与测试:pytest、mypy、flake8
- 沙箱:Docker 容器,内部构建工具链完整
配置模板长这个样子:
# harness_config.yaml model: provider: openai-compatible model_name: coding-model-v2 temperature: 0.1 max_tokens: 4096 harness: context_depth: 2 diff_max_hunks: 50 allowed_paths: - src/** - tests/** validation: static: true mypy: true pytest: true coverage_check: false review: require_human_approval: true audit_log_path: ./audit_logs/temperature 设成 0.1,是为了让代码生成尽量确定。重构任务不需要创造性发散,稳定、可预期比“有惊喜”更重要。max_tokens 按单次 diff 生成量设了 4096,任务太大时宁可让 Agent 分多轮处理,也不要憋一次长输出。
3.2 流水线核心代码骨架
主流程的关键代码(简化版)如下,剥离了真实项目里的细节,但骨架清晰:
# core/harness.py from dataclasses import dataclass from pathlib import Path @dataclass class RefactorTask: task_id: str objective: str target_files: list[str] constraints: list[str] validation_cmds: list[str] class RefactorHarness: def __init__(self, llm_client, repo_path: str): self.llm = llm_client self.repo = Path(repo_path) self.audit = AuditLogger(Path("./audit_logs")) async def run(self, task: RefactorTask) -> dict: # 1. 构建调用图并提取目标上下文 symbol_index = build_symbol_index(self.repo) call_graph = build_call_graph(symbol_index) context = build_context( task, symbol_index, call_graph.limit(task.target_files, depth=2) ) # 2. 请求模型生成 diff prompt = build_prompt(task, context) diff_text = await self.llm.generate_diff(prompt) # 3. 清洗管道:白名单 + 最小化 + 语法检查 clean_diff = clean_diff_pipeline( diff_text, allowed_paths=task.target_files, objective=task.objective ) # 4. 应用 diff 并执行验证 apply_diff_to_worktree(self.repo, clean_diff) results = await run_validations(task.validation_cmds) # 5. 记录审计日志并返回审批清单 self.audit.log(task, clean_diff, results) restore_worktree(self.repo) return { "task_id": task.task_id, "diff": clean_diff, "validation_results": results, "status": "pending_review" }这个骨架看起来简单,但工程化部署时要注意:调用图构建必须在干净的基线上执行,如果工作区有未提交改动,分析结果会失真;apply_diff 和 restore_worktree 要用事务式设计,任何一步失败都要保证仓库能回到原始状态。
3.3 一次完整重构的执行记录
我拿模拟项目 X 的一个遗留模块说事。这是个支付中台服务,核心类 PaymentService 耦合了渠道适配、风控检查、对账、日志、重试五种职责,主方法 charge() 有 300 多行,且全部是同步阻塞逻辑。重构目标是把渠道调用改成异步回调,同时保持对外 API 不变。
任务切片拆了三个:
- TASK-0041:新增回调注册接口和事件结构体。
- TASK-0042:将 charge() 内部渠道调用改造成异步事件发布。
- TASK-0043:更新内部调用方,适配新的异步调用方式。
每个任务单独跑一遍流水线,总耗时约 40 分钟。人工复核阶段,每个任务花了 10 到 20 分钟不等。整体效果如下:
| 指标 | 重构前 | 重构后 | 变化 |
|---|---|---|---|
| 平均函数行数 | 214 | 48 | -77.6% |
| 平均圈复杂度 | 27.4 | 9.6 | -65.0% |
| 模块扇出均值 | 9.2 | 3.8 | -58.7% |
| 单元测试通过率 | 93.7% | 100% | +6.3% |
| 接口文档覆盖率 | 41% | 100% | +59% |
最让我满意的不是行数下降,而是接口文档覆盖率从 41% 拉到了 100%。这是因为我在任务约束里要求“新增的异步回调接口必须带完整类型注解和 docstring”,Agent 在执行候选集生成时会主动补全这些信息,省了我们团队大量补文档的时间。
3.4 过程观测与审计留痕
重构过程必须可观测。我给每个任务生成了一个“执行轨迹”,包括:输入上下文快照、模型原始输出、清洗管道各步骤的中间结果、验证命令的 stdout/stderr、最终 diff 内容和人工审批意见。这些记录统一落到 audit_logs 目录,按日期和任务 ID 组织。
这套审计日志帮过大忙。有一次线上出问题,排查到最后发现是某次重构引入了隐藏的时序依赖。靠审计日志里的原始改动记录,十分钟就定位到了具体 hunk,回滚也只花了几分钟。没有这套留痕,这种问题通常要翻半天 git 历史才能找到元凶。
4. 常见问题排查与调优实录
4.1 上下文被截断,导致边界逻辑丢失
现象:重构后的代码在正常路径上没问题,但异常分支、边界条件经常被丢掉。比如一个处理超时重试的逻辑,重构后只剩下“发起请求”,重试机制整个消失了。
排查思路:先看审计日志里的 token 统计和上下文快照,如果发现给模型的输入已经接近上下文窗口上限,那基本就是上下文被截断了。模型在长上下文里会优先关注前面的内容,后面塞进去的边界分支很容易被“忽略”。
解法:第一,把任务切片调小,让每个任务只处理一个行为变更;第二,用调用图裁剪上下文,把间接依赖压缩成签名摘要,别全文塞进去;第三,在 prompt 里把边界场景单独列一个“必须保留”清单,加重约束权重。
4.2 Agent 改动了不该碰的模块
现象:提交的 diff 里出现了目标文件之外的改动,或者目标文件里出现和任务目标无关的“顺手优化”。我在早期遇到过模型顺手把日志格式统一改掉的例子,合入之后触发了日志解析告警。
排查思路:先确认清洗管道里的路径白名单是否配置完整。如果白名单本身没配,那不用说,问题出在源头;如果白名单配了,但文件在范围内,就要看语义相关性校验有没有生效。
解法:路径白名单要写全,项目里可能出现“文件名相似但职责不同”的模块,白名单建议用仓库根目录下的完整相对路径。另外,把语义相关性校验的阈值调高一点,宁可多拦几个疑似无关的 hunk 到人工复核,也不要放水。
4.3 构建通过,但运行期出现回归
现象:静态检查、单测全绿,一到真实环境就暴露问题。有一次重构后,某个模块在并发场景下出现“先读到旧数据、后读到新数据”的时序错乱,单测没覆盖到这个场景。
排查思路:这是 Harness 验证闭环的盲区。纯静态验证和单元测试覆盖不了运行时行为,特别是异步化改造涉及并发、时序、超时这类场景时必须补集成测试。
解法:在任务卡片的 validation 命令里加入针对性的集成测试用例;对异步化任务,额外加并发压测脚本。另外,引入动态调用分析,让 Harness 自动识别反射调用、动态注册这类静态分析看不到的隐式依赖,并把它们展现在上下文里。
4.4 循环依赖导致验证失败
现象:两个切片任务分别改动一个模块,单任务验证都通过,但合到一起后出现循环依赖,静态检查直接报错。
排查思路:这是切片之间缺乏依赖关系管理导致的。TASK-0041 新增了回调接口,TASK-0042 依赖这个接口,如果两个任务并行执行,或者合入顺序不对,就可能在中间态出现引用不一致。
解法:在任务切片时做一次依赖拓扑排序,给每个任务标记“前置依赖”,Harness 调度时按依赖顺序执行,确保任意时刻仓库都处于可编译状态。这个优化之后,流水线的整体失败率降了至少 30%。
| 现象 | 根因 | 解决方案 |
|---|---|---|
| 边界逻辑丢失 | 上下文被截断 | 切小任务 + 调用图裁剪 + 边界清单 |
| diff 出现无关改动 | 白名单缺失/语义校验失效 | 完整路径白名单 + 提高语义阈值 |
| 运行期回归 | 静态验证盲区 | 集成测试 + 并发压测 + 动态调用分析 |
| 循环依赖 | 切片缺乏调度 | 依赖拓扑排序 + 串行合入 |
5. 一些经验积累与后续扩展想法
我实际跑完几个项目之后,最深的一个体会是:自动化重构工具的效果,七分在任务定义,两分在上下文构造,一分在模型能力。模型本身再强,任务卡片写得含糊、上下文里缺了依赖信息,产出的 diff 照样没法用。所以我把大部分精力放在“怎么把目标拆清楚、怎么让 Agent 看到它需要看到的东西”上,而不是换更强的模型。
另一个很值得分享的点是:从一开始就要建立效果测评基线。重构前先把复杂度指标、测试覆盖率、接口文档覆盖率量化记录下来,重构后做一次对比。没有基线,你根本不知道重构到底是变好了还是变坏了,也很容易被“行数下降了”这种表面指标误导。我用的指标组合是圈复杂度、认知复杂度、模块扇出、测试通过率和接口文档覆盖率,这套组合对大多数后端服务项目都适用。
后来我还在这套 Harness 上做了两个小扩展,收益都不错。一个是自动生成 code review 意见草稿,合入前让 Agent 基于 diff 产出一份审查建议,人工在它基础上补充,效率提升明显;另一个是让 Agent 在重构过程中同步生成迁移笔记,直接沉淀成团队文档,省掉了事后补充文档的环节。后续我计划把它扩展成支持多 Agent 协作的形式,让一个 Agent 负责拆解,一个负责执行,一个负责交叉验证,毕竟不同角色对代码的理解角度不一样,交叉检查能发现更多盲区。
如果你想在自己的项目里落地这套思路,建议先挑一个边界清晰、测试覆盖比较好、风险相对可控的模块做试点。不要一上来就啃历史包袱最重的核心链路,把流程跑顺、把指标基线建好,再逐步扩大范围。踩过几次坑之后你会认同一个判断:自动化重构的价值不在于把活全干了,而在于让重构过程变得可规划、可验证、可回溯,把风险控制在可接受的范围里。