结论:在项目根目录和每一层子目录各放一份 README,只写「本层有什么、谁负责、什么不该放这儿」,再在提示工程里要求 AI 动手前先读它------这是投入产出比最高的一条 AI Coding 约定。 它不改代码、不引框架、不装插件,只花写文档的时间,换回的是 AI 少读十个无关文件、少改错一个模块。下面是规则、模板、一段可直接抄进 rules 的提示词,以及一个我自己跑过的 README 缺口检查器。
目录
- 结论先说:README 是给 AI 看的目录索引(Index, Not Doc)
- AI 为什么爱改错文件:上下文里缺一张「哪块归谁」的地图(Why AI Misses)
- 三条硬规则:一层一份、只写直系、先读后改(Three Rules)
- 一份 README 写什么:五个字段的固定模板(Template)
- 提示工程怎么写:把「先读」变成硬约束(Prompt Rules)
- 一次可复算的实测:14 个目录、9 份 README、64.3%(Measurement)
- 完整可运行代码:README 缺口检查器(Runnable Checker)
- README 与 AGENTS.md、CLAUDE.md 的分工(Boundaries)
- 症状、原因、解法对照表(Symptom → Cause → Fix)
- 落地顺序:先根、再三层、最后卡 CI(Rollout Order)
1. 结论先说:README 是给 AI 看的目录索引(Index, Not Doc)
传统项目里,README 是写给「第一次来看这个仓库的人」的:装什么依赖、怎么启动、项目干什么。这个定位导致一个后果------绝大多数仓库只有根目录那一份,而且十年不更新。
AI Coding 改变了这份文档的读者结构。现在每天打开你仓库的,除了人,还有一个只会 ls、grep、read 三种动作的 AI。它没有你脑子里的架构图,它能看到的只有文件路径和搜索结果。 你不给它索引,它就只能靠猜。
所以 README 的定位要改一句话:从「项目说明书」变成「目录索引」。索引的判断标准很朴素------读完这一份,你(或者 AI)应该能立刻回答三个问题:
- 这个目录是干什么的(一句话)
- 这里每个文件干什么(一行一个)
- 什么东西不该放这里、该放哪(边界)
三问答得出来,这份 README 就合格;答不出来,写三千字也是废纸。
这里有个生活化的对照:一个没有货架标签的超市,你只能一排排拿起来看生产日期;有标签的超市,你走到第 3 排直接伸手。AI 在你的仓库里逛,读 README 就是看货架标签,
ls加read是拿起来看生产日期。前者一次到位,后者十次有九次白读。
2. AI 为什么爱改错文件:上下文里缺一张「哪块归谁」的地图(Why AI Misses)
AI 改错文件,很少是因为「笨」,绝大多数是因为三个结构性原因。在如何审核 AI 生成的代码里我们讲过一套六层检查清单,那套管的是「AI 写完之后的验收」;这一篇管的是更靠前的一步------「AI 动手之前怎么定位」。定位错了,后面验收再细也只是给错的地方抛光。
第一,同名文件无法区分。 apps/api/src/utils.ts 和 packages/core/src/utils.ts 在搜索结果里长得一模一样,只有路径不同。AI 拿到三条命中,选哪条全看它读到第几条停手。这不是能力问题,是信息问题------你从来没告诉过它「utils 有两套,一套是纯函数,一套是日期格式化,别混用」。
第二,越层改动没有成本感。 人改代码时,看到 import { X } from '@fleet/db' 会下意识想「这是另一个包的边界」,手会停一下。AI 没有这个停顿,它看到函数不够用,第一个念头是「我给它加个参数」,第二个念头是「我直接在这个文件里内联一份实现」。这两种操作在 diff 里都只是几行,但一个破坏了封装,一个制造了重复实现。
第三,探索成本会把上下文吃光。 AI 接到任务后的默认动作序列是:ls 根目录 → 猜一个目录进去 → 再 ls → 打开一个不像的文件 → 关掉 → 再打开一个。每次试探都要吃 token。等到它终于找对文件,可用上下文已经被试探消耗掉一半,剩下的一半要用来写代码和自检------质量从这里开始掉。
这三个原因指向同一个补救动作: 在 AI 开始试探之前,先把「地图」塞给它。 地图不能是根目录一份大文档------AI 读完 500 行根 README,仍然不知道 apps/api/src/routes/chat.ts 里那个 chat 到底归谁管。地图必须是分层的:根目录给全局,每一层给本层。
3. 三条硬规则:一层一份、只写直系、先读后改(Three Rules)
规则只有三条,但每条都有人踩坑。
规则一:一层一份,不做大全。 根目录一份,每个子目录各一份,每份只服务自己那一层。理由不是「好看」,是维护成本:一份大全要随目录数量线性膨胀,任何一个人动任何一个目录都得改同一个文件,冲突天天有;分层之后每份控制在 30 行以内,谁建目录谁写,谁改目录谁改,作者就是唯一负责人。
规则二:只写直系,不写孙辈。 本目录这一份只写「直接躺在这个文件夹里的文件」和「直接子文件夹各自负责什么」,不展开子文件夹内部的细节------那些细节属于子文件夹自己那份 README。这条规则是整套结构能长期活下去的关键:一旦允许「顺手把下层的也写了」,就会出现两处描述同一件事,然后它们必然在三个月内互相矛盾,AI 读到哪份算哪份,比不写还糟。
规则三:先读后改,而且要求上溯。 AI 动手前先读目标文件所在目录的 README,再往上读一层父目录的 README 补背景,最多上溯两层。只看本层,容易不知道自己在整个系统里的位置;无限上溯,等于又读了一遍根目录大全。两层是够用的分界:本层告诉你「这个文件干什么」,父层告诉你「这个目录为什么存在」。
三条规则合起来的效果是: AI 的探索路径从「广度优先试探」变成「按索引直达」。 它不再是先 ls 再猜,而是先定位目标文件、读同级 README 确认职责、读父级 README 确认边界,然后动手。这三步的 token 成本是固定的,不随仓库规模膨胀------这是它和「让 AI 自己摸索」最本质的区别。
4. 一份 README 写什么:五个字段的固定模板(Template)
不要自由发挥。字段固定,AI 才能形成稳定的阅读预期------它知道第三段一定是边界说明,就不会在边界说明里找启动命令。
五个字段,按顺序:
- 一句话职责:这个目录为什么存在。写「不能干什么」比写「能干什么」更有信息量。
- 直系文件清单:文件名 → 干什么,一行一个,按重要性排。
- 边界:什么不该放这里、该放哪。这一节是防越层改动的核心。
- 入口与命令:这个目录如果有可执行入口,写清命令。
- 变更提示:改这里会牵连谁。给 AI 一个「改之前先想一下」的钩子。
一份真实可用的样例:
markdown
# apps/api/src/routes
## 职责
只做 HTTP 路由层:解析参数、调用 feature、返回响应。不写业务判断。
## 直系文件
- `health.ts` --- 健康检查,返回进程与依赖状态
- `chat.ts` --- 对话接口,透传给 features/chat
- `ma.ts` --- 内容库接口,透传给 features/ingest
## 边界
- 不写 SQL:数据库访问一律走 packages/db
- 不写重试/降级:这些在 features 层做
- 不做鉴权实现:中间件在 ../middleware
## 入口
无独立入口,由 ../index.ts 挂载。
## 变更提示
新增路由必须同时在 ../container.ts 注册依赖,否则启动即报错。
注意这份样例里没有一句废话:没有「本目录采用现代化的模块化设计」这类形容词,全是可执行的事实。形容词对 AI 是噪声。
5. 提示工程怎么写:把「先读」变成硬约束(Prompt Rules)
README 写完放在那里,AI 不会自动读。必须在提示工程(Prompt Engineering)里显式要求,而且要写成「条件 + 动作 + 例外」的形式,不能写成「建议先阅读相关文档」------这种软话 AI 一律跳过。
可直接抄进 rules 文件或系统提示的一段:
markdown
## 目录索引约定
1. 修改任何文件之前,必须先读该文件所在目录的 README.md。
2. 再向上读一层父目录的 README.md 补上下文;最多上溯两层,不要继续向上。
3. 目标目录缺少 README.md 时,先按模板补一份再改代码,不要跳过。
4. README 与代码冲突时,以代码为准,并在同一次改动里顺手修正 README。
5. 新增、删除、重命名文件后,必须同步更新所在目录的 README.md 直系清单。
6. 只信直系描述:子目录的细节读子目录自己的 README,不要从上层的概述里推断。
第 4 条最容易被忽略,但它决定了这套机制能不能长期活着。 如果不说「以代码为准」,AI 会拿着一份过期 README 当权威,照着三年前的描述去改今天的代码------那比不读 README 危险得多。文档是索引,不是法律。
第 5 条把「更新 README」变成改动的一部分,而不是一句口头约定。约定不进流程就一定会腐化,这跟代码格式化一个道理:没人会在提交前手动对齐空格,所以才有 formatter。
6. 一次可复算的实测:14 个目录、9 份 README、64.3%(Measurement)
关于这套做法的收益,网上常见「效率提升 40%」「bug 减少一半」这类数字。我一个都不打算引用,因为没人给出可复算的口径------什么算「一次改动」、什么算「改错」、基线是什么,全都没定义。 不可复算的数字,在技术文章里就是噪声。
能复算的只有一个量:目录覆盖率。口径写清楚------递归遍历项目下所有目录,跳过 node_modules、.git、dist、build 这类产物目录,对每个目录检查它是否直接含有一份 README 文件;覆盖率 = 含 README 的目录数 ÷ 目录总数。
我搭了一棵 14 个目录的演示树(apps/api/src/routes、packages/core/src 这种典型结构),先只给根目录和中间层 9 个目录写 README,实测输出是:
text
根目录: .
目录总数: 14 | 已有 README: 9 | 缺失: 5
覆盖率: 64.3%
缺失清单(按路径深度由深到浅,前 20 条):
- apps/api/src/middleware
- apps/api/src/routes
- apps/worker/src
- packages/core/src
- packages/db/src
这 5 个缺口全在叶子层,而且全在 src 下面------恰好是 AI 动手最频繁、最容易迷路的地方。这说明一件事: 人写 README 的直觉天然偏向高层,而 AI 出错的位置天然在底层。 你以为重要的 apps/ 概述,AI 读一次就够;它真正需要的是 src/routes 这种「文件名长得都差不多」的目录。
补齐这 5 份之后再跑一次,覆盖率到 100%,退出码从 1 变 0:
text
目录总数: 14 | 已有 README: 14 | 缺失: 0
覆盖率: 100.0%
这两个数字你可以用下面的脚本在自己的仓库上复现,口径和跳过名单都在代码里,不存在解释空间。
7. 完整可运行代码:README 缺口检查器(Runnable Checker)
这套约定的死穴是「靠自觉」------建了新目录忘了写,半年后覆盖率掉回 60%,AI 又开始在没索引的目录里瞎逛。所以必须有工具卡住。
下面这段 Node.js 脚本零依赖,复制保存为 scripts/readme-audit.mjs 即可运行,Node 18 以上都行(我在 Node 24 上实测过):
javascript
// 用法:node readme-audit.mjs <项目根目录> [--max-rows 20]
import { readdirSync } from "node:fs";
import { join, relative, resolve, sep } from "node:path";
// 产物目录与工具目录不参与统计,否则覆盖率的账会被噪声淹没
const SKIP = new Set([
"node_modules", ".git", ".hg", ".svn", "dist", "build", "out",
"coverage", ".next", ".nuxt", ".venv", "venv", "__pycache__", ".idea", ".vscode",
]);
// 认 README.md / readme.markdown / README.txt,大小写不敏感
const README_RE = /^readme(\.md|\.markdown|\.txt)?$/i;
const args = process.argv.slice(2);
const root = resolve(args.find((a) => !a.startsWith("--")) ?? ".");
const rowsFlag = args.indexOf("--max-rows");
const MAX_ROWS = rowsFlag >= 0 ? Number(args[rowsFlag + 1] ?? 20) : 20;
// 递归收集目录,并记录每个目录是否「直接」含有 README 文件
function walk(dir, acc) {
const names = readdirSync(dir, { withFileTypes: true });
const files = names.filter((d) => d.isFile()).map((d) => d.name);
acc.push({ dir, hasReadme: files.some((f) => README_RE.test(f)) });
for (const d of names) {
if (d.isDirectory() && !SKIP.has(d.name)) walk(join(dir, d.name), acc);
}
return acc;
}
// 统一成正斜杠相对路径,Windows 和 macOS 输出一致
const pretty = (p) => relative(root, p).split(sep).join("/") || ".";
const all = walk(root, []);
const missing = all.filter((d) => !d.hasReadme);
const covered = all.length - missing.length;
const pct = all.length === 0 ? 100 : (covered / all.length) * 100;
console.log(`根目录: ${pretty(root)}`);
console.log(`目录总数: ${all.length} | 已有 README: ${covered} | 缺失: ${missing.length}`);
console.log(`覆盖率: ${pct.toFixed(1)}%`);
if (missing.length) {
console.log(`\n缺失清单(按路径深度由深到浅,前 ${MAX_ROWS} 条):`);
missing
.sort((a, b) => pretty(b.dir).split("/").length - pretty(a.dir).split("/").length)
.slice(0, MAX_ROWS)
.forEach((d) => console.log(` - ${pretty(d.dir)}`));
}
// 有缺口时以非 0 退出,便于在 CI 或 pre-commit 里卡住
process.exit(missing.length ? 1 : 0);
运行方式与关键设计:
node scripts/readme-audit.mjs .扫当前仓库;--max-rows 50调整清单长度。- 退出码即结论:有缺口返回 1,接进 CI 的 verify 步骤就能直接卡住合并;接进 pre-commit 则连提交都过不去。
- 按深度倒序输出:先把最深的缺口列出来。因为叶子层才是 AI 的主战场,先补这里收益最大,而不是先补根目录那句「本项目采用 Monorepo 架构」。
- 跳过名单必须显式维护 :不跳
node_modules,覆盖率会被 vendor 代码稀释成个位数,指标就失去意义了。
8. README 与 AGENTS.md、CLAUDE.md 的分工(Boundaries)
写到这里必须回答一个合理的质疑:既然已经有 AGENTS.md 这套分层标准,为什么还要在 README 上做文章。
因为它们回答的不是同一个问题。按 AGENTS.md 社区约定(规范细节见 AGENTS.md 分层标准 与 AGENTS.md Specification,都不是学术论文,无需 DOI):
| 文件 | 回答什么 | 读者 | 生效范围 |
|---|---|---|---|
README.md |
这个目录有什么 | 人 + AI | 本层,靠提示词要求先读 |
AGENTS.md |
在这个目录里必须怎么做 | AI | 本层,离被改文件最近的那份优先 |
CLAUDE.md / 工具专属文件 |
某个工具的特殊指令 | 特定 AI | 由该工具解析 |
关键差别是 生效机制:AGENTS.md 有明确的「就近生效」规则------AI 从被改文件向上查找,最近的一份优先,子目录可以覆盖父目录;README 没有这种机制,它生效完全靠你在提示工程里写的那句「先读」。所以这两者不是替代关系:
- 你要写约束 (不许直接连数据库、提交前必须跑哪个测试、命名规范),写
AGENTS.md,它更硬。 - 你要写索引 (这个目录有什么、谁消费它、改它会牵动谁),写
README.md,它更全。
一个反直觉的取舍: 这两类内容不要混写在一个文件里。 理由见 §4 的模板设计------README 一旦混进十条「必须遵守」,它的第三段就不再是稳定的边界说明,AI 的阅读预期被打破,索引功能先废掉。分工混乱的成本,通常比多维护一份文件更高。
9. 症状、原因、解法对照表(Symptom → Cause → Fix)
| 症状 | 原因 | 解法 |
|---|---|---|
AI 改了 packages/core/src/utils.ts,而你要的是 apps/api/src/utils.ts |
同名文件无职责说明,搜索结果只差路径 | 两侧目录各写一份 README,直系清单里注明该 utils 的领域与非领域边界 |
| AI 在路由层直接写了 SQL,绕过了 db 包 | 越层没有显式禁令,AI 只看到「这里能用」 | 在 routes/README.md 的「边界」节写明「不写 SQL,一律走 packages/db」 |
| 新增文件后没人更新目录说明,半年后文档全过期 | 更新文档是口头约定,不进入改动流程 | 提示词加第 5 条,同时把检查器接进 CI,缺 README 直接失败 |
| AI 照着过期 README 改代码,改出一堆不存在的函数 | 没有约定「文档与代码冲突时以谁为准」 | 提示词加第 4 条:以代码为准,同一次改动顺手修文档 |
| 每个任务开头都要连读七八个文件才定位到目标 | 缺索引,AI 只能用广度试探换信息 | 铺分层 README,把探索路径从「试探」改成「直达」 |
根 README 写了 800 行,AI 读完还是不知道 src/routes 归谁管 |
用一份大全替代分层索引,粒度不对 | 按「一层一份、只写直系」拆开,根目录只保留全局与目录导航 |
10. 落地顺序:先根、再三层、最后卡 CI(Rollout Order)
不要一次把几百个目录全写完,那件事没有人能坚持到第二天。按这个顺序推,每一步都能独立生效:
- 根目录:写全局职责 + 顶层目录导航(每个顶层目录一行,指向它自己的 README)。这一步花二十分钟。
- 第二层 :
apps/、packages/下每个直接子目录各一份。这一步决定 AI 能不能分清「这是可部署单元还是共享包」。 - 第三层 :
src/这一层。这是收益最大的一层------AI 出错密度最高的地方就在这,因为这里的文件名最像。 - 接检查器 :
node scripts/readme-audit.mjs .进 CI 的 verify 步骤,允许存量缺口(先只对新增目录做校验),再逐步收紧。 - 进提示词:把 §5 那段规则贴进项目的 rules 文件,让「先读」从建议变成硬约束。
前三层做完,通常已经覆盖 80% 以上的实际改动。 再往下的叶子目录按需补:AI 反复出错的目录优先补,一年不动一次的目录不补。
也有明确不该用这套的地方,写清楚免得被滥用:
- 单目录的小脚本仓库,一共五个文件,写 README 的时间比读的时间长。
- 纯生成的代码目录(
dist/、协议自动生成的 client),改了会被覆盖,写了也是废纸。 - 第三方 vendor 目录,契约不归你管。
判断标准始终是同一条: 这个目录会不会有人(或 AI)在三个月后回来改它。 会,就值得一份索引;不会,就不写。
目录超过十个、AI 每周要在这里改五次以上代码,这套 README 就值得铺;单层小工具写清根目录那份即可。
本专栏《AI 开发范式》讲的是 AI 参与开发时的工程约束------代码审核清单、AI 接口接入、上下文组织与目录约定,每篇都配一段可直接运行的脚本,每周更新一篇。想按顺序读完,可在专栏页订阅。