每个文件夹都放一份 README 并让 AI 先读:把「AI 乱翻代码」变成「按目录索引定点取件」

结论:在项目根目录和每一层子目录各放一份 README,只写「本层有什么、谁负责、什么不该放这儿」,再在提示工程里要求 AI 动手前先读它------这是投入产出比最高的一条 AI Coding 约定。 它不改代码、不引框架、不装插件,只花写文档的时间,换回的是 AI 少读十个无关文件、少改错一个模块。下面是规则、模板、一段可直接抄进 rules 的提示词,以及一个我自己跑过的 README 缺口检查器。


目录

  1. 结论先说:README 是给 AI 看的目录索引(Index, Not Doc)
  2. AI 为什么爱改错文件:上下文里缺一张「哪块归谁」的地图(Why AI Misses)
  3. 三条硬规则:一层一份、只写直系、先读后改(Three Rules)
  4. 一份 README 写什么:五个字段的固定模板(Template)
  5. 提示工程怎么写:把「先读」变成硬约束(Prompt Rules)
  6. 一次可复算的实测:14 个目录、9 份 README、64.3%(Measurement)
  7. 完整可运行代码:README 缺口检查器(Runnable Checker)
  8. README 与 AGENTS.md、CLAUDE.md 的分工(Boundaries)
  9. 症状、原因、解法对照表(Symptom → Cause → Fix)
  10. 落地顺序:先根、再三层、最后卡 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 才能形成稳定的阅读预期------它知道第三段一定是边界说明,就不会在边界说明里找启动命令。

五个字段,按顺序:

  1. 一句话职责:这个目录为什么存在。写「不能干什么」比写「能干什么」更有信息量。
  2. 直系文件清单:文件名 → 干什么,一行一个,按重要性排。
  3. 边界:什么不该放这里、该放哪。这一节是防越层改动的核心。
  4. 入口与命令:这个目录如果有可执行入口,写清命令。
  5. 变更提示:改这里会牵连谁。给 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)

不要一次把几百个目录全写完,那件事没有人能坚持到第二天。按这个顺序推,每一步都能独立生效:

  1. 根目录:写全局职责 + 顶层目录导航(每个顶层目录一行,指向它自己的 README)。这一步花二十分钟。
  2. 第二层 :apps/、packages/ 下每个直接子目录各一份。这一步决定 AI 能不能分清「这是可部署单元还是共享包」。
  3. 第三层 :src/ 这一层。这是收益最大的一层------AI 出错密度最高的地方就在这,因为这里的文件名最像。
  4. 接检查器 :node scripts/readme-audit.mjs . 进 CI 的 verify 步骤,允许存量缺口(先只对新增目录做校验),再逐步收紧。
  5. 进提示词:把 §5 那段规则贴进项目的 rules 文件,让「先读」从建议变成硬约束。

前三层做完,通常已经覆盖 80% 以上的实际改动。 再往下的叶子目录按需补:AI 反复出错的目录优先补,一年不动一次的目录不补。

也有明确不该用这套的地方,写清楚免得被滥用:

  • 单目录的小脚本仓库,一共五个文件,写 README 的时间比读的时间长。
  • 纯生成的代码目录(dist/、协议自动生成的 client),改了会被覆盖,写了也是废纸。
  • 第三方 vendor 目录,契约不归你管。

判断标准始终是同一条: 这个目录会不会有人(或 AI)在三个月后回来改它。 会,就值得一份索引;不会,就不写。

目录超过十个、AI 每周要在这里改五次以上代码,这套 README 就值得铺;单层小工具写清根目录那份即可。

本专栏《AI 开发范式》讲的是 AI 参与开发时的工程约束------代码审核清单、AI 接口接入、上下文组织与目录约定,每篇都配一段可直接运行的脚本,每周更新一篇。想按顺序读完,可在专栏页订阅。

相关推荐
怕浪猫3 小时前
面试官问你:“AI 能写 80% 的代码了,公司为什么还需要你?”
ai编程
聪明的笨猪猪4 小时前
Java 工程案例:AI 生成 @Async 异步线程池踩坑
java·经验分享·面试·ai编程
这个DBA有点耶5 小时前
MCP隔离层+行级语义审计+输出校验:AI访问数据库的安全边界拆解
数据库·人工智能·ai编程
_砖家5 小时前
DeepSeek Harness 原来还是我们熟悉的 Webpack 和中间件模型
agent·ai编程·deepseek
沉默王二5 小时前
Gemini 4 Carbon 正式开放,正面硬刚 Fable 5.2。
ai编程·gemini·vibecoding
孟健8 小时前
我用Claude Skills把出海视频月账单从$86打到了$16
人工智能·ai编程·claude
ZzT12 小时前
AI-SDLC 实践 05|开发阶段的工程结构:在一个主仓里跨多个仓库写代码
ai编程·claude
boombb13 小时前
AI Engineer in robotic系列:从地图导入到机器人执行:导览平台 MVP 技术设计与落地
机器人·ai编程
爱勇宝13 小时前
Claude Fable 5.2 发布后,开发者该关注什么
程序员·ai编程