学完你能
- 说清为什么规矩要写进仓库,而不是留在聊天里。
- 说出六样东西各自的合格标准,给仓库做体检。
- 写出能抓住"看着对、其实错"的测试。
- 按卡点补仓库,用一次演练判断改动有没有用。
标签:实测这次跑过读码看代码得出,没跑推断根据代码和文档推出,没跑建议讲义的看法
Agent 只认仓库,不记得聊天
新开的会话默认看不到上一次的聊天。它能依据的只有两样:仓库里的文件,和你这次说的话。
所以,靠口头讲、靠聊天传下去的规矩,每开一次新会话就丢一次。你会发现自己一遍遍解释同样的事:测试怎么跑、哪个目录别动、改完要交什么。漏讲一句,它就按自己的猜测来。
办法是把这些写进仓库,并让仓库自己能检查。这就是"基于代码仓库的 AI-native":
把仓库建成人和 Agent 共用的工作环境:要做什么、项目知识、怎么做、怎样算对、改了什么,都存在仓库里,能复用、能检查。
只靠仓库,走完五步
检验标准只有一个:开一个新会话,不做口头补充,只给一张任务单。它能不能靠仓库走完这五步?
- 理解任务
- 修改
- 验证
- 修正
- 交付
这五步由三方分工完成:
仓库存规矩和依据
任务说明、AGENTS.md、Makefile、测试、Git 历史。
Agent 运行环境负责动手
读写文件、跑命令、把结果交回模型。权限、沙箱、次数上限也在这里。
你定目标、做取舍、验收
决定做什么、什么算完成,批准有风险的操作。
每一步卡在哪,就补什么
倒过来想:新会话在每一步会卡在哪里?把让它不卡的东西补上,就得到六样。它们不是一份清单,是从这五步里推出来的。
- 理解任务
会卡在不知道要做什么、做到哪算完;不知道代码在哪、有什么规矩。
- 修改
会卡在改到不该改的地方,也没有东西拦它。
- 验证
会卡在不知道用什么命令跑;跑了也看不出对错。
- 修正
会卡在错了没人告诉它,或者一出错程序就崩。
- 交付
会卡在你看不懂它改了什么、凭什么说做完了。
把六样做好,靠四条原则
写清楚:具体、能直接用,而且是真的
- 理解
- 修改
- 验证
- 修正
- 交付
写进仓库的每一句话,都会被 Agent 当真。所以要过三关:
- 具体:命令要能直接复制,写明在哪个目录跑。写
make test,别写"运行测试"。 - 只写这个仓库特有的:"请写高质量代码"这种放哪都成立的话,模型本来就会做;照抄 README 也是浪费。说明文件每次都会读进上下文,越长越费。
- 是真的:写之前验证过。一句没验证的"事实",比不写更糟。
好的例子:CS146S 2025 年第 4 周的 Makefile,把启动、测试、格式化、检查、灌数据都写成了固定命令。
.PHONY: run test format lint seedrun: PYTHONPATH=. uvicorn backend.app.main:app --reload --host $${HOST:-127.0.0.1} --port $${PORT:-8000}test: PYTHONPATH=. pytest -q backend/testsformat: black . ruff check . --fixlint: ruff check .seed: PYTHONPATH=. python -c "from backend.app.db import apply_seed_if_needed; apply_seed_if_needed()"练习:留、删还是改
假设你要给这个示例应用(FastAPI + SQLite)写 AGENTS.md。下面六句,该留、删还是改?想好再点开。
- 跑测试:在 week4/ 目录下执行 make test(等于 PYTHONPATH=. pytest -q backend/tests)
看答案
留读码命令准确,能直接复制,还说了在哪个目录跑(Makefile L6–7;作业 L59、L71)。
- 请编写高质量、可维护、符合最佳实践的代码。
看答案
删放在哪个仓库都成立,模型本来就会这么做。作业 L56 管这叫凑字数:这个文件每次都会读进上下文,越长越费。
- 本项目基于 FastAPI,这是一个高性能的现代 Python Web 框架。
看答案
删重复 README 和常识。有用的是下一句那种"东西在哪"。
- 路由在 backend/app/routers/,测试在 backend/tests/;测试用 client 夹具,它把数据库换成临时 SQLite。
看答案
留读码说清了入口和测试的写法(conftest.py L14–34)。
- 测试和开发数据库完全隔离,可以放心反复跑。
看答案
改推断测试只把请求用的数据库换成了临时库。但
with TestClient(app)会触发应用启动(FastAPI 文档),启动时会在真实库上建表;data/app.db不存在时,还会新建它并灌入种子数据(db.py L43–56)。没跑过,就别写"完全隔离"。先跑一次make test,看data/app.db有没有冒出来。2025/week04/assignments/backend/tests/conftest.pyL34–37 · 打开原文 app.dependency_overrides[get_db] = override_get_db with TestClient(app) as c: yield c2025/week04/assignments/backend/app/main.pyL21–24 · 打开原文 @app.on_event("startup")def startup_event() -> None: Base.metadata.create_all(bind=engine) apply_seed_if_needed() - data/app.db 是运行时生成的本地库,别提交、别手改;要改初始数据,改 data/seed.sql。
看答案
留读码说清了哪里不能改、该改哪里(db.py L12、L43–56)。
查得出:测试过了不算数
- 理解
- 修改
- 验证
- 修正
- 交付
Agent 靠检查结果判断自己做没做对。检查太松,它就会带着错误报"完成"。
例子还是那个示例应用。任务单要求搜索不分大小写:
## 2) Add search endpoint for notes- Add/extend `GET /notes/search?q=...` (case-insensitive) using SQLAlchemy filters- Update `frontend/app.js` to use the search query- Add tests in `backend/tests/test_notes.py`现有的搜索测试只查了请求成功、结果不为空:
r = client.get("/notes/search/") assert r.status_code == 200 r = client.get("/notes/search/", params={"q": "Hello"}) assert r.status_code == 200 items = r.json() assert len(items) >= 1一个不管搜什么都返回全部笔记的实现,也能通过。
测试过了不算数,要看它能不能抓住你担心的那种错。下面的小实验可以亲手试。
课程现在的搜索代码如下。注意第 31–32 行:没给搜索词时,返回全部。
@router.get("/search/", response_model=list[NoteRead])def search_notes(q: Optional[str] = None, db: Session = Depends(get_db)) -> list[NoteRead]: if not q: rows = db.execute(select(Note)).scalars().all() else: rows = ( db.execute(select(Note).where((Note.title.contains(q)) | (Note.content.contains(q)))) .scalars() .all() ) return [NoteRead.model_validate(row) for row in rows]故意写错,看测试抓不抓得住
这里有四版搜索:课程原版,加三种常见写错法。先看原来的测试结果,再点按钮加三条新测试。
| 版本 | 原测试只有 1 条笔记;搜 Hello,有结果就算过 | ① 大小写都能搜到搜 hello,要有 #1 和 #3 | ② 不混进无关的搜 hello,不能有 #2 | ③ 搜不到就返回空搜 zzz,结果为空 |
|---|
试试 Hello、zzz、%、_,或者留空。
新测试用的 3 条笔记
| # | 标题 | 内容 |
|---|---|---|
| 1 | Test | Hello world(和课程测试一样) |
| 2 | Groceries | milk and eggs |
| 3 | HELLO title | caps in title |
def test_search_is_case_insensitive_and_filters(client): for title, content in [("Test", "Hello world"), ("Groceries", "milk and eggs"), ("HELLO title", "caps in title")]: client.post("/notes/", json={"title": title, "content": content}) titles = {n["title"] for n in client.get("/notes/search/", params={"q": "hello"}).json()} assert titles == {"Test", "HELLO title"} # ① 和 ② assert client.get("/notes/search/", params={"q": "zzz"}).json() == [] # ③推断换个数据库,结果可能不同
课程代码能过 ①,是因为 SQLite 的 LIKE 默认不分英文大小写(SQLite 文档)。PostgreSQL 的 LIKE 分大小写,要换成 ILIKE(PostgreSQL 文档)。代码不变、换到 PostgreSQL,就不满足任务单第 9 行了。另外 实测非英文字母不会忽略大小写:用 école 搜不到 École。
实测% 和 _ 会被当成通配符
SQLAlchemy 的 contains() 默认不处理 % 和 _(文档),所以搜 % 会返回全部,上面可以试。想按原样搜,写 contains(q, autoescape=True)。算不算 bug,要看需求怎么定。
读码测试写不出来,先补需求
不给搜索词时返回全部(notes.py L31–32),测试第 13–14 行只查了状态码。需求没说这时该返回什么,测试也就没法判断对错。
建议带回你自己的仓库
故意写几版"看着对、其实错"的代码,看测试能不能抓出来。抓不出来的那版,就告诉你测试缺了哪条。
页面里的结果是浏览器现算的,按 SQLite 的搜索规则模拟。同样四版代码用 Python 自带的 sqlite3 跑过,结果一样:search_mutants.py,输出在 results.txt。课程原版的 FastAPI 应用和测试没跑:本机缺依赖,沙箱也挡住了下载。
拦得住:靠运行环境,不靠文字
- 理解
- 修改
- 验证
- 修正
- 交付
AGENTS.md 里写"别改仓库外的文件",只是一句话。真正能跑测试、能拒绝越界、能把错误交回模型的,是 Agent 的运行环境。
看一个最小的例子。CS146S 2026 年第一课用不到 200 行 Python 写了个 Agent,能读文件、列目录、改文件。它的循环是:模型回一行 tool: 名字(参数),程序照做,把结果塞回对话,直到模型不再调工具。
while True: assistant_resp = execute_llm_call(conversation) tool_invocations = extract_tool_invocations(assistant_resp) if not tool_invocations: print(f"{ASSISTANT_COLOR}Assistant{RESET_COLOR}: {assistant_resp}") conversation.append({ "role": "assistant", "content": assistant_resp }) break print(f"{ASSISTANT_COLOR}Assistant{RESET_COLOR}: {assistant_resp}") conversation.append({ "role": "assistant", "content": assistant_resp }) for name, args in tool_invocations: tool = TOOL_REGISTRY[name] resp = [] if name == "read_file": resp = tool(args.get("filename", ".")) elif name == "list_files": resp = tool(args.get("path", ".")) elif name == "edit_file": resp = tool(args.get("path", "."), args.get("old_str", ""), args.get("new_str", "")) conversation.append({ "role": "user", "content": f"tool_result({json.dumps(resp)})" })讲原理够了。真拿来改仓库,运行环境还缺这些:
| 缺什么 | 依据 | 影响哪步 | 一般怎么补 建议 |
|---|---|---|---|
| 没法跑命令和测试 | 读码只有读、列、改三个工具(L96–100) | 验证 | 给一个受控的跑命令工具,把测试结果交回模型 |
| 不限制路径 | 实测../、绝对路径、~、指向外面的链接都能用(见下表) | 修改 | 检查路径还在仓库里,再加沙箱 |
| 覆盖了文件,却说是"新建" | 实测文件已存在也会被整个覆盖,返回的还是 created_file(L77–82) | 交付 | 分清新建和覆盖,如实报告 |
| 一出错就整个崩掉 | 实测调不存在的工具(KeyError)、读不存在的文件(FileNotFoundError),程序直接退出;L176–184 没处理错误 | 修正 | 把错误当结果交回模型,让它自己改 |
| 没有次数和花费上限 | 读码循环是 while True(L161),只限了单次输出长度(L143) | 全程 | 限制轮数、花费、时间 |
| 不经批准就写入 | 读码写文件立刻生效(L78、L90) | 修改 | 危险操作先让人批准 |
路径实验
def resolve_abs_path(path_str: str) -> Path: path = Path(path_str).expanduser() if not path.is_absolute(): path = (Path.cwd() / path).resolve() return path实测把这个函数原样拿出来,和加了检查的版本比一比(path_boundary.py):
| Agent 给的路径 | 课程函数解析到 | 加检查后 |
|---|---|---|
notes.md | 仓库里 | 放行 |
../secret.txt | 仓库外 | 拒绝 |
/etc/hosts | 仓库外 | 拒绝 |
~/.ssh/config | 仓库外 | 拒绝 |
link(仓库里的链接,指向外面) | 仓库外 | 拒绝 |
sub/../notes.md | 仓库里 | 放行 |
def resolve_in_repo(path_str: str, root: Path) -> Path: root = root.resolve() candidate = Path(path_str).expanduser() if not candidate.is_absolute(): candidate = root / candidate resolved = candidate.resolve() # 展开 .. 并跟随链接 if not resolved.is_relative_to(root): raise OutsideRepoError(f"{path_str!r} resolves outside {root}") return resolved另外三条"实测"来自 agent_functions.py:从课程代码里取出这几个函数原样运行,用写死的回复代替模型,没调用真模型。
交得明白:不看聊天,也能审、能撤回
- 理解
- 修改
- 验证
- 修正
- 交付
Agent 做完了,你要决定收不收。你手上应该有这些,而不是一句"已完成":
- 一件事一份改动:一个分支、一个 PR,出了问题能单独撤回。
- 改了什么、为什么:问题是什么,怎么解决的。
- 跑了什么、结果如何:命令和输出原样贴上。
- 还有什么没做:取舍、限制、之后要补的。
- 怎么撤回:有什么风险,出事怎么回退。
CS146S 2025 年第 7 周作业对每个 PR 就是这么要求的,还要人逐行看过改动,再拿 AI 审查的意见和自己的比。
报告本身也得是真的。原则 ③ 里那个教学 Agent,覆盖了一个已有文件,返回的却是"新建"(created_file)。实测工具报错了动作,后面的人就会看错改动。
六样东西,每样的规范
前面讲的是道理,这一章是查阅用的清单。每样分三块:必须写清什么、怎么算合格、常见问题。括号里是出处。规范是讲义根据课程作业整理的,不是统一标准。
必须写清(2026 W4 作业 L50–54 的五块)
- 项目是什么:一两句,让 Agent 知道是哪个领域。
- 结构:关键目录、入口、各部分怎么连起来。
- 命令:构建、测试、检查、运行,能直接复制(细节归"执行入口")。
- 规矩:这个仓库实际在用的测试写法、代码风格、命名。
- 禁区:不能手改的文件,容易用错的命令或目录。
怎么算合格
- 每句都只对这个仓库成立,一页左右。(L56)
- 拿两个真任务试过:Agent 确实用了里面的命令、守了里面的规矩。(L58–62)
常见问题
- "请写高质量代码"这类空话;照抄 README。
- 写了没验证过的话(原则 ① 练习里"完全隔离"那句)。
- 只写"不要做 X",却没有东西真拦。那属于操作边界。
必须写清
- 安装、启动、测试、检查、格式化,各有一条命令。(2025 W4 Makefile L3–17)
- 在哪个目录运行。(2025 W4 作业 L59、L71)
- 需要的环境变量、数据和前置步骤,比如先灌初始数据。
- 命令可以重复跑,不会把环境弄乱。(2025 W4 作业 L105:"prefer idempotent steps")
怎么算合格
在一台干净的机器上照抄就能跑通。失败时命令返回非 0,报错看得懂。
常见问题
- 写"运行测试",不写具体命令。作业原话:要写
pixi run pytest tests/,不要写 "run the tests"。(2026 W4 作业 L52) - 命令依赖一个没写下来的前置步骤。
必须做到
- 每条需求至少一条测试,既测"该有的有",也测"不该有的没有"。
- 覆盖边界情况:大小写、空输入、搜不到。
- 测试数据和开发数据分开。(conftest.py 换成了临时库,但看原则 ① 练习的第五句)
- 失败时看得出哪条错、为什么错。
- 新功能先写一个会失败的测试,再实现。(2025 W4 作业 L115 的例子)
怎么算合格
故意写错一处(像小实验那样),至少一条测试会失败。
常见问题
- 只查状态码和"结果不为空"。(test_notes.py L13–19)
- 结果依赖数据库的特殊行为,自己却不知道(SQLite 的大小写)。
- 没测"不该发生"的情况。2026 W3 作业专门要求测一个近似但不该触发的提示词。(L67)
必须做到
- 能写的目录有范围,超出就拒绝。
- 密钥不进仓库,Agent 默认拿不到。
- 删除、强推、发布、对外发消息这类操作,要人批准。
- 轮数、花费、时间有上限。
- 工具出错时,把错误交回模型,而不是整个崩掉。
怎么算合格
真去试一次:让 Agent 写仓库外的文件、读密钥、做没批准的操作,看会不会被拒(原则 ③ 的路径实验)。
常见问题
- 只在 AGENTS.md 里写"不要……",没有任何机制真拦。
- 只限制了路径,命令和网络照样随便用。
- 课程教学 Agent 这几项都没有,是反面例子。(L28–32、L67–94)
必须写清(2025 W7 作业 L16–22)
- 一件事一个分支、一个 PR。(L16)
- 改了什么、为什么这样改。(L20)
- 跑了哪些命令、结果是什么,贴上输出。(L21)
- 取舍、限制、之后还要做的。(L22)
- 怎么撤回、有什么风险。(2025 W4 作业 L147)
- 有人逐行看过改动。(W7 L18)
怎么算合格
审的人不看聊天记录,也能判断能不能合并;任何一个改动都能单独撤回。
常见问题
- 只写"已完成,测试通过",不给命令和结果。
- 一个 PR 混了好几件事,没法单独撤回。
从哪开始:看症状,补一样
别一上来就把六样全写一遍。先看你最常遇到哪种情况:
| 你遇到的情况 | 先补 | 对应原则 |
|---|---|---|
| 做出来的不是你要的 | 任务规格 | ① 写清楚 |
| 每次都要重新讲目录和规矩 | 项目上下文 | ① 写清楚 |
| 命令老猜错,跑不起来 | 执行入口 | ① 写清楚 |
| 测试全过,还是有 bug | 质量反馈 | ② 查得出 |
| 改到不该改的地方 | 操作边界 | ③ 拦得住 |
| 看不懂它交的活,不敢合并 | 交付证据 | ④ 交得明白 |
拿你自己的一个仓库,对照第五章的规范逐项看:哪样已经有,哪样只写在文字里、没有东西真拦,哪样完全没有。
补完之后,做一次交接演练
- 挑三个真实小任务,比如修一个 bug、加一个小功能、改一页文档。每个写清:目标、范围、不做什么、用什么命令验收。
- 开一个新会话,只给任务单,不额外解释。
- 记下:Agent 找没找对地方、用没用仓库里的命令、你插手了几次、交活时附了什么证据。
- 检查结果,记下漏掉的错和返工花的时间。
- 每次只改一处(命令、限制、测试或需求),用同样三个任务再跑一遍,前后对比。
| 看什么 | 怎么记 | 说明什么 |
|---|---|---|
| 完成率 | 验收命令通过的任务占几成 | 仓库能不能撑起 Agent 独立干活 |
| 人工插手 | 每个任务你补充或纠正了几次 | 还有哪些知识只在你脑子里 |
| 漏掉的错 | 审查时或事后才发现的问题 | 测试和验收够不够严 |
| 返工成本 | 返工花的时间,或被撤回的改动数 | 改动够不够小、好审、好撤 |
五道题,看看学会没有
为什么规矩要写进仓库,不能每次在聊天里讲?
看答案
新会话默认看不到上次的聊天,靠聊天传的规矩每次都会丢。写进仓库的东西每个会话都能读到,还有版本、能审、能撤回(第一章)。
哪条检查能分出"搜索正常"和"不管搜什么都返回全部"?
assert r.status_code == 200assert len(items) >= 1- 结果里没有无关的笔记
看答案
C。A 和 B 两种实现都满足。只有检查"不该出现的没出现",才能抓住它,就是小实验里的 ②(原则 ②)。
提示词里写"只能改仓库里的文件",能拦住教学 Agent 写
../secret.txt吗?看答案
拦不住。路径函数照单全收,改文件的函数直接写。得在运行环境里检查路径,或者放进沙箱(原则 ③)。
仓库里有 AGENTS.md,测试全过,就算 AI-native 了吗?
看答案
不一定。要看新开的 Agent 能不能只靠仓库走完五步,并交出你能核对的证据。AGENTS.md 只是指路,测试还得抓得住错(第二章、原则 ②)。
改了 AGENTS.md,怎么知道有没有用?
看答案
拿同一组真实任务前后各跑一次,模型、版本、工具不变,比完成率、插手次数、漏掉的错和返工成本。也看 Agent 有没有用文件里写的命令、守里面的规矩(第六章)。
这份讲义靠什么说话
读码看过的代码
页面里的代码按行原样摘自课程公开仓库的固定提交(2025:ca2df55,2026:a6dcbf2)和课程公开的 Google Drive 文件,行号就是原文行号。点代码块右上角能打开原文。
实测跑过的
- search_mutants.py:四版搜索在新旧测试下的结果,以及 LIKE 的大小写和通配符规则。
- path_boundary.py:课程路径函数和加检查版本的对比。
- agent_functions.py:从课程代码取出工具函数和循环,用写死的回复驱动。
- 环境:Python 3.14.6 自带库、SQLite 3.53.3,2026-10-26。输出都在 results.txt。
推断没跑过、靠推的
- 课程测试会在
data/app.db不存在时新建它并灌数据:依据 conftest.py L36、main.py L21–24、db.py L43–56 和 FastAPI 文档。 - 课程搜索放到 PostgreSQL 会分大小写:依据 PostgreSQL 文档,没在 PostgreSQL 上跑。
没做的
- 没跑课程的 FastAPI 应用和测试:本机缺依赖,沙箱挡住了下载。
- 没调用模型,没用真 Agent 做交接演练。
- 没有教学效果数据。"建议"是看法,不是实验结论。
来源
- 2026 W4 作业:给仓库写 Agent 说明 L35–62
- 2026 W4 writeup 模板 L34–42
- 2026 W3 作业:写一个 Skill L67
- 2025 设计文档模板 L58–60、L79–89
- 2025 W4 作业 L59、L71、L144–149
- 2025 W4 Makefile L1–17
- 2025 W4 TASKS L8–11
- 2025 W4 test_notes.py L13–19
- 2025 W4 routers/notes.py L29–39
- 2025 W4 conftest.py L14–37
- 2025 W4 main.py L21–24、db.py L12、L43–56
- 2025 W7 作业 L15–24
- 2026 W1 教学 Agent L28–32、L67–100、L143、L161–188
- AGENTS.md 官方说明
- FastAPI:测试里的启动事件
- SQLite:LIKE
- PostgreSQL:模式匹配
- SQLAlchemy:contains
- 课程官网 The Modern Software Developer · 课程作业仓库