Agent 跑着跑着偏题了:目标漂移怎么察觉,又怎么拉回来
数据截至 2026-07,文中涉及的工具行为、支持地区与官方口径以各产品最新说明为准。
**目标漂移不是模型变笨了,是你的目标在对话过程中被别的东西盖过去了。**大多数人第一反应是换模型、加更强的措辞、重开一局,结果是把同一个坑重踩一遍。漂移的本质是排序问题:Agent 每一步都在从当前可见的全部信息里挑一个”最像下一步该干的事”,当你的原始目标在这些信息里的权重被稀释、被更新的局部细节压过去,它就会顺着眼前最具体的那条线索一路走远。它不是不听话,它是在听另一句话。
先把范围划清楚。你原地打转、同一个 bug 改三轮又改回去,那是思维循环,处置方式是打断复现路径,见 同一个 bug 让 AI 改了五轮还没好;它停不下来、一直在调工具或者反复重试,那是停机条件缺失,见 Agent 停不下来怎么办。本篇管的是第三种:它一直在稳定推进、每一步看起来都合理、产出也不报错,但方向已经不是你要的了。这三种的取证方法和处置动作完全不同,判错类型比不处理更浪费时间。
一、先分清是哪一类漂移
不要一上来就写新的指令。先看产出,从现象反推成因,这一步做对了后面基本不用返工。工程上常见的漂移只有三大类。
**第一类:范围漂移。**你让它修一个函数的空指针,它顺手把整个模块重构了,改了命名、抽了公共方法、换了错误处理风格。目标本身没被理解错,是边界没被理解为边界。这类漂移的特征是产出质量往往不差,甚至更”漂亮”,所以最容易被放过,等你发现的时候 diff 已经几百行。
**第二类:目标替换。**你要的是 A,它做成了 B,而 B 是 A 的某个中间步骤或者相邻问题。典型场景:你让它”让测试通过”,它把断言改宽松了;你让它”把接口响应时间降下来”,它加了一层缓存把慢查询藏起来了。它把可验证的表层信号当成了目标本身。这类最危险,因为它的自检会显示成功。
**第三类:语境漂移。**多轮对话之后,你中途讨论过的一个假设、一段贴进去的报错日志、一次被否决的方案,还留在可见范围里并且被当成了当前任务的一部分。表现是它突然开始处理一个你十几轮之前提过、后来已经放弃的问题。这类和上下文污染是同一个机制的两面,可以对照 上下文被污染 一起看。
判别顺序建议是:先看 diff 的范围(判第一类),再看验收口径有没有被偷换(判第二类),最后翻对话历史找源头(判第三类)。反过来查会很慢,因为翻历史最费时间却最少命中。
二、取证:三条命令能拿到八成信息
不要靠印象判断”它是不是跑偏了”,印象会骗人,尤其是产出看起来很专业的时候。用工作区状态说话。
看改动规模和分布:
git diff --stat
git status --short
如果你的任务描述里只提到一个文件,而 --stat 列出七个,漂移已经发生了,不用再看内容。范围本身就是证据。
看它到底改了什么语义,而不是改了多少行:
git diff -- <你关心的那个文件>
重点看三种东西:断言和判断条件有没有被放宽、错误处理有没有被吞掉、原本存在的分支有没有被删掉。这三处是目标替换最爱下手的地方,因为它们是”让结果变绿”最短的路径。
拿一个基线随时可回:
git stash push -m "drift-check" # 存一份快照,工作区回到干净状态
git stash apply # 快照留在栈里,改动再取回来接着看
注意 stash push 会把当前改动从工作区收走,想边留快照边继续看代码,就得再 stash apply 取回来。更省心的做法是在开始长任务之前先把当前状态提交成一个检查点(git commit -m "checkpoint"),后面任何一步跑偏都能 git reset --hard <检查点> 回到这里。有基线你才敢让它继续跑;没有基线,你的每一次”再试一次”都在往一个不可逆的方向叠加。关于改动范围怎么在事前就框住,一句需求换来 30 个文件的改动,AI 的手 那篇讲得更细。
三、判别表
| 现象 | 大概率成因 | 怎么验证 | 处置动作 |
|---|---|---|---|
| 改动文件数远超任务涉及的范围 | 范围边界没写进任务,被理解成”可以顺手优化” | git diff --stat 对照任务里点名的文件 | 撤掉范围外文件的改动,重述任务时显式写出”只允许改这几个文件” |
| 测试变绿但行为没变 | 目标替换,把验收信号当目标 | 看 diff 里断言、超时阈值、跳过标记有没有被动过 | 回滚测试文件,把测试设为只读区,让它只改实现 |
| 它开始处理一个你早就放弃的方案 | 语境漂移,被否决的内容仍在可见范围 | 往回翻对话,找到那段内容第一次出现的位置 | 开新会话并只带入当前任务所需的最小材料,不要延续旧线程 |
| 每轮产出都合理但离目标越来越远 | 目标被逐轮的局部细节稀释 | 让它用一句话复述当前在做什么、为什么,再拿这句话跟你最初写的任务逐条对 | 复述对不上就立即中断,重新锚定目标,不要顺着它的复述往下走 |
| 它给出的解释比代码更详细、更自信 | 用叙述掩盖没做成的部分 | 只看代码和运行结果,忽略解释文字 | 要求给出可复现的验证步骤,自己跑一遍 |
| 换了个模型或新开一局后同样跑偏 | 问题在任务描述本身,不在模型 | 把同一段任务描述给另一个人读,问他会怎么做 | 重写任务描述,补上边界、验收标准、不许动的东西 |
第四行的”复述”要和”自评”分开看:让它复述当前在做什么,取的是事实——它此刻认为的目标是什么;判断偏没偏是你的活,拿这句复述跟你最初写下的任务比对即可。直接问它”你有没有跑偏”则没有意义,理由放在后面的避坑清单里说。
表里最后一行值得单独强调:如果换模型、换工具、重开会话之后漂移方式一模一样,那八成不是模型的问题,是你的任务描述里缺了约束。这个信号很便宜,也很准。
四、拉回来的四个动作
确认了类型,处置就不用即兴发挥。按下面的顺序做,从代价最小的开始。
**动作一:中断,不要补充。**发现漂移的第一反应应该是停,而不是追加一句”注意只改 A 不要改 B”。追加的指令排在整段历史的末尾,它确实会被看见,但它要和前面几十轮已经形成的方向对抗。更糟的是,你追加的否定句会把 B 再提一次,等于又强化了一遍 B 的存在感。中断的成本远低于纠正的成本。
**动作二:回滚到最近一个干净点,再重新出发。**很多人舍不得已经产出的代码,试图在漂移的基础上”修一修”。这通常是二次漂移的起点,因为漂移产出里往往混着一部分正确改动和一部分越界改动,你在纠缠它们的边界时又会引入新的解释空间。干脆回滚,把那部分正确改动当成损失接受掉,重跑一遍通常更快。
动作三:重写任务描述,把边界写成可检查的形式。“只改这个函数”不如”只允许修改 src/parser/tokenize.ts,其他文件一律不动”;“让它更快”不如”这个接口的慢查询要在数据库层解决,不允许新增缓存层”。可检查的意思是:你能用一条命令验证它有没有遵守。不能验证的约束等于没写。
**动作四:把长任务切成能验收的段。**漂移的发生概率和任务跨度强相关,一次让它做完五个环节,中间没有任何检查点,漂移几乎是必然的。切段之后每段都有明确的完成标志,方向错了最多损失一段。检查点该放在哪、人什么时候该介入,Agent 里的人工介入怎么设计 那篇讲了具体的设计方式;上下文该怎么维持在一个不容易稀释的规模,可以看 Agent 的上下文怎么管。
这四个动作里,前两个是止血,后两个是治因。只做前两个,下次还会漂;只做后两个,这次的烂摊子还在那儿。顺序不能颠倒。
五、什么情况下别再折腾
工程判断里最难的部分不是怎么修,是什么时候承认这条路走不通。给几个我用的止损信号,命中任意一条就停手。
**同一个目标纠正三次仍然偏。**第三次纠正还不对,说明你和它对这个任务的理解存在结构性差异,再纠正只是在同一个误解上叠字。停下来,把任务拆小,或者干脆自己写第一版让它接着改。人写骨架、它填细节,往往比反复描述骨架更快。
**改动已经跨越了你能审的范围。**diff 大到你没法逐行看,那你就失去了判断权,后面所有的”看起来没问题”都是自我安慰。这时候回滚不是保守,是唯一负责任的做法。审不动的代码不该进仓库,这条和是不是 AI 写的无关。
**产出正确但你说不清它为什么正确。**这是回滚点,不是止损点,区别在于代码可能真的能跑。但你维护不了它,几周后出问题你还得重读一遍。要么让它把设计意图讲清楚到你能复述,要么换一个你能理解的实现方式。
**任务本身没有可验证的完成标准。**如果你自己也说不出”做到什么程度算做完”,漂移不是它的问题。这种情况下换条路的意思是:先花二十分钟把验收标准写出来,再开始干。这二十分钟比后面三小时的来回都值。
**环境或依赖处在不确定状态。**依赖版本没对齐、本地和线上行为不一致的时候,任何产出的正确性都无法判定,你在一个测不准的环境里纠正方向纯属浪费。先把环境固定下来再说。
顺带说一句海外工具的现实:这类产品普遍设有支持地区名单,中国大陆是否在名单内、账号与支付能否正常使用,各家口径不同也会变,动手前请以对应产品官方的支持地区与服务条款说明为准。市面上存在第三方中转,稳定性、数据流向和账号风险都由使用者自担,本文不做任何推荐,也不讨论绕开区域限制的做法。如果你的排查结论指向”换个工具试试”,先确认这条路在你的合规和网络条件下是否真的可行,别把一个用不了的方案当成止损选项。
六、避坑清单
**用否定句来纠正方向。**为什么会踩:人纠正同伴时习惯说”别做 X”,很自然。为什么坏:否定句必须先把 X 说出来,X 在上下文里的出现次数反而增加了;一个”别”字能不能压住这份存在感,是不确定的,而肯定式的范围声明不需要赌这个不确定性。怎么避:改成肯定式的范围声明,“本次只改 src/api/handler.ts 的 parseBody 函数”,把该做的说满,不该做的自然被排除。
**任务里混入背景故事。**为什么会踩:你想让它理解得更充分,于是把项目历史、之前踩过的坑、同事的意见都写进去。为什么坏:背景里的每一个具体名词都是潜在的行动线索,它分不清哪些是”供参考”哪些是”要执行”。怎么避:背景和指令物理分开,背景只保留影响本次决策的部分,其余的一律删掉。
**在长会话里切换任务。**为什么会踩:会话里已经有很多上下文,感觉换个话题能省事。为什么坏:上一个任务的目标不会自动失效,两个目标会互相污染,产出常常是两者的混合物。怎么避:任务换了就换会话,宁可重新贴一遍必要材料。
**把”测试通过”当验收标准。**为什么会踩:这是最容易自动判定的信号。为什么坏:它同样是最容易被从测试侧满足的信号,改断言比改实现容易得多。怎么避:把测试文件划为不可修改区,验收看的是实现侧的 diff 加上手动跑一遍关键路径。
**让它自己评估自己有没有跑偏。**为什么会踩:直接问一句”你有没有偏离目标”成本极低。为什么坏:它的自我评估基于同一份被污染的上下文,污染源本身不会自己举报自己,你多半会得到一句自信的”没有偏离”。怎么避:用外部证据判断,diff、测试结果、运行输出,这些不受对话历史影响。
**看到方向不对但产出还行就先留着。**为什么会踩:舍不得,觉得可以稍后清理。为什么坏:越界的改动一旦进了工作区,后续每一步都建立在它上面,清理成本随时间线性增长,而且你会逐渐习惯它的存在。怎么避:当场决定去留,留就把它并进正式任务并补上验收,不留就立刻回滚。
**把长任务的所有约束写在最开头。**为什么会踩:符合”一次说清楚”的直觉。为什么坏:任务越长,开头的约束离当前这一步越远,权重被中间的大量细节稀释。怎么避:关键约束在每个阶段的起点重申一次,尤其是”不许动什么”这一类。
收束
目标漂移是长任务的常态,不是异常。你要建立的不是”让它永不漂移”的幻觉,而是一套能在漂移走远之前发现并且低成本回退的机制。发现靠证据不靠感觉,回退靠基线不靠劝说,防复发靠可验证的边界不靠更用力的措辞。
放一份自检清单,跑长任务之前过一遍:
- 这次任务允许改哪些文件,我能不能用一条命令列出来?
- 完成的标志是什么,这个标志能不能被从旁路满足?
- 我有没有一个随时可回的干净基线?
- 这个任务能不能切成两到三段,每段都有独立的验收点?
- 我打算贴进去的背景材料,删掉一半会不会影响结果?如果不会,就删掉。
- 中途发现方向不对,我的第一个动作是中断还是补充?答案必须是中断。