1. 为什么“定位”和“修复”不能塞进同一个任务里
1.1 一个真实场景引出的问题
去年我在做一个内部代码维护助手的时候,踩过一个很典型的坑。当时的需求很简单:给一个报错堆栈,让 AI 找出问题并改掉。我一开始的做法是把“找 bug”和“改 bug”写进同一个提示词里,让模型一次性输出“问题在哪、怎么改、改完的代码是什么”。结果跑了几十次之后发现,成功率低得离谱——要么是定位错了地方,改了一堆无关代码;要么是定位对了,但改的时候又把别的地方改坏了。
后来我把这个任务拆成两步:第一步只让模型输出“问题出在哪个文件、哪一行、根因是什么”,第二步拿着第一步的结论去生成补丁。成功率一下子从三成左右提到了七成以上。这个经历让我意识到,定位和修复本质上是两种不同的认知任务,硬塞在一起,模型会互相干扰。
这篇内容就是围绕这个现象展开的。我会从任务本质、上下文管理、Agent 架构、实操拆解几个角度,把“为什么要拆”讲透,同时给出可以直接参考的拆分方案和提示词模板。不管你是刚接触 AI Agent 开发,还是已经在做多 AI 协作的工程化落地,应该都能从中拿到一些能直接用的东西。
1.2 定位和修复,到底差在哪
先把这个核心问题说清楚。定位(Localization)的目标是:在给定的代码库、日志、报错信息里,找到“问题发生在哪里”以及“为什么会发生”。它的输出是一个判断,是一段解释,是“病灶的位置和成因”。
修复(Repair)的目标是:在已知问题位置和成因的前提下,生成一段能通过验证的代码变更。它的输出是一个动作,是一段 diff,是“手术方案”。
这两件事对模型能力的要求完全不同。定位更依赖理解、检索、推理能力,模型需要在大量信息里做筛选和判断,容错率相对高——定位偏一点,后面还能纠正。修复更依赖生成、约束、验证能力,模型需要在明确的边界内产出精确的代码,容错率极低——改错一个字符,整个补丁就废了。
打个比方,定位像是医生做诊断,修复像是医生做手术。你不会让一个医生一边做 CT 一边开刀,因为这两件事需要的注意力、工具、心态都不一样。AI 也一样。
1.3 混在一起会触发哪些具体问题
我把实际遇到过的失败模式整理了一下,大概有这么几类:
| 失败模式 | 具体表现 | 根本原因 |
|---|---|---|
| 定位漂移 | 模型直接跳到“怎么改”,跳过了“为什么” | 生成任务的目标压过了理解任务的目标 |
| 过度修改 | 定位对了,但顺手改了无关代码 | 修复阶段缺少明确的边界约束 |
| 幻觉补丁 | 改的代码引用了不存在的函数或变量 | 模型在没完全理解上下文时就动手 |
| 上下文污染 | 前面的错误定位结论影响了后面的修复 | 两个任务的中间状态混在同一个上下文里 |
| 无法回溯 | 改错了不知道是哪一步出的问题 | 定位和修复的中间结果没有分离 |
这些问题的共同点是:当模型同时承担“判断”和“生成”两个目标时,它会在两者之间做妥协,而妥协的结果往往是两边都做不好。
2. 拆分背后的核心原理:认知负荷与上下文管理
2.1 认知负荷理论在 AI 任务上的映射
认知负荷这个概念原本是教育心理学里的,说的是人的工作记忆容量有限,同时处理太多信息就会过载。这个逻辑放到大模型身上同样成立,只不过“工作记忆”变成了上下文窗口和注意力分配。
当你让模型同时做定位和修复时,它需要在同一个推理过程里维护两套目标:一套是“我要找对地方”,一套是“我要写对代码”。这两套目标会争夺模型的注意力资源。实测下来,模型往往会偏向生成任务,因为生成任务的输出更“显眼”——它要吐出代码,而定位任务的输出只是一段分析文字,容易被忽略。
拆开之后,每个阶段只有一个明确目标。定位阶段的提示词里全是“找、分析、判断”这类词,修复阶段的提示词里全是“改、生成、约束”这类词。模型不需要在两者之间切换,注意力集中度明显提升。
2.2 上下文窗口的“干净度”问题
还有一个很实际的原因:上下文污染。
假设你把定位和修复放在一个对话里。模型先输出了一段定位分析,里面可能包含一些猜测性的内容,比如“我怀疑是这里的问题”。然后它基于这个猜测去修复。如果猜测是错的,但修复代码看起来又挺像那么回事,你就很难判断到底是定位错了还是修复错了。
拆成两个任务之后,定位阶段的输出是一个结构化的结论,比如:
{ "file": "src/utils/parser.js", "line": 142, "root_cause": "正则表达式未处理空字符串输入", "confidence": 0.85 }修复阶段只接收这个结论,不接收定位过程中的所有中间推理。这样上下文是干净的,修复阶段不会被定位阶段的猜测干扰。而且如果修复失败,你可以单独回去检查定位结论对不对,排查路径非常清晰。
2.3 两个任务的验证方式完全不同
定位的验证方式是人工确认或交叉验证——你可以让另一个模型或者人来判断“这个定位对不对”。修复的验证方式是自动化测试——跑一遍测试用例,过了就是过了,没过就是没过。
这两种验证方式对任务设计的要求不一样。定位任务需要输出可解释的推理过程,方便人工审查;修复任务需要输出可执行的代码变更,方便自动验证。如果混在一起,你既拿不到干净的推理过程,也拿不到干净的代码变更。
提示:在实际工程里,定位阶段的输出建议强制结构化(JSON 或固定格式),这样后续无论是人工审查还是自动流转都方便。修复阶段的输出建议直接给 diff 或完整文件,不要给“修改建议”这种模糊的东西。
3. Agent 架构下的拆分方案设计
3.1 两种主流拆分模式
在 Agent 开发里,拆分定位和修复有两种常见模式:
串行模式:一个 Locator Agent 负责定位,输出结构化结论;一个 Fixer Agent 负责修复,接收结论并生成补丁。两个 Agent 之间通过一个明确的数据结构传递信息。
并行模式:多个 Locator Agent 从不同角度定位(比如一个看堆栈、一个看代码变更历史、一个看测试失败信息),然后把定位结果汇总,再交给 Fixer Agent。
串行模式适合大多数场景,实现简单,调试方便。并行模式适合复杂 bug,比如涉及多个模块的连锁问题,但工程复杂度高,需要处理结果冲突和置信度合并。
我个人的建议是:先从串行模式做起,跑通了再考虑并行。很多团队一上来就搞多 Agent 协作,结果定位结果对不齐,反而更难调试。
3.2 Locator Agent 的设计要点
Locator Agent 的核心任务是“缩小范围”。它的输入通常包括:
- 报错信息或异常堆栈
- 相关代码文件(或代码库索引)
- 最近的代码变更记录
- 测试失败信息
它的输出应该是一个排序后的候选列表,而不是一个单一结论。因为定位本身是有不确定性的,给修复阶段多个候选,让修复阶段自己判断,往往比强行给一个结论更好。
一个实际可用的 Locator 提示词结构大概是这样:
你是一个代码问题定位助手。你的任务是在给定代码库中找到问题所在位置和根本原因。 输入: - 报错信息:{error_message} - 相关文件:{relevant_files} - 最近变更:{recent_changes} 要求: 1. 列出最多 3 个可能的出错位置,按可能性从高到低排序 2. 对每个位置,说明判断依据 3. 不要生成任何修复代码 4. 输出格式为 JSON 输出格式: { "candidates": [ { "file": "文件路径", "line": 行号, "reason": "判断依据", "confidence": 0.0-1.0 } ] }注意最后一条要求:“不要生成任何修复代码”。这是为了防止模型“手痒”直接跳到修复。实测下来,如果不加这条约束,大概有 20% 到 30% 的情况模型会直接开始改代码。
3.3 Fixer Agent 的设计要点
Fixer Agent 的核心任务是“在约束内生成补丁”。它的输入是 Locator 的输出加上原始代码,输出是代码变更。
关键约束有几个:
- 只改定位结论指向的位置,不要顺手改别的地方
- 保持代码风格一致,不要引入新的依赖
- 生成可验证的变更,最好是 diff 格式
- 如果定位结论明显不对,要拒绝修复并说明原因
最后一条特别重要。实际跑的时候,Locator 有时候会给出错误的定位,如果 Fixer 盲目去修,就会产生“看起来改了但没改对”的补丁。让 Fixer 有拒绝权,可以过滤掉一部分错误定位。
3.4 两个 Agent 之间的数据契约
拆分方案能不能跑通,很大程度上取决于两个 Agent 之间的数据契约设计得好不好。我踩过的坑是:一开始让 Locator 输出自然语言,Fixer 去解析自然语言,结果解析经常出错。
后来改成强制 JSON 之后,稳定性好了很多。数据契约大概包含这些字段:
| 字段 | 类型 | 说明 |
|---|---|---|
| file | string | 问题所在文件路径 |
| line_start | number | 起始行号 |
| line_end | number | 结束行号 |
| root_cause | string | 根本原因描述 |
| confidence | number | 置信度 0-1 |
| evidence | array | 判断依据列表 |
| suggested_scope | string | 建议修改范围 |
这个契约的好处是:Locator 必须把“为什么这么判断”说清楚,Fixer 拿到的是一个明确的范围,不会乱改。
4. 实操拆解:从报错到补丁的完整流程
4.1 准备阶段:构建代码库索引
在跑定位之前,需要先让模型能“看到”代码。有两种做法:
全量塞入:把整个代码库塞进上下文。适合小项目,代码量在几千行以内。缺点是上下文占用大,而且模型容易在大量代码里迷失。
检索增强:先对代码库做索引,根据报错信息检索相关文件,只把相关部分塞进上下文。适合中大型项目。实现方式可以用简单的关键词匹配,也可以用向量检索。
我一般用混合方案:先用报错信息里的文件名、函数名做关键词匹配,拿到一批候选文件;再用向量检索补充一批语义相关的文件;最后合并去重,控制在上下文窗口的 60% 以内。
4.2 定位阶段:让模型输出结构化结论
定位阶段的提示词需要包含几个关键要素:
- 角色设定:明确告诉模型它是定位助手,不是修复助手
- 输入信息:报错信息、相关代码、变更记录
- 输出格式:强制 JSON,包含候选列表
- 约束条件:不生成修复代码,按可能性排序
实际跑的时候,我会让 Locator 输出 3 个候选,然后人工或者用另一个模型做一次交叉验证。如果 Top1 的置信度超过 0.8,直接进入修复;如果低于 0.5,就补充更多上下文重新定位。
这里有个经验:定位阶段的温度参数建议调低,比如 0.1 到 0.3。定位需要的是准确和稳定,不需要创造性。修复阶段可以稍微高一点,0.2 到 0.5,给模型一点灵活性。
4.3 修复阶段:在约束内生成补丁
修复阶段的提示词结构:
你是一个代码修复助手。你的任务是根据定位结论生成代码补丁。 定位结论: {locator_output} 原始代码: {original_code} 要求: 1. 只修改定位结论指向的位置 2. 保持代码风格一致 3. 输出 unified diff 格式 4. 如果定位结论明显错误,输出 {"status": "rejected", "reason": "..."} 5. 不要引入新的依赖 输出格式: { "status": "fixed" | "rejected", "diff": "unified diff 内容", "explanation": "修改说明" }跑完修复之后,一定要跑测试。测试通过才算修复成功。如果测试失败,把失败信息反馈给 Fixer,让它重新生成。一般重试 2 到 3 次还不行,就退回定位阶段重新定位。
4.4 验证阶段:自动化测试与人工复核
验证分两层:
自动验证:跑单元测试、集成测试。这是硬指标,过了就是过了。
人工复核:对于自动验证通过但改动较大的补丁,建议人工看一眼。特别是涉及核心逻辑的修改,自动化测试不一定能覆盖所有边界情况。
我一般会设置一个阈值:如果 diff 行数超过 50 行,或者涉及的文件超过 3 个,就触发人工复核。小改动直接自动合并。
4.5 一个完整的实操示例
假设报错信息是:
TypeError: Cannot read property 'length' of undefined at parseInput (src/utils/parser.js:142:23) at handleRequest (src/api/handler.js:56:12)第一步:定位
把报错信息、parser.js 和 handler.js 的相关代码、最近的 git 变更记录一起塞给 Locator。Locator 输出:
{ "candidates": [ { "file": "src/utils/parser.js", "line_start": 140, "line_end": 145, "root_cause": "parseInput 函数未对输入参数做空值检查,当 input 为 undefined 时访问 input.length 报错", "confidence": 0.92, "evidence": ["报错堆栈直接指向 parser.js:142", "该行代码为 input.length", "最近变更记录显示该函数上周刚修改过"] } ] }第二步:修复
把定位结论和 parser.js 的原始代码给 Fixer。Fixer 输出:
--- a/src/utils/parser.js +++ b/src/utils/parser.js @@ -139,7 +139,10 @@ function parseInput(input) { + if (input === undefined || input === null) { + return []; + } const tokens = []; for (let i = 0; i < input.length; i++) { tokens.push(input[i]); } return tokens; }第三步:验证
跑测试,通过。合并。
这个流程跑下来,从报错到补丁,大概需要 30 秒到 2 分钟,取决于代码库大小和模型响应速度。比人工排查快很多,而且定位结论是可追溯的,出了问题能查到是哪一步出的错。
5. 常见问题与排查技巧实录
5.1 定位不准怎么办
定位不准是最常见的问题。排查思路:
- 检查上下文是否足够:是不是相关文件没塞进去?报错信息是不是太模糊?
- 检查提示词是否有歧义:是不是让模型同时做了太多事?
- 检查温度参数:是不是太高了导致输出不稳定?
- 增加候选数量:从 3 个增加到 5 个,给修复阶段更多选择
- 引入交叉验证:用另一个模型独立定位一次,对比结果
我遇到过一次定位一直不准的情况,最后发现是代码库索引没更新,模型看到的是旧版本的代码。所以索引更新这个环节一定要自动化,不能靠手动。
5.2 修复阶段改坏了别的地方
这个问题通常是约束不够明确导致的。解决办法:
- 在提示词里明确“只改定位结论指向的位置”
- 输出 diff 而不是完整文件,减少误改范围
- 修复后跑全量测试,不只是跑相关测试
- 设置 diff 行数阈值,超过就人工复核
还有一个技巧:让 Fixer 在生成 diff 之前,先输出一个“修改计划”,说明它打算改哪些地方、为什么。这样你可以在它动手之前就发现范围不对。
5.3 两个 Agent 之间传递信息丢失
这个问题一般出在数据契约设计上。排查清单:
| 问题 | 可能原因 | 解决办法 |
|---|---|---|
| Fixer 说“定位信息不完整” | Locator 输出格式不对 | 强制 JSON schema 校验 |
| Fixer 改错了位置 | 行号偏移 | 用代码片段而不是行号定位 |
| Fixer 拒绝修复 | 置信度太低 | 补充上下文重新定位 |
| 定位结论和修复补丁对不上 | 中间有格式转换错误 | 加一层格式校验 |
行号偏移是个很隐蔽的坑。因为代码库在变,Locator 看到的行号和 Fixer 看到的行号可能不一致。解决办法是用代码片段做定位锚点,而不是纯行号。比如 Locator 输出“问题在 parseInput 函数的 for 循环那一行”,Fixer 根据这个描述去定位,比纯行号可靠。
5.4 成本和时间开销
拆成两个任务,意味着两次模型调用,成本大概翻倍。但实际算下来,因为成功率提升了,总体成本反而可能更低。
我做过一个粗略的对比:不拆分的情况下,平均需要 3.2 次尝试才能修好一个 bug;拆分之后,平均 1.4 次。虽然单次成本翻倍,但总成本降低了大概 12%。而且时间开销上,因为减少了无效重试,整体耗时也短了。
如果成本敏感,可以考虑用便宜模型做定位,贵模型做修复。定位任务对模型能力要求相对低一些,用中等模型就能跑得不错。
5.5 什么时候不适合拆分
拆分不是万能的。以下几种情况可能不适合:
- 极简单的 bug:比如拼写错误、明显的语法错误,直接修就行,拆分反而增加开销
- 上下文极小的场景:代码量很小,模型一眼就能看全,拆分意义不大
- 实时性要求极高的场景:两次调用带来的延迟不可接受
我一般的判断标准是:如果人工修这个 bug 需要先看几分钟代码才能动手,那就值得拆分。如果一眼就能看出问题,直接修。
6. 一些实操心得和后续扩展方向
6.1 提示词里的“禁止项”比“要求项”更重要
这是我踩了很多坑之后总结出来的。在 Locator 的提示词里,写“不要生成修复代码”比写“请仔细分析问题”更有效。因为模型天生倾向于“完成任务”,如果你不明确禁止,它就会顺手把修复也做了。
同理,在 Fixer 的提示词里,写“不要修改定位范围之外的代码”比写“请谨慎修改”更有效。明确的禁止项能显著降低模型的越界行为。
6.2 中间结果要落盘
定位结论和修复补丁都要存下来。一方面方便回溯,另一方面这些数据可以用来做后续的模型微调或者提示词优化。
我一般会把每次运行的输入、定位结论、修复补丁、测试结果存成一个 JSON 文件,按时间戳命名。跑多了之后,你会发现哪些类型的 bug 定位容易出错,哪些类型的修复容易失败,然后针对性地优化提示词。
6.3 后续可以扩展的方向
这个拆分方案跑通之后,有几个自然的扩展方向:
多 Locator 并行:针对复杂 bug,用多个 Locator 从不同角度定位,然后合并结果。比如一个看堆栈,一个看变更历史,一个看测试覆盖。
Fixer 自验证:让 Fixer 在生成补丁之后,自己先跑一遍逻辑推演,检查有没有明显的逻辑错误,再输出最终补丁。
定位结果缓存:相似的报错信息可以复用之前的定位结论,减少重复调用。
人工反馈闭环:人工复核的结果反馈回系统,用来优化定位和修复的提示词。
这些扩展不需要一次性全做,可以先把基础的串行模式跑稳,再逐步加。
6.4 一个容易被忽略的细节:错误信息的预处理
报错信息里往往包含大量噪音,比如时间戳、进程 ID、无关的堆栈帧。在塞给 Locator 之前,建议做一次预处理,提取关键信息:错误类型、错误消息、最顶层的业务代码堆栈帧。
这个预处理步骤看起来不起眼,但对定位准确率影响很大。我实测过,预处理之后定位准确率大概能提升 15% 到 20%。因为模型不会被噪音干扰,能更聚焦在真正的问题上。
预处理可以用简单的正则规则,也可以用一个小模型来做。规则方式更可控,推荐先用规则跑起来,不够用了再上模型。
6.5 关于模型选择的一点经验
定位和修复对模型的要求不一样。定位更看重长上下文理解和指令遵循能力,修复更看重代码生成和格式约束能力。
我试过几种组合,目前比较稳定的是:定位用上下文窗口大、指令遵循好的模型;修复用代码能力强、输出格式稳定的模型。不一定要用同一个模型。
如果预算有限,定位可以用中等模型,修复用强模型。因为定位错了可以重试,修复错了代价更大。
注意:不管用什么模型,提示词里的输出格式约束一定要严格。我见过太多次因为模型输出格式不对导致整个流程卡住的情况。建议在解析输出之前加一层格式校验,格式不对就重试,不要硬解析。
6.6 最后分享一个小技巧
如果你刚开始做这个拆分,不知道提示词怎么写,可以先手动模拟一遍:自己先做定位,把定位结论写下来,然后只拿着这个结论去做修复。感受一下“只有定位结论”的情况下,修复需要哪些信息。然后把这些信息补进 Fixer 的输入里。
这个手动模拟的过程能帮你快速找到数据契约里缺了哪些字段。我一开始就是靠这个方法发现“需要把原始代码片段一起传给 Fixer”的,因为光有行号,Fixer 找不到具体位置。
另外,定位阶段的输出里,evidence 字段特别有用。它不仅是给人工看的,也可以作为 Fixer 的参考。如果 evidence 里提到了某个具体的变量名或函数名,Fixer 在修复时就能更准确地定位到代码位置。
这个拆分方案我用了大半年,从最初的单 Agent 硬塞,到现在的双 Agent 串行,中间踩了不少坑,但整体方向是对的。核心就一句话:让每个 Agent 只做一件事,把中间结果结构化,把验证自动化。剩下的就是不断调提示词和补上下文了。