🛠️ 第6章 AGENTS.md:给 Agent 的项目说明书
📄 一句话概括

分层的机制很简单:按目录叠加,离文件近的追加在后面。 但覆盖顺序必须写死成「子只能加、不能减」——能减的话硬规则就不硬了。 Demo 里同一份规则,两种设计下 data/ 那条的命运完全相反。

📝 本章和前面几章的关系

上一篇讲三段写什么,这一篇讲写在哪儿。 一个仓库只有根目录一份 AGENTS.md 是不够的:后端的规矩跟前端不一样, 共享目录的规矩跟业务目录也不一样。 但分层会带来一个新问题:两份规则打架时听谁的。 Demo 用同一份数据跑了两种覆盖设计,结果差在一条硬规则上。

一、根目录放通用规则

根目录那份管整个仓库,所以每一条都要能承受「所有目录都遵守」。 这是个很强的约束。 「组件文件名用大驼峰」写在根目录,后端目录也得遵守——显然不对。 一个判断方法:把这条规则套到最不相关的那个目录上,还成立吗? 不成立,就往下挪一层。 根目录适合放三类: 全仓通用的入口(跑、验)、全仓通用的禁区(不许加依赖)、全仓通用的硬规则(提交前必须验)。

二、子目录放局部规则

子目录那份管自己这一片,写「本目录特有」的东西。

目录 该写什么
src/api/AGENTS.md 改接口要同步更新文档
src/web/AGENTS.md 组件命名与目录约定
tests/AGENTS.md 测试放哪儿、命名怎么起

子目录规则最容易写多。 写多了会变成「每个目录一份小文档」,没人维护,也没人读。 一个实用上限:子目录那份不超过五条。 还有一个隐蔽的好处:子目录规则是「按需加载」的。 Agent 只在碰到那个目录时才读它——所以你把局部细节放在子目录,等于给根目录减负。

三、谁覆盖谁

这是这一篇唯一需要记住的机制:按目录从外往里叠加,近的追加在后面。 Demo 里三个路径拿到三份不同的规则表: src/api/handler.py 七条、src/web/Chat.jsx 六条、tasks.py 五条。 叠加本身没问题,问题在「同名条目」怎么办。 两种设计:

设计 同名条目 后果
子可覆盖 子目录的顶掉父的 一句「本目录例外」就废掉硬规则
子只能加 父的保留,冲突的标出来 硬规则一直硬

Demo 里两种设计跑出来是这样: 可覆盖那版,data/ 那一条变成了「本目录允许直接改 data/」; 只能加那版,原规则还在,新条目后面标了「与硬规则冲突,已忽略」。 为什么必须选后者? 因为前者的问题不是「这次改错了」,是「你不知道它改错了」。 任何人在子目录里写一句例外,你的硬规则就静默失效了。 顺带一个推论:软规则可以被子目录覆盖,硬规则不行。 「注释用中文」这种偏好,某个目录想用英文,随它;但「data/ 不许改」这种,谁也不能推翻。

四、动手 Demo:分层与覆盖:子目录能推翻根目录吗

这个 Demo 建三层规则文件,然后写一个合并器,算出某个路径最终听哪些规则。 同一份数据跑两遍,只改一个开关:子目录能不能覆盖父的同名条目。 看 data/ 那一条在两遍里变成了什么。

def _merge(root, subs, path, strict):
    rules = list(root)
    d = os.path.dirname(path)
    while d:                            # 从文件所在目录一路往上
        for topic, text, lvl in subs.get(d, []):
            same = [i for i, x in enumerate(rules) if x[0] == topic]
            if not same:
                rules.append((topic, text, lvl))
            elif strict and rules[same[0]][2] == u"hard":
                rules.append((topic, text + u"(冲突,已忽略)", lvl))
            elif not strict:
                rules[same[0]] = (topic, text, lvl)   # 覆盖
        d = os.path.dirname(d)
    return rules

实跑输出:

--------------------------------------------------------------------
分层与覆盖:子目录能推翻根目录吗
--------------------------------------------------------------------


  ① 三层规则文件
  --------------------------------------------------------------------
| 文件 | 管哪儿 | 条数 |
|---|---|---|
| AGENTS.md | 整个仓库 | 5 |
| src/api/AGENTS.md | api 层 | 2 |
| src/web/AGENTS.md | web 层 | 2 |


  ② 某个文件到底听谁的
  --------------------------------------------------------------------
  src/api/handler.py     -> 最终 7 条,来自 src/api
  src/web/Chat.jsx       -> 最终 6 条,来自 src/web
  tasks.py               -> 最终 5 条,来自 (根)
  三个路径拿到三份不同的规则表:离得近的追加在后面。
  这就是「分层」的全部机制——没有魔法,就是按目录叠加。

  ③ 覆盖顺序:两种设计,差在一条硬规则
  --------------------------------------------------------------------
  [子目录可覆盖] data -> 本目录允许直接改 data/
  [子目录只能追加] data -> data/ 下不许改 / 本目录允许直接改 data/(与硬规则冲突,已忽略)
  左边那版,api 目录里那条「允许直接改 data/」把根目录的硬规则顶掉了。
  右边那版,它被追加进来但标了「与硬规则冲突」。
  ! 这就是为什么覆盖顺序要写死成「子只能加、不能减」。
  能减的话,任何人在子目录里写一句「本目录例外」,你的硬规则就废了——而且你不会知道。

  ④ 谁离文件近,谁说了算
  --------------------------------------------------------------------
| 问题 | 答案 | 为什么 |
|---|---|---|
| 子目录能加规则吗 | 能 | 局部规矩就得局部写 |
| 子目录能删父规则吗 | 不能 | 删了没人知道 |
| 同名条目冲突怎么办 | 硬规则赢 | 否则硬规则不硬 |
| 规则写在哪一层 | 离影响范围最近那层 | 太靠上会误伤别的目录 |

  ! 一个实用判断:这条规则只对某个目录成立,就写进那个目录;对整个仓库都成立,才写根目录。
  写在根目录的每一条,都要能承受「所有目录都遵守」。

同一份输入,两行输出完全相反: 可覆盖那版 data/ 变成「本目录允许直接改」,只能加那版还是「不许改」。 差别只在一个 elif。 ② 那一段值得多看一眼:三个路径、三份规则表。 tasks.py 在根目录,拿到五条;src/api/handler.py 拿到七条(根五条加局部两条)。 这就是「按需加载」的具体样子。 注意「来自 src/api」这个标注,它是排查问题的入口:规则不对时,你先要知道它读的是哪一份。 最后那张四行表,其实只有第二行是要背的: 子目录能删父规则吗——不能,删了没人知道。

30 秒实验

  1. 看一眼你仓库里有没有子目录级规则。 有的话,检查它有没有写「本目录例外」这种话。
  2. 把根目录的规则逐条套到最不相关的目录上。 套不上的,往下挪一层。
  3. 给子目录那份定个上限,五条。 超了说明该再分一层,或者该合并。

常见误区

  1. 「子目录应该能覆盖父规则,不然不灵活。」 灵活的是软偏好,硬规则不该灵活。 而且覆盖是静默的——你不知道它发生了。
  2. 「所有规则都写根目录最省事。」 省的是你的事,费的是它的注意力。 根目录四十条,它每条都要读,读多了会漏。
  3. 「分层太复杂,一份就够。」 一个目录一个团队的时候确实够。 两个团队改一个仓库,一份就不够了。
自测三问(点开对答案) 1. 分层的叠加顺序是什么? —— **按目录从外往里,离文件近的追加在后面。** **它不是「覆盖」,是「叠加」——除非你专门设计了覆盖。** 2. 为什么覆盖顺序要写死成「子只能加、不能减」? —— **因为能减的话,任何人在子目录写一句「本目录例外」,硬规则就静默失效。** **而你不会知道。** 3. 软规则和硬规则在覆盖上有什么区别? —— **软规则可以被子目录覆盖**(「注释用中文」在某目录想用英文,随它); **硬规则不行**(「`data/` 不许改」谁也不能推翻)。

下一篇 6.3 讲另一种分层:团队规矩和个人偏好,一个进仓库,一个只在自己机器上。

相关笔记