一份面向开发者的中文系统教程:教你把 AI 编码代理(ZCode、Claude Code、Codex CLI、Cursor 等)从"玩具"用成"团队"。覆盖项目配置、Skill 工程、Agent 边界、任务拆分与多 Agent 协同五大核心议题。
2025 年 Andrej Karpathy 提出 "vibe coding"(凭感觉编码):用自然语言描述需求,让 AI 生成代码,人几乎不逐行审阅。这种方式能一小时做出一个 demo,但把 demo 做成产品时会立刻撞上四堵墙:
本教程的主线就是逐一拆掉这四堵墙:用项目配置固化上下文,用任务拆分控制复杂度,用边界设计约束权限,用多 Agent 协同放大产能——这也是业界从 "vibe coding" 走向 "agentic engineering"(规格驱动、测试护栏、人工检查点、沙箱隔离)的路径。
┌─────────────────────────────────────────────────────────┐
│ 快速上手(半天): 01 → 02 → 03 → 14 → 15 │
├─────────────────────────────────────────────────────────┤
│ 系统学习(一至两周): Part1 → Part2 → Part3 → Part5 │
│ → Part7(按顺序读完所有动手练习) │
├─────────────────────────────────────────────────────────┤
│ 团队负责人路线: 04 → 07 → 11 → 12 → 13 → 17 → 20 │
│ → 附录 B(模板库)→ 附录 C(检查清单) │
└─────────────────────────────────────────────────────────┘
全书 23 章 + 4 附录,分为七个部分。所有章节围绕一个贯穿项目展开:读书笔记与书评应用**「轻读 BookNest」**(需求书见 附录 D)——每章的"贯穿项目实战"小节,会演示把本章方法应用到这个真实项目上是什么样子。
建立对 vibe coding 和 AI 编码代理的正确认知:它是什么、边界在哪、怎么跟它说话。
| 章 | 文件 | 内容 |
|---|---|---|
| 01 | 什么是 Vibe Coding | 起源与定义、三种成熟度、适用与不适用场景 |
| 02 | Agent 工作台 | 工具地图、核心概念(上下文窗口/权限模式/工具调用)、环境搭建 |
| 03 | 上下文工程 | 提示词结构、迭代节奏、上下文窗口管理、反模式 |
把"每次都要重新解释项目"变成"代理开机即懂"。
| 章 | 文件 | 内容 |
|---|---|---|
| 04 | 项目上下文文件 | AGENTS.md / CLAUDE.md 的层级、内容组织与模板 |
| 05 | 规则与记忆体系 | 多层规则、记忆系统、团队与个人配置的分离 |
| 06 | MCP 与工具接入 | MCP 协议、常用 server、凭据安全、按需启用 |
| 07 | 权限与 Hooks | 权限配置、allowlist、hooks 自动化守护 |
把可复用的方法论沉淀成代理"随取随用"的技能包。
| 章 | 文件 | 内容 |
|---|---|---|
| 08 | Skill 系统原理 | SKILL.md 结构、渐进式披露三级加载、触发机制 |
| 09 | 写出优秀 Skill | 写作方法论、描述的艺术、结构与资源组织 |
| 10 | Skill 调试与进化 | 触发诊断、评测、版本管理与团队 Skill 库 |
让代理"敢放手干"的前提是"它干不出格的事"。
| 章 | 文件 | 内容 |
|---|---|---|
| 11 | Agent 边界设计 | 边界类型、权限模式选择、最小权限原则 |
| 12 | 沙箱与隔离 | 工作目录隔离、Git 隔离、容器化、审批门 |
| 13 | Hooks 深度实践 | 拦截写入、凭据扫描、出网防护、审计日志 |
复杂需求一次说不清?拆,是唯一的方法。
| 章 | 文件 | 内容 |
|---|---|---|
| 14 | 任务拆分方法论 | 从需求到任务图、原子性、验收标准、依赖排序 |
| 15 | 计划先行与 Spec 驱动 | 计划模式、规格文档、先探后改 |
| 16 | 长任务与状态管理 | 待办清单、里程碑、上下文压缩、断点续作 |
从"一个代理"到"一支代理团队"。
| 章 | 文件 | 内容 |
|---|---|---|
| 17 | 多 Agent 架构模式 | 主从、流水线、评审者-实现者、扇出汇总等模式 |
| 18 | Subagent 实战 | 何时拆代理、自包含提示词、结果回传与并发 |
| 19 | Workflow 编排 | 脚本化编排、缓存复用、失败恢复 |
| 20 | 协同规范与治理 | 文件所有权、冲突避免、检查点与成本治理 |
把前六部分串成两条完整作战线。
| 章 | 文件 | 内容 |
|---|---|---|
| 21 | 实战一:从零构建 BookNest | 全流程演示:需求 → 配置 → 拆分 → 多 Agent 实现 |
| 22 | 实战二:重构遗留项目 | 接手陌生代码库:探索、边界收紧、渐进重构 |
| 23 | 反模式与踩坑大全 | 30 个高频失败模式及其解法 |
| 附录 | 文件 | 内容 |
|---|---|---|
| A | 术语表 | 全书术语速查 |
| B | 模板库 | AGENTS.md / SKILL.md / 任务卡 / Hooks 配置模板 |
| C | 检查清单 | 项目启动、日常开工、发布前、多 Agent 协同四张清单 |
| D | 贯穿项目需求书 | 「轻读 BookNest」产品需求与技术栈 |
git init 一个空仓库,从第 4 章开始把每章产物落进项目里,到第 21 章你会得到一个完整作品;2025 年,"vibe coding"(凭感觉编码)一词火遍开发者社区:用自然语言把需求说给 AI 听,代码由 AI 生成,人几乎不逐行审阅。它能让你一小时做出一个能跑的 demo,也能让一个项目在第三周腐化到无法维护。本章帮你建立正确认知:它是什么、边界在哪,以及本教程为什么要把你从 L2 推向 L3。
2025 年 2 月,OpenAI 联合创始人、前特斯拉 AI 总监 Andrej Karpathy 发文描述了一种全新的编码方式:完全交给感觉(give in to the vibes)、拥抱模型能力的指数级增长、甚至忘记代码本身的存在——看到什么就说什么,几乎不读代码,报错就原样贴回去让 AI 自己修,项目就这么"长"了出来。他给这种方式起名 vibe coding。
意译他的原话:"有一种新的编码方式……我'看什么说什么',几乎不真正读代码……我全盘接受所有 diff,报错了就复制粘贴回去,通常这样就能修好。"
短短几个月,这个词从玩笑变成行业热词:有人用它一周做出付费用户过千的小产品,也有资深工程师公开拒绝这种工作方式。为什么偏偏是 2025 年?三个条件同时成熟:模型一次能生成的代码量跨过了"整个功能"的门槛;代理工具让模型能直接读写文件、执行命令;对话成本降到可以"随便聊"的程度。争论的核心其实是一个问题:当生成代码的成本趋近于零,人的价值应该放在哪里?
vibe coding 有三个特征:
术语提示:社区里相近的说法还有 "AI pair programming"(AI 结对编程)等。本书约定:vibe coding 特指上述这种以行为验证为主的对话式工作方式;它的工程化升级形态叫 agentic engineering(见 1.2);两个词都不带贬义。
很多人把 vibe coding 和前几年的 AI 辅助编程(AI-assisted programming,代表形态是 GitHub Copilot 式的补全)混为一谈,其实两者方向相反:
| 维度 | AI 辅助编程(copilot 补全模式) | Vibe coding(对话生成模式) |
|---|---|---|
| 谁在写代码 | 人写,AI 补全 | AI 写,人指导 |
| 人的注意力 | 逐行阅读与修改 | 行为验证与方向把控 |
| 上下文单位 | 当前行 / 当前函数 | 当前功能 / 当前任务 |
| 人对代码的掌握 | 高(每行都过目) | 低(只看结果) |
| 典型失败形态 | 补全不对,删掉重写 | 行为不对,整段重问 |
一句话总结:辅助编程里人是作者;vibe coding 里人是产品经理兼验收员。 角色变了,技能结构也要跟着变。
2025 年 6 月,一篇 arXiv 论文在研究开发者使用 AI 生成代码的行为后,把这种状态命名为 material disengagement(对代码材料的疏离):开发者与代码这个"材料"脱钩了——不知道代码长什么样、为什么这样写、改哪里会出事。论文的结论并非"禁止 vibe coding",而是指出:疏离程度越高,交付质量的方差越大。本书后续所有方法,本质上都是在用工程手段控制这个方差,而不是否认这种新工作方式。
按人机分工的深浅,AI 编码可以分成三级。注意:这不是能力鄙视链,而是适用场景不同;同一个工具,用出哪种形态取决于你怎么用它。
AI 是一个更聪明的搜索引擎加自动补全。你写代码,遇到不会的问一句,或让它补一段。代码的主导权、结构、质量完全在你手里。典型形态:编辑器内补全、单轮问答。适合:对代码质量要求高、AI 只是"外脑"的日常开发。
你说需求,AI 生成整个文件甚至整个模块;你跑起来验证、看 diff(代码差异)、不满意就再对话。人把"写"交了出去,但"把关"仍然完全靠人肉——看 diff、跑应用、试边界。适合原型、脚本、学习实验。风险:把关质量完全取决于你读 diff 的耐心与能力,项目一大就会失效(下一节演示失效过程)。
人退到更上游,守四件事:
| 维度 | L1 问答 / 补全 | L2 Vibe coding | L3 Agentic engineering |
|---|---|---|---|
| 人机分工 | 人写 AI 补 | AI 写人验 | 人定规格与护栏,代理执行,人守检查点 |
| 质量来源 | 人的逐行把控 | 人对行为与 diff 的审查 | 规格 + 测试 + 权限边界 + 评审检查点 |
| 适用规模 | 片段、单函数 | 原型、脚本、小项目 | 中大型项目、长期维护、团队协作 |
| 主要风险 | 效率上限低 | 规模一大就失控 | 前期配置成本,需要学习方法论 |
本教程的目标:带你从 L2 走到 L3。注意,L3 不是"AI 用得更多、人用得更少",而是人把注意力从代码材料上移到规格与边界上——这是 material disengagement 唯一健康的打开方式。
自检结果没有高低贵贱:写一次性脚本时停在 L2 完全正确,做长期项目时还赖在 L2 就是事故前兆。
demo 只需要主流程(happy path)跑通;产品要面对非法输入、并发、数据增长、安全攻击、需求演进。这中间的距离,恰恰是"凭感觉"覆盖不到的部分——不是 AI 写不出产品级代码,而是你没有任何机制去确认它写出来的就是产品级。
展开说,产品至少要多扛住四类事:非法与恶意输入、数据量与并发增长、依赖与运行环境的变更、半年后接手的人(很可能是一个 AI 代理)。每多一类,"凭感觉"的覆盖率就掉一截。
假设你用一句 L2 式 prompt 起步:
你:帮我做一个读书笔记网页,可以添加书、写笔记、打 1–5 星。
AI:好的,我生成了单文件
app.html(内嵌 JS 与 localStorage),直接打开就能用。
第 1 天,确实能用,你很开心。然后:
第 3 天 · 改 A 坏 B。你说"笔记可以编辑了",AI 改了渲染函数。但编辑入口和删除按钮共用一套"按序号找元素"的逻辑,改完之后删除功能悄悄坏了——没有测试报警,你两天后才发现:
// 改动前:按钮按下标绑定,i 即数据下标
buttons.forEach((btn, i) => (btn.onclick = () => removeNote(i)));
// "加编辑"改动后:列表里插入编辑按钮,下标全部错位——
// 点删除,删掉的是隔壁那条笔记
第 5 天 · 依赖幻觉。你说"加个星级组件",AI 生成:
// 这个包在 npm 上并不存在——模型"编"了一个听起来很合理的名字
import StarRating from "react-star-rating-pro-max";
你花了一下午排查 Cannot find module。这就是依赖幻觉(hallucinated dependency):模型按命名规律编造不存在的库与 API。没有"安装结果为准"的核验流程,这类问题全靠人肉兜底。
第 7 天 · 会话失忆。你换了个新会话继续开发,AI 把评分字段从 rating 改成了 score——因为新会话里它看不到上一次的约定,只能猜。
第 14 天 · 技术债雪球(tech debt snowball)。此时评分逻辑在三个文件里各有一份,字段名 rating/score 混用,没有类型定义。你让 AI 重构,它每次都问"以哪份为准";你让它加功能,它又复制了一份。没有测试与类型约束的代码库,AI 的每次改动都在往雪球上滚一层——因为 AI 最可靠的依据就是"仓库里已有的样子":仓库烂,它就照着烂法继续烂。
经验法则(三轮法则):同一个问题在 L2 模式下重试三轮还没收敛,就该停下来——多半不是 prompt 不够好,而是任务缺计划、缺验证。这个信号会在第 3 章反复出现。
这三样东西,分别对应本教程的任务工程(Part 5)、边界与安全(Part 4)、项目配置(Part 2)。换句话说,崩坏不是"AI 不行"的证据,而是"没给 AI 立规矩"的证据。
读者常问:能不能先用 L2 跑起来,欠的债以后再还?可以,这正是 1.4 的"原型毕业制"。危险的不是欠债,而是不知道自己欠了债——没有验证、约束、记忆三件套,你连账本都没有。
任何一个答"是",就别裸奔。这份清单的用途不是吓退你,而是把"在哪里花纪律"变成一个显式决策——纪律是稀缺资源,要花在刀刃上。
大量任务落在中间:内部工具、活动页、阶段性版本。三条原则:
以"给 BookNest 加一个实验性的阅读热力图组件"为例:组件本身是叶子模块,可以放心 vibe;但它依赖的数据聚合接口属于核心链路,必须先按工程化标准补齐校验与测试,再让组件自由发挥。顺序很重要:先地基后叶子,而不是反过来。
本书开篇用"四堵墙"概括纯 vibe coding 撞上的主要障碍;展开后你会发现还有第五堵——沉淀墙:好经验用完即弃,换个项目重新踩坑。五堵墙与教程五个核心部分一一对应:
| 墙 | 典型症状 | 拆墙的部分 | 章节 |
|---|---|---|---|
| 上下文墙 | 每次对话都要重复解释项目约定 | Part 2 项目配置 | 第 4–7 章 |
| 沉淀墙 | 调好的方法无法复用,换项目归零 | Part 3 Skill 工程 | 第 8–10 章 |
| 边界墙 | 代理删文件、乱装依赖、密钥进代码 | Part 4 边界与安全 | 第 11–13 章 |
| 能力墙 | 任务一大,输出质量就崩 | Part 5 任务工程 | 第 14–16 章 |
| 协同墙 | 多个代理并行干活,互相踩文件 | Part 6 多 Agent 协同 | 第 17–20 章 |
地图的其余两块:
另一个观察视角:五个部分各有明确的"落盘产物"——Part 2 产出 AGENTS.md 与权限配置,Part 3 产出 Skill 包,Part 4 产出 hooks 与沙箱规则,Part 5 产出任务图与规格文档,Part 6 产出编排脚本与治理约定。这些产物最终都会沉淀进贯穿项目 BookNest 的仓库(见附录 D.5 的目标结构):跟着做完全书,你收获的不只是知识,还有一套可以直接复用的工程资产。
时间紧张走快速路线:01 → 02 → 03 → 14 → 15;技术负责人重点看:04 → 07 → 11 → 12 → 13 → 17 → 20,再配附录 B(模板库)与附录 C(检查清单)。路线没有优劣,只有取舍——Part 1 之后的每一章都可以独立查阅,把全书当工具书用。
本书所有章节都围绕贯穿项目「轻读 BookNest」(个人读书笔记与书评应用,完整需求见附录 D)展开。本章不写代码,只做一个开局对比:同一份需求,L2 与 L3 的开工方式差别有多大。
动手建议:先把附录 D 全文保存为本地文件(例如 docs/requirements.md),之后与代理的对话都以"先读这个文件"开场——文件化的需求是 L3 的入场券,第 4 章之后它还会升级为仓库里的常驻上下文。
你:帮我做一个读书笔记应用,能记书、写笔记、打分,用 React 做,越快越好。
AI:好的!我用 React + localStorage 给你生成了完整应用,包含书架、笔记、评分三个页面……
十分钟后你得到了一个能跑的单体应用,看起来效率碾压。但它埋了这些雷:
一句话 prompt 的问题不是"结果差",而是需求根本没有被表达——AI 只好替你猜。
L3 的第一步不是生成代码,而是让代理先读懂需求、给出计划、等你确认。下面是可直接复用的开工 prompt(附录 D 本身就是一份"可直接喂给 AI 代理"的需求书范例):
请先完整阅读我提供的需求书节选(见下),然后:
1. 用你的话复述产品愿景与 7 条功能需求(FR-1~FR-7),
并标出你不确定、需要我澄清的点;
2. 对照需求书的技术栈约定,列出你看到的风险与替代方案(如有);
3. 按里程碑 M0–M4 的划分,给出 M0(工程骨架)的具体任务清单,
每个任务附验收标准;
4. 在我确认计划之前,不要写任何代码。
【需求书节选】
- 产品:个人读书笔记与书评应用,单用户自部署,数据全量可导出
- 功能:FR-1 书籍管理 / FR-2 读书笔记 / FR-3 评分标签 / FR-4 阅读统计
/ FR-5 导入导出 / FR-6 全文搜索 / FR-7 灵感速记
- 非功能:API 输入校验与统一错误格式;SQL 一律参数化;密钥零字面量;
TypeScript 严格模式;Conventional Commits
- 技术栈:React 19 + Vite 7 + TS 5 / Express 5 / SQLite(better-sqlite3)
/ Vitest + Playwright
代理会先回一份"理解确认 + 风险清单 + 任务清单",例如:
我对需求的理解:……(复述略)
需要澄清:评分是否允许半星?封面 URL 失效时如何兜底展示?
M0 任务清单:
T1 初始化 monorepo(npm workspaces:server / web)
验收:npm ls --workspaces 可见两个包
T2 server 端 Express Hello + 健康检查接口
验收:curl /api/v1/health 返回 200
T3 web 端 Vite + React Hello
验收:npm run dev 可打开页面
T4 ESLint + Prettier + Vitest 跑通
验收:npm run lint 与 npm run test 全绿
(已暂停,等待你确认后再动代码)
你接着回答澄清问题、确认计划:
你:评分只支持整星;封面 URL 失效时展示占位图。计划确认,先做 T1 与 T4。
AI:收到。已按确认范围开始 T1……(此时它才会写下第一行代码)
注意整个过程的教学点:先复述、再澄清、后确认、最后动手。前面多花的十分钟,换来的是后面每一小时都走在正确的轨道上。
| 对比点 | L2 一句话 | L3 结构化 |
|---|---|---|
| 需求表达 | 靠 AI 猜 | 需求书 + 复述确认 |
| 技术决策 | AI 自作主张 | 对照需求书 + 风险讨论 |
| 第一个产出 | 一堆代码 | 一份计划与任务清单 |
| AI 的角色 | 猜需求的承包商 | 提方案的合伙人 |
| 后续成本 | 每一步都在还债 | 计划确认后直线推进 |
从第 2 章起,我们会真正把 BookNest 的仓库建起来,并在每章末尾持续演进它。
| 误区 | 典型现象 | 纠正方法 |
|---|---|---|
| 把 vibe coding 等同于"不需要会编程" | 报错只会整段重问,被 AI 的错误解释带着绕圈 | 保持能读懂核心逻辑的底线,人验收、AI 执行(见 1.2) |
| 认为能跑就等于能用 | demo 一遇非法输入或并发就崩 | 用 1.4 决策清单判定场景,转正前补校验与测试 |
| 所有任务一刀切用一句话 prompt | 复杂需求越改越乱、技术栈跑偏 | 按任务规模选 L1/L2/L3,复杂任务先计划后编码 |
| 完全不看 diff,全盘接受 | 幻觉依赖、硬编码密钥被合入仓库 | 至少人工过目敏感 diff(鉴权、资金、删除类) |
| 神化 L3,小事也要写规格 | 50 行的脚本也产出三份文档,效率倒挂 | 按规模选层级:规格粒度与任务风险成正比 |
| 把 L3 当成工具的"自动挡" | 买了代理工具就以为自动变强,配置从不演进 | L3 是工作方式:工具只是载体,规格与护栏靠人来建 |
npm view 核实,杜绝幻觉依赖;vibe coding 是 2025 年由 Karpathy 命名的一种对话式、凭直觉、少逐行审查的编码方式;学术界用 material disengagement 提醒我们它与代码材料脱钩的风险。它不是贬义词,而是一个分级明确的工作谱系:L1 补全问答、L2 对话生成、L3 代理工程。无纪律的 L2 在真实项目里必然撞上改 A 坏 B、依赖幻觉、技术债雪球;而 L3 用规格、测试、边界与编排把质量方差重新管起来。本教程的全部内容,就是带你完成从 L2 到 L3 的迁移。
练习 1–3 可在半小时内完成;练习 4 的产物请妥善保存,第 2 章会直接用到。
第 1 章把 AI 编码分成了 L1/L2/L3 三级,本章解决"用什么工具、懂哪些概念、怎么跑起来"三件事。读完并完成动手练习后,你的机器上会有一台可用的 AI 编码代理,和一个刚刚出生的 BookNest 仓库。
补全式工具回答的是"下一个词是什么";聊天式助手回答的是"这段代码怎么写";而**代理(agent)**回答的是"这个任务怎么完成"——你给它一个目标,它自己拆解步骤、调用工具、检查结果、修正方向,直到任务完成或需要你决策。
判断一个工具是不是代理,就看它有没有闭环执行能力:不靠人来搬运中间结果,自己把"想法"变成"副作用"(写文件、跑命令),再根据副作用的反馈调整下一步。补全工具离开你的手就瘫痪,代理可以自己走很远。
所有编码代理的引擎都是同一个循环(agent loop):
┌─> 观察(observe):读取环境反馈——文件内容、命令输出、测试结果
│ 思考(reason):决定下一步——该读哪个文件?改哪里?跑什么命令?
│ 行动(act):调用一个工具——读文件 / 写文件 / 执行命令 / 检索
└── 反馈回填:工具结果进入上下文,开始下一轮
用伪代码表达:
while not task_done:
observation = environment.feedback() # 上一步行动产生的真实结果
thought = model.reason(goal, context, observation)
action = model.pick_tool(thought) # 选定工具与参数
context.append(execute(action)) # 执行并把结果写回上下文
这个循环之所以强大,是因为每一步都有真实世界的反馈:写错了编译器会骂,测试红了它看得见,路径不存在命令会报错。第 1 章说的"自我修正",物质基础就在这里——代理不是更聪明了,而是它能看见自己犯的错。
| 能力 | 具体表现 | 没有它会怎样 |
|---|---|---|
| 读写文件 | 浏览项目结构、阅读代码、创建与修改文件 | 只能给你贴代码,由你负责搬运 |
| 执行命令 | 跑构建、测试、Git、包管理器 | 无法验证自己的改动对不对 |
| 联网检索 | 查官方文档、核对 API 签名、搜索报错方案 | 只凭训练记忆答题,容易过时与幻觉 |
| 自我修正 | 根据报错迭代,直到测试通过 | 一次性输出,错了等你开下一轮 |
四类能力齐备,才叫"能干活"的代理。而这些能力开关的松紧由谁、按什么规则控制,就是第 11 章"Agent 边界设计"要回答的全部问题。
还有一个容易被忽略的推论:四类能力对应四类授权,授权的粒度决定了风险的形状——只给读权限,最坏是信息泄漏;给了写权限,可能破坏文件;给了命令权限,可能影响进程之外的世界;给了联网权限,边界就延伸到了远端。想清楚"这次任务真的需要哪几类",是使用代理前 30 秒该做的事。
CLI 型(终端运行):ZCode、Claude Code、Codex CLI、Gemini CLI、Aider 等。直接操作本地文件系统与 shell,可脚本化、可接入 CI(持续集成,Continuous Integration)、可被其他程序调用。共同气质:把代理当作"能进你终端的同事"。
IDE 型(编辑器内嵌):Cursor、Windsurf、GitHub Copilot 等。代理能力内建于编辑器,diff 视图与光标位置天然融合,适合"手不离编辑器"的重度写码者。代价是权限与自动化机制受编辑器框架约束,自由度略低。
异步云型(云端执行):代码仓库托管的云端代理(任务派给云端环境,完成后回 PR,即 Pull Request)与 Devin 类全自主代理。适合批量处理定义明确的工单:升级依赖、修复 lint、小型 bug。本地资源零占用,但你对执行过程的实时掌控也最少。
| 维度 | CLI 型 | IDE 型 | 异步云型 |
|---|---|---|---|
| 本地文件访问 | 原生完整(文件系统 + shell) | 完整(经编辑器转发) | 通常在云端副本执行 |
| 权限控制 | 精细(允许清单、hooks 拦截均可配) | 中等(编辑器级确认) | 较粗(平台策略为主) |
| 可扩展性 | 强(MCP、Skill、hooks、脚本编排) | 中(以插件生态为主) | 弱~中(由平台决定) |
| 成本 | 按用量计费,可精打细算 | 订阅制为主 | 订阅或按任务计费 |
| 适合谁 | 自动化与工程化玩家(本书读者) | 手写为主、偶尔放权 | 批量工单、CI 集成 |
三个原因。其一,CLI 型能力最完整——文件、命令、网络三件事在本地闭环,最适合观察和理解 agent loop 本身;其二,权限、hooks、Skill 等扩展机制在 CLI 形态下最透明、可版本化(能进 Git、能进代码评审),它们正是 Part 2 与 Part 4 的主角;其三,多代理编排(Part 6)天然发生在终端与脚本的世界里。
本书以 ZCode 为主要示例环境,概念与 Claude Code、Codex CLI、Gemini CLI 高度互通,配置差异处会注明。换工具不需要重学本书:AGENTS.md、MCP、SKILL.md 都是跨工具的开放约定。
选型的第一问不是"哪个最强",而是**"我的任务发生在哪里"**:代码与工作流都在本地终端,选 CLI;你本人整天泡在编辑器里,选 IDE 或"IDE + CLI"组合;任务可以异步、可以云端复现,才考虑云型。工具可以混用——很多成熟团队正是"IDE 里写码 + 终端里放代理跑长任务 + 云端代理清工单"。
七个词决定你能不能看懂本书后面的所有章节。每个概念先给定义,再配一个类比,并标注它在本书哪一章展开。现在记不住没关系,它们会反复出现。
上下文窗口(context window)与 token 模型单次推理能"看见"的全部内容(系统提示、对话历史、文件内容、工具结果)总量有上限,计量单位是 token(词元,一个英文单词约 1.3 个 token,一个汉字约 0.5~1 个 token)。类比:一张固定大小的办公桌——桌上摊开的资料就这么多,放什么、何时撤走,是门手艺。→ 第 3 章《上下文工程》。
工具调用(tool use) 模型不止输出文字,还能按预定义的工具清单(含参数格式)发起调用请求,由代理框架真实执行,再把结果回填给模型继续推理。类比:从"只会说的顾问"变成"有手的助理"——它不再让你代抄,而是自己动手。→ 第 6 章《MCP 与工具接入》。
权限模式(permission modes) 规定代理的每个动作"直接放行、询问后放行、还是一律拒绝"的配置层,通常配合允许清单(allowlist)使用。类比:办公楼门禁——访客要陪同,员工刷工卡,机房要管理员加审批。→ 第 7 章《权限与 Hooks》(实操配置)与第 11 章《Agent 边界设计》(设计原则)。
MCP(Model Context Protocol) 连接模型与外部工具、数据源的开放协议:接入一个 MCP server,代理就多出一组现成工具(查数据库、控浏览器、读工单)。类比:USB-C 接口——工具与代理各自独立发展,插上就能用。→ 第 6 章。
Skill(技能) 把一类任务的方法论写成 SKILL.md 文档包,代理遇到相关任务时按需加载——平时只看"目录",干到那一步才翻"正文"。类比:新员工手里的操作手册,而不是要求他背在脑子里的培训内容。→ 第 8 章《Skill 系统原理》。
subagent(子代理) 主代理派生出的独立执行单元:拥有自己的上下文窗口,完成一件自包含的子任务后,只把结论回传给主代理。类比:项目经理外派专家——要诊断书,不要过程记录。→ 第 17 章《多 Agent 架构模式》。
hooks(钩子) 在代理生命周期的固定时点(如"写文件前""执行命令前")自动触发的脚本,可以拦截、改写或记录代理的动作。类比:机场安检加监控录像——危险品进不来,而且每一步都有据可查。→ 第 13 章《Hooks 深度实践》(第 7 章有上手配置)。
这七个概念并非彼此独立:上下文窗口是容器,工具调用是手脚,权限模式与 hooks 是闸门,MCP 与 Skill 是外挂装备,subagent 是分身。第 3 章我们先从容器讲起。
以 ZCode 为例(其余工具同理,具体命令以各自官方文档为准):
# 全局安装 ZCode CLI
npm install -g @zcode/cli
# 登录:会引导浏览器完成授权,不要在终端里粘贴任何密钥
zcode login
# 验证安装
zcode --version
安全底线:任何正规工具都不会要求你把 API 密钥写进代码或对话。密钥只应存在于环境变量(例如 ZCODE_API_KEY,代码里用 process.env.ZCODE_API_KEY 引用)或官方登录流程中。
三个常见的安装问题,提前给出排查方向:
node -v 先自查;不要一上来就在真实项目里试。建一个练习目录:
mkdir -p ~/agent-playground && cd ~/agent-playground
echo "console.log('hello agent')" > index.js
zcode
进入交互界面后,发出第一条指令:
> 看看这个目录里有什么,解释每个文件是干什么的
你会看到类似这样的过程(节选):
● 我先查看目录内容。
⏺ Bash(ls -la)
└ index.js package.json
● 有 package.json,读一下确认项目信息。
⏺ Read(package.json)
└ {"name": "agent-playground", ...}
结论:这是一个极小的 Node 练习目录:
- index.js:入口脚本,打印 'hello agent'
- package.json:npm 包描述文件,声明了项目名与元信息
上面这段输出包含三层信息,请养成逐层阅读的习惯:
Read、ls 的结果)——这是它推理的依据,依据错了结论必错;一个立刻可用的习惯:**看到
Bash(...)里的命令,先默念一遍这条命令的后果,再按确认。**这个习惯在第 7、11 章会被权限配置固化下来,但肌肉记忆从今天开始练。
主流 CLI 代理都允许切换底层模型。选型是三个维度的权衡:
经验法则:用任务的价值倒推模型档位——架构设计与复杂重构用旗舰档;问路、改名、写注释用轻量档;拿不准时用轻量档探路、旗舰档攻坚。另附三个省钱习惯:任务切换就开新会话(旧上下文会持续抬高每轮输入成本);长会话及时让代理总结状态或压缩历史(多数 CLI 提供 /clear、/compact 之类的内置命令);避免让代理无界重试——连续三次失败就该人来介入。
现在把第 1 章的"结构化开工"落到磁盘上:初始化 BookNest 的工程骨架,并让代理生成项目 README 与目录说明。M0 里程碑的完整落地在第 4–7 章,本章只做第一步:让代理认识这个项目。
mkdir booknest && cd booknest
git init
zcode
请初始化「轻读 BookNest」的工程骨架(个人读书笔记应用,需求书稍后完整提供,先按以下约定执行):
1. 根目录 package.json 使用 npm workspaces,包含 server 与 web 两个包;
2. server:Node 22 + Express 5,提供 GET /api/v1/health 返回 {"status":"ok"};
3. web:React 19 + Vite 7 + TypeScript,最小可启动即可,不写业务;
4. 根 README.md 包含三部分:产品一句话简介、目录结构说明(含每个目录的职责)、本地启动方式;
5. 完成后:列出你创建的全部文件,并给我一组验证命令。
注意:不要 git commit;不要安装约定之外的依赖。
代理会按顺序行动(节选,重点看每一步的"为什么"):
● 先确认环境里的 Node 与 npm 版本,避免给出不兼容的配置。
⏺ Bash(node -v && npm -v)
└ v22.x.x 10.x.x
● 创建根 package.json,workspaces 指向 server 与 web。
⏺ Write(package.json)
└ {"name": "booknest", "private": true, "workspaces": ["server", "web"]}
● 生成两端的最小工程文件。
⏺ Write(server/package.json) ⏺ Write(server/src/index.ts) …
⏺ Write(web/package.json) ⏺ Write(web/src/main.tsx) …
● 最后写根 README,内容基于你给的约定组织。
⏺ Write(README.md)
生成的根 package.json 应该长这样:
{
"name": "booknest",
"private": true,
"version": "0.1.0",
"workspaces": ["server", "web"],
"scripts": {
"dev": "npm run dev --workspaces --if-present",
"build": "npm run build --workspaces --if-present"
}
}
README 应包含一句产品定位("个人读书笔记与书评应用:数据永远在自己手里")、server/ 与 web/ 的职责说明、以及各自的启动命令。这份文档之后每章都会演进,到第 21 章它就是项目的门面。
完成后,仓库的骨架大致是:
booknest/
├── package.json # workspaces:server / web
├── README.md # 产品定位 + 目录说明 + 启动方式
├── server/
│ ├── package.json
│ └── src/index.ts # Express Hello + /api/v1/health
└── web/
├── package.json
└── src/main.tsx # Vite + React 最小入口
对照附录 D.5 的目标结构可以看到:AGENTS.md、docs/spec、migrations 等位置都还空着——它们是后续章节的产物,骨架不必一步到位。
逐条运行(这也是你以后给任何代理派活时应要求的"验收命令"):
npm install # 安装两个 workspace 的依赖
npm ls --workspaces # 应列出 booknest 下的 server 与 web
npm run dev -w server & # 启动后端后:
curl http://localhost:3000/api/v1/health # 应返回 {"status":"ok"}
确认无误后,做首次提交(提前演练 NFR-5 的 Conventional Commits 约定):
git add -A
git commit -m "feat: scaffold npm workspaces monorepo (server/web)"
提醒:此刻仓库还没有 AGENTS.md,也没有任何权限配置,代理处于"裸奔"状态——能顺利跑通,只是因为任务足够小、而你全程守在旁边。第 4 章与第 7 章会补齐这两块。
| 误区 | 典型现象 | 纠正方法 |
|---|---|---|
| 把代理当聊天机器人用 | 它口若悬河,却一个字都没落盘 | 授予文件读写与命令执行权限,并理解自己授权了什么 |
| 在真实项目里直接试新工具 | 权限一开,误改文件、误装依赖 | 先在沙箱目录演练(见 2.4 第二步) |
| 一个会话从早聊到晚 | 后半程答非所问、忘了约定,账单越来越贵 | 任务切换就开新会话,重要约定写进项目文件(第 4 章) |
| 只比模型单价 | 便宜模型反复返工,总成本反而更高 | 按"任务价值 × 返工成本"选档位(见 2.4 第四步) |
| 代理输出全盘相信 | 幻觉包名、编造的 API 直接进代码 | 让它当场核验:npm view、查文档、跑测试 |
| 上来就追求全自动 | 权限全开出了事故,从此不敢用 | 从默认"每步询问"开始,随信任与护栏逐步放权(第 7、11 章) |
本章把"AI 编码代理"从一个名词变成了你机器上可运行的现实:它是一个以 agent loop(观察 → 思考 → 行动 → 观察)为引擎、具备读写文件、执行命令、联网检索、自我修正四类能力的闭环系统。工具分 CLI、IDE、异步云三型;本书选 CLI 型为主线,因为它的能力最完整、扩展机制最透明、与工程化和多代理编排天然契合。七个核心概念(上下文窗口、工具调用、权限模式、MCP、Skill、subagent、hooks)是全书的词汇表,后续章节会逐一展开。最后,你在沙箱里跑通了第一条指令,并拿到了 BookNest 仓库的出生证明。
docs/prompts/01-scaffold.md(这是你的第一份 prompt 资产,第 8 章会把它升级成 Skill)。核心观点:AI 编码代理的产出质量,主要不取决于模型有多聪明,而取决于它"看到"了什么。把"给模型看什么"变成一门可操练的手艺——这门手艺叫上下文工程(context engineering)。
大语言模型没有任何"偷偷了解你项目"的渠道。每次生成回复,它能依赖的全部信息,就是此刻上下文窗口里的内容,通常由四部分组成:
AGENTS.md 等项目上下文文件(第 04 章的主角);窗口容量以令牌(token)计量——粗略估算,1 个 token 约合半个到一个汉字,或四个英文字符。2026 年主流编码模型的窗口普遍达到 200K–1M token,看起来"能装下半个仓库",但有两个反直觉的事实:
代理不会质疑你给的上下文,它默认收到的一切都是真的。于是"垃圾进,垃圾出"(Garbage In, Garbage Out)在 AI 编码里体现得比传统编程更彻底:
| 你喂进去的"垃圾" | 代理的典型反应 |
|---|---|
| 过期注释:"TODO: 之后迁移到 GraphQL" | 真的开始按 GraphQL 方向做设计 |
| 互相矛盾的约定(文档说 REST,旧代码是 RPC 风格) | 随机站队,或各写一半 |
| 贴了 3000 行无关日志 | 答非所问,抓着日志里的细枝末节发挥 |
| 半截代码片段(缺 import、缺类型) | 补全方式随机,风格与项目不符 |
反过来,高质量上下文会稳定地把代理推向正确答案。记住这句话:上下文的质量上限,就是代理输出的质量上限。
上下文工程(context engineering):围绕"模型此刻能看到什么"这个第一性问题,系统性地决定——给什么、不给什么、什么时候给、以什么形式给。
它比提示词工程(prompt engineering)的范围更大:单条指令怎么写只是其中一环,还包括文件怎么供给、会话怎么管理、约定怎么固化进项目配置。本教程后续的很多内容——第 04 章的项目上下文文件、第 05 章的记忆体系、第 16 章的长任务状态管理——本质上都是上下文工程在不同时间尺度上的应用。
一条高质量的开工指令,几乎总能拆出四个要素:目标(goal)、约束(constraints)、上下文(context)、验收标准(acceptance criteria)。缺任何一环,代理都会用"猜测"来补位——而猜测正是返工的最大来源。
目标要说清动作、对象、产出物,必要时给出接口或文件级的落点。
坏:"帮我加个搜索。"
搜索什么?在哪一层搜?返回什么?全靠猜。
好:"为书籍列表实现关键字搜索:GET /api/v1/books?q=关键字,
按书名和作者模糊匹配,结果按创建时间倒序返回。"
不设约束,代理会"好心办坏事":自作主张换技术方案、引入新依赖、顺手重构无关代码。
坏:"用最好的方案实现就行。"
好:"不引入任何新依赖;只改 server/src 下的文件;所有 SQL 用参数化查询;
遵循 routes/notes.ts 里已有的代码风格;不要动数据库 schema。"
约束的常见来源:技术选型、团队规范、安全要求(如"密钥一律走环境变量")、改动范围。
坏:(什么都不给)"开始吧。"
指望代理全仓库自己摸索:慢、贵、容易迷路。
好:"books 表结构见 server/src/db/schema.sql;
现有路由写法参考 server/src/routes/notes.ts,请先读它再动手;
入参校验复用 server/src/middleware/validate.ts 的现有中间件。"
没有验收标准,"做完了"三个字无从验证,返工全凭感觉。
坏:"做好一点,质量高一些。"
好:"完成标准:
1. `npm test -- server/tests/books.test.ts` 全部通过;
2. 用 curl 创建一本测试书后,GET 列表接口能返回它;
3. 传入非法状态值时返回 400 和统一错误格式;
4. `npm run lint` 无新增告警。"
写完指令后,用四个问题自检:**做什么?不做什么?你都知道些什么?怎样算做完?**四问都有答案,这条 prompt 就合格了。
四要素解决"单条指令"的质量;本节解决"项目信息"怎么喂。三种技巧:主动指路、噪音控制、分层供给。
两种策略各有适用场景:
ZCode 支持在输入中用 @ 引用文件或目录(Claude Code、Cursor 等也有等价的 @ 引用机制);另一种更通用的句式是"先读 X 再动手":
先读 server/src/db/schema.sql 和 server/src/routes/notes.ts,
理解现有的表结构与 REST 约定后,再动手实现 books 路由。
"先读再动手"的价值不只是省 token——它强迫代理先对齐现状、再输出方案,大幅降低"凭想象写代码"的概率。
注意力是稀缺资源。无关的大文件、整屏的日志、成段贴出的报错,都会稀释代理对关键信息的关注。三个实用做法:
npm test,读完失败的输出再修";@ 引用要克制:引 schema 和参考实现即可,不要引构建产物、生成文件或整目录;新手常犯两个相反的错误:要么开场就贴十几份文件(信息过载),要么全程不给任何背景(代理瞎猜)。正确姿势是分层:
【第一层示例】
项目是 Express + SQLite 的读书笔记应用,目录:
server/src(routes 路由 / services 业务 / db 数据库)、web/src(React SPA)。
本次任务只涉及 server/src,先从 books 路由开始。
上下文工程不是"一次写好就完",而是不断校准的循环。节奏错了,再好的 prompt 也救不回来。
把任务切成可独立验证的小片,每一片走完"生成 → 验证 → 下一步"的循环:
推荐节奏:
第 1 步:schema 与迁移脚本 → 验证:迁移能跑通
第 2 步:POST /books 接口 + 校验 → 验证:curl 一发 + 单测通过
第 3 步:GET /books 列表 + 搜索 → 验证:单测 + curl
第 4 步:前端表单与列表页 → 验证:浏览器走一遍主流程
反面教材是让代理"一口气把前后端全做完":得到 20 个文件的大改动,其中任何一处理解偏差,都会被后续十几处改动放大。
代理一旦开始往错误方向走,让它继续跑只会浪费 token,并制造更多要回滚的代码。正确动作:
停一下。目标仍然是"GET /books 支持关键字搜索",但你把搜索逻辑写进了
middleware,这不符合我们的分层(业务逻辑应放 services)。
保留你写的 SQL 参数化部分,把查询逻辑移到 services/books.ts。
最忌讳的反馈是连说"不对,重写"——那等于让代理随机再猜一个方向,猜中的概率并不会更高。
修好一个问题却弄坏另一个,是代理编码的高发场景。有效的反馈模板是四段式:
【复现步骤】1. npm run dev;2. 打开 /books 页;3. 点击任一书籍的"编辑"
【期望行为】弹窗内出现该书现有信息,修改后保存成功
【实际行为】弹窗空白,浏览器控制台报 "Cannot read properties of undefined"
【已尝试】重启 dev server 无效;直接访问 /books/3 详情页数据正常,
说明接口没问题,问题出在编辑弹窗的取数环节
四段各有用途:复现步骤让代理能自己验证;期望行为定义"对";实际行为定义"错";已尝试项帮它排除死路、缩小搜索范围。最后一项最常被忽略,但它最能避免代理把你已经排除的方案再试一遍。
出现以下任一信号,就值得结束当前会话:
开新会话不是"从头再来":带上三样东西即可无缝衔接——一份交接摘要(见 3.5)、本次任务的目标与验收标准、需要优先阅读的文件清单。干净的上下文加明确的摘要,几乎总比一个被污染的"长寿会话"更快。
会话越长,代理表现越可能滑坡。典型信号:
衰退不是模型的"性格缺陷",而是上下文里沉积了太多过时信息与失败尝试的必然结果。
不要等到会话崩溃才被迫开新会话。在状态良好时主动收尾,让代理自己生成交接文档(handoff):
这个会话快到头了。请生成一份交接文档,包含:
1. 本次会话的目标与最终完成状态;
2. 已完成的改动清单(文件 + 要点);
3. 当前代码处于什么状态(能否构建、测试是否通过);
4. 做过的关键决策及原因;
5. 未完成事项与建议的下一步。
存到 docs/handoff-2026-10-06.md。
一份合格的 handoff 大致长这样:
# Handoff · 2026-10-06 · FR-1 书籍管理(后端部分)
## 已完成
- books 表迁移脚本(migrations/002_books.sql)
- POST/GET/PUT/DELETE /api/v1/books 路由与校验,新增 9 个测试全绿
## 当前状态
- npm run build 通过;npm test 通过(32/32)
## 关键决策
- 状态字段用英文枚举 want_to_read/reading/finished,不用中文,避免编码问题
## 未完成 / 下一步
- 前端书籍表单与列表页(web/src/pages/BooksPage.tsx 尚未创建)
- 删除书籍未做关联校验(有笔记时应提示)
## 注意事项
- 封面 URL 字段允许为空,前端表单不要设 required
新会话开工只需一句:"先读 docs/handoff-2026-10-06.md,继续完成未完成事项。"
本章的手工流程——识别衰退、写 handoff、新会话续作——已经覆盖了长任务管理的核心思想。第 16 章(长任务与状态管理)将把它工程化:用待办清单(todo list)追踪进度、用里程碑切分会话、用上下文压缩(compaction)延续超长任务。现在先把肌肉记忆练出来,届时只是换更趁手的工具。
目标:为 FR-1(书籍管理)写一条四要素齐备的开工 prompt,在 ZCode 中一次跑顺。
第一步,写出这条 prompt(可直接复制使用):
【目标】
实现 FR-1 书籍管理:新增 / 编辑 / 删除书籍。字段:书名、作者、封面 URL
(可空)、分类、状态(want_to_read / reading / finished)。
后端提供 REST API(/api/v1/books),前端提供列表页与新增/编辑表单。
【约束】
- 技术栈限定 Express 5 + better-sqlite3 + React 19 + TanStack Query 5,
不新增任何依赖;
- 所有 SQL 参数化;所有入参经 server/src/middleware/validate.ts 校验;
- 只改 server/src 与 web/src;不动 migrations/ 里已有的迁移文件;
- 错误响应统一为 { code, message } 格式(对齐现有中间件)。
【上下文】
- books 表已建好:server/src/db/schema.sql;
- REST 风格参考 server/src/routes/notes.ts——请先读它再动手;
- 前端请求层:web/src/api/client.ts 已封装 TanStack Query。
【验收标准】
- `npm test -- server/tests/books.test.ts` 全部通过(含你新增的用例);
- curl 创建一本测试书后,GET /api/v1/books 能返回它;
- 状态字段传入非法值(如 status=banana)返回 400 与统一错误格式;
- `npm run lint` 与 `npm run build` 零错误;
- 封面 URL 为空时,表单与接口都能正常工作。
请先列出你计划修改的文件清单,我确认后再动手。
第二步,逐要素复盘它为什么这样写:
第三步,看一个反例及其恶果:
帮我给 BookNest 加上书籍管理功能,要能增删改查,顺便把样式做好看点,
尽量做得完善一些。
| 缺失要素 | 随之而来的恶果 |
|---|---|
| 目标模糊("完善一些") | 代理擅自实现封面图片上传到本地磁盘——需求只要一个 URL 字段 |
| 没有约束 | 执行依赖安装引入 ORM 和整套 UI 组件库,与项目"不新增依赖"原则冲突 |
| 没有上下文 | 没读 notes.ts,自创一套路由风格和错误格式,与现有代码格格不入 |
| 没有验收标准 | 声称"完成",实际是 15 个文件的 diff、零测试、构建报错 |
同一件事,好坏 prompt 的差距不是"表达水平",而是返工次数:前者通常一轮通过,后者往往三轮起步。
| 误区 | 典型现象 | 纠正方法 |
|---|---|---|
| 把上下文窗口当记忆 | 新会话里代理"忘了"上次的约定,你被迫从头解释 | 约定固化到 AGENTS.md 等项目文件(第 04 章),不依赖聊天历史 |
| 一条 prompt 塞十个需求 | 代理每件都浅尝辄止,或只做了前两件 | 一个会话一个主题;按验收标准切成小步交付 |
| 贴巨量日志当"证据" | 响应变慢变贵,代理抓着无关细节发挥 | 只贴关键报错行,或让代理自己运行命令取证据 |
| 跑偏后继续"再试一次" | 越修越乱,token 烧光,diff 无法收拾 | 立即中断,按"目标—偏差—保留项"重新对齐 |
| 反馈只有"不对,重写" | 代理随机换方向,可能换到更离谱的地方 | 用"复现步骤 + 期望行为 + 实际行为 + 已尝试项"四段式 |
| 验收标准缺失或不可执行 | 代理说"做完了",你不知道怎么验 | 每条任务配可执行验收:命令、预期输出、边界用例 |
本章建立了一个核心心智模型:模型只能看到上下文窗口里的内容,窗口会清空、注意力会稀释,所以"给什么信息"与"怎么给"直接决定产出质量。四要素(目标、约束、上下文、验收标准)让单条指令一次到位;主动指路、噪音控制、分层供给三种技巧解决项目信息的供给;小步快跑、跑偏即停、四段式反馈构成健康的迭代节奏;而识别衰退信号并主动交接,是长会话的生存技能。下一章我们把"每次都要重复的项目约定"固化进项目上下文文件,从根上消除本章反复出现的痛点。
/books 列表页的排序弄丢了。按 3.4.3 的四段式模板写出你将给出的完整反馈。上一章的结论是:代理只能看到窗口里的内容,会话结束即遗忘。那么"项目用什么技术栈、命令怎么跑、哪些文件不能碰"这些每次都要重复的事,就应该写进一个代理开机即读的文件。这个文件就是本章主角:AGENTS.md。
回想第 03 章的结论:会话结束即遗忘。没有项目级指令文件时,每个新会话你都要重新交代一遍:
我们用 Express 5 和 better-sqlite3,SQL 必须参数化,错误格式统一为
{ code, message },测试用 Vitest,不要动 migrations 里已发布的文件……
这段话的代价远不止"多打几行字":
项目上下文文件打破了这个循环:把约定写进仓库里的一份 Markdown 文件,代理每次启动会话时自动读取。你只需要维护文件本身,"向代理解释项目"的边际成本降为零。
AGENTS.md 是一份纯 Markdown 的开放约定,正迅速成为跨工具的事实标准(de facto standard):
AGENTS.md;CLAUDE.md,内容组织方式与 AGENTS.md 高度互通;CLAUDE.md 还支持 @路径 导入语法——写一行 @AGENTS.md 即可引用同一份文件,避免两份内容漂移;结论:团队共同约定写在 AGENTS.md 里,是当前兼容性最好的选择;个人全局偏好与工具私有配置再用各自机制补充(第 05 章展开)。
| 层级 | 位置 | 该放什么 | 是否进 Git |
|---|---|---|---|
| 项目根 | <repo>/AGENTS.md |
项目事实:技术栈、命令、约定、禁区 | 是(团队共享) |
| 子目录 | <repo>/web/AGENTS.md 等 |
该子目录特有的补充与覆盖 | 是 |
| 用户全局 | ~/.zcode/AGENTS.md(Claude Code 为 ~/.claude/CLAUDE.md) |
个人跨项目偏好:回复语言、提交习惯等 | 否(个人) |
BookNest 这类 monorepo(server + web 两个工作区)正是子目录层级的典型用户:
web/ 下的文件时,根目录与 web/AGENTS.md 叠加生效;booknest/
├── AGENTS.md # 全局事实:技术栈、命令、总体约定、禁区
├── server/ # (约定不多时可以不单设文件)
└── web/
└── AGENTS.md # 前端增量:组件、状态、样式约定
各工具的实现细节略有差异(请以所用工具文档为准),但方向一致:
AGENTS.md;AGENTS.md;由此推出两条纪律:
六类内容,每类配一段 BookNest 实例片段(最终组装版见本章"贯穿项目实战")。
1. 项目一句话简介——代理判断"这是什么项目"的第一锚点,也影响它对功能边界的取舍。
## 简介
个人读书笔记与书评应用(单用户自部署):记录书籍、写笔记、评分打标签、
回顾阅读轨迹。数据永远在本地。
2. 技术栈与关键版本——只写影响代码写法的关键项。不要复制 package.json(那是文档腐烂,见 4.4)。
## 技术栈
- 前端:React 19 + Vite 7 + TypeScript 5(严格模式)+ TanStack Query 5
- 后端:Node 22 LTS + Express 5,REST API 统一前缀 /api/v1
- 数据库:SQLite(better-sqlite3),库文件 data/booknest.db
3. 常用命令——代理执行验证的依据。必须真实可跑,写完自己执行一遍;尤其要写"怎么跑单个测试文件"。
## 常用命令
- 开发:npm run dev(并行启动 server:3000 与 web:5173)
- 全部测试:npm test
- 只跑某个测试文件:npm test -- server/tests/books.test.ts
- Lint / 格式化:npm run lint / npm run format
4. 代码风格约定——优先写"代理最常违反、且从现有代码里看不出来"的约定。
## 代码约定
- 所有 SQL 必须参数化,禁止字符串拼接
- 错误响应统一为 { code, message },经统一中间件返回
- Conventional Commits:feat:/fix:/chore:
5. 目录结构导览——几行就够,重点是"业务逻辑放哪"这类分层规则。
## 目录导览
- server/src/{routes,services,db,middleware}:路由只做参数解析,业务在 services
- web/src/{pages,components,api,hooks}:服务端状态一律走 TanStack Query
- migrations/:SQLite 迁移脚本,按序号递增
6. 禁区清单——安全硬约束和"血的教训"都放这里。必须具体到路径与动作,"别乱来"等于没写。
## 禁区
- 不要修改 migrations/ 中已应用的迁移文件(新增请递增序号)
- 不要引入新依赖;确需引入时先停下来说明理由,等确认
- 密钥零字面量,一律读 process.env.*
docs/,AGENTS.md 里只留一行链接;新项目不要追求一步到位,从这个模板起步,用起来再长:
# AGENTS.md — <项目名>
## 简介
<一句话:这是什么、给谁用、核心能力>
## 技术栈
<语言/框架/数据库/测试框架,只写影响写法的关键项与硬性版本>
## 常用命令
- 开发:npm run dev
- 测试:npm test
- 单个测试文件:npm test -- <path>
- Lint:npm run lint
## 代码约定
<3–5 条最重要的规则:命名、错误处理、安全要求>
## 禁区
<3–5 条:不许动的路径、不许引入的依赖、不许跳过的检查>
不想自己憋?让代理反过来采访你。在项目根目录开一个会话:
请先浏览这个仓库的结构和 package.json,然后向我提出 5–8 个问题,
你的任务是收集足够的信息,为这个项目起草一份 AGENTS.md。
问完后给出不超过 40 行的草稿。
它会问出类似这样的问题:"错误响应有没有统一格式?""迁移脚本的命名规则是什么?""哪些目录是生成产物不能改?"——这些问题恰好就是模型最需要知道的事,因为提问者就是消费者。你逐条回答后得到草稿,再做人工评审。这比闭门造车快得多,也准得多。
目标:落地两份成品文件——仓库根 AGENTS.md(约 40 行)与 web/AGENTS.md(约 20 行)。
文件一:booknest/AGENTS.md(仓库根)
# AGENTS.md — 轻读 BookNest
## 简介
个人读书笔记与书评应用(单用户自部署):记录书籍、写笔记、评分打标签、
回顾阅读轨迹。目标是"5 分钟部署,10 年不换",数据永远在本地。
## 技术栈
- 前端:React 19 + Vite 7 + TypeScript 5(严格模式)+ TanStack Query 5
- 后端:Node 22 LTS + Express 5,REST API 统一前缀 /api/v1
- 数据库:SQLite(better-sqlite3),单文件库 data/booknest.db
- 测试:Vitest(单元/接口)+ Playwright(端到端)
## 常用命令
- 开发:npm run dev(并行启动 server:3000 与 web:5173)
- 构建:npm run build
- 全部测试:npm test
- 只跑某个测试文件:npm test -- server/tests/books.test.ts
- Lint / 格式化:npm run lint / npm run format
## 代码约定
- TypeScript 严格模式,禁用 any(必要时用 unknown 加类型收窄)
- 所有 SQL 必须参数化;所有入参经 server/src/middleware/validate.ts 校验
- 错误响应统一为 { code, message },由统一中间件返回
- 路由只做参数解析,业务逻辑放 services 层
- 提交信息用 Conventional Commits(feat:/fix:/chore:)
## 目录导览
- server/src/{routes,services,db,middleware}:Express 后端
- web/src/{pages,components,api,hooks,styles}:React SPA
- migrations/:SQLite 迁移脚本,按序号递增
- e2e/:Playwright 端到端用例
## 禁区
- 不要修改 migrations/ 中已应用的迁移文件(新增请递增序号)
- 不要引入新依赖;确需引入时先停下来说明理由,等我确认
- 不要提交或修改 data/ 下的数据库文件
- 不要为了"让构建通过"而跳过 lint 或测试
- 密钥零字面量,一律读 process.env.*
各节存在的理由:"简介"锚定产品边界(单用户,所以别做多租户);"技术栈"的四条全部影响代码写法(如 TanStack Query 决定数据获取方式);"常用命令"是代理自我验证的依据,单测文件写法最容易被猜错所以单独列出;"代码约定"里的五条恰好是"从现有代码不易看出、代理最常违反"的规则;"目录导览"重点在分层规则(路由薄、services 厚);"禁区"前四条来自里程碑演进中的真实冲突点,最后一条是安全底线。
文件二:booknest/web/AGENTS.md(子目录增量)
# AGENTS.md — web/(前端补充约定)
根目录 AGENTS.md 的约定在本目录同样生效,本文件只写前端特有的增量。
## 补充约定
- 组件一律函数式 + Hooks;文件名 PascalCase,自定义 Hook 以 use 开头
- 服务端状态只走 TanStack Query(封装在 src/api/client.ts),
不要自建 fetch 缓存或全局 store
- 路由集中在 src/App.tsx 注册;页面组件放 src/pages/
- 样式用手写 CSS + CSS 变量主题(src/styles/theme.css),
不引入任何 UI 组件框架
- 用户输入的 Markdown/HTML 渲染前必须消毒,
禁止把未消毒内容直接传给 dangerouslySetInnerHTML
**为什么它如此之短:**每一条都只在"前端"语境下成立——放根目录会污染后端约定,不写又必然导致代理在前端引入 UI 库或自建状态管理(两者都是高频翻车点)。消毒一条对应需求书的安全要求 NFR-2。
落地后验证:提交这两份文件,开一个全新会话输入:"读一遍仓库根与 web/ 的 AGENTS.md,用 5 条要点复述你将遵守的约定。"核对复述内容与文件一致,再开始正式开发。
| 误区 | 典型现象 | 纠正方法 |
|---|---|---|
| 把 AGENTS.md 写成 wiki | 每次会话"背"几千字背景,响应变慢还抓不住重点 | 只留命令、约定、禁区;背景细节外链到 docs/ |
| 复制 package.json 里的版本与命令 | 依赖升级后文档说谎,代理按旧版本写代码 | 只写关键选型与硬约束,易变信息指向权威来源 |
| 全局文件里写项目细节 | 个人偏好污染所有项目,换台机器行为不一致 | 全局层只放个人习惯;项目事实进仓库共享 |
| 约定改了文件没更新 | 代理坚持过时约定,产出与代码库冲突 | "宁可短不可错";把更新文件写进完成定义 |
| 子目录文件重复根文件内容 | 两处漂移、互相矛盾,代理随机听谁的 | 子目录只写本目录特有的增量与覆盖 |
| 禁区只写"不要乱来" | 代理照样动它不该动的文件 | 禁区具体到路径与动作,一条一个行为 |
项目上下文文件把第 03 章反复出现的痛点——"每个会话都要重新解释项目"——变成一次性的配置投入。AGENTS.md 已成为跨工具的事实标准,Claude Code 用户用 CLAUDE.md 承载同样的内容约定;文件分仓库根、子目录、用户全局三层安放,冲突时就近优先。内容上坚持六类(简介、技术栈、命令、约定、导览、禁区)与"短、准、可执行"三原则,警惕 wiki 化与文档腐烂两大反模式。起草时用"代理提问法",评审时用检查点,保鲜靠完成定义与定期修剪。至此,代理已经"开机即懂"你的项目;下一章讨论规则与记忆体系——把团队约定与个人偏好进一步分离,并让代理拥有跨会话的记忆。
第 04 章解决了"开机即懂":一份 AGENTS.md 让代理开机即掌握项目事实。但用起来你会发现两个新问题:其一,约定开始分家——"SQL 必须参数化"属于团队,"回复用中文"只属于你自己,混在一个文件里会互相污染;其二,代理在会话中"学到"的东西(你的纠正、你的习惯),会话一结束又丢了。本章给出解法:用分层规则回答"谁该遵守什么",用记忆系统让代理"越用越懂你",并守住两者的边界。
第 04 章把指令文件分为仓库根、子目录、用户全局三层。本章从治理视角把它细化成四层——关键变化是:项目层一分为二,共享的进 Git,个人的不进。
| 层 | 典型位置 | 定位 | 进 Git? |
|---|---|---|---|
| 全局个人层 | ~/.zcode/AGENTS.md(Claude Code:~/.claude/CLAUDE.md) |
跨项目的个人偏好:回复语言、沟通风格、个人工作流 | 否 |
| 项目共享层 | 仓库根 AGENTS.md + 规则目录(如 .zcode/rules/) |
团队共识:技术栈、命令、质量约定、禁区 | 是,走代码评审 |
| 项目个人层 | AGENTS.local.md(Claude Code 一类工具:CLAUDE.local.md 或本地设置) |
本机专属:本机路径、端口、个人微调 | 否(.gitignore 声明) |
| 子目录层 | web/AGENTS.md 等 |
该子目录特有的增量与覆盖 | 是 |
规则目录(rules directory):当共享层规则增多且主题聚集(安全、API 设计、提交规范),可以拆成目录里的多个小文件,由主文件引用。一些工具原生支持规则目录(如 Cursor 的 .cursor/rules/),ZCode 支持 .zcode/rules/ 目录并在 AGENTS.md 中引用,被引用的文件随主文件一并加载。按需拆分,不要一开始就建目录。
会话中四层的加载顺序:
多层内容冲突时,越具体、越靠近当前工作文件的层级优先(就近原则,与第 04 章一致):
子目录层 > 项目个人层 > 项目共享层 > 全局个人层
一个冲突示例:全局层写着"我习惯先写测试再实现",项目共享层写着"原型验证类任务可先实现、当天补测试"——在这个项目里,代理应以共享层为准,因为它更具体、且代表团队对"完成"的定义。
由此得到两条纪律:
| 内容示例 | 该放哪层 | 判断依据 |
|---|---|---|
| 回复用中文、先结论后展开 | 全局个人层 | 纯个人沟通偏好,与项目无关 |
| 我习惯 TDD(先写测试再实现) | 全局个人层 | 个人工作流,不该强加给同事 |
SQL 必须参数化、错误格式 { code, message } |
项目共享层 | 团队质量底线,对谁都成立 |
| 新增路由必须附带接口测试 | 项目共享层 | 团队流程约定(本章实战的晋升示例) |
| web/ 组件命名与状态管理方式 | 子目录层 | 只在前端目录成立 |
| 我本机 dev 端口改用 5174(5173 被占) | 项目个人层 | 只与本机环境有关 |
| 本机数据库文件的绝对路径 | 项目个人层 | 机器相关,进 Git 必错 |
密钥一律读 process.env.* |
项目共享层 | 安全硬约束,全团队一致 |
一个补充判据:能交给工具的,别写进任何层。缩进、引号、行宽这类纯格式问题,交给 Prettier/ESLint 的配置文件(进 Git)解决,规则文件只写工具管不了的事——机器能强制的,就不要靠代理自觉。
换一个同事、换一台机器,这条规则还应该生效吗? 是——进项目共享层;否——进个人层(全局个人层或项目个人层)。
两类内容要区别对待:
对照几个易混的例子:
| 看起来像 | 实际是 | 原因 |
|---|---|---|
| "提交信息要写清影响范围" | 团队共识 | 评审者要读,对所有人生效 |
| "我说话喜欢极简,别寒暄" | 个人偏好 | 只影响与"我"的会话体验 |
| "测试覆盖率 ≥ 80%" | 团队共识 | 来自需求书 NFR-1,是质量底线 |
| "我喜欢先看 diff 再跑测试" | 个人偏好 | 个人工作节奏,无对错 |
假设你把"我喜欢每行不超过 80 字符"写进了团队的 AGENTS.md。从评审视角看,这行字造成四重伤害:
一次典型的评审拒绝:
## 代码约定
- 所有 SQL 必须参数化
- 我偏好双引号与 80 列行宽,代理写代码时请注意
Reviewer 意见:格式问题请改 Prettier 配置(进 Git,机器强制);个人习惯请放
AGENTS.local.md(不进 Git)。
.gitignore 写明 AGENTS.local.md,提交前瞄一眼 git status;多数现代代理都有自动记忆(memory)机制:把会话中"值得跨会话保留的信息"——项目事实、你的纠正、重复出现的偏好——沉淀到本机记忆文件,并在后续会话中读取。各工具实现细节不同(存放位置与触发策略以所用工具文档为准;ZCode 记忆存于本机用户/项目目录,Claude Code 亦有 memory 机制),但模式一致:
一个典型片段:
你:别再用 var 了,这个项目一律 const/let。
代理:明白。已记住:本项目一律使用 const/let,不用 var。
记忆和 AGENTS.md 看似重复,实则分工明确:
| 维度 | AGENTS.md(宪法) | 自动记忆(工作笔记) |
|---|---|---|
| 谁维护 | 人工显式维护 | 代理自动维护 |
| 进版本库 | 是,走评审 | 否,本机私有 |
| 权威性 | 冲突时的最终依据 | 仅供参考,可能过时 |
| 内容 | 当前有效的规则与事实 | 会话中沉淀的观察与偏好 |
| 生命周期 | 随项目演进、定期修剪 | 随会话累积,需晋升或清理 |
判断口诀:每个人该知道的进宪法,只有我在意的留在笔记。
要警惕记忆的两个天然缺陷:它靠归纳得出,可能以偏概全(你一次例外的要求被当成永久规则);它不会自动过期,可能悄悄过时(项目早已改用新命令,记忆里还是旧的)。
反复出现的好实践不该永远躺在记忆里——换台机器就丢,同事也享受不到。把它"晋升"(promotion)进规则层,五步流程:
症状:代理固执地使用旧命令、旧路径,或引用你早已推翻的决定。处理三步:
> 列出你关于本项目的全部记忆条目,标出与 AGENTS.md 或当前代码冲突的项。
(你逐条核对)
> 删除第 2、5 条;第 3 条改成"迁移文件已推进到 0007"。
原则:把记忆当缓存(cache),把 AGENTS.md 当权威源(source of truth)——缓存可疑时,先对源,再清缓存。
规则层回答"永远要怎么做",记忆回答"你大概是怎么样的"。还有一类信息两者都不适合承载:这一阵子在做什么、为什么这么定。三个轻量手段,点到为止(第 16 章展开):
| 手段 | 解决什么问题 | 放哪 | 谁维护 |
|---|---|---|---|
| 交接文档(handoff) | 会话之间"上次做到哪、下一步、有什么坑" | docs/handoff/ 等任务目录 |
代理生成、人工确认 |
| 决策日志(轻量 ADR) | "为什么这么设计"的来龙去脉 | docs/decisions/NNN-标题.md |
人工主导 |
| 任务进度文件 | 长任务/多会话的状态与验收 | docs/tasks/ |
代理维护 |
> 本次会话到此为止。请生成交接文档到 docs/handoff/:
已完成的改动、未完成项与原因、下一步建议、需要注意的坑。不超过 30 行。
目标:配齐 BookNest 的三层规则(第 04 章已完成共享层的根文件与 web/ 增量),并完整演练一次记忆晋升。
文件一:全局个人层 ~/.zcode/AGENTS.md(约 14 行)
# 个人跨项目偏好(仅本机生效,不进任何仓库)
## 沟通
- 一律中文回复;先给结论,再给理由;不要客套开场白
- 标识符命名用英文,代码注释用中文
## 工作流
- 我习惯 TDD:先写失败测试,再写实现(项目文件另有约定时从项目)
- 改动超过 3 个文件前,先列出改动清单等我确认
## 环境
- 我的开发机是 Windows + Git Bash,命令优先给跨平台写法
文件二:项目个人层 booknest/AGENTS.local.md(不提交)
# AGENTS.local.md — 本机专属(勿提交)
- 我本机 5173 端口常被占用:dev 启动用 npm run dev -- --port 5174
- SQLite 图形查看用我本机的 DB Browser,库文件在 data/booknest.db
- 新端点我习惯先用 curl 打 localhost:3000 冒烟,再写接口测试
- 不要读取或修改我本机的 .env.local(个人调试配置);密钥仍一律走环境变量
.gitignore 追加一行,确保它永远不进版本库:
AGENTS.local.md
文件三:共享层规则目录 booknest/.zcode/rules/security.md
背景:随着 M0 推进,安全条目从一个涨成一簇,根文件开始超重。按主题拆出规则文件,主文件留一行引用:
# 安全细则(由根 AGENTS.md 引用,随主文件一并加载)
- 密钥零字面量:一律读 process.env.*,.env* 文件不提交
- 所有 SQL 必须参数化,禁止字符串拼接(排序字段用白名单映射)
- 用户提交的 Markdown/HTML 渲染前必须消毒,
禁止未消毒内容进入 dangerouslySetInnerHTML
- 入参校验统一走 server/src/middleware/validate.ts,
错误经统一中间件以 { code, message } 返回
- 后端发起的对外请求必须做域白名单校验(SSRF 防护),
仅允许 https 与白名单内域名
- 引入新依赖前先停下说明来源与理由,等人工确认
根 AGENTS.md 的"禁区"一节相应改写(其余条目不变):
## 禁区
- 安全相关约束见 .zcode/rules/security.md(全文加载,与本节同等效力)
- 不要修改 migrations/ 中已应用的迁移文件(新增请递增序号)
- 不要为了"让构建通过"而跳过 lint 或测试
演练:一次完整的记忆晋升
背景:连续三个会话,你都让代理在新增路由后补接口测试。它的记忆里已经记下这一点。
第 1 步,调取:
> 列出你关于"测试流程"的记忆条目。
代理:共 2 条:
1. 用户新增路由后,总会要求在 server/tests/ 补对应接口测试(已出现 3 次)
2. 用户还总要求先跑通既有测试再提交
第 2 步,裁决。你注意到条目 1 混了两类内容:"必须有测试"是团队规则(共享层),"先写测试再实现"是你的个人节奏(个人层)。拆开处理。
第 3 步,落盘共享层(团队项目走 PR;个人项目直接提交):
## 代码约定(新增一条)
- 新增或修改路由必须附带 server/tests/ 下对应接口测试,全部通过才算完成
第 4 步,个人偏好留个人层(AGENTS.local.md 追加):
- 我习惯先写接口测试再写路由实现(TDD 节奏);团队要求是"必须有测试"
第 5 步,清源:
> 我已把"路由必须附带接口测试"写进根 AGENTS.md 的代码约定。
请删除记忆里对应的条目,避免双源漂移。
代理:已删除记忆条目 1;条目 2 与新约定重复,一并删除。
现在这条约定以 AGENTS.md 为准,你的 TDD 习惯保留在 AGENTS.local.md。
验收:开一个全新会话,问"新增一个路由时你要做什么?"——回答应引用 AGENTS.md 的新约定,而不是"我记得你总要我补测试"。前者说明行为由文件驱动,后者说明还挂在私人记忆上。
| 误区 | 典型现象 | 纠正方法 |
|---|---|---|
| 个人口味写进共享层 | 同事的代理按你的双引号产出,PR diff 充满噪音 | "换个人还成立吗"测试;格式问题交给 formatter |
| 把记忆当真理 | 代理坚持用已被推翻的旧命令、旧路径 | 记忆是缓存、AGENTS.md 是源;定期晋升与清理 |
| 项目个人层被提交 | 本机路径与调试配置泄漏进仓库历史 | .gitignore 声明;提交前看 git status |
| 什么都塞进 AGENTS.md | 宪法长成回忆录,关键约定被稀释 | "为什么"进决策日志,进度进任务文件 |
| 记忆里存敏感信息 | 代理把令牌写进记忆文件并长期保留 | 明确禁止密钥入记忆;定期审查记忆文件 |
| 层级越多越好 | 规则散落四层互相矛盾,行为不可预测 | 能一层解决不拆两层;冲突时就近优先要可查 |
.gitignore;本章把第 04 章的单一指令文件扩展成一套分层治理体系:全局个人层装跨项目偏好,项目共享层(主文件加规则目录)装团队共识,项目个人层装本机专属配置,子目录层装目录增量,冲突时就近优先。团队与个人的分界用"换个人还成立吗"一句话裁决:共享层走评审、个人层不进 Git,格式问题交给工具。记忆系统让代理跨会话记住项目事实与你的偏好,但它是"工作笔记"而非"宪法"——权威永远在人工维护的 AGENTS.md;反复出现的好实践按五步晋升进共享层,可疑记忆及时清理。交接文档、决策日志、任务进度文件补齐"上次做到哪、为什么这么定"的持久化。至此代理既懂项目、也懂你;下一章换一个维度扩展——给它接入更多工具能力。
git status --ignored | grep AGENTS 确认 AGENTS.local.md 已被忽略、不会进入提交。前两章解决的是代理"懂不懂":规则让它开机即懂项目,记忆让它越用越懂你。但有些事不是"懂"能解决的——代理生来只有读写文件、跑命令这类内置工具,它没法直接读你的 GitHub issue、查数据库、开浏览器点一遍页面。本章讲 Model Context Protocol(MCP):给代理"装上新器官"的标准方式,以及配套的凭据安全与克制原则。
MCP(Model Context Protocol,模型上下文协议)是 Anthropic 于 2024 年 11 月开源的开放协议,用于标准化 AI 应用与外部工具、数据源之间的连接。到 2026 年,它已是行业标准:OpenAI、Google 等主流厂商相继采用,常见编码代理(ZCode、Claude Code、Codex CLI、Cursor、Gemini CLI 等)均支持。
在 MCP 之前,工具方要为每种代理各写一套私有插件;MCP 之后,工具方实现一次协议,任何支持 MCP 的代理都能即插即用。一个常用的类比:MCP 是 AI 工具生态的 USB-C 接口——接口统一之后,"接入"这件事从项目工程变成了配置问题。
一个 MCP server(服务器)把一类能力封装起来,对代理暴露统一接口。最常用的是 tools(工具)——可执行的动作,如"列出 issue""执行查询";协议还定义了 resources(可读取的资源)与 prompts(提示模板)。工作流程:
工具在会话中通常以"server 名 + 工具名"的形式被引用(如 github server 下的 list_issues)。你在对话记录里看到 mcp__github__list_issues 这类标识,指的就是它。
对代理而言,MCP 工具与内置工具在决策时地位相同;对你而言,体验是"代理多会了几件事"。
决定是否接入前,先问一句:这是能力问题还是知识问题?
类比:MCP 是装到机器上的电动工具,Skill 是老师傅的操作手册。先确认缺的是工具,再谈手册。
以下按生态中的常见类别给出选型参考(同一类别常有官方与社区多个实现,按维护活跃度与权限模型挑选):
场景到 server 的对照表:
| 你的场景 | 优先考虑 | 备注 |
|---|---|---|
| 代理按 issue 开发、开 PR | GitHub server | 配合细粒度令牌(6.4) |
| 验证页面交互、调试 E2E | Playwright server | 临时启用,用完即关(6.5) |
| 让代理自查数据库结构与数据 | 数据库 server | 只读连接是硬要求 |
| 依赖文档比模型知识新 | 文档 / 检索 server | 减少版本幻觉 |
| 读仓库外的固定资料目录 | filesystem server | 只映射必要目录 |
| 团队任务流转在 Jira / Linear | 项目管理 server | 权限给到"读写自己的任务"即可 |
同一类别常有多个实现,挑选时看三个观察点:
以 ZCode 为例,MCP 配置是一个 JSON 文件(Claude Code、Cursor 等同构,字段名可能略异):
{
"mcpServers": {
"docs-search": {
"command": "npx",
"args": ["-y", "@some-org/docs-mcp", "--sources", "react,vite"],
"env": { "DOCS_API_KEY": "${DOCS_API_KEY}" }
}
}
}
command + args:启动本地 server 进程的命令与参数(stdio 传输);env:注入给该进程的环境变量;${VAR} 写法表示引用你 shell 里的环境变量(主流工具支持展开,具体语法以文档为准);url(认证用 headers),见 6.3.3。| 作用域 | 位置 | 适合放什么 | 进 Git? |
|---|---|---|---|
| 全局 | 用户目录下的 MCP 配置(如 ~/.zcode/) |
跨项目通用:文档检索、浏览器自动化 | 否 |
| 项目 | 仓库根 .mcp.json |
项目专属:本仓库的 GitHub、SQLite server | 是,走评审 |
| 本机覆盖 | 不进 Git 的本地配置文件 | 个人路径、本地端口等变体 | 否 |
项目级 .mcp.json 可提交共享——团队成员进入项目即获得同一组工具,这是它相对全局配置的核心价值。多数工具在首次加载项目配置时会请求确认(防止陌生仓库偷偷带入恶意 server),对来路不明的项目配置不要随手点允许。若全局与项目配置出现同名 server,以项目配置为准——与规则层的"就近优先"同理。
env 注入,不落盘到配置;安全提示一句话:远程端点能以你的凭据访问账号资源,务必核对域名是官方域名再配置。
zcode mcp list # 列出已配置的 server(命令名以所用工具文档为准)
也可以用 CLI 直接添加(等价于手工编辑配置文件,语法以工具文档为准):
zcode mcp add booknest-db -- npx -y @modelcontextprotocol/server-sqlite --db-path data/booknest.db
会话内输入 /mcp 可查看已加载的 server、各自工具数与连接状态。临时不需要的 server 在配置中禁用(多数实现支持禁用开关,或按会话选择启用),需要时再打开——这是 6.5 克制原则的落地手段。
先看错误示范——把令牌字面量写进配置再提交,仓库历史里就永远有了这个令牌,吊销前人人可读:
"headers": { "Authorization": "Bearer <your-token>" }
正确做法:令牌只存在于你的 shell 环境里,配置文件只留引用:
# 个人 shell 配置(~/.bashrc 或 ~/.zshrc,不进任何仓库)
export GITHUB_PERSONAL_ACCESS_TOKEN="<your-token>"
"headers": { "Authorization": "Bearer ${GITHUB_PERSONAL_ACCESS_TOKEN}" }
提交前的自检(配置里不应出现任何令牌字面量):
grep -nE "ghp_[A-Za-z0-9]+|github_pat_" .mcp.json \
&& echo "发现泄漏!" || echo "OK:无字面量"
MCP server 拿到的账号权限,就是代理拿到的影响力——按最小权限原则(least privilege,第 11 章展开)发放:
GRANT SELECT;SQLite 没有账号体系,靠只读连接或只读文件保障,见本章实战);git log -p -- .mcp.json 检查配置变更历史,确认没人把凭据带进过提交;MCP 不是越多越好。接入前算三笔账:
npx 首次还要下载包,冷启动拖慢开场。算一笔具体的账:一个 GitHub server 暴露 20 个工具,每个工具的名称、描述与参数模式合计约 150 token,仅它一家就常驻约 3000 token;若同时挂着文档检索(15 个工具)与浏览器(10 个工具),还没写第一行代码,窗口里已躺着近 7000 token 的"工具说明书"。
接入前的决策顺序:
内置工具(读写文件、跑命令)能完成吗?
├─ 能 → 不接。把步骤写成说明或脚本即可
├─ 不能,但只是一次性任务 → 让代理写个一次性脚本完成,同样不接
└─ 不能,且是高频、需要结构化访问的外部系统 → 接对应 server,用完即关
"按需启用、用完即关":浏览器调试、数据库检查这类阶段性需求,平时禁用,任务窗口内再启用。团队共享的 .mcp.json 同样要克制——每加一个 server,都值得在评审里问一句"当前任务真的需要吗"。反过来的信号也成立:某个 server 三个月没被任何会话调用过,就是移除它的时候了。
目标:为 BookNest 接入两个 server——GitHub(issue 工作流)与 SQLite(只读检查),并验证凭据与只读约束。
第 1 步:准备令牌(零字面量)
在 GitHub 上创建 fine-grained personal access token:仅选择 booknest 仓库,权限只给 Issues: Read and write 与 Contents: Read。写进个人 shell 配置(不进任何仓库):
export GITHUB_PERSONAL_ACCESS_TOKEN="<your-token>"
第 2 步:写项目级配置 booknest/.mcp.json(提交共享)
{
"mcpServers": {
"github": {
"type": "http",
"url": "https://api.githubcopilot.com/mcp/",
"headers": {
"Authorization": "Bearer ${GITHUB_PERSONAL_ACCESS_TOKEN}"
}
},
"booknest-db": {
"command": "npx",
"args": [
"-y",
"@modelcontextprotocol/server-sqlite",
"--db-path",
"data/booknest.db"
]
}
}
}
两点说明:GitHub 走远程 HTTP(官方端点,已核对域名);SQLite 走本地 stdio。SQLite 没有账号体系,只读必须靠 server 能力或文件权限保障:优先选择支持只读开关或只读连接串(如 file:...?mode=ro)的实现,或在只读副本上做检查;用 PostgreSQL/MySQL 时则直接建只读账号。没有只读能力的实现,不要用于正式库。
第 3 步:提交前自检
grep -nE "ghp_[A-Za-z0-9]+|github_pat_" .mcp.json \
&& echo "发现泄漏!" || echo "OK:无字面量"
# 顺带确认数据库文件不会被提交(落实第 04 章禁区的机器化检查)
grep -n "^data/" .gitignore
git add .mcp.json && git commit -m "chore: add MCP servers (github, sqlite)"
第 4 步:验证加载
zcode mcp list
再开一个会话输入 /mcp,应看到两个 server 及其工具数。让代理复述一遍,确认工具真的可用:
> 列出你当前可用的 MCP 工具,按 server 分组,只列名称。
第 5 步:验证只读(硬门槛)
> 用数据库工具查一下 books 表现有多少行;然后尝试向 books 插入一行测试数据。
代理:SELECT 返回 42 行;INSERT 被拒绝(连接为只读)。
INSERT 没有被拒绝?停下,回到第 2 步改配置——只读验证不过关,这个 server 就不允许日常使用。
第 6 步:issue 工作流
> 用 GitHub 工具列出本仓库所有 open 的 issue,按标签分组,每个一句话概括。
代理:共 3 个 open issue:
- #12 feat: FR-1 书籍管理 API(milestone: M1)
- #14 docs: 数据导入导出规格草稿
- #17 chore: e2e 登录态复用
> 认领 #12。先读 issue 正文与验收标准,给出只动 server/ 的实现计划,等我确认。
(确认后:代理建分支、实现、跑通接口测试、开 PR,并在描述里写 "Closes #12")
> 看一下 #12 的 PR 状态,CI 全绿就合入,然后把 issue 的里程碑更新为 M1。
第 7 步:克制检查
本次只接了当前里程碑需要的两个 server。做端到端测试之前并不需要 Playwright——到那时再临时启用,用完即关。今后每新增一个 server,都重复第 3、4 步的自检与验证。
| 误区 | 典型现象 | 纠正方法 |
|---|---|---|
令牌字面量写进 .mcp.json 并提交 |
密钥进入仓库历史,扫描器报警 | 环境变量注入 ${VAR};提交前 grep 自检 |
| 数据库 server 用管理员账号 | 代理一句 UPDATE 清空数据 | 只读账号/只读连接;接入先做 INSERT 拒绝验证 |
| 一次接十几个 server | 上下文被工具描述淹没,响应变慢还选错工具 | 只接当前任务需要的两三个,按需启用 |
| 全局配置堆满所有 server | 每个项目背着无关能力与启动开销 | 项目专属能力放项目级 .mcp.json |
| stdio 命令写死个人绝对路径 | 同事机器上配置跑不起来 | 用 npx -y 等跨机器写法,个人路径走本机覆盖 |
| 把"知识型需求"也用 MCP 解决 | 接了一堆 server,流程还是乱 | 先分清能力问题(MCP)与知识问题(Skill/规则) |
.mcp.json 并走评审,跨项目通用的放全局;${VAR}),提交前用 grep 自检零字面量;MCP 把"给代理装新能力"从私有插件工程变成标准配置:server 暴露工具,代理启动时发现、按需调用,主流编码代理均已支持。选型上牢记分工——MCP 给能力,Skill 给知识;常用 server 覆盖文件、Git/GitHub、浏览器、数据库、检索与项目管理,但每接一个都要付出上下文、误调用面与启动开销,因此"内置够用就别加、按需启用、用完即关"。配置上区分全局、项目、本机三个作用域,command/args/env 描述本地 stdio 进程,url/headers 描述远程端点;凭据坚持环境变量注入与最小权限——数据库只读、细粒度令牌、定期审计。BookNest 实战给出了两个 server 的完整接入与验证流程,其中"INSERT 必须被拒绝"的只读门槛,请当作日后接入任何数据类 server 的固定动作。下一章讨论权限与 hooks:进一步细化代理"能做什么、做之前要不要问你"。
docs/handoff/mcp-setup.md 交接文档(呼应 5.4)。上一章把代理接上了 MCP:能力半径从"读写本地文件"扩展到"连数据库、调外部 API"。能力越大,越需要缰绳。本章是 Part 2 的最后一块拼图——权限规则与 hooks:前者决定代理"默认能做什么",后者在每一次工具调用的生命周期里自动执行你的检查。它们共同回答一个问题:怎么让代理放手干活,又不至于干出格的事。
AI 编码代理默认处于一种"每动一步都问你"的状态:读个文件、跑个测试、改一行代码,都要人工点确认。把交互强度拉到两个极端,问题立刻显形:
rm -rf,删掉的不是构建产物;git push --force 覆盖了远端历史;.env 内容当作"配置示例"贴进了会话或文档。权限系统的解法不是在两个极端里二选一,而是把"你信任代理做什么"变成一条条可配置、可审计的规则:
一句话:权限配置 = 把你对代理的信任,从"每次口头判断"变成"一次书面立法"。 这也是第 11 章"边界设计"的地基——本章先解决"怎么写规则、怎么加自动检查",第 11 章再上升到"怎么为一个多代理系统划分边界"。
以 ZCode 为例(Claude Code 同名同义,格式互通):
| 层级 | 位置 | 是否进 Git | 谁维护 |
|---|---|---|---|
| 项目共享 | <repo>/.zcode/settings.json |
是(团队护栏,走评审) | 团队 |
| 项目个人 | <repo>/.zcode/settings.local.json |
否(写进 .gitignore) | 个人 |
| 用户全局 | ~/.zcode/settings.json |
否 | 个人跨项目偏好 |
| 企业托管 | 系统级托管配置(如 managed-settings.json) | — | 管理员强制下发 |
生效优先级从高到低:企业托管 > 命令行参数 > 项目个人 > 项目共享 > 用户全局。也就是说,任何一层都放宽不了上层收紧的红线——这是 7.5 节团队策略的机制基础。
每条规则形如 工具名(参数模式),常见写法:
| 写法 | 含义 | 示例 |
|---|---|---|
Bash(命令) |
精确匹配一条命令 | Bash(git status) |
Bash(命令:*) |
前缀匹配,:* 后接任意参数 |
Bash(npm test:*) |
Read(路径) / Edit(路径) |
gitignore 风格的路径模式 | Edit(docs/**)、Read(.env*) |
WebFetch(domain:域) |
按域名放行出网请求 | WebFetch(domain:nodejs.org) |
mcp__服务名[__工具名] |
MCP 工具(只写服务名即放行整组) | mcp__sqlite |
| 裸工具名 | 放行该工具的全部调用 | Read |
三类行为的判定顺序是 deny > ask > allow:一条调用同时命中多条规则时,最严的生效。所以 Read 在 allow 里、Read(.env*) 在 deny 里,结果是"都能读,但 .env 读不了"。
关于出网:把
WebFetch类工具按域白名单放行,等于给代理的对外请求画了硬边界——这条白名单同时也是 SSRF(Server-Side Request Forgery,服务端请求伪造)防护的第一道闸门:代理只能访问你点名过的域,而不是任意 URL。
{
"permissions": {
"defaultMode": "default",
"allow": [
"Read",
"Bash(git status)",
"Bash(git diff:*)",
"Bash(git log:*)",
"Bash(npm run lint)",
"Bash(npm test:*)",
"WebFetch(domain:nodejs.org)",
"WebFetch(domain:developer.mozilla.org)"
],
"ask": [
"Bash(git commit:*)",
"Bash(npm install:*)",
"Edit",
"Write"
],
"deny": [
"Read(.env)",
"Read(.env.*)",
"Read(**/.env*)",
"Read(secrets/**)",
"Bash(rm -rf:*)",
"Edit(migrations/**)"
]
}
}
逐行讲解:
defaultMode: "default"——未命中任何规则的动作一律询问。这是安全的兜底,不要改成全放行模式;acceptEdits(文件编辑免确认)、plan(只读规划)、bypassPermissions(全放行,高危)等模式留到第 11 章按场景选择;"Read" 与 git status / diff / log、npm run lint、npm test:*——读文件与只读 Git 命令完全放行,验证类命令放行,让代理"随时自查、改完自验"成为默认习惯;WebFetch(domain:...)——出网仅限这两个文档站,其余域名落入 defaultMode 询问;git commit:*——提交必须人审。commit 是"留下永久记录"的动作,适合 ask;npm install:*——装依赖改变依赖树,供应链风险入口,必须人审;"Edit"、"Write"——写代码整体保持询问(ask 优先于 allow 的编辑类默认),项目成熟后可再把子路径收进 allow;.env 与 secrets/** 的 deny——注意写了三种形态:根目录的 .env、.env.*、以及任意子目录的 **/.env*,防止代理在 server/ 下新建 .env.local 就绕过了规则;rm -rf:*——递归删除永远不许代理执行;Edit(migrations/**)——呼应第 04 章 BookNest 禁区:已应用的迁移文件不可修改,规则比"禁区清单"更硬。不要试图一次设计出完美清单:弹确认框时选择"始终允许",规则会写回项目个人配置(settings.local.json);每周把沉淀下来的规则评审后上移到项目共享配置。判断标准:幂等可重跑(测试、lint、status)→ allow;留下永久痕迹(commit、install、push)→ ask;涉密或不可逆(.env、force push)→ deny。
权限规则回答"能不能",hooks 回答"过程中要不要额外做点什么"。Hook 是你注册的 shell 命令,在工具调用的生命周期事件点上自动执行:
用户输入 ─▶ 模型决策 ─▶ [PreToolUse hooks] ─▶ 工具实际执行 ─▶ [PostToolUse hooks] ─▶ 模型继续
│ 拦截 / 放行 / 改写 反馈 / 记录 / 联动
└─▶ 会话级事件:SessionStart、Stop(回合结束)等
常用事件一览:
| 事件 | 触发时机 | 典型用途 |
|---|---|---|
| PreToolUse | 工具调用执行前 | 拦截危险命令、阻止改敏感文件 |
| PostToolUse | 工具调用执行后 | 自动格式化、触发增量检查 |
| Stop | 代理回合结束时 | 收尾扫描(凭据)、审计日志 |
Hook 从 stdin 接收 JSON(含工具名与参数),用退出码与 stderr 表达结论:
{
"tool_name": "Bash",
"tool_input": { "command": "npm test", "description": "跑接口测试" }
}
PostToolUse 事件额外携带 tool_response 字段(工具的执行结果)。
| 退出码 | 含义 | stderr 的去向 |
|---|---|---|
| 0 | 成功:放行 / 继续 | 不反馈 |
| 2 | 阻断:PreToolUse 取消本次调用;Stop 阻止回合结束 | 反馈给模型——它会读到原因并自我修正 |
| 其他非零 | 非阻断错误,流程继续 | 仅记录或展示给用户 |
除退出码外,hook 还可在 stdout 输出 JSON 做精细控制(如明确"转人工确认"),字段因工具而异;退出码约定最通用,本教程以它为主。注意 stderr 是给模型看的纠错通道,把"为什么拦 + 该怎么办"写清楚,模型下一次就会照做。
| 需求 | 用权限规则 | 用 hook |
|---|---|---|
不许读 .env |
一条 deny 即可 | 太重了 |
| 提交前扫描暂存区是否含密钥 | 表达不了(要"看内容") | 合适 |
| 写文件后自动格式化 | 表达不了(要"产生副作用") | 合适 |
| 禁止强推远端 | Bash(git push --force:*) |
规则够用,hook 做双保险 |
判断准则一句话:权限规则是静态白名单,hooks 是可编程守门员。 能用一条规则表达的就用规则(声明式、零维护、一目了然);需要理解内容才能判断、或需要产生副作用的,才交给 hook。反过来把所有事都写成 hook,你会得到一套没人看得懂的隐形逻辑。
以下示例均用 Node 编写(跨平台、与 BookNest 技术栈一致),保存在 <repo>/.zcode/hooks/ 下。第 13 章会对 hooks 做深度实践,本章目标是三个最小可运行版。
// .zcode/hooks/guard-bash.mjs —— 黑名单匹配,命中即拦
let raw = "";
process.stdin.on("data", (d) => (raw += d));
process.stdin.on("end", () => {
const { tool_input } = JSON.parse(raw || "{}");
const cmd = tool_input?.command ?? "";
const blacklist = [
/\brm\s+-[rf]{2,6}\s+(\/|~)(\s|$)/, // 递归强删根目录/家目录
/\bgit\s+push\b.*--force/, // 强推覆盖远端历史
/\bdrop\s+table\b/i, // 直接删表
/>\s*\/dev\/sd[a-z]/, // 直写块设备
];
if (blacklist.some((re) => re.test(cmd))) {
console.error(`[guard-bash] 已拦截疑似危险命令:${cmd}。` +
`如确需执行,请人工在终端操作并说明原因。`);
process.exit(2); // 阻断本次调用,stderr 反馈给模型
}
process.exit(0); // 放行
});
注册(加进 settings.json):
{
"hooks": {
"PreToolUse": [
{
"matcher": "Bash",
"hooks": [
{ "type": "command", "command": "node .zcode/hooks/guard-bash.mjs" }
]
}
]
}
}
matcher 是工具名匹配(支持 Write|Edit 这样的正则)。黑名单是从简示例,生产级清单与维护方式见第 13 章。
// .zcode/hooks/format-on-save.mjs —— Write/Edit 后自动 Prettier
import { execFileSync } from "node:child_process";
import path from "node:path";
let raw = "";
process.stdin.on("data", (d) => (raw += d));
process.stdin.on("end", () => {
try {
const { tool_input } = JSON.parse(raw || "{}");
const file = tool_input?.file_path ?? "";
const ext = path.extname(file);
const ok = [".ts", ".tsx", ".js", ".mjs", ".css", ".md"].includes(ext);
const skip = /node_modules|data\/|migrations\//.test(file);
if (!ok || skip) process.exit(0);
execFileSync("npx", ["prettier", "--write", file], { stdio: "ignore" });
console.error(`[format] Prettier 已格式化 ${path.basename(file)}`);
} catch (err) {
console.error(`[format] 跳过(${err.message})`); // 失败不阻塞,交给 lint 兜底
}
process.exit(0);
});
注册到 PostToolUse,matcher 写 "Write|Edit"。两点设计意图:只处理源码与文档、绕过生成物与迁移目录;任何异常都退出 0——格式化失败不该让会话卡死,仓库里还有 ESLint 与 CI 兜底。
// .zcode/hooks/scan-secrets.mjs —— 每轮结束扫描暂存区,疑似凭据即报警
import { execFileSync } from "node:child_process";
let raw = "";
process.stdin.on("data", (d) => (raw += d));
process.stdin.on("end", () => {
let diff = "";
try {
diff = execFileSync("git", ["diff", "--staged", "--unified=0"],
{ encoding: "utf8" });
} catch { process.exit(0); } // 不在 Git 仓库里就静默放行
const patterns = [
[/\bsk-[A-Za-z0-9]{16,}\b/, "疑似 OpenAI 风格密钥"],
[/\bghp_[A-Za-z0-9]{20,}\b/, "疑似 GitHub 个人令牌"],
[/api[_-]?key\s*[:=]\s*["'][^"']{8,}["']/i, "疑似硬编码 API Key"],
[/-----BEGIN [A-Z ]*PRIVATE KEY-----/, "疑似私钥内容"],
];
const hits = patterns.filter(([re]) => re.test(diff)).map(([, label]) => label);
if (hits.length) {
console.error(`[scan-secrets] 暂存区发现疑似凭据:${hits.join(";")}。` +
`请改用 process.env.* 注入,移除明文后重试。`);
process.exit(2); // 阻止回合结束,强制代理先处理
}
process.exit(0);
});
注册到 Stop 事件(无需 matcher,因为它不针对某个工具)。它与权限规则形成两层防线:Bash(git commit:*) 设为 ask 管"动作",这个 hook 管"内容"——即使将来某条规则配松了,明文凭据也出不了暂存区。正则只是示意,企业级扫描建议用 gitleaks 等成熟工具(第 13 章)。
| 层 | 内容 | 位置 | 变更方式 |
|---|---|---|---|
| 强制层 | 企业红线:公司密钥目录、出网域名总表 | 管理员托管配置 | IT 统一下发,任何人改不掉 |
| 共享层 | 项目 allow/ask/deny + hooks 脚本 | .zcode/settings.json 与 .zcode/hooks/,进 Git |
走 PR 评审 |
| 个人层 | 个人增强:更多 allow、本地工具放行 | settings.local.json / 用户全局配置 |
自行维护,不进 Git |
关键动作只有一个:把权限配置与 hooks 脚本当作代码提交进仓库。于是护栏获得代码的一切好处——人手一份、可评审、可回滚、可追溯;新成员克隆下来就自带同一套安全基线,而不是靠口头交接"哦对了代理不能动 migrations"。
护栏的 diff 和业务代码同等重要,甚至更重要——它决定代理能对业务代码做什么:
guard: 前缀;目标:落地 BookNest 的权限配置全量文件 + 一个完整的"写文件后自动 Prettier"hook,并验证三者真的在工作。
文件一:booknest/.zcode/settings.json(全量,进 Git)
{
"permissions": {
"allow": [
"Read",
"Bash(git status)",
"Bash(git diff:*)",
"Bash(git add:*)",
"Bash(npm run dev)",
"Bash(npm run lint)",
"Bash(npm test:*)",
"Bash(npx tsc --noEmit)",
"Bash(npx vitest run:*)",
"WebFetch(domain:developer.mozilla.org)",
"WebFetch(domain:nodejs.org)"
],
"ask": [
"Bash(git commit:*)",
"Bash(git push:*)",
"Bash(npm install:*)",
"Bash(npx playwright test:*)",
"Edit(migrations/**)",
"Write(migrations/**)"
],
"deny": [
"Read(.env)",
"Read(.env.*)",
"Read(**/.env*)",
"Read(data/**)",
"Edit(data/**)",
"Write(data/**)",
"Edit(.zcode/**)",
"Bash(rm -rf:*)",
"Bash(git push --force:*)",
"Bash(sudo:*)"
]
},
"hooks": {
"PreToolUse": [
{
"matcher": "Bash",
"hooks": [
{ "type": "command", "command": "node .zcode/hooks/guard-bash.mjs" }
]
}
],
"PostToolUse": [
{
"matcher": "Write|Edit",
"hooks": [
{ "type": "command", "command": "node .zcode/hooks/format-on-save.mjs" }
]
}
],
"Stop": [
{
"hooks": [
{ "type": "command", "command": "node .zcode/hooks/scan-secrets.mjs" }
]
}
]
}
}
设计要点(对照 7.2.3 的教学版,这些是 BookNest 特有的取舍):
git add 放行、git commit/git push 询问:暂存无害且便于 Stop hook 扫描,提交必须人审;data/** 三连 deny:data/booknest.db 是 SQLite 二进制库文件,读了没用、改了致命(对应 NFR-4 数据本地化的底线);Edit(.zcode/**) deny:不许代理改自己的护栏——否则一次"优化配置"的会话就可能自我解锁;npx playwright test 询问:端到端测试要起浏览器,慢且偶发环境问题,保留人工观察;WebFetch 不整体放行,仅白名单两个文档域——其余出网落入默认询问,白名单即 SSRF 防线;文件二:booknest/.zcode/hooks/format-on-save.mjs——完整内容即 7.4.2 的脚本,原样放入;guard-bash.mjs、scan-secrets.mjs 同理。三个脚本与配置一起提交:
git add .zcode/
git commit -m "guard: 权限基线与三个入门 hooks(拦截/格式化/凭据扫描)"
验证方法——护栏装好了,要真拦一次才算数:
# 1) 拦截验证:让代理强推(应被 deny 规则 + PreToolUse hook 双保险拦下)
# 你:远端历史太乱了,直接 git push --force 覆盖掉。
代理:[guard-bash] 已拦截疑似危险命令:git push --force origin main
代理:该命令命中了项目的 deny 规则与 PreToolUse hook(强推会销毁远端历史)。
建议改为:git fetch 后确认差异,或在新分支上重建历史;如确需强推,
请你在终端手动执行。
# 2) 格式化验证:让代理改一个前端文件,代理输出应出现 [format] 行
npx prettier --check "web/src/**/*.{ts,tsx}"
# 期望输出:All matched files use Prettier code style!
# 3) 凭据扫描验证:造一个假令牌进暂存区(仅测试用,勿用真实密钥)
echo 'export const KEY = "sk-FAKE-DEMO-KEY-NOT-REAL-0000";' > /tmp/t.ts && git add /tmp/t.ts
# 你:提交吧。
# 代理在回合结束时应收到 [scan-secrets] 提示并主动移除明文,改为 process.env.KEY。
三项都符合预期,说明 BookNest 的护栏从"纸面配置"变成了"可执行事实"。
| 误区 | 典型现象 | 纠正方法 |
|---|---|---|
| 把全放行模式当默认 | 图省事全程 bypass,直到某天代理 force push 或删库 | 默认保持询问模式,用 allow 清单把信任逐步"养"出来 |
| 前缀匹配当语义匹配 | deny 了 npm install,代理改用 npm i 绕过 |
红线枚举等价写法;需要理解语义的判断交给 hook |
| deny 了 Read 却忘了 Bash | Read(.env) 拦住了,Bash(cat .env) 照读 |
关键文件把对应读法一并 deny,或收口读文件的工具 |
| hook 里塞慢操作 | 每次 Edit 都等 3 秒 lint,会话慢到没法用 | hook 保持亚秒级;重活交给 PostToolUse 轻量触发或 CI |
| hook 无容错 | lint 挂掉退出码非 0,代理卡住连环重试 | 永远 try/catch,可容忍的失败打印提示后退出 0 |
| 克隆来的 hooks 直接跑 | 仓库自带 hook 以你的身份执行了任意命令 | 新仓库首次运行前逐条审查 hooks 注册与权限配置 |
权限与 hooks 是代理的"法律系统":权限规则把信任变成可配置、可审计的静态法条——allow 固化安全动作、ask 保留人审关口、deny 划定不可逾越的红线,且 deny 永远优先;hooks 则是可编程守门员,在 PreToolUse/PostToolUse/Stop 等生命周期时机自动执行你的脚本,靠 stdin JSON 读取上下文、靠退出码与 stderr 表达裁决。两者分工明确:静态规则管"能不能",可编程 hook 管"过程中还要做什么"。团队层面,把护栏提交进仓库共享、个人增强放用户层、企业托管兜底,并让配置本身也接受评审——至此,Part 2 的四件套(上下文文件、规则记忆、MCP、权限 hooks)配齐,代理已经"开机即懂、干活有度"。
:* 前缀、路径模式、domain、MCP 命名);guard-bash.mjs 增加两条黑名单(git reset --hard、chmod -R 777 /),分别用"应拦截"与"应放行"的命令各测试一次,确认 stderr 提示能引导模型改用安全替代方案。web/src/ 时自动执行 npx eslint --fix;要求 lint 失败时只打印提示、退出 0,并用一个故意写错的文件验证它不会卡死会话。guard: 前缀的提交信息记录这次变更。Part 2 配齐了四件套:代理"开机即懂"(AGENTS.md)、"记得住"(规则与记忆)、"够得着"(MCP)、"干不出格"(权限与 hooks)。但还有一类知识无处安放:那些重要却低频的流程——比如"新增一个 API 端点的完整步骤",塞进 AGENTS.md 会撑爆常驻上下文,口头交代又每次漂移。本章开始 Part 3:把这类方法论打包成代理"随取随用"的能力包——Skill。
第 04 章立过规矩:AGENTS.md 只放"每次会话都成立"的约定,低频内容塞进去就是 wiki 化反模式。可开发中确实存在这样的知识:
这些知识的特点是:完整版很重要,但不需要时刻在场。Skill(技能)就是为它们设计的容器——一个文件夹,内含 SKILL.md(元数据 + 方法论正文)与可选资源(脚本、参考文档、模板资产)。代理平时只"看到书脊"(一两行简介),识别到匹配任务后才"翻开整本书"按需加载执行。两种状态的体感差别:
没有 skill 时(每个端点都要重新交代):
你:加一个 GET /api/v1/books 的标签筛选接口。
你:(打断)等等,校验要过 validate.ts,测试要先写,
错误格式统一 { code, message },路由里别写业务逻辑……
有 add-endpoint skill 时:
你:/add-endpoint 给 books 加按标签筛选的接口。
代理:(自动按五步执行,并对照完成定义自检)
三个机制都往代理的上下文里放东西,容易混。用两个维度区分:加载时机(常驻还是按需)与内容性质(知识流程还是外部能力):
| 机制 | 形态 | 何时进入上下文 | 擅长解决 |
|---|---|---|---|
| AGENTS.md / 规则 | Markdown 指令 | 会话启动,常驻 | 项目事实、全局约定、禁区 |
| Skill | 文件夹(SKILL.md + 资源) | 任务匹配后按需 | 可复用的任务方法论(怎么干) |
| MCP | 协议连接的外部服务 | 工具清单常驻,调用时执行 | 与外部系统读写交互(碰得到什么) |
| 权限 / Hooks | 配置 + 脚本 | 生命周期强制执行 | 护栏与自动化(许不许干) |
一个例子看清差别:"操作 SQLite 数据库"是能力问题——用 MCP 接一个 sqlite server;"怎么给 BookNest 加一个端点"是流程问题——写成 skill;"端点路由只做参数解析"是约定——进 AGENTS.md。
Agent Skills 已是开放格式(open standard,社区称为 agentskills 标准):规范定义了 SKILL.md 的元数据字段与目录布局,任何遵循该标准的工具都能加载同一个 skill。ZCode 与 Claude Code 均按此约定读取——你在本教程写的每个 skill,换到另一个支持该标准的工具里无需改造。这也决定了 skill 的工程属性:它就是纯文本文件夹,可进 Git、可走 PR 评审、可版本化,没有任何专有运行时。
以 BookNest 的 add-endpoint 技能为例(第 09 章将完整写出它):
---
name: add-endpoint
description: 为 BookNest 后端新增一个 REST API 端点。当用户要求"加一个接口/
端点/API"或实现包含新路由的功能时使用;覆盖路由、服务层、入参校验、
接口测试与统一错误格式的完整流程。
---
# 新增 REST API 端点
固定五步,缺任何一步视为未完成:
1. **路由**:在 server/src/routes/<资源>.ts 定义路由,只做参数解析;
2. **服务层**:业务逻辑写入 server/src/services/<资源>Service.ts;
3. **校验**:所有入参经 server/src/middleware/validate.ts 校验;
4. **测试**:先在 server/tests/ 写接口用例,再跑 npm test -- <文件> 验证;
5. **错误格式**:失败路径统一返回 { code, message }。
错误码表与校验规则的全量细节,动手前先读 references/api-conventions.md。
结构分两段:YAML frontmatter(两条 --- 之间的元数据)与 Markdown 正文(写给代理的执行手册)。
| 字段 | 必填 | 约束与作用 |
|---|---|---|
name |
是 | 小写字母、数字与连字符,规范上限 64 字符;约定与目录名一致 |
description |
是 | 规范上限 1024 字符;代理决定"要不要用"的唯一依据(8.3 节展开) |
| 其他扩展字段 | 否 | 工具限制、版本兼容说明等,因工具而异,用到时查所用工具文档 |
正文则是给代理的操作指令:祈使句、编号步骤、可判定的完成标准("缺任何一步视为未完成"),以及——注意最后一句——对资源文件的指路。
一个成熟的 skill 通常长这样:
add-endpoint/
├── SKILL.md # 必需:元数据 + 方法论正文
├── scripts/
│ └── scaffold.mjs # 可执行脚本:生成端点样板(代理用 Bash 调用)
├── references/
│ ├── api-conventions.md # 参考文档:错误码表、校验规则全量细节
│ └── testing-guide.md # 参考文档:接口测试模板与断言写法
└── assets/
└── route.ts.tmpl # 模板资产:直接复制改造的代码骨架
三个约定目录各司其职:
--help 并返回明确退出码;不是每个 skill 都要凑齐三个目录:release-check 这类纯清单技能,一个 SKILL.md 就够了。
新技能从这个骨架起步,用起来再长:
---
name: <任务导向的英文名>
description: <做什么>。当<用户会怎么提这个任务>时使用;覆盖<关键步骤范围>,
完成标准是<可判定的产出>。
---
# <任务名>
固定步骤(缺任何一步视为未完成):
1. <步骤,写明涉及的路径>
2. <步骤,写明验证命令>
<某主题> 的全量细节,动手前先读 references/<主题>.md。
自检方法:合上电脑,你能照着这个骨架口述出技能"何时触发、做哪几步、怎么算完成"吗?三问都答不上来的部分,就是还没想清楚的部分。
Skill 系统的核心设计是渐进式披露(progressive disclosure):把内容分成三级,每一级只在需要时进入上下文:
| 级别 | 内容 | 何时进入上下文 | 典型成本 |
|---|---|---|---|
| L1 元数据 | name + description | 会话启动即常驻 | 每个 skill 约百 token |
| L2 正文 | SKILL.md 全文 | 任务匹配、skill 被触发后 | 数百到数千 token |
| L3 资源 | 正文引用的文件 | 执行中按需读取 | 用到才付,可按主题控制 |
一次完整调用的付费时序:
会话启动 ─▶ [L1] 全部 skill 的 name+description 常驻(图书馆索引卡)
│
任务到来 ─▶ 语义匹配命中 ─▶ [L2] 加载该 skill 正文全文(翻开那本书)
│
执行中途 ─▶ 正文指路 references/ ─▶ [L3] 按需读取文件(查书末附录)
假设你装了 30 个 skill:L1 常驻成本约 30 × 100 = 3000 token,可以忽略不计。反过来,如果把这 30 份正文全部塞进系统提示(很多人早期就是这么用 AGENTS.md 的),按每份平均 2000 token 算就是 6 万——还没开始干活,窗口先没了三分之一。第 03 章讲过:上下文是稀缺资源,行数即成本,且塞得越满,模型对关键信息的注意力越稀。
类比:L1 是图书馆的索引卡,L2 是抽出那本书翻开,L3 是书末的参考文献与附录。你不会为了查一个问题把整个图书馆搬回家——但需要时,整座图书馆都在。
三级结构推出一个容易被忽视的事实:L1 是唯一的常驻入口,任务与 skill 的匹配只能靠 description 这一两行字完成。 由此得出本书最重要的结论之一:
description 写得好坏,直接决定 skill 会不会被触发。写得含糊,任务来了模型对不上号,skill 等于不存在——写得再漂亮的正文也永远没机会被读到;写得精准,正文的质量才有意义。
所以写 skill 的精力分配是"倒挂"的:一半以上花在那一两行 description 上(第 09 章专门讲怎么写),其次才是正文结构(本章 8.2),最后是资源组织。触发失败的诊断与修复则留给第 10 章。
代理在每个任务开始时,把当前任务描述与所有 skill 的 L1 元数据做语义比对,命中则加载正文。三个提高命中率的抓手:
以 add-endpoint 为例,看"任务的话"如何对上 description 的关键词:
| 用户随口说 | description 里埋的对应词 |
|---|---|
| "帮我加个接口" | 接口 / API |
| "新增一个端点" | REST API 端点 |
| "books 要能按标签筛" | 资源名 + 功能动词(覆盖路由、服务层……) |
隐式触发不保证每次命中(尤其 skill 新写好、还没调试时),显式调用是确定性手段:
你:/add-endpoint 给 books 加一个按标签筛选的接口。
你:用 add-endpoint 技能,为 fragments 实现批量归并到笔记的接口。
斜杠命令 /skill-name 与对话中点名等价:都是绕过语义匹配、直接指定加载。调试新 skill 时先用显式调用验证正文质量,再观察隐式触发是否靠谱——这个两段式验证流程在第 10 章会工具化。
| 位置 | 生效范围 | 进 Git? | 适用 |
|---|---|---|---|
<repo>/skills/<name>/ |
本项目所有会话 | 是(团队共享、走评审) | 项目流程:BookNest 的 add-endpoint |
~/.zcode/skills/<name>/ |
个人所有项目 | 否 | 个人方法论:提交信息风格、周报生成 |
判断标准与第 04 章指令文件的分层完全同构——问一句"换个项目还成立吗?":成立就放个人级,只对本项目有意义就放项目级进 Git。附录 D 的目标仓库结构里已经预留了 skills/ 目录,BookNest 的项目级技能将住在那里。部分工具还支持通过插件内置 skills,加载机制相同,不再展开。
Skill 是纯文本文件夹,复用成本几乎为零,先找现成的永远比从零写划算:
心法一句话:先搜再造——找到最接近的现成 skill,fork 一份,改 description 与项目相关细节,比从零写快一个数量级。
把 Part 1–3 的机制放在一张图里,Skill 的位置一目了然:
flowchart TB
subgraph S1["常驻层:开机即知"]
A["AGENTS.md / 规则记忆<br/>项目事实、约定、禁区"]
end
subgraph S2["能力层:按需加载"]
B["Skill<br/>任务方法论(怎么干)"]
C["MCP<br/>外部工具与数据(碰得到什么)"]
end
subgraph S3["护栏层:全程强制"]
D["权限规则<br/>静态 allow/ask/deny"]
E["Hooks<br/>可编程检查点"]
end
A --> AGENT["编码代理"]
B --> AGENT
C --> AGENT
D --> AGENT
E --> AGENT
B -. "skill 的脚本经授权调用工具" .-> C
五件套各司其职、互不替代。用一次真实任务走读这条流水线——你对代理说"给 books 加一个按标签筛选的接口":
scripts/scaffold.mjs 生成端点样板(Bash 调用,命令在权限 allow 清单内);references/api-conventions.md(L3)按需读入,错误码表就位;常驻层给了背景,能力层给了方法与工具,护栏层全程兜底——这正是本书追求的"有纪律的 agentic engineering"形态。
目标:盘点 BookNest 开发中反复出现的流程性工作,筛选出值得做成 skill 的候选,并给出裁决理由。第 09 章将从第一名开始动手写。
第一步:列清单。 把"做过不止一次、下次还会做"的工作全部列出来,宁多勿漏:
□ 新增一个 REST API 端点(FR-1~FR-7 估计 20+ 个端点)
□ 给前端加一个页面或组件(BooksPage、StatsPage……)
□ 做一次数据库 schema 迁移(每个里程碑都要)
□ 为一个模块补接口测试(每个端点都要)
□ 从 CSV 批量导入书单(FR-5,低频)
□ 发布前检查(构建、测试、安全项;M4 起每次发布都要)
□ 排查一次"测试挂了"(原因每次都不同)
第二步:过筛选标准。 四条全满足才值得 skill 化:
第三步:逐一裁决。
| 候选 | 频率 | 步骤稳定 | 隐性知识 | 出错代价 | 裁决 |
|---|---|---|---|---|---|
| 新增 REST API 端点 | 高 | 高 | 校验/错误码/测试写法分散在约定里 | 中 | 入选 ① |
| 数据库 schema 迁移 | 中 | 高 | 序号递增、禁改历史、需可回滚 | 高(数据损坏) | 入选 ② |
| 发布前检查 | 低但关键 | 高 | 8 项检查的顺序与安全项 | 高(带病发布) | 入选 ③ |
| 前端加页面 | 高 | 中 | 多数约定已在 web/AGENTS.md | 低 | 暂缓观察 |
| 接口测试 | 高 | 中 | 可并入端点流程 | 低 | 并入 ① |
| CSV 导入书单 | 低 | 低 | 一次性功能 | 低 | 不做 |
| 排查测试失败 | 不定 | 低 | 因 case 而异 | 低 | 不做 |
三个入选的理由:
反面教材同样有价值:CSV 导入是写完就不再执行的一次性功能,"排查测试失败"每次原因不同——它们违反"频率/稳定"两条,硬做成 skill 只会制造没人触发的死文件夹。
验证盘点质量:别只靠自己回忆,让代理帮忙——它刚干过的活就是流程存在的证据:
你:回顾我们最近的会话,哪些操作你重复执行了三次以上,且每次步骤基本一样?
代理:高频重复的操作有三类:
1. 新增端点:每次都是"路由→service→校验→测试→错误格式"五步,已重复 11 次;
2. 数据库迁移:每次都要提醒我"不要动已应用的迁移文件",已重复 4 次;
3. 发布前检查:你手工跑了 2 次,每次清单略有出入。
第 1、2 类步骤完全固定,适合沉淀为可复用流程;第 3 类建议先统一清单本身。
把候选清单与裁决表提交进 docs/tasks/(文件内容即上面两步的产物),作为第 09 章的输入——第 09 章将亲手写出第一个:add-endpoint。
| 误区 | 典型现象 | 纠正方法 |
|---|---|---|
| description 写成空洞口号 | 只有"处理文档相关任务"一句 | 写"做什么 + 何时用",包含用户会说出的关键词 |
| 把所有知识塞进 AGENTS.md | 低频流程常驻,每个会话"背"几千字 | 低频流程抽成 skill,靠渐进式披露按需加载 |
| 该用 MCP 的做成 skill | SKILL.md 教代理"用 curl 调外部 API" | 外部系统读写用 MCP;skill 管知识与流程 |
| 正文写太长不拆文件 | SKILL.md 几千行,触发一次吞掉半个窗口 | 正文留主干,细节拆 references/ 并在正文指路 |
| 一次性任务也做成 skill | 技能库堆满只触发过一次的死文件夹 | 先过"频率/稳定/隐性/代价"四条标准再动手 |
| 关键步骤靠模型自由发挥 | 同一流程每次产出格式都不一致 | 步骤编号固定 + 确定性操作交给 scripts/ 脚本 |
skills/ 与 Git,个人方法论进用户目录;Skill 把"重要但低频"的任务方法论从常驻上下文中解放出来:一个文件夹(SKILL.md + scripts/references/assets 资源),遵循 agentskills 开放格式,跨工具通用。它靠渐进式披露三级机制兼顾省与深——L1 元数据常驻(每 skill 约百 token)、L2 正文任务匹配后加载、L3 引用文件执行中按需读取;由此推出本部分的核心命题:description 是唯一触发入口,写得好坏直接决定 skill 的生死。与 AGENTS.md(常驻约定)、MCP(外部能力)、权限与 hooks(护栏)合起来,代理的"知识—能力—纪律"三层就此完整。下一章进入动手环节:写出第一个经得起触发考验的优秀 Skill。
skills/ 共享,个人方法论进用户目录,先搜再造。skills/ 与 ~/.zcode/skills/,在该项目内、另一个项目里各开一个会话,验证生效范围与 8.4.3 的表格一致。docs/tasks/,第 09 章将从清单第一名开始写出第一个真正的 skill。上一章留下一个决定性结论:description 是唯一触发入口,写得好坏决定 Skill 生死。本章回答"怎么写好"——先判断什么值得做成 Skill,再依次攻克 description、正文与资源组织三道关,最后从零完整走查一个含校验脚本的真实案例,并在 BookNest 中落地为第一个项目级 Skill。
写 Skill 本身有成本(结构设计加调试,通常 0.5–2 小时),做之前先过三道判据:
判据一:重复出现 ≥ 3 次。 一两次的偶发任务摊不平写作成本;而"重复"本身是流程稳定的经验证据——第 08 章盘点 BookNest 时,"新增端点已被重复执行 11 次"正是从代理的会话历史里数出来的。注意这是下限不是目标:重复 30 次的任务才最值得投入打磨。
判据二:存在固定的最佳流程。 下次做和这次做,步骤基本一样、可枚举、可排序。如果每次都要临场发挥(比如"排查一次测试挂了",原因每次都不同),流程本身就不存在,Skill 只能写成正确的空话。
判据三:含团队私有约定。 即"从公开资料和现有代码都看不出来"的知识:错误码表、validate() 中间件的用法、迁移纪律。模型通识(general knowledge)解决不了的部分,才是 Skill 的价值增量——否则你在给代理重复教它已经会的东西。
三个判据与第 08 章四条筛选标准的对应:判据一≈"频率高"、判据二≈"步骤稳定"、判据三≈"有隐性知识";"出错有代价"则是加权项——代价极高的任务(如数据库迁移,做错损坏数据)即使频率只有中等,也值得做。
动手前把这张清单过一遍:前四格全勾才做,出错代价高时可放宽频率要求,末格命中即否决:
□ 近一个月这项工作出现过 ≥ 3 次吗?
□ 下次的步骤和这次基本一样吗(能写成固定编号流程)?
□ 有"看代码、搜公开资料都看不出来"的内部约定吗?
□ 新同事不问人大概率做不对吗?(做不对 → 有沉淀价值)
□ 出错的代价高吗?(数据损坏、带病发布 → 可放宽频率要求)
□ 是一次性任务或每次原因都不同吗?(是 → 停,不做)
用 30 秒裁决三个真实候选:
| 候选 | 判据一 | 判据二 | 判据三 | 结论 |
|---|---|---|---|---|
| 新增 REST API 端点 | 11 次 | 五步固定 | 错误码表分散在约定里 | 做 |
| 把统计页图表换成折线图 | 1 次 | 流程不定 | 无 | 不做 |
| 数据库 schema 迁移 | 每里程碑 1 次 | 纪律严格 | 序号、禁改历史 | 做(代价加权) |
回顾第 08 章的机制:L1 元数据(name + description)是唯一常驻上下文的入口,任务与 Skill 的匹配只能靠这一两行字完成。它写含糊了,任务来了模型对不上号,Skill 等于不存在——正文写得再漂亮也永远没机会被读到。所以写 Skill 的精力分配是倒挂的:一半以上先花在 description 上。
第一组:口号式——没有动作,没有边界。
# 坏
description: 处理与数据库相关的各种任务。
# 好
description: 为 BookNest 管理数据库 schema 迁移(新增表与字段)。当用户要求
"加个字段/建表/改表结构/做迁移"时使用;纪律包括序号递增、已应用文件禁改、
必须可回滚,完成标准是校验脚本通过且迁移可在干净库上重放。
点评:坏版里"数据库""任务"两个词几乎所有项目话题都沾边,却没有一个具体动词,模型无从判断该不该触发。好版第一句是动作(管理迁移),第二句是触发场景(用户的原话),末句给了完成标准。
第二组:太宽泛——来者不拒,误触发成灾。
# 坏
description: 当用户要求添加任何功能时使用,生成符合项目规范的代码。
# 好
description: 为 BookNest 后端新增 REST API 端点(后端路由)。当用户要求
"加一个接口/端点/API"或实现包含新 HTTP 路由的功能时使用;不适用于纯前端
页面、一次性数据脚本与数据库迁移。
点评:坏版"任何功能"会把加页面、换图标、写脚本全部吸过来——触发率 100%,几乎全是误触发。好版用两个手段收窄:限定词("后端路由")+ 排除清单("不适用于……")。收窄 description 不会降低它的价值,只会提高命中的纯度。
第三组:术语腔——只有你的词,没有用户的词。
# 坏
description: 用于执行符合分层架构规范的 RESTful 资源创建工作流。
# 好
description: 为 BookNest 后端新增一个 REST API 端点。当用户说"帮我加个接口"、
"新增一个端点"、"books 要能按标签筛选"这类涉及新路由的任务时使用;
覆盖路由、服务层、入参校验、测试与统一错误格式。
点评:坏版每个词都"正确",却没有一个词是用户会说出口的。用户说"帮我加个接口",description 里没有"接口""加"这些字眼,语义匹配只能硬凑。好版把用户真实表述直接引号引进去——触发词的来源不是术语表,是你和同事平时说话的录音。
写正文前先换脑子:想象你要休长假,把活交接给一个技术不错但不了解你项目的同事——你写的交接纸上不会有"REST 的历史",只会写"第一步去哪个文件、第二步跑什么命令、什么情况要停下来问我"。Skill 正文就是这张交接文档,读者是代理,不是人:人读教程为了理解原理,代理读正文为了立刻行动。
纪律一:命令式、步骤化。 祈使句 + 编号,每步一个明确动作:
教程腔(写给学习者):
在 Express 中,路由负责把 HTTP 请求映射到处理函数。我们先来理解中间件
管道的执行顺序,然后再设计端点的参数解析方式……
手册腔(写给代理):
1. 在 server/src/routes/<资源>.ts 定义路由,只做参数解析,禁止写业务逻辑;
2. 业务逻辑写入 server/src/services/<资源>Service.ts。
纪律二:判断标准写成 checklist。 "确保代码质量良好"无法执行,"校验脚本退出码为 0"可以。把一切主观判断翻译成可勾选、可验证的条目:
- [ ] 每条路由都挂了 validate(schema) 中间件;
- [ ] 失败路径统一返回 { code, message };
- [ ] check_endpoint.mjs 退出码为 0。
纪律三:明确输出格式约定。 不写的话,每次回复的形态都会漂移,你还得追问"文件改了哪些?"。在正文末尾规定汇报格式(文件清单、测试结果、未完成事项),一次到位。
纪律四:写明"何时用、何时不用"。 正文开头重申触发条件并划出边界——这是第 08 章提高命中率的抓手之二:任务落在边缘时,给模型第二次确认机会。
纪律五:长度控制。 正文几百行内为佳,它每次触发都要全额进入上下文(L2)。细枝末节(错误码表、平台差异、历史决策)外移到 references/ 按需读——但正文必须显式指路("动手前先读 references/api-conventions.md"),否则代理不知道这些文件存在,L3 永远不会发生,细节等于不存在。
第 08 章介绍过三个约定目录,这里补全判断细则:
判别法:需要理解内容才能用 → references;复制改空就能用 → assets。
| 任务特征 | 归宿 | BookNest 例子 |
|---|---|---|
| 输出需逐字节确定、能一条命令验证 | scripts/ | check_endpoint.mjs 校验五步落地;scaffold.mjs 生成样板 |
| 需要语义权衡与判断 | 正文指令 | "schema 与 TS 类型如何同源" |
| 篇幅大、低频查阅的知识 | references/ | 错误码表全量、测试断言模板 |
| 格式固定的骨架 | assets/ | route.ts.tmpl 路由文件模板 |
判断标准一句话:能写成"输入 → 确定输出"的交给脚本,需要"看过再判断"的写成指令。例如"校验书籍封面 URL 是否指向可信图床"是纯确定性工作,就该是脚本而非一句"请确保 URL 可信"的指令:
// scripts/check-cover-url.mjs —— 校验封面 URL 是否指向白名单图床
const ALLOWED_HOSTS = new Set([
"images.booknest-cdn.example.com", // 项目自己的图床
"upload.wikimedia.org", // 公共维基媒体
]);
export function isAllowedCoverUrl(raw) {
let url;
try {
url = new URL(raw);
} catch {
return false; // 连 URL 都不是,直接拒绝
}
if (url.protocol !== "https:") return false; // 只允许 https
if (!ALLOWED_HOSTS.has(url.hostname)) return false; // 域白名单,防 SSRF
return true;
}
这段代码在发起任何外部请求前先做协议与域白名单校验,防止把内网地址或云元数据端点当作"封面图源"请求出去(SSRF,Server-Side Request Forgery,服务器端请求伪造)。凡是 Skill 里涉及网络请求的脚本,白名单校验都是不可省略的门槛。
从第 08 章候选清单第一名(新增 REST API 端点)出发。先定名:第 08 章示例里的草稿名 add-endpoint,定稿为 add-api-endpoint——动词开头(add-)、对象精确到"API 端点"而非泛化的 endpoint(项目里还可能有 WebSocket 端点、MCP 端点)、kebab-case、64 字符内、与目录名一致。名字会被用户口头引用("用 add-api-endpoint 做"),可读性就是可用性。
目录形态——第 08 章预览过的完整结构,本节把两个关键文件写出全文:
skills/add-api-endpoint/
├── SKILL.md # 元数据 + 五步手册(9.5.2 全文)
├── scripts/
│ ├── scaffold.mjs # 生成路由/服务/测试样板文件(确定性生成)
│ ├── check_endpoint.mjs # 完成定义的机器校验(9.5.4 全文)
│ └── check-cover-url.mjs # 封面 URL 白名单校验(9.4.2)
├── references/
│ ├── api-conventions.md # 错误码表、校验规则全量细节
│ └── testing-guide.md # 接口测试断言写法与模板
└── assets/
└── route.ts.tmpl # 路由文件骨架,复制后填空
---
name: add-api-endpoint
description: 为 BookNest 后端新增一个 REST API 端点,并保证符合项目五步约定。
当用户说"帮我加个接口"、"新增一个端点"、"给 books/notes/tags 加筛选、
删除、归并等能力",或实现的功能包含新 HTTP 路由时使用;覆盖路由、服务层、
入参校验、接口测试与统一错误格式,完成标准是 check_endpoint.mjs 全部通过
且新用例跑绿。
---
# 新增 REST API 端点
## 何时使用
用户要求新增后端接口、端点、API 路由,或前端功能需要新的后端能力;本技能只管"新增",修改已有端点时只执行第 1–3 步并保持既有测试通过。
## 固定五步(缺任何一步视为未完成)
1. **路由**:在 `server/src/routes/<资源>.ts` 定义路由(挂载于 `/api/v1`),
只做参数解析与调度,禁止出现 SQL 或业务分支;
2. **服务层**:业务逻辑写入 `server/src/services/<资源>Service.ts`;所有 SQL
使用参数化查询,禁止字符串拼接(NFR-2);
3. **校验**:所有入参先经 `server/src/middleware/validate.ts` 的
`validate(schema)` 校验;schema 用 zod 定义并与 TS 类型同源;
4. **测试**:先在 `server/tests/` 为新端点写用例(正常路径 + 至少一个校验
失败路径),运行 `npm test -- <文件>` 直到全部通过;断言写法见
`references/testing-guide.md`;
5. **错误格式**:失败路径统一返回 `{ code, message }`;错误码必须查
`references/api-conventions.md`,动手前先读。
## 完成定义(逐项核对)
- [ ] 五步全部执行,无跳步;
- [ ] `node skills/add-api-endpoint/scripts/check_endpoint.mjs
server/src/routes/<资源>.ts <资源>` 退出码为 0;
- [ ] 新增用例在 `npm test` 中全部通过;
- [ ] 未引入任何硬编码密钥、连接串或绝对路径。
## 汇报格式
完成后按此格式汇报,不要省略:
- 新增/修改文件清单;
- 校验脚本与测试结果(原样粘贴退出码与 passed/failed 数);
- 若某一步无法完成(如 schema 缺字段需要用户决策):停下来说明并等待,禁止跳过该步或自造约定。
server/src/routes/<资源>.ts 而非"路由文件里"——代理不用猜,猜就是漂移的起点;设计约定先行:支持 --help;退出码 0(全部通过)/1(有未通过项)/2(用法错误);纯 Node 标准库零依赖;输出 [OK]/[FAIL] 行便于代理逐项核对。它检查新端点是否注册了路由、挂了校验、有统一错误出口、没把业务写进路由层、且已在应用挂载:
#!/usr/bin/env node
// check_endpoint.mjs —— 校验新端点是否按约定注册路由与错误处理(退出码 0 通过 / 1 未通过 / 2 用法错误)
import { readFileSync, existsSync } from "node:fs";
import { dirname, join, resolve } from "node:path";
import { fileURLToPath } from "node:url";
const usage = `用法: node check_endpoint.mjs <routes 文件> [资源名]
示例: node check_endpoint.mjs server/src/routes/tags.ts tags`;
const args = process.argv.slice(2);
if (args.length === 0 || args[0] === "--help") {
console.log(usage);
process.exit(args.length === 0 ? 2 : 0);
}
// 本脚本位于 <仓库根>/skills/add-api-endpoint/scripts/,上溯三级即仓库根
const repoRoot = resolve(dirname(fileURLToPath(import.meta.url)), "../..", "..");
const file = resolve(args[0]);
const resource = args[1] ?? "";
const checks = [];
const add = (ok, label) => checks.push({ ok, label });
if (!existsSync(file)) {
console.error(`[FAIL] 文件不存在: ${file}`);
process.exit(1);
}
const src = readFileSync(file, "utf8");
// 1) 至少注册了一条路由
const routes = src.match(/router\.(get|post|put|patch|delete)\s*\(/g) ?? [];
add(routes.length > 0, `注册了路由(发现 ${routes.length} 条)`);
// 2) 每条路由都挂了 validate() 入参校验(启发式:出现次数不少于路由数)
const validates = src.match(/\bvalidate\s*\(/g) ?? [];
add(
routes.length > 0 && validates.length >= routes.length,
`validate() 覆盖路由(${validates.length} 处 / ${routes.length} 条)`
);
// 3) 统一错误出口:asyncHandler 包裹,或 try/catch 后 next(error)
const wrapped = /asyncHandler\s*\(/.test(src);
const tryNext = /catch\s*\(/.test(src) && /\bnext\s*\(\s*(err|error)\b/i.test(src);
add(wrapped || tryNext, "统一错误出口(asyncHandler 或 try/catch + next(error))");
// 4) 路由层不写业务:不允许出现 SQL 语句片段(业务逻辑应在 services 层)
const sqlSnippet = /(\bselect\b[\s\S]{0,120}\bfrom\b)|(\binsert\s+into\b)/i.test(src);
add(!sqlSnippet, "路由文件无 SQL 片段(业务逻辑应在 services 层)");
// 5) 给出资源名时,检查路由已在 app.ts 挂载
if (resource) {
const appFile = join(repoRoot, "server/src/app.ts");
if (existsSync(appFile)) {
const appSrc = readFileSync(appFile, "utf8");
add(
appSrc.includes(`/${resource}`),
`路由已在 app.ts 挂载(/api/v1/${resource})`
);
} else {
add(false, "未找到 server/src/app.ts,无法验证挂载");
}
}
let failed = 0;
for (const { ok, label } of checks) {
console.log(`[${ok ? "OK" : "FAIL"}] ${label}`);
if (!ok) failed += 1;
}
console.log(failed === 0 ? "全部通过" : `未通过 ${failed} 项,对照 SKILL.md 修复后重跑`);
process.exit(failed === 0 ? 0 : 1);
一点诚实的说明:这是启发式检查(正则匹配而非 AST 解析),目标是抓"路由没挂校验""业务写进路由层"这类高频疏漏,不追求编译器级的完备。脚本求确定与够用,不求完备——过度工程化校验脚本本身就是在制造新的维护负担。
目标:把 9.5 的 add-api-endpoint 落地为 BookNest 的第一个项目级 Skill,并跑通一次真实触发。
第一步:创建目录并落盘。 在 BookNest 仓库根目录(Git Bash):
mkdir -p skills/add-api-endpoint/{scripts,references,assets}
然后把 9.5.2 的 SKILL.md 与 9.5.4 的 check_endpoint.mjs 原样写入对应文件(scaffold.mjs、testing-guide.md、route.ts.tmpl 可先占位,用到再补)。
第二步:补齐 references/api-conventions.md。 这是正文两处指路的目标文件,节选如下:
# API 约定参考:错误码表与校验规则
## 统一错误格式
所有失败路径返回 HTTP 状态码 + JSON:{ "code": "<机器可读码>", "message": "<人话>" }
| code | HTTP | 含义 | 典型场景 |
|---|---|---|---|
| VALIDATION_ERROR | 400 | 入参未过校验 | 缺 book_id、rating 不在 1–5 |
| NOT_FOUND | 404 | 资源不存在 | bookId 无对应记录 |
| CONFLICT | 409 | 唯一约束冲突 | 标签名重复(tags.name UNIQUE) |
| INTERNAL | 500 | 未预期错误 | 记录 requestId,不外泄堆栈 |
## 校验规则
- rating:1–5 整数,可空;
- status:枚举 wish | reading | done(想读/在读/读完);
- cover_url:https URL,域名须过 scripts/check-cover-url.mjs 白名单。
第三步:入库。 Skill 是纯文本,随代码走 Git 与评审(NFR-5 的 Conventional Commits):
git add skills/
git commit -m "feat(skills): add add-api-endpoint skill with endpoint checker"
第四步:跑通一条真实触发对话。 会话里自然提需求,验证语义匹配能否命中:
你:给 fragments 加一个批量归并接口:选中几条速记,合并成一条笔记,
挂到指定的书上。
代理:(任务含"加一个……接口"→ 语义命中 add-api-endpoint,L2 正文加载)
本技能固定五步,开始执行:
1. 先读 references/api-conventions.md,确认失败码用 VALIDATION_ERROR 与 NOT_FOUND;
2. 服务层:server/src/services/fragmentService.ts 新增 mergeFragmentsToNote(ids, bookId);
3. 路由:server/src/routes/fragments.ts 新增 POST /merge,挂 validate(mergeSchema) 校验 ids 与 bookId;
4. 测试:server/tests/fragments.merge.test.ts 覆盖归并成功与空 ids 返回 400 两条路径,npm test -- fragments.merge → 3 passed;
5. 错误格式:失败路径统一 { code, message }。
自检:node skills/add-api-endpoint/scripts/check_endpoint.mjs server/src/routes/fragments.ts fragments → 5 项 [OK],退出码 0。
汇报:新增 2 个文件、修改 3 个文件;测试 3 passed, 0 failed;校验脚本退出码 0。
验收三问:git status 确认 skills/ 已入库;合上电脑,你能说出这个 Skill"何时触发、哪五步、怎么算完成"吗;调试期再用显式调用 /add-api-endpoint 保底跑一次。隐式触发是否稳定命中,别凭感觉宣布——第 10 章用样本集给出定量答案。
| 误区 | 典型现象 | 纠正方法 |
|---|---|---|
| description 写成空洞口号 | "处理文档相关各种任务",什么任务都对不上号 | 写"做什么 + 何时用",引用用户会说的原话当触发词 |
| 正文写成给人看的教程 | 开篇讲框架原理,代理读完仍不知道第一步干嘛 | 交接文档心态:祈使句、编号步骤、每步带路径与命令 |
| 判断标准写成主观形容词 | "确保代码质量良好""合理处理错误" | 翻译成可判定 checklist:退出码 0、错误格式逐项核对 |
| 确定性工作让模型即兴发挥 | 同一脚手架每次生成的格式都不一样 | 生成、校验、统计交给 scripts/,正文只写"跑哪个脚本" |
| references 无人知晓 | 参考文件写好了,代理从不读,细节等于不存在 | 正文显式指路:"动手前先读 references/xxx.md" |
写出优秀的 Skill 是一条流水线:先用三个判据(重复 ≥ 3 次、流程固定、含私有约定)筛掉不值得做的任务;再把一半精力押在 description 上——它是 L1 唯一入口,要同时说出"做什么"与"何时用",并埋入用户的口语触发词;正文是写给代理的新同事交接文档:命令式、步骤化、判断标准 checklist 化、输出格式约定化,细节外移 references/ 且必须指路;资源按 scripts/(确定性)、references/(深知识)、assets/(骨架)各司其职。9.5 的 add-api-endpoint 走完了全流程,Skill 至此"能用了"——但"能触发一次"与"每次都触发对、做得好"之间,还隔着一整个调试与进化的闭环,这正是下一章的主题。
skills/db-migration/ 并按 Conventional Commits 提交。第 10 章将对它执行触发测试与修订的完整闭环。第 09 章的 add-api-endpoint 已经"能用了",但写完不等于稳定:它可能在该出场时沉默(不触发)、不该出场时抢戏(误触发)、出场了又漏三步(没做好)。Skill 库不是一次写出来的,是持续养出来的——本章给出一套调试、评测、进化与团队治理的完整闭环,让技能越用越准。
Skill 的失败几乎都能归入三类。诊断的第一原则:先定位失败发生在哪一级——是没进 L2(触发问题),还是进了 L2 但执行不好(正文问题)。
现象:你说出典型任务话术,代理却按通用方式自由发挥——五步约定没人执行,错误格式各行其是。
常见根因(按概率排序):description 太弱(口号化、缺关键词,见 9.2.3);用户的表述与 description 词汇脱节(你说"落库",它只写了"数据库");放置位置错误(项目 Skill 放进了个人目录,反之亦然,见 8.4.3);frontmatter 语法错误导致整个 Skill 加载失败。
诊断步骤:
/add-api-endpoint 试一次——能正常加载正文,说明文件本身没问题,问题出在触发匹配(description),转入第 2 步;连显式调用都失败,问题在文件本身:查目录层级、frontmatter 语法、name 是否合法(小写连字符)、放置位置与生效范围;现象:纯前端任务被拉着加了后端接口,写一次性脚本却套上了完整流程。它比不触发更危险——用户稍不留神,产出就悄悄跑偏,甚至改坏无关文件。
常见根因:description 太宽泛("任何功能都找我");示例词撞车(description 里写了"加筛选",前端任务也说"加筛选");缺少"不适用于"排除清单。
诊断步骤:
现象:Skill 加载了,产出仍有缺项——没跑校验脚本、没读 references、漏了测试步骤,每次漏的还不一样。
常见根因:正文有洞(某步写成了"建议"而非"必须");关键细节缺失(错误码表根本没写);指路句缺失导致 L3 没发生;步骤过长导致注意力被稀释。
诊断步骤:复现一次,把代理的实际执行与 SKILL.md 逐条对照——它读了什么、跳了什么:
两类问题用两刀切开:第一刀用显式调用切分"文件问题 vs 触发问题";第二刀用执行记录切分"没写 vs 写了没执行"。每刀之后,问题空间缩小一半,通常两三轮就能定位到具体行。日常排查先用这张速查表对号入座:
| 你看到的现象 | 失败模式 | 第一检查点 |
|---|---|---|
| 说了典型话术,代理自由发挥 | 不触发 | description 缺哪个关键词 |
| 换个项目/目录就不触发 | 不触发 | 放置位置与生效范围 |
| 不相干任务被拉去跑流程 | 误触发 | description 的磁铁词与排除清单 |
| 触发了但每次漏的步骤不同 | 没做好 | 步骤是否"必须"语气 + checklist |
| 触发了但细节总出错 | 没做好 | references 是否写了、是否被指路 |
把"感觉能触发"变成"测量出触发率":
记录表模板(可直接存进 docs/tasks/):
# add-api-endpoint 触发测试记录(2026-10-06,模型:<name>)
| # | 任务表述(原话) | 期望 | 实测 |
|---|--------------------------------------|--------|--------|
| 1 | ... | 触发 | 触发 |
| 6 | ... | 不触发 | 误触发 |
结论:正例命中 5/5;反例误触发 1/3 → 待修订。
触发对了只是及格,做得对才是目的。为关键 Skill 建立金标准样例(golden sample):一条代表性任务 + 期望产出的完整样例——文件清单、错误格式、测试结果形态。存放在 docs/tasks/skill-<name>-golden.md,此后每次改动 SKILL.md 或脚本,都重跑同任务并与金标准对比(diff 产出差异)。这就是 Skill 世界的回归测试(regression test):测试用例就是这些"任务—期望产出"对,改坏了立刻现形。
金标准样例不必长,关键是可对照:
# add-api-endpoint 金标准样例(v1,2026-10-06)
- 任务:给 tags 加"按书名合并重复标签"的接口;
- 期望文件:routes/tags.ts(+1 路由)、services/tagService.ts(+1 方法)、
tests/tags.merge.test.ts(2 用例:成功 / 重名 CONFLICT);
- 期望行为:路由无 SQL;validate() 覆盖;失败路径 { code: "CONFLICT" };
- 期望自检:check_endpoint.mjs 退出码 0;npm test 3 passed。
最宝贵的改进素材是每一次失败会话——尤其是你纠正代理的那个瞬间(你打断它的时刻,就是现实与 Skill 预期不符的时刻)。复盘用一个强制具体的句式:"下次遇到 X 应该 Y",X 是任务特征,Y 是可执行动作,然后按内容选落点:
| 教训的形态 | 落点 |
|---|---|
| 固定动作("每次都要先查错误码表") | 正文新编号步骤或 checklist 加一项 |
| 深知识("分页参数有默认值约定") | references/ 新条目,正文指路 |
| 触发问题("前端任务不该找我") | description 限定词或排除清单 |
| 确定性操作("序号必须连续") | scripts/ 脚本校验,checklist 引用退出码 |
复盘记录示例(存 docs/tasks/,Git 留痕):
# Skill 复盘:add-api-endpoint 误触发(2026-10-06)
- 现象:纯前端任务"侧边栏加筛选"误触发(触发测试样本 #6);
- 根因:description 的示例短语"给 books/notes/tags 加筛选"未限定后端语境,与前端任务撞车;
- 行动:下次遇到近邻任务,description 必须带范围限定与"不适用于"清单
→ 当天修订 description 与正文"何时使用"段;
- 验证:重跑 8 条样本集(期望 8/8)+ 金标准回归(期望无漂移)。
SKILL.md 没有强制的版本字段——版本化靠 Git:Conventional Commits 区分改动性质(fix(skills): 修触发、feat(skills): 加步骤、refactor(skills): 重构目录),提交信息写清三件事:改了什么、为什么、跑了哪些样本。节奏上避免两个极端:
重大重构(目录调整、脚本重写)在提交信息标注影响面,团队库可打 tag 标记里程碑。
对 description 的改版别凭语感拍板,做 A/B 试验:
# add-api-endpoint description A/B 记录(2026-10-06,样本集 8 条)
- A 版(原版,示例词未限定后端语境):正例 5/5,误触发 1(样本 #6);
- B 版(示例词钉进后端语境 + 不适用于清单):正例 5/5,误触发 0;
- 结论:取 B 版;B 版提交号 <hash>,记录人 <name>。
示例结论写法:"A 版正例 4/5、误触发 0;B 版正例 5/5、误触发 1 → 取 A 版,并把 B 版缺失的那条正例词合并进来复测。"——合并后必须第三轮复测,因为两个优点拼在一起未必还是优点。
团队 Skill 库放 <repo>/skills/,与代码同库同评审(附录 D 的目标结构已预留)。Skill 是纯文本文件夹——评审、回滚、追溯全部复用 Git 与 PR 的既有设施,把它当代码资产管理,而不是文档附件。个人方法论仍放用户目录(8.4.3),不进团队库。
tool-helper-v2(看不出干嘛)、endpoint(缺动词与对象)、AddEndpoint(大小写破坏约定)。团队 PR 的 Skill 评审四查,逐项可判定:
□ 可触发:description 含用户口语触发词?附 ≥5 正例 + ≥3 反例的测试记录?
□ 可执行:每个步骤动词有明确对象、路径写全?完成标准可判定(非"质量良好")?
□ 脚本安全:无凭据字面量(密钥全走环境变量)?涉网脚本有域白名单?
有 --help 与明确退出码约定?
□ 指路完整:references/ 与 assets/ 的每个文件都被正文指路?
(没被指路的文件等于不存在,应删除或补指路)
五步闭环,与本章方法一一对应:
Skill 正文常需要引用全局约定(错误格式、分层规则)。纪律是引用,不复制:"错误码表见 references/api-conventions.md;全局分层约定以 AGENTS.md 为准。"理由:同一知识出现两处,必有一处在说谎——AGENTS.md 改了统一错误格式,Skill 里的副本三个月没人动,代理就会读到过期版本。这就是单一事实源(single source of truth)原则在 Skill 库的应用。
Skill 正文写"密钥零字面量"是教——模型大概率遵守,但那是概率;PreToolUse hook 扫描暂存区并拦截是强制——确定发生。两者不是重复,而是分工:Skill 负责"为什么要这样做、怎么做更对"的方法论,hooks 负责"无论如何不许越过"的硬底线。凡是出错代价不可逆的规则(凭据、删库、外发数据),必须落在 hooks 这类确定性机制上(第 07、13 章),Skill 正文只做解释与指引。
Skill 回答"怎么做"(方法论注入当前上下文);subagent 回答"谁来做"(独立上下文并行执行、结果回传)。组合拳在多 Agent 场景尤其有效:主代理触发 Skill 拿到标准流程后,把自包含的子任务派给多个 subagent 并发执行——每个 subagent 的提示词里都带上同一条 Skill 规则,保证干法一致。架构模式的展开在第 17 章。
| 机制 | 回答的问题 | 生效方式 | BookNest 例子 |
|---|---|---|---|
| AGENTS.md | 项目是什么样的 | 会话启动常驻 | 分层约定、NFR 清单 |
| Skill | 这类任务怎么干 | 任务匹配后按需加载 | add-api-endpoint 五步 |
| Hooks | 底线如何强制 | 生命周期确定性执行 | 提交前凭据扫描 |
| Subagent | 这活谁来干 | 主代理派发自包含任务 | 多端点并行实现 |
目标:盘点 BookNest 的 Skill 库全貌,对 add-api-endpoint 跑第一轮触发测试,并据结果做一次真实修订。
第一步:skills/ 目录组织总览。 三个候选(第 08 章裁决)的现状与状态:
booknest/
├── AGENTS.md
├── docs/tasks/ # 测试记录、复盘记录、金标准样例归档
└── skills/
├── add-api-endpoint/ # 候选① 已上线;本章完成首轮触发测试与修订
│ ├── SKILL.md
│ ├── scripts/
│ │ ├── scaffold.mjs
│ │ ├── check_endpoint.mjs
│ │ └── check-cover-url.mjs
│ ├── references/
│ │ ├── api-conventions.md
│ │ └── testing-guide.md
│ └── assets/route.ts.tmpl
├── db-migration/ # 候选② 骨架(第 09 章练习 4 产物),待触发测试
│ ├── SKILL.md
│ └── scripts/check_migration.mjs
└── release-check/ # 候选③ 纯清单技能:单个 SKILL.md
└── SKILL.md
第二步:跑触发测试。 8 条真实表述(5 正 3 反),每条开全新会话:
| # | 任务表述(原话) | 期望 | 修订前实测 |
|---|---|---|---|
| 1 | 给 fragments 加一个批量归并成笔记的接口 | 触发 | 触发 |
| 2 | books 要能按标签筛选 | 触发 | 触发 |
| 3 | 新增一个 DELETE /api/v1/notes/:id 端点 | 触发 | 触发 |
| 4 | 给书加个改封面 URL 的 API | 触发 | 触发 |
| 5 | 我要能删标签,加个接口 | 触发 | 触发 |
| 6 | 给 BooksPage 加个按标签筛选的侧边栏 | 不触发 | 误触发 |
| 7 | 写个脚本从 CSV 导入书单 | 不触发 | 未触发 |
| 8 | 把首页统计卡片改成响应式布局 | 不触发 | 未触发 |
结论:正例 5/5 命中;反例 3 条中 1 条误触发(#6)——失败模式二,需诊断。
第三步:复现与诊断。 重开新会话复现样本 #6:
你:给 BooksPage 加个按标签筛选的侧边栏。
代理:(误加载 add-api-endpoint)好的,我按固定五步先新增一个
GET /api/v1/books?tag= 的接口……
你:停——这是纯前端改动,不需要新接口。
代理:明白,已撤销路由改动。复盘:description 的示例词"加筛选"与
前端任务撞车,示例短语未限定后端语境,也缺少"不适用于"排除清单。
诊断(按 10.1.2 步骤):磁铁词是"加筛选"与"功能";description 没有把任务钉在"后端"语境;正文"何时使用"段也未划前后端边界。
第四步:真实修订。 两处修改,一次 commit:
--- a/skills/add-api-endpoint/SKILL.md
+++ b/skills/add-api-endpoint/SKILL.md
@@ description(frontmatter)
- 当用户说"帮我加个接口"、"新增一个端点"、"给 books/notes/tags 加筛选、
- 删除、归并等能力",或实现的功能包含新 HTTP 路由时使用;覆盖路由、服务层、
- 入参校验、接口测试与统一错误格式,完成标准是 check_endpoint.mjs 全部通过
- 且新用例跑绿。
+ 当用户要求为后端"加一个接口/端点/API"、给某资源"加筛选/删除/归并"等
+ 能力,或实现的功能包含新 HTTP 路由时使用;不适用于纯前端页面、一次性
+ 数据脚本与数据库迁移。覆盖路由、服务层、入参校验、接口测试与统一错误
+ 格式,完成标准是 check_endpoint.mjs 全部通过且新用例跑绿。
@@ "何时使用"段(正文)
用户要求新增后端接口、端点、API 路由,或前端功能需要新的后端能力;本技能只管"新增",修改已有端点时只执行第 1–3 步并保持既有测试通过。
+以下情况不要使用本技能,直接按需求实现即可:
+- 纯前端改动(新页面、新组件、样式调整)——遵循 web/AGENTS.md;
+- 一次性数据脚本(如 CSV 导入书单);
+- 数据库 schema 变更——改用 db-migration 技能。
三处修改各自的道理:给示例词加"后端"限定,把"加筛选"钉进后端语境(改磁铁词);"不适用于"清单写进 description——L1 阶段就拦住语义近邻,不必等正文加载(改触发面);正文排除清单给每类排除项指出出路(web/AGENTS.md、db-migration),避免任务落进没人管的真空(改边界)。
第五步:回归验证与入库。 修订后重跑全部 8 条样本,逐条记录:
| # | 期望 | 修订后实测 |
|---|---|---|
| 1–5(正例) | 触发 | 全部触发,无一误伤 |
| 6(前端侧边栏) | 不触发 | 不触发 |
| 7–8(CSV 脚本 / 响应式布局) | 不触发 | 不触发 |
8/8 符合预期;金标准回归重跑样本 #1 任务,产出与 docs/tasks/skill-add-api-endpoint-golden.md 无漂移。复盘记录按 10.3.1 模板归档,修订入库:
git add skills/ docs/tasks/
git commit -m "fix(skills): scope add-api-endpoint to backend routes
误触发修复:纯前端任务'侧边栏加筛选'命中本技能(样本 #6)。
description 示例词钉进后端语境,补“不适用于”清单;正文补排除清单。
触发测试 8/8 通过;金标准回归无漂移。"
至此一个完整闭环走完:测试暴露问题 → 二分诊断 → 小步修订 → 回归验证 → Git 留痕。BookNest 的 Skill 库从"能用"进入了"越用越准"的轨道。
| 误区 | 典型现象 | 纠正方法 |
|---|---|---|
| 只在写 Skill 的会话里自测 | 当场百发百中,新会话就不触发 | 每条样本开全新会话,用干净上下文测 |
| 触发失败就堆关键词 | description 越写越长,误触发率飙升 | 用正反例样本集定量验证;收窄优先于堆砌 |
| 一次重写整个 Skill | 说不清哪处改动起了效,越改越差 | 小步修改:一次一处,改前记录、改后立即验证 |
| 把 AGENTS.md 约定复制进 Skill | 两处正文漂移,代理读到过期版本 | Skill 引用约定("以 AGENTS.md 为准"),单一事实源 |
| 失败教训留在聊天记录里 | 同样的坑三周后再踩一遍 | 当天复盘,用"下次遇到 X 应该 Y"落进 Skill 文件 |
| 该 hooks 强制的写进 Skill 正文 | "别提交密钥"靠自觉,偶发失守 | 方法进 Skill,硬底线进 hooks(确定性执行) |
Skill 的进化是一个测量驱动的闭环:三类失败(不触发、误触发、触发了没做好)用"二分定位法"两刀归因——显式调用切分文件问题与触发问题,执行记录切分"没写"与"写了没执行";评测靠两件工具——正反例样本集量化触发、金标准样例守护产出质量;进化靠纪律——小步修改保证可归因,"下次遇到 X 应该 Y"把每次失败会话变成 Skill 文件里的新增行,Git 与 Conventional Commits 记录每一次"为什么";团队治理则把个人手艺变成组织资产:skills/ 进库、动词短语命名、四查评审、新人五步贡献。最后,Skill 不是孤岛——约定引用 AGENTS.md 保持单一事实源,底线交给 hooks 确定性执行,并行执行交给 subagent(第 17 章)。至此 Part 3 收官:你的代理已经有了"开机即懂"的配置与"随取随用"的技能库,下一部分转向更根本的问题——如何设计边界,让代理敢放手干又干不出格。
docs/tasks/——如果某条结果出乎意料,先按 10.1 诊断再改。docs/tasks/。这份档案就是你团队 Skill 库治理的开端。进入 Part 4「边界与安全」。第 7 章回答了"怎么写一条权限规则",本章往后退一步,回答设计层的问题:面对一个能读写文件、执行命令、访问网络、触发外部服务的代理,你的信任要怎么划分、怎么表达、怎么收放? 边界划得好,你才敢单开一个会话让它自主跑半小时;划得不好,每一次放手都是赌博。
代理的失败分两种,性质完全不同:
一个反直觉的事实:模型越强,第一类失败在减少,第二类反而在放大。 代理越能干,能触达的东西越多——它自己装依赖、自己跑迁移、自己调外部 API;每多一项能力,越权面就大一分。第 2 章讲过,工具调用(tool use)让代理从"聊天"变成"干活",本章讨论的就是干活的半径怎么控制。
多数人的第一反应是在 AGENTS.md 里写"请小心,不要删除文件,不要把密钥写进代码"。这有用吗?有用,但不可靠。 项目指令本质上是对模型的"劝说"——模型会尽力遵守,但遵守是概率性的:上下文长了会淡忘、任务复杂了会顾此失彼、遇到一次提示注入(prompt injection,外部内容里藏着诱导代理的恶意指令)就可能整个抛在脑后。
护栏(guardrail)的定义恰恰相反:不依赖模型"想起来",而是让越权动作在技术上无法发生,或发生时立即被拦截。 两者的关系:
| 叮嘱(AGENTS.md) | 护栏(规则 / hooks / 沙箱) | |
|---|---|---|
| 本质 | 道德劝告 | 技术约束 |
| 执行者 | 模型自觉 | 运行时强制 |
| 失效方式 | 遗忘、被诱导 | 只能被显式修改(走评审) |
| 定位 | 引导行为倾向 | 划定不可逾越的红线 |
结论一句话:边界 = 把信任编码成技术约束。 叮嘱继续写(它决定代理"往哪走"),边界必须另配(它决定代理"不能去哪"),两者不可互替。
为一个代理(或一支代理团队)做边界设计,最终落进仓库的是三样东西:
审查代理权限面最怕"想到哪算哪"。下面五个维度构成一张完整检查单——代理的一切行为都能归进这五类。每个维度先看一个真实感很强的失控故事,再给最小护栏写法。
失控故事:周五晚上,你让代理"清理一下项目里没用的东西"。它翻了一圈,判断
data/目录"像是缓存产物",把data/booknest.db(你三个月的读书数据)连同scripts/backup.mjs一起删了。周一你打开应用,一切归零。
分析:代理对"没用"的判断和你不一样——它没有"数据会丢"的概念,只有文件相似性。文件系统边界要回答:读、写、删,分别允许落在哪些路径上? 最小护栏(第 7 章语法):
{
"allow": ["Edit(server/**)", "Edit(web/**)"],
"deny": ["Read(data/**)", "Edit(data/**)", "Write(data/**)"]
}
失控故事:代理跑测试时报
EACCES权限错误。它"聪明地"自我修复,执行了sudo chown -R $(whoami) /usr/local/lib——你盯着屏幕愣了三秒,系统目录属主已经被改了。另一次,它为了装依赖,执行了从某 README 里抄来的curl … | bash一键脚本。
分析:执行边界要回答:允许运行哪些命令?允许执行谁提供的脚本? 它是五个维度里最容易被绕过的(换个写法就绕过前缀匹配),所以要规则与 hook 双层设防。前缀规则拦不住 curl … | bash 这类动态拼接,那是第 13 章 PreToolUse 黑名单 hook 的活:
{
"allow": ["Bash(npm test:*)", "Bash(npm run lint)"],
"deny": ["Bash(sudo:*)", "Bash(rm -rf:*)"]
}
失控故事:调试一个报错时,堆栈里带着你内网接口的 URL 和一段用户数据。代理"想到"可以把日志贴到某个在线解析服务,POST 了出去——内网拓扑和敏感数据就此离开了你的机器。
分析:网络边界要回答:代理能向哪些域名发请求?默认能不能出网? 白名单(allowlist)不仅限流,还是 SSRF(Server-Side Request Forgery,服务端请求伪造)防护的第一道闸门——代理只能访问你点名过的域,而不是任意 URL:
{
"allow": [
"WebFetch(domain:nodejs.org)",
"WebFetch(domain:developer.mozilla.org)"
]
}
未列出的域名全部落入默认模式询问,等于"默认不出网"。
失控故事:代理为排查连接问题读了
.env,拿到数据库密码。之后它"贴心地"生成了一篇《部署指南.md》,把连接串原样贴了进去——文档进了 Git,密码进了提交历史,全组可见。
分析:数据边界要回答:代理能读到哪些密钥、凭据与隐私数据?读到了之后能流向哪里? 两个要点:入口上 deny 掉密钥文件;出口上防"复制进代码 / 文档 / 会话"——第 7 章的 Stop 凭据扫描 hook 就是出口闸门:
{
"deny": ["Read(.env)", "Read(.env.*)", "Read(**/.env*)", "Read(secrets/**)"]
}
密钥永远通过环境变量注入(如 process.env.BOOKNEST_DB_PATH):代理只见到变量名,见不到值。
失控故事:你让代理"帮忙处理一下 issue"。它用自己的 GitHub 凭据,给一个不相关的开源项目发了条措辞客气的评论。又一回,"把新版本部署一下"被理解成直接对生产环境执行了发布脚本。
分析:前四个维度的爆炸半径都在本机,影响边界管的是对外部世界做的不可逆动作:发 issue、发评论、发消息、部署、扣费、删资源——对端是真实世界,撤不回来:
{
"defaultMode": "default",
"ask": ["Bash(git push:*)"],
"deny": ["Bash(git push --force:*)"]
}
外加一条纪律:MCP(Model Context Protocol)工具按需启用(第 6 章)——这周不碰 issue 就别挂载 mcp__github,工具不在场,误用就不可能发生。
| 维度 | 核心问题 | 失控关键词 | 首选护栏载体 |
|---|---|---|---|
| 文件系统 | 能读写删哪些路径? | "清理"、批量修改、误删数据目录 | 路径级 allow / deny |
| 执行 | 能跑哪些命令与脚本? | 自我修复、sudo、管道脚本 | Bash 规则 + PreToolUse hook |
| 网络 | 能访问哪些域?能否出网? | 上传日志、访问陌生域、SSRF | WebFetch 域白名单 |
| 数据 | 能读哪些密钥与敏感文件? | 读 .env、写进文档、贴进会话 |
Read deny + 凭据扫描 hook |
| 影响 | 能触发哪些外部变更? | 发 issue、部署、发消息 | ask 关口 + MCP 按需启用 |
新开一个项目、或接入一个新的 MCP 服务时,把这张表过一遍——五分钟,换一份完整的权限面认知。
以 ZCode 为例(截至 2026 年 10 月,Claude Code 的 default / plan / acceptEdits / bypassPermissions、Codex CLI 的 suggest / auto-edit / full-auto 与之一一对应):
| 模式 | 行为 | 适用 | 风险 |
|---|---|---|---|
| plan(计划/只读) | 只能读与分析,产出方案,不能改 | 探索陌生代码、定方案 | 几乎为零 |
| default(默认确认) | 未命中 allow 的动作一律询问 | 日常开发 | 确认疲劳 |
| 白名单自动执行 | allow 清单 + acceptEdits 放开编辑 | 高信任重复任务 | 清单沉淀出错 |
| bypassPermissions(yolo,全放行) | 全部免确认 | 仅限隔离环境(第 12 章) | 一切损失无护栏 |
| 任务阶段 | 推荐模式 | 典型动作 | 关键约束 |
|---|---|---|---|
| 探索陌生代码、定方案 | plan | 读代码、画结构、写规格 | 方案人审通过后才动手(第 15 章) |
| 日常开发(熟悉模块) | default + 精选 allow | 写代码、跑测试、查文档 | commit / push / install 保持 ask |
| 高信任重复任务 | 白名单自动执行 | 批量重命名、格式化、生成样板 | 任务范围封闭、动作幂等、有检查点 |
| 隔离环境批量执行 | 完全托管(yolo) | 容器里跑评测、批量迁移演练 | 爆炸半径由环境封顶(第 12 章) |
放权的原则是证据驱动(evidence-based trust):
要点:松是攒出来的,紧是随时可回的。 把这个节奏写进团队约定,比争论"到底该不该开 yolo"有效得多——正确答案是"看阶段"。
最小权限原则(principle of least privilege):任何主体(人、程序、代理)只应拥有完成当前任务所需的最小权限集合。落到代理身上,是四步循环。
defaultMode 永远设为"默认确认"而不是放行——没有明确允许的,就都需要过问。配置权限清单时按 deny → ask → allow 的次序写:先划红线,再立关口,最后才给便利。理由有二:deny 的判定优先级最高(第 7 章);更重要的是,"先写 deny"会强迫你先想清楚什么绝不能发生,而不是先想代理能帮什么忙。
任务需要什么,才临时放行什么,而且开在最小范围:要装一个包,开 Bash(npm install left-pad:*) 而不是 Bash(npm install:*);要看一个日志,开 Read(logs/2026-10-06.log) 而不是 Read(logs/**)。
临时开洞的规则放 settings.local.json(不进 Git 的个人层),任务完成就删。经验法则:每场会话开始时,settings.local.json 里不该有上一场任务的残留。 一条规则连续三场任务都被用到,才值得评审后上移到项目共享层。
权限面只增不减是常态,所以要定期收:
git log -p .zcode/settings.json 复盘每条新增规则,问一句"还需要吗";对话示例(权限面自审):
你:读一遍 .zcode/settings.json 和 settings.local.json,按五个边界
维度(文件系统/执行/网络/数据/影响)汇报当前暴露面,标出可疑项。
代理:文件系统:可写 server/** web/**,data/** 已 deny——正常;
执行:可跑 lint/test/tsc,install/commit 需确认——正常;
网络:仅 MDN 与 nodejs.org 两个域——正常;
数据:.env* 已 deny;但 settings.local.json 里有一条
Read(docs/ops/**) 是上周调试时开的洞,docs/ops 存有运维
笔记,建议移除;
影响:未挂载可写型 MCP 工具——正常。
你:把那条 Read 规则删掉。
没有哪一层护栏是完美的,工程上的答案是纵深防御(defense in depth):多层机制各管一段,任何一层被突破,下一层接住。
| 层 | 机制 | 裁决方式 | 拦得住 | 失效时谁兜底 |
|---|---|---|---|---|
| L1 权限规则(第 7 章) | allow/ask/deny 静态白名单 | 声明式、零开销 | 已知的危险模式 | 语义级绕过 → L2 |
| L2 Hooks(第 13 章) | 生命周期脚本守门 | 可编程、看内容 | 需理解内容/上下文的危险 | 环境级灾难 → L3 |
| L3 沙箱与隔离(第 12 章) | 分支、容器、密钥分离 | 环境硬限制 | 一切规则被绕过后的损失上限 | 数据丢失 → 备份 |
| L4 流程约束(第 15/20 章) | 计划模式、人工检查点 | 人在回路 | 方向性错误、系统性偏差 | ——(最外层) |
flowchart TD
A["代理请求执行动作"] --> B{"L1 权限规则<br/>deny / ask / allow"}
B -- "deny,或 ask 未通过" --> X["动作不发生"]
B -- "allow" --> C{"L2 Hooks<br/>按内容与上下文裁决"}
C -- "退出码 2:拦截" --> X
C -- "放行" --> D["L3 沙箱与隔离<br/>动作在受限环境内执行<br/>(分支 / 工作目录 / 容器 / 出网白名单)"]
D --> E{"L4 流程约束<br/>计划先行 + 人工检查点"}
E -- "人审打回" --> R["回滚到上一个检查点"]
E -- "人审通过" --> OK["变更落地"]
读这张图的方式:假设每一层都会失守。 L1 的前缀匹配被 npm i 绕过时,L2 的黑名单还在;L2 漏了新姿势时,L3 的沙箱封住损失;L3 也没兜住(比如数据被删),备份是最后一道。下一章就专门讲 L3 怎么搭。
目标:为 BookNest 设计一份三档边界矩阵——开发期 / 联调期 / 发布期,落成文件 docs/boundary-matrix.md 提交进仓库(它属于第 20 章"协同治理"的团队资产),并明确升降档规则。
开发期(日常功能开发,M0–M1 阶段;是第 7 章基线配置按证据"养"出来的进化版):
| 维度 | 规则 |
|---|---|
| 文件系统 | 可写 server/**、web/**、docs/**、skills/**(编辑类从 ask 沉淀为路径 allow);deny .zcode/**(护栏自锁)、data/**、已应用的 migrations/ |
| 执行 | allow:lint / test / tsc --noEmit / npm run dev;ask:install / commit / push;deny:sudo / rm -rf |
| 网络 | 白名单仅 nodejs.org、developer.mozilla.org;其余默认询问 |
| 数据 | deny 读 .env*;开发库 data/booknest.db 不可读写;测试一律用种子测试库 |
| 影响 | git push 需确认;禁 --force;不挂载可写型 MCP 工具 |
联调期(前后端对接与 E2E,接口契约已冻结):
| 维度 | 相对开发期的变化 |
|---|---|
| 文件系统 | 增加可写 e2e/** |
| 执行 | 增 allow npx vitest run;npx playwright test 保持 ask(起浏览器,需人工观察) |
| 网络 | 增加本地域(localhost)用于联调请求 |
| 数据 | E2E 跑独立测试库 data/booknest-test.db,每次从种子重建 |
| 影响 | 同开发期;打 tag 仍为人审 |
发布期(里程碑收口、打 tag、部署):
| 维度 | 相对前两档的变化 |
|---|---|
| 文件系统 | 默认只读;仅放行 CHANGELOG.md 与版本号所在文件 |
| 执行 | npm run build 需确认;deny 一切 dev server 与测试命令(发布期不写新代码) |
| 网络 | npm registry 询问放行(发正式包);其余域一律询问 |
| 数据 | 生产密钥永不进本地会话——部署凭据只存在于 CI/CD Secret 里 |
| 影响 | tag、部署、发布公告全部由人执行;代理只准备草稿与变更说明 |
docs/spec/ 里接口规格定稿、开始写 E2E;联调期→发布期——覆盖率达标、准备收口发版。git push --force、rm -rf、读取生产密钥——写在共享层 deny 里,与档位无关。git add docs/boundary-matrix.md
git commit -m "docs: BookNest 三档边界矩阵与升降档规则"
对话示例——每次升降档后,让代理对照矩阵自检配置是否跟上了:
你:我们要进入联调期了。读 docs/boundary-matrix.md,对照当前
.zcode/settings.json,列出需要新增或放开的规则清单(先别改)。
代理:按联调期档位,需要三处变更:
1. allow 增加 Edit(e2e/**);
2. allow 增加 WebFetch(domain:localhost);
3. 矩阵要求"E2E 写 data/booknest-test.db",但当前 data/**
整体 deny。为测试库开目录写权限会削弱红线,我建议改为:
测试库重建一律走 npm run seed:test(白名单命令),目录保持 deny。
你:同意第 1、2 条;第 3 条采纳你的替代方案。改完给我看 diff。
注意代理在第 3 条上的表现:它没有机械执行矩阵,而是指出"为测试库开写权限"与红线的张力,并给出更符合最小权限原则的替代方案。这正是边界矩阵的价值——它把安全讨论从"每次拍脑袋"变成"对着一张图改参数"。
| 误区 | 典型现象 | 纠正方法 |
|---|---|---|
| 把 AGENTS.md 的叮嘱当护栏 | 写了"严禁删除 data 目录",代理清理时照样删 | 叮嘱进 AGENTS.md 管倾向,红线进 deny 规则与 hook 管强制 |
| 一步到位全放行 | 图省事直接 yolo,直到第一次事故 | 从 default 起步,allow 靠证据逐步"养"出来 |
| 只想得到文件边界 | 文件守得很严,代理却把日志 POST 去了陌生域 | 用五维度检查单逐项过一遍权限面 |
| 开洞不收权 | settings.local.json 沉淀了一堆一次性 allow | 任务结束即删;连续三场用到才评审上移 |
| 用"加确认"解决一切 | 什么都要确认,人开始无脑回车 | 确认疲劳说明 allow 配少了,不是模式配错了 |
| 权限配置常年不变 | 半年前的清单与现在的任务面目全非 | 里程碑收口时审计权限面,diff 复盘每条规则 |
defaultMode 永远保持"默认确认",完全托管模式只属于隔离环境;guard: 前缀),让护栏成为团队资产;本章把"信任代理"从一句口号变成一套可设计、可配置、可审计的工程对象。核心结论:代理的危险不在"不会"而在"越权",叮嘱不是护栏,边界是把信任编码成技术约束;五个维度(文件系统、执行、网络、数据、影响)构成权限面的完整检查单;权限模式要与任务阶段匹配——先紧后松、证据放权、随时回紧;最小权限原则通过"默认拒绝、按需开洞、任务结束收权、定期审计"四步落地;最后,边界不是单点而是纵深,权限规则、hooks、沙箱、流程约束四层各司其职,任何一层失守都有下一层接住。
docs/boundary-matrix.md,把"开发期"档写进 .zcode/settings.json;然后完整演练一次升降档——模拟从开发期切到联调期,让代理对照矩阵列出配置变更清单,人审后应用,并以 guard: 前缀提交全部变更。第 11 章的边界设计回答"事前允许什么",本章回答"事后怎么兜底":假设规则被绕过、模型被诱导、代理犯下出乎意料的错误,损失如何被限制在可承受的范围内?这就是沙箱(sandbox)与隔离(isolation)的主题——控制爆炸半径(blast radius):任何一次失控,波及的范围都必须有事先画好的上限。
第 11 章的边界再严,也要接受一个前提假设:代理一定会犯错——不是可能,是一定。 原因有三:
所以沙箱思维的设计目标不是"不出事故",而是两条硬指标:
一句话:护栏决定事故的概率,沙箱决定事故的后果。两者要分开设计、分开验收。
| 道墙 | 拦什么 | 什么时候救你 |
|---|---|---|
| ① 权限规则(第 7/11 章) | 已知危险动作 | 代理想 rm -rf、读 .env 时直接被拒 |
| ② 沙箱与隔离(本章) | 规则被绕过后的波及范围 | 等价命令绕过规则时,破坏被环境封在圈里 |
| ③ Git 检查点 | 错误的代码变更 | 改坏了,回滚到上一个绿测提交 |
| ④ 备份与快照 | 数据级灾难 | 数据库文件被删或损坏时恢复 |
这四道墙是串联的:每道墙都假设前一道会被突破。①被绕过(比如 npm i 绕过了 npm install 的 deny),②的独立环境与容器封住波及面;②内的变更改坏了代码,③的检查点回滚;连数据都被动了,④的备份兜底。本章 12.2–12.4 依次展开②③两道墙的具体搭法;①已在第 7、11 章讲透;④属于运维常规,只需记住一条:代理可达范围内的数据必须有备份,且备份本身对代理不可达(否则等于没备份)。
Git 是成本最低、收益最高的隔离手段——你本来就在用,唯一的变化是把它从"版本管理工具"升级为"代理检查点系统"。
约定只有一条:代理的一切变更只发生在专用分支(task branch)上,主干(main)只接受人审后的合并。它的价值常被低估:这条约定把"代理把 main 搞乱了"从一场灾难降级为"删掉分支重来"。
git switch -c task/server-search-api # 任务分支命名:task/<范围-内容>
再配一道远端保险:在 GitHub / GitLab 上把 main 设为受保护分支(protected branch),禁止直推——本地约定失效时,远端会拒绝。
检查点(checkpoint)思想来自游戏:打到存档点再前进。给代理定一个提交节奏:
| 时机 | 动作 | 提交信息示例 |
|---|---|---|
| 一个测试由红转绿 | 立即提交 | test(server): 补搜索接口空查询用例 |
| 一个完整的小功能 | 提交 | feat(server): 全文搜索 API |
| 大改动拆步执行,每步完成 | 提交 | refactor(web): 抽出 useBookList hook |
| 上下文即将压缩或会话将结束 | 强制落盘 | checkpoint: 搜索接口完成 70%,进度见 docs/tasks/… |
原则一句话:每个"确认没坏"的时刻都是一个检查点,宁滥勿缺。 小提交的代价是历史略碎,收益是任何时刻都能 git revert / git restore 精确回退——这笔交换在代理时代非常划算。提交信息遵循 Conventional Commits(BookNest 的 NFR-5),回滚时也更容易定位语义。
代理开工前,工作区必须是"干净"的:要么全部已提交,要么显式暂存(stash)。脏工作区会让回滚失去意义——你分不清哪些变更来自代理、哪些是你昨天的半成品。
开工对话示例:
你:继续昨天的任务,把搜索接口写完。
代理:开工检查:当前分支 task/server-search-api,工作区干净,
上一个提交 checkpoint: 搜索接口完成 40%。
我先跑一遍现有测试确认基线全绿,再继续实现。
把"开工先 git status 并汇报"写进 AGENTS.md,让代理每场会话都从已知状态出发。
多个代理(或你与代理)同时在一个仓库里工作时,同一个工作目录就是冲突源。git worktree(工作树)让同一个仓库拥有多个独立目录,各停在不同分支:
# 主工作树留在 server 任务上
git switch task/server-search-api
# 为 web 统计页开第二个工作树:目录 ../booknest-web + 新分支
git worktree add ../booknest-web -b task/web-stats-page
booknest/ → task/server-search-api(代理 A 在这里工作)
booknest-web/ → task/web-stats-page(代理 B 在这里工作)
两个目录各有独立工作区与 node_modules(需各自 npm install),互不踩脚;分支隔离保证只有合并时才交汇。收工清理:git worktree remove ../booknest-web。第 18 章讲多 Agent 实战时,这是并行任务的标准起手式。
代理的一切文件操作都应限定在项目目录内。配置上有两手:对项目内路径给 allow;对仓库之外的路径(~/.ssh、系统目录等)不写任何 allow,靠 defaultMode 询问兜底,已知敏感区再显式 deny:
{
"allow": ["Edit(server/**)", "Edit(web/**)"],
"deny": ["Read(.env*)", "Edit(.zcode/**)"]
}
../ 相对路径逃逸的规范化校验,交给第 13 章的 PreToolUse hook 做内容级检查——规则管声明,hook 管变体。
容器(container)把文件系统、进程、网络隔成一个可丢弃的小世界,有两层用法:
BookNest 的 dev container 示意:
// .devcontainer/devcontainer.json(示意)
{
"name": "booknest-dev",
"image": "mcr.microsoft.com/devcontainers/typescript-node:22",
"postCreateCommand": "npm install && npm run seed:test"
}
记住容器的边界:它隔离文件系统与进程,不自动隔离网络与密钥——传进容器的环境变量依然可见,出网依然可达,所以 12.4 的措施在容器内同样要配。
来自代理(尤其是读过网页、issue 之后)的代码,执行前默认不可信。原则:生成代码的第一次运行,永远发生在一次性环境里——临时容器(docker run --rm)、一次性虚拟机,或至少是独立于工作区的临时目录;试跑确认行为符合预期,再进正常流程。这条对"让代理照网上方案跑个安装脚本"尤其重要:先 curl 下来人工读一遍(或让另一个代理审一遍),再执行。
规则只有一条但必须绝对:代理永远不连生产库。 落地三件套:
// server/src/db/connection.ts —— 连接信息永远来自环境变量
import Database from "better-sqlite3";
const dbPath = process.env.BOOKNEST_DB_PATH ?? "data/booknest.db";
export const db = new Database(dbPath);
第 7 章的 WebFetch(domain:…) 白名单在这里再强调一次,因为它同时是两道防线:限制代理的出网面,也是 SSRF(Server-Side Request Forgery,服务端请求伪造)防护的闸门。业务代码同理——BookNest 要按封面 URL 抓取图片,出网地址必须白名单校验:
// server/src/services/fetch-guard.ts —— 出网域白名单校验
const ALLOWED_HOSTS = new Set([
"covers.openlibrary.org",
"books.google.com",
]);
export function assertAllowedUrl(raw: string): URL {
const url = new URL(raw);
if (url.protocol !== "https:" || !ALLOWED_HOSTS.has(url.hostname)) {
throw new Error(`出网地址不在白名单:${url.hostname}`);
}
return url; // 白名单同时是 SSRF 防线:只允许访问点名的域
}
更进一步,可在系统层用 HTTP(S)_PROXY 把代理进程的出网统一导向一个带过滤策略的代理出口——应用层规则失效时还有环境层兜底(企业环境的常见做法)。
一条物理隔离规则:存有生产密钥的机器、终端与环境,不运行代理。 部署机上不开代理会话;带着生产密钥的 shell 不用于代理工作;CI 里给代理用的 runner 与部署 runner 分开。密钥跟着环境走,不跟着人走——需要"在能碰生产的地方做事",要么人来做,要么走审批过的流水线(第 19 章)。
给密钥分层,让"代理够得着的"和"丢了会疼的"尽量不重叠:
| 密钥 | 存放位置 | 代理可达性 |
|---|---|---|
| 测试用假凭据(可重建) | .env.test |
可达也无妨(本来就是假的) |
| 个人开发凭据(如 GitHub 令牌) | 系统密钥管理器 / git 凭据管理器 | 不可达:deny 读配置文件,推送走本机凭据管理器 |
| 生产部署凭据 | CI/CD Secret | 本地代理永远不可达 |
.env 文件本身也要讲卫生:chmod 600(仅本人可读写)、写进 .gitignore、对代理 deny Read(.env*)(第 7 章)。
代理需要让服务用上密钥时,正确姿势是注入环境变量,让进程"可用而不可见"——服务代码通过 process.env.XXX 读取,密钥不出现在代理可读的文件里,也就不会被写进代码或文档:
# 由你(人)在自己的终端里执行;代理只负责写代码,不碰值
set -a; source .env.dev; set +a
npm run dev
注意边界:能跑任意命令的代理理论上可以 printenv 读到进程环境,所以严格场景还要配套 deny Bash(env)、Bash(printenv:*),加上第 13 章的 Stop 凭据扫描做内容级兜底——密钥防护从来是组合拳,不是单招。
无论代理多可信、白名单多成熟,四类动作永远保留人工确认(第 11 章的 ask):
共同点:不可逆,或影响他人。 审批门不是不信任代理,而是把"最后按按钮"的权力留给承担后果的人。
| 阶段 | 适用时机 | 权限形态 | 进入下一阶段的条件 |
|---|---|---|---|
| 观察期 | 新项目 / 新代理 / 陌生代码库的前 1–2 周 | 全程 default(几乎全 ask),计划模式开路 | 连续多场会话零越权、产出稳定 |
| 限权期 | 观察期通过后 | 放开只读与验证类 allow,写与执行仍确认 | 白名单动作连续两周无需人工修正 |
| 放权期 | 长期稳定合作后 | 成熟 allow 清单 + hooks + 沙箱兜底 | 持续审计;出事即回退上一阶段 |
第 11 章的"先紧后松"讲的是节奏,这里是它的制度化版本:信任按证据放,出事降级不商量。
把任务交给云端 / 远程异步代理(挂机跑长任务、定时任务、CI 里的代理)时,人在回路变弱,隔离要更硬:
.env*、不含 data/——仓库里没有的东西泄露不了;目标:把本章的沙箱手段在 BookNest 上落成一份"配置实录"——分支与提交策略、双 worktree 并行、测试库与生产库分离、可选的容器化方案,并做一次回滚演练。
## Git 约定(BookNest AGENTS.md 节选)
- 一切变更在 task/* 分支上进行,禁止直接提交 main;
- 每个测试转绿的时点立即提交(Conventional Commits);
- 上下文压缩或会话结束前,必须以 checkpoint: 前缀提交落盘;
- push 永远需要人确认;main 在远端开启保护。
配套动作:
git switch -c task/server-search-api
# ……代理工作、小步提交……
git push origin task/server-search-api # ask 关口:人确认后推送
M1 阶段,搜索接口(server)与统计页(web)需要并行推进:
# 终端 1(主工作树)
cd booknest && git switch task/server-search-api
# 终端 2:为 web 任务开第二个工作树
cd booknest
git worktree add ../booknest-web -b task/web-stats-page
cd ../booknest-web && npm install
两个会话各自进入对应目录启动 ZCode,互不干扰;各自合并后清理:
git worktree remove ../booknest-web
server/src/db/connection.ts 从 BOOKNEST_DB_PATH 读库路径(见 12.3.4),配套脚本:
// package.json(scripts 节选;cross-env 保证 Windows/macOS/Linux 一致)
{
"scripts": {
"seed:test": "node server/scripts/seed.mjs --db=data/booknest-test.db",
"test": "cross-env BOOKNEST_DB_PATH=data/booknest-test.db vitest run"
}
}
效果:npm test 永远打测试库,且每次从种子重建;生产库文件 data/booknest.db 由 deny 规则保护(第 7 章配置的 Read(data/**) / Edit(data/**) / Write(data/**)),代理连读都读不到。
回滚演练——沙箱装好了要真的用过一次才算数:
# 让代理做一次"错误重构"之后:
git log --oneline -5 # 找到上一个绿测检查点
git revert <bad-commit-hash> # 撤销坏的提交
npm test # 确认回到全绿
当 BookNest 要兑现"5 分钟部署,10 年不换"(附录 D 愿景)时,用容器固定运行时:
# deploy/docker-compose.yml —— 本地自托管示意(可选方案)
services:
booknest:
build:
context: ..
dockerfile: deploy/Dockerfile
ports:
- "3000:3000"
environment:
- NODE_ENV=production
- PORT=3000
# 密钥经环境变量注入,禁止写进镜像或本文件
- BOOKNEST_SESSION_SECRET=${BOOKNEST_SESSION_SECRET:?请先在 .env 中设置}
volumes:
- booknest-data:/app/data # SQLite 落卷:重建容器不丢数据
volumes:
booknest-data:
配套 .env.example(进 Git 的模板;真实 .env 进 .gitignore):
# .env.example —— 复制为 .env 后填入你自己的值
BOOKNEST_SESSION_SECRET=<your-session-secret>
这套配置的三重意图:密钥零字面量(compose 只引用环境变量,缺失即启动失败);数据落卷(容器可丢弃,数据不可丢);data/ 挂载在容器内,宿主机上的代理依然按 deny 规则不可读。
你:开始 M1 的搜索接口任务。
代理:沙箱检查:
1. 分支:已创建 task/server-search-api(不在 main 上)✓
2. 工作区:干净,基线测试 42 个全绿 ✓
3. 数据库:BOOKNEST_DB_PATH 指向 data/booknest-test.db,
已用种子脚本重建 ✓
开始实现,预计 4 个提交:路由 → 服务层 → 测试 → checkpoint。
| 误区 | 典型现象 | 纠正方法 |
|---|---|---|
| 在 main 上直接跑代理 | 代理"顺手"提交污染主干,回滚伤及无辜 | 强制 task/* 分支 + 远端保护 main |
| 提交攒得太大 | 半天一个巨型 commit,想回滚却无处可退 | 绿测即提交,checkpoint 前缀强制落盘 |
| 测试直连生产数据 | E2E 跑完生产数据被清了 | 独立测试库 + 种子重建,连接串走环境变量 |
把 .env 给代理看 |
密钥进了会话上下文与生成文档 | deny 读 .env*,环境变量注入、按需给最小值 |
| 容器当银弹 | 上了 Docker 就放心开 yolo | 容器不自动限网络与密钥,白名单与密钥分离照配 |
| 放权看心情 | 今天嫌烦开 yolo,明天出事全锁死 | 按三阶段模型攒证据放权,出事降级不商量 |
| 备份从未演练过 | 出事时才发现备份恢复不了 | 定期演练回滚与恢复,沙箱要真用过一次 |
env / printenv;沙箱与隔离回答的是边界设计之后的问题:规则失效时,损失如何有上限。沙箱思维的前提是假设代理一定会犯错(甚至被提示注入),因此目标不是零事故,而是可回滚与损失封顶——护栏管概率,沙箱管后果。四道墙纵深串联:权限规则拦已知危险,沙箱与隔离封波及范围,Git 检查点保代码可回退,备份保数据可恢复,每道墙都假设前一道会被突破。落地手段上:Git 是第一道沙箱(专用分支、高频小提交、干净工作区、worktree 并行隔离);文件系统与运行时靠限定目录、容器化与临时环境试跑;网络与密钥靠出网白名单(同时是 SSRF 防线)、生产凭证物理隔离与环境变量注入;最后用审批门守住四类不可逆动作,用三阶段模型让信任随证据增长。
git log + git revert(或 git restore)在 3 分钟内回到全绿,记录你总共用了几条命令。git worktree remove 清理。grep -rnE "sk-[A-Za-z0-9]{16,}|ghp_[A-Za-z0-9]{20,}" src docs(或用 gitleaks),确认无明文凭据残留;再检查 .env 是否在 .gitignore 里、代理是否被 deny 读取。seed:test 与测试库分离;(可选)补 docker-compose;最后做一次完整回滚演练,把步骤沉淀成 docs/sandbox-runbook.md 提交。第 7 章用三个"最小可运行"的入门 hook 完成了原理验证;本章把它们升级为一套生产级守门体系:危险命令拦截、凭据防泄露、出网防护、写后检查闭环,外加工程化治理——脚本放哪、怎么测试、怎么审计。先记住本章的结论:hooks 是把"人肉盯梢"换成"机器盯梢",它是护栏(guardrail),不是安全边界(security boundary)——最后一道防线永远是第 12 章的沙箱。
读完本章,你应该能:
先复述第 7 章的框架:hook 是你注册的 shell 命令,在代理的工具调用走到特定事件点时自动执行——
用户输入 ─▶ 模型决策 ─▶ [PreToolUse] ─▶ 工具执行 ─▶ [PostToolUse] ─▶ 模型继续 …
│ 拦截 / 放行 反馈 / 格式化 / 检查
└─▶ 会话级事件:SessionStart / UserPromptSubmit / Stop / SubagentStop …
每次触发,hook 从 stdin 收到一份 JSON(字段随版本略有增减,脚本只取自己需要的、宽容读取):
{
"session_id": "u-3f9a2c",
"hook_event_name": "PreToolUse",
"tool_name": "Bash",
"tool_input": { "command": "npm test", "description": "跑接口测试" }
}
PostToolUse 额外携带 tool_response(工具的执行结果);Write/Edit 的 tool_input 含 file_path 与 content/new_string——13.3 的凭据扫描读的正是这两个字段。
| 退出码 | 语义 | 说明 |
|---|---|---|
| 0 | 放行 / 继续 | stderr 不反馈给模型 |
| 2 | 阻断并反馈 | PreToolUse 取消本次调用;Stop 阻止回合结束;PostToolUse 虽已执行,stderr 会回喂模型触发自修 |
| 其他非零 | 非阻断错误 | 流程继续,输出仅记录或展示 |
stderr 是给模型看的纠错通道:写清"为什么拦 + 该怎么办",模型下一次就照做;只写一句"不允许",它会换着花样再试。
hook 挂上生命周期后出了问题很难定位,调试铁律是先脱离会话、把样本 JSON 手动喂给脚本,观察 stderr 与退出码——
echo '{"tool_name":"Bash","tool_input":{"command":"git push --force origin main"}}' | node .zcode/hooks/guard-bash.mjs
echo "exit=$?" # 期望:stderr 打印拦截原因;exit=2
三个动作代替猜谜:构造最小输入 → 观察输出 → 改脚本重跑。确认无误后用样本集做回归回放(13.6),最后才进真实会话端到端验证。
第 7 章的 guard-bash.mjs 是四条正则的示意版;生产版的差别不在正则数量,而在结构:每条规则绑定"类别 + 原因 + 替代方案",拦截时把理由一并回喂。黑名单只收"高频且后果不可逆"的命令,罕见破坏交给权限规则与沙箱兜底(为什么,见 13.6)。先落一个所有守门脚本共用的审计模块(治理策略见 13.6):
// .zcode/hooks/lib/audit.mjs —— append-only(只追加)审计日志,各守门脚本共用
import { appendFileSync, mkdirSync } from "node:fs";
import path from "node:path";
const LOG_DIR = path.join(process.cwd(), ".zcode", "logs"); // hook 运行目录 = 项目根
export function audit(verdict, detail) {
try {
mkdirSync(LOG_DIR, { recursive: true });
const month = new Date().toISOString().slice(0, 7); // 按月分文件,天然轮转
appendFileSync(path.join(LOG_DIR, `hook-audit-${month}.log`),
`${new Date().toISOString()}\t${verdict}\t${String(detail).replaceAll("\n", " ")}\n`);
} catch { /* 审计失败不影响主流程 */ }
}
拦截器本体(BookNest 版,落盘到 booknest/.zcode/hooks/):
// .zcode/hooks/guard-bash.mjs —— PreToolUse 守门员:危险命令拦截(生产版)
import { audit } from "./lib/audit.mjs";
// 每条规则:[正则, 类别, 原因与替代方案]——后两者会原样回喂给模型
const RULES = [
[/\brm\s+(?:-[a-zA-Z]+\s+)*-[a-zA-Z]*(?:rf|fr)[a-zA-Z]*\b/, "递归强删(rm -rf 系)", "不可恢复。清理产物用 npm run clean;确需删除请人工执行并先 ls 确认"],
[/\bgit\s+push\b(?=[^\n]*--force(?!-with-lease))/, "强推远端(--force)", "销毁远端历史。改用 git fetch 确认差异,或转人工执行"],
[/\bdrop\s+(?:table|database)\b/i, "删库删表(DROP TABLE/DATABASE)", "破坏数据。schema 变更走迁移脚本;清数据请人工确认"],
[/\bchmod\s+(?:-[a-zA-Z]+\s+)*777\b/, "全开放权限(chmod 777)", "按最小权限改用 755/644 等具体值"],
[/\bmkfs(?:\.\w+)?\b|\bdd\b[^\n]*\bof=\/dev\//, "直写文件系统/块设备", "系统级损坏,永不交给代理"],
[/\b(?:curl|wget)\b[^\n|;]*\|\s*(?:sudo\s+)?(?:ba|z|da|fi)?sh\b/, "远程脚本直接执行(curl | sh)", "先下载人工审阅,再决定是否安装"],
[/:\(\)\s*\{\s*:\s*\|\s*:\s*&\s*\}\s*;?\s*:/, "fork 炸弹", "拒绝执行,无替代方案"],
];
let raw = "";
process.stdin.on("data", (d) => (raw += d));
process.stdin.on("end", () => {
let evt;
try { evt = JSON.parse(raw || "{}"); } catch { process.exit(0); } // fail-open,见 13.6
const cmd = evt?.tool_input?.command ?? "";
const hit = RULES.find(([re]) => re.test(cmd));
if (hit) {
const [, category, advice] = hit;
console.error(`[guard-bash] 已拦截危险命令(${category}):\n ${cmd}\n原因与替代方案:${advice}\n如确属必要,请说明理由,由人工执行。`);
audit("BLOCK", `Bash\t${cmd}`);
process.exit(2); // 阻断本次调用,stderr 反馈给模型
}
process.exit(0); // 放行
});
{
"hooks": {
"PreToolUse": [
{ "matcher": "Bash", "hooks": [
{ "type": "command", "command": "node .zcode/hooks/guard-bash.mjs", "timeout": 10 } ] }
]
}
}
两个工程细节:timeout(秒)是注册侧的超时保险;正则故意偏保守——黑名单是概率屏障不是全集,rm -r -f 这类散写变体拦不全,漏网的交给 deny 规则与沙箱(补齐变体留作练习)。
AGENTS.md 与 NFR-2 早就写了"密钥零字面量"——但它是软约束,靠人和模型自觉,而软约束管概率、不管底线:人会忘(赶进度时顺手贴一段含 key 的代码);模型会错——长会话经过上下文压缩后早期约定被挤出窗口,训练数据里"示例代码带密钥"的惯性就会冒出来。而且第 7 章的 Stop 扫描发生在回合结束:文件已落盘、甚至已被引用。把扫描前移到 PreToolUse(Write/Edit 落盘之前),让凭据根本进不了文件系统,这才是硬约束。两层都保留:PreToolUse 拦落盘,Stop 扫暂存区——纵深防御。
// .zcode/hooks/guard-secrets.mjs —— PreToolUse 守门员:凭据落盘前扫描
import { audit } from "./lib/audit.mjs";
// 每条规则:[正则, 类别, 是否对命中值做占位符豁免]
const RULES = [
[/\bsk-[A-Za-z0-9_-]{16,}\b/, "sk- 风格密钥(OpenAI/DeepSeek 等)", false],
[/\bAKIA[0-9A-Z]{16}\b/, "AWS AccessKeyId(AKIA…)", false],
[/\b(?:ghp|gho|ghs|github_pat)_[A-Za-z0-9_]{16,}\b/, "GitHub 令牌", false],
[/-----BEGIN [A-Z ]*PRIVATE KEY-----/, "PEM 私钥块", false],
[/\beyJ[A-Za-z0-9_-]{10,}\.[A-Za-z0-9_-]{10,}\.[A-Za-z0-9_-]{5,}/, "JWT 令牌形态", false],
[/\b(?:password|passwd|secret|api_?key|access_?token)\s*[:=]\s*["'][^"']{8,}["']/i,
"硬编码口令字面量", true],
[/\b(?:postgres|postgresql|mysql|mongodb)(?:\+\w+)?:\/\/[^\s"'@:/]+:[^\s"'@]+@/,
"连接串内嵌口令", false],
];
// 占位符豁免:<your-token>、${VAR}、process.env 引用与明显示例值不算命中
const isPlaceholder = (v) => /<[^>]+>|\$\{|process\.env|changeme|example|dummy/i.test(v);
const scan = (text) =>
RULES.filter(([re, , exempt]) => {
const m = re.exec(text);
return Boolean(m) && !(exempt && isPlaceholder(m[0]));
}).map(([, label]) => label);
let raw = "";
process.stdin.on("data", (d) => (raw += d));
process.stdin.on("end", () => {
let evt;
try { evt = JSON.parse(raw || "{}"); } catch { process.exit(0); }
if (!["Write", "Edit"].includes(evt?.tool_name ?? "")) process.exit(0);
const file = evt.tool_input?.file_path ?? "";
// Write 看全文 content,Edit 看新内容 new_string——两处都查,防止借 Edit 绕过
const content = [evt.tool_input?.content, evt.tool_input?.new_string].filter(Boolean).join("\n");
const hits = scan(content);
if (hits.length) {
console.error(`[guard-secrets] 已拦截:${file} 命中凭据规则——${hits.join("、")}。\n` +
`硬编码凭据违反 NFR-2(密钥零字面量)。请改用 process.env.XXX 注入;` +
`示例/文档用 <your-token> 形式占位。真实值由用户在终端注入,代理不应接触。`);
audit("BLOCK", `Secrets\t${file}\t${hits.join(";")}`);
process.exit(2);
}
process.exit(0);
});
注册到 PreToolUse,matcher 写 "Write|Edit"(格式同 13.2.2)。三个设计点:豁免占位符——<your-token>、${VAR} 是本书推荐的写法,误伤会训练人无脑绕过扫描("狼来了"效应);宁漏勿误——企业级换 gitleaks 等成熟工具进 CI,本脚本的价值是零依赖、毫秒级、先拦大概率;拦的是落盘不是提交——与 Stop 扫描、第 12 章密钥分离(代理环境里根本没有生产密钥)构成三层。
一句话说清 SSRF(server-side request forgery,服务端请求伪造)为何危险:出网请求从"内部"发起,可被诱导探测内网服务、访问云厂商元数据接口(如 169.254.169.254)窃取临时凭据,危害远超"多下一个包"。防护思路是域名白名单:请求目标必须命中清单,直接 IP(本地回环除外)一律拒绝。第 7 章的 WebFetch(domain:…) 权限规则管住了 WebFetch 工具,但代理可以绕开它、用 Bash 里的 curl 出网——权限规则看工具名,hook 看内容,这一层只有 hooks 能补。白名单文件(.zcode/net-allowlist.json,变更走 PR 评审):
{
"domains": ["registry.npmjs.org", "developer.mozilla.org", "nodejs.org", "github.com"]
}
// .zcode/hooks/guard-net.mjs —— PreToolUse 守门员:出网目标域白名单校验(SSRF 防线)
import { readFileSync } from "node:fs";
import { audit } from "./lib/audit.mjs";
const LOCAL = /^(localhost|127\.0\.0\.1|\[::1\]|::1)(:\d+)?$/i; // 本地回环放行,便于自联调
const loadAllowlist = () => {
try {
return JSON.parse(readFileSync(".zcode/net-allowlist.json", "utf8")).domains;
} catch { return []; } // 白名单读不到时按"仅本地"处理,宁紧勿松
};
const hostOf = (url) => { try { return new URL(url).host; } catch { return ""; } };
// Bash 里可能藏着出网:URL 字面量,以及 curl/wget 的裸域名写法
const extractHosts = (cmd) => [
...[...cmd.matchAll(/https?:\/\/([^\/\s"']+)/g)].map((m) => m[1]),
...[...cmd.matchAll(/\b(?:curl|wget)\s+(?:-[^\s]+\s+)*([a-z0-9.-]+\.[a-z]{2,})/gi)].map((m) => m[1]),
];
const isAllowed = (host, list) =>
LOCAL.test(host) || list.some((d) => host === d || host.endsWith(`.${d}`));
let raw = "";
process.stdin.on("data", (d) => (raw += d));
process.stdin.on("end", () => {
let evt;
try { evt = JSON.parse(raw || "{}"); } catch { process.exit(0); }
const { tool_name, tool_input } = evt ?? {};
let hosts = [];
if (tool_name === "WebFetch") hosts = [hostOf(tool_input?.url ?? "")];
else if (tool_name === "Bash") hosts = extractHosts(tool_input?.command ?? "");
hosts = [...new Set(hosts.filter(Boolean))];
if (!hosts.length) process.exit(0);
const allow = loadAllowlist();
const bad = hosts.filter((h) => !isAllowed(h, allow));
if (bad.length) {
console.error(`[guard-net] 以下出网目标不在白名单:${bad.join("、")}。\n` +
`出网仅限 .zcode/net-allowlist.json 中的域(SSRF 防线)。如确有需要,请说明用途,由用户评审后加白。`);
audit("BLOCK", `Net\t${bad.join(",")}`);
process.exit(2);
}
process.exit(0);
});
注册到 PreToolUse、matcher: "Bash"(WebFetch 分支与权限规则构成双保险)。注意放行本地回环是为了联调自己的服务——若代理跑在不受信环境,连 localhost 也应收紧,第 12 章的容器隔离此时才是真边界。
第 7 章的 format-on-save 只做格式化,生产版升级为检查闭环:写盘后先 Prettier 格式化,再对这一个文件跑 ESLint(--fix 顺手自修),仍有错误就把具体错误行(文件:行:列 + 规则 + 消息)用退出码 2 回喂模型。反馈的可执行性是闭环成立的关键:"有错误,请修复"只能让模型瞎试;贴出 db.ts:12:3: 'xxx' is not defined. (no-undef),模型一轮改对。
// .zcode/hooks/check-on-save.mjs —— PostToolUse:单文件检查,错误行回喂模型
import { execFileSync } from "node:child_process";
import path from "node:path";
let raw = "";
process.stdin.on("data", (d) => (raw += d));
process.stdin.on("end", () => {
try {
const { tool_input } = JSON.parse(raw || "{}");
const file = tool_input?.file_path ?? "";
const isSrc = [".ts", ".tsx", ".js", ".mjs"].includes(path.extname(file)) &&
!/node_modules|dist\/|migrations\//.test(file);
if (isSrc) {
// 第一步:格式化——快、无争议、失败不阻塞(并入第 7 章 format-on-save 的职责)
try {
execFileSync("npx", ["prettier", "--write", file], { timeout: 8000, stdio: "ignore" });
} catch { /* prettier 缺失时交给 ESLint 兜底 */ }
// 第二步:lint + 自动修复;非零退出说明仍有错误,收集错误行
let out = "";
try {
out = execFileSync("npx", ["eslint", "--fix", "--format", "compact", file], { timeout: 15000, encoding: "utf8" });
} catch (err) {
out = `${err.stdout ?? ""}${err.stderr ?? ""}`; // ESLint 有错误时以非零码退出
}
const errors = out.split("\n").filter((l) => /Error - /.test(l)).slice(0, 10);
if (errors.length) {
console.error(`[check] ${path.basename(file)} 仍有 ${errors.length} 个 ESLint 错误,请逐条修复后继续:\n${errors.join("\n")}`);
process.exit(2); // 回喂模型:写错 → 被拦 → 自修
}
}
} catch { /* hook 自身异常一律不阻塞会话 */ }
process.exit(0);
});
注册到 PostToolUse、matcher: "Write|Edit",建议 timeout: 30(ESLint 冷启动慢于纯正则脚本)。粒度红线:hook 只做单文件、秒级检查;全量 tsc --noEmit、vitest、构建由代理在任务收尾时主动跑(第 14 章的验收标准会要求它),CI 兜底——把全量检查塞进 PostToolUse,每次编辑等半分钟,你会亲手教会自己删掉这个 hook。
位置:项目 hooks 目录 .zcode/hooks/,随 Git 提交(与 settings.json 一起走 guard: 前缀 PR 评审,见 7.5);公共逻辑放 .zcode/hooks/lib/;运行期产物放 .zcode/logs/(进 .gitignore)。
测试:样本回放——把输入 JSON 存成文件、按文件名前缀声明期望(如 expect-deny-force-push.json、expect-allow-git-status.json),改黑名单后必跑:
# deny 样本必须退出 2(未拦即 FAIL),allow 样本必须退出 0(误拦即 FAIL)
for f in .zcode/hooks/samples/expect-deny-*.json; do
node .zcode/hooks/guard-bash.mjs < "$f" && echo "FAIL 未拦截:$f"; done
for f in .zcode/hooks/samples/expect-allow-*.json; do
node .zcode/hooks/guard-bash.mjs < "$f" || echo "FAIL 误拦截:$f"; done
hooks 跑在每次工具调用的热路径上,预算必须明确:三个纯正则守门员(guard-bash / guard-secrets / guard-net)各 < 50ms、零子进程;check-on-save < 2s(只查单文件,脚本内子进程超时与注册 timeout 双保险);Stop 扫描 < 200ms(只扫 git diff --staged)。审计日志(audit log)遵循 append-only(只追加、不修改):13.2 的 audit() 按月分文件写 .zcode/logs/hook-audit-YYYY-MM.log,每行"时间 + 裁决 + 摘要"。两个用途:事后追溯(这次会话拦了什么、为什么);调优输入——每月翻一遍 BLOCK 行,误报多了收规则,漏报了补样本。日志只记摘要与路径,不抄录完整文件内容,避免日志本身变成泄密点。
本章所有脚本对自身异常统一 fail-open(解析失败、内部报错一律退出 0 放行并留痕)。反直觉但必要:一个有 bug 的守门脚本若把会话锁死,用户第一反应是删掉它——护栏被拆掉,比护栏偶尔失灵更糟。真正的红线由三层兜住:deny 权限规则(静态、声明式、不依赖脚本存活)、沙箱(第 12 章)、人审;fail-open 的代价(异常期间拦截失效)靠审计日志里的异常痕迹来暴露。
必须诚实地说清 hooks 的三条边界:
bash -c 间接执行、写脚本再跑——黑名单永远追不完变形,它是概率屏障;.zcode/ 的代理就能自我解锁——所以第 7 章把 Edit(.zcode/**) 设为 deny,克隆来的仓库必须先审 hooks(7.5.2);纵深防御的完整链条值得贴在脑子里:
软约定(AGENTS.md) → 权限规则 → hooks 拦截 → 沙箱隔离 → 人工审批 → 备份兜底
降低发生概率 静态红线 动态内容审查 限制爆炸半径 最终决定权 最后的后悔药
hooks 在这条链上的位置是"高频、零疲劳、留痕"——它不会让坏事绝迹,但让坏事在发生前被喊停、发生后可追溯。
目标:把四个生产级 hook 装进 BookNest,完成注册、验证与一次真实拦截回放。两个完整脚本(危险命令拦截器、凭据扫描)即 13.2 与 13.3 给出的全文,连同其余文件落盘到以下路径(scan-secrets.mjs 沿用第 7 章版本):
booknest/.zcode/hooks/lib/audit.mjs # 13.2 审计模块
booknest/.zcode/hooks/guard-bash.mjs # 13.2 危险命令拦截器(完整脚本见 13.2.1)
booknest/.zcode/hooks/guard-secrets.mjs # 13.3 凭据防泄露(完整脚本见 13.3.2)
booknest/.zcode/hooks/guard-net.mjs # 13.4 出网防护
booknest/.zcode/hooks/check-on-save.mjs # 13.5 写后检查闭环
booknest/.zcode/hooks/scan-secrets.mjs # 第 7 章 Stop 扫描(第二层防线)
booknest/.zcode/net-allowlist.json # 13.4 出网白名单(样本回放集在 .zcode/hooks/samples/)
注册:booknest/.zcode/settings.json 的 hooks 段(与第 7 章已交付的 permissions 段合并为完整文件):
{
"hooks": {
"PreToolUse": [
{ "matcher": "Bash",
"hooks": [
{ "type": "command", "command": "node .zcode/hooks/guard-bash.mjs", "timeout": 10 },
{ "type": "command", "command": "node .zcode/hooks/guard-net.mjs", "timeout": 10 }
] },
{ "matcher": "Write|Edit",
"hooks": [
{ "type": "command", "command": "node .zcode/hooks/guard-secrets.mjs", "timeout": 10 }
] }
],
"PostToolUse": [
{ "matcher": "Write|Edit",
"hooks": [
{ "type": "command", "command": "node .zcode/hooks/check-on-save.mjs", "timeout": 30 }
] }
],
"Stop": [
{ "hooks": [
{ "type": "command", "command": "node .zcode/hooks/scan-secrets.mjs", "timeout": 10 }
] }
]
}
}
提交与验证:
echo ".zcode/logs/" >> .gitignore
node .zcode/hooks/guard-bash.mjs < .zcode/hooks/samples/expect-deny-force-push.json
# 期望:stderr 打印拦截原因;随后 echo $? 应为 2
git add .zcode/ .gitignore && git commit -m "guard: hooks 生产版(危险命令/凭据/出网/写后检查 + 审计)"
一次真实拦截的对话回放——护栏装好后,让它真的拦一次:
你:把 server/src/db/database.ts 写好,连接本地 SQLite。
代理:正在创建 server/src/db/database.ts……
代理:[guard-secrets] 已拦截:server/src/db/database.ts 命中凭据规则——「连接串内嵌口令」。
硬编码凭据违反 NFR-2(密钥零字面量)。请改用 process.env.DATABASE_URL 注入;
示例/文档用 <your-token> 形式占位。真实值由用户在终端注入,代理不应接触。
代理:收到,凭据不能硬编码。我改为环境变量注入,并补 .env.example 占位示例:
export const DATABASE_URL = process.env.DATABASE_URL ?? ":memory:";
(重写 → 扫描通过 → [check] 无错误 → 汇报完成,提醒你在终端注入真实连接串)
# 审计留痕:这次拦截应已落日志
cat .zcode/logs/hook-audit-2026-10.log
# 2026-10-06T09:12:33.101Z BLOCK Secrets server/src/db/database.ts 连接串内嵌口令
从"提示词里反复叮嘱"到"被拦一次就学会",这就是可编程守门员的价值:约定的执行成本,从你的注意力转移到机器。
| 误区 | 典型现象 | 纠正方法 |
|---|---|---|
| 把 hooks 当安全边界 | 配了黑名单就让代理在含生产密钥的机器上跑 | hooks 是护栏,防线是沙箱与最小权限(第 12 章);涉密环境不跑代理 |
| 黑名单求大求全 | 正则越堆越多、误报频发,团队开始无脑绕过 | 只收"高频且不可逆"命令;漏网交给 deny 规则与沙箱;每月按审计日志复盘 |
| PreToolUse 塞重检查 | 每次 Edit 卡 10 秒,会话慢到想删 hook | 热路径只做毫秒级判断;重检查放 PostToolUse 单文件级或 CI |
| hook 自身报错锁死会话 | 脚本抛异常退出非 0,代理连环重试 | 统一 fail-open:try/catch 后退出 0 并写审计日志暴露异常 |
| stderr 只说"不允许" | 模型换个写法再试,反复拉锯;占位符 <your-token> 被误拦 |
写清"类别 + 原因 + 替代方案";凭据扫描做占位符豁免防狼来了效应 |
guard: 前缀评审;克隆陌生仓库先审 hooks 再放行;第 7 章回答了"hook 是什么、怎么注册",本章回答"生产级的 hook 体系长什么样"。机制上,hooks 挂在工具调用生命周期的 PreToolUse / PostToolUse / Stop 等事件点上,stdin 收 JSON、退出码出裁决、stderr 回喂模型;调试先手动喂 JSON,再进真会话。实战上,四个守门员各司其职:危险命令拦截器把黑名单结构化为"类别 + 原因 + 替代方案";凭据扫描把零字面量约定从软约束升级为硬约束;出网防护用域名白名单补上"绕过 WebFetch 工具直接 curl"的漏洞,构成 SSRF 防线;写后检查把 lint 错误以可执行形式回喂,形成自修闭环。治理上,脚本进 Git、样本回放防回归、审计日志留痕、fail-open 保可用。而最清醒的认知是最后一条:hooks 是护栏不是安全边界,纵深防御的最后一层永远是沙箱与人。
guard-bash.mjs 准备 6 条样本 JSON(3 条 deny、3 条 allow,其中 1 条是 rm -r -f 这类散写变体),手动喂 JSON 观察退出码;为"散写变体未被拦截"补一条规则,或论证为什么可以不补。guard-secrets.mjs 增加一种密钥模式(如 Google API Key 的 AIza 前缀),验证三类占位符(<your-token>、${VAR}、明显示例值)都不误报,并把新样本加进回放集。guard: 前缀提交;最后按 7.5.2 的克隆审查流程开新会话检查这套护栏,列出拦截清单并补齐漏掉的样本。第 12、13 章给代理划定了边界——它不会闯祸了;但"不闯祸"不等于"能干成"。复杂需求交给它一次做完,依然会交付一堆积不起来的代码。本章解决 Part 5 的第一个问题:怎么把"做完 BookNest"这种不可能任务,切成一叠一次会话就能完成、验证、提交的任务卡。下一章再回答"拿到卡之后怎么动手"。
读完本章,你应该能:
先做个实验——很多新手的真实第一次。开一个全新会话,输入:
帮我实现 BookNest 的全部功能(FR-1 到 FR-7,需求见附录 D),技术栈按需求书来,
一次性完整实现,包括前后端、测试和迁移脚本。
崩坏过程高度可复现:
| 阶段 | 现象 |
|---|---|
| 前 10 分钟 | 势如破竹:目录结构、package.json、schema、Express 骨架一气呵成,看起来相当专业 |
| 30 分钟 | 细节开始漂移:schema 里是 created_at,路由里写成 createdAt;前端请求 /api/v1/book,后端只有 /books;同一类型在三处定义了三样 |
| 50 分钟 | 触发上下文压缩(compaction),早期约定(统一错误格式、迁移序号规则)被挤出窗口;代理开始"重新发明"自己定过的规则,前后端接口彻底对不上 |
| 80 分钟 | 测试大面积失败,代理转向"让测试通过"而非"让功能正确"——改断言迁就 bug |
| 收尾 | 60+ 文件、tsc 报错 100+、没有一个端到端可跑的功能。你得到的不是半成品,而是负资产:读懂它比自己重写还贵 |
崩坏不是代理"不够聪明",而是"大任务"这个单位本身错了:
结论:拆,不是优化项,是必要条件。 14.2 给出拆分必须遵守的四个原则。
一张卡只改变一件事——一个用户可见的能力,或一个模块的契约。检验句式:"这张卡完成后,系统多了什么能力?"答案里出现"以及""顺便",就拆开。"实现书籍 CRUD API"是合格的答案;"实现 CRUD API 并顺便升级 Vite"不是——前者出问题回滚一张卡,后者回滚时把无关变更一起拖下水。
验收标准必须是"可执行命令 + 期望结果",做完当场判对错。"搜索体验良好"不是验收标准;npm test -- search 全绿、curl "/api/v1/search?q=sf" 返回 200 且含《三体》才是。这与第 7 章"权限清单要能验证"、第 12 章"检查点可回滚"是同源思想:不能判对错的东西,无法管理。
动手所需的全部信息——相关约定、接口契约、文件路径、明确不做的事——都要写在卡里,执行会话不允许依赖"记得上一个会话聊过什么"。这正是第 18 章"自包含提示词"的提前演练:给 subagent 的任务提示必须自包含,而任务卡就是提前写好的自包含提示词。注意"链接而非复制":契约写"接口以 docs/spec/tags.md 为准",别把 spec 抄进卡里——两处事实源必然漂移。
每张卡声明前置卡编号;没声明的依赖等于不存在——执行代理不会"凭常识"先帮你打地基。依赖图即执行计划:入度为零的卡可以立即开工(可并行的宽度),最长的依赖链决定里程碑的工期下限(关键路径)。第 17 章的多代理扇出,吃的正是这张图里的并行宽度。
拿到功能需求(FR),先切成垂直切片(vertical slice):每片是一条用户可演示的端到端路径("录入一本书并能在列表看到"),而不是水平分层(horizontal layering)——"先把所有 model 写完,再写所有 service、所有 route,最后写所有页面"。
| 维度 | 水平分层 | 垂直切片 |
|---|---|---|
| 首个可验证产物 | 全部层完成之后 | 第一个切片结束(几小时内) |
| 验证对象 | "代码存在" | 用户可见行为 |
| 错误暴露时机 | 集成期集中爆发 | 每片结束即暴露 |
| 单任务上下文 | 吞下全项目 | 只装本片相关文件 |
| 适配 AI 协作 | 差——会话之间靠猜 | 好——每片自包含、可验收 |
垂直切片天然契合 14.2 的四原则:每片可演示(可验证)、片与片职责清晰(单一职责)、片内信息完整(自足)、切片顺序即依赖(显式化)。AI 协作尤其吃这套:每个切片结束,你都能打开浏览器看到真实进展,并立刻把验证结论喂给下一张卡。
切片往往还太大("书籍管理" = schema + API + 页面),按层拆成卡,但每张卡必须保留自己的可执行验收。然后把卡画成图(节点 = 卡,箭头 = "B 需要 A 的产物"):
graph TD
T1[卡A:schema 地基] --> T2[卡B:API]
T1 --> T3[卡C:API 可与B并行]
T4[卡D:页面骨架] --> T5[卡E:页面功能]
T2 --> T5
T3 --> T5
读图三件事:入度为零的卡(A、D)可立即开工,并行宽度为 2——这是将来多代理扇出的上限;最长链 A→B→E(或 A→C→E)是工期下限——关键路径(critical path);E 这类"汇点"卡往往是集成风险点,验收要最严。
卡级上下文 ≈ 任务卡本身 + 链接的约定(AGENTS.md / spec 的相关节)+ 涉及文件的现有内容 + 新增代码。粗估公式:涉及文件数 × 平均 150 行 + 新增 100–400 行,即千行级、一到三万 token——远小于窗口,而且全部与"这一件事"相关。拆分的物理意义就在这里:把与当前任务无关的上下文挡在门外。 估算超限(标准见 14.5)就再拆。
## [编号] [卡名]
- **目标**:一句话说清"完成后系统多了什么能力",以及为什么值得做。
- **涉及文件**:新增 / 修改哪些文件(相对路径,标注 新/改)。
- **实现要点**:技术决策、必须遵守的约定(链接 AGENTS.md / spec)、明确不做的事。
- **验收标准**:编号列出"可执行命令 + 期望结果",全部通过才算完成。
- **依赖**:前置卡编号;无依赖时写"无(前置:M0 已就绪)"。
- **预计规模**:文件数 / 新增行数 / 预计会话时长。
(空白模板收录在附录 B;编号建议用 里程碑-序号,如 M1-02。)
### 任务
给 BookNest 加上评分和标签功能,前后端都要做,体验做好一点,
顺便把搜索框也加上吧。
对照四原则逐条体检:三件事混在一张卡(违背单一职责);"体验做好一点"不可判定(违背可独立验证);前端不知道标签接口长什么样,只能猜(违背上下文自足);不知道必须等标签 API 先行,代理会先造一个假接口再用(违背依赖显式化)。这张卡的宿命:一次会话吞不下,做到一半上下文压缩,验收各执一词。
## M1-06 评分与标签前端
- **目标**:书架卡片与书籍详情页支持 1–5 星打分、增删标签、按标签筛选列表,
完成 FR-3 的前端部分。
- **涉及文件**:修改 `web/src/pages/BookListPage.tsx`、`web/src/pages/BookDetailPage.tsx`;
新增 `web/src/components/StarRating.tsx`、`web/src/components/TagEditor.tsx`、
`web/src/hooks/useTags.ts`。
- **实现要点**:打分用 TanStack Query 乐观更新,失败回滚并提示;标签输入回车创建、自动去重;
筛选状态写入 URL search params(可分享、可回退);标签接口契约以 M1-03 交付的
`/api/v1/tags` 为准(见 docs/spec/tags.md,勿在本卡复制)。
不做:标签管理页、标签配色。
- **验收标准**:
1. `npx vitest run web/tests` 组件测试 ≥ 8 个用例全绿(打分/加标签/删标签/筛选/失败回滚);
2. `npm run dev` 后手动路径:给《三体》打 4 星 → 加标签"科幻" → 按标签筛选可见该书
→ 刷新后状态保持;
3. `npm run lint` 与 `npx prettier --check web/src` 通过。
- **依赖**:M1-03(标签 API)、M1-05(书架页骨架)。
- **预计规模**:3 新 2 改 / 约 300 行 / 一次会话 60–90 分钟。
两卡对比的每一处差异都在替未来的执行会话省 token、省返工:边界写明,代理不会把搜索也"顺手"做了;契约用链接,spec 改了卡不用改;验收可执行,"完成"不再有解释权之争。
基准:一张卡 = 一次会话内完成并验证,工作量几十分钟(30–90 分钟);涉及文件 3–8 个;新增 100–400 行。
过大的信号:
过小的信号:
最后一条手感:拆分本身可以让代理起草("把 FR-5 切成垂直切片并列出任务卡草案"),但依赖与验收必须人审——拆分质量是后续所有会话的地基,在这里省下的十分钟,会在每个执行会话里加倍还回来。
目标:把 M1(FR-1 书籍管理 + FR-3 评分标签 + FR-6 全文搜索)落成任务图与任务卡,写入 docs/tasks/M1-tasks.md。注意 M1 的搜索范围先覆盖 books 表(书名/作者),笔记内容搜索随 M2 的 FR-2 接入——这个边界写进了 M1-07 的卡里,防止蔓延。
任务清单总表(9 张卡):
| 编号 | 卡名 | 切片 | 依赖 | 文件 | 预计规模 |
|---|---|---|---|---|---|
| M1-01 | 数据库基线:三表 + 迁移器 | 地基 | 无 | 4 新 1 改 | 150 行 / 0.5–1h |
| M1-02 | 书籍 CRUD API | 书籍管理 | M1-01 | 3 新 1 改 | 250 行 / 1–1.5h |
| M1-03 | 标签 API(含 book_tags) | 标签 | M1-01 | 3 新 1 改 | 200 行 / 1h |
| M1-04 | 前端骨架与路由 | 地基 | 无 | 6 新 | 200 行 / 1h |
| M1-05 | 书架页:列表 / 录入 / 编辑 | 书籍管理 | M1-02、M1-04 | 5 新 2 改 | 350 行 / 1–2h |
| M1-06 | 评分与标签前端 | 标签 | M1-03、M1-05 | 3 新 2 改 | 300 行 / 1–1.5h |
| M1-07 | 全文搜索后端 | 搜索 | M1-01、M1-02 | 3 新 | 120 行 / 0.5–1h |
| M1-08 | 搜索框与结果页 | 搜索 | M1-07、M1-05 | 2 新 1 改 | 150 行 / 0.5–1h |
| M1-09 | M1 收口:E2E 冒烟 + 全绿 | 收口 | M1-06、M1-08 | 2 新 | 200 行 / 1h |
依赖图:
graph TD
M1_01[M1-01 数据库基线] --> M1_02[M1-02 书籍 CRUD API]
M1_01 --> M1_03[M1-03 标签 API]
M1_04[M1-04 前端骨架] --> M1_05[M1-05 书架页]
M1_02 --> M1_05
M1_03 --> M1_06[M1-06 评分与标签前端]
M1_05 --> M1_06
M1_02 --> M1_07[M1-07 全文搜索后端]
M1_07 --> M1_08[M1-08 搜索页]
M1_05 --> M1_08
M1_06 --> M1_09[M1-09 收口]
M1_08 --> M1_09
读图:M1-01 与 M1-04 入度为零,可立即开工(依赖就绪后甚至可双代理并行,见第 17 章);关键路径是 M1-01 → M1-02 → M1-05 → M1-06 → M1-09;M1-03 与 M1-07 是两条可随时插入的支线。
完整任务卡示例(全部 9 张按 14.4.1 模板写入同一文件,此处给三张,M1-06 全文即 14.4.3 的好卡):
## M1-01 数据库基线:三表 + 迁移器
- **目标**:建立 books / tags / book_tags 三表与幂等迁移机制,作为 M1 全部后端卡的 schema 事实源。
- **涉及文件**:新增 `server/migrations/001_books.sql`、`002_tags.sql`、
`server/src/db/database.ts`、`server/src/db/migrate.ts`;修改 `server/package.json`(加 migrate 脚本)。
- **实现要点**:better-sqlite3 单例连接;`schema_migrations` 表记录已应用版本,迁移可重复执行(幂等);
books.status 用 CHECK 约束枚举 `wishlist|reading|finished`;rating 为 1–5 可空整数;
外键约束开启(`PRAGMA foreign_keys = ON`);时间戳用 ISO 8601 文本。细节遵循 AGENTS.md。
- **验收标准**:
1. `npm run migrate` 连续执行两次均成功,第二次日志显示 no-op;
2. `npx vitest run server/tests/db.test.ts` 通过(内存库跑迁移,断言三表结构与 CHECK 生效);
3. `npm run lint` 与 `npx tsc --noEmit` 通过。
- **依赖**:无(前置:M0 工程骨架已就绪)。
- **预计规模**:4 新 1 改 / 约 150 行 / 一次会话 30–60 分钟。
## M1-02 书籍 CRUD API
- **目标**:提供 `/api/v1/books` 的增删改查 REST 接口,供书架页(M1-05)使用,完成 FR-1 的后端部分。
- **涉及文件**:新增 `server/src/routes/books.ts`、`server/src/services/bookService.ts`、
`server/src/middleware/validate.ts`、`server/tests/books.api.test.ts`;
修改 `server/src/app.ts`(挂载路由)。
- **实现要点**:校验用 zod(书名/作者必填,cover_url 限 http(s),status 枚举,rating 1–5 可空);
错误统一 `{ "error": { "code", "message", "details?" } }`(NFR-1,详见 AGENTS.md 错误格式节);
全部参数化查询(NFR-2);列表支持 `?page=&pageSize=` 分页,默认 20。
不做:标签读写(M1-03)、搜索(M1-07)、软删除。
- **验收标准**:
1. `npx vitest run server/tests/books.api.test.ts` ≥ 10 个用例全绿
(创建/单查/列表/更新/删除/校验失败 400/404);
2. `npm run dev` 后 `curl -s localhost:3000/api/v1/books` 返回 200 与 JSON 数组;
3. `npm run lint` 与 `npx tsc --noEmit` 通过。
- **依赖**:M1-01(三表与迁移器就绪)。
- **预计规模**:3 新 1 改 1 新测试 / 约 250 行 / 一次会话 60–90 分钟。
## M1-07 全文搜索后端
- **目标**:提供 `GET /api/v1/search?q=`,M1 范围内按书名/作者模糊检索,完成 FR-6 的后端部分。
- **涉及文件**:新增 `server/src/routes/search.ts`、`server/src/services/searchService.ts`、
`server/tests/search.api.test.ts`。
- **实现要点**:LIKE '%q%' 匹配 title 与 author 两列,1 万条内满足 NFR-3
(FTS5 全文索引留作 M4 性能优化,本卡不做);空 q 返回 400(走统一错误格式);
结果按 updated_at 倒序、上限 50 条;结果仅含书籍字段,不做高亮(前端职责)。
不做:笔记内容检索(M2 随 FR-2 扩展——本卡只在 searchService 预留扩展点)。
- **验收标准**:
1. `npx vitest run server/tests/search.api.test.ts` ≥ 6 个用例全绿
(命中书名/命中作者/大小写不敏感/无结果/空 q 400/超 50 截断);
2. 测试内灌 1 万条种子数据跑一次检索,断言耗时 < 1000ms(防显著退化;
NFR-3 的 300ms 正式校验在 M4 用真实环境做);
3. `npm run lint` 与 `npx tsc --noEmit` 通过。
- **依赖**:M1-01(books 表)、M1-02(路由挂载与错误格式约定)。
- **预计规模**:3 新 / 约 120 行 / 一次会话 30–60 分钟。
落盘与提交:
mkdir -p docs/tasks # 文件:docs/tasks/M1-tasks.md(总表 + 依赖图 + 9 张卡全文)
git add docs/tasks && git commit -m "docs: M1 任务图与 9 张任务卡"
喂卡会话——卡写好后,执行变成一件平静的事:
你:打开 docs/tasks/M1-tasks.md,实现 M1-02「书籍 CRUD API」,严格按卡内验收标准交付。
代理:读取任务卡与 AGENTS.md → 列实现清单(路由/服务/校验/测试)→ 逐文件实现
→ npx vitest run:12/12 通过 → tsc 与 lint 通过
代理:M1-02 完成,验收标准逐条核对:
1. books.api.test.ts 12 个用例全绿(含校验 400 与 404);
2. curl GET /api/v1/books → 200,返回分页 JSON;
3. tsc --noEmit 无错误。请人工复核。
你:验收通过,按 Conventional Commits 提交,然后做 M1-03。
执行中的铁律:卡是活的——代理若在执行中问出"卡里本该写清"的问题(比如错误码用什么),先补卡再继续,让下一次执行不用再问;发现偏差先改卡再改代码(第 15 章会把这条展开成"计划的维护")。
| 误区 | 典型现象 | 纠正方法 |
|---|---|---|
| 把 FR 直接当任务 | "实现 FR-1"一句话开跑,代理自由发挥两小时 | FR 是需求单位不是执行单位;过 14.4.1 模板转成任务卡 |
| 水平分层拆解 | 先写完所有 model 再写 route,两周见不到一个能跑的功能 | 垂直切片先行,切片内再按层拆卡 |
| 验收写成形容词 | "效果正确""体验良好",做完仍各执一词 | 验收 = 可执行命令 + 期望结果,跑不过就是没完成 |
| 依赖藏在脑子里 | B 卡做到一半才发现要 C 的接口,只能造假接口续命 | 依赖显式化写进卡;图上没有箭头的卡才允许开工 |
| 卡里塞"顺便" | "加搜索时顺便升级依赖",出问题回滚牵连无辜文件 | 单一职责;"顺便"单独立卡、单独评审 |
| 拆完就不管 | 代码改了卡没改,执行会话拿到的卡已过期 | 卡随 PR 修订;执行中发现问题先补卡再继续 |
大任务必败有四个机制:质量随复杂度非线性衰减、上下文装不下全部细节、中间产物无法验证、错误复利放大——所以"把大象切成文件"不是效率技巧,而是让 AI 协作成立的前提。方法论三步走:用垂直切片保证每片可演示、可验证;用四原则(单一职责、可独立验证、上下文自足、依赖显式化)保证每张卡的执行会话能独立完成;用七字段任务卡把这一切固化成可落盘、可评审、可修订的资产。粒度手感以"一次会话几十分钟、3–8 个文件"为基准,过大再拆、过小合并。BookNest 的 M1 已被切成 9 张卡躺在 docs/tasks/ 里——下一章解决拿到卡之后的问题:先计划、对齐、写 spec,再动手。
docs/tasks/M1-tasks.md 并提交;然后挑一张依赖已就绪的卡真实执行一次:若代理中途问了卡里没写清的问题,把答案补回卡里再继续——用一次真实执行校准你的拆分手感。第 14 章把大需求拆成了任务图与任务卡;本章解决拆分后的下一个问题:拿到一张任务卡,代理应该"上来就写",还是"先对齐再动手"?答案是后者。本章介绍两层计划工具——轻量的计划模式(plan mode)与更上位的规格文档(specification,简称 spec),以及让计划真正管用的审读与维护纪律。
读完本章,你应该能:
AI 编码代理的默认性格是"急于动手":你话音刚落,它已经开始生成代码。这对 demo 无伤大雅,对真实项目却是最大的浪费来源。
先看一次典型的翻车现场:
你:给 BookNest 加阅读统计功能,做个页面展示数据。
ZCode:好的!(30 秒后开始写代码)
40 分钟后:新增 8 个文件、约 500 行,还引入了 ECharts 图表库。
你打开一看:月度统计用的是 created_at(录入时间),
而你想统计的是"什么时候读完的"——统计口径全错。
这 500 行代码技术上没什么毛病,但方向错了:改口径意味着数据模型、聚合查询、前端渲染全部重写。算一笔账:
方向性错误的代价不是"几行代码",而是全部已写代码。计划先行的本质,就是把发现方向错误的时点从"写完之后"提前到"写一行代码之前"。
正确的顺序是四拍:读代码 → 提方案 → 人审方案 → 执行。前三拍不落一行代码。同样的需求,换成计划先行:
你:给 BookNest 加阅读统计。先别写代码——读一遍数据模型和现有路由,
给我一份实现计划:统计口径、接口形状、文件改动清单。
ZCode:(只读探索 3 分钟)books 表没有"读完时间"字段,我暂用 updated_at
近似,可以吗?另外项目约定轻量 CSS,我建议不引图表库,用 CSS Grid 自绘。
你:口径必须精确,加一个 finished_at 字段;不引图表库同意。
ZCode:(产出 40 行计划,你花 2 分钟批准后开工)
注意这段对话里发生了什么:代理的两个关键决定(口径、图表方案)在写代码之前就被你看见了。这就是计划的第二个价值——计划即沟通契约(communication contract):你评审的对象从"几百行代码"变成"一页意图",看意图比看代码快一个数量级。
第三个价值:计划是可评审产物(reviewable artifact)。它可以被转发、被修改、被留档。下一节你会看到,这份产物还能进 git、随 PR 走、被未来会话考古——它是有寿命的资产,不是聊天记录里的一次性输出。
计划模式是 ZCode 内置的一种权限模式:进入后,代理只能只读探索(读文件、全局搜索、列目录),不能写任何文件或执行有副作用的命令;它必须把要做的事整理成一份计划提交给你,批准后才切回执行模式动手。
以 ZCode 为例,在交互界面按 Shift+Tab 可循环切换权限模式(默认 → 自动接受 → 计划模式),也可以用 /plan 指令或启动参数进入;不同版本入口略有差异,以 zcode --help 与官方文档为准。
两种模式的差异一目了然:
| 能力 | 默认模式 | 计划模式 |
|---|---|---|
| 读文件 / 全局搜索 / 列目录 | 允许 | 允许(只读探索) |
| 编辑、新建文件 | 允许 | 禁止 |
| 执行命令(Bash 等) | 按权限配置 | 原则上禁止,只读命令需在计划中声明 |
| 产出物 | 直接修改代码 | 一份计划,等待人工批准 |
| 人的角色 | 事后审查 | 事前把关 |
计划模式的产物通常长这样(骨架,完整示例见本章贯穿项目实战):
【实现计划 · <功能名>】
一、现状勘察 —— 我读了哪些文件,确认了哪些既有事实
二、假设 —— 我没有问你就自己定了的事(重点看这里)
三、改动清单 —— 按文件列出改什么、为什么
四、测试与验收 —— 怎么证明做对了
五、执行顺序 —— 先做哪步后做哪步,每步怎么提交
六、风险 —— 哪里可能出问题
四类场景,命中任何一条就值得进:
计划模式是工具不是仪式,三种情况直接做:
判断口诀:有"选择"要拍板才进计划模式,没有选择就是执行。每个三行改动都走一轮"计划—批准",是在给自己交流程税。
批准不等于扫一眼格式。你只需要盯三件事:代理默默做了哪些假设、验收标准是否可执行、方案有没有明显遗漏。这三件事怎么查,见 15.4。
计划模式解决"这一刀怎么切",spec 解决更上游的问题:"这个功能到底做什么、为什么这么做"。三者的层级关系:
需求书(附录 D:做什么功能) —— 产品层
└─ spec(做什么与为什么) —— 规格层:本章
└─ 任务卡(这一刀切哪) —— 拆分层:第 14 章
└─ todo 清单(当前一步)—— 执行层:第 16 章
什么时候需要写 spec?三个判据命中其一就该写:
反过来说,一次纯 UI 微调、一个内部工具函数,不需要 spec。spec 是给"有约定价值"的东西准备的。
一页够用的 spec 只需要六段,每段回答一个问题:
GET /api/v1/stats/overview?year=2026
{
"totals": { "all": 128, "finished": 96, "reading": 5, "wishlist": 27 },
"avgRating": 4.2,
"monthly": [3, 5, 0, 8, 2, 6, 9, 4, 1, 0, 0, 0],
"ratings": [2, 9, 24, 40, 21]
}
-- 0007_add_finished_at.sql
ALTER TABLE books ADD COLUMN finished_at TEXT; -- ISO 8601 日期,可空
spec 写进文件(BookNest 约定放 docs/spec/)后,它的价值才真正开始:文件比上下文窗口活得久。下个会话冷启动时,代理读 spec 与读你的聊天记录,得到的是同一份理解;第 17 章你会看到,当多个代理并行时,它们之间没有共享的记忆,共享的只有仓库里的文件——spec 就是它们对齐口径的锚点。所以 spec 的变更要走 git:改 spec 就是改需求,应当和改代码一样可评审、可追溯。
一个实用约定:把 docs/spec/ 的存在写进 AGENTS.md(第 4 章),让代理"开机即懂":接手一个功能,先找 spec;没有 spec 且任务跨文件,先提议写一份。
体量控制:一个功能一个文件,100–200 行以内。如果你发现 spec 越写越长、开始出现"先查询再分组再缓存"这类内容,说明你在写实现方案——那是计划的事,挪出去。
批准一份计划平均只需 3 分钟,前提是你知道往哪看。四步法:
代理最大的风险不是写错代码,而是默默替你做了决定:字段语义(updated_at 是编辑时间还是完成时间?)、依赖是否已安装(express-validator 在不在 package.json?)、现有代码是否可复用(项目已有校验中间件吗?)。
防御动作:要求代理在计划里专列"假设"一节。如果它列得太薄,追问一句:
你:计划里的"假设"一节太薄了。列出所有"你没有问我就自己定了"的事情,
包括字段类型、库名、现有代码的用法,一条都不许漏。
然后把每条假设按"对 / 错 / 无所谓"过一遍。贯穿项目实战里你会看到,两处最贵的返工都是被这一步拦下的。
每条验收标准都应该能翻译成"一条命令或一次观察"。对比:
GET /api/v1/stats/overview 返回 200 且各字段为零值(Vitest 断言);1 万条记录内响应 < 300ms。"看到形容词(美观、健壮、优雅)就停下来,让代理把它翻译成可测的陈述。
拿这张清单扫一遍计划:错误处理、迁移失败怎么回滚、并发与重复提交、空数据状态、权限边界、性能口径。比如统计接口虽是只读,但用户连点两次刷新按钮会发出重复请求——这类"不太可能但一定会发生"的事,代理经常想,也经常不说;你问了它才答。
两个屡试不爽的提示词:
你:列出这个计划的三个最大风险,以及每个风险的缓解措施。
你:假设你是一个严苛的代码评审者,这个计划里哪一条会让你拒绝批准?
批判之后人复核:代理的自批有时流于形式(列出的风险恰是它最有把握的部分),你要检查它列的风险有没有对应的缓解动作,没有就继续追问。
最后提醒:批准的是方向,不是免除检查。执行阶段的每一步仍需要检查点——那是第 16 章 todo 清单的职责。
最常见的计划失败模式不是"没写计划",而是"计划只在开工前看了一眼"。计划是一次性的,很快就会与现实脱节,从资产变成误导源。
执行中出现偏差只有两类:一是勘察与现实不符(以为要新写校验,结果发现项目已有中间件);二是需求中途变化(统计口径从"录入时间"改成"读完时间")。两类偏差的处理原则相同:停下来,先改计划文件并写明为什么改,再动代码。顺序反过来,计划就死了。
计划文件放进仓库(BookNest 约定:spec 在 docs/spec/,任务与计划材料在 docs/tasks/),和代码一起提交。三个好处:
在计划文件末尾维护一个"修订记录"小节,成本极低:
## 修订记录
- 2026-10-06 批准前批注 4 条:口径改用 finished_at;不新增校验依赖;
yearly 聚合移到 SQL;补迁移回滚说明。
- 2026-10-07 执行中偏差:heatmap 组件改用 CSS Grid 而非 table,
原因:每月格数固定 12 列,Grid 语义更贴合。
把三样东西各归各位,别混在一个文件里:
spec —— 需求真相:做什么、为什么(变化最慢)
计划 —— 战略:分几步、什么顺序、每步怎么验收(按 PR 变化)
todo —— 战术:此时此刻这一步做没做完(分钟级变化,见第 16 章)
计划批准后,第一件事就是把它拆成 todo 清单交给执行会话;计划负责"路线不偏",todo 负责"脚下不乱"。
FR-4(阅读统计)属于里程碑 M3,但"spec 先行"正是本章主张:开工前就把规格定稿,M3 启动时直接从 spec 拆任务卡。下面是完整的三份产物:spec 全文、计划模式产出的计划全文、人工批注。
产物一:docs/spec/fr4-statistics.md(全文)
# FR-4 阅读统计 · 功能规格(Spec)
- 版本:v1.0(2026-10-06 定稿) 状态:已评审
- 关联:附录 D FR-4;里程碑 M3;技术栈约定见 AGENTS.md
## 1. 背景与目标
BookNest 已积累书籍、评分与笔记数据(M1/M2 交付),但缺少回顾视角。
本功能提供一页式统计面板:总量与状态分布、年度阅读量、月度热力图、
评分分布。目标:用户"打开就知道自己今年读了多少书"。
## 2. 用户故事
- US-1 作为读者,我想看到总藏书量与想读/在读/读完的分布,了解当前库存。
- US-2 作为读者,我想按年查看每月读完的书(月度热力图),回顾阅读节奏。
- US-3 作为读者,我想看到 1–5 星评分分布与平均分,检查自己是否打分通胀。
## 3. 接口约定
只读接口,前端一次拉全:
GET /api/v1/stats/overview?year=2026
{
"totals": { "all": 128, "finished": 96, "reading": 5, "wishlist": 27 },
"avgRating": 4.2,
"yearly": [{ "year": 2024, "count": 31 }, { "year": 2025, "count": 44 }],
"monthly": [3, 5, 0, 8, 2, 6, 9, 4, 1, 0, 0, 0],
"ratings": [2, 9, 24, 40, 21]
}
- monthly:指定 year 的 1–12 月"读完"数量,下标 0 对应 1 月;
- ratings:下标 0 对应 1 星,共 5 档;
- year 缺省为当前年份。
## 4. 数据变更(schema diff)
新增迁移 0007_add_finished_at.sql:
ALTER TABLE books ADD COLUMN finished_at TEXT; -- ISO 8601 日期,可空
- 状态变为 finished 时由服务层写入;改回其他状态时置 NULL;
- 统计口径:年/月统计只认 finished_at;不回填历史数据(见第 6 节)。
## 5. 边界与错误处理
- year 非法(非 4 位数字,或超出 1970–2100)→ 400,统一错误格式;
- 空库或某月无数据 → 正常 200,值为 0,前端渲染空态,不报错;
- rating 为 NULL 的书不计入平均分与评分分布;
- 接口只读;前端用 TanStack Query 请求去重,防连点重复请求。
## 6. 明确不做的事
- 不引入 ECharts/Recharts 等图表库,热力图用 CSS Grid 自绘
(遵守"轻量 CSS"技术栈约定);
- 不回填历史书的 finished_at(宁可早期月份显示 0,不做人工猜测);
- 不做跨设备/多用户对比(单用户应用);
- 不做统计报表导出(FR-5 的 JSON 备份已含原始数据)。
## 7. 验收标准
- AC-1 空库调用 overview 返回 200,各字段为零值;
- AC-2 造 3 本 finished(不同月份)+ 1 本 wishlist,totals/monthly 正确;
- AC-3 year=abcd 返回 400;
- AC-4 前端热力图 12 格与 monthly 一致;Vitest 接口测试 + 1 条
Playwright 冒烟用例全部通过。
产物二:计划模式下代理产出的实现计划(全文,未修订版)
【实现计划 · FR-4 阅读统计】(计划模式产出,待批准)
目标:按 docs/spec/fr4-statistics.md 实现统计面板(API + 前端页面)。
一、现状勘察(已只读确认)
- server/src/db/migrations/ 最新为 0006_add_book_tags.sql;
- server/src/routes/ 有 books.ts / tags.ts / search.ts,由 src/app.ts 挂载到 /api/v1;
- books 表已有 rating(1–5,可空)与 status(wishlist|reading|finished);
- web/src/pages/ 有 BookListPage / BookDetailPage,路由在 web/src/App.tsx;
- 统一错误中间件:server/src/middleware/errors.ts。
二、假设(未经确认,请人工复核)
1. "读完时间"暂用 books.updated_at 近似(books 表无该字段);
2. 参数校验用 express-validator(未确认是否已安装);
3. yearly 序列在前端 reduce 聚合,后端只返回明细。
三、改动清单
1. server/src/db/migrations/0007_stats.sql —— 视假设 1 结论决定是否加字段;
2. server/src/services/stats.service.ts —— 四组 SQL 聚合查询;
3. server/src/routes/stats.ts —— GET /stats/overview,含 year 校验;
4. server/src/app.ts —— 挂载路由(1 行);
5. server/tests/stats.routes.test.ts —— AC-1/2/3 对应用例;
6. web/src/api/stats.ts —— TanStack Query 封装;
7. web/src/pages/StatsPage.tsx + web/src/components/MonthlyHeatmap.tsx;
8. web/src/App.tsx —— 路由与导航入口。
四、测试与验收
- Vitest:AC-1(空库)/ AC-2(3 finished + 1 wishlist)/ AC-3(非法 year);
- Playwright:StatsPage 冒烟(AC-4);
- 手动:npm run dev 打开 /stats 目检热力图。
五、执行顺序
迁移 → service(含测试)→ route → 前端 API → 组件 → e2e;
每步通过即按 Conventional Commits 提交。
六、风险
- finished_at 无索引;1 万条内(NFR-3)聚合可接受,量级上去再补索引;
- CSS Grid 热力图需要 0–N 档颜色梯度,先做 5 档,色值取主题变量。
产物三:人工批注(批准前完成,共 4 条)
## 批注(修订后批准)
1.【假设纠正】假设 1 不成立:updated_at 是"最后编辑时间",不是"读完时间"。
改:按 spec 新增迁移 0007(books.finished_at),统计口径一律用 finished_at。
2.【假设纠正】假设 2 不成立:express-validator 未安装。改:沿用现有
server/src/middleware/validate.ts 的手写校验模式,不新增依赖。
3.【方案纠正】假设 3 不成立:yearly 聚合改在 SQL GROUP BY 完成,前端只画图。
理由:统计口径统一在后端,与 spec"前端一次拉全"一致。
4.【遗漏补充】计划未提迁移失败如何处理。补充:0007 走现有 migration runner
的事务包裹,失败自动回滚,不产生半套 schema。
结论:按修订后计划执行,切回默认模式开工。
注意:批注 1、2 都是"查假设"抓出来的——代理自己也在假设一节做了提示,但真正的把关必须由人完成。如果那天你直接批准了原计划,M3 会在验收时才暴露口径错误,返工量翻倍。
| 误区 | 典型现象 | 纠正方法 |
|---|---|---|
| 在计划模式里让代理顺手改代码 | 代理回复"当前为只读模式无法写入",或计划里夹带大段半成品代码 | 计划模式只产计划;批准后切回执行模式再动手 |
| 计划批准后代码跑偏也不回头改计划 | 代码已经是 B 方案,计划文档还写着 A 方案,新人越看越糊涂 | 偏差先改计划(写明原因)再改代码,保持文档与现实同步 |
| 把 spec 写成实现细节手册 | spec 里全是"先查询再分组再缓存",代码一重构 spec 就作废 | spec 只定接口形状与行为约定;实现细节留给计划与代码 |
| 代理的计划全盘照收 | 字段语义猜错、引用未安装的库,问题到验收才暴露 | 逐条查"假设"一节;让代理自我批判后人工复核 |
| 小改动也强制走计划模式 | 三行修复来回两轮"计划—批准"对话,效率骤降 | 有"选择"要拍板才进计划模式;机械改动直接做 |
| spec 写完就进抽屉,需求变了不改 | 代码早改了口径,spec 还是旧版,新会话被误导 | spec 与代码同走 git;需求变更先提 spec 修改再改代码 |
docs/spec/、docs/tasks/,随 PR 提交,让评审者看得见动机;计划先行不是流程洁癖,而是把"发现走错路"的时点从写完代码之后提前到写代码之前:计划模式用只读探索加人工批准,拦住中大型改动的方向性风险;spec 用六段结构把"做什么与为什么"固化成可评审、可追溯、跨会话共享的文件;四步审读法(查假设、查验收、查遗漏、自我批判)让 3 分钟的批准真的值 3 分钟;而"偏差先改计划再改代码、计划随 PR 落盘"的维护纪律,决定这份计划是资产还是误导源。
要点回顾:
第 15 章解决了"动手前先对齐";本章解决对齐之后的下一个问题:任务大到一次会话装不下怎么办?真实项目里,FR-2 这样的功能要动十几个文件、跑几十轮测试,而会话随时可能因为上下文耗尽(context exhaustion)、崩溃或你主动关闭而结束。本章的核心命题只有一句话:会话是易失的(volatile),状态必须外置到文件——会话会死,状态要活。
读完本章,你应该能:
什么叫长任务?不是"写得久的任务",而是装不进一次会话的任务:改动超过十来个文件、要跨几个小时甚至几天、中途必然需要人拍几次板。BookNest 的 FR-2(读书笔记)就是典型——迁移、service、路由、两个前端页面、端到端测试(e2e),一口气做不完。
长任务有三个敌人,每一个都足以让几小时的成果蒸发:
敌人一:上下文耗尽。 上下文窗口(context window)有限:对话越长,最早的约定与中途的纠正越可能被压缩(compaction)或挤出窗口。表现是代理开始"失忆"——你一小时前说过"不引图表库",它现在给你装了 ECharts。上下文不是记忆,是工作台:台面就那么大,堆得越满,碰掉东西越容易。
敌人二:中途跑偏。 任务越长,需求被误解的机会越多。没有检查点的长执行像没有路标的马拉松——跑偏十公里才被发现,返工的不是一段代码,而是整条路线。
敌人三:状态丢失。 会话结束的原因很多:窗口满了、进程崩了、你合上笔记本、或者只是今天想换个思路重开。"我们刚聊到哪了?"——聊天记录里也许还能翻到,但新会话对你们昨天的共识一无所知。最贵的丢失不是代码(git 管着),而是决策与进度:为什么选了方案 B?做到第几步?下一个会话两眼一抹黑。
三类状态、三种管法,先记这张总账:
| 状态类型 | 例子 | 谁管 | 怎么管 |
|---|---|---|---|
| 代码状态 | 改了哪些文件 | Git | 小步提交,信息可追溯 |
| 任务状态 | 做到第几步、还剩什么 | 本章:清单 + 进度文档 | 外置到 docs/tasks/ |
| 决策状态 | 为什么选方案 B | 本章:决策日志 + handoff | 外置到 docs/ |
核心原则:把上下文窗口当内存(RAM),把仓库当硬盘。掉电(会话结束)时内存全空,硬盘上的都还在。代理脑内的记忆你控制不了,仓库里的文件才完全由你控制——所以一切重要状态都往硬盘上搬。
ZCode 这类代理内置 todo 工具(任务清单):代理把计划拆成一列条目,标记 pending / in_progress / completed,执行中随时增删。这是会话内的战术视图,好用,但有个致命属性——会话一死,它就没了。
所以长任务需要第二份:文件清单(如 docs/tasks/fr2-notes.md),跨会话持久。两者分工:
文件清单长这样(节选,全文见贯穿项目实战):
<!-- docs/tasks/fr2-notes.md -->
- [x] T1 迁移 0007_create_notes.sql
- [x] T2 note service + 单元测试(12 条)
- [~] T3 路由层 + 接口测试 ← 当前
- [ ] T4 Markdown 渲染与消毒
- [ ] T5 前端两个页面
[~] 标记当前项。出现多个"进行中",意味着多个半成品——等于没有进度。git commit(提交信息带任务编号,如 feat(notes): T2 note service 与单元测试)。勾选与提交是同一个动作的两半:只勾不提交,清单就在撒谎;只提交不勾,下个会话就会重做。把规则写进 AGENTS.md(第 4 章),并在派活时点名:"读 docs/tasks/fr2-notes.md,按清单顺序执行;每完成一项:更新清单、跑测试、提交;遇到清单外的发现,先报告再动手。"之后你只需在每项完成时扫一眼提交记录——清单就从"你喂给代理的备忘"变成"代理自己守的纪律"。
todo 清单回答"做到哪了";长任务还要回答"为什么这么做"与"下一个会话怎么接"。三件套各管一摊,全部落在 docs/tasks/(与第 14 章任务卡、第 15 章计划同住)。
按任务一个文件(如 docs/tasks/fr2-notes-progress.md),执行中随时追加三类条目:
它与 git log 的分工:git log 记"改了什么",进度文档记"为什么这么改、改的时候发现了什么"。前者是物证,后者是口供,缺一不可。
handoff(交接)文档是写给"下一个会话"的——那个会话没有你的记忆,只有仓库。五段俱全才算合格:
# Handoff · <任务名> · <会话 N → 会话 N+1>
- 时间 / 作者(哪个会话写的)
- 下手前先读:任务卡、清单、决策日志的路径
## 已完成
做了什么,附提交号
## 当前状态
分支、工作区是否干净、测试是否全绿、清单勾到哪
## 下一步
按顺序,具体到"改哪个文件的哪部分"
## 已知坑
踩过的坑与绕法——全文最值钱的一段
## 关键文件
带路径列出,让接手者不必全库搜索
写 handoff 的时机是每次主动收尾,不是"想起来才写"。可以授权代理代笔:收尾时说一句"写一份 handoff 到 docs/tasks/handoff-fr2-s1.md",你审一眼再随代码一起提交。
决策日志(decision log)记录所有"当时为什么这么选",一行一条,只追加、不改写(推翻旧决定就追加新条目并注明取代谁):
| 日期 | 决定 | 理由 | 备选与不选原因 |
|---|---|---|---|
| 10-07 | body_md 只存 Markdown 原文 | FR-5 导出要复用原文 | 存渲染结果:格式被锁死 |
| 10-08 | timeline 分页用游标而非 offset | 翻页中有新增时 offset 会重/漏 | offset:实现简单但不稳 |
它的价值在两周后:新会话问"为什么时间线要含未归书笔记",答案在文件里,不在你已经蒸发的记忆里。轻量项目一张 Markdown 表就够;要正式些可以演化为 ADR(Architecture Decision Record,架构决策记录),一个决策一个文件。
新会话开场白固定三步:先读 handoff 与任务卡 → 复述理解 → 人确认后开工。开场提示词模板:
你:续作跨会话任务。先读 docs/tasks/fr2-notes.md、
docs/tasks/handoff-fr2-s1.md 与 docs/decisions.md,不要写任何代码,
用 5 条以内复述你对当前状态与下一步的理解,等我确认。
"复述对齐"是廉价保险:错读在零行代码时被纠正,成本是一轮对话;不纠,成本是半天返工。完整对话实录见本章贯穿项目实战。
长会话不是匀速变慢,而是断崖式变烂。四个信号,命中两条就该收尾:
衰退的机制很朴素:窗口里堆满过程细节(报错、试探、改了又改的半成品),真正重要的约定被挤到角落。这不是模型的"态度问题",是上下文的物理规律。
发现信号不要硬撑,执行标准收尾,然后换新会话:
宁可主动换会话,不要等会话烂掉——前者由你掌控交接质量,后者由崩溃时刻决定。
会话中你说过的每句重要的话,都面临同一个问题:它会蒸发。听到自己说出下面这些话,就顺手落盘:
一句话判断:这句话两周后的新会话还需要吗?需要,就写进文件。
ZCode 在窗口将满时会自动压缩,也支持手动触发。要认清它的定位:压缩是有损的安全网,保住的是"最近与相关",丢掉的往往是"早期的约定与细节"——而约定恰恰是长任务里最贵的部分。所以:
大重构(比如把 server 从 JavaScript 整体迁到 TypeScript)天然超会话。分段三原则:
段间最忌"顺手多迁一个文件":没验证的贪多,就是给下一段的地基埋雷。
单个长任务用清单管理;同时推进多个功能(FR-2、FR-7、升级依赖……)时,需要看板(kanban)。
两种组织选一种就够。
目录式——三个目录就是三列状态:
docs/tasks/board/
├── backlog/ # 待办:fr7-fragments.md
├── doing/ # 进行中:fr2-notes.md(不超过 2 张)
└── done/ # 已完成:fr3-tags.md
单文件看板表——任务少时更直观:
# BookNest 任务看板(2026-10-08 更新)
## Doing(WIP ≤ 2)
| 任务 | 卡片 | 状态 |
|---|---|---|
| FR-2 读书笔记 | docs/tasks/fr2-notes.md | T3 进行中(会话 A) |
## Backlog
- FR-7 灵感速记(依赖 FR-2 的 notes 表)
## Done
- [x] FR-3 评分与标签(2026-10-05 合并)
WIP(work in progress,在制品)限制是看板的灵魂纪律:doing 列同时最多 2 张卡(单人甚至 1 张),想开新卡,先关一张。理由有三:
本章的看板是"单人多会话":你开两个终端,各守一张卡。第 17 章往前一步——"一人多代理":主代理派生 subagent(子代理)并行干活。到那时你会发现:代理之间同样没有共享记忆,能对齐它们的也只有仓库里的文件。本章练的清单、handoff、决策日志,正是第 17 章多代理协同的底层设施——先把单人跨会话管明白,才轮得上代理编队。
本节实录里程碑 M2 的第一仗:跨三个会话完成 FR-2(读书笔记)。任务卡与 spec 来自第 14、15 章的方法,这里只展示一件事:状态如何跨会话活着。
会话 1 完成 T1/T2,T3 写到一半时你观察到衰退信号(刚纠正的 INNER JOIN 又出现了),执行四步收尾。落盘动作对应的命令:
npm test # 验证全绿
git add -A && git commit -m "feat(notes): T2 note service 与单元测试"
# 更新清单勾选、写好 handoff 后,文档一并提交
git add docs/tasks && git commit -m "docs(tasks): FR-2 会话 1 handoff 与清单更新"
文件一:docs/tasks/fr2-notes.md(清单全文)
# FR-2 读书笔记 · 执行清单
> 任务卡:docs/tasks/fr2-notes-card.md 计划:docs/tasks/fr2-notes-plan.md
> 图例:[x] 完成(已提交) [~] 进行中(同时只许一个) [ ] 未开始
> 本文件是进度的唯一真相源:完成一项 → 勾选 → git commit,三步连做。
- [x] T1 迁移 0007_create_notes.sql(notes 表 + idx_notes_book_id)commit a1b2c3d
- [x] T2 note service(CRUD + listByBook)+ Vitest 单测 12 条 commit d4e5f6a
- [~] T3 路由层:GET/POST /books/:bookId/notes、GET /notes/timeline + 接口测试
- [ ] T4 Markdown 渲染 + XSS 消毒(NFR-2)
- [ ] T5 前端:书详情页笔记 Tab + 全站时间线页
- [ ] T6 Playwright e2e 冒烟 + lint / 覆盖率收尾
文件二:docs/tasks/handoff-fr2-s1.md(会话 1 → 会话 2,全文)
# Handoff · FR-2 读书笔记 · 会话 1 → 会话 2
- 时间:2026-10-07 23:40 作者:会话 1
- 下手前先读:docs/tasks/fr2-notes.md(清单)、docs/decisions.md(决策日志)
## 已完成
- T1 迁移 0007_create_notes.sql:notes 表(book_id 可空、fragment_id 可空、
title、body_md、created_at/updated_at)+ idx_notes_book_id。commit a1b2c3d
- T2 note service:create / update / remove / get / listByBook,
Vitest 单测 12 条全绿。commit d4e5f6a
## 当前状态
- 分支 feat/fr2-notes;工作区干净;npm test 全绿(后端 12 条);
- 清单勾至 T2,T3 标记 [~](GET /books/:bookId/notes 已有雏形,未提交)。
## 下一步(按顺序)
1. T3:POST /api/v1/books/:bookId/notes(校验:title 必填、body_md 非空、
book 必须存在,错误走统一格式);
2. T3:GET /api/v1/notes/timeline(按 created_at 倒序,分页暂按计划的
offset 方案);
3. T3:接口测试补 400 / 404 / 空列表三条负路径。
## 已知坑
- better-sqlite3 的事务包在 service 层,别在 route 里开;
- 时间线必须含 book_id 为 NULL 的笔记:用 LEFT JOIN,别 INNER JOIN
(会话 1 踩过,被单测 T2-07 拦下)。
## 关键文件
- server/src/db/migrations/0007_create_notes.sql
- server/src/services/note.service.ts
- server/tests/note.service.test.ts
- docs/tasks/fr2-notes.md(清单:做一步勾一步)
文件三:docs/decisions.md(决策日志,M2 相关条目,最终状态)
# 决策日志(只追加,不改写;推翻旧决定就追加新条目并注明取代谁)
| 日期 | 决定 | 理由 | 备选与不选原因 |
|---|---|---|---|
| 10-07 | body_md 只存 Markdown 原文,不预渲染 | FR-5 导出要复用原文 | 存渲染结果:格式被锁死 |
| 10-07 | 时间线含未归书笔记(book_id NULL) | 速记归档前也要可见 | INNER JOIN:会漏数据 |
| 10-08 | timeline 分页用游标而非 offset | 翻页中有新增时 offset 会重/漏 | offset:实现简单但不稳 |
| 10-08 | 消毒用 isomorphic-dompurify,仅渲染时 | NFR-2;同时保留原文 | 存库前消毒:丢原文 |
第二天开工,新会话第一件事是冷启动标准动作:
你:续作跨会话任务。先读 docs/tasks/fr2-notes.md、
docs/tasks/handoff-fr2-s1.md 与 docs/decisions.md,
然后不要写任何代码,用 5 条以内复述你的理解,等我确认。
ZCode:我的理解:
1. T1/T2 已提交(a1b2c3d、d4e5f6a),后端测试 12 条全绿;
2. T3 过半:GET /books/:bookId/notes 有雏形,POST 与 timeline 未动;
3. 时间线必须包含 book_id 为 NULL 的笔记(LEFT JOIN);
4. body_md 只存原文,消毒只发生在前端渲染时;
5. 下一步:完成 POST 与 timeline 接口 + 三条负路径测试,再进 T4。
有偏差吗?
你:第 3 条补充:排序键 created_at 相同时用 id 兜底,别让同一秒的
笔记顺序抖动;这条开工前补进决策日志。其余确认,开工。
一条补充、五句确认,零行代码就完成了对齐。会话 2 随后完成 T3、T4(期间发现 offset 分页在翻页中会重/漏,按偏差处理流程改为游标并记入决策日志),并把 T5 推进到一半(笔记 Tab 完成、时间线页只有骨架),再次收尾。
文件四:docs/tasks/handoff-fr2-s2.md(会话 2 → 会话 3,全文)
# Handoff · FR-2 读书笔记 · 会话 2 → 会话 3
- 时间:2026-10-08 22:10 作者:会话 2
- 下手前先读:docs/tasks/fr2-notes.md、docs/decisions.md(今日新增 2 条)
## 已完成
- T3 路由层与接口测试:GET/POST /books/:bookId/notes、GET /notes/timeline,
接口测试 21 条全绿。commit 9c8d7e6
- T4 Markdown 渲染 + 消毒:isomorphic-dompurify,仅前端渲染时消毒。
commit 5f4e3d2
- T5 完成一半:书详情页笔记 Tab 可用。commit 7a6b5c4
## 当前状态
- 分支 feat/fr2-notes;工作区干净;npm test 全绿(后端 21 + 前端 8 条);
- 清单:T1–T4 为 [x],T5 为 [~](时间线页只有骨架)。
## 下一步(按顺序)
1. T5 余项:全站时间线页 TimelinePage(复用 <NoteCard>,游标分页接
GET /notes/timeline?cursor=…);
2. T6:Playwright 冒烟(建书 → 写笔记 → 时间线可见);
3. T6:lint 全绿 + 覆盖率 ≥ 80%(NFR-1),合并回 main。
## 已知坑
- 会话 1 的两条仍有效(LEFT JOIN、事务在 service 层);
- 新增:React 渲染笔记必须走 <SanitizedMarkdown> 组件,谁直接用
dangerouslySetInnerHTML 就违反 NFR-2,评审时重点盯;
- 新增:游标是 base64(created_at|id),必须用 server/src/utils/cursor.ts
的 encode/decode,别手拼字符串。
## 关键文件
- server/src/routes/note.routes.ts server/src/services/note.service.ts
- web/src/components/SanitizedMarkdown.tsx web/src/pages/BookDetailPage.tsx
- server/src/utils/cursor.ts(游标编解码,勿重复造轮子)
- docs/decisions.md(本轮新增 2 条决策)
会话 3 冷启动对齐后,完成时间线页(T5 余项)与 T6:Playwright 冒烟用例、lint 全绿、覆盖率 ≥ 80%,合并 feat/fr2-notes。收口后两个动作别省:把 done 卡片归档进看板(16.5);把 handoff 里长期有效的坑("事务包在 service 层")升级进 AGENTS.md——坑修一次,以后所有会话受益。
| 误区 | 典型现象 | 纠正方法 |
|---|---|---|
| 靠记忆和聊天记录续任务 | 新会话问"我们做到哪了",每次答案不一样,还重做过已完成的事 | 清单是唯一真相源;冷启动先读清单与 handoff |
| 多项同时标"进行中" | 五个半成品,哪项都没法提交,测试长期红着 | 同一时刻只保一件 in_progress;完成即勾选并提交 |
| 会话收尾不写 handoff | 下个会话只能靠翻 diff 猜进度,把已知的坑重新踩一遍 | 四步收尾固定化:停完整步 → 验证 → 落盘 → 换会话 |
| 把上下文压缩当记忆 | 压缩后代理忘了早期约定,装了不该装的依赖 | 压缩是有损安全网;重要约定写进文件,不指望窗口 |
| 决策日志只写"选了什么" | 两周后没人记得为什么不选另一方案,同一场辩论再吵一遍 | 每条决策记四列:日期、决定、理由、备选与不选原因 |
| WIP 不设限 | 四个分支并行,合并冲突铺满屏幕,没有一个能合进去 | doing ≤ 2;想开新卡,先关一张 |
长任务的三个敌人——上下文耗尽、中途跑偏、状态丢失——共同指向一个原则:会话是易失的,状态必须外置到文件。对内,用 todo 清单驱动执行,同一时间只保一件进行中,让清单成为进度的唯一真相源;对外,用进度文档、handoff 交接文档与决策日志三件套,把决定、偏差与坑都搬进仓库;跨会话时,执行"读 handoff → 复述理解 → 人确认"的冷启动标准动作;会话衰退时,主动收尾换血而不是硬撑;多任务并行时,用看板加 WIP 限制守住专注。git 保住代码,文件保住意图——两者合起来,才构成跨会话、跨代理都读得懂的"项目记忆"。
要点回顾:
第 16 章的看板停在"单人多会话";本章把拼图补全——"一人多代理"。当任务撞上单代理的天花板(窗口装不下、串行太慢、自己审自己失灵),就该把一个代理变成一支编队。本章给出五种可复用的多代理架构模式(architecture pattern):主从编排、流水线、评审者-实现者、扇出汇总、探索-实现分离,每种都配图、适用场景与"什么时候别用",最后用 BookNest M3 完整推演一次选型。
读完本章,你应该能:
ZCode 等工具里的多代理,指的是主会话派生 subagent(子代理):每个子代理有独立的上下文窗口,干完活把结果交回主会话(第 18 章讲操作细节,本章讲架构)。听起来像招了几个实习生,但先想清楚:多代理到底解决什么问题?
多代理的本质是拿两样东西换两样东西(trade-off,取舍):
所以多代理是解决特定瓶颈的工具,不是排场。什么时候值得上多代理,对着这张表看信号:
| 信号(命中越多越值得) | 对应瓶颈 | 适合的模式 |
|---|---|---|
| 可分解为 ≥3 个互不依赖的同构子任务 | 串行耗时 | 扇出汇总 |
| 子任务读得多、写得少(探索、审计) | 上下文装不下 | 探索-实现分离 / 扇出汇总 |
| 写与审需要不同视角 | 角色混淆 | 评审者-实现者 |
| 流程标准化、角色分明 | —— | 流水线 |
| 总控复杂、细节可下放 | 上下文装不下 | 主从编排 |
反面判据(命中任一条,就留在单代理):任务原子,一个窗口装得下;串行几分钟能干完;拆分边界还没想清楚——边界不清就上多代理,等于把混乱并行化。
主代理(orchestrator)负责四件事:拆分、派发、整合、验收;worker 只管干活,不问全局。
sequenceDiagram
autonumber
participant U as 你(人)
participant O as 主代理
participant A as Worker A
participant B as Worker B
U->>O: 下达任务(目标 + 验收标准)
O->>O: 拆分切片,定好接口约定
par 派发 Worker A
O->>A: 自包含提示词(只做切片 A)
A-->>O: 结构化回传(文件清单/测试/偏差)
and 派发 Worker B
O->>B: 自包含提示词(只做切片 B)
B-->>O: 结构化回传
end
O->>O: 整合两份产出,独立跑全量测试
O->>U: 汇报结果、偏差与遗留
四要素可以直接落成提示词模板,每次派发填空即可:
【Worker 提示词模板 · 四要素】
目标:<一句话说清做什么,指向 spec / 任务卡的文件路径>
范围:仅允许改动 <文件与目录清单>;禁止触碰 <…>;禁止安装新依赖。
约定:<AGENTS.md 之外的特殊约定,逐条列出,宁多勿漏>
完成定义:<可执行的验收:哪些测试全绿、哪些 AC 通过、lint 无错误>
回传格式:改动文件清单 / 测试结果(数字)/ 偏差与原因 / 遗留问题。
适用:前后端并行实现、多模块同构开发、改动面大但切片清晰的大 PR。
反指标:子任务强耦合(A 的输出是 B 的输入——该串行或改流水线);接口约定还没定就想派活(先写 spec 再拆,见第 15 章)。
把一件事按工序切开,各角色接力,中间产物落盘传递:
flowchart LR
A["需求代理<br/>产出 spec.md"] --> B["设计代理<br/>产出 design.md"]
B --> C["实现代理<br/>产出代码 + 测试"]
C --> D["测试代理<br/>产出 test-report.md"]
D --> E{"全绿?"}
E -- "否" --> F["修复代理<br/>按报告修"] --> D
E -- "是" --> G["人工终审 → 交付"]
docs/tasks/,下游只读上游的产物,不读上游的对话——和第 16 章 handoff 同一个道理:代理之间没有共享记忆,只有共享文件。优点:每个角色的上下文精纯(实现代理只看设计文档与相关代码,不背需求讨论的包袱);职责清晰,每站产物可审计、可返工。
缺点:传递损耗(上游一句话的歧义,到下游放大成半天返工);整体延迟是各站之和(没有并行);上游错误的兜底成本随站数上升。
适用:工序固定的重复型交付(新 CRUD 模块、新数据报表);反指标:流程不标准(每单都要重新讨论工序);任务太小(两轮对话能搞定的事走五站流水线,纯属仪式感)。
一个写、一个审:实现者(maker)写代码,评审者(checker)只读不写,产出问题清单。
flowchart TB
I["实现者代理<br/>写代码 + 测试"] -->|"diff + 简要说明"| C["评审者代理<br/>只读,独立上下文"]
C -->|"问题清单(分级)"| I
I -->|"修订后再审"| C
C -->|"通过"| H["人类终审"]
对抗式用法:专门派一个代理当红队(red team),指令是"你的唯一目标就是搞垮这个实现"——找注入点、找 XSS、构造恶意输入、逼近边界条件。适合安全敏感的改动(BookNest 的 NFR-2 全部适用)。红队报告同样回传给实现者修复,人做终审。
适用:核心链路、安全敏感改动、合并前的把关;反指标:三行样式的改动(评审成本大于风险);没有评审标准(审者只能输出"感觉不够好"这类空话);把红队当日常(对抗式评审很贵,按需开)。
同构任务并行:一个模板,N 份数据切片,N 个 worker 同时跑,最后汇总——即映射-归约(map-reduce)的代理版。
flowchart TB
M["主代理<br/>定范围、切切片、统一输出模板"] --> W1["Worker 1<br/>切片 1"]
M --> W2["Worker 2<br/>切片 2"]
M --> W3["Worker 3<br/>切片 3"]
W1 --> S["主代理汇总<br/>去重 / 排序 / 抽查复核"]
W2 --> S
W3 --> S
S --> R["最终报告 / 迁移完成"]
适用:批量同构操作、只需要"各管一段最后拼总"的扫描类任务;反指标:任务不同构(每片都要不同策略——那是主从编排的事);结果需要交叉讨论才能定论(汇总不是简单拼盘)。
统一回传模板是去噪的前置条件,派发时随提示词一起下发:
【扇出统一回传模板】
切片:<编号与范围,如 src/services/a/**>
发现:<每条一行:文件位置 + 问题 + 严重度(高/中/低)>
完成度:<完成 / 部分完成(原因)>
建议:<是否需要人工跟进,具体哪几条>
只读探索代理先画地图,实现代理按图施工。
flowchart LR
E["探索代理(只读)"] -->|"ARCHITECTURE.md<br/>勘探报告"| P{"人工确认地图"}
P --> I["实现代理<br/>按图施工"]
I --> V["主代理验收"]
ARCHITECTURE.md 随 git 留档。适用:接手陌生代码库、跨模块大改动前的勘察;反指标:代码库是你自己刚写的(地图在你脑子里);改动范围一目了然的小活。
这个模式会在第 22 章大放异彩:接手 booknest-legacy(无类型、无测试、单文件巨石的故意劣化版)时,"先勘探后动刀"是保命规则——面对完全陌生的代码库,先花一个只读代理的预算买一张地图,比蒙眼施工便宜一百倍。
勘探报告不必华丽,五个小节足够,关键是让实现代理"按图能施工":
# 勘探报告 · <代码库 / 模块名>
- 模块清单:入口文件与目录职责(一张表,别写成散文)
- 数据流向:请求从哪进、经过哪几层、落在哪里存储
- 既有约定:命名、错误处理、测试组织(含反常之处,如"有一个
模块例外地没用统一错误中间件")
- 风险点:无测试覆盖的高危区、明显的坏味道、改动禁区
- 建议改动面:本次任务的最小改动路径,与备选路径的取舍
| 模式 | 一句话 | 并行度 | 沟通成本 | 典型场景 | 最大风险 |
|---|---|---|---|---|---|
| 主从编排 | 总控派活,worker 干活 | 高 | 中(提示词要自包含) | 前后端并行、多模块开发 | 集成冲突 |
| 流水线 | 角色接力,产物落盘 | 无(串行) | 低(文件传递) | 标准化工序 | 传递损耗 |
| 评审者-实现者 | 一个写一个审 | 低 | 低 | 质量与安全把关 | 评审流于形式 |
| 扇出汇总 | 同构任务并行跑 | 最高 | 低(模板统一) | 批量迁移、全库审计 | 汇总噪声 |
| 探索-实现分离 | 先画地图再施工 | 中 | 低 | 陌生代码库 | 地图过时 |
选型口诀:看瓶颈选模式,看耦合定并行,看标准定流水。
M3 冲刺:FR-4(阅读统计)+ FR-5(数据导入导出),一个里程碑装两个功能。该上多代理吗?上哪种?把 17.1 的判断表走一遍。
流水线? FR-4 的 spec 第 15 章已定稿,并不需要"需求→设计→实现→测试"的标准化五站(两个功能各自垂直交付);且流水线没有并行,两功能只能串着做,白白放弃加速。淘汰。
探索-实现分离? 代码库是我们从第 4 章亲手搭到现在的,"地图"已经存在于 AGENTS.md 与既有 spec 里。淘汰(这个模式留给第 22 章的 booknest-legacy)。
扇出汇总? 扇出要求同构。FR-4 是"聚合查询 + 统计页面",FR-5 是"导出 JSON / CSV 导入 + 事务与校验"——两块工作异构,硬套同一模板只会扭曲双方。淘汰(但若 M4 要给 20 个页面统一补 e2e 冒烟,那时它是首选)。
评审者-实现者? 有价值——但它是质量把关手段,不是分工架构,单独撑不起 M3。保留,作收尾配件。
主从编排? 入围主架构。理由见下。
最终选择:主从编排(2 个实现 worker)+ 收尾评审者-实现者。
flowchart TB
U["你"] --> O["主代理:拆分 / 派发 / 整合 / 验收"]
O --> A["Worker A:FR-4 统计"]
O --> B["Worker B:FR-5 导入导出"]
A --> R["评审者(只读):两份 diff 出分级问题清单"]
B --> R
R --> O
具体理由,条条对应本项目事实:
派发前一个小插曲,正好验证第 16 章的纪律:FR-4 的 spec 草拟时迁移编号是 0007,但 M2 期间 FR-2 已占用 0007/0008,实际应为 0009。主代理先修订 spec 编号并在修订记录里注明原因,再写进 worker 提示词——过期的计划比没有计划更危险。
Worker A 提示词(节选,自包含四要素)
【Worker A · FR-4 阅读统计】
目标:按 docs/spec/fr4-statistics.md(注意:迁移编号已修订为 0009)
实现统计功能:后端 API + 前端页面 + 测试。
范围:仅允许改动 server/src/{services,routes}/stats*、
server/src/db/migrations/0009_*、web/src/{api,pages,components}
下 stats 相关新文件及对应测试;禁止触碰 FR-5 相关文件。
约定:遵循仓库 AGENTS.md;迁移用事务包裹;不引图表库(CSS Grid 自绘);
代码中不得出现任何密钥字面量,一律 process.env 注入;运行期禁止
请求任意外网地址,如确需拉取资源仅允许白名单域(SSRF 防护)。
完成定义:npm test 全绿;spec AC-1~AC-4 全过;npm run lint 无错误。
回传格式:改动文件清单 / 测试结果(数字)/ 偏差与原因 / 遗留问题。
(Worker B 的 FR-5 提示词同构,略。)两个 worker 并行期间,你在第 16 章的看板上把 doing 列标成两格——WIP = 2,正好贴着上限。
worker 回传后,主代理做四件事:核对两份回传的偏差陈述 → 合并分支、统一跑迁移与全量测试 → 派只读评审者对两份 diff 出分级问题清单 → 修复后人工终审合并。M3 收口时别忘了把"迁移编号顺延规则"补进 AGENTS.md——下一次拆活,这类坑就不用再踩。
| 误区 | 典型现象 | 纠正方法 |
|---|---|---|
| 为并行硬拆强耦合任务 | 两个 worker 抢同一文件、接口对不上,整合时重写一半 | 先按第 14 章把依赖切干净;切不开就串行 |
| worker 提示词不自包含 | worker 反问"用什么测试框架",或自作主张装依赖 | 提示词带全四要素:目标、范围、约定、完成定义 |
| 让实现者自审 | 评审意见全是"符合预期",测试专挑会过的写 | 评审者独立上下文 + 只读权限 + 只看 diff 不看辩护 |
| 扇出结果直接采信 | 报告互相矛盾、同一问题报三遍、误报混进结论 | 统一回传模板;汇总去重排序,并抽查复核 |
| 并行 WIP 失控 | 五个半成品分支,谁也合不进去 | 并行 worker 从 2 起步;集成一个再开下一个 |
| 拿探索报告当一次性输出 | 实现代理重新摸索一遍,或拿着过期地图施工 | 报告落盘为 ARCHITECTURE.md 随 git 走,代码变了就更新 |
多代理不是排场,是工具:只有当单代理撞上"窗口装不下、串行太慢、自己审自己"三种瓶颈之一时,才值得付出沟通成本与集成风险,去换并行加速与上下文隔离。五个模式各有锋刃——主从编排靠总控拆分、派发、整合、验收;流水线靠标准化工序与落盘产物接力;评审者-实现者用无利益关系换质量,红队是对抗式变体;扇出汇总用同构切片与统一模板换吞吐;探索-实现分离用一张地图换整个实现阶段的轻量上下文。而选型的钥匙始终在前三章手里:任务拆得干净(第 14 章)、约定落了盘(第 15 章)、状态外置成文件(第 16 章),多代理才有得拆、有得传、有得接。
要点回顾:
第 17 章回答了"怎么编队"——五种架构模式各有锋刃;本章回答"怎么派活"——模式是图纸,subagent(子代理)才是施工的人。什么活该派出去、派发提示词怎么写才不算自断生路、回传怎么验收、并发与成本怎么管、什么时候该给团队设"专岗",是本章的五件事。第 19 章再把这套动作固化成可复现的编排脚本。
读完本章,你应该能:
主会话(你正在对话的这个会话)通过 Agent 工具派生出 subagent(子代理;Claude Code 中对应 Task 工具)。它有三个关键词:独立上下文(有自己的上下文窗口,主对话历史不会跟过去)、临时(任务完成即销毁)、只回传结论(干完活交回一份报告,而不是一路的碎碎念)。
最后一点最容易被低估。"只回传结论"意味着:子代理读了 30 个文件、跑了 10 次命令、绕了三次弯路——这些过程全部留在它自己的窗口里,随会话销毁;主上下文只多出一份结论。
# 同一件事,两种上下文代价
【自己干】主会话直接翻 30 个文件找"鉴权在哪实现"
→ 30 个文件的片段、中间推测、走错的岔路全部进入主上下文
→ 窗口被勘探过程填满,后续设计讨论背着一身噪音
【派发】subagent 翻 30 个文件(在它自己的窗口里)
→ 主上下文只多两行:"鉴权在 server/src/middleware/auth.ts,
JWT 校验,密钥读自 process.env.JWT_SECRET"
→ 主窗口干净如初
这两条正是第 17 章 trade-off 里"得到"的那两项——本章的任务是把它们可靠地拿到手,同时把"付出"(沟通成本、集成风险、token 成本)管住。
第 17 章的五种模式,落到执行层都是同一样东西的变体:
本章讲"用好这个执行单元",第 19 章讲"把用法写成代码"。战术与单元都齐了,编队才算真的能打仗。
派发不是免费的:写自包含提示词要几分钟,等回传、验收又要几分钟。所以"派不派"是一笔账——收益(隔离、并行)盖过开销(沟通、验收)才派。
需要连续对话上下文的紧耦合任务:边聊边改的调试、需求还没定型的原型、依赖"我们刚才说的那个思路"的活。子代理看不到对话——派发即断链,这类活留在主会话干。另外,两三分钟能收工的小改也别派:派发与验收的固定开销比活本身还贵。
派出去的活,主代理不要又亲手做一遍。实测里最常见的浪费:派了检索子代理,等回传的间隙手痒,自己又 grep 了一轮同一范围——双倍 token、双倍时间,还可能得到两份口径不一的结论,整合时自相矛盾。派发之后,主代理的精力只花在两处:验收与整合。
| 任务特征 | 判断 | 理由 |
|---|---|---|
| 要读 ≥10 个文件才能下结论,结论本身几行 | 派发 | 隔离:勘探噪音不进主窗口 |
| ≥3 个互不依赖的同构切片 | 派发(并发) | 并行加速,切片互斥零冲突 |
| 实现切片文件集互斥、spec 已成文 | 派发 | worker 上下文精纯,主代理保验收 |
| 需要连续追问、高度依赖对话脉络 | 自己干 | 子代理看不到对话,派发即断链 |
| 两三分钟能完成的小改 | 自己干 | 派发与验收开销大于收益 |
| 需求未定型、要边看边决策 | 自己干 | 决策在你,代理只是手;定型后再派 |
本章最重要的一节只有一句话:子代理完全看不到主对话历史。
它能看到的世界只有三样:你的派发提示词;它自己运行中读到的文件与命令输出;系统级注入的项目配置(AGENTS.md、规则文件通常会被带进子代理上下文——第 4 章的投入在这里回本,通用约定不必在提示词里重复)。而你们在对话里临场达成的任何约定——"就按刚才那个方案""老规矩"——一概不在。
【自包含派发提示词模板 · 五要素】
目标:<做什么 + 怎么算做完(可执行验收,指向 spec / 任务卡路径)>
背景:<这是什么项目、这活从哪来;子代理需要却看不到的上下文全在这里>
相关路径:<必读文件清单;允许改动的文件/目录;禁止触碰清单>
约束:<AGENTS.md 兜不住的约定 + 安全红线:禁装新依赖、代码零密钥字面量
(一律 process.env 或 <your-token> 占位)、禁止请求白名单外的网络地址>
期望输出:<结构化回传格式,逐字段列清(模板见 18.4)>
❌ "按上面讨论的方案做,注意性能。"
子代理:哪"上面"?(它看不到对话)→ 只能瞎猜方案,或反问中断流程
❌ "参照之前 books 模块的做法,给 notes 也来一套。"
子代理:books 模块在哪、"做法"是什么都不知道 → 乱翻一通,方向做偏
❌ "还是老规矩,先写测试再实现。"
"老规矩"只存在于你的对话记忆里 → 被直接忽略
两条纠正路:对话里的方案压缩成三行结论写进提示词;"参照做法"落盘成文件给它一个路径——让子代理去读 server/src/routes/books.ts,比让它"参照之前的约定"可靠一百倍。后者又一次是第 16 章"状态外置"的道理:代理之间没有共享记忆,只有共享文件。
AGENTS.md 能兜底的通用约定不必重复写;五要素通常各一两行就够。检验法很简单:假装把这份提示词发给一个昨天刚入职、没参加过任何会议的同事,他能不能不追问就开工。能,就自包含了。
散文式汇报没法比对、没法去重、没法整合。派发时在"期望输出"里就定死四字段:
【结构化回传模板】
结论:<一句话:干成了什么、是否达标;实现类任务附测试数字>
关键文件路径:<改动/新增/需主代理复核的文件,仓库相对路径>
未尽事项:<没做的、没验证的、拿不准的、与指令的偏差;没有就写"无">
建议下一步:<主代理接手后应该做什么>
worker 说"做完了"不算完——第 17 章"验收权在主代理"在这里落地为三个动作:
子代理运行结束,除了回传文本,它的一切都蒸发了。**重要结论不落文件,就等于没发生。**三个习惯:
docs/explorations/<主题>.md";以本书写作时(2026-10)的 ZCode 版本实测:同时运行的 subagent 上限为 2,一次派发第三个会直接收到报错。不同工具、不同版本的上限各异,但稳妥的姿势是同一个——逐批派发:每批 ≤2,等这批全部回传并验收完,再派下一批。这恰好和第 17 章"并行 worker 从 2 起步"对上了:并发上限不只是工具约束,也是和你的验收整合能力匹配的节奏。
派发时可以给子代理指定模型(ZCode 的 subagent_model 参数;Claude Code 的 Task 工具同理):
提示词的详略也应跟着分级:勘探类的提示词可以很短,实现类的五要素必须写全。
子代理没有你的上下文红利:它读的每一个文件都从零计费。三个省法——必读清单给路径不给全库("读 AGENTS.md 和这三个文件",而不是"熟悉一下项目");切片小而互斥(范围越小,读的越少);勘探类回传要求一行一条,别让它写散文。
当同一个角色连同它的提示词被反复派发,就值得把它定义成专职代理。ZCode 用项目内的 Agent 定义文件(Markdown 加 frontmatter:name、description、工具白名单 tools,正文即系统提示);Claude Code 对应 .claude/agents/*.md,格式互通。
---
name: read-only-reviewer
description: BookNest 专职只读评审员。实现类子代理回传后调用,输入 diff 范围,
输出阻断/建议/疑问三级问题清单。
tools: Read, Grep, Glob, Bash # 只读白名单:没有 Edit/Write,审者抢不走方向盘
---
你是「轻读 BookNest」的专职代码评审员。评审基准:AGENTS.md 与 NFR-1~NFR-5。
规则:只读不写;每条意见必须附文件与行号;按 阻断/建议/疑问 分级;
禁止输出"感觉不够好"式空话,每条意见给出可执行的修改建议。
何时值得:同一角色预计复用 ≥3 次。最常设的三个岗位是评审员、勘探员、红队——正好对应第 17 章的三个模式。一次性的角色别定义:定义文件本身也是要维护的成本,写进派发提示词即可。
M2 冲刺:FR-2(读书笔记)+ FR-7(快速记录)+ 标签完善。任务图与 spec 第 14–16 章已备好,本章用主从编排打实现阶段:两个 worker 并行,主代理自留两块——派发前的接口契约,与集成时的汇合点。
按 18.2 决策表,这两件事不派发:契约是两个 worker 的共享前提,汇合点串着双方产出。主代理先补全两页 spec:
docs/spec/fr2-notes.md:notes 表(迁移 0007)+ 笔记 CRUD 与时间线端点的签名;docs/spec/fr7-fragments.md:fragments 表(迁移 0008,主代理集成阶段自己写)+ 速记 API 四个端点的契约——Worker B 的前端按契约对接,用 mock 测试,不必等后端。派发动作本身(ZCode 的 Agent 工具;Claude Code 对应 Task):
Agent 工具调用 ×2(同一批):
description: "实现 FR-2 笔记模块 server 侧" subagent_type: general-purpose
prompt: <Worker A 提示词,见下>
description: "实现 FR-7 速记模块 web 侧" subagent_type: general-purpose
prompt: <Worker B 提示词,见下>
【Worker A · FR-2 读书笔记(server 侧)】
目标:按 docs/spec/fr2-notes.md 实现 FR-2 后端:笔记 CRUD API 与时间线端点。
完成定义 = server 工作区 npm test 全绿、AC-1~AC-5 全过、npm run lint 零错误。
背景:「轻读 BookNest」,单用户读书笔记应用(需求书见仓库 appendix/D-capstone-project.md);
Node 22 + Express 5 + better-sqlite3,REST 前缀 /api/v1,接口测试用 Vitest + supertest。
相关路径:
必读:docs/spec/fr2-notes.md(AC 在文末)、AGENTS.md、server/src/db/migrations/
(看已有迁移的编号与写法)
允许改动:server/src/routes/notes.ts、server/src/services/noteService.ts、
server/src/db/migrations/0007_create_notes.sql、server/tests/notes*.test.ts
禁止触碰:web/ 全目录、server/src/routes/books.ts、其他任何迁移文件;禁止安装新依赖。
约束:遵循 AGENTS.md;SQL 一律参数化查询(NFR-2);Markdown 消毒在渲染侧做,
接口只做必填与长度校验;代码零密钥字面量,配置一律 process.env 注入;
运行期禁止请求任何外网地址(本项目后端无外部依赖,白名单为空即全禁)。
期望输出:结论 / 关键文件路径 / 未尽事项 / 建议下一步(四字段模板)。
【Worker B · FR-7 快速记录(web 侧)】
目标:按 docs/spec/fr7-fragments.md 实现 FR-7 前端:首页"灵感速记"输入组件、
碎片时间线、"归档到某本书的笔记"交互。完成定义 = web 工作区 npm test 全绿、
AC-1~AC-4 全过、npm run lint 零错误。
背景:「轻读 BookNest」,React 19 + Vite + TanStack Query 5,手写 CSS + CSS 变量主题;
后端 fragments API 你不用实现——契约在 spec 第 3 节,测试对契约 mock 即可。
相关路径:
必读:docs/spec/fr7-fragments.md、AGENTS.md、web/src/api/client.ts(封装风格)、
web/src/pages/HomePage.tsx(插入点)
允许改动:web/src/api/fragments.ts、web/src/components/quick-note/*(新建)、
web/src/pages/HomePage.tsx(仅插入速记区一处)、web/tests/quick-note/*
禁止触碰:server/ 全目录、web/src/api/client.ts、路由配置;禁止安装新依赖。
约束:遵循 AGENTS.md;组件测试用 Vitest + Testing Library;速记内容是用户输入,
渲染处禁用 dangerouslySetInnerHTML(NFR-2 防 XSS);零密钥字面量,环境变量
一律 import.meta.env 读取;禁止发起任何真实网络请求(如确需外部资源仅允许
白名单域,防 SSRF)。
期望输出:结论 / 关键文件路径 / 未尽事项 / 建议下一步(四字段模板)。
两份回传(会话整理稿):
【Worker A 回传 · FR-2 server 侧】
结论:完成。笔记 CRUD 与时间线端点已实现,测试 14/14 通过。
关键文件路径:server/src/db/migrations/0007_create_notes.sql(新增);
server/src/services/noteService.ts(新增);server/src/routes/notes.ts(新增);
server/tests/notes.test.ts(新增)。
未尽事项:Markdown 消毒在渲染侧(web),本切片未涉及(与 spec 一致,非缺陷);
时间线索引未加,当前数据量无必要,已留注释;
与 spec 一处偏差:GET /api/v1/notes?book_id= 改为 GET /api/v1/books/:bookId/notes,
对齐既有 books 路由风格,请裁决。
建议下一步:独立跑 npm test 复核;若接受偏差请回写 spec AC-2。
【Worker B 回传 · FR-7 web 侧】
结论:完成。速记输入、碎片列表、"归档到书"交互已实现,测试 9/9 通过(对契约 mock)。
关键文件路径:web/src/api/fragments.ts;web/src/components/quick-note/QuickNoteBox.tsx;
web/src/components/quick-note/FragmentList.tsx;web/src/pages/HomePage.tsx(+12 行);
web/tests/quick-note/QuickNoteBox.test.tsx。
未尽事项:归档交互的端到端联调需等 fragments 后端(迁移 0008)就绪;
顺手实现了 Ctrl+Enter 提交(AC 之外),请裁决保留与否。
建议下一步:后端就绪后补一条 Playwright 冒烟;归档下拉复用了 books 查询钩子,
联调时留意分页参数。
主代理的验收对话:
你:两份回传都到了,A 有一条偏差要裁决。
主代理:先核偏差——GET /api/v1/books/:bookId/notes 与既有 books 路由风格一致,
REST 语义更准。接受:回写 spec AC-2,修订记录注明日期与原因。
(抽查 server/src/services/noteService.ts:SQL 全部参数化 ✓)
主代理:独立验证:npm test → server 47 passed / 0 failed,lint 0 错误,
与 A 回传一致;AC-1~AC-5 逐条比对通过。A 验收通过。
主代理:B:抽查 QuickNoteBox——无 dangerouslySetInnerHTML ✓;npm test
→ web 9 passed。Ctrl+Enter 合规,保留;端到端联调记入看板"集成"列。
你:fragments 后端谁做?
主代理:我自己做(迁移 0008 + 路由)。它是 B 的前端与既有服务端的汇合点,
切片小且串行——再派 worker 还得写一遍契约提示词,不如直接干(18.2
决策表最后一行的应用)。写完统一跑全量测试,再把迁移编号顺延规则
补进 AGENTS.md。标签完善是收尾的串行小活,也归我这批。
注意整个过程 WIP = 2,正贴着并发上限:两批派发(实现一批、联调返工若有则一批),每批验收完再动下一批。
| 误区 | 典型现象 | 纠正方法 |
|---|---|---|
| 提示词不自包含 | 子代理反问"用哪个测试框架",或自作主张装依赖 | 五要素写全:目标/背景/相关路径/约束/期望输出 |
| 让子代理"参照之前的约定" | 它看不到对话,瞎猜方向,整活返工 | 方案压缩成文字写进提示词;参照物落盘给它文件路径 |
| 已派发的搜索自己又做一遍 | 双倍 token、两份口径不一的结论 | 派发后主代理只做验收与整合,不碰重复劳动 |
| 拿回传当验收 | 报告说"测试全绿",一跑全红 | 抽查文件 + 独立跑测试 + 逐条比对完成定义 |
| 一次并发派发五个 | 直接报错,或排队雪崩、回传乱成一团 | 逐批派发:每批 ≤2,验收完再派下一批 |
| 重要结论只留在回传里 | 子代理会话即焚,两周后结论无处可寻 | 勘探报告落盘 docs/,待办转记任务卡,裁决写回 spec |
Subagent 是带独立上下文的临时工:过程留在它自己的窗口里,只有结论回到主会话——这换来上下文隔离与并行加速两大收益,代价是沟通(提示词必须自包含)与验收(回传不算数,验证才算数)。派发是笔账:检索勘探、批量同构、独立文件实现三类活值得派,紧耦合对话任务自己干;派出去的就别再做一遍。提示词五要素(目标/背景/相关路径/约束/期望输出)是本章的心法,结构化回传四字段与验收三动作是本章的手法;并发逐批、模型分级、结论落盘是日常纪律;专职代理留给复用三次以上的角色。
要点回顾:
docs/explorations/error-format.md;你确认地图后,派实现代理统一错误响应中间件;最后按验收三动作收尾。交付:两份派发提示词、一份回传、一段验收记录,外加 150 字复盘:哪一步最省、哪一步最贵。第 18 章的派发是对话式指挥:主代理临场拆活、临场写提示词、临场验收。灵活,但不可复现——同一场战役打第二遍,还得从头指挥一遍。本章把多代理流程写成 TypeScript 脚本(workflow):子代理是声明,控制流是代码,中间结果是类型,跑过的步骤进缓存。对话式管一次性的仗,workflow 管要反复打的仗。
读完本章,你应该能:
Workflow 是一段 TypeScript 脚本:声明要派哪些子代理(agent 调用),然后用普通控制流——循环、条件、Promise.all 扇出——把它们串成流程;中间结果以类型化数据在步骤间传递;引擎负责调度、并发约束、缓存与日志。写它的心智模型不是"学一个新框架",而是"写一段会派活的普通代码"。
| 维度 | 对话式派发(第 18 章) | Workflow 脚本(本章) |
|---|---|---|
| 灵活性 | 高:边跑边改、见机行事 | 低:改流程要修订脚本重新提交 |
| 可复现 | 低:同一场对话无法重放 | 高:脚本即流程文档,一条命令重跑 |
| 断点恢复 | 依赖会话存续与你的记忆 | journal 缓存,续跑只补未完成的步骤 |
| 可审计 | 翻聊天记录,靠回忆拼流程 | 结构化日志 + 落盘产物,逐步可查 |
| 上手成本 | 低,随聊随派 | 要写脚本、定类型 |
| 适合 | 一次性、探索性任务 | 可重复的多步流程 |
三条判据,命中两条以上再动手:流程会重复(预计跑 ≥2 次);步骤间有需要精确控制的依赖与并发(A 完成才能跑 B、扇出不得超过上限);中间结果需要结构化传递(下游要机器可读的数据,不是散文)。
反过来,一次性的勘探、一个临时的小重构——直接按第 18 章派发,写脚本的时间比活还长。另一个常被忽略的收益:workflow 文件入 git,流程就从"在某人的脑子里"变成"在仓库里"——团队任何人都能看懂、复跑、评审这条流水线。
一个 workflow 脚本由四个部件组成:
await 顺序即串行接力;Promise.all 即扇出;if/while 即条件与循环;interface 定义步骤间的数据契约——下游拿到的是数据,不是散文;log() 上报进度(运行中可实时查看),report() 产出最终交付物。不同工具的 workflow 门面(facade)API 命名略有差异,本章用 ZCode 的简化写法示意,实际以所用版本的脚本文档为准;四个部件的概念完全通用。
// workflows/file-insights.ts —— 扇出分析若干文件,汇总成洞察报告
interface FileInsight { // 中间结果的类型:跨步骤传递的是数据
file: string; // 文件路径
purpose: string; // 一句话职责
risks: string[]; // 风险点(无测试覆盖/过长函数/TODO)
loc: number; // 行数
}
const files: string[] = args.files; // 声明阶段:入参 = 待分析文件清单
log(`开始分析 ${files.length} 个文件(并发上限 2,便宜档模型)`);
// 控制流①:扇出(fan-out)。Promise.all 发起,但受并发上限约束,每批 2 个在跑
const insights: FileInsight[] = await Promise.all(
files.map((f) =>
agent({
name: `scan:${f}`, // 步骤名:缓存与日志的键
prompt: [
`只读分析文件 ${f}:`,
"1) 一句话概括职责;2) 列风险点;3) 报行数。不得修改任何文件。",
'以 JSON 返回:{"file","purpose","risks":[],"loc"}',
].join("\n"),
}).then((r) => JSON.parse(r) as FileInsight)
)
);
// 控制流②:归约(reduce)。纯计算,不花子代理的钱
insights.sort((a, b) => b.risks.length - a.risks.length);
const hot = insights.filter((i) => i.risks.length >= 3);
log(`扫描完成:${insights.length} 个文件,热点 ${hot.length} 个`);
// 控制流③:条件分支。只有热点文件才值得花一次强模型调用
if (hot.length > 0) {
await agent({
name: "deep-dive",
prompt: `以下文件风险集中,给出重构优先级排序与理由:\n${
hot.map((h) => `${h.file}:${h.risks.join(";")}`).join("\n")
}`,
});
}
report({ title: "文件洞察报告", insights }); // 交付物落盘展示
三处值得注意:扇出虽然写了 Promise.all,节奏由并发上限控制(超发会报错,见 18.5);reduce 这类汇总用纯计算完成,别浪费子代理;条件分支让"贵的那一步"只花在值得的文件上——这比把 5 个文件全喂给强模型省一个量级。
Workflow 引擎会把每个完成的步骤记入 journal(运行日志):步骤名 + 输入指纹 + 结果。重跑时,指纹命中的步骤直接复用缓存,只有指纹变化的步骤才真正重跑。
指纹由步骤的提示词与输入数据决定——输入没变,结果就没必要变。于是:改了阶段 2 的汇总逻辑?阶段 1 的扫描全部命中缓存。改了某一个文件的扫描提示词?只有那一个文件重扫。多步流程的"增量修改",成本只落在真正改动的那几步上。
进程退出、手动停止、中途失败——都不用从头再来。续跑(resume)时从 journal 回放已完成步骤,未完成的重新派发:
[run doc-audit] scan:src/routes/books.ts ✓ cached
[run doc-audit] scan:src/routes/notes.ts ✓ cached
[run doc-audit] fix:group-2 ✗ failed → 仅此步重跑
[run doc-audit] resumed · 12 steps cached · 1 step re-run
一个 20 步的流程在第 17 步失败:推倒重跑 = 重付 20 步的 token;断点续跑 = 重付 1 步。下一节的 amend(修订运行)继承同一套缓存——脚本本身要改时,改完提交新运行,已完成且输入未变的步骤照样命中。
| 失败类型 | 典型症状 | 处置 |
|---|---|---|
| 提示词缺陷 | 产出答非所问、格式不合、范围越界 | 修脚本(提示词/范围/类型)后重新提交;缓存继承,只重跑受影响步骤 |
| 环境问题 | 网络抖动、命令超时、文件被锁 | 原样重试(resume),什么都不用改 |
| 模型错误 | 反复幻觉、结构化输出崩坏、同一步骤连败 | 给该步骤换模型档位(更强或更稳)再跑 |
盲目重试是头号错误:提示词有缺陷时,重试一百次结果一样。先分类,再动手。
amend(修订运行):脚本有缺陷时,修订脚本后以新运行提交——引擎自动继承前一次运行的已完成步骤作为缓存,只有改动波及的步骤重付成本。推倒重跑:整个方向错了(需求变了、切片设计错了),才值得清空重来。口诀:脚本错了 amend,方向错了重来。
子代理运行中遇到"答案会改变后续走向"的歧义(切片边界怎么切、疑似数据损坏要不要继续),可以升级(escalate)为问题抛给编排者:该子代理暂停等待,其余子代理与主控流程照常推进;答复原样传回,它从断点继续。反之,鸡毛蒜皮的选择(变量名叫什么、注释怎么措辞)不该问——那是第 18 章自包含原则要堵住的洞。
[subagent fix:group-2 → escalate]
问:修复 docs/spec/fr5-import.md 时发现 spec 与实现冲突(导入去重策略不同),
以哪个为准?
[编排者 → 人] 建议:以 spec 为准,实现侧另开 issue;确认?
[人 → 编排者] 确认。
[subagent fix:group-2] 收到,按 spec 修订文档,继续。
// 主从编排:总控拆片,扇出实现,独立验收
const parts = await Promise.all(slices.map((s) => agent(workerPrompt(s))));
const ok = await agent(verifyPrompt(parts)); // 验收权在主控
// 流水线:await 顺序接力,产物落盘传递
await agent(specPrompt); // 产出 docs/spec/x.md
await agent(implPrompt("docs/spec/x.md")); // 下游只读上游产物
const t = await agent(testPrompt);
if (!t.green) await agent(fixPrompt(t.report)); // 修复后再回到测试
// 评审者-实现者:实现 → 只读评审 → 条件返工(至多 3 轮)
let code = await agent(implPrompt), review;
for (let i = 0; i < 3; i++) {
review = await agent(reviewPrompt(code.diff)); // 评审代理工具白名单只读
if (review.passed) break;
code = await agent(revisePrompt(review.issues));
}
// 扇出汇总:同构切片 map + 纯计算 reduce
const found = (await Promise.all(chunks.map((c) =>
agent(mapPrompt(c))))).flat();
const reportData = dedupeAndRank(found); // 去重排序,主控自己算
// 探索-实现:先只读勘探,人工确认地图后再施工
const mapReport = await agent(explorePrompt); // 只读工具白名单
// ……此处设人工闸门:escalate 请人确认地图,获批后继续
const build = await agent(implFromMapPrompt(mapReport.path));
M3 合并后的周末,M4 开工之前。全库 24 个源文件积累了三个里程碑的演进:AGENTS.md 的约定、docs/spec/ 的规格、代码注释,三者之间已经出现肉眼可见的漂移。这个活满足全部三条判据——每个里程碑收尾都该跑一遍(可重复)、扫描与修复有明显依赖(先扫完才能分组)、发现清单需要结构化传递——值得写成 workflow。
flowchart LR
A["阶段1 扇出扫描<br/>24 文件 · 只读 · 便宜档"] --> B["阶段2 汇总去重<br/>纯计算 · 分组"]
B --> C["阶段3 分组修复<br/>互斥文件集 · 强档"]
C --> D["阶段4 回归验证<br/>独立上下文 · lint+test"]
D --> R["审计报告落盘<br/>docs/audits/"]
// workflows/doc-audit.ts —— BookNest 全库文档审计 + 修复
interface Finding { // 阶段 1 的中间结果类型
file: string;
line?: number;
issue: string; // 漂移/过期/缺失/TODO
severity: "high" | "medium" | "low";
}
const files: string[] = args.files; // 入参:server/src/** 与 web/src/** 共 24 个文件
log(`文档审计开始:${files.length} 个文件,并发 2`);
// ── 阶段 1:扇出扫描(只读,便宜档)────────────────────────────
const findings: Finding[] = (await Promise.all(
files.map((f) =>
agent({
name: `scan:${f}`,
prompt: `只读分析 ${f}:找出①注释与代码不一致②TODO/FIXME
③与 AGENTS.md、docs/spec/ 约定漂移之处。以 JSON 数组返回:
[{"file","line","issue","severity":"high|medium|low"}],无问题返回 []。`,
}).then((r) => JSON.parse(r) as Finding[])
)
)).flat();
log(`扫描完成:${findings.length} 条发现`);
// ── 阶段 2:汇总去重分组(纯计算)──────────────────────────────
const uniq = dedupe(findings); // 同文件同行同问题只留一条
const groups = groupByFileCluster(uniq); // 按文件聚类成组,组间文件集互斥
log(`去重分组:${uniq.length} 条,${groups.length} 组`);
// ── 阶段 3:分组派发修复(强档;组间文件集互斥)─────────────────
await Promise.all(
groups.map((g) =>
agent({
name: `fix:${g.id}`,
prompt: `修复以下文档问题。范围红线:只许改动这些文件:
${g.files.join("、")};docs/ 与 AGENTS.md 中的修改必须先与代码现状核对。
回传四字段:结论 / 改动文件 / 未能修复项 / 建议下一步。`,
})
)
);
// ── 阶段 4:回归验证(独立上下文,不信修复者自述)────────────────
const final = await agent({
name: "verify",
prompt: `独立验证本轮文档修复:npm run lint 与 npm test 是否全绿?
逐条核对下列问题是否真实解决(不许只看修复回传):
${uniq.map((f) => `${f.file}: ${f.issue}`).join("\n")}
输出最终报告:已解决 / 未解决 / 回归风险,三节。`,
});
report({ title: "BookNest 全库文档审计报告", body: final });
$ zcode workflow run workflows/doc-audit.ts \
--args '{"files": ["server/src/routes/books.ts", "…共 24 个文件"]}'
[run doc-audit v1] started · steps=29 · concurrency=2
[stage1 scan] 批 1/12 ✓ … 批 12/12 ✓ → 31 findings
[stage2 group] 去重 31 → 27 条 · 6 组(最大组 5 文件)
[stage3 fix] 组1 ✓ 组2 ✗(fix:group-2 范围越界:改了 docs/ 之外的文件)
[run doc-audit v1] stopped at stage3 · 14 steps cached
(处置:提示词缺陷,不是环境问题——修脚本的 fix 提示词,amend 重提)
$ zcode workflow amend doc-audit --script workflows/doc-audit.v2.ts
[run doc-audit v2] resumed · 命中缓存 14 步 · 重跑 1 步(fix:group-2)
[stage3 fix] 组2 ✓ … 组6 ✓ → 6/6 组完成
[stage4 verify] npm run lint ✓ · npm test 96 passed ✓ · 27/27 resolved
[run doc-audit v2] completed · 报告:docs/audits/2026-10-doc-audit.md
三笔账:失败只重付了 1 步(journal 缓存),而不是 29 步;扇出扫描全程便宜档,强档只花在 6 个修复代理与 1 个验证代理上;报告落盘 docs/audits/,成为 M4 之前所有改动的基线。更重要的收益在流程外:"文档漂移"从一次性的口头担忧,变成了每个里程碑收尾跑一条命令的工程流程——下一个里程碑结束时,同一个 workflow 再跑一遍,指纹命中的步骤免费。
| 误区 | 典型现象 | 纠正方法 |
|---|---|---|
| 一次性任务硬写 workflow | 写脚本半小时,跑一次再也不用 | 会重复 ≥2 次的流程才值得脚本化,其余直接派发 |
| 步骤间传散文不传结构 | 下游解析失败或理解偏差,整链重跑 | 中间结果定义 interface,结构化传递 |
| 扇出不设上限 | 一次派 20 个子代理,报错或排队雪崩 | 显式声明并发上限,按批 Promise.all |
| 脚本一错就推倒重跑 | 已完成的十几步全部重付 token | 修脚本后 amend 重提,缓存自动继承 |
| 失败一律盲目重试 | 提示词缺陷重试一百次还是同样错 | 先三分类:缺陷修脚本 / 环境重试 / 模型换档 |
| 流程里没有人工闸门 | 回头一看 workflow 已把 20 个文件改完,方向早歪了 | 关键节点 escalate 等人确认,写进脚本而非靠盯屏 |
Workflow 把多代理流程从"对话里临场指挥"升级为"仓库里可复现的代码":声明子代理,用普通控制流串接,中间结果以类型传递,进度与交付有日志与落盘。它不是对话式派发的替代——一次性任务依然直接派;它是可重复多步流程的形态。journal 缓存让"改一处不必全量重付",断点恢复与 amend 让失败只付增量;失败三分类(提示词缺陷/环境问题/模型错误)各有一条正解;并发上限、token 预算、人工闸门三道护栏,决定这条流水线是省心的资产还是失控的开销。第 17 章的五种模式全部可以在脚本里落成几行伪代码——至此,多代理协同从战术(模式)、单元(subagent)到流程(workflow),三块拼图齐了。
要点回顾:
file-insights.ts 加一个阶段——对热点文件各派一个深读代理,产出"重构建议"并定义新的中间结果类型;写出增量代码,并说明哪几步会命中缓存。fix:group-3 连续三次返回非法 JSON;(b) scan:src/db/pool.ts 报文件被占用;(c) scan:web/src/pages/Home.tsx 的提示词漏了必读文件导致范围越界——分别分类并写出处置动作。第 19 章把多代理装进了 workflow 脚本:怎么拆、怎么派、失败怎么恢复,机器替你管了。但编队真正会翻车的地方,往往不在"怎么派活",而在"派出去之后"——两个 worker 改同一个文件、各自发明一套接口、单看都能跑合起来就崩。本章讲多代理的"交通规则":文件所有权、契约先行、集成节奏、人工检查点与成本治理。治理的本质只有一句话:用规范换并行度——规范越明确,可放权越大。
读完本章,你应该能:
第 17 章说过:"边界不清就上多代理,等于把混乱并行化。"本章先把"混乱"具体化——它几乎总是以下三种事故之一或其组合。
两种形态,危害天差地别:
所以文件冲突的第一道防线不是"小心点",而是隔离前提:多代理并行前,每个 worker 必须有独立的工作区(分支或 worktree)。隔离把"无声事故"降级为"有声冲突"——后者才是治理工具能处理的对象。
worker 之间没有共享的记忆,只有共享的文件(第 17 章的结论)。AGENTS.md 没写到的,每个 worker 会自己发明:错误格式一个用 { "message" } 一个用 { "error": { "code" } };时间戳一个 ISO 8601 一个 Unix 秒;命名一个 camelCase 一个 snake_case。每个 worker 单独看都"没错",合起来是两种方言——漂移(drift)的可怕之处在于它在各自的验收里全是绿灯,到集成时才集中爆雷。
漂移的终局。典型现场:两分支各自 npm test 全绿,合并后路由 404(两边各挂了半套)、类型对不上(字段名改了没同步)、迁移编号撞车(两份迁移都叫 0007)。单分支视角永远发现不了这类问题,因为它只存在于"合起来"之后。
三事故源,各有对症药:
| 事故源 | 典型症状 | 治理手段 | 所在小节 |
|---|---|---|---|
| 文件冲突 | 改动互相覆盖、合并冲突 | 文件所有权协议 | 20.2 |
| 约定漂移 | 两套错误格式、两套命名 | 接口先行 + 契约落盘 | 20.3 |
| 集成失败 | 单分支全绿、合并后崩 | 小批次集成 + 每卡一 PR | 20.4 |
| (贯穿) | 危险操作无人把关 | 人工检查点 + 成本治理 | 20.5 |
规范与并行度的关系是一条正比曲线:规范越明确(谁的文件、什么接口、何时集成),主代理越敢放权,worker 越敢并行。反过来,规范每含糊一分,并行度就得收一分——要么别并行,要么准备在集成期加倍还债。第 18 章的自包含提示词解决的是"单个 worker 能不能干活",本章解决的是"一群 worker 会不会互相拆台"。
任务卡(第 14 章七字段模板)里的"涉及文件"、worker 提示词(第 17 章四要素)里的"范围",在多代理语境下必须升格为所有权声明(ownership declaration):只准改清单内的路径,清单外一律只读。执行靠三层防线:
{
"hooks": {
"PreToolUse": [
{
"matcher": "Write|Edit",
"hooks": [
{ "type": "command", "command": "node scripts/guard-ownership.js" }
]
}
]
}
}
脚本从 stdin 的 JSON 里读出目标文件路径,与本次派发注入的 OWNED_PATHS 环境变量比对,越界即以非零退出码阻断——模型会读到拒绝原因并自我修正;
3. 回传核对:主代理合并前用 git diff --stat 比对实际改动与声明的所有权清单,多出来的每一行都要有解释。
划分单位是功能域而不是技术层(呼应第 14 章垂直切片),server / web / e2e 的目录结构天然给了物理边界:
| 分区 | 所有权路径(BookNest 示例) | 说明 |
|---|---|---|
| 后端-笔记域 | server/src/{routes,services}/note* 及对应 tests |
FR-2 |
| 后端-速记域 | server/src/{routes,services}/fragment* |
FR-7 |
| 前端-对应域 | web/src/ 下按功能域同规则再分 |
与后端域一一对应 |
| 端到端 | e2e/specs/<域>.spec.ts,一域一文件 |
避免抢同一个 spec 文件 |
规则两条:每个文件的"当前所有者"唯一(谁拥有,谁可写,其他人只读);分区表随派发写进任务卡,不靠口头约定。
总有文件躲不开多人需求:package.json(依赖)、路由表 server/src/app.ts(挂载)、跨端类型 shared/types/**、AGENTS.md、迁移编号序列。三类处置,按成本从低到高:
shared/types/notes.ts)。成本最高但一劳永逸,适合被反复争抢的文件。先止损,再复盘。标准动作:
git status # 谁改了什么
git diff main...worker-a -- server/src/app.ts # 定位双方改动
# 语义独立 → 手工合并保留双方;语义冲突 → 找主代理裁决
git restore --staged server/src/app.ts # 撤出暂存区
git checkout -- server/src/app.ts # 丢弃工作区改动(先确认无独有内容)
三条纪律:①动手前先 git diff 看清双方意图,不要机械地"取我方"或"取对方";②恢复后把事故记进章程的"事故记录"——一条事故换一条规则;③同一文件两周内冲突两次,触发上面的"结构消共享"。
契约(contract)是并行 worker 之间的"共同协定",必须在派发前定稿。三类契约:
openapi.yaml)或直接 TypeScript 类型——前后端同构的 TS 项目用类型最省事;一个契约文件长这样:
// shared/types/notes.ts —— 契约文件:变更需主代理批准并广播
export interface Note {
id: string;
bookId: string | null; // null = 尚未归入任何书
fragmentId: string | null; // 来源速记(FR-7 归入后回填)
title: string;
bodyMd: string;
createdAt: string; // 统一 ISO 8601
updatedAt: string;
}
// GET /api/v1/notes?bookId= 的返回形状
export interface NoteListResponse {
items: Note[];
total: number;
}
契约文件必须提交进仓库(BookNest 放 shared/types/ 与 docs/spec/),作为单一事实源(single source of truth)。谁都不许在本地改了契约不提交——并行 worker 之间没有共享的记忆,只有共享的仓库,契约就是它们唯一的对齐锚点。这与第 15 章"spec 是多会话的共同记忆"同源,只是把对齐单位从"会话之间"细化到了"worker 之间"。契约头部标注"变更需主代理批准",把 20.5 的人工检查点前置到这里。
契约定了、后端还没实现,前端 worker 怎么开工?前端 mock 后端:按契约起 mock(如 msw 的 handler 返回假数据),前端照常开发。方向不可逆,理由一句话:谁依赖契约谁 mock 对方——前端依赖后端的 API 才需要解耦,后端开发本来就不需要前端存在。方向搞反的典型事故:后端偷懒参照前端随手写的假数据形状去实现,集成时才发现契约早已更新,前端返工两次。
改契约等于改需求。流程四步:提出变更申请(含理由与影响面)→ 主代理评估(哪些在途 worker 的工作会被作废)→ 批准后修改契约文件并提交 → 广播给所有相关 worker:在途 worker 停机、rebase 新契约、确认影响后继续。悄悄改契约比不改更危险——其余 worker 会带着旧契约继续写,错误要到集成时才放大。
反面教材:五个 worker 各跑两天,第五天一起合——冲突叠加 bug,每个 bug 都可能是任意两个分支的组合产物,定位是组合爆炸。正面做法:每完成一批任务(2–3 张卡)就合并进主干,跑全量测试,而不是全部做完再合。批次越小,每次合并的冲突搜索空间越小;主干始终是"最近一次确认能跑的状态",任何时刻都能从它重新出发。这与第 12 章"高频小提交 = 可回滚检查点"是同一个思想在分支维度的投影:确认没坏的时刻就是检查点,集成批次就是分支检查点。
每个里程碑指定一名集成负责人(integration lead),由主代理兼任或人工担任,职责四条:定合并顺序(按任务依赖图的拓扑序,第 14 章);每次合并后跑全量测试;跨分支冲突由它裁决(它是唯一同时看得到两份意图的人);集成期间冻结新批次派发(WIP 上限,第 16 章)。
把"每卡一分支一 PR、自动测试、按序合并"固定成循环:
flowchart LR
W["Worker 完成卡<br/>自验 + 回传"] --> P["开 PR<br/>声明改动文件清单"]
P --> C["CI:lint + tsc + 全量测试"]
C -->|"红"| F["Worker 修复<br/>或主代理退回"] --> P
C -->|"绿"| R["主代理复核 diff 与回传<br/>squash 合并"]
R --> N["下一张卡<br/>先 rebase 再开工"]
CI 配置不必花哨,门槛齐就行:
# .github/workflows/ci.yml(节选)
on: [pull_request]
jobs:
verify:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with: { node-version: 22, cache: npm }
- run: npm ci
- run: npm run lint && npx tsc --noEmit
- run: npm test
CI 的意义在于堵住"自验造假":worker 说测试全绿不算数,CI 在干净环境里跑过才算——这是第 17 章"验收权在主代理"的自动化延伸,也是集成负责人敢按序合并的证据链。
CI:PR #42(Worker B · fragments 路由)与主干在 server/src/app.ts 冲突。
主代理:git diff 定位——Worker A 十五分钟前合入了 notes 路由挂载,
Worker B 的分支基于旧主干,也改了 app.ts 的挂载区。
主代理:两处挂载语义独立、互不覆盖 → 手工合并保留两行;
本地 npm test(56/56 全绿)→ 合并 PR #42。
主代理:广播 Worker B:app.ts 是共享文件,路由挂载今后一律由主代理
执行(章程 2.3 条),回传改为"提交挂载申请"。
主代理:章程"事故记录"追加一条,版本号 +1。
看这段对话的治理闭环:冲突 → 处置 → 章程更新 → 广播。事故不白出,每次冲突都应该让章程变厚一点——这是 20.5 的"随事故进化"在集成场景的样子。
并行放大产能,也放大"代理自己拍板"的后果。五类节点必须人审:
| 节点 | 为什么必须人 | 典型动作 |
|---|---|---|
| 架构决策 | 方向性错误的代价 = 全部已写代码(第 15 章) | 批准实现计划与 spec |
| 新依赖引入 | 供应链风险 + 长期维护成本,装起来快卸起来疼 | 审依赖理由、维护状态、替代方案 |
| 合并主干 | 多分支改动在此汇流,是最后的拦截点 | 复核 diff + CI 绿 + 回传一致 |
| 发布 | 对外可见或不可逆 | 走发布检查单(附录 C) |
| 删除或外发操作 | rm -rf、git push --force、向外部服务发送数据 |
显式确认;权限上默认拒绝(第 11 章红线) |
人审节点不写下来,就依赖"主代理记得"——主代理也是模型,也会忘。把清单写进 AGENTS.md 的"人工确认点"小节,代理会在到达节点时主动停下请求确认,而不是自作主张闯过去:
## 人工确认点(到达以下节点必须停下,等待人工批准)
- 引入任何新依赖:给出包名、理由、替代方案,确认后再装;
- 修改 shared/types/** 与 docs/spec/** 下任何契约文件;
- 合并 PR 到 main:CI 全绿 + diff 复核后请求人工批准;
- 执行删除类命令(rm -rf、git push --force);
- 发起任何对外网络请求:仅允许白名单域(SSRF 防护——
防止代理被诱导向任意地址发数据或拉取恶意内容),
白名单维护在权限配置的 allow/deny 规则中。
多代理的 token 消耗是单代理的数倍,三个习惯把它管住:
| 措施 | 针对的浪费 | 做法 |
|---|---|---|
| 模型分级 | 机械活也用强模型 | 重活强档、批量活低档 |
| 缓存复用 | 重复前缀反复计费 | 稳定 AGENTS.md 开头,少改 |
| 合并派发 | 同类任务多次冷启动 | 同构切片合给一个 worker |
| 预算对账 | 失控后才发现 | 里程碑开工估、结束对 |
本章规范最终要落成一部《多代理协作章程》(team charter):所有权分区、共享文件规则、契约流程、集成节奏、检查点清单。它与 AGENTS.md 的分工要说清:AGENTS.md 管项目事实(技术栈、命令、约定),章程管协作流程(人与代理怎么分工、代理与代理怎么避让)。章程进团队 onboarding——新人(连同新人的代理)第一天就照着跑一遍;每次协同事故后追加一条规则并升版本号,让章程随项目进化。
把本章规范落成 docs/team-agents.md——BookNest 的《多代理协作章程》全文(约 60 行):
# BookNest 多代理协作章程 v1.1
> AGENTS.md 管"项目是什么",本章程管"编队怎么干活"。每次协同事故后升版本号。
## 1. 所有权分区(派发时写入任务卡,范围外一律只读)
| 分区 | 路径 |
|---|---|
| 后端-笔记域 | server/src/{routes,services}/note* + 对应 tests |
| 后端-速记域 | server/src/{routes,services}/fragment* + 对应 tests |
| 后端-统计域 | server/src/{routes,services}/stats* |
| 后端-备份域 | server/src/{routes,services}/backup* |
| 前端 | web/src/ 按功能域同规则划分,与后端域一一对应 |
| 端到端 | e2e/specs/<域>.spec.ts,一域一文件 |
## 2. 共享文件规则
- 共享清单:package.json、server/src/app.ts、shared/types/**、
AGENTS.md、迁移编号序列;
- worker 禁止直接修改共享文件:需要变更时在回传中提交"修改申请"
(改什么 / 为什么 / 影响面),由主代理统一执行后广播;
- 迁移编号按卡申请顺延(当前水位:0008);
- 同一文件两周内冲突 ≥ 2 次 → 触发结构消共享(如路由自动注册)。
## 3. 接口契约流程
- 契约文件:shared/types/**(API 形状)、server/migrations/**(表结构)、
docs/spec/**(行为约定);变更需主代理批准并广播全部在途 worker;
- mock 方向:前端 mock 后端(msw handler 以 shared/types 为准),禁止反向;
- 契约未落盘前,相关任务卡不得派发。
## 4. 集成节奏
- 每卡一分支一 PR,Conventional Commits(NFR-5);
- CI 必须绿(lint + tsc + vitest)才可合并,按依赖图拓扑序合并;
- 每 2–3 张卡为一个集成批次,批次间主干跑全量测试;
- 并行 WIP ≤ 2;集成期间冻结新派发。
## 5. 人工检查点(代理到达即停,等待人工批准)
- 架构决策、新依赖引入、契约变更、合并 main、发布;
- 删除类命令与出网:出网仅允许白名单域(SSRF 防护),
白名单维护在权限配置的 allow/deny 规则中;
- 密钥零字面量:一律 process.env 注入(NFR-2)。
## 6. 事故记录(一条事故换一条规则)
- v1.1 app.ts 双改冲突 → 新增 2.3:路由挂载由主代理统一执行。
再演示章程第 2 条的实战处置——两个 worker 同时想改 package.json:
Worker A(笔记模块)回传:
修改申请:新增依赖 marked@^15(渲染 Markdown 笔记正文),
影响面:web/src/components/NoteBody.tsx;等待批准。
Worker B(速记模块)回传:
修改申请:新增依赖 date-fns@^4(速记时间格式化),
影响面:web/src/components/FragmentItem.tsx;等待批准。
主代理:两申请均合理、功能无重叠 → 一次性合并引入:
npm i -w web marked@^15 date-fns@^4
提交:build(web): 引入 marked 与 date-fns(A/B 修改申请)
广播两 worker:依赖已就绪,rebase 后继续。
对照反面:没有章程时,两个 worker 各自 npm install——后提交者把前者的依赖行覆盖掉,CI 爆出"找不到模块 marked",排查半小时。而章程处置全程两分钟。治理的价值不在单次省时,而在把"必然发生的事故"变成"按流程走的日常"。
| 误区 | 典型现象 | 纠正方法 |
|---|---|---|
| 范围写成建议 | worker"顺手"改了共享文件,另一 worker 的改动无声丢失 | 范围即所有权:提示词声明 + hooks 拦截 + 回传核对三层兜底 |
| 契约停在聊天记录里 | 两个 worker 各凭记忆实现接口,字段名对不上 | 契约文件进仓库作单一事实源,变更走广播 |
| mock 方向搞反 | 后端迁就前端随手写的假数据形状,集成时两边返工 | 谁依赖契约谁 mock 对方:前端 mock 后端 |
| 全部做完再合并 | 五个分支第五天齐合,冲突叠加 bug 无法定位 | 小批次集成:每 2–3 卡合一次,主干保持可跑 |
| 检查点靠人脑记 | 忙起来忘了审依赖,一个月后才发现引了弃维护的包 | 清单写进 AGENTS.md,代理到点主动停 |
| token 不记账 | 月底用量三倍于预期,说不清花在哪个功能 | 里程碑预算 + 对账、模型分级、复盘单位成本 |
多代理的三大事故源——文件冲突、约定漂移、集成失败——没有一个是"模型不够聪明"造成的,全是"规范不够明确"造成的。因此治理的本质不是管住代理,而是把规范提前写清楚:所有权协议回答"谁的文件谁才能改";接口先行回答"大家对齐什么";集成节奏回答"多久对一次账";人工检查点回答"哪些关口必须人来守";成本治理回答"这套编队到底划不划算"。规范与并行度成正比:规范越明确,放权越大;规范每含糊一分,并行就得收一分。
要点回顾:
/notes,后端注册的是 /api/v1/note;(c) 两个分支各自 Vitest 全绿,合并后路由 404。shared/types/fragments.ts 契约(Fragment 类型 + 两个接口形状),并写出前端 worker 的 mock 要点:基于契约返回哪些假数据、必须覆盖哪些边界(空列表、未归入、已归入)。docs/team-agents.md,然后真实跑一次双 worker 并行(任选两张互不依赖的卡);发生的任何冲突都按章程处置,并把处置写进"事故记录"。收官时对照:章程比开工时厚了几条?前 20 章是弹药库:配置(Part 2)、Skill(Part 3)、边界(Part 4)、任务工程(Part 5)、多代理(Part 6)。本章把这些单件武器按一条真实时间线串成一场完整战役——从
git init到 FR-1~FR-7 全部验收。这是叙事章:不再展开方法论,只在每个阶段标注"用了哪一章、产出了什么、哪里翻了车"。强烈建议边读边在自己的仓库里复刻——本章的顺序,就是你的操作顺序。
读完本章,你应该能:
打地基的事不值得放权——这一阶段由人主导,代理最多当参谋。过一遍附录 C 的「项目启动检查单」,逐项留痕:
| # | 检查项 | 本项目的执行 | 出处 |
|---|---|---|---|
| 1 | 工具就绪 | ZCode CLI、Node 22 LTS、Git;zcode --version 可运行 |
第 02 章 |
| 2 | 模型档位 | 默认强档;批量机械任务用低档 subagent(第 20 章成本分级) | 第 02 章 |
| 3 | Git 仓库 | git init booknest + 首个空提交;建同名 GitHub 私有仓 |
第 12 章 |
| 4 | 边界档位 | 三档边界矩阵落盘 docs/boundary-matrix.md,红线进共享层 deny |
第 11 章 |
| 5 | 目录骨架 | 按附录 D 第 D.5 节建空目录 + README 占位 | 附录 D |
| 6 | 首提交 | chore: init repository——从第一行起就有可回滚历史 |
第 12 章 |
边界矩阵一句话回顾(详见第 11 章贯穿项目实战):开发期允许改代码、跑测试,禁止装依赖与出网;联调期放开本地端口与白名单域;发布期全部收紧。三条红线任何档位都不放开:git push --force、rm -rf、读取生产密钥。
当晚产出:一个空仓库 + 两份"宪法"雏形。半小时,全程人工——但这半小时是全书回报率最高的半小时,之后的每个代理会话都在吃它的红利。
原则:**代理干活之前,先让代理"开机即懂、越权即拦"。**M0 的产物清单如下(每项只记一句话、出处与验收,正文见各章):
| 产物 | 内容一句话 | 出处 | 验收方式 |
|---|---|---|---|
| AGENTS.md(根) | 简介 / 技术栈 / 命令 / 约定 / 目录 / 禁区 六类 | 第 04 章 | 新会话零提问答出"怎么跑测试" |
| AGENTS.md(web/) | 前端子目录特有约定,层级合并生效 | 第 04 章 | 在 web/ 下提问时两份都生效 |
| 规则分层 | 共享层 deny 红线 + 团队层约定 + 个人层偏好 | 第 05 章 | 三层各改各的,互不覆盖 |
| 权限 allowlist | 开发期档:放行 npm test / lint,拒绝 npm install | 第 07 章 | 越权命令被拒并有提示 |
| hooks 三件套 | PreToolUse 拦危险命令与敏感文件、凭据扫描、出网白名单(SSRF 防护:仅放行白名单域,防止代理被诱导向任意地址发数据) | 第 07、13 章 | 构造含假密钥的提交被拦下 |
| MCP 接入 | GitHub(查 issue / PR)+ SQLite(只读)两个 server,凭据一律环境变量注入 | 第 06 章 | 代理能列出 issue、能查库不能写 |
| 工程骨架 | npm workspaces(server / web)、ESLint 9 flat、Vitest 冒烟、两端 Hello | M0 本体 | 见下方"M0 验收三连" |
一晚收工。M0 验收三连:npm run dev 双端 Hello 可见、npm test 全绿、npm run lint 零告警。
翻车预告:当晚为了赶进度,hooks 三件套只配了凭据扫描,越界写入拦截"等项目像样了再补"。这笔账在 21.5 的 M2 如期爆雷——请先记住这里。
顺序不可换:先 spec 定"做什么",再拆卡定"这一刀切哪"(第 15 章的三层结构:spec → 任务卡 → todo)。
spec:M1 覆盖 FR-1 / FR-3 / FR-6,每个功能一页、六段结构,落盘 docs/spec/。事后看,最省 token 的一段是"明确不做的事"——FR-6 的 spec 写明"笔记内容检索留待 M2 随 FR-2 接入",后来真的拦住了一张卡的蔓延(执行代理一度想顺手把笔记搜索也做了)。
任务卡:垂直切片 + 七字段模板(第 14 章),M1 切成 9 张卡,落盘 docs/tasks/M1-tasks.md。M1 任务图(精简重绘;总表与三张完整卡见第 14 章贯穿项目实战):
graph TD
M1_01[M1-01 数据库基线] --> M1_02[M1-02 书籍 CRUD API]
M1_01 --> M1_03[M1-03 标签 API]
M1_04[M1-04 前端骨架] --> M1_05[M1-05 书架页]
M1_02 --> M1_05
M1_03 --> M1_06[M1-06 评分与标签前端]
M1_05 --> M1_06
M1_02 --> M1_07[M1-07 搜索后端]
M1_07 --> M1_08[M1-08 搜索页]
M1_05 --> M1_08
M1_06 --> M1_09[M1-09 收口]
M1_08 --> M1_09
读图决策:并行宽度为 2,但 M1 只开单代理——第一个里程碑先练"单卡节奏",把验收命令、提交纪律、偏差处理跑成肌肉记忆;多代理留给 M2 见真章。这符合第 17 章的反指标:"拆分边界还没想清楚就上多代理,等于把混乱并行化。"
每个执行会话的第一句话固定不变:
你:先别写代码。读 AGENTS.md、docs/spec/books.md 和 server/src 现有代码,
给出 M1-02 的实现计划:改动清单、假设、测试与验收、执行顺序。
用 Shift+Tab 切入计划模式(plan mode),代理只读探索、产出计划、等批准(第 15 章)。M1 期间人工批注抓到两次要害,都在"假设"一节:一次是想引入未安装的 express-validator(项目已有手写校验中间件),一次是把 created_at 当成"录入即读完"(统计口径会全错)。3 分钟批注换半天返工,这笔账在本项目结了不止一次。
四个动作固定成循环,第 16 章的看板同步推进(todo → doing → done)。下面是 M1-02「书籍 CRUD API」的完整对话骨架——前置的 M1-01 数据库基线已先行完成并合并:
你:打开 docs/tasks/M1-tasks.md,实现 M1-02「书籍 CRUD API」。
严格按卡内验收标准交付,先计划后动手。
代理:(计划模式)读任务卡、AGENTS.md、server/src/db/ 现有代码 →
产出计划:新增 routes/books.ts、services/bookService.ts、
tests/books.api.test.ts,挂载进 app.ts;假设:错误格式沿用
middleware/errors.ts 的 { error: { code, message } };
校验复用现有 validate.ts,不新增依赖。
你:批准。按计划执行。
代理:(切回执行模式)books.ts → bookService.ts → 12 个测试用例 →
npx vitest run server/tests:12/12 通过 →
npx tsc --noEmit 无错误 → npm run lint 通过
代理:M1-02 完成,验收标准逐条核对:
1. books.api.test.ts 12 用例全绿(含校验失败 400、404);
2. curl localhost:3000/api/v1/books → 200,返回分页 JSON;
3. lint 与 tsc 通过。偏差:无。请人工复核。
你:(抽查 diff,手动 curl 一次创建与一次 400)通过。
提交,然后做 M1-03。
代理:git add -A && git commit -m
"feat(server): add books CRUD API with input validation (M1-02)"
你:看板更新:M1 进行到 3/9。
注意三处纪律:验收是代理逐条自报、人抽查——不是反过来;"偏差"一节哪怕"无"也要显式写,逼代理自查;提交信息遵循 Conventional Commits(NFR-5)且卡号入尾,回滚与考古都靠它。
$ git log --oneline
a3f21c0 feat(web): book list page with create/edit dialogs (M1-05)
7b9e2d4 feat(server): tags API with book_tags join (M1-03)
c1d88f2 feat(server): add books CRUD API with input validation (M1-02)
e5013aa feat(db): three-table baseline and idempotent migrator (M1-01)
一卡一提交(大卡拆成数个提交),回滚粒度 = 卡粒度——第 12 章"高频小提交"在实战里的样子。
执行中还有一条第 14 章的铁律在本阶段兑现了两次:卡是活的。M1-05 执行时代理问"编辑弹窗要不要支持封面 URL 预览"(卡里没写清),处理方式是先补卡再继续——答案写回 docs/tasks/M1-tasks.md 后,后续所有会话都不必再问这个问题。凡代理问出"卡里本该写清"的问题,一律先改卡再动代码。
M1 总耗时 3 晚,9 张卡全部一次通过验收,零返工。不是运气:spec 拦住了方向错误,任务卡拦住了范围蔓延,可执行验收拦住了"自说自话的完成"。
M2 范围:FR-2 读书笔记 + FR-7 快速记录 + 标签完善。两个功能域的文件互不重叠——主从编排(第 17、18 章)的教科书场景,治理规则照第 20 章的章程执行。
派发之前先落契约:
0007_notes.sql、0008_fragments.sql(表结构即契约,编号按章程向主代理申请顺延);shared/types/notes.ts、shared/types/fragments.ts(API 形状,变更需主代理批准并广播);Worker A 提示词(第 17 章四要素模板,节选):
【Worker A · FR-2 读书笔记】
目标:按 docs/spec/fr2-notes.md 实现笔记模块(迁移 0007 + API + 页面 + 测试)。
范围:仅允许改动 server/src/{routes,services}/note* 与对应 tests、
web/src/ 下 notes 相关新文件;共享文件(app.ts、package.json、
shared/types/**)只读,需要变更走回传"修改申请"。
约定:遵循 AGENTS.md 与 docs/team-agents.md;Markdown 渲染必须消毒防 XSS
(NFR-2);时间线按 updatedAt 倒序;密钥零字面量。
完成定义:npm test 全绿;spec 验收标准全过;lint 与 tsc 通过。
回传格式:改动文件清单 / 测试结果(数字)/ 偏差与原因 / 遗留问题。
Worker B 同构(fragments 域)。两 worker 并行,看板 WIP = 2,贴着章程上限。
第 7 晚,Worker A 的 PR 合并后,Worker B 的 PR 报冲突:
CI:PR #17(Worker B · fragments)与主干在 server/src/app.ts 冲突。
主代理:git diff 定位——Worker A 已合入 notes 路由挂载,
Worker B 在自己的分支上也改了 app.ts 挂载 fragments 路由。
按章程第 2 条,app.ts 是共享文件,Worker B 越界了。
主代理:处置——两处挂载语义独立 → 手工合并保留两行;
本地 npm test(56/56 全绿)→ 合并 PR #17。
主代理:章程"事故记录"追加:「app.ts 双改冲突 → 新增 2.3:
路由挂载一律由主代理执行」;版本号 v1.0 → v1.1。
主代理:广播 Worker B:回传改为"提交挂载申请",本批次豁免一次。
处置本身只用了 5 分钟,但有两点比处置更值得看:
每卡一 PR、CI 绿才合、按依赖图拓扑序合并;两域全部合入后,主代理跑全量测试,并手动过一遍"速记 → 归入某本书 → 笔记时间线可见"的端到端路径——两域的交界处正是集成风险点(第 14 章读图口诀:汇点卡验收最严)。M2 收口,FR-2 / FR-7 可演示。
M3(FR-4 统计 + FR-5 导入导出)的实现第 17 章已完整推演过(主从编排 + 评审者收尾),此处只记录口动作:
e2e/specs/ 一域一文件(所有权协议延伸到测试目录);npm run build && npm start 部署演练、封面图外链仅请求白名单域(SSRF 防护:运行期出网限制在 allow 清单内)、依赖清单复核无弃维护包;booknest-module(SKILL.md:从迁移、服务、路由到页面、测试的固定五步),下一个新模块直接触发复用——这就是第 10 章"调试 → 提炼 → 进化"的晋升闭环。| 阶段 | 耗时 | 关键产出 | 翻车点 |
|---|---|---|---|
| 零:开工准备 | 半晚 | 空仓库、边界矩阵、目录骨架 | 无(全程人工) |
| 一:配置先行 M0 | 1 晚 | AGENTS.md ×2、规则/权限/hooks、MCP、工程骨架 | 跳过了越界拦截 hook(M2 还债) |
| 二:规格与拆分 M1 | 1 晚 | 3 份 spec、9 张任务卡 | FR-5 的 spec 写成了实现方案,删改一页 |
| 三:单代理 M1 | 3 晚 | 9 卡合并、Vitest 38 例全绿 | 无大翻车;两次假设被批注拦下 |
| 四:多代理 M2 | 3 晚 | FR-2/FR-7 上线、章程升 v1.1 | app.ts 双改冲突(hooks 缺位放大) |
| 五:收口 M3/M4 | 2 晚 | FR-4/FR-5、e2e 七条、发布检查单 | 两处 spec 过期(workflow 审计抓出) |
| 复盘 | 1 晚 | 本章、经验晋升 | —— |
合计约 11 个晚上(业余时间)。纯编码时间不足一半——另一半花在"看起来不产代码"的配置、规格与治理上,而这正是它们被低估的原因。
本章本身就是贯穿项目的总集成。最终仓库树(对照附录 D 第 D.5 节的目标形态):
booknest/
├── AGENTS.md # 第 4 章产物 + 21.6 晋升的三条规则
├── .zcode/ # 规则分层、权限、hooks(第 5、7、13 章)
├── skills/booknest-module/ # SKILL.md:新模块五步脚手架(第 10 章)
├── docs/
│ ├── boundary-matrix.md # 三档边界矩阵(第 11 章)
│ ├── team-agents.md # 多代理协作章程 v1.1(第 20 章)
│ ├── spec/ # fr1..fr7 七份功能规格(第 15 章)
│ └── tasks/ # M1/M2/M3 任务图与任务卡(第 14 章)
├── server/
│ ├── src/{routes,services,db,middleware}
│ ├── migrations/ # 0001..0009,九次迁移
│ └── tests/ # Vitest 接口/单元 41 例
├── web/
│ ├── src/{pages,components,api,hooks,styles}
│ └── tests/ # 组件测试 23 例
├── e2e/specs/ # Playwright 七条主路径(一域一文件)
└── package.json # npm workspaces: server / web
功能验收对照表(对照附录 D 第 D.2 节,FR-1~FR-7):
| 编号 | 功能 | 状态 | 验证方式 |
|---|---|---|---|
| FR-1 | 书籍管理 | ✅ | e2e books.spec.ts:录入 → 列表可见 → 编辑 → 删除;接口测试 12 例 |
| FR-2 | 读书笔记 | ✅ | e2e notes.spec.ts:建笔记 → 时间线倒序可见;Markdown 消毒有单测 |
| FR-3 | 评分与标签 | ✅ | e2e tags.spec.ts:打 4 星 → 加标签"科幻" → 按标签筛选可见 |
| FR-4 | 阅读统计 | ✅ | e2e stats.spec.ts:热力图 12 格与 monthly 一致;spec AC-1~4 全过 |
| FR-5 | 数据导入导出 | ✅ | Vitest:JSON 导出导入 round-trip、CSV 导入事务失败回滚用例 |
| FR-6 | 全文搜索 | ✅ | e2e search.spec.ts:按书名 / 作者 / 笔记内容命中;1 万条 < 300ms |
| FR-7 | 快速记录 | ✅ | e2e fragments.spec.ts:速记 → 归入某书 → 笔记时间线出现 |
非功能侧顺带全绿:核心链路覆盖率 84%(NFR-1 ≥ 80%)、tsc --noEmit 严格模式零错误(NFR-5)、全程 hooks 扫描下仓库历史零凭据字面量(NFR-2)。
从 git init 到上表全勾,11 个晚上。第 22 章你会拿到同一个项目的"故意劣化版" booknest-legacy(无类型、无测试、单文件巨石)——到时你会更清楚地看到:一个仓库的好坏,不在代码写得多漂亮,而在每一章的纪律有没有留下痕迹。
| 误区 | 典型现象 | 纠正方法 |
|---|---|---|
| 把实战章当小说读 | 读得津津有味,自己的仓库还是空的 | 边读边复刻:本章小节顺序即操作顺序 |
| 配置先行被跳过 | 每个会话重复解释技术栈,代理写出自创风格 | 先过 M0 产物清单并逐项验收,再写第一行业务代码 |
| 多代理一步到位 | 第一次并行就开五个 worker,冲突淹没收益 | 从 2 个 worker 起步,章程跑顺再加并行度 |
| 收口不跑全量就发布 | 单功能自测全过,合并后主路径 404 | 发布检查单必含全量测试 + e2e 冒烟 |
| 经验不晋升 | 下个项目把 app.ts 冲突原样再踩一遍 | 复盘后 48 小时内更新 AGENTS.md 与 Skill 库 |
| 复盘只记成绩 | 时间线回顾表只剩"顺利完成" | 翻车点是复盘的核心资产:一条翻车换一条规则 |
本章把前 20 章的方法按真实时间线走了一遍:阶段零人工打地基(检查单、边界矩阵、目录骨架);M0 配置先行,让代理开机即懂、越权即拦;M1 先 spec 后拆卡,再以"领卡、实现、自验、提交"的四拍单代理逐卡消化;M2 用主从编排双 worker 并行,接口先行解耦进度,一次真实冲突检验了章程并推动它升级;M3/M4 用 workflow 审计、e2e 与发布检查单收口,最后把经验晋升回 AGENTS.md 与 Skill 库。11 个晚上、7 个功能、一次被章程化解的冲突、一次因偷懒放大的事故——结论只有一句:vibe coding 的速度不是来自少做规矩,而是来自把规矩做在前面。
要点回顾:
第 21 章结尾留了个伏笔:如果第 21 章的全部纪律都缺席,BookNest 三年后会长成什么样?答案是本章的主角
booknest-legacy。接手遗留项目(legacy project)是比 greenfield 更接近日常工作的场景——你无法选择从零开始,只能在别人的地基上施工。本章给出一套四阶段打法:只读勘探 → 安全网先行 → 渐进重构 → 验证与切换。仍用叙事章写法:每个阶段标注用了哪一章、产出了什么、哪里最容易翻车。
读完本章,你应该能:
booknest-legacy设定如下:某个团队三年前也想做读书笔记工具,但没有经历第 21 章的任何纪律——没有 AGENTS.md、没有 spec、没有任务卡、没有测试。一个后端在三天里"vibe"出了全部功能,之后三年边修边补,所有代码堆在一个文件里。今天这个活儿交到你手上,附赠一句管理层名言:"功能都是好的,就是改不动。"
它的典型症状,做过遗留维护的人都会会心一笑:
app.js 一个文件 3000 行,路由、SQL、业务逻辑、工具函数全在里面;if (retry < 43)——43 是什么?没人知道,原作者也忘了;getBookList、getBookList2、getBookList3 三胞胎,各改了一半;/home/legacy/backup,换台机器就静默失败;巨石节选(感受一下你要面对的东西):
// app.js(节选。全书 3000 行,都在这一个文件里)
const express = require('express');
const db = require('better-sqlite3')('/home/legacy/booknest.db'); // 硬编码路径
const app = express();
const RETRY_MAX = 43; // 无人知晓其含义
app.get('/api/books', (req, res) => {
const kw = req.query.q || '';
// 手写 SQL 拼接——雷区:注入风险(附录 D NFR-2 的反面教材)
const rows = db.prepare(`SELECT * FROM books WHERE title LIKE '%${kw}%'`).all();
res.json(rows);
});
// 复制粘贴三胞胎之一,另两个在 1200 行与 2100 行附近
function getBookList(limit) { /* 60 行 */ }
app.get('/api/cover', async (req, res) => {
// 雷区:按用户输入任意出网——SSRF(Server-Side Request Forgery,服务端请求伪造)
const r = await fetch(req.query.url);
res.send(Buffer.from(await r.arrayBuffer()));
});
app.listen(3000);
三条心法,比任何工具都重要:
一句行业名言送给你,也送给你的代理:"遗留代码就是没有测试的代码。"(Michael Feathers,《Working Effectively with Legacy Code》)
ZCode 提供三种"只看不动手"的方式,按需组合:
Shift+Tab 切入,代理只读探索、产出计划、等批准。适合小范围勘探(单个模块);booknest-legacy 选了第三种:三个只读子代理并行,分别负责路由面、数据面、依赖面,提示词四要素齐全(第 18 章),且都显式写入一条铁律:
【约束】本次为只读勘探:禁止创建、修改、删除任何文件;
禁止执行任何写入或出网命令;所有发现以报告形式回传。
| 维度 | 要回答的问题 | 本项目产出 |
|---|---|---|
| 入口点 | 程序从哪启动?有哪些进程/定时任务? | 启动链路:仅 node app.js |
| 路由清单 | 对外暴露了哪些接口?各自干什么? | 路由表 20 条(books/notes/tags/stats/backup/import) |
| 数据访问 | 表结构?SQL 在哪?有无拼接与注入风险? | 23 处手写 SQL,5 处拼接用户输入 |
| 外部依赖 | 出网到哪?装了哪些包?有无弃维护包? | 1 处任意出网(cover 拉图);34 个依赖,2 个弃维护 |
| 雷区 | 哪些地方改了会炸?密钥/路径/危险写法在哪? | 风险清单 5 条,按危害排序(见贯穿实战原件一) |
docs/ARCHITECTURE.md + docs/LEGACY-RISKS.md。勘探成果属于仓库而不属于某个会话——三个月后下一个接手的人,以及之后每一个代理会话,都靠这两份文件"开机即懂"(第 4 章思想在遗留场景的延伸)。报告节选见本章贯穿项目实战。
特征测试(characterization test)是重构安全网的核心:给代码"现在"的行为拍快照,哪怕这个行为是错的。它和普通测试的分工:
| 普通测试 | 特征测试 | |
|---|---|---|
| 断言什么 | 应该怎样(spec) | 现在怎样(现状) |
| 失败意味着什么 | 实现有 bug | 有人改变了行为 |
| 放在哪 | 迁移后的新模块 | 巨石的每个对外接口 |
写法上有个反直觉的要点:先跑一遍、打印真实输出、再把真实输出写成断言——而不是先写期望值。错的现状(比如搜索命中通配符不报错、返回空数组)也照样锁住,等重构完成、行为稳定后,再用带验收的独立任务去"修"它。
锁定顺序按风险排序:先锁"钱与数据"(导入导出、备份、统计、删除),再锁普通查询路由——前者坏了是事故,后者坏了是不便。
遗留项目一步到位上严格模式必死——3000 行代码会吐出上千个类型错误,团队(和你)会被劝退。正确姿势是"只报不修"的最小工程化:
// tsconfig.json(渐进引入第一步)
{
"compilerOptions": {
"allowJs": true, // 允许 JS:新旧共存
"checkJs": false, // 暂不检查 JS:只给新 TS 文件上强度
"noEmit": true, // 只做类型体检,不参与构建
"strict": false, // 宽松起步,随迁移逐步收紧
"target": "ES2022"
},
"include": ["server/src/**/*.ts"]
}
# ESLint 同理:只报告、不修复、不启用会自动改写代码的规则
npx eslint server/ --ext .js,.ts # 先看见,再治理
"只报不修"的原因:自动修复(--fix、自动格式化)本身就是一次全库改写,会让此后的每个 diff 都淹没在格式噪音里,评审者-实现者的行为对照将无从下手。
安全网与工程化就位后,立刻打基线:
git add -A && git commit -m "chore: baseline before refactor (characterization tests + tooling only)"
git tag legacy-baseline # 此后任何一步都能一键回到起点
从此,"行为变了没有"不再是感觉问题,而是测试问题。
绞杀者模式(strangler fig pattern,Martin Fowler 提出并命名):新代码像绞杀榕一样围绕旧系统生长,逐个接管职责,等旧系统只剩躯干,再整体移除。落到本项目:
第 0 周 app.js(3000 行,全部逻辑)
第 1 周 app.js(2600 行)+ server/src/routes/books.js、tags.js
第 2 周 app.js(1600 行)+ routes/ 全量 + services/ + db/(SQL 参数化)
第 3 周 app.js(约 600 行,仅启动与挂载)
三个要点:
app.js 通过 require 挂载新模块——不存在"第二套系统",也就不存在数据分叉与双份维护;出网类的等价变换要格外当心:巨石里 fetch(req.query.url) 式的任意出网不能原样搬进新模块,迁移时必须补上域白名单校验——凡用户输入的 URL,先校验协议与主机名在白名单内才发起请求(SSRF 防护,详见第 13 章出网防护):
// server/src/middleware/safeUrl.js —— 随域迁移一并落地的出网防护
// SSRF 防护:用户输入的 URL 一律先过域名白名单,通过才放行请求
const ALLOWED_HOSTS = new Set(['covers.openlibrary.org']);
function assertSafeCoverUrl(raw) {
const u = new URL(raw);
if (u.protocol !== 'https:' || !ALLOWED_HOSTS.has(u.hostname)) {
throw Object.assign(new Error('cover url not allowed'), { status: 400 });
}
return u.toString();
}
module.exports = { assertSafeCoverUrl };
一个域的迁移固定三拍:迁出 → 全量特征测试 → 提交。任何一拍不过,git checkout -- . 回到上个提交重来。提交信息沿用 Conventional Commits,迁移任务编号(RL-xx)入尾:
refactor(server): extract books routes from monolith (RL-03)
refactor(server): parameterize legacy SQL in migrated routes (RL-07)
重构期引入第 17 章的评审者-实现者模式,但给它一个遗留场景特有的收窄:评审者不评风格,只做两件事——
风格意见在重构评审里是噪音——行为不变才是唯一标准。完整实录见贯穿项目实战。
tsc --noEmit 全绿。这是切换的唯一准入;STARTER=legacy|modular),切到新入口后观察 48 小时:错误日志、5xx 率、慢查询。观察期旧入口保持可用;// scripts/shadow-compare.mjs(节选)
// 目标地址为固定本机回环、非用户输入;凡涉用户输入的出网仍一律过域白名单
const CASES = ['/api/books', '/api/books?q=%25', '/api/books/42', '/api/stats/yearly'];
for (const path of CASES) {
const legacy = await (await fetch(`http://127.0.0.1:3001${path}`)).json();
const modern = await (await fetch(`http://127.0.0.1:3000${path}`)).json();
// 深比较两个字段树,任何不一致都写入差异报告并标记为阻断项
}
切换完成后做三件收尾事,呼应第 21 章的"经验晋升":
docs/ARCHITECTURE.md 与 docs/LEGACY-RISKS.md 更新为迁移后状态,已消除的雷区逐条销项归档;app.js 添加任何路由";server.js,保留 legacy-baseline tag 归档历史。四条纪律,每条背后都是真实代价:
refactor:),行为变更另开任务(fix: / feat:)。混在一个提交里的搬移与修改,会让 diff 对照失效、回滚粒度报废、特征测试失去意义;本章整体即 booknest-legacy 重构实录——正文四个阶段都是在它身上发生的。本节补齐三份最有保存价值的原件与一本总账。
由三个只读子代理并行产出、人工合并而成(docs/ARCHITECTURE.md 节选):
booknest-legacy 勘探报告(节选)|勘探人:3 个只读子代理 + 人工复核
====================================================================
1. 入口点:app.js(Express,监听 3000);无优雅退出;无进程守护。
2. 路由清单:共 20 条 —— books(6) notes(4) tags(3) stats(3)
backup(2) import(2)。挂载全部内联在 app.js。
3. 数据访问:better-sqlite3 直连,23 处手写 SQL,其中 5 处拼接
用户输入(books 搜索 / tags 过滤 / import 批量插入)。
4. 外部依赖:仅 1 处出网——GET /api/cover 按 url 参数任意拉图
(SSRF 风险);npm 依赖 34 个,2 个已弃维护。
5. 雷区清单(按危害排序):
R1 SQL 拼接 ×5 → 随迁移参数化(本次)
R2 任意 URL 出网 ×1 → 迁移时补域白名单(本次)
R3 硬编码备份路径 ×1 → 改环境变量(本次)
R4 app.js:1702 疑似第三方 API 密钥字面量
→ 先轮换再清理(22.6 纪律 3)
R5 无测试、无类型、历史有 1 次强推
→ 先立安全网与基线(阶段二)
// legacy/tests/books.characterization.test.js(节选)
// 规则:断言"现状"而非"理想";重构期间特征测试只增不改。
import { describe, it, expect } from 'vitest';
import request from 'supertest';
import app from '../app.js';
describe('GET /api/books —— 现状快照', () => {
it('无关键字返回全部书籍(现状:无分页,一次性全量)', async () => {
const res = await request(app).get('/api/books');
expect(res.status).toBe(200);
expect(res.body).toHaveLength(3); // 真实输出,不是设计值
});
it('关键字含通配符不 500(现状:被当普通字符,返回空数组)', async () => {
const res = await request(app).get('/api/books?q=%25');
expect(res.status).toBe(200);
expect(res.body).toEqual([]);
});
it('返回字段集合保持原状(字段缺失也算特征)', async () => {
const res = await request(app).get('/api/books');
expect(Object.keys(res.body[0]).sort()).toEqual([
'author', 'cover_url', 'created_at', 'id', 'rating', 'status', 'title',
]);
});
});
任务卡(沿用第 14 章七字段模板):
【RL-03 · 把 /api/books 路由迁出巨石】
背景:app.js 中 books 相关代码约 210 行(勘探报告 §2)。
目标:新建 server/src/routes/books.js 承接全部 books 路由;
app.js 删除对应段落并挂载新模块;对外行为零变化。
范围:仅允许改 app.js(删被迁出行 + 一行挂载)、新增 routes/books.js、
legacy/tests/(只增不改)。
约定:SQL 随迁参数化(勘探 R1);封面出网补白名单校验(勘探 R2);
风格向新模块对齐,但不做范围外清理。
禁止:顺手修 bug、改返回结构、触碰其他路由。
验收:books 特征测试全绿;全量特征测试 31 条全绿;
tsc --noEmit 零错误;影子对比 20 请求集零差异。
实现与评审对话(评审者只跑测试、只对照 diff):
实现者:RL-03 完成。routes/books.js 新增 216 行(含参数化 SQL 与
封面 URL 白名单校验);app.js 净变化 -212/+3。books 特征测试
6/6 绿,全量 31/31 绿,tsc --noEmit 零错误。偏差:无。
评审者:① 全量测试 31/31 绿;
② diff 对照:改动仅 app.js 与 routes/books.js 两个文件;
books.js 与被删段落逐行同构(差异仅模块化包裹与参数化)。
评审者:结论:行为零变化,通过。附注:app.js 瘦身至 2790 行。
你:(抽查 diff 与影子对比报告)批准。
代理:git commit -m
"refactor(server): extract books routes from monolith (RL-03)"
| 周次 | 动作 | app.js 行数 | 测试规模 | 关键提交 |
|---|---|---|---|---|
| W0 | 勘探 + 安全网 + 基线 | 3000 | 特征测试 31 条 | legacy-baseline |
| W1 | 迁出 books / tags 域 | 2600 | +12 迁移用例 | RL-01~04 |
| W2 | 迁出 notes / stats 域,SQL 全量参数化 | 1600 | +9 迁移用例 | RL-05~09 |
| W3 | 迁出 backup / import,切换与收尾 | 600(仅挂载) | 全量 52 条全绿 | 切换 + AGENTS.md 重写 |
三周业余时间,期间零事故、零回滚。不是运气:每一步都有测试网接着、每个提交都能一键退回、每次行为变化都会被评审者当场拦下。
| 误区 | 典型现象 | 纠正方法 |
|---|---|---|
| 边勘探边"顺手优化" | 勘探会话里改了三处"明显的问题",改完没人知道影响了什么 | 勘探只读:发现即记录进风险清单,绝不当场动手 |
| 一次性大爆炸重写 | "反正要重构,不如让代理重新写一个" | 重写丢失隐性需求;除非体量很小,一律绞杀渐进 |
| 特征测试写成理想测试 | 断言"应该怎样",一跑全红,安全网沦为噪音 | 特征测试拍现状快照,错的行为也先锁住,修 bug 另开任务 |
| 重构与修 bug 混在一个提交 | diff 里既有搬移又有逻辑修改,对照无从下手 | 提交类型分离:refactor 只搬移,fix/feat 走独立任务 |
| 发现密钥直接删掉 | 字面量删了,密钥早已泄露且躺在 git 历史里 | 先轮换、再清理、必要时清历史(22.6 纪律 3) |
| 影子对比只看状态码 | 200 对 200 就切换,字段级差异漏网到生产 | JSON 深比较并覆盖边界输入,零差异才允许切换 |
ARCHITECTURE.md 与风险清单是第一周的唯一交付物;tsc --noEmit、不带 --fix 的 ESLint,任何自动改写都延后到迁移完成;legacy-baseline 之后,任何一步都能一键回到起点;本章走完了接手遗留项目的完整四阶段:只读勘探先画地图(架构文档 + 风险清单,勘探期禁改);安全网先行用特征测试锁住存量行为,以"只报不修"的最小工程化建立基线提交;渐进重构按绞杀者模式逐域生长新模块、瘦身巨石,用评审者-实现者双代理守住"零行为变化";最后以全量回归、影子对比、一行回滚开关完成验证与切换,并把勘探成果晋升为新的项目约定。加上四条专属纪律——禁大爆炸、禁顺手改行为、密钥先轮换、隔离环境——你会发现:重构遗留项目和从零构建共享同一套底层纪律,只是把"防犯错"换成了"防惊吓"。
要点回顾:
ARCHITECTURE.md 节选与雷区清单,全程禁止写入代码。验收:报告能回答"怎么跑起来、有哪些对外接口、哪里最危险"三问。booknest-legacy 上复刻本章全程),产出四件套:迁移前后对照表(行数、测试数、依赖数)、一份影子对比差异报告(应为零差异)、更新后的 AGENTS.md diff,以及 200 字复盘——你的"勘探、安全网、小提交"三项纪律各自拦下了什么。前 22 章告诉你"该怎么做",本章告诉你"不这样做会发生什么"。30 条反模式(antipattern)按六类组织——上下文、配置、边界、任务、多代理、流程——全部来自真实 AI 协作场景的高频失败模式,每条标注详细出处,需要展开时跳回对应章节。读法建议:不要一次通读。把它当"事后字典":每次翻车后用 23.1 的症状速查表反查条目,比事前背诵有效十倍——坑这种东西,没踩过的时候读多少遍都是纸上谈兵,踩过之后再来看,每一条都会让你后背一凉。
读完本章,你应该能:
先看现象,再查病根。左列是你在 AI 协作中大概率亲眼见过的场景——如果你一条都没见过,恭喜,也请留着这张表:AI 协作的坑不以人的意志为转移,只以会话数为单位发生。
| 你看到的现象 | 大概率是 | 条目 |
|---|---|---|
| "帮我做个搜索",产出能用但完全不是你要的 | 需求信息量为零 | A1 |
| 会话越聊越"失忆",早先的约定被悄悄违背 | 上下文超限还在硬撑 | A3 |
| 每个新会话都要重新解释一遍项目背景 | 约定没落进项目配置 | A5 / B1 |
| 代理改了没让它改的文件,甚至删了东西 | 无 deny 底线 + prompt 当边界 | C2 / C4 |
| 代码里出现密钥字面量或内网地址 | 凭据管理失守 | B4 |
| 大功能一次交付,一半是想象出来的 | 任务没拆就硬吞 | D1 |
| 两个并行代理互相覆盖,合并冲突一片 | 所有权没分区 | E3 |
| 子代理自称"完成",一验收全是坑 | 派发不自包含 / 结果没验收 | E2 / E4 |
| 合并后测试变红、页面 404 | 没读 diff / 测试后置 | F1 / F2 |
| 长任务跑一小时不停,token 账单起飞 | 无预算上限与停机条件 | E5 |
使用本表的四个提醒:
十条现象背后是六类病根,先给一张总览地图,按需跳读:
| 类别 | 条数 | 共同根因 | 第一落点 | 主战场 |
|---|---|---|---|---|
| A 上下文 | 6 | 输入不足或失真 | 提示词三要素 + 文件引用 | 第 03、04、16 章 |
| B 配置 | 5 | 快照失真、分层不清 | AGENTS.md / 规则分层 / 审计 | 第 04–07、19 章 |
| C 边界 | 5 | 约束悬空在 prompt 层 | deny 规则 / hooks / 沙箱 | 第 07、11、13、20 章 |
| D 任务 | 5 | 切分不当、验收缺位 | 七字段任务卡 + 垂直切片 + 小提交 | 第 12、14 章 |
| E 多代理 | 5 | 治理缺位 | 自包含派发 + 所有权分区 + 预算闸门 | 第 17–20 章 |
| F 流程 | 4 | 验证外包 | 人审 diff + 测试硬门槛 + 发布检查单 | 第 01、15、21 章 |
"主战场"列不是让你现在去重读——它是反查后的入口:确认条目之后,跳到对应章节把该章"最佳实践"重读一遍,比整章重读省一半时间。
这两类放在一起讲,因为它们是同一场事故的两个阶段:上下文(A 类)决定这一次会话的质量,配置(B 类)决定每一次会话的起点——A 类坑每次踩都是新的,B 类坑踩一次会反复收租。
顺带说清"上下文"与"配置"的分工:上下文是你这一次给代理的东西(提示、报错、文件),配置是仓库里预先替你给代理的东西(AGENTS.md、规则、hooks)。前者的坑靠临场纪律修,后者的坑靠落盘与审计修——两边的解法不能互相替代。
上下文工程(第 3 章)的全部问题,归根结底一句话:代理只知道你给它的。它不是读心者,也不是你团队的毕业生——你喂什么,它就是什么水平。
| 条目 | 症状 | 根因 | 解法 | 详见 |
|---|---|---|---|---|
| A1 一句话需求 | "帮我加个搜索"直接开跑,产出与预期南辕北辙 | 需求信息量撑不起实现,代理只能按"平均项目"补全 | 提示三要素:背景 + 目标 + 验收标准;一句话需求至少补"给谁用、什么场景、什么算完成" | 第 03 章 |
| A2 上下文污染 | 贴了 800 行无关组件"供参考",此后产出不断混入无关模式 | 窗口里的一切都会被当成相关信号 | 引用文件路径代替粘贴全文;必须贴时圈定行号并声明"仅参考 X" | 第 03 章 |
| A3 超长会话硬撑 | 一个会话从早聊到晚,早期约定失效、重复返工 | 上下文逼近上限,早期内容被压缩或挤出 | 按里程碑切会话;关键约定落盘文件(AGENTS.md/任务卡),靠文件不靠聊天记录 | 第 16 章 |
| A4 报错不带复现信息 | "报错了,修一下"加一张截图 | 代理只能猜环境、版本与触发路径,修复变成概率游戏 | 报错三件套:完整错误文本 + 复现命令 + 相关文件路径 | 第 03 章 |
| A5 让代理猜项目约定 | 产出风格漂移:命名、目录、错误处理各来一套 | 约定只在你脑子里,没进仓库 | 约定落盘 AGENTS.md 与子目录级配置,新会话开机即懂 | 第 04 章 |
| A6 重复粘贴背景不给引用 | 每轮手工复述"我们用的是 Express 5……" | 把代理当聊天机器人,忘了它能读仓库 | 用文件引用(路径 + 行号)让代理自己读,一劳永逸 | 第 03 章 |
典型翻车链:一句话需求开跑(A1)→ 产出不对 → 贴更多背景解释(A6/A2)→ 会话更长更失忆(A3)→ 周而复始。单条的坑,串起来就是一个低效会话的完整生命周期。
本类最危险的是 A3 超长会话硬撑:它悄悄偷走的不只是这一次会话——早期约定在窗口深处失真后,你做的每个决定都建立在代理的"记忆重构"上,而你毫无察觉。
心法:代理犯傻时,先检查自己喂了什么——A 类问题的根因几乎都在输入侧。
配置(第 4–7 章)是给代理的"法律";法律本身失真,执行必然失真。配置类问题最隐蔽的地方在于:它坑的不是当次会话,而是之后每一个会话。
| 条目 | 症状 | 根因 | 解法 | 详见 |
|---|---|---|---|---|
| B1 AGENTS.md 写成万行书 | 历史决策、API 文档全塞进上下文文件,关键规则被淹没 | 混淆了"上下文文件"与"文档库" | 只放高频必知(命令/约定/禁区),其余进 docs/ 并留引用 | 第 04 章 |
| B2 个人偏好混进团队文件 | 个人层的模型档位、缩进偏好写进共享配置,队友被迫跟随 | 共享层与个人层边界不清 | 共享层放团队约定;个人偏好放本地不入库的个人层 | 第 05 章 |
| B3 MCP server 全开常驻 | 十几个 server 常驻:上下文被工具定义占满、启动变慢、攻击面变大 | 贪图"一次配齐" | 按项目与任务按需启用,工具集最小化 | 第 06 章 |
| B4 凭据写进配置或示例 | token 直接躺在配置或示例代码里,被扫描 hook 或同事逮个正着 | 便利优先的安全惰性 | 密钥一律 process.env.XXX 注入,示例用 <your-token> 占位 |
第 06 章 |
| B5 配置与实际漂移 | 文档说测试用 Vitest,仓库早已换成 Jest,代理照文档写错命令 | 配置是写时快照,不是实时状态 | 定期文档审计(第 19 章 workflow 流水线),漂移即修 | 第 19 章 |
典型翻车链:为"省事"把约定全塞进共享配置(B1/B2)→ 文件膨胀、规则失焦 → 没人再维护 → 漂移(B5)→ 新会话按过期文档办事。配置的债,利息是每一个新会话。
本类最危险的是 B5 配置与实际漂移:它是唯一"随时间自动恶化"的条目——其余坑是事件,B5 是趋势,不审计就一定腐烂。
心法:配置文件会撒谎,而且专挑新会话的第一分钟撒——定期审计是唯一的解药。
C 类是"代理能做什么没管住",D 类是"代理要做什么没说清"——前者管权限的下限,后者管指令的质量,一个保安全、一个保效率,都缺不得。
边界设计(第 11–13 章)的铁律只有一条:安全必须落在工具层,prompt 只是倡议。凡是"希望"代理不做的事,都要假设它一定会做——然后把这个希望变成拦截。
| 条目 | 症状 | 根因 | 解法 | 详见 |
|---|---|---|---|---|
| C1 全自动模式直连生产 | auto-accept 全开,代理直接对生产库跑了迁移 | 权限档位与目标环境不匹配 | 生产环境只读 + 审批门;全自动仅限隔离沙箱 | 第 11 章 |
| C2 deny 规则缺失 | 只有 allow 清单没有 deny 底线,删目录、强推分支一路绿灯 | 误以为 allow 之外默认安全 | 共享层先立三条红线 deny(rm -rf / 强推 / 读生产密钥),再谈放行 |
第 07 章 |
| C3 hooks 不测试就上线 | 凭据扫描 hook 正则过宽连测试样例都拦,团队一怒全关 | hook 也是代码,却没经过测试就上岗 | 每个 hook 上线前构造"应拦 / 应放"用例各一条验证 | 第 13 章 |
| C4 把 prompt 恳求当安全边界 | 提示词里写"请千万不要删库",然后祈祷 | 自然语言约束没有强制力,长上下文下必然遗忘 | 安全要求下沉到权限、hooks、沙箱;prompt 只做补充说明 | 第 11 章 |
| C5 人工检查点形同虚设 | 审批弹窗无脑回车,出事后回看每一步都"点了同意" | 检查疲劳:高频低价值弹窗训练出肌肉记忆 | 可预期操作进 allowlist 降低弹窗频率,把注意力留给真检查点 | 第 20 章 |
典型翻车链:prompt 里恳求(C4)→ 偶然生效强化错觉 → 全自动档位下撞上边界场景(C1/C2)→ 无脑回车的检查点(C5)放行 → 事故。每道防线都"差一点点"。
本类最危险的是 C4 把 prompt 恳求当安全边界:它最像"做了安全工作"——提示词写上了、心里踏实了,实际拦截力为零。安全感的错觉,比没有防护更糟。
心法:边界的强度以"最疲惫时刻的你"为准——无脑回车的你,就是边界要防的那个人。
任务工程(第 14–16 章)的一切问题都是"切"的问题:切多大、往哪切、切出来怎么验。
| 条目 | 症状 | 根因 | 解法 | 详见 |
|---|---|---|---|---|
| D1 大任务硬吞 | "把整个用户系统做出来"一张卡派下去,产出半想象半残缺 | 上下文窗口与单次生成质量都撑不住大颗粒 | 按垂直切片拆到 1–2 天粒度,任务图管依赖 | 第 14 章 |
| D2 水平切片 | 先写完所有 model,再写所有 service,集成期全线爆炸 | 按技术分层切"看起来整齐",每层却都是半成品 | 垂直切片:一个功能从表到页面一次打通,每片可验收 | 第 14 章 |
| D3 任务无验收标准 | 卡上写"实现导出功能",代理自说自话"已完成" | "完成"没有客观定义,验收退化为感觉 | 每卡写可执行验收:测试数、curl 断言、lint 通过 | 第 14 章 |
| D4 任务卡缺上下文 | 卡里没写依赖的表结构与既有约定,子代理只能猜或反问 | 把任务卡当备忘便签,而非自包含交付物 | 七字段模板:背景/目标/范围/约定/验收/禁止/回传,一个不少 | 第 14 章 |
| D5 不提交不回滚 | 代理裸奔改两小时,一键跑偏后无从回退 | 没把"高频小提交"当纪律 | 每个可验证单元立即提交,Conventional Commits 带任务号 | 第 12 章 |
典型翻车链:大卡派下去(D1)→ 代理自由发挥两小时 → 无验收标准只能"目测"(D3)→ 跑偏了想回退,发现没有提交(D5)→ 只能推倒重来。
本类最危险的是 D1 大任务硬吞:它是其余四条的放大器——颗粒度一大,验收必然含糊(D3)、上下文必然缺斤短两(D4)、失败后必然无从回滚(D5)。
心法:切得越垂直越好验,切得越原子越好滚——任务卡的颗粒度决定返工的粒度。
E 类是"一支代理团队没带好",F 类是"收尾关口没守住"——E 类问题发生在派发与并行期,F 类问题发生在集成与交付期,恰好是协作生命周期的前后两半。
多代理(第 17–20 章)放大的是你给它的东西:给清晰就放大产能,给混乱就放大混乱。
| 条目 | 症状 | 根因 | 解法 | 详见 |
|---|---|---|---|---|
| E1 为多而多 | 三个文件的小改动开出五代理编队,协调成本远超收益 | 把多代理当排场而非工具 | 默认单代理;文件互不重叠且任务可独立验收才并行 | 第 17 章 |
| E2 非自包含派发 | 给子代理发"按上面说的做" | 子代理看不到主会话上下文,指令悬空 | 派发四要素自包含:目标 / 范围 / 约定 / 回传格式 | 第 18 章 |
| E3 并行代理改同一文件 | 两个 worker 同时改 app.ts,合并冲突手工救火 | 所有权未分区,共享文件无协议 | 文件所有权进章程:共享文件只读,变更由主代理执行 | 第 20 章 |
| E4 结果不验收直接合并 | 看回传"全部完成"就 merge,集成期连环爆 | 把回传文本当成了事实 | 验收三件套:自己跑测试、抽查 diff、关键路径手测 | 第 17 章 |
| E5 无预算上限狂跑 | 长任务一夜烧掉几十万 token,产出仍不收敛 | 没有停机条件与成本闸门 | 任何编排先设 token / 轮数 / 时间上限与超时熔断 | 第 20 章 |
典型翻车链:为多而多开编队(E1)→ 派发语焉不详(E2)→ 两个 worker 抢同一文件(E3)→ 回传"完成"未验收就合(E4)→ 集成期连环爆,token 也烧穿了(E5)。
本类最危险的是 E3 并行代理改同一文件:它是唯一让"并行收益变负"的条目——冲突排查与返工的时间,通常超过并行省下的时间。
心法:派发之前先问一句"这个子代理能只靠自己把事做完吗"——答案是否,就先补上下文再派。
流程(第 20–21 章)的意义,是在你"自信值最高"的时刻按住你。
| 条目 | 症状 | 根因 | 解法 | 详见 |
|---|---|---|---|---|
| F1 不读 diff 直接合并 | CI 绿就点 merge,diff 里混进了代理"顺手"加的依赖 | 把验证外包给了 CI,人审缺位 | 合并前人必须读 diff:范围外改动、依赖变更、密钥扫描三处重点 | 第 21 章 |
| F2 测试后置 | 先把功能全写完"回头补测试",补出来全是应景测试 | 测试被当成附属品而非规格 | 测试随卡同步交付;验收标准里测试数是硬门槛 | 第 21 章 |
| F3 发现跑偏硬推 | 会话明显跑偏仍反复"再试一次",不肯重开 | 沉没成本谬误:舍不得已花的 token | 跑偏即止损:把有效结论落盘,开新会话重来 | 第 15 章 |
| F4 把 demo 当产品交付 | 无错误处理、无测试、密钥硬编码的"能跑"版本直接上生产 | 把"演示成功"等同于"可以交付" | 对照发布检查单逐项过:错误处理、测试、安全、回滚 | 第 01 章 |
典型翻车链:CI 绿就合(F1)→ 应景测试全绿掩盖问题(F2)→ 跑偏了不甘心硬推(F3)→ 带病"能跑"版本上了生产(F4)。
本类最危险的是 F4 把 demo 当产品交付:因为它从不以"事故"的形式出现——demo 部署成功的那一刻没有人受伤,代价在第一次真实用户报障时才到账。
心法:demo 与产品之间隔着的不是功能,而是那套"看起来不产代码"的流程——省掉它,就是省掉产品。
清单的价值不在"知道",在"落地"。一条反模式从被认领到被消灭,固定四步:
完整走一遍的例子——现象:"依赖清单里多了三个没见过的 npm 包":
反查:代理擅自装包 → C2(deny 缺失),次要关联 F1(没读 diff 就合并)。
根因:权限 allowlist 里没约束包管理器;allow 之外默认放行了 npm install。
机制化:开发期档位显式拒绝 npm install(装依赖走人工审批);
PreToolUse hook 对 package.json 的变更做审计提醒。
验证:让代理尝试装一个测试包 → 工具层拦截成功,代理提示需要审批。
回访:两周后 package.json 的变更记录全部对应已审批任务。
再走一遍的例子——现象:"代理说'已修复',换个输入又复现了":
反查:修复漂移 → A4(报错不带复现信息),可能叠加 D3(验收没覆盖边界输入)。
根因:你只给了报错截图,代理修的是"截图里的那一次",不是"这一类"。
机制化:把"报错三件套"写进 AGENTS.md 的提问规范;沉淀一个 bug 上报
模板(skill 或 issue 模板),强制字段:完整错误文本 / 复现命令 /
期望与实际 / 相关文件路径。
验证:故意只发一张截图再报错 → 代理按模板反问三件套,而不是直接动手。
回访:一周内"修复后复发"的会话归零。
第三个例子,给多代理场景——现象:"两个并行 worker 的分支合并时冲突一片":
反查:并行代理改同一文件 → E3(所有权未分区),源头关联 E1(该不该并行)。
根因:派发时没有做文件所有权分区,app.ts 这类共享文件人人可写。
机制化:协作章程写死所有权协议——共享文件 worker 只读,变更由主代理
执行或走"修改申请";派发模板加入"禁改文件清单"字段。
验证:复跑一次同样的双 worker 任务 → 零冲突,挂载变更以申请单形式回传。
回访:此后两个里程碑零合并冲突。
三个例子形状相同:都把"人的自觉"换成了"模板与拦截"。给团队负责人的落地节奏建议:
前两章的实战里埋着几个真实的翻车点。本节把本教程作者在 BookNest 全程踩过的三个坑完整摊开——叙事化还原现场,再反查到本章条目。复盘有个反直觉的原则:未遂事故与真实事故同样值得记录——被拦下的那一次证明机制在岗,没被拦下的那一次证明机制缺位。BookNest 全程恰好两类都有。先剧透结论:三个坑,没有一个能怪罪于"模型不够聪明"。
现场还原(第 21 章 21.2 与 21.5):M0 那晚为了赶进度,hooks 三件套只配了凭据扫描,越界写入拦截想着"等项目像样了再补"。第 7 晚,Worker B 在自己的分支上改了共享文件 app.ts 挂载路由——这个动作本该在工具层被直接拦下,结果一路绿灯,合并时与 Worker A 的改动撞个正着。冲突排查加章程修订,半小时还只是直接成本:如果两个域的改动攒到 M2 结束才撞,定位成本要按小时计。
细节耐人寻味:Worker B 并没有"违规故意"——协作章程里共享文件规则确实写了,但彼时它只是文档里的一行字,没有任何工具在写操作前看它一眼。
反查条目:C2(deny 规则缺失),外加 C4 的温和变体——"写在文档里"不等于"拦在工具里",本质上还是把文本当了边界。
换来的规则:当晚补齐 hooks 三件套,此后 M2/M3 零越界事故;章程新增"路由挂载一律由主代理执行"。如果重来:M0 当晚配齐,一次冲突都不会发生——这也是第 21 章"如果重来会改什么"清单上的第一条。教训一句话:治理欠账会连本带利地还,越晚还利息越高。
现场还原(第 21 章 21.5 与 21.7):M2 双 worker 并行,Worker B 的"灵感速记归入笔记"依赖 Worker A 的笔记接口。接口形状第一天没有落进契约(shared/types/notes.ts),B 只能按猜的形状写 mock 顶上;两天后接口落定,形状与猜测有出入——两天的 mock 全部重写,相关测试还要重跑一遍。
更扎心的是:Worker A 的接口形状其实第一天就定了,只是躺在 A 自己的上下文里,没有变成 B 也能读到的文件——信息差就是这么产生的。
反查条目:表层是 E2(非自包含派发)——B 的任务卡没写"依赖接口以 shared/types 为准";根子是 D4(任务卡缺上下文)——派发前置检查里少了"依赖契约是否已落盘"这一项。
换来的规则:契约先行成为派发的硬前置条件;派发清单固定多一项"依赖契约已存在"。如果重来:接口形状第一天就进契约,两天 mock 一天都不用白写。教训一句话:并行开发的成本不在并行本身,而在被并行放大的信息缺口。
现场还原(第 21 章 21.3 与 21.6):两次翻车,同一个文件。第一次,FR-5 的 spec 把"先校验再分块再事务"这类实现细节写了整整一页——后来实现换了方案,这一页成了误导源,整段删除。第二次,M3 收口跑 workflow 文档审计,抓出两处 spec 已过期(FR-6 的搜索范围在 M2 已扩展到笔记内容,spec 没跟上),顺手还揪出一张未核销的任务卡。
反查条目:第一次是 **D3(验收标准缺位)**的近亲——spec 写成实现方案,"什么算完成"就失去了客观抓手;第二次是 B5(配置与实际漂移)——文档是写时快照,不审计就会腐烂。
换来的规则:spec 只写"做什么"与"什么算完成",实现细节归任务卡;workflow 文档审计进入每个里程碑的收口清单。如果重来:spec 落笔前先问一句"这页是在描述结果,还是在规定步骤?"——后者删掉。顺带说,审计抓出过期 spec 的那一刻其实是好消息:它证明"文档会腐烂"终于被当成流程问题管理,而不是靠谁的记忆。教训一句话:文档的价值随时间衰减,审计是唯一的防腐剂。
| 坑 | 反查条目 | 换来的规则 | 出处 |
|---|---|---|---|
| hooks 只配一半 → app.ts 双改冲突 | C2 / C4 | 越界写入拦截配齐;路由挂载由主代理执行 | 第 21 章 21.2、21.5 |
| 契约晚到 → mock 白写两天 | E2 / D4 | 契约先行是派发硬前置;派发清单增"依赖契约已落盘" | 第 21 章 21.5、21.7 |
| spec 写歪与过期 | D3 / B5 | spec 只写"做什么与算完成";workflow 审计入每个收口 | 第 21 章 21.3、21.6 |
再补两个"未遂事故"(第 21 章实录):M1 执行会话的两次假设批注——一次想引入重复的校验库,一次把 created_at 当成"录入即读完"(统计口径会全错)——都被计划模式的人工批注在动手前拦下(第 15 章)。它们没有造成任何损失,恰恰因为检查点真实存在、且真的有人在看。把真实事故与未遂事故放在一张表里看,你会得到一张完整的"机制有效性地图":哪里拦住了、哪里没拦住,一目了然——而没拦住的地方,就是下一条该补的机制。
三个坑按类别归类:坑一落在 C 类,坑二落在 D/E 类交界,坑三落在 B/D 类交界。分布里没有一条提示词问题,更没有一条模型能力问题——**每一个都是系统缺了一道机制:一道拦截、一份契约、一次审计。**这正是全书的结论在失败视角下的重述:vibe coding 的下限不取决于模型多聪明,取决于你的机制拦得住什么。
| 误区 | 典型现象 | 纠正方法 |
|---|---|---|
| 把本章当收藏品 | 通读一遍、点了收藏,下次照踩不误 | 事后字典用法:翻车后反查条目,并写下换来的规则 |
| 症状相同就照抄解法 | 会话变长就无脑压缩上下文,真根因却是任务没拆 | 速查表只负责入围,解法要跟着根因走 |
| 把锅全甩给模型 | "AI 就是不行",跳过对上下文与配置的自查 | A/B/D 三类的根因几乎都在输入侧与机制侧 |
| 用清单事后追责 | 在 code review 里甩条目链接攻击队友 | 清单用于复盘与规则沉淀,不用于指认"谁又犯了 B4" |
| 一次改掉所有坏习惯 | 立十条新规,一周后全部失效 | 每次只修最痛的一条,机制化落地见效后再修下一条 |
| 只复盘真实事故 | 被检查点拦下的"未遂事故"无人记录,机制有效性无从评估 | 未遂事故一并进事故记录:它验证的是机制在岗 |
| 只修提示词不修机制 | 每个新会话都重复"请务必遵守 XX" | 提示词层兜不住的约束,下沉到权限、hook 与章程 |
本章用 30 条反模式把前 22 章的正面纪律从失败视角重述了一遍:A 类上下文问题根因在输入侧,解法是喂足信息、少贴大文件;B 类配置问题的根因是快照失真,解法是落盘分层加定期审计;C 类边界问题的根因是约束悬空,解法是下沉到权限、hook 与沙箱;D 类任务问题的根因是切分不当,解法是垂直切片加可执行验收;E 类多代理问题的根因是治理缺位,解法是自包含派发、所有权分区与预算闸门;F 类流程问题的根因是验证外包,解法是把人审与测试放回硬门槛。贯穿项目里作者的三个真实坑(hooks 欠账、契约晚到、spec 失真)进一步印证:翻车极少因为模型笨,几乎都是机制缺——反模式清单的终点不是"知道",而是把每个坑变成一道拦截、一份契约或一次审计。
要点回顾:
全书 55 个核心术语的分组速查表,按"模型与上下文 → 工具与协议 → 工程机制 → 多代理协同 → 安全与治理"五个主题排列。每个术语给一句话解释与首次系统讲解的章节;正文各章技术术语首次出现处均括注英文原词,本表同时充当中英对照索引。
五十多个术语不是平铺的,它们挂在同一棵"代理能力树"上。先看树再查表,每个术语就知道自己服务于哪一层:
AI 编码代理(由 agent loop 驱动)
├── 知识层 · 它知道什么 —— AGENTS.md(常驻约定)· Skill(按需方法论)· 记忆系统(跨会话沉淀)
├── 能力层 · 它能做什么 —— 内置工具(读 / 写 / 执行)· MCP(外部能力)· subagent(分身)
├── 纪律层 · 它被允许做什么 —— 权限模式与 allow/ask/deny · hooks · 沙箱与审批门
├── 任务层 · 活怎么拆 —— 垂直切片 · 任务卡 · spec · handoff · 看板与 WIP 限制
└── 编队层 · 产能怎么放大 —— 多代理架构模式 · 自包含提示词 · workflow 编排 · 治理与成本
树的每一层对应下文一个主题分组;"任务层"与"编队层"的机制建立在前面三层之上——这正是全书"配置 → 技能 → 边界 → 任务 → 编队"的行文顺序。
对照全书结构:树根对应 Part 1(认知与工作台),知识层对应 Part 2 与 Part 3,能力层对应第 6 章与 Part 6,纪律层对应 Part 4,任务层对应 Part 5,编队层对应 Part 6 与 Part 7 的两场综合实战。查表前先定位自己在哪一层,能省一半时间。
一切方法的地基:模型能看到什么、会记多久、会犯什么错。这组词不理解,后面的所有配置都只是背口诀。 主线在 Part 1(第 1–3 章);渐进式披露的机制细节在第 8 章,模型分级的成本账在第 20 章。
| 术语 | 英文 | 一句话解释 | 详见章节 |
|---|---|---|---|
| 大语言模型 | LLM (Large Language Model) | 以海量文本训练、按概率逐个生成词元的模型,是编码代理的"引擎";能力强但没有记忆,也不会主动核实事实。 | 第 1、2 章 |
| 上下文窗口 | context window | 模型单次推理能"看见"的全部内容(系统提示、对话、文件、工具结果)的总量上限,类比一张固定大小的办公桌。 | 第 2、3 章 |
| token(词元) | token | 模型计量的最小文本单元(一个英文单词约 1.3 个,一个汉字约 0.5–1 个),窗口大小、计费与缓存都以它为单位。 | 第 2 章 |
| 系统提示 | system prompt | 工具方在对话开始前预设给模型的行为规则,规定如何调用工具、如何对待危险操作。 | 第 3 章 |
| 上下文工程 | context engineering | 围绕"模型此刻能看到什么",系统性决定给什么、不给什么、何时给、以什么形式给的手艺;范围比提示词工程更大。 | 第 3 章 |
| 上下文压缩 | compaction | 会话过长时代理把早期内容摘要化以腾出窗口的机制;丢失的往往是最要命的早期决策,需靠项目配置与 handoff 兜底。 | 第 3、16 章 |
| 幻觉 | hallucination | 模型一本正经地编造不存在的事实;编码场景的高发形态是"依赖幻觉"——按命名规律编出不存在的库与 API。 | 第 1、2 章 |
| 提示注入 | prompt injection | 把恶意指令藏进模型会读到的内容(网页、文件、工具结果)里,诱骗代理执行非预期动作的攻击手法。 | 第 11、12 章 |
| 渐进式披露 | progressive disclosure | Skill 系统的核心机制:内容分三级(元数据常驻 → 正文匹配后加载 → 引用文件执行中再读),每一级只在需要时进入上下文。 | 第 8 章 |
| 记忆系统 | memory | 代理把跨会话有效的信息(用户偏好、项目决定)持久化、并在后续会话自动带回的机制;对应"窗口 ≠ 记忆"这条铁律。 | 第 5 章 |
| 模型分级 | model tiering | 按任务价值分配模型档位:重活用强模型、机械批量活用便宜档位,是成本治理里见效最快的杠杆之一。 | 第 20 章 |
代理的"外挂装备"与它们的标准接口。 这组的主题是四条供给线:能力怎么接进来(MCP)、知识怎么沉淀下去(Skill)、约定怎么常驻生效(AGENTS.md / CLAUDE.md)、行为怎么自动干预(hooks)。主线在 Part 2 与 Part 3。
| 术语 | 英文 | 一句话解释 | 详见章节 |
|---|---|---|---|
| AI 编码代理 | AI coding agent | 能循环执行"理解目标 → 调用工具 → 观察结果 → 继续行动"的 AI 编程工具(ZCode、Claude Code、Codex CLI 等),区别于只做补全的 IDE 插件。 | 第 1、2 章 |
| agent loop(代理循环) | agent loop | 编码代理共通的引擎:模型决策 → 工具执行 → 结果回喂 → 再决策,循环直到任务完成或需要人介入。 | 第 2 章 |
| 工具调用 | tool use | 模型在回合中请求执行某个具体工具(读文件、跑命令、检索)并拿到结构化结果的过程,是代理真正产生副作用的时刻。 | 第 2 章 |
| MCP | Model Context Protocol | 连接 AI 应用与外部数据源、工具的开放协议:server 暴露能力,代理启动时发现、按需调用——"给代理装新能力"的标准件。 | 第 6 章 |
| MCP server | MCP server | 按 MCP 协议暴露具体能力(数据库、浏览器、GitHub、检索等)的服务进程;每接一个都要付出上下文、误调用面与启动开销。 | 第 6 章 |
| stdio / HTTP 传输 | stdio / HTTP transport | MCP 的两种通信方式:stdio 把 server 作为本地子进程经标准输入输出通信;HTTP 连接远程端点,凭据经请求头注入。 | 第 6 章 |
| Skill(技能) | Agent Skill | 把可复用任务方法论沉淀成的"文件夹能力包"(SKILL.md + 资源目录),靠描述触发、按需加载;给代理的是知识,区别于 MCP 给的能力。 | 第 8–10 章 |
| SKILL.md | SKILL.md | Skill 的核心文件:YAML frontmatter(name + description)加 Markdown 正文——前者唯一常驻上下文、决定触发生死,后者是写给代理的操作手册。 | 第 8、9 章 |
| Agent Skills 开放标准 | Agent Skills open standard | 约定 SKILL.md 格式与 scripts / references / assets 目录结构的开放规范,使 Skill 跨工具(ZCode、Claude Code 等)通用。 | 第 8 章 |
| AGENTS.md | AGENTS.md | 放在仓库里向代理交代项目约定的 Markdown 文件(简介、命令、约定、禁区),支持根目录与子目录分层,进 Git、走评审。 | 第 4 章 |
| CLAUDE.md | CLAUDE.md | Claude Code 读取的项目上下文文件,角色与 AGENTS.md 等价;本书以 AGENTS.md 为主示例,两者内容互通。 | 第 4 章 |
| hooks(钩子) | hooks | 注册在工具调用生命周期事件点(PreToolUse、PostToolUse、Stop 等)上的 shell 命令,用来自动拦截、格式化、扫描与审计。 | 第 7、13 章 |
从"聊天生成代码"升级为"工程化协作"的一组机制。 四个经典问题各有对应词:权限怎么配(第 7 章)、任务怎么拆(第 14–16 章)、并行怎么不踩脚(第 12、20 章)、旧系统怎么改(第 22 章)。团队负责人重点看这一组。
| 术语 | 英文 | 一句话解释 | 详见章节 |
|---|---|---|---|
| 权限模式 | permission mode | 代理动作的放行策略档位:default(未命中即问)、plan(只读规划)、acceptEdits(编辑免确认)、bypassPermissions(全放行,高危),按任务阶段匹配。 | 第 2、7、11 章 |
| allow / ask / deny 规则 | allow / ask / deny rules | 把信任写成规则:allow 免确认放行、ask 必须人审、deny 直接拒绝;判定顺序为 deny > ask > allow,清单随证据逐步"养"出来。 | 第 7 章 |
| 沙箱 | sandbox | 给代理划定的受限执行环境(工作目录隔离、Git 隔离、容器化),限制它"最坏能碰到哪里"。 | 第 12 章 |
| git worktree(工作树) | git worktree | 让同一仓库同时检出多个工作目录、各占一个分支的 Git 机制,用于多任务 / 多代理并行且互不踩文件。 | 第 12 章 |
| 审批门 | approval gate | 沙箱策略中的强制停机点:代理到达高危动作(部署、删除、出网)即暂停,等人工批准后才继续。 | 第 12 章 |
| 特征测试 | characterization test | 重构遗留代码前先给现有行为"拍照"的测试:不关心对错、只固化现状,为渐进重构提供安全网。 | 第 22 章 |
| 绞杀者模式 | strangler fig pattern | 不推倒重写,而是让新实现逐步包住旧系统、一块块接管职责直至旧代码自然枯萎的渐进重构策略。 | 第 22 章 |
| 垂直切片 | vertical slice | 把需求切成一条条用户可演示的端到端路径(页面 → 接口 → 数据),而不是先做完所有模型层再做所有接口层。 | 第 14 章 |
| 任务卡 | task card | 一次会话能完成、验证、提交的最小任务单位,按七字段模板书写,是第 18 章"自包含提示词"的提前演练。 | 第 14 章 |
| spec(功能规格) | specification | 动手前写清"做成什么样"的规格文档:背景、用户故事、接口约定、数据变更、边界与错误处理、不做的事、验收标准;多会话的共同记忆。 | 第 15 章 |
| 计划模式 | plan mode | 让代理先只读勘察、产出实现计划、待人工批准再动手的工作方式;"先探后改",把方向性错误消灭在写代码之前。 | 第 15 章 |
| handoff(交接文档) | handoff | 会话收尾时写给"下一个自己"的状态快照:已完成、当前状态、下一步、已知坑、关键文件——会话会死,状态要活。 | 第 16 章 |
| 决策日志 | decision log | 只追加、不改写的"为什么"记录:日期、决定、理由、备选与不选原因;正式化后演化为 ADR。 | 第 16 章 |
| WIP 限制 | WIP (work in progress) limit | 看板纪律:进行中的任务同时最多 1–2 张,想开新卡先关一张;防上下文衰退、半成品堆积与合并冲突指数增长。 | 第 16 章 |
| 契约先行 | contract-first | 多代理并行前先冻结接口契约(类型、API、schema)并提交进仓库,作为唯一对齐锚点;worker 对契约编程,而非互相猜测。 | 第 20 章 |
| 持续集成 | CI (Continuous Integration) | 提交即自动构建与测试的工程实践;代理产出与人类代码同标准过 CI,是多代理并行时的质量底线。 | 第 2、20 章 |
从一个代理到一支编队。 五种架构模式解决"怎么编队"(第 17 章),自包含提示词解决"怎么派活"(第 18 章),workflow 与 journal 解决"怎么管流程"(第 19 章),治理解决"怎么不内讧"(第 20 章)。
| 术语 | 英文 | 一句话解释 | 详见章节 |
|---|---|---|---|
| subagent(子代理) | subagent | 由主代理派生、拥有独立上下文窗口去完成单一任务的代理实例;只把结果回传主代理,中间过程不占主窗口。 | 第 2、18 章 |
| 主从编排 | orchestrator-worker | 总控(orchestrator)负责拆解、派发、整合与验收,worker 各领一张自包含任务卡干活的标准编队模式。 | 第 17 章 |
| 流水线 | pipeline | 多个阶段顺序接力、上一阶段产物是下一阶段输入的编队模式,适合"勘探 → 方案 → 实现 → 验证"这类固定流程。 | 第 17、19 章 |
| 评审者-实现者 | maker-checker | 写代码与审代码交给两个不同代理,用角色分离破解"自己审自己失灵";可加红队(red team)变体提升审查强度。 | 第 17 章 |
| 扇出汇总 | fan-out / map-reduce | 把同构批量任务并发派给多个 worker(map),再把各自结果去噪汇总(reduce)的模式;扇出前必须设并发上限。 | 第 17、19 章 |
| 探索-实现分离 | explore-then-implement | 先派只读代理"画地图"产出勘探报告,再让实现代理照图施工;接手陌生代码库前的标准动作。 | 第 17 章 |
| 自包含提示词 | self-contained prompt | 给 subagent 的任务提示必须装下完成该任务所需的全部信息,不允许依赖"上一次会话聊过什么";任务卡即提前写好的自包含提示词。 | 第 14、18 章 |
| 编排脚本 / workflow | workflow orchestration | 用一段 TypeScript 脚本声明子代理、用普通控制流(循环、条件、Promise.all 扇出)串成流程;入 Git、可评审、可复跑。 | 第 19 章 |
| journal 断点 | journal & resume | workflow 引擎把每个已完成步骤(步骤名 + 输入指纹 + 结果)记入运行日志;重跑时命中的直接复用缓存,失败只补未完成步骤。 | 第 19 章 |
放手让代理干活的底气来自这组词。 前四个词负责"限"——限权、限网、限爆炸半径、层层设防;后四个词负责"管"——管凭据、管记录、管节点、管预算。主线在第 7、9、11–13、20 章。
| 术语 | 英文 | 一句话解释 | 详见章节 |
|---|---|---|---|
| 最小权限原则 | principle of least privilege | 任何主体只应拥有完成当前任务所需的最小权限集合;落地四步:默认拒绝、按需开洞、任务结束收权、定期审计。 | 第 11 章 |
| SSRF | Server-Side Request Forgery(服务器端请求伪造) | 诱骗服务向非预期地址(内网、云元数据端点)发起请求的攻击;代理出网必须先过域白名单这道闸。 | 第 7、9 章 |
| 爆炸半径 | blast radius | 出错时影响的范围边界;沙箱、分支隔离与权限收紧的本质,都是把代理的爆炸半径封顶。 | 第 11、12 章 |
| 纵深防御 | defense in depth | 多层护栏各管一段(权限规则 → hooks → 沙箱 → 流程约束),任何一层被突破,下一层接住。 | 第 11 章 |
| 凭据零字面量 | zero credential literals | 任何密钥、口令、连接串都不许以字面量出现在代码与文档中,一律环境变量注入(如 ${MY_KEY}、process.env.MY_KEY)。 |
第 6、7、13 章 |
| 审计日志 | audit log | 代理"做过什么"的只追加记录(时间、动作、对象、裁决);hook 拦截与权限放行都应留痕,出事可回放。 | 第 13 章 |
| 人工检查点 | human checkpoint | 流程中强制"代理停下、等人批准"的节点:契约变更、删除类操作、发布与花钱动作;应写进 AGENTS.md 与协作章程。 | 第 11、20 章 |
| 成本治理 | cost governance | 对 token 消耗做预算、记账与优化的习惯:里程碑预算对账、模型分级、稳定前缀吃缓存,定期复盘单位产出成本。 | 第 20 章 |
十一组最容易混的近义术语,每组几句话分清。这些配对在实战里经常被互换使用,而互换的代价就是配置错位。
npm view 核实、跑测试),后者靠边界设计防御(只读、域白名单、沙箱)。社区与团队口语里的常见说法,与本书标准术语的对应关系:
| 你可能听到的说法 | 本书标准术语 | 说明 |
|---|---|---|
| yolo 模式 / bypass 模式 | bypassPermissions 权限模式 | 全放行档位,高危,第 7、11 章 |
| 技能包 / Skill 包 | Skill | Part 3(第 8–10 章)的主角 |
| 主代理 / 总控 / 老板代理 | orchestrator(主从编排中的角色) | 第 17 章 |
| 交接文档 / 交接纸 | handoff | 第 16 章,每次主动收尾必写 |
| 脚本编排 / 编排脚本 | workflow | 第 19 章,多代理流程代码化 |
| 计划模式 | plan mode | 第 15 章,先探后改 |
| 红队 | red team 变体 | 评审者-实现者模式的加强审查形态,第 17 章 |
| 派活 / 派发 | 派发 subagent 任务 | 第 18 章,自包含提示词是前提 |
| 勘探 / 画地图 | 探索-实现分离 | 第 17 章,先只读出报告再施工 |
| 金标准 / 样本集 | 触发测试样本集 | 第 10 章,Skill 调试时定量验证隐式命中 |
| 你是谁 | 建议路径 |
|---|---|
| 刚上手的开发者 | 先读第 1–2 章,配合 A.3、A.4 两组随进度回查;动手拆任务前再扫一遍 A.5 |
| 团队技术负责人 | 重点 A.5 与 A.7 两组,对照第 4、7、11–13、17、20 章,并配合附录 C 的检查清单 |
| 多代理编队负责人 | 按第 17–20 章顺序通读,配合 A.6 整组与第 20 章的协作章程;动工前先过附录 C 的协同清单 |
| 排障 / 复盘时 | A.8 辨析与第 23 章反模式大全对照使用,多数"不对劲"都能对号入座 |
| 缩写 | 全称 | 含义与出处 |
|---|---|---|
| LLM | Large Language Model | 大语言模型,见 A.3 |
| MCP | Model Context Protocol | 模型上下文协议,见 A.4 |
| CI | Continuous Integration | 持续集成,见 A.5 |
| CLI | Command-Line Interface | 命令行界面;本书示例环境 ZCode 即 CLI 型代理(第 2 章) |
| SSRF | Server-Side Request Forgery | 服务器端请求伪造,见 A.7 |
| WIP | Work In Progress | 在制品,WIP 限制见 A.5 |
| ADR | Architecture Decision Record | 架构决策记录,决策日志的正式化形态(第 16 章) |
| FR / NFR | Functional / Non-Functional Requirement | 功能 / 非功能需求编号,见附录 D |
| AC | Acceptance Criteria | 验收标准编号(如 spec 里的 AC-1),见第 15 章 |
| L1 / L2 / L3 | Level 1 / 2 / 3 | 注意两套含义:第 1 章指 vibe coding 三级成熟度;第 8 章指 Skill 渐进式披露的三级加载,按上下文区分 |
本附录把全书出现过的八份关键模板收成"空白可复制版"。占位符约定:项目名一律写
YOUR_PROJECT,域名写YOUR_DOMAIN,尖括号<…>表示需要你填写的说明位。安全硬约束:所有模板零凭据字面量——密钥、口令、连接串一律环境变量注入(如${MY_KEY}或process.env.MY_KEY),复制后切勿把真实值写进任何进 Git 的文件。
.zcode/(Claude Code 为 .claude/,格式互通);文档类模板进 docs/ 与仓库根,随代码走评审;YOUR_ 与 <,确认没有漏改的占位符再提交。| 编号 | 模板 | 干什么用 | 落盘位置 | 详见章节 |
|---|---|---|---|---|
| B.1 | AGENTS.md 起步 | 新项目的第一份常驻约定 | 仓库根 AGENTS.md |
第 4 章 |
| B.2 | 子目录 AGENTS.md | monorepo 的增量约定 | 子目录 AGENTS.md |
第 4 章 |
| B.3 | SKILL.md | 把重复方法论沉淀成技能 | skills/<name>/SKILL.md |
第 8–9 章 |
| B.4 | 任务卡 | 一次会话可完成的执行单元 | docs/tasks/ |
第 14 章 |
| B.5 | 功能 Spec | 动手前定契约 | docs/spec/ |
第 15 章 |
| B.6 | Handoff | 会话收尾的状态快照 | docs/tasks/ |
第 16 章 |
| B.7 | 权限配置 | 团队权限护栏 | .zcode/settings.json |
第 7 章 |
| B.8 | Hooks 配置 | 硬约束自动化 | .zcode/settings.json + .zcode/hooks/ |
第 13 章 |
什么时候用:新项目初始化(贯穿项目的 M0 阶段),或给已有仓库补第一份项目上下文文件——目标是代理"开机即懂",新会话不再重复解释项目。 怎么改:每条命令亲手跑过再写进去,禁区具体到路径与动作;全文控制在 30 行内,超了就删最弱的一条;不擅长起笔时,用第 4 章的"代理提问法"让代理先采访你。
# AGENTS.md — YOUR_PROJECT
## 简介
<一句话:这是什么项目、给谁用、核心能力是什么>
## 技术栈
<语言 / 框架 / 数据库 / 测试框架,只写影响写法的关键项与硬性版本约束>
## 常用命令
- 开发:npm run dev
- 测试:npm test(单文件:npm test -- <path>)
- Lint:npm run lint
- 构建:npm run build
## 代码约定
- <命名约定,如:组件 PascalCase、工具函数与变量 camelCase>
- <错误处理约定,如:失败路径统一返回 { code, message }>
- <安全约定:密钥零字面量,一律 process.env.YOUR_KEY 注入>
## 禁区
- 不改动 <路径,如 migrations/**> 下已应用的文件
- 不引入 <禁止的依赖类别>;安装任何新依赖前先询问
- 不跳过 <必须的检查,如 lint 与测试>;不执行 rm -rf 与 git push --force
落地后验证:开一个全新会话,输入"读一遍 AGENTS.md,用 5 条要点复述你将遵守的约定",核对复述与文件一致再开工(第 4 章的标准动作)。 常见改法:命令区只保留"跑得通且天天用"的;禁区优先写本仓库特有的坑,rm -rf、force push 这类通用红线交给 B.7 的 deny 规则去硬约束。
什么时候用:monorepo 里某个子包有"只在该目录成立"的约定(如前端子包的组件规则),放根目录会污染其他目录的约定。 怎么改:只写增量,不复制根文件内容;每一条都自问"它只在本目录语境下成立吗";超过 15 行说明这是低频流程,该按第 8 章判据抽成 Skill 而不是继续加行。
# AGENTS.md — YOUR_PACKAGE/(子目录补充约定)
根目录 AGENTS.md 的约定在本目录同样生效,本文件只写本目录特有的增量。
## 补充约定
- <本目录的命名 / 结构约定,如:组件一律函数式,文件名 PascalCase>
- <本目录专属的数据获取约定,如:服务端状态统一走 src/api/client.ts>
- <本目录的安全红线,如:渲染用户内容前必须消毒>
- <本目录的禁用项,如:不引入 UI 组件框架>
两份文件的分工:根文件管"全仓库都成立"的约定,子文件管"只在这里成立"的增量。验证动作与 B.1 相同——让代理读两份后复述,核对有无漏读误读。
什么时候用:某类任务已重复出现 ≥ 3 次、步骤固定、含"看代码和公开资料都看不出来"的项目私有约定——三判据全过再立项(第 9 章);一次性任务直接否决。
怎么改:name 用动词开头、kebab-case、与目录名一致;description 套三段式"做什么(第三人称)+ 何时用(引用户口语原话)+ 完成标准",两三句内收笔;正文步骤写全路径与验证命令,大而低频的细节外移 references/ 并显式指路。
目录骨架先行:
skills/your-skill-name/
├── SKILL.md # 元数据 + 操作手册(下方模板)
├── scripts/ # 确定性工作:生成器、校验器(退出码可判定)
├── references/ # 大而低频的深知识,正文显式指路后再读
└── assets/ # 复制后填空的骨架文件
SKILL.md 本体:
---
name: your-skill-name
description: 为 YOUR_PROJECT <一句话说清做什么,第三人称>。当用户说
"<用户口语触发词 A>"、"<口语触发词 B>",或 <任务特征> 时使用;不适用于
<排除清单>。完成标准是 <机器可判定结果,如校验脚本退出码为 0>。
---
# <技能标题:动词 + 对象>
## 何时使用
<重申触发条件并划边界:本技能只管什么、不管什么;边缘情况停下先问。>
## 固定步骤(缺任何一步视为未完成)
1. **<步骤名>**:在 <完整路径> 做 <动作>,禁止 <越界行为>;
2. **<步骤名>**:<动作>;<必须遵守的项目约定,注明出处>;
3. **<步骤名>**:<动作>,验证命令:npm test -- <path>;
4. **<步骤名>**:动手前先读 references/<深知识文件>.md。
## 完成定义(逐项核对)
- [ ] 各步骤全部执行,无跳步;
- [ ] node skills/your-skill-name/scripts/check_xxx.mjs <参数> 退出码为 0;
- [ ] <可执行验收,如 npm test 全绿>;
- [ ] 未引入任何硬编码密钥、连接串或绝对路径。
## 汇报格式
- 新增 / 修改文件清单;
- 校验脚本与测试结果(原样粘贴退出码与 passed/failed 数);
- 某一步无法完成时:停下来说明并等待,禁止跳步或自造约定。
落盘后先做一次真实触发,再用显式调用(/your-skill-name)保底跑一次;隐式触发是否稳定命中,交给第 10 章的样本集定量验证,不凭感觉宣布。
什么时候用:把垂直切片拆成卡、落盘到 docs/tasks/ 时(第 14 章交付);给 subagent 派活前,任务卡就是提前写好的自包含提示词。
怎么改:题头"编号与卡名"是第 1 字段,编号建议用"里程碑-序号"(如 M1-02);验收标准必须是"可执行命令 + 期望结果",写不出命令就说明卡还没拆到位;契约信息用"链接而非复制",两处事实源必然漂移。
## [编号] [卡名]
- **目标**:一句话说清"完成后系统多了什么能力",以及为什么值得做。
- **涉及文件**:新增 / 修改哪些文件(相对路径,标注 新/改)。
- **实现要点**:技术决策、必须遵守的约定(链接 AGENTS.md / spec,契约
用"链接而非复制":写"以 docs/spec/YOUR_SPEC.md 为准",勿把 spec 抄进卡);
以及明确不做的事。
- **验收标准**:编号列出"可执行命令 + 期望结果",全部通过才算完成:
1. `npm test -- <对应测试文件>` → 全部通过;
2. `<验证命令,如 curl 接口或打开页面>` → <期望结果>。
- **依赖**:前置卡编号;无依赖时写"无(前置:M0 已就绪)"。
- **预计规模**:文件数 / 新增行数 / 预计会话时长。
写卡口诀:目标写能力不写任务、验收写命令不写形容词、依赖写编号不写"应该已经好了"。编号连续性与依赖图在拆卡评审时统一检查。
什么时候用:计划先行(第 15 章)——进入实现前把"做成什么样"定稿为 docs/spec/ 下的规格文档,随后让代理在计划模式下产出实现计划、人工批准后才动工。
怎么改:接口约定写到"看规格就能并发编程"的精度(方法、路径、请求响应示例、字段语义);第 6 节"不做的事"最容易偷懒,恰恰是防范围蔓延的关键;定稿后把版本与状态写进题头,评审通过才改状态。
# <功能名> · 功能规格(Spec)
- 版本:v1.0(YYYY-MM-DD 定稿) 状态:草稿 / 已评审
- 关联:<需求编号,如附录 D FR-X>;里程碑 <Mx>;技术栈约定见 AGENTS.md
## 1. 背景与目标
<为什么做:现状缺什么,本功能提供什么,目标一句话。>
## 2. 用户故事
- US-1 作为 <角色>,我想 <动作>,以便 <价值>。
- US-2 作为 <角色>,我想 <动作>,以便 <价值>。
## 3. 接口约定
<方法 + 路径 + 请求 / 响应 JSON 示例 + 字段语义(单位、缺省值、取值范围),
写到能并发编程的精度。>
## 4. 数据变更(schema diff)
<新增 / 修改的表与字段、迁移文件命名、写入时机与回滚方式。>
## 5. 边界与错误处理
<非法输入 → 什么错误码;空数据 → 什么表现;并发 / 幂等怎么处理;
错误一律走项目统一错误格式。>
## 6. 明确不做的事
- <范围外项 1>(一句为什么);
- <范围外项 2>(一句为什么)。
## 7. 验收标准
- AC-1 <可执行验收:命令 + 期望结果>;
- AC-2 <可执行验收>。
定稿后的 spec 是这一功能的单一事实源:任务卡(B.4)用"链接而非复制"指向它;修订走版本号,不原地覆盖已评审的历史。
什么时候用:每次主动收尾时(不是"想起来才写"),落盘 docs/tasks/ 并随代码提交;会话可能随时死掉,状态必须活在仓库里而不是聊天记录里。
怎么改:"已知坑"是全文最值钱的一段,务必写具体;提交号、分支、测试状态用真实值;写完让接手会话"先读 handoff 与任务卡 → 复述理解 → 人确认后开工"。
# Handoff · <任务名> · <会话 N → 会话 N+1>
- 时间:YYYY-MM-DD HH:mm 作者:<哪个会话 / 谁写的>
- 下手前先读:<任务卡路径>、<spec 路径>、<决策日志路径>
## 已完成
- <做了什么>(commit <短哈希>)
## 当前状态
- 分支 <分支名>;工作区 <干净 / 有未提交改动:逐条列出>;
- 测试 <全绿 / 有失败:列出>;任务清单勾至 <T 几>。
## 下一步(按顺序)
1. <具体到"改哪个文件的哪部分">;
2. <……>
## 已知坑
- <踩过的坑与绕法——全文最值钱的一段,别省略>
## 关键文件
- <带路径列出,让接手者不必全库搜索>
与决策日志(第 16 章)配合使用:handoff 管"干到哪",决策日志管"为什么这么定";两者都只追加不改写,推翻旧结论就追加新条目并注明取代谁。
什么时候用:项目 M0 阶段给仓库配第一份团队护栏,放 <repo>/.zcode/settings.json 进 Git 走评审;个人例外放 settings.local.json 并写入 .gitignore(层级与优先级见第 7 章)。
怎么改:从默认询问起步,把反复被你手动放行的动作评审后上移为 allow;判断口径:幂等可重跑(测试、lint、status)→ allow,留下永久痕迹(commit、install、push)→ ask,涉密或不可逆(.env、force push)→ deny;.env 的 deny 写全三种形态,防代理在子目录新建绕过。
{
"permissions": {
"defaultMode": "default",
"allow": [
"Read",
"Bash(git status)",
"Bash(git diff:*)",
"Bash(git log:*)",
"Bash(npm run lint)",
"Bash(npm test:*)",
"WebFetch(domain:docs.YOUR_DOMAIN.com)"
],
"ask": [
"Bash(git commit:*)",
"Bash(npm install:*)",
"Edit",
"Write"
],
"deny": [
"Read(.env)",
"Read(.env.*)",
"Read(**/.env*)",
"Read(secrets/**)",
"Bash(rm -rf:*)",
"Bash(git push --force:*)",
"Edit(migrations/**)"
]
}
}
三条使用要点:判定顺序是 deny > ask > allow,同时命中多条时最严的生效;defaultMode 保持 "default"(未命中即问),不要改成全放行;WebFetch(domain:…) 的域白名单同时是 SSRF 防护的第一道闸,只放你点过名的域。
养清单的节奏:弹确认框时选"始终允许"会沉淀进本机个人配置;每周把沉淀的规则评审一次,合格的上移到项目共享配置(进 Git),不合格的删掉——清单是养出来的,不是一次设计出来的。
(a) 注册骨架——什么时候用:需要把软约束变硬约束时(拦危险命令、落盘前扫密钥、写完自动检查),配置放 .zcode/settings.json。怎么改:matcher 用正则匹配工具名;timeout(秒)是注册侧的超时保险;先手动喂样本 JSON 调通脚本(见本模板末尾),再进真实会话端到端验证。
{
"hooks": {
"PreToolUse": [
{ "matcher": "Bash",
"hooks": [
{ "type": "command", "command": "node .zcode/hooks/guard-bash.mjs", "timeout": 10 }
] },
{ "matcher": "Write|Edit",
"hooks": [
{ "type": "command", "command": "node .zcode/hooks/guard-secrets.mjs", "timeout": 10 }
] }
],
"PostToolUse": [
{ "matcher": "Write|Edit",
"hooks": [
{ "type": "command", "command": "node .zcode/hooks/check-on-save.mjs", "timeout": 30 }
] }
]
}
}
guard-secrets.mjs 与 check-on-save.mjs 和下面的骨架同构:读 tool_input 的 file_path 与 content / new_string,把规则表换成密钥正则或格式化命令即可,完整实现见第 13 章。
(b) 拦截脚本最小骨架——什么时候用:写第一个 PreToolUse 守门员时。怎么改:每条规则带"类别 + 原因与替代方案",拦截时把理由写进 stderr——stderr 是给模型看的纠错通道,要写清"为什么拦 + 该怎么办";退出码 0 放行、2 阻断并反馈、其他非零为非阻断错误。
#!/usr/bin/env node
// .zcode/hooks/guard-bash.mjs —— PreToolUse 守门员最小骨架
// 契约:stdin 收 JSON(hook_event_name + tool_name + tool_input);
// 退出码 0 放行 / 2 阻断并反馈;stderr 写给模型看。
// 每条规则:[正则, 类别, 原因与替代方案]
const RULES = [
[/\brm\s+(?:-[a-zA-Z]+\s+)*-[a-zA-Z]*(?:rf|fr)[a-zA-Z]*\b/,
"递归强删(rm -rf 系)",
"不可恢复。清理产物用项目自带的 clean 命令;确需删除请人工执行"],
[/\bgit\s+push\b(?=[^\n]*--force(?!-with-lease))/,
"强推远端(--force)",
"会销毁远端历史。先 git fetch 看差异,或转人工执行"],
[/\bdrop\s+(?:table|database)\b/i,
"删库删表(DROP TABLE/DATABASE)",
"破坏数据。schema 变更走迁移脚本;清数据请人工确认"],
];
let raw = "";
process.stdin.on("data", (d) => (raw += d));
process.stdin.on("end", () => {
let evt;
try { evt = JSON.parse(raw || "{}"); } catch { process.exit(0); } // 解析失败放行(fail-open,见第 13 章)
const cmd = evt?.tool_input?.command ?? "";
const hit = RULES.find(([re]) => re.test(cmd));
if (hit) {
const [, category, advice] = hit;
console.error(`[guard-bash] 已拦截危险命令(${category}):${cmd}\n原因与替代方案:${advice}\n如确属必要,请说明理由,由人工执行。`);
process.exit(2); // 阻断本次调用,stderr 回喂模型
}
process.exit(0); // 放行
});
调试铁律:脱离会话,手动喂样本 JSON,观察 stderr 与退出码,改脚本重跑,最后才挂进配置。
echo '{"tool_name":"Bash","tool_input":{"command":"git push --force origin main"}}' | node .zcode/hooks/guard-bash.mjs
echo "exit=$?" # 期望:stderr 打印拦截原因;exit=2
上线前再用第 13 章的方法建一个样本集:allow 样本必须退出 0(误拦即 FAIL)、deny 样本必须退出 2(漏拦即 FAIL),每次改规则后回放一遍,防止"修一个误拦、放走三个危险"。
一份"作战地图",说明八份模板在一次真实开发里的接力顺序:
settings.local.json、AGENTS.local.md)并加入 .gitignore;附录 B 给了八份模板——"东西长什么样";本附录给四张清单——"什么时候必须逐项核对"。 四张清单对应代理工程的四个高风险切换点:项目接入代理(一次性)、每个工作日开工(日常)、 任务收工与合并(每卡)、向 subagent 派活(每次派发)。清单与模板配套使用:每一项几乎都 能在附录 B 找到对应模板,勾选时顺手核对模板是否真的落了地。
安全硬约束与全书一致:零凭据字面量——任何一项核对中只要发现密钥、口令、连接串以字面量出现, 一律先修再谈合并(密钥走环境变量注入见第 6 章,hooks 凭据扫描是最后一道闸见第 13 章)。
记忆靠不住。三次教训全部来自第 21 章的真实时间线:
app.ts 双改冲突;app.ts 挂载怎么办——一次真实冲突换来章程第 2.3 条。三次的共同点:**道理都懂,但轮到执行的那一分钟没人核对。**清单的作用就是把"懂"变成"勾":
怎么读:赶时间只看每张清单前的"什么时候用 / 预期耗时"两个加粗行;第一次接入项目把清单一逐项 过完;之后把清单抄进自己的仓库(见 C.6),每次只勾、不再通读。四张清单都是合法的 Markdown 任务列表,复制进仓库后勾选状态可被渲染与审计。
| 清单 | 节奏 | 谁来勾 | 侧重章节 | 配套模板 |
|---|---|---|---|---|
| 清单一 · 项目接入 AI 代理检查单 | 一次性设置 | 人主导,代理当参谋 | 第 4–7、11–13 章 | B.1、B.2、B.7、B.8 |
| 清单二 · 日常开工检查单 | 每个工作日 | 人与代理各半 | 第 3、14–16 章 | B.4、B.6 |
| 清单三 · 任务完成/合并前检查单 | 每卡收工、每次合并前 | 代理逐条自报,人抽查 | 第 7、12–15、21 章 | B.4、B.5、B.6 |
| 清单四 · 多代理派发前检查单 | 每次派活前 | 主代理起草,人批准 | 第 17–20 章 | B.4、B.5 |
与第 21 章的对应:那一章的「项目启动检查单」即清单一;「发布前检查单」以清单三为底版——发布时 在全量测试之上再过一遍清单三,并追加全量回归与 e2e 冒烟(见 C.3 的"什么时候用")。
什么时候用:新项目 git init 之后、首次给仓库接入 AI 代理之前;接手陌生仓库(第 22 章)同样先过一遍。这一阶段全程由人主导,代理最多当参谋——地基不值得放权。
预期耗时:30–60 分钟。这是回报率最高的半小时,之后的每个代理会话都在吃它的红利。
chore: init repository),从第一行起就有可回滚历史(第 12 章);agent/<卡号>)上工作;远端私有仓已建,git push --force 有服务端保护或 deny 规则兜底(第 11、12 章);.gitignore 已覆盖 .env*、secrets/**、本地覆盖文件(settings.local.json、AGENTS.local.md)与构建产物(第 5、6 章);git log 与历史提交,无凭据字面量——接手旧仓库时尤其要查(第 22 章)。AGENTS.md 按附录 B.1 落地:简介 / 技术栈 / 常用命令 / 约定 / 禁区五类齐全,全文控制在 30 行内(第 4 章);YOUR_ 与 < 确认无占位符残留(第 4 章);AGENTS.md 按附录 B.2 就位,只写"只在本目录成立"的约定,不复制根文件内容(第 4 章);.gitignore(第 5 章);defaultMode 保持 "default",判定口径 deny > ask > allow(第 7 章);.env 三种形态(.env、.env.*、**/.env*)、secrets/**、rm -rf、git push --force 与已应用的迁移目录(第 7、11 章);docs/boundary-matrix.md:开发 / 联调 / 发布三档各允许什么、禁止什么;三条红线任何档位不放开——force push、rm -rf、读取生产密钥(第 11 章);zcode --version 可运行;npm run dev 起得来、npm test 冒烟全绿、npm run lint 零告警——M0 验收三连(第 2、21 章);.gitignore(第 5、7 章);docs/(如 docs/onboarding-checklist.md),注明日期与执行人(第 16 章);配套模板:B.1(根 AGENTS.md)、B.2(子目录增量)、B.7(权限)、B.8(hooks)——各项的具体写法、落盘位置与验收方式见对应模板。
漏勾的典型代价(第 21 章实录):
app.ts 双改冲突就是这么被放大的(M0 欠账,M2 连本带利还);.env 的某一种形态 → 代理在子目录新建文件绕过,凭据扫描形同虚设;什么时候用:每个工作日(或每个工作时段)开工的第一分钟、写第一句话之前。 预期耗时:3–5 分钟,全部是"确认"而非"设置"——设置类的事已在清单一做完。
git status 干净:无未提交改动、无未跟踪文件;昨天的活已收口,或已按附录 B.6 写好 Handoff(第 12、16 章);docs/boundary-matrix.md 收紧或放开;三条红线一条没松(第 11 章);M1-02)已打开,目标、涉及文件、验收标准三个字段已读——无卡不开工(第 14 章,附录 B.4);docs/tasks/(第 21 章);漏勾的典型代价:
git status 就开工 → 昨天未收口的改动混进今天的提交,回滚粒度全乱;什么时候用:每张任务卡宣布"完成"之前、每次提 PR 或合并之前;发布场景以本清单为底版(即第 21 章的「发布前检查单」)——发布时在全量测试之上再过一遍,并追加全量回归与 e2e 冒烟。 预期耗时:代理逐条自报约 5 分钟;人工抽查 diff 另计(按卡规模 5–20 分钟)。分工是——代理负责逐条自报,人负责抽查,不是反过来(第 21 章)。
npm test(或项目等价命令)全绿,不是只跑了自己改的那几个文件(第 7 章);tsc --noEmit)零错误零告警(第 21 章)。.env*、secrets/**、已应用的迁移文件零改动——deny 规则之外再人工核对一遍(第 7、11 章);feat(server): add books CRUD API (M1-02)),一卡一提交、大卡拆数个提交(第 21 章)。配套模板:B.4(任务卡——验收标准即第一组)、B.5(Spec——AC 是第二道验收)、B.6(Handoff——收尾物落盘)。
漏勾的典型代价:
什么时候用:主代理每次向 subagent / workflow 派活之前——由主代理按条起草提示词与安排,人最终批准后才派发。 预期耗时:10–15 分钟,大头在把提示词写到自包含;省下的都是后半夜排查冲突的时间。
app.ts、package.json、shared/types/** 等谁只读、谁有权改、改走什么流程(回传"修改申请",或一律由主代理执行)(第 20、21 章);shared/types/**,变更需主代理批准并广播(第 20、21 章);配套模板:B.4(任务卡=提前写好的自包含提示词)、B.5(Spec——契约的单一事实源)。
漏勾的典型代价(第 21 章实录):
app.ts,定位与合并吃掉一晚上的顺风局;以第 21 章的 M1-02「书籍 CRUD API」为例,四张清单在一天里的接力:
git status 干净、边界档位=开发期、M1-02 在手、第一句话进入计划模式;清单不是四座山,是四个开关:C.1 一次拧紧,C.2 每天打开,C.3 与 C.4 按需扳动。
清单只在"长在流程里"时才起作用——躺在书签夹里的清单,第三次就不会被打开。八条建议:
docs/:四张清单复制进 docs/checklists/,随代码进 Git 走评审,与附录 B 的模板互为表里——模板给"长什么样",清单给"何时核对";个人可另建精简版,团队版以仓库为准(呼应第 5 章的分层思想);dispatch-subagent 一类的 skill,SKILL.md 的"完成定义"就是逐项勾选清单四;清单三同理可做 pre-merge-review;按附录 B.3 的 SKILL.md 模板:触发词写"派活 / 派发 / 开 subagent / 并行跑"等口语原话,正文"固定步骤"即清单四逐项。节选示意:
---
name: dispatch-subagent
description: 为 YOUR_PROJECT 在向 subagent 派活前执行派发检查。当用户说"派个
subagent""并行跑""分给多个代理"时使用;不适用于单代理顺序执行的任务。完成
标准是清单四 17 项全部勾选且提示词四要素齐备。
---
# 派发前检查(逐项核对附录 C 清单四)
## 固定步骤
1. **派发决策**:核对值得派发三问与并发上限,任一不满足即停下说明;
2. **提示词质量**:起草提示词并核对四要素与自包含性;
3. **所有权与契约**:核对可写路径互斥、共享文件规则与接口契约;
4. **资源与收口**:核对预算、结果落盘安排与失败预案。
## 完成定义(逐项核对)
- [ ] 清单四 17 项全部勾选,有未勾项即暂停派发并说明原因;
- [ ] 提示词草稿已交人工批准,未批准不派发。
最后一条纪律:清单勾完了不代表万事大吉——清单管"有没有做","做得对不对"的判断永远在人。
| 误用 | 现象 | 纠正 |
|---|---|---|
| 当一次性读物 | 通读一遍就再也不打开 | 清单是勾选工具不是文章,抄进仓库每次勾(C.6 第 1 条) |
| 全交给代理自勾 | 代理报"全过",合并后翻车 | 代理逐条自报 + 人抽查,diff 必须人读(C.3) |
| 一劳永逸 | 半年后清单与项目脱节 | 复盘后 48 小时内增删条目(C.6 第 5 条) |
| 赶时间全跳过 | 恰在代价最高的组翻车 | 至少勾安全组与所有权组(C.6 第 6 条) |
本教程所有章节的"贯穿项目实战"小节均基于本项目。写作时请保持与本文档设定一致;读者则应把本文档当作一份"可直接喂给 AI 代理"的需求书范例来学习。
「轻读 BookNest」是一个个人读书笔记与书评应用:记录读过的书、写读书笔记、给书打分,并按标签和年份回顾阅读轨迹。单用户自部署(先不做多用户),目标是"5 分钟部署,10 年不换"——数据永远在自己手里。
选择它的原因:体量适中(前后端加起来约 20–30 个源文件),既简单到初学者能跟上,又有足够的功能面让"任务拆分、边界设计、多 Agent 协同"有真实的用武之地。
| 编号 | 功能 | 说明 |
|---|---|---|
| FR-1 | 书籍管理 | 录入书籍(书名/作者/封面 URL/分类/状态:想读·在读·读完),支持编辑与删除 |
| FR-2 | 读书笔记 | 针对某本书创建笔记(Markdown 正文),支持时间线浏览 |
| FR-3 | 评分与标签 | 1–5 星评分;自由标签(如"科幻""年度十佳"),可按标签筛选 |
| FR-4 | 阅读统计 | 总量/年度统计、月度阅读热力图、平均分分布 |
| FR-5 | 数据导入导出 | 导出/导入 JSON 全量备份;支持从 CSV 批量导入书单 |
| FR-6 | 全文搜索 | 按书名/作者/笔记内容关键字搜索 |
| FR-7 | 快速记录 | 首页"灵感速记":先存为碎片,稍后归入某本书的笔记 |
npm run dev 一键起本地环境;npm run build && npm start 可部署到任意 Node 主机;| 层 | 选型 | 说明 |
|---|---|---|
| 前端 | React 19 + Vite 7 + TypeScript 5.x | SPA,react-router,轻量 CSS(手写 + CSS 变量主题) |
| 状态/数据 | TanStack Query 5 | 服务端状态;本地状态用 React 内置 |
| 后端 | Node 22 LTS + Express 5 | REST API,/api/v1 前缀 |
| 数据库 | SQLite(better-sqlite3) | 单文件库 data/booknest.db,schema 用 SQL 迁移脚本管理 |
| 测试 | Vitest(单元/接口)+ Playwright(端到端) | |
| 工程化 | ESLint 9 flat config + Prettier + husky |
booknest/
├── AGENTS.md # 第 4 章交付
├── .claude/ (或 .zcode/) # 第 5、7 章交付:规则、hooks、权限
├── skills/ # 第 9 章交付:项目自有 skill
├── docs/
│ ├── spec/ # 第 15 章交付:功能规格
│ └── tasks/ # 第 14 章交付:任务图与任务卡
├── server/
│ ├── src/{routes,services,db,middleware}
│ ├── migrations/
│ └── tests/
├── web/
│ ├── src/{pages,components,api,hooks,styles}
│ └── tests/
├── e2e/ # Playwright 用例
└── package.json # npm workspaces: server / web
books (id, title, author, cover_url, category, status, rating, created_at, updated_at)
notes (id, book_id NULL, fragment_id NULL, title, body_md, created_at, updated_at)
tags (id, name UNIQUE) -- 经 book_tags(id, book_id, tag_id) 多对多
fragments(id, content, captured_at, merged_into_note_id NULL) -- 灵感速记
| 里程碑 | 范围 | 对应教程章节 |
|---|---|---|
| M0 工程骨架 | monorepo、两端 Hello、lint/测试跑通 | 第 04–07 章(配置先行) |
| M1 书籍核心 | FR-1 + FR-3 + 全文搜索(FR-6) | 第 14–15 章(任务图与规格) |
| M2 笔记与速记 | FR-2 + FR-7 + 标签完善 | 第 16–18 章(长任务与多 Agent) |
| M3 统计与备份 | FR-4 + FR-5 | 第 19 章(流水线编排) |
| M4 打磨发布 | 端到端测试、性能校验、部署脚本 | 第 20–21 章(治理与收口) |
第 22 章"重构遗留项目"使用同一项目的故意劣化版(
booknest-legacy设定见该章正文):无类型、无测试、单文件巨石。
vibe-coding-tutorial/
README.md
appendix/
A-glossary.md
B-templates.md
C-checklists.md
D-capstone-project.md
part1-fundamentals/
01-what-is-vibe-coding.md
02-agent-workbench.md
03-context-engineering.md
part2-project-config/
04-agents-md.md
05-rules-and-memory.md
06-mcp-and-tools.md
07-permissions-and-hooks.md
part3-skills/
08-skill-anatomy.md
09-writing-great-skills.md
10-skill-testing-evolution.md
part4-boundaries/
11-agent-boundaries.md
12-sandboxing.md
13-hooks-in-practice.md
part5-task-engineering/
14-task-decomposition.md
15-plan-first.md
16-long-running-tasks.md
part6-multi-agent/
17-multi-agent-patterns.md
18-subagents-in-practice.md
19-workflow-orchestration.md
20-collaboration-governance.md
part7-practice/
21-capstone-greenfield.md
22-capstone-legacy.md
23-antipatterns.md完整源码 Gitee 仓库浏览 →