跳转至

新人 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.pycore.pyhttp_adapter.py。 然后打开 pyproject.toml 看依赖。然后可能执行 grep -r "def " 找函数入口。 整个过程可能花 10 到 20 分钟。

README 的职责就是把这 10 到 20 分钟的探索缩短到 3 分钟——通过告诉读者 在哪里能找到他需要的答案。

注意措辞:"告诉读者在哪里找到答案",而不是"把所有答案写在 README 里"。 这个区分后面会反复出现。

现在我们明确了首次进入者的三个核心任务:

序号 读者任务 回答这个问题需要什么
1 判断项目目的 一句话说明解决什么问题
2 判断当前范围 已有什么能力、明确不包含什么
3 找到入口 从哪个目录或文件开始阅读

这三个任务是 README 设计的出发点——不是"README 应该有哪些章节"。章节是为 任务服务的,而不是反过来。

2. 信息不足和虚构事实:两种让读者失败的方式

现在我们来看两份候选 README,判断它们能不能帮首次进入者完成那三个任务。

README A:

# AI 学习助手

使用 Python 开发的学习规划工具。

就这么多。读者看完之后能回答什么?

  • 目的?——"学习规划工具",模糊但勉强算有。
  • 当前范围?——不知道。有几个模块?能运行吗?有 API 吗?一无所知。
  • 入口?——不知道。先看哪个文件?main.pyapp.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.pycli.py。文件树变成了:

learning_planner/
├── contracts.py
├── core.py
└── adapters/
    ├── http.py
    └── cli.py

但 README 里还写着:

## 模块结构

- `contracts.py` — 业务类型定义
- `core.py` — 计划生成逻辑
- `http_adapter.py` — HTTP 入口

新人看到 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 跟着工程事实 的增长而增长,但始终只做导航,不做复制。

回顾一下本章建立的整个判断链:

  1. 确定读者任务(目的、范围、入口)
  2. 每项信息选择处理方式(概括、链接、不出现)
  3. 工程变化时检查是否有直接提到的路径或命令需要同步

这三步就是"README 如何与工程事实保持一致"的答案——不是靠写得少,也不是 靠写得多,而是靠对每一项事实明确谁负责维护它

边界与常见误区

本章建立的是 README 的导航职责和权威来源边界。以下是它意味着的东西:

  • 不是 README 模板。本章没有给出"标准章节顺序"或"必须有这五个 section"。 章节是为读者任务服务的,不同项目的读者任务不同,README 结构自然不同。

  • 不意味着 README 越短越好。"不复制"不等于"不写"。项目目的、当前状态、 入口指引——这些只有 README 能概括,其他文件做不到。删掉它们不叫简洁,叫 缺失。

  • 不替代完整的文档体系。README 是入口,不是百科全书。API 文档、架构设计、 部署手册各有各的位置。README 只需要告诉读者"那些东西在哪里看"。

  • 不涉及自动化工具。CI 链接检查、文档生成、markdown lint 等工具有用, 但本章关注的是"判断什么该写、什么该链接、什么会漂移"的思维方式,不是 工具配置。

  • 不要在当前阶段写未来功能。"计划支持 Docker 部署"——如果你非要提, 必须明确标注"未实现"。否则读者会尝试执行不存在的命令。

本章小结

README 的职责是帮助首次进入者在最短时间内完成三个判断:项目做什么、当前 到哪里、从哪里开始。

处理每一项信息时,有三种选择:

  1. 概括——只有 README 负责的事实(目的、范围概述、入口指引);
  2. 链接——由其他文件维护的详细事实(依赖版本→声明文件,API schema→OpenAPI);
  3. 不出现——当前不存在的能力,避免构成虚构。

工程变化后,检查 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 适配

请回答:

  1. 哪些内容在当前 P1 阶段(无 FastAPI 服务、无可运行端点)属于虚构?
  2. 哪些内容属于"复制了其他文件维护的事实",有漂移风险?
  3. 哪些内容是合理的,可以保留?
  4. 如果让你重写,你会如何处理"运行"和"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() 如何
使用这些类型。

请回答:

  1. 哪些内容因为变更而失效了?
  2. 对于失效内容,哪些属于"概括层面的更新",哪些属于"如果当初不复制就不用改"?
  3. 重写这段内容,使其在下一次函数重命名时不需要更新。

练习三:FastAPI 上线后的 README 增量

W01-L02 完成后,仓库新增了以下能力:

  • main.py 中定义了 FastAPI app
  • 启动命令:uvicorn learning_planner.main:app
  • 自动生成 docs/openapi.json
  • 新增 .env.example,包含 PORT=8000

请回答:

  1. README 应该新增哪些内容?对每项说明是"概括"还是"链接"。
  2. 以下哪些写法会产生漂移风险?为什么?
  3. A:"API 详情见 docs/openapi.json"
  4. B:"POST /plans 接收 {topic: str, weekly_hours: int} 返回 {daily_minutes: int}"
  5. C:"环境变量配置见 .env.example"
  6. D:"需要设置 PORT=8000"
  7. 如果 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. 复制的核心区别—— 链接面向来源的存在性,复制面向某时刻的具体内容。

参考资料