你让模型把某个函数的一个参数改名。它改了。顺便”清理”了下面两个函数的重试循环,把 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 是你必须主动施加的;它的默认行为是”能改的都改”。
先判断你属于哪一类
把你的现象对到原因,再跳到对应步骤。
| 现象 | 最可能的原因 | 看哪步 |
|---|---|---|
| 只让改一个名字,收回整个文件 | 整文件贴入、没标编辑区,且没要 diff | Step 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优先于ask和allow,所以对某目录设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 压过 ask 和 allow,自动 accept 下也照样生效。光在 prompt 里点名路径并不可靠。
我 prompt 写了”refactor”,这算模型的错吗?
不算。你写了 “refactor”、“polish”、“modernize”,模型只是执行了一个宽指令。修法是把动词换成带具名 scope 的 edit、rename、replace,而不是骂模型。
手术级编辑该用小模型吗?
经常该。中档模型(比如 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。
预防建议
- 默认动词:
edit、rename、replace——refactor、improve、polish不加明确 scope 就永不用。 - 超过约 20 行的代码改动一律要 diff 输出。
- 用注释把编辑区圈起来——
// AI-EDIT-START/// AI-EDIT-END——并告诉模型只改内部。 - 手术级编辑用精确匹配的
Edit/apply_patch模式。 - 每次 AI 跑之前先 commit,让
git diff和git restore保持干净。 - 每次都审 diff,永远别信自述总结。
相关阅读
外部参考:Cursor Agent 安全文档、Claude Code 设置与权限、git restore 文档。
标签: #排查 #Prompt #Prompt 质量 #误改重写