一次做对:把 Agent 重构代码这事工程化,到底要解决什么问题
这两年不管是开源社区还是公司内部,用大模型做代码重构的尝试越来越多。但绝大多数尝试都停留在“写个提示词让模型改一段代码”的层面,改完一看好像能跑,结果一过测试就翻车,要么改出隐性问题,要么一次只敢改一个小函数,根本谈不上规模化落地。我自己也在这个坑里反复横跳了很久,直到动手搭了一套完整的自动化代码重构 Agent Harness,才真正理解了问题出在哪。
这个项目标题里最关键的不是“重构”,也不是“Agent”,而是“Harness”。Harness 这个词在工程领域通常指“夹具”或“运行框架”,在 AI Agent 的语境下,它更像是一整套约束和编排机制:让 Agent 知道该做什么、按什么顺序做、做到什么程度算完成、以及做错了怎么收场。没有 Harness 的 Agent 就像没有轨道的过山车,能跑,但你不敢坐。
所以这篇内容适合谁?适合那些已经试过用大模型改代码、但觉得效果不稳定的人,适合想在公司内部落地“AI 辅助重构”但不知道从哪下手的人,也适合想理解 Agent 工程化背后那些“看不见的坑”的开发者。我会把整个 Harness 的架构设计、核心细节、实操过程、以及我踩过的典型问题全部拆开来讲,不藏私。
1. 整体设计思路:为什么重构这件事需要一套 Harness
1.1 代码重构的特殊之处:它不是生成,而是“受控修改”
代码生成和代码重构,看起来都是让模型输出代码,但本质上是两件完全不同的事情。生成是从零到一,模型有极大的自由发挥空间,只要输出能用、符合需求,结果就算成立。重构则是从一到一点五,代码已经有了,结构在那里,逻辑在那里,你要的是在“不改变外部行为”的前提下改进内部实现。
这就带来一个关键问题:模型的自由度必须被严格限制。你让模型“优化一下这段代码”,它大概率会给你返回一版面目全非的实现,看着挺漂亮,但原来的边界条件、异常处理、隐式约定全丢了。这不是模型能力不行,而是任务定义出了问题——你在让模型做一件没有边界的事,它就只能凭“想象力”来做。
所以 Harness 的第一职责,就是把重构任务从开放式提问变成受限操作。具体来说,我做了三层限制:
第一层是目标函数的限制。每次重构必须明确“改什么、不改成什么”。比如一次重构只处理性能优化,那所有跟性能无关的改动都应该被拒绝;一次重构只负责抽取公共方法,那业务逻辑就不允许被顺手调整。
第二层是变更范围的限制。Harness 在启动时会用静态分析定位需要修改的文件和函数边界,Agent 只能在这个边界内操作。我见过太多案例,Agent 为了“优化”一个函数,把调用它的三个地方也一起改了,最后排查问题的时候根本分不清哪次改动引入了 bug。
第三层是验证标准的限制。机器重构和人工重构最大的区别是,人靠脑补验证,机器只能靠测试验证。所以 Harness 必须把“重构完成”和“所有测试通过”绑定在一起,没有通过验证的重构,一律视为未完成。
这三层限制写进系统里之后,重构流程的可控性会大幅提升,但代价是 Harness 本身的复杂度上来了。这就需要我们把它当成一个正式系统来设计,而不是一段调 API 的脚本。
1.2 核心架构选择:为什么不是“Agent 直接改代码”这么简单
我在动手之前,试过几种不同的架构方案。最早是“一锤子买卖”:把代码塞给模型,让它输出修改后的完整文件,然后人肉 review、人肉跑测试。这种方式对单个文件的单点修改确实能用,但如果一个重构任务涉及十个文件、二十个函数,模型输出的内容长度会超出上下文限制,而且一旦某个文件改错了,你都不知道该从哪里开始排查。
后来我试过“多轮对话式”:让模型自己决定改哪个文件、改成什么,遇到问题再问它。这种方式的问题是,模型经常高估自己的能力,它会在没有验证的情况下连续修改多个文件,最后告诉你“重构完成”,但代码已经处于一个中间状态,既不是改前也不是改后,特别被动。
最终我采用的方案,是一个三层流水线架构,把整个重构拆成“计划—执行—验证”三个独立阶段,每一个阶段都由独立的模块控制,而不是让 Agent 自由发挥。
最上层是规划器,负责把用户提交的重构需求解析成具体的任务列表。比如用户说“把订单模块的金额计算改成使用 BigDecimal”,规划器会先做静态分析,找出涉及金额计算的所有文件和函数,然后生成一个有序的修改清单,并标注每个文件的变更类型和风险等级。
中间层是执行器,也就是 Agent 真正工作的部分。执行器拿到规划器输出的任务清单后,会逐个文件、逐个函数地进行修改,每次修改都会记录变更前后内容,并附带一段简短的修改说明。
最底层是验证器,它负责在每次修改之后立刻运行测试、静态检查和类型检查,把结果反馈给执行器。如果测试失败,执行器要尝试修复;如果连续几次修复失败,这个任务的修改会被回滚,并且把失败原因记入日志,留给人工处理。
这套架构最大的好处是把“不确定性”限制在了执行器这一个环节。规划是确定的,验证是确定的,只有中间的执行阶段允许 Agent 输出变化。一旦出了问题,我们可以确定问题出在执行阶段,排查范围被大大缩小。
1.3 数据流与状态管理:让 Agent 不“失忆”的关键
另一个容易被人忽略的设计点是状态管理。Agent 本质上是一个有状态的过程,它需要知道已经完成了什么、正在做什么、下一步做什么。但这个“状态”如果只存在模型对话上下文里,那一旦超出 token 上限,或者中间某次调用返回异常,整个状态就丢了,Agent 会“失忆”。
我在这套 Harness 里把状态全部外置了,用一份结构化的任务清单文件来记录整个重构过程。每一轮 Agent 操作之后,我会更新清单的状态字段,标记哪些任务是 done、哪些是 failed、哪些 pending,然后在下一次调用时把清单完整塞回给模型,让它基于最新状态继续工作。
这么做还有个额外的好处:支持断点恢复。有一次我在重构过程中,某个被修改文件的测试一直过不了,我就手动把那个文件回滚了,然后更新任务清单,让 Agent 跳过这个文件继续处理其他部分。这种操作在没有外置状态的架构下根本没法实现,因为你无法让模型“忘记”它刚才做过什么。
状态外置还引出了另一个有用的机制——检查点。每完成一个文件的重构并通过验证,Harness 就会以 git commit 的方式打一个检查点。后续如果某个修改引入了问题,我可以直接对比检查点之间的差异,定位到具体是哪次修改造成的。这个机制在排查问题的时候价值极高,后面我会详细讲。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 核心细节解析:Harness 的五个关键模块
2.1 任务分解:把“重构一个模块”变成“修改 N 个函数”
任务分解是整个 Harness 里我最看重的部分,它直接决定了后续 Agent 执行的质量。最初我尝试让模型自己分解任务,让它看完一段代码后自己说“我要改 A、B、C”,结果发现模型很容易漏掉一些隐蔽的依赖点。比如它改了函数 A 的签名,但没注意到函数 B 调用了 A,结果 B 那直接编译报错。
后来我把任务分解的活从模型手里拿出来了,改成先跑静态分析,再让模型基于分析结果做分解。具体流程是:先用静态分析工具扫描目标模块,提取出函数调用关系、依赖图、数据流,然后把这些信息连同重构需求一起交给模型,让它在“已知依赖关系”的基础上做修改计划。
这一步的效果非常明显。模型拿到函数调用关系之后,不仅会列出直接修改点,还能标注出“这些函数被以下位置调用,签名变更后需要同步修改”之类的联动任务。我在实际跑的过程中,大概有六成左右的重构任务会在规划阶段就发现一个以上的人工遗漏点,这要是直接让 Agent 闷头改,后面肯定要返工。
任务分解的输出格式我用的是 JSON,每个任务包含 file、function、change_type、depends_on、risk_level 五个字段。change_type 控制这次修改的类型(比如 modify_signature、extract_function、inline_function、optimize_logic),depends_on 标记任务之间的依赖顺序,risk_level 用于决定是否需要更严格的验证策略。
2.2 上下文裁剪:让模型在“局部视图”里看代码
上下文管理是我踩坑最多的一个模块。一开始我图省事,直接把整个项目的代码塞给模型,让它自己找需要改的地方。结果有两个问题:第一是 token 消耗极其夸张,一个中型项目跑一次重构,光输入就要几百万 token,成本根本扛不住;第二是模型在大量无关代码的干扰下,反而容易遗漏真正需要改的地方。
后来的方案是:每个任务只提供“局部视图”。Harness 会从静态分析结果里提取出跟当前任务直接相关的代码片段,通常是目标函数本身、它的直接调用者、以及它依赖的工具函数或数据结构,拼装成一份精简的上下文包,然后再交给模型。
这个方案有一个前提条件,就是静态分析结果必须足够准确。我用的是基于语法树的解析方案,先拿到每个函数完整的 AST 节点,再通过符号解析找到调用关系,这样裁剪出来的上下文基本能保持在 100 到 200 行的量级。有些任务需要跨文件修改,我会把涉及的文件都裁剪进去,但每个文件只保留相关函数,避免无关代码占用上下文。
裁剪之后还有个细节要注意,就是保留必要的脚手架。如果目标函数引用了某个枚举类型或者全局配置,这些定义也要放进上下文,否则模型看到一半发现类型不存在,就会靠“猜”来补全,结果往往是造出一个不存在的东西。我的做法是让静态分析模块自动提取目标函数的依赖符号表,再把符号定义一并打包进上下文。
2.3 提示词工程:给 Agent 立规矩,而不是提要求
很多人写 Agent 的提示词,喜欢用“请优化以下代码”这种问法,这在 Harness 架构里是不适用的。Agent 需要的是明确的操作指令,而不是开放式的“请”。我用的提示词模板分三个部分,每一部分都有具体的约束。
第一部分是角色边界设定,我会告诉模型:“你是一个代码重构执行器,你只能修改任务清单中指定的函数,禁止修改任何其他函数。”这个边界设定在开头说清楚,效果比在结尾补一句“注意不要改其他代码”好得多。
第二部分是操作约束,包含几条硬性规则:保持函数的外部行为完全一致;保留所有注释中的 TODO 和 FIXME 标记;不改变字符串字面量内容和顺序;不主动修改缩进风格(除非任务明确要求);每次只输出修改后的完整函数体,不输出解释性文字。
第三部分是输出格式约束,我要求模型必须输出 JSON,包含修改后的代码、变更描述、以及一个自查清单(是否改变了函数签名、是否改变了返回值语义、是否更新了文档字符串)。这种结构化输出让后续的自动化验证可以很方便地解析结果,不用在自然语言里翻找代码片段。
写好提示词之后我自己也迭代了很多轮。核心的体会是:明确告诉模型“不要做什么”比“要做什么”更有效。因为重构任务里“不改变行为”是最高优先级,而模型天然有“表现欲”,容易顺手做点“锦上添花”的改动,所以每一次不期望的改动都要写进负面清单。
2.4 验证闭环:测试套件是 Agent 唯一信任的裁判
重构的安全性归根结底靠的是验证,而不是模型的能力。所以我在这套 Harness 里把验证做成了强制闭环,任何被修改的函数都必须通过三类检查才能算完成。
第一类是静态检查,包括语法检查、类型检查、lint 检查。这一步是为了快速发现问题,比如模型输出了一段缺少逗号的代码,或者引用了不存在的变量,静态检查能在几秒内报错,不用等到跑测试。
第二类是单元测试,Harness 会在每次修改后自动运行目标函数相关的全部测试用例。这里有个关键操作:先把修改前的测试结果跑一遍作为基线,修改后测试通过率只能等于或高于基线,不允许出现“这次改好了 A 的测试但搞挂了 B 的测试”的隐性回退。
第三类是差异分析,我会用 diff 工具对比修改前后的代码,自动检查是否存在“函数签名变更”“删除公共函数”“改变返回值类型”等高风险操作。这些操作即使测试通过了,也可能影响外部调用方,必须列入人工复核清单。
验证的优先级我设成“先快后慢”:先跑静态检查过滤明显错误,再跑测试,最后做 diff 分析。流程跑下来,一次验证通常在一两分钟内完成,不会让 Agent 长时间空等。
注意:验证阶段不能只跑测试就完了。测试只能证明“当前用例覆盖到的行为没变”,覆盖不到的行为可能已经悄悄改了。所以 diff 分析是必须的防守线。
2.5 检查点与回滚机制:敢于让 Agent 失败,但要让它失败得干净
Agent 在执行过程中一定会失败,这不是问题,问题在于失败之后系统能不能恢复到一个干净的状态。我的方案是:每个任务执行前创建一个轻量级检查点,只记录本次任务涉及的文件的当前内容,不记录全量快照。
如果某个任务执行后验证失败,Harness 先尝试让 Agent 自我修复两次。这两次修复都失败的话,就直接回滚到检查点状态,并把这个任务标记为“需要人工处理”,继续执行下一个任务。这避免了两个问题:一是 Agent 在同一个问题上反复死循环,浪费大量时间和 token;二是失败的半成品代码残留到最终结果里,污染整个重构的效果。
检查点管理在实现上很简单,就是对涉及的文件做一个副本保存,或者直接用 git stash 功能。我后续又加了一个增强:每次回滚时自动生成一份回滚报告,把失败原因、Agent 的修改内容、验证输出归档到日志里,方便人工接手时快速定位问题。
这套机制的建立让 Harness 的容错能力有了质的提升。之前我担心 Agent 会把代码“改坏了没发现”,现在有了自动回滚,最坏的情况也只是某个任务没完成,不会出现整个项目被改得千疮百孔却毫无察觉的情况。
3. 实操过程与核心环节实现
3.1 环境准备:一套能跑 JSON 化 Agent 流程的最小配置
先说环境。我这套 Harness 用的是 Python 作为主语言,主要因为静态分析这块 Python 的生态最顺手。核心依赖没几个:一个支持语法树解析的库、一个 Git 操作库、一个 HTTP 客户端用来接大模型 API,再加上 Node.js 环境(有些前端项目里的代码需要用到针对 JavaScript/TypeScript 的分析工具)。
如果你要复现这套流程,我不建议一上来就搭完整框架,而是先把最小闭环跑通。最小闭环包含四个脚本文件:
planner.py:负责静态分析、任务分解,输出tasks.jsonexecutor.py:负责调用模型接口,执行单个任务,输出修改后的代码validator.py:负责跑静态检查、测试、diff 分析,输出验证结果manager.py:负责调度,串起整个流水线,管理检查点和回滚
我第一次跑通大概是用了两个周末,不是因为这四个脚本有多难写,而是中途反复处理各种边缘情况花了不少时间。比如有些函数有嵌套定义,静态分析提取的时候会丢掉内层函数;有些文件编码不是 UTF-8,解析直接报错;还有一些代码库里的测试用例本身就是坏的,跑起来一直是红的,导致验证逻辑没法判断是否是 Agent 引入的问题。
所以这里给一个非常实际的建议:首次接入 Harness 时,选一个测试覆盖率高、代码风格规范、git 历史干净的模块作为试点。哪怕只是一个几十个函数的小模块,也足以把整套流程跑通并发现大部分问题,有了经验之后再扩展到更大的代码库。
3.2 任务分解模块:从需求描述到结构化任务清单
先看 planner 的实现思路。输入是一个自然语言的重构需求,输出是一份 tasks.json。
我举个实际跑过的例子:需求是“把订单模块中所有金额字段从 Float 改成 BigDecimal,并确保四舍五入规则一致”。planner 会做这几件事:
第一步,定位目标模块。通过需求里的关键词“订单模块”,在项目结构里找到对应目录和入口文件。
第二步,提取相关函数。扫描该目录下所有函数,过滤出参数或返回值类型包含 Float、以及函数体内部调用了 Float 运算的函数,得到一个候选函数清单。
第三步,分析函数间的依赖关系。这一步比较关键,因为改完一个函数的签名后,所有调用它的位置也要改。比如 calculateTotal 改了签名,那 checkout 和 invoice 这两个调用方也要进任务清单,哪怕它们本身的代码不需要动。
第四步,生成任务清单并排序。每个任务带有 depends_on 字段描述依赖顺序,先改底层函数,再改上层调用方。排序规则是:如果 A 被 B 调用,则 A 排在 B 前面。
最终生成的 tasks.json 长这个样子:
json复制[
{
"id": 1,
"file": "src/order/calculator.py",
"function": "calculateTotal",
"change_type": "modify_signature",
"depends_on": [],
"risk_level": "high",
"description": "把入参 total 的类型从 Float 改为 BigDecimal"
},
{
"id": 2,
"file": "src/order/checkout.py",
"function": "checkout",
"change_type": "update_callsite",
"depends_on": [1],
"risk_level": "medium",
"description": "适配 calculateTotal 的签名变更"
}
]
注意我在写 planner 逻辑时踩过一个坑:千万不要让模型直接输出这个 JSON 清单。模型生成的 JSON 经常出现字段拼写错误、任务遗漏、依赖关系自相矛盾的情况,使用成本很高。正确的姿势是让静态分析工具生成一个“任务候选集合”,然后让模型在这个集合的基础上做增删和排序,而不是从零生成。
3.3 执行模块:Agent 如何“小心地”改代码
executor 的工作流程是:从 tasks.json 里拿一个任务,构造上下文,调用模型接口,拿到修改结果,然后交给 validator 验证。
上下文构造这一步我前面已经提过,这里补充一个具体的操作细节。假设当前任务是修改 calculateTotal 函数,Harness 会提取它的源码、它的文档字符串、它的调用方 checkout 和 invoice 的调用片段、以及它依赖的 Money 类的定义,打包成一份精简的代码上下文。然后提示词会包含这么一段指令:
“你正在执行一个受控重构任务。目标函数是 calculateTotal,修改要求是:把入参 total 的类型从 Float 改为 BigDecimal。修改范围仅限于该函数本体。输出格式为 JSON,包含修改后的代码和变更说明。以下是你需要修改的代码及其相关上下文……”
模型返回结果之后,executor 会先做一次简单的本地解析验证,检查输出 JSON 格式是否正确、代码片段是否完整,然后再交给 validator。
这里有一个我很想强调的细节:executor 需要维护一个“已修改文件列表”。当一个文件里的多个函数都需要修改,但分散在不同任务里时,如果不合并修改,模型每一次拿到的都是原始代码,前面改的内容就丢了。我在实现时,executor 会先检查文件是否已经被改过,如果是,就先把当前文件内容读取出来,连同待改函数一起作为上下文传给模型,让模型在“当前的版本”上继续修改。
3.4 验证模块:自动化判定“改得对不对”
validator 是整套流水线里最“机械”但也最不能偷懒的模块。它的核心逻辑是一个三层检查流程:
第一层是语法与类型检查。Python 项目我用编译检查加类型检查,如果是 JS/TS 项目就跑相应的 linter。这一步要快,因为大部分模型输出的小错误在这一层就能暴露,不需要浪费时间去跑完整测试。
第二层是单元测试。这里的关键是实现增量测试策略:先从测试文件和静态分析结果里,找出覆盖了当前被修改函数的所有测试用例,只跑这些用例。找到增量测试才能保证验证速度,如果每改一个函数就全量跑一遍测试,任务一多就变成瓶颈了。
第三层是差异对比。我会把修改前后的代码进行 AST 级别的对比,而不是简单的文本 diff。AST 对比的好处是可以做语义级判断,比如忽略纯格式变化(缩进、空格、换行),只关注结构性变化(新增了判断、改变了返回值、调整了函数参数顺序等)。对于结构变化点,validator 会逐一判断是否符合任务要求,如果发现超出任务范围的改动,会直接标记为“违规改动”并且要求 Agent 修复。
validator 最后输出一个验证报告,包含结论(pass/fail)、每层检查的详细输出、以及违规改动列表。如果验证通过,manager 就提交本次修改并更新任务状态;如果验证失败,manager 会根据失败次数决定是让 Agent 修复还是回滚。
3.5 调度与回滚:流水线的“总导演”
manager 模块的职责是串起整个流程,它做的事情看起来简单,但实现起来有不少边界情况要处理。
调度逻辑是这样的:启动时读取 tasks.json,找到所有 depends_on 为空的任务放入待执行队列;每完成一个任务,更新状态并把依赖该任务的后续任务“解锁”进队。这个调度逻辑保证 Agent 永远不会在依赖项还没完成时就去修改上层函数。
回滚逻辑则是这样设计的:每个任务在执行前,先记录当前文件内容,存为临时检查点。如果任务执行后验证失败,并且连续修复次数超过阈值,就恢复检查点内容,同时把该任务标为“skipped”。这种做法的前提是任务间的依赖足够规范,如果一个任务失败,依赖它的下游任务也会跟着跳过,直到人工介入处理。
还有一类特殊情况:同一个文件被多个任务修改。比如任务 A 修改了文件 X 的 func1,任务 C 修改了文件 X 的 func2,如果任务 C 执行时验证失败需要回滚,回滚的应该只是 func2 的修改,而不是整個文件恢复原样。我的处理方案是:检查点在回滚时会做一次自动的“两方合并”,把该文件的当前版本和检查点版本做一次基于 AST 的补丁合并,保留检查点之前的修改(func1),回滚掉本次修改(func2)。
提示:回滚时的“局部回滚”不是一个 trivial 操作。最简单的方案是每个函数单独存一份检查点,回滚时只恢复函数级别的代码块,而不是文件级别的快照。我是用 AST 节点替换实现的,先定位目标函数的 AST 节点,把节点替换为检查点版本,再生成新的文件源码。
4. 常见问题与排查:实录我踩过的那些坑
4.1 问题一:Agent 总是试图“越权”修改无关代码
这是我遇到的最频繁、也是最难根治的问题之一。即使我在提示词里明确写了“禁止修改任务范围外的代码”,模型在具体执行时还是会偶尔顺手调整一下相邻函数的格式、改动一个变量名、或者删掉一段看起来“没用”的注释。
一开始我尝试加强提示词,在负面清单里加了很多条“禁止做 X”,但效果有限。后来我想通了:提示词防御解决不了所有问题,需要从机制上拦截。于是我在 validator 里加了一条硬性规则——凡是 diff 分析发现超出任务范围的改动,直接判定验证失败,要求 Agent 撤销这部分改动后再提交。
这个策略上线之后,Agent 的越权行为得到了有效遏制。原因很简单:模型是能“感知”到验证机制存在的。当它发现每次越权都会导致验证失败、白白浪费几轮 token 时,它会主动约束自己的行为空间。所以提示词里一定要写一句“本次修改将被自动化验证,任何超出任务范围的改动都会导致验证失败”,这句话的威慑力比十条负面清单加起来都大。
4.2 问题二:上下文裁剪后 Agent 经常“看不到”全局定义
上下文裁剪虽然大大降低了 token 消耗,但也带来一个新问题:如果被修改函数引用了一个跨模块的全局变量、枚举值或者配置项,而我在裁剪时漏掉了这个定义,Agent 就会自己“脑补”一个定义,然后导致后续编译失败。
排查的过程很折磨人。有一回一个函数明明只做了一处小修改,但编译死活过不去,我看代码发现 Agent 引用了一个不存在的枚举值。后来我在静态分析模块里增加了符号依赖追踪功能:提取目标函数的传入参数、返回值、函数体引用的所有标识符,然后去项目里查找这些标识符的定义位置,全部打包进上下文。
经过这轮改进,类似的“脑补定义”问题基本绝迹了。如果你的项目比较大、符号关系复杂,我建议给上下文裁减模块单独写一套缓存:由于同一个符号定义会被很多任务反复引用,不用每次重新解析全项目,一次分析后把结果缓存下来,后续任务直接读取即可。
4.3 问题三:测试结果不一致导致误判“重构失败”
这个坑比较隐蔽,不是代码改错了,而是验证环境本身有问题。具体场景是:重构前的测试跑出来是绿的,重构后同一个测试跑出来是黄的,于是 validator 判定“重构失败”,让 Agent 陷入了修复循环,但 Agent 怎么也修复不好,因为它根本没引入 bug。
排查后发现,问题出在测试的“非确定性”上。有些测试用例依赖系统时间、随机数或者外部服务的返回值,两次运行结果天然可能不同。这个锅不该让 Agent 背,于是我在 validator 里引入了基线重跑机制:重构后的测试失败时,先不着急判负,而是把上一次通过的构建拿出来重跑同一组测试,如果基线也失败,就说明测试本身不稳定,忽略这次失败,不触发修复流程。
这个机制上线后,误判率降了非常多。如果你的项目里也有“偶尔红一下”的测试,这个机制几乎是必须的,否则 Agent 会把大量时间浪费在跟环境问题搏斗上。
4.4 问题四:Agent 修改了函数语义但测试全绿
这可能是最危险的一种失败模式。模型的代码改动没有引发编译错误,也没有触发测试失败,但函数的行为已经悄悄变了,只是现有测试用例覆盖不到。
比如有一次任务是优化一个排序函数,Agent 在优化时把稳定排序改成了不稳定排序,测试结果全绿,但实际输出里相同权重的记录顺序发生了改变,肉眼根本发现不了。等这条代码合并到主干,过了两周才被下游的数据对账逻辑发现。
这个问题单纯靠验证闭环很难完全解决,因为测试的覆盖范围是有限的。我的应对策略分两层:一层是 diff 分析时重点关注语义敏感的操作(比如修改了比较逻辑、改变了排序稳定性、替换了数据结构),另一层是给高风险任务增加“语义一致性检查”——让一个独立的模型实例对比修改前后的代码,专门回答“这两个函数的行为是否可能不一致”的问题。这有点像是在验证闭环外面再套一层 AI 质检,虽然不能保证 100% 覆盖,但确实能大幅度降低漏网比例。
4.5 问题五:Agent 在同一个错误上反复横跳
还有一个常见情况:Agent 第一次修改失败了,Harness 让它修复,它却基于“错误的记忆”继续修改,结果越改越乱。比如它把 total 改成了 amount,测试报错说 amount 不存在,它没有发现问题是自己引入了不存在的变量,反而认为是类型定义缺失,然后去别的文件“补齐”了一个莫名其妙的定义。
制止这种行为的办法是:给 Agent 的修复轮次增加“重置上下文”机制。每次让 Agent 修复之前,不是直接把它上一次的输出继续当作上下文,而是回到“修改前的最新代码 + 失败原因列表”这个干净状态,让 Agent 从正确基线重新思考,而不是在错误的基础上打补丁。这个调整对修复成功率的提升非常明显,从原来的不到一半提高到了七八成。
与之配套的还有修复次数限制,我设置为两次。第一次修复失败后,Harness 会自动重新生成干净的上下文,提供更具体的失败信息,让 Agent 再试一次;第二次仍然失败,就直接回滚并转人工。这个阈值不是拍脑袋定的,我试过三次和五次,发现超过两次之后,Agent 的修复成功率不会显著提高,但 token 消耗会成倍增加,所以两次是性价比最优的选择。
5. 复盘、扩展与心得体会
5.1 效果评估:Harness 到底帮我省了多少事
整套 Harness 跑下来,我用它处理了一个实际的中型模块重构。这个模块大概有 40 多个函数、8 个文件,重构需求是把底层金额计算全面替换为高精度类型。全手动的话,我预估需要大半天到一天的工作量,而且因为涉及的函数链路长,人工排查很容易遗漏某一处调用位置。
用 Harness 跑完一次完整重构,耗时大概是两小时出头,其中大部分时间花在测试和检查上。最终 40 多个任务里,Agent 一次通过的超过六成,经过一次修复通过的两成,剩下的没通过,被自动回滚并转入人工处理。人工处理的部分主要是边界情况,比如某个函数里存在模型很难理解的业务规则,需要人工判断到底该怎么改。
这里面最有价值的反而不是“快”,而是放心。以前我手动重构的时候,最担心的就是某个隐性的行为变更没有被发现。现在 Harness 会在每次修改之后强制 diff 分析、语义一致性检查、测试验证,不需要人盯在每一步旁边。
5.2 这套架构还可以怎么扩展
Harness 搭好之后,你很快会发现它天然适配其他代码处理场景,不需要伤筋动骨。我列几个我觉得方向上比较顺的扩展:
第一个是多 Agent 并行。当前架构是串行处理任务,主要受限于文件之间的依赖关系。如果两个任务改的是完全不同的文件、没有任何依赖交集,完全可以并行执行。改动点主要在 manager 的调度逻辑,需要增加一个并发任务池,以及并发写文件的锁机制。
第二个是自动生成测试用例。既然 Harness 已经能对每次修改做 diff 分析,那可以在生成修改后代码的同时,要求 Agent 输出新增的测试用例,用于补充覆盖“重构时发生变化的部分”。这相当于把“改代码”和“验证代码”的闭环进一步加密。
第三个是把 Harness 接入到 CI 流水线里,让它在每次合并请求提交时自动跑一遍“重构影响分析”,识别高危改动,甚至自动生成重构建议。这个方向适合需要长期维护的代码库,能让重构从“一次性活动”变成“常态化机制”。
5.3 一些实在话和最终建议
最后说几句实在的。Agent 代码重构这件事,模型能力哪怕是只做到今天这个水平,也能极大缩短重构的耗时。但如果只靠模型本身,不搭 Harness,你大概率会觉得“这个东西中看不中用”——改出来的代码你敢用吗?你不敢。
Harness 的本质,就是把模型的能力装进一个可控的框架里。它做的事儿一点都不高深,无非是把任务拆得更细、验证放得更严、回滚做得更干净。但如果少了任何一环,整个链条的可靠性就建立在了模型的“自觉”上,而模型恰恰没有自觉。
如果你想自己动手搭一套,我建议从最小的闭环开始:先实现一个单函数的修改加验证,跑通之后再加任务分解,再加检查点和回滚,最后再优化并行和智能调度。别一上来就想着“一步到位”,因为每一步都会遇到真实的边界问题,你处理过一遍之后,才真正理解为什么某些设计是必须的。
我实际使用下来最感慨的一点是:一旦这套流程跑顺了,你会发现自己作为工程师的角色悄然发生了变化。你不再需要通宵做那些机械的、重复的、容易遗漏的重构工作,而是把精力放在定义重构目标和策略、处理模型搞不定的语义难题上。这才是这类工具应该有的样子——不是取代你,而是让你去做更有价值的事情。
