AI 改动了你不想动的关键逻辑:对症定位 + 手术级修复

让 AI 改一个参数名,它顺手把重试循环、timeout、forEach 也悄悄改了。本文讲怎么把 AI 编辑约束成可审计的手术级 diff。

你让模型把某个函数的一个参数改名。它改了。顺便”清理”了下面两个函数的重试循环,把 forEach 换成 map,还悄悄把默认 timeout 从 30s 调成 10s。你 prompt 里一句都没提。模型的总结里也只有一句”已按要求改名”。更糟:测试全过,因为那条重试路径只在生产里被触发。你会在凌晨 3 点的故障里发现这个改动,而不是在 code review 里。

最快修复: 别再让它返回”新文件”,改成强制约束式编辑。给 prompt 加三行——点名只改哪一个函数、加上 Do not modify any other function or any imports、要求返回 unified diff。然后自己跑 git diff HEAD,逐个 hunk 审。如果工具有精确匹配的 Edit/apply 动作(Cursor、Claude Code、Codex),优先用它而不是聊天式重写:模型对 old_string 匹配不上的区域物理上改不了,没标到的代码就动不了。

agent 爱过度编辑的根本原因:大多数聊天式指令被它解读为”顺手把代码改得更好”——这是它从训练集里那些真这么干的”高级工程师”身上学来的习惯。scope 是你必须主动施加的;它的默认行为是”能改的都改”。

先判断你属于哪一类

把你的现象对到原因,再跳到对应步骤。

现象最可能的原因看哪步
只让改一个名字,收回整个文件整文件贴入、没标编辑区,且没要 diffStep 1、Step 3
改动出现在你没点名的文件里agent 有大范围写入权限Step 4、收缩 scope 的 FAQ
逻辑被改了,总结却说”只改名”你信了散文总结Step 5
常量 / timeout / 默认值被翻了开放动词(“重构”、“清理”)Step 1、Step 2
没碰的行也出现格式变动缺”不要重排版”约束Step 2

常见原因

1. 动词开放

重构改进清理现代化——这些都允许模型动任何看起来不够好的代码,而按训练集标准,“不够好”几乎是所有遗留代码。

如何判断:你的动词没有具体宾语。“重构这个模块”=能改到的都改。

2. 整文件粘进去,没标编辑区

模型收到整文件 +“改一处”的指令,重生成整个文件是阻力最小路径。重生成就会漂。

如何判断:你贴了 400 行让它改一个名字,收回 400 行。

3. Agent 有大范围写入权限

Cursor Agent、Claude Code 自动 accept、Codex 宽 scope——模型能动 workspace 里任何文件。没显式收缩 scope 时,“scope”就等于”它能读到的全部”。

如何判断:改动出现在你从未提过的文件里。

4. 没要求 diff 格式

输出格式是”新文件”时模型得产出每一行,每一行都有机会漂。输出格式是”unified diff”时它被迫只产改动的 hunk。

如何判断:你拿回的是整个新文件,不是 diff。

5. 总结说”改名”,diff 说做了更多

模型有时会在总结里少报改动。总结是自述,审计不能信,要审 diff 本身。

如何判断:总结说改了 1 处,diff 里有好几处。

重新 prompt 前先做

  • 先 git commit 当前状态,之后才能干净地 git diff
  • 写下哪几行/哪些函数该改、哪些必须不动。
  • 保存过度编辑的版本,方便和手术级版本对照。
  • agent 运行前先收缩 scope(见 Step 4)。
  • 决定输出模式:unified diff、精确匹配的 Edit 调用,还是带约束的完整文件。

最短修复路径

Step 1:开放动词换成手术级操作

差:  Refactor this module for readability.

好:  In function handlePayment, rename parameter amt to amountCents.
      Update references inside handlePayment only.
      Do not modify any other function. Do not touch imports.
      Do not reformat unchanged lines.

动词带上点名的宾语和点名的范围,就抽掉了”能改的都改”这张许可证。

Step 2:声明”禁动”清单

Constraints:
- Do not modify any function other than handlePayment.
- Do not touch imports, exports, or comments.
- Do not change constants MAX_RETRIES or TIMEOUT_MS.
- Do not reformat unchanged lines.
- If you would normally clean something up, list it under
  "Suggested follow-ups" instead of doing it.

最后那条 follow-ups 很关键:它给模型一个安放”改进想法”的地方,于是它不再把这些改进偷偷塞进 diff。

Step 3:要 unified diff,不要整文件

要求 patch 形式的输出:

Return only a unified diff:

--- path/to/file.ts
+++ path/to/file.ts
@@ ... @@
- old line
+ new line

Then a section "Suggested follow-ups:" listing anything you noticed
but did not change.

diff 强制最小改动,因为重生成没动过的代码是浪费。附带好处:你可以直接把 diff 喂给 git apply 在本地审。

Step 4:在工具层锁死 scope(最强保证)

prompt 约束是”建议”,工具权限才是”强制”。用你工具提供的那一套:

  • Cursor: 加一个 .cursorignore 文件,挡住 agent 绝不该碰的路径。在 Settings > Cursor Settings > Agents > Run Mode 设置运行行为;较新版本新增了 “Auto-review” 模式,会把 shell、MCP、fetch 调用先送进一个分类器再放行。注意 Cursor 自己说这个分类器只是省事的辅助、不是安全边界,别拿它当硬防线。Agent Sandbox(截至 2026 年初已在所有平台正式可用)把写入限制在 workspace 内。把反复用到的 scope 规则放进 .cursor/rules/
  • Claude Code: 把 scope 限制写进 CLAUDE.md(例如 Do not modify files in src/lib/ or src/utils/ unless explicitly asked)。要硬保证就在 settings.json 里写权限规则:deny 优先于 askallow,所以对某目录设 deny,即使在宽松模式下也挡得住。像 Bash(git checkout:*) 这样的 ask 规则会对高风险命令强制弹确认。Claude Code 还把 .git/.claude/ 这类路径视为受保护路径,过去即便加了 --dangerously-skip-permissions 也仍会弹确认;不过具体行为在不同版本间有变动,请以你装的那一版实际表现为准。
  • Cursor / Claude Code / Codex: 优先用精确匹配的 Edit/apply_patch 动作,而非聊天式重写。它要求 old_string(或锚点)与源码逐字符匹配,所以模型改不了没标到的区域——匹配直接失败。这是对抗漂移最强的机械防线。

Step 5:审 diff,不要审总结

绝对不要把模型的散文总结当真。

git diff HEAD

每个 hunk 都翻一遍。只想看 agent 这次动了哪些文件:

git diff --name-only HEAD

看到意外的 hunk 就单独回退那一块。git 2.23+ 用新命令,旧命令也仍然有效:

git restore -p path/to/file.ts   # 新,推荐
git checkout -p path/to/file.ts  # 旧,仍可用

两者都会打开交互式选择器,让你保留改名、丢掉那段不想要的重试循环改动。

Step 6:多区改动拆成多个 prompt

真要改 3 个函数,就发 3 个 prompt。合并 prompt 会模糊 scope,模型会跨区”平均”约束,而不是逐区遵守。

怎么确认已经修好

  • git diff 只在你点名的文件和函数里有改动。
  • import、export、常量都没移位。
  • 现有测试不改测试文件就能过。
  • 如果模型发现了别的问题,“Suggested follow-ups” 列表非空——说明它尊重了边界,而不是闷头乱动。
  • 用同一 prompt 第二次跑,diff 形态稳定(行为可复现、受约束)。

常见问题

agent 改了我没提过的文件,怎么专门堵住?

在工具层堵,别靠 prompt。Cursor 里把该路径加进 .cursorignore,并靠 Agent Sandbox 把写入限制在 workspace。Claude Code 里在 settings.json 给该目录加 deny 规则——deny 压过 askallow,自动 accept 下也照样生效。光在 prompt 里点名路径并不可靠。

我 prompt 写了”refactor”,这算模型的错吗?

不算。你写了 “refactor”、“polish”、“modernize”,模型只是执行了一个宽指令。修法是把动词换成带具名 scope 的 editrenamereplace,而不是骂模型。

手术级编辑该用小模型吗?

经常该。中档模型(比如 Sonnet 4.6、GPT-5.5 Instant、Gemini 3.1 Pro)守紧约束往往不输甚至胜过顶级推理模型——后者更倾向于”帮忙”顺手改周边代码。先在你自己的仓库里试再决定。

怎么只留下那个不想要的改动、撤掉其余,或者反过来?

用 hunk 级回退:git restore -p <file>(旧 git 用 git checkout -p <file>)会打开交互式提示,让你逐个 hunk 选。于是你能保留改名、丢掉那个悄悄改的 timeout,而不必重做整次编辑。

逻辑都被改了,测试为什么还过?

覆盖率盲区。被过度编辑的那条路径(重试循环、错误分支、只在生产生效的 timeout)往往正是测试套件从不触发的部分。测试通过不等于 diff 安全;读 diff 才算数。

能让每次会话默认就安全吗?

能。每次 AI 跑之前先 commit,prompt 动词默认用 edit/rename/replace,超过约 20 行的改动一律要 diff 输出,高风险代码绝不让 agent 直接 commit——先审 diff。

预防建议

  • 默认动词:editrenamereplace——refactorimprovepolish 不加明确 scope 就永不用。
  • 超过约 20 行的代码改动一律要 diff 输出。
  • 用注释把编辑区圈起来——// AI-EDIT-START / // AI-EDIT-END——并告诉模型只改内部。
  • 手术级编辑用精确匹配的 Edit/apply_patch 模式。
  • 每次 AI 跑之前先 commit,让 git diffgit restore 保持干净。
  • 每次都审 diff,永远别信自述总结。

相关阅读

外部参考:Cursor Agent 安全文档Claude Code 设置与权限git restore 文档

标签: #排查 #Prompt #Prompt 质量 #误改重写