新人 clone 了仓库,README 却让他执行一条不存在的命令¶
跨章复习检查点¶
在进入正文之前,暂停回忆两件事:
- 如果一个新模块要使用
build_plan(),它应该 import 哪个文件? (P03:adapter import core 和 contracts;core 的公开入口是build_plan()函数 和 contracts 中的类型。) - 一个可交付仓库中,哪些文件属于"协作者理解和重建项目所必需的声明性输入"? (P05:源码、依赖声明、安全模板等属于项目事实;本地环境、缓存、日志和 secret 属于机器快照,不应跟踪。)
这两个判断直接关系到本章的核心问题:README 应该导航读者找到哪些真实存在的 代码入口和声明文件。如果你对"模块公开入口"或"artifact 跟踪决策"感觉模糊, 可以回到 P03 第一节或 P05 小结浏览一下再继续。
你的同事第一次打开 AI 学习助手仓库。当前项目只有 typed core(domain models + contracts)和 http_adapter 模块。没有 FastAPI 路由实例、没有 Docker 配置、没有 部署说明、没有数据库。
他双击了 README.md,准备在三分钟内搞清楚三件事:这个项目干什么、当前做到哪里、
从哪里开始看代码。
三分钟之后,他可能顺利进入源码——也可能关掉文件,向你发消息问"这东西怎么跑"。 区别不在 README 写了多少字,而在那些字是否指向当前可验证的工程事实。
材料说明¶
本章不涉及编程代码。所有示例以 README 片段、仓库文件树、命令输出和前后对比 呈现。讨论对象是 AI 学习助手 P1 仓库当前阶段的真实文件结构。
1. 第一次打开仓库的人需要完成什么¶
让我们先不看任何 README 内容。换个角度:假设你是那个第一次 clone 仓库的人。 你的目标不是"浏览一遍所有文件",而是尽快做出判断:
- 这个项目解决什么问题?(目的)
- 它现在有什么能力、什么还没有?(范围和状态)
- 我应该先看哪个文件或目录?(入口)
暂停预测一下:如果 README 不存在,你会怎么做?
大概率你会先看文件树——src/、contracts.py、core.py、http_adapter.py。
然后打开 pyproject.toml 看依赖。然后可能执行 grep -r "def " 找函数入口。
整个过程可能花 10 到 20 分钟。
README 的职责就是把这 10 到 20 分钟的探索缩短到 3 分钟——通过告诉读者 在哪里能找到他需要的答案。
注意措辞:"告诉读者在哪里找到答案",而不是"把所有答案写在 README 里"。 这个区分后面会反复出现。
现在我们明确了首次进入者的三个核心任务:
| 序号 | 读者任务 | 回答这个问题需要什么 |
|---|---|---|
| 1 | 判断项目目的 | 一句话说明解决什么问题 |
| 2 | 判断当前范围 | 已有什么能力、明确不包含什么 |
| 3 | 找到入口 | 从哪个目录或文件开始阅读 |
这三个任务是 README 设计的出发点——不是"README 应该有哪些章节"。章节是为 任务服务的,而不是反过来。
2. 信息不足和虚构事实:两种让读者失败的方式¶
现在我们来看两份候选 README,判断它们能不能帮首次进入者完成那三个任务。
README A:
就这么多。读者看完之后能回答什么?
- 目的?——"学习规划工具",模糊但勉强算有。
- 当前范围?——不知道。有几个模块?能运行吗?有 API 吗?一无所知。
- 入口?——不知道。先看哪个文件?
main.py?app.py?不存在的话呢?
结论:三个任务中,只有第一个勉强完成(而且描述过于笼统)。读者不得不自己探索 整个仓库结构——README 的存在几乎没有帮到他。
README B:
# AI 学习助手
基于 FastAPI 的学习规划 API 服务。
## 快速开始
docker compose up -d
curl http://localhost:8000/api/plans -d '{"topic":"Python","weekly_hours":6}'
## 依赖
- fastapi==0.115.0
- pydantic==2.9.0
- uvicorn==0.30.0
## API 接口
- POST /api/plans — 创建学习计划
- GET /api/plans/:id — 获取计划详情
看起来信息丰富多了。但暂停想一下:当前 P1 仓库里有 Docker 吗?有 FastAPI 路由
吗?有 /api/plans 端点吗?
没有。当前仓库只有 typed core 和 http_adapter 模块——连一个可运行的 HTTP 服务都还没有。
读者执行 docker compose up -d,得到的是"文件不存在"。他访问
/api/plans,得到的是连接拒绝。他对照着 README 做的每一步都会失败。
而且还有另一个问题:pydantic==2.9.0 写在 README 里。但依赖版本的权威来源是
pyproject.toml。如果下次升级了 Pydantic 版本但忘了改 README 呢?读者看到的
版本和实际安装的不一致——又一种失败。
两种 README 让读者在不同的任务上失败:
| README | 失败模式 | 读者在哪个任务上卡住 |
|---|---|---|
| A | 信息不足 | 无法判断范围和入口 |
| B | 虚构/复制 | 按照入口操作后发现命令和端点不存在;版本信息可能过时 |
A 的问题是"缺失"——读者得不到导航。B 的问题是"多余且不实"——读者被导向 一个不存在的目的地,或者拿到一份可能已经漂移的事实副本。
3. 概括、链接还是不该出现:谁维护什么事实¶
B 的教训不是"不该写那么多",而是"不该把别处维护的事实复制到 README 里"。 那 README 应该怎么处理这些信息?逐项分析当前 P1 仓库中的事实:
项目目的——没有其他文件负责声明,由 README 自己概括。
依赖列表和版本——权威来源是 pyproject.toml。正确做法是告诉读者"依赖在
pyproject.toml,环境重建方式是 uv sync"——导航到来源,不复制内容。
模块结构和公开入口——README 可以概括目录级别的职责分工,但具体函数签名 由代码维护,不需要列出。
启动命令——当前无可运行服务入口。如果写了不存在的命令就是虚构。
未来计划——不存在的能力不应出现,除非明确标注"未实现"。
暂停想一下:对每一项事实,你会选择"概括"、"链接到来源"还是"不应出现"?
整理成表:
| 信息 | README 的处理方式 | 原因 |
|---|---|---|
| 项目目的和背景 | 概括 | 没有其他权威来源负责此事 |
| 依赖列表和版本 | 链接(指向 pyproject.toml) |
版本由声明文件维护 |
| 模块结构概览 | 概括(目录级别) | 帮助读者定位,不复制函数签名 |
| 环境重建命令 | 概括(如 uv sync) |
命令本身稳定,不随代码变化 |
| 具体函数签名 | 不出现 | 由代码自身维护,复制会漂移 |
| 尚未实现的命令/端点 | 不应出现(或明确标注"未实现") | 构成虚构,误导读者 |
| API schema 详情 | 链接(未来指向 OpenAPI) | 由自动生成契约维护 |
总结成一句话:README 概括"只有它负责"的事实,链接"别处维护"的事实, 不出现"尚不存在"的事实。
类比地图:标注路口和方向,但不复制每栋建筑的内部平面图;画了一条未修好的 路——不是地图问题,是地图在撒谎。不过工程项目的文件结构会频繁变化,所以 README 的导航信息也会过时——这就是下一节要解决的问题。
4. 模块入口变了,README 还指着旧路径¶
两周后,团队做了一次重构:把 http_adapter.py 拆成了 adapters/ 目录,里面
有 http.py 和 cli.py。文件树变成了:
但 README 里还写着:
新人看到 README 说有 http_adapter.py,实际在根目录没找到这个文件。他可能
花几分钟在仓库里搜索,才发现入口移到了 adapters/http.py。
这就是漂移:README 的内容在某个时间点是正确的,但工程事实已经变化, README 没有跟着更新。
暂停预测:在这个例子里,README 中哪些内容必须更新,哪些不需要?
让我们逐项检查:
| README 中的内容 | 需要更新吗? | 原因 |
|---|---|---|
| "contracts.py — 业务类型定义" | 不需要 | 文件位置没变 |
| "core.py — 计划生成逻辑" | 不需要 | 文件位置没变 |
| "http_adapter.py — HTTP 入口" | 必须更新 | 文件已移动到 adapters/http.py |
| 项目目的描述 | 不需要 | 目的没变 |
| "依赖见 pyproject.toml" | 不需要 | 这是链接,不是复制 |
关键发现:README 中直接写了路径或文件名的内容,在对应文件移动时必须更新。 而"链接到来源"的表述(如"依赖见 pyproject.toml")只要来源文件本身没换名字, 就不需要动。
这揭示了一个实用判断:当你完成一次重构或重命名后,问自己——"README 里有没有 直接提到被改动的路径或命令?"如果有,那就是需要同步更新的内容。
反过来看,这也解释了为什么第三节建议"链接而不是复制":你在 README 里写的 详细事实越多,每次变更后需要检查和同步的位置就越多。
同样的道理适用于命令。如果 README 写了 python -m learning_planner.http_adapter,
重构后这条命令会变成 python -m learning_planner.adapters.http——新人执行
旧命令会得到 ModuleNotFoundError,看起来像环境问题但实际是文档过时。
本章的范围是建立"什么时候需要检查"的判断意识。CI 链接检查、文档测试等 自动化手段存在,但它们建立在你先知道"哪些内容会漂移"的前提之上。
5. FastAPI 来了,README 该加什么¶
到了 W01-L02,团队真正加入了可运行的 FastAPI 服务,也自动生成了 OpenAPI 规范。 文件树变成了:
learning_planner/
├── contracts.py
├── core.py
├── adapters/
│ ├── http.py
│ └── cli.py
├── main.py # FastAPI app 入口
├── pyproject.toml
└── docs/
└── openapi.json # 自动生成
现在有些事实真的存在了:
- 服务入口:
uvicorn learning_planner.main:app - API 端点:
POST /plans(可从 OpenAPI 验证) - OpenAPI 文档:
docs/openapi.json
暂停想一下:按照前面建立的原则,README 应该对这些新事实做什么?
逐项判断:启动命令不由其他文件维护,README 自己概括。API 端点的详细 schema
由 OpenAPI 维护,README 只需链接到 docs/openapi.json,不手写请求体格式。
OpenAPI 文件由代码自动生成,README 链接即可。
整理成表:
| 新事实 | README 的处理 | 权威来源 |
|---|---|---|
| 服务启动命令 | 概括 | README 自身 |
| API 端点概览(有哪些端点) | 概括(一句话) | 代码路由定义 |
| 端点详细 schema | 链接到 OpenAPI | docs/openapi.json |
| 环境变量说明 | 链接到 .env.example |
.env.example |
注意 README 的核心职责没有变——它仍然是在帮首次进入者完成同样的三个任务 (目的、范围、入口)。只是"范围"变宽了,"入口"变多了。README 跟着工程事实 的增长而增长,但始终只做导航,不做复制。
回顾一下本章建立的整个判断链:
- 确定读者任务(目的、范围、入口)
- 每项信息选择处理方式(概括、链接、不出现)
- 工程变化时检查是否有直接提到的路径或命令需要同步
这三步就是"README 如何与工程事实保持一致"的答案——不是靠写得少,也不是 靠写得多,而是靠对每一项事实明确谁负责维护它。
边界与常见误区¶
本章建立的是 README 的导航职责和权威来源边界。以下是它不意味着的东西:
-
不是 README 模板。本章没有给出"标准章节顺序"或"必须有这五个 section"。 章节是为读者任务服务的,不同项目的读者任务不同,README 结构自然不同。
-
不意味着 README 越短越好。"不复制"不等于"不写"。项目目的、当前状态、 入口指引——这些只有 README 能概括,其他文件做不到。删掉它们不叫简洁,叫 缺失。
-
不替代完整的文档体系。README 是入口,不是百科全书。API 文档、架构设计、 部署手册各有各的位置。README 只需要告诉读者"那些东西在哪里看"。
-
不涉及自动化工具。CI 链接检查、文档生成、markdown lint 等工具有用, 但本章关注的是"判断什么该写、什么该链接、什么会漂移"的思维方式,不是 工具配置。
-
不要在当前阶段写未来功能。"计划支持 Docker 部署"——如果你非要提, 必须明确标注"未实现"。否则读者会尝试执行不存在的命令。
本章小结¶
README 的职责是帮助首次进入者在最短时间内完成三个判断:项目做什么、当前 到哪里、从哪里开始。
处理每一项信息时,有三种选择:
- 概括——只有 README 负责的事实(目的、范围概述、入口指引);
- 链接——由其他文件维护的详细事实(依赖版本→声明文件,API schema→OpenAPI);
- 不出现——当前不存在的能力,避免构成虚构。
工程变化后,检查 README 中直接提到的路径和命令是否仍然有效。"链接到来源" 比"复制来源内容"更抗漂移——来源变了,链接的表述通常不需要动。
这个判断逻辑和 P05 的 artifact 分类思路是一致的:文件跟踪与否看"谁是权威 来源";README 写什么也看"谁是权威来源"。权威来源只有一份,其他地方都应该 指向它而不是复制它。
练习¶
练习一:诊断一份现有 README¶
以下是 AI 学习助手 P1 仓库的一份 README 草稿。请找出其中的问题:
# AI Learning Planner
AI 驱动的个性化学习规划工具,基于 FastAPI + Pydantic 构建。
## 安装
pip install -e .
## 运行
uvicorn learning_planner.main:app --reload
## API
- POST /api/plans - 创建学习计划
- 请求体: {"topic": str, "weekly_hours": int}
- 响应: {"status": "ok", "daily_minutes": int}
## 依赖
- Python 3.12
- pydantic==2.9.0
- fastapi==0.115.0
## 模块
- contracts.py: 数据类型
- core.py: build_plan() 函数
- http_adapter.py: HTTP 适配
请回答:
- 哪些内容在当前 P1 阶段(无 FastAPI 服务、无可运行端点)属于虚构?
- 哪些内容属于"复制了其他文件维护的事实",有漂移风险?
- 哪些内容是合理的,可以保留?
- 如果让你重写,你会如何处理"运行"和"API"这两个部分?
练习二:重构后的漂移检查¶
团队完成了以下两项变更:
- 把
core.py中的build_plan()重命名为generate_plan() - 把
contracts.py移动到learning_planner/models/contracts.py
当前 README 中有这些内容:
## 项目结构
- `contracts.py` — 定义 LearningGoal、Plan 和 PlanFailure
- `core.py` — 包含 build_plan() 计划生成函数
- `http_adapter.py` — HTTP 入口适配
## 开始阅读
建议从 contracts.py 开始了解数据类型,然后看 core.py 的 build_plan() 如何
使用这些类型。
请回答:
- 哪些内容因为变更而失效了?
- 对于失效内容,哪些属于"概括层面的更新",哪些属于"如果当初不复制就不用改"?
- 重写这段内容,使其在下一次函数重命名时不需要更新。
练习三:FastAPI 上线后的 README 增量¶
W01-L02 完成后,仓库新增了以下能力:
main.py中定义了 FastAPI app- 启动命令:
uvicorn learning_planner.main:app - 自动生成
docs/openapi.json - 新增
.env.example,包含PORT=8000
请回答:
- README 应该新增哪些内容?对每项说明是"概括"还是"链接"。
- 以下哪些写法会产生漂移风险?为什么?
- A:"API 详情见
docs/openapi.json" - B:"POST /plans 接收
{topic: str, weekly_hours: int}返回{daily_minutes: int}" - C:"环境变量配置见
.env.example" - D:"需要设置 PORT=8000"
- 如果 OpenAPI 规范后来增加了一个
GET /plans/:id端点,A 和 B 哪个需要更新?
练习解析¶
练习一解析¶
1. 当前阶段属于虚构的内容:
- "基于 FastAPI + Pydantic 构建"——当前没有 FastAPI 应用,只有 Pydantic 用于 边界模型,但不存在 Web 服务。
- "uvicorn learning_planner.main:app --reload"——
main.py不存在,执行会 得到ModuleNotFoundError。 - "POST /api/plans"及其请求/响应格式——端点不存在,当前无可运行服务。
2. 复制了其他文件维护的事实(有漂移风险):
- "pydantic==2.9.0"、"fastapi==0.115.0"——版本号由
pyproject.toml维护。 升级依赖后如果忘了改 README,读者看到的版本就是错的。 - "build_plan() 函数"——函数名由代码维护。重命名函数后 README 会漂移。
- 请求体和响应格式
{"topic": str, "weekly_hours": int}——即使未来有了端点, 这些细节也应该由 OpenAPI 维护,不该手写在 README 里。
3. 合理可保留的内容:
- 项目名称 "AI Learning Planner"——README 自己负责。
- "pip install -e ."——安装命令相对稳定,且 README 是告诉读者如何开始的地方。
- 模块结构的概括性描述(如果路径正确的话)。
4. 重写建议:
"运行"部分:当前阶段应诚实说明"当前为库模块,无独立服务入口。可在 Python
中直接 from learning_planner.core import build_plan 使用。"
"API"部分:当前阶段应删除整个部分,或写"API 服务入口计划在后续版本实现, 当前不可用。"
练习二解析¶
1. 失效内容: contracts.py 路径(已移动到 models/contracts.py)、
build_plan() 函数名(已重命名为 generate_plan())、"开始阅读"段落中的
路径和函数名——三处全部失效。
2. 概括 vs. 复制: 路径变更属于概括层面的必要更新(README 需要指向正确
位置)。函数名则属于"如果当初不写就不用改"——README 可以只说"计划生成逻辑
在 core.py",不提函数名。
3. 抗重命名的重写:
## 项目结构
- `models/contracts.py` — 业务类型定义(学习目标、计划、失败状态)
- `core.py` — 计划生成逻辑
- `http_adapter.py` — HTTP 入口适配
## 开始阅读
建议从 `models/contracts.py` 了解核心数据类型,然后看 `core.py` 如何使用
这些类型生成计划。
核心变化:不写具体函数名,只写职责描述。函数重命名时 README 不受影响;
读者需要精确函数名时可在 IDE 中搜索 def 快速定位。
练习三解析¶
1. README 应新增的内容:
- 服务启动命令
uvicorn learning_planner.main:app→概括(README 自身负责) - "API 文档见
docs/openapi.json或访问/docs"→链接(schema 由 OpenAPI 维护) - "环境变量配置见
.env.example"→链接(具体变量由模板维护) - 当前范围更新(新增"可运行 FastAPI 服务")→概括
2. 漂移风险判断:
- A(见 openapi.json)和 C(见 .env.example)——低风险,是链接。
- B(手写端点格式)——高风险,复制了具体 schema,字段变化即漂移。
- D(写出 PORT=8000)——中等风险,复制了模板文件中的具体值。
3. 新增 GET /plans/:id 后:A 不需要更新(它指向来源,来源自动包含新端点);
B 必须更新(手动列表不会自动出现新端点)。这就是链接 vs. 复制的核心区别——
链接面向来源的存在性,复制面向某时刻的具体内容。
参考资料¶
- GitHub Docs: About READMEs——GitHub 对 README 角色和展示方式的官方说明
- Make a README——README 设计的实践建议和示例
- Daniel Beck: Write the Readable README——Write the Docs 大会演讲,讨论 README 的读者导向设计