如果你的编程代理无视你的规则,请观看此视频(你需要钩子)
如果你正在用 AI 编程代理写代码,大概率已经遇到过这种时刻:你在项目文档里写清楚了“所有提交必须通过 lint”,代理每次都会说“好的”,然后在下一次提交时给你留下一堆console.log。你给代理配了一份非常详细的规则文件,告诉它不要修改某个核心模块,它答应用了不到两分钟,就开始在那个模块里加业务代码。
问题不在于代理“笨”,也不在于你“没说清楚”。问题在于,提示词本质上是一种软约束:它通过概率和上下文去影响模型的行为,而不是一道绝对不可逾越的程序边界。只要上下文变长、注意力漂移、任务被拆得太碎,规则就容易被“忘掉”。想让规则真正生效,需要的不是更长更严格的提示词,而是在关键节点上插入带有强制语义的钩子。
本文会从原理到落地,讲清楚为什么编程代理会无视规则、钩子为什么能解决这个问题,以及如何在本地 Git 提交、CI 管道和 Agent 工作流三层场景中配置钩子。读完你会得到一套“提示词表达意图,钩子守住底线”的工程方法。
1. 这篇文章真正要解决的问题
先说一个很容易被拒绝的事实:把规则交给模型自觉执行,本质上是一种概率赌博。同一个规则,在上下文较短、任务简单的场景下,可能被遵守 90% 次;一旦上下文拉长、任务复杂、涉及多个文件修改,遵守率会明显下降。你可能觉得把规则写得再狠一点、加更多感叹号就够了,但模型不会像编译器一样严格执行提示词,它只是在“生成最可能的文本”。
于是开发流程中出现了一个奇怪的断层:
- 规则写在文档里,是给人和模型看的“建议”;
- 代码提交进仓库后,lint、类型检查、测试这些工具才形成真正的“拦截”;
- 但代理完成工作、生成代码的阶段,往往没有任何机制在中间拦一下。
这导致代理会生成一堆看似合理、实际未通过校验的代码,然后信心满满地告诉你“已完成”。团队如果完全信任这个结果,问题就会一路流到 CI 甚至生产环境。
钩子解决的就是这个断层:它在代理运行的关键节点上触发一个外部脚本,用命令行程序的退出码来决定流程是否可以继续。钩子不看模型心情,不依赖上下文长度,不关心提示词里有没有“必须”两个字。它是死的,也是可靠的。
这篇文章适合几类读者:
- 正在用 AI 编程代理写业务代码,但发现代理经常违反项目约定的开发者;
- 团队想推行“AI 辅助开发”,但担心代码质量和安全风险的技术负责人;
- 已经在用 Git Hooks、CI 管道,但想知道这些机制如何与 AI 代理协同的人。
读完你会理解:为什么提示词是必要但不充分的,以及怎样用钩子把“建议”变成“强制”。
2. 为什么提示词会被“无视”:三个技术原因
2.1 提示词是概率建议,不是程序逻辑
模型生成文本的过程,是在给定上下文时逐 token 预测最可能的序列。它对“规则”的处理和对你其他话的处理方式没有本质区别:规则只是上下文中的若干 token,而上下文中的信息并不会被百分百吸收。
这意味着,规则写得再详细,对模型而言也只是一个“应该重视”的信号,而不是一个“必须满足”的约束。尤其当模型需要在长上下文里做大量推理时,早期出现的规则信息会被后续内容稀释。这不是设计缺陷,而是当前生成式模型的基本工作方式。
2.2 注意力漂移和上下文稀释
当你和代理聊了 20 轮,修改过十几个文件之后,早期规则在上下文中的权重会下降。代理很容易“盯着眼前的问题”,忽略全局约定。
一个非常典型的场景是:你在一开始告诉代理“不要添加任何调试输出”,但是进行到某个复杂 bug 的排查时,代理会自然地写下一个console.log,因为它觉得这是在“帮你排查问题”。从模型的角度看,这个行为完全合理;从你的规则角度看,它越界了。
2.3 规则模糊导致无法验证
很多规则本身是含糊的:“代码要优雅”“不要破坏现有功能”“保持一致性”。这类规则模型无法精确执行,人也没法精确验收。
反过来,如果规则是机器可验证的——“代码必须通过 ESLint”“所有新函数必须有单元测试”“密钥文件不能出现在 diff 中”——它就应该从提示词中剥离出来,变成一个钩子。能用程序判断的规则,永远比用自然语言描述更可靠。
小结:提示词适合表达意图、偏好、架构方向,但凡是能够被脚本校验的规则,都应该下沉到钩子里强制执行。
3. 钩子的核心概念:让规则拥有强制语义
钩子(Hook)这个概念并不新鲜。它本质上是“在某个事件发生时,自动调用你预先注册的代码”。Web 开发中的 Webhook、Git 版本控制中的 Git Hooks、编辑器插件里的生命周期回调,都是同一类机制。
放到 AI 编程代理的场景里,钩子的意义就变得非常直接:
- 事件是代理解码过程中的关键节点,比如“准备生成代码前”“写完某个文件后”“准备执行命令前”“准备提交结果前”;
- 你注册的代码是一个脚本、一条命令或一个程序;
- 这个脚本返回的结果(退出码、标准输出)会被流程使用:退出码非 0,流程中断;退出码为 0,流程继续。
与传统 Git Hooks 相比,AI 代理场景的钩子有一个关键差异:钩子的输出可以重新喂回给模型。也就是说,钩子不只是“拦住”,还可以“告诉代理错在哪、应该怎么改”。这构成了一个反馈闭环:生成 → 校验 → 失败信息回传 → 代理自我修正 → 再次校验。
这正是文章标题里“你需要钩子”的核心含义:你不是要更长的规则文件,你要的是一个能在代理身边不断喊停的质检员。
4. 第一层钩子:用 Git Hooks 守住提交边界
先从不依赖任何特定 AI 工具的最基础方式讲起:Git Hooks。无论你用的是哪个编程代理,代理最终修改的代码都会落到工作区,最终都要通过git commit才能进入仓库历史。只要在提交这个节点上设卡,代理生成的垃圾代码就无法轻易混入主线。
4.1 检查调试语句的最小脚本
先看一个最经典的例子:阻止带调试语句的代码提交。你可以在项目的.git/hooks/pre-commit里直接写脚本,但.git/hooks目录默认不会被提交到仓库,团队协作不方便。更推荐的方式是使用husky这类工具,将钩子脚本保存在项目中。
先看不依赖任何工具的裸脚本,理解原理:
#!/bin/sh # 文件路径:.git/hooks/pre-commit # 在提交前检查暂存区里是否出现了调试代码 FILES=$(git diff --cached --name-only --diff-filter=ACM -- '*.js' '*.ts' '*.py' '*.go') if [ -z "$FILES" ]; then exit 0 fi # 在暂存文件内容中搜索调试语句 if echo "$FILES" | xargs grep -nE "console\.log\(|print\(|debugger|TODO|FIXME" 2>/dev/null; then echo "错误:检测到疑似调试语句或待办标记,提交被拦截。" echo "请移除上述内容后再提交。" exit 1 fi echo "检查通过:暂存区未发现调试语句。" exit 0这段脚本的逻辑很简单:
- 从 git 暂存区取出本次真正要提交的文件;
- 只检查常见源码文件类型;
- 在这些文件内容里搜索调试语句模式;
- 找到就输出错误并以退出码 1 结束,阻止提交。
把这段脚本放到.git/hooks/pre-commit后需要赋予执行权限:
chmod +x .git/hooks/pre-commit注意:直接修改.git/hooks只对你本地生效。如果团队协作中每个人都要用同一套钩子,建议使用husky或pre-commit这类工具把钩子配置提交到仓库。
4.2 使用 husky 管理团队级钩子
husky是前端项目里非常常用的 Git 钩子管理工具。它以package.json为核心,配合lint-staged实现对暂存文件的快速检查。
仍然以 Node.js 项目为例,安装依赖:
npm install --save-dev husky lint-staged在package.json中准备prepare脚本,便于安装依赖时自动初始化钩子:
{ "name": "ai-agent-hooks-demo", "private": true, "version": "1.0.0", "scripts": { "prepare": "husky", "lint": "eslint .", "typecheck": "tsc --noEmit", "test": "vitest run" }, "devDependencies": { "husky": "^9.0.0", "lint-staged": "^15.0.0" } }初始化 husky:
npx husky init这会生成.husky/目录,然后在.husky/pre-commit中写入 lint-staged 配置:
#!/usr/bin/env sh # 文件路径:.husky/pre-commit npx lint-staged在package.json中增加 lint-staged 的独立配置块,让它在每次暂存代码时自动做 lint 和格式化:
{ "lint-staged": { "*.{js,ts,vue}": [ "eslint --fix", "prettier --write" ] } }这套配置的作用是:当代理生成代码并执行git add后,一旦运行git commit,lint-staged会自动对暂存文件执行 ESLint 检查和 Prettier 格式化。如果 lint 失败,提交会被中断,并且代理会看到失败信息。
有人会问:如果代理并不通过命令行提交,而是通过 IDE 的图形界面提交,husky 还有效吗?仍然有效,因为 IDE 的提交操作底层调用的仍然是git commit,Git Hooks 的事件机制是统一的。
4.3 pre-push 钩子:提交前拦截弱,推送前拦截强
提交时的检查有一个弱点:代理完全可以先跳过检查完成提交,等到推送时才暴露出问题。所以建议在 pre-push 阶段再设一道更完整的检查,比如类型检查和测试。
在.husky/pre-push中写入:
#!/usr/bin/env sh # 文件路径:.husky/pre-push echo "开始推送前检查:类型检查 + 单测" npm run typecheck || exit 1 npm run test || exit 1 echo "推送前检查通过"这里的思路是分层设防:
pre-commit:快速检查,只针对暂存文件,秒级完成;pre-push:完整检查,跑全量类型检查和测试,耗时稍长但更严格。
这个组合对 AI 代理尤其有用。代理倾向于“生成后立刻提交”,如果你只有 CI 没有本地钩子,代理会不断把有问题的代码推送到远端,然后等待 CI 反馈。把更严格检查放进 pre-push,可以让代理在推送前就收到失败信息,节省大量循环时间。
5. 第二层钩子:CI 管道兜底,规则不再依赖本地
本地钩子只能约束本地行为,不能约束所有协作者,更不能约束代理在合并时的操作。CI 管道是第二道防线,也是一道对所有人、所有工具都有效的强制边界。
CI 的本质也是一个“钩子”:当push或pull request事件发生时,触发一个自动化流水线运行。你可以在流水线中挂上 lint、类型检查、测试、覆盖率、安全扫描、密钥泄露检测等步骤。任何一步失败,合并操作就会被阻止。
以 GitHub Actions 为例,在仓库.github/workflows/ci.yml中写入:
name: CI on: push: branches: [ "main", "develop" ] pull_request: branches: [ "main" ] jobs: validate: runs-on: ubuntu-latest steps: - name: 检出代码 uses: actions/checkout@v4 - name: 配置 Node.js uses: actions/setup-node@v4 with: node-version: 20 - name: 安装依赖 run: npm ci - name: ESLint 检查 run: npm run lint - name: 类型检查 run: npm run typecheck - name: 单元测试 run: npm test - name: 密钥泄露扫描 run: npx secretlint "**/*"这份配置每次推送和每个 PR 都会执行。AI 代理无法绕过它,因为它不读取提示词,不理会模型是否“觉得正确”,它只关心命令的退出码。
CI 还有一个本地钩子无法替代的价值:它为代理提供了一个稳定、客观的反馈源。把这个观点延伸一下,最理想的开发循环不是“代理生成 → 人检查 →发现问题 → 人描述 → 代理修改”,而是“代理生成 → 钩子校验 → 校验失败信息自动回传 → 代理根据失败信息自我修正”。
因此,CI 里的检查项要尽量输出可读的错误信息。比如 ESLint 的输出包含了文件和行号,这比“检查失败”四个字有用得多,模型看到行号和规则描述后,往往可以直接定位并修复。
6. 第三层钩子:Agent 原生钩子,把校验插入生成流程
Git Hooks 和 CI 覆盖了“代码进入版本库前后”的环节,但它们有一个盲区:代理在工作区里生成代码、修改文件、执行命令的过程中,没有任何东西在中间拦住它。如果代理写了一个语法错误的文件,你只能等到提交时才发现。更糟的是,如果代理要执行的命令本身有风险,比如删除文件、修改数据库连接配置,通常没有任何机制能够提前干预。
这正是 Agent 原生钩子要解决的问题。所谓原生化,指的是编程代理在自身的执行循环里预留了事件接口,允许你在特定节点挂接外部命令。
6.1 Agent 钩子能拦截什么
不同的代理产品名称不同,有的叫 Hooks,有的叫 Guardrails,但标准的拦截点通常包括以下几类:
before_command:代理执行任何 Shell 命令前;after_command:命令执行完成后;after_file_write:代理写完文件后;before_submit:代理向你提交最终答案前;conversation_feed:新对话输入到达时。
在这些节点上,你配置的命令会被调用。命令的标准输出会被代理看到,退出码决定是否允许流程继续。这个机制把“根据规则做判断”从模型内部移到了外部程序里。
6.2 一个通用的事件配置结构
下面提供一个通用结构示例,用来说明这种配置形态。请注意,这不是某一个产品的真实配置格式,具体字段名请以你使用的代理工具文档为准。
{ "hooks": { "beforeCommand": [ "bash scripts/guard-command.sh" ], "afterFileWrite": [ "bash scripts/check-style.sh" ], "beforeSubmit": [ "npm run lint", "npm run typecheck" ] } }配置的意图非常明确:
- 在代理执行命令之前,先检查这条命令是否属于白名单;
- 在代理写完每个文件之后,立刻做轻量校验;
- 在代理汇报“完成”之前,强制执行全套静态检查。
6.3 命令安全钩子示例
先看 beforeCommand 这个事件的价值。AI 代理为了完成任务,会主动执行各种命令。大多数时候没问题,但偶尔它会做一些你不希望发生的事情,比如:
rm -rf node_modules或者修改 Git 远程地址。用脚本来拦截这些风险命令:
#!/usr/bin/env bash # 文件路径:scripts/guard-command.sh # 在代理执行 Shell 命令前调用,参数是需要执行的命令字符串 COMMAND="$*" # 定义不允许执行的命令模式 BLOCK_PATTERNS=( "rm -rf /" "git push --force" "git remote set-url" "mysql -u root" "psql -U postgres" ) for pattern in "${BLOCK_PATTERNS[@]}"; do if echo "$COMMAND" | grep -q "$pattern"; then echo "已阻止危险命令:$pattern" exit 1 fi done echo "命令安全检查通过" exit 0这个钩子解决的是一个很实际的失控场景:代理在并行处理多文件时,可能误判“某目录不再需要”,直接执行删除;或者因为权限问题而尝试强制推送。命令级钩子能在发生之前喊停。
6.4 文件写后校验钩子示例
另一个更常见的场景是代理写完一个文件后,还没有提交,就已经引入了语法错误或调试代码。不要等提交,while 文件落盘时就立刻检查:
#!/usr/bin/env bash # 文件路径:scripts/check-style.sh # 在代理写完某个文件后调用,第一个参数为文件路径 FILE="$1" if [ -z "$FILE" ] || [ ! -f "$FILE" ]; then exit 0 fi case "$FILE" in *.js|*.ts|*.py|*.go) ;; *) exit 0 ;; esac if grep -nE "console\.log\(|print\(|debugger|password\s*=|api[_-]?key\s*=" "$FILE" 2>/dev/null; then echo "文件 $FILE 中检测到调试语句或疑似敏感信息,请修复后再继续。" exit 1 fi echo "文件校验通过:$FILE" exit 0这个钩子的执行频率会很高。每次代理写文件都会触发,因此脚本必须足够快,通常只做文本搜索,不做重量级编译。
6.5 将钩子反馈喂回给代理
Agent 原生钩子最有趣的能力是:失败信息不只是中断流程,还可以成为代理下一步行动的输入。一个理想的调优流程是:
- 代理生成代码;
- after_file_write 钩子发现调试语句;
- 钩子输出“某文件第 12 行检测到 console.log,违反项目规则”;
- 代理把这段输出纳入上下文;
- 代理主动修改该文件,移除调试语句;
- 再次触发钩子,校验通过。
在这个闭环中,提示词里的规则仍然存在,但负责兜底的已经是钩子。提示词只是让代理少犯错,钩子保证即使犯错也能被拦住并修正。
7. 常见问题与排查思路
钩子机制并不复杂,但实际用起来会遇到一些很典型的问题。
| 问题现象 | 可能原因 | 排查方式 | 解决方案 |
|---|---|---|---|
| 钩子脚本没有执行 | 文件没有执行权限 | 检查脚本权限,执行ls -l或chmod +x | 给钩子脚本添加可执行权限 |
| 钩子执行报错但 Git 不拦截 | 脚本退出码始终为 0 | 手动运行脚本并检查退出码,使用echo $?查看 | 在失败分支显式exit 1 |
| Windows 环境下 shell 脚本无法运行 | 使用了 Linux 专属语法或路径 | 查看报错信息,检查换行符是否为 CRLF | 统一使用 LF 换行,或改用跨平台的 Node 脚本 |
| 代理不执行本地钩子,代码直接推到远端 | 代理可能绕过了 git commit 钩子或走了其他提交路径 | 检查 CI 是否配置了相同的校验 | 以 CI 作为最终兜底,不要把本地钩子当作唯一防线 |
| 钩子运行太慢,开发效率严重下降 | 钩子里放入了重量级任务,比如全量打包 | 统计钩子耗时,查看卡在哪个命令 | 本地钩子只放秒级快速检查,重量级校验留给 CI |
| 钩子误报,拦截了正常代码 | 正则匹配条件过宽 | 复制被误报的内容逐一验证规则 | 细化白名单或调整匹配范围 |
| 代理无法理解钩子返回的错误 | 错误输出不够结构化 | 检查钩子的 stdout 是否包含文件名和行号 | 格式化错误输出,例如“文件+行号+规则描述” |
实际排查时,最重要的一步是先手动执行一次钩子脚本,而不是怀疑 Git 或代理。绝大多数问题都是脚本本身出错、路径不对或权限不足。
8. 最佳实践与工程建议
8.1 规则分层:提示词管意图,钩子管底线
这是全文最重要的一个原则。不要把所有规则都试图写进提示词,也不要把所有规则都变成钩子。合理的方式是:
- 提示词:架构方向、代码风格偏好、业务语义、模块边界;
- 本地钩子:快速校验,如 lint、格式、调试语句、敏感信息;
- CI 钩子:完整校验,如类型检查、测试、覆盖率、安全扫描;
- Agent 原生钩子:高频即时拦截,如命令白名单、文件写后检查。
提示词给模型一个“正确方向”,钩子保证流程“不会越界”。两者组合,而不是互相替代。
8.2 钩子脚本必须幂等且可重复执行
钩子会被反复触发,尤其是 Agent 原生钩子可能一次会话中触发几十次。脚本必须保证:
- 不修改业务文件内容,除非它本身就是格式化工具;
- 不依赖上一次运行的遗留状态;
- 不做网络请求,避免超时和不稳定;
- 不在脚本里硬编码团队成员的本地路径。
简单说,钩子应该是一个“只读裁判”,而不是另一个“代码修改工具”。
8.3 速度是本地钩子的生命线
一旦钩子让开发流程变得明显迟钝,团队成员和代理都会想办法绕过它。本地钩子只放轻量检查:lint 暂存文件、检查模式匹配、做类型快照。重量级操作留给 CI。
一个可用标准是:本地钩子总耗时控制在 5 秒以内。超过这个时间,认真考虑把该检查项转移到 CI。
8.4 安全边界与最小权限
钩子脚本本身也是代码,也会有安全风险,尤其当脚本能够执行命令时。注意几点:
- 不要将密码、Token 硬编码在钩子脚本中;
- 钩子应该使用最小权限运行,不要用 root 或管理员权限;
- Agent 原生钩子调用的外部脚本需要经过 review,因为代理可能会尝试利用它;
- 从外部复制钩子脚本到团队项目里时,要求先阅读,再执行。
8.5 日志留痕,方便复现
给钩子加上简单的日志输出,比如“哪个事件、哪个文件、哪条规则、结果如何”。方便出现问题时快速复现和定位。也可以把钩子失败时的错误信息统一格式化成:
[文件路径] [行号] [检查项名称] [错误详情]这种结构化输出,对模型自动修复尤其有价值。
9. 总结与后续方向
回到标题提出的问题:如果你的编程代理无视你的规则,你不需要一个更长的提示词文件,你需要钩子。
钩子解决的是生成式模型最大的不确定性:上下文越长,规则被遗忘的概率越高。通过 Git Hooks 守住提交边界,通过 CI 守住合并边界,通过 Agent 原生钩子把校验插入到生成的每一个环节,规则就从“模型可遵守、也可不遵守的建议”变成了“必须满足才能继续执行的条件”。
下一步你可以做三件事:
- 选一个实际项目,先给本地加上 pre-commit 和 pre-push 钩子,用最基础的
grep脚本把调试语句和敏感信息拦下来; - 把团队现有的 lint、测试、类型检查全部配置进 CI,让代理提交代码后的第一个反馈来源不是同事,而是机器;
- 查阅你正在使用的编程代理的文档,看它支持哪些原生钩子事件,把命令安全检查和文件写后检查加进去。
钩子的价值不在于写得多复杂,而在于它让流程中的每个关键节点都有了一个不讲情面的“守门员”。当规则成为机器可以执行、可以验证、无法绕过的程序,AI 编程代理的无视就不再是问题了。