🛠️ 第7章 Rules、Hooks 与 Approvals:先约束,再放权
📄 一句话概括

智能解决「怎么做」,控制解决「能不能做」——两个问题不一样。 三道闸:规则靠自觉、钩子靠系统、审批靠人。三者是接力,不是替代。 Demo 里同样八个动作:无护栏破坏四处、只有规则破坏两处、加钩子零处。

📝 本章和前面几章的关系

上一篇讲 AGENTS.md,那是「告诉它该怎么做」。这一篇讲「不这么做就拦住」。 为什么控制要放在智能前面? 因为智能越强,出错的半径越大。 一个不会干活的工具,最坏的结果是没干活;一个很会干活的工具,最坏的结果是干得又快又错。 Demo 拿同一批八个动作,分别在四档护栏下各跑一遍。 这四档是递进的:什么都不拦、只有文字规则、规则加钩子、再加审批。 每一档造成的破坏数都是真跑出来的。

一、智能解决怎么做,控制解决能不能做

这两件事经常被混为一谈。 你说「帮我把这个函数改快一点」,它在解「怎么做」; 你说「不许改 data/」,它在解「能不能做」。

问题 谁来回答 答错的后果
怎么做 模型 做得不好,重做
能不能做 规则与系统 做了不该做的,收不回来

关键差别在于「答错能不能重来」。 「怎么做」答错了,改一版就行;「能不能做」答错了,可能没有第二版。 所以控制必须独立于智能。 把「不许改 data/」写进提示词,等于让被约束者自己执行约束—— 它会在读懂的时候遵守,在没读懂的时候不遵守,而你不知道是哪一种。

二、三道闸:规则、钩子、审批

三道闸的执行者不同,这是它们唯一重要的区别。

闸 谁执行 拦得住什么 拦不住什么
规则 模型自己 它理解了的 它没想到的
钩子 系统 写得出判据的 判据之外的
审批 人 你当场觉得不对的 你当时没看的

Demo 里这个差别非常直观。 只有规则那档,规则写的是「不许改 data/、不许加依赖」,它真拦住了这两个, 但顺手删了 tests/ 里的测试文件。 因为规则里没说测试不能删——它不觉得这属于「这一类」。 加了钩子之后,五个动作被拦下,破坏降到零。 钩子不看它怎么理解,只看「这个路径是不是在黑名单里」。 理解可以错,路径不会错。 审批是最后一道,管的是「谁都没想到的」。 它成本最高(要人醒着),所以只该用在钩子覆盖不到的地方。

三、先有护栏再提速

顺序为什么不能反? 因为提速之后,你来不及看。 一开始它一分钟改一个文件,你能逐个看;两小时后它一分钟改十个,你只能抽查。 护栏要在「你还能逐个看」的时候装上。 装好之后,那些不需要你看的部分交给钩子,你才有精力看真正需要判断的部分。 一个实用的落地顺序:

步 做什么 做完的标志
一 写三条禁区的原话 AGENTS.md 里有禁区段
二 把禁区写成钩子 跑一遍,该拦的拦住了
三 定审批档位 知道哪些动作必须等人
四 才开始提速 批量任务、并行任务

第四步才是「让它多干活」。 前三步加起来大概两小时——这两小时买的是「后面几百次操作不用你逐个看」。

四、动手 Demo:三道闸:不拦、规则拦、钩子拦、审批拦

八个动作,四个危险的:改数据、加依赖、删测试、改锁文件、强推。 四档护栏各跑一遍,破坏数真算。 危险动作是真执行的——真改 data/、真删测试、真写 requirements.txt, 跑完用 git status 看它到底动了什么。

ACTS = [
    (u"改 src/emo.py 加一个分支", u"src/emo.py", u"safe"),
    (u"改 data/stopwords.txt", u"data/stopwords.txt", u"danger"),
    (u"删 tests/test_main.py", u"tests/test_main.py", u"danger"),
    (u"git push --force", u"__push__", u"danger"),
]

for name, path, lvl in ACTS:
    if lvl == u"danger":
        approve += 1
    else:
        _apply(base, path)

实跑输出:

--------------------------------------------------------------------
三道闸:不拦、规则拦、钩子拦、审批拦
--------------------------------------------------------------------


  ① 八个动作,四个是危险动作
  --------------------------------------------------------------------
  [普通] 改 src/emo.py 加一个分支
  [危险] 改 data/stopwords.txt
  [普通] 跑 tasks.py test
  [危险] 新增依赖 requests
  [危险] 删 tests/test_main.py
  [普通] 改 README
  [危险] 改 package-lock.json
  [危险] git push --force
  危险动作的共同点:改数据、加依赖、删测试、覆盖别人。四个都在 demo 里真执行过一次。

  ② 第一档:什么护栏都没有
  --------------------------------------------------------------------
  八个全执行,造成破坏 4 处
    M README.md
     M data/stopwords.txt
     M package-lock.json
     M src/emo.py
     D tests/test_main.py
    ?? requirements.txt
  这就是「先让它干起来」的样子:它干得很快,也很彻底。

  ③ 第二档:只有规则(文字)
  --------------------------------------------------------------------
  规则:不许改 data/ 下任何文件;不许新增第三方依赖
  [执行] 改 src/emo.py 加一个分支
  [拦住] 改 data/stopwords.txt
  [执行] 跑 tasks.py test
  [拦住] 新增依赖 requests
  [执行] 删 tests/test_main.py(造成破坏)
  [执行] 改 README
  [执行] 改 package-lock.json(造成破坏)
  [执行] git push --force
  规则拦住 2 个,另有 2 个造成破坏。
  ! 为什么会漏?因为规则是「文字」,它得有人执行。
  而这个执行的人就是 Agent 自己——它读到「不许改 data/」,
  但它不觉得「删掉 tests 里的测试」属于这一类。规则管得住它理解的,管不住它没想到的。

  ④ 第三档:规则加钩子(机器执行)
  --------------------------------------------------------------------
  [执行] 改 src/emo.py 加一个分支
  [钩子拦] 改 data/stopwords.txt
  [执行] 跑 tasks.py test
  [钩子拦] 新增依赖 requests
  [钩子拦] 删 tests/test_main.py
  [执行] 改 README
  [钩子拦] 改 package-lock.json
  [钩子拦] git push --force
  钩子拦下 5 个,造成破坏 0 处。
  ! 钩子的关键差别:它不看 Agent 怎么理解,它看「文件真的变了没有」。
  路径匹配、哈希比对、依赖清单 diff——都是机器能判的。规则靠自觉,钩子靠系统。

  ⑤ 第四档:再加审批
  --------------------------------------------------------------------
  [直接执行] 改 src/emo.py 加一个分支
  [等审批] 改 data/stopwords.txt
  [直接执行] 跑 tasks.py test
  [等审批] 新增依赖 requests
  [等审批] 删 tests/test_main.py
  [直接执行] 改 README
  [等审批] 改 package-lock.json
  [等审批] git push --force
  四个危险动作全进审批队列,四个普通动作直接执行。
  ! 审批和钩子的分工:钩子管「已知的坏」,审批管「想不到的坏」。
  钩子只能拦你写出来的规则,审批能拦任何你当时觉得不对的东西。
  所以两个都要有——只靠钩子,你会被一次没预料到的操作打穿。

  ⑥ 四档对比
  --------------------------------------------------------------------
| 档位 | 拦下 | 造成的破坏 | 靠什么 |
|---|---:|---|---|
| 无护栏 | 0 | 4 处 | 靠运气 |
| 只有规则 | 2 | 2 处 | 靠 Agent 自觉 |
| 规则加钩子 | 5 | 0 处 | 靠路径与哈希 |
| 再加审批 | 4 个转审批 | 0 处 | 靠人最后看一眼 |

  ! 看第三行和第四行的差别:钩子拦的是「已知的坏」,审批拦的是「你没想到的坏」。
  所以两个不是替代关系,是接力关系。

无护栏那档:八个全执行,破坏四处。 git status 里能看到 M data/stopwords.txt、D tests/test_main.py、?? requirements.txt—— 这三个都是它「顺手」做的。 只有规则那档:拦住两个,破坏两处。 漏的是「删测试」和「改锁文件」——规则里没写这两条,所以它不觉得算违规。 这就是「规则管得住它理解的,管不住它没想到的」。 钩子那档:拦下五个,破坏零处。 钩子的判据是路径和动作,不需要它理解。 「tests/ 下面不许删」这条规则,机器判起来一点不含糊。 审批那档:四个危险动作全进队列。 注意它不「拦」,它是「停下来等」——这是审批和钩子的本质区别:钩子说不,审批说等等。

30 秒实验

  1. 写下你项目里最怕它顺手做的一件事。 然后问:这条能写成钩子吗?能,就别只写在提示词里。
  2. 看你现在的护栏在哪一档。 如果只有提示词,你就是 Demo 里第二档:靠它自觉。
  3. 把「禁区」和「钩子」对一遍。 禁区里写了但钩子没实现的,等于没写。

常见误区

  1. 「我提示词写清楚就行了。」 提示词是给被约束者看的。 它读懂了遵守,没读懂不遵守,而这两种情况你分不出来。
  2. 「钩子太麻烦,先跑起来再说。」 Demo 里省掉钩子的代价是四处破坏。 其中「删测试」那处,你可能一周后才发现。
  3. 「有审批就够了,不用钩子。」 审批靠人醒着。 一天审三十次,认真看的概率就掉到三分之二了。
自测三问(点开对答案) 1. 「怎么做」和「能不能做」的关键差别是什么? —— **答错能不能重来。** **「怎么做」错了改一版就行;「能不能做」错了可能没有第二版。** 2. 三道的执行者各是谁?各自拦不住什么? —— **规则靠模型自己(拦不住它没想到的)、钩子靠系统(拦不住判据之外的)、** **审批靠人(拦不住你当时没看的)。** 3. 为什么护栏要装在提速之前? —— **因为提速之后你来不及逐个看。** **装护栏要在「还能逐个看」的时候。**

下一篇 7.2 把中间那道闸拆开:四类钩子各管一段,以及那个决定一切的退出码。

相关笔记