Vibe Coding 系统教程

From Vibes to Agentic Engineering · 从「凭感觉生成」到有纪律的 Agentic 工程:7 部分 + 附录,覆盖 AI 编程基础、项目与配置、Skills、边
← 返回教程馆
8阶段
28章节
28源文档
vibecoding仓库目录

📊 我的学习进度 本地自动保存

加载中…
🧭 导览0/0
part1 fundam0/0
part2 projec0/0
part3 skills0/0
part4 bounda0/0
part5 task e0/0
part6 multi 0/0
part7 practi0/0
appendix0/0
💡 每章卡片右上角「✓ 标记已学」;教程间互链已自动映射到本页章节;代码示例请配合 Gitee 仓库 使用。

🧭 Vibe Coding 系统教程 —— 从凭感觉生成到有纪律的 Agentic 工程 README.md

一份面向开发者的中文系统教程:教你把 AI 编码代理(ZCode、Claude Code、Codex CLI、Cursor 等)从"玩具"用成"团队"。覆盖项目配置、Skill 工程、Agent 边界、任务拆分与多 Agent 协同五大核心议题。


这份教程解决什么问题

2025 年 Andrej Karpathy 提出 "vibe coding"(凭感觉编码):用自然语言描述需求,让 AI 生成代码,人几乎不逐行审阅。这种方式能一小时做出一个 demo,但把 demo 做成产品时会立刻撞上四堵墙:

  1. 上下文墙 —— AI 不了解你的项目约定,每次对话都要重复解释;
  2. 能力墙 —— 复杂需求一次说不清,任务越大输出质量越差;
  3. 边界墙 —— 代理权限一放开就敢删文件、乱装依赖、把密钥写进代码;
  4. 协同墙 —— 一个代理干一件事没问题,十件事并行就互相踩文件。

本教程的主线就是逐一拆掉这四堵墙:用项目配置固化上下文,用任务拆分控制复杂度,用边界设计约束权限,用多 Agent 协同放大产能——这也是业界从 "vibe coding" 走向 "agentic engineering"(规格驱动、测试护栏、人工检查点、沙箱隔离)的路径。

适合谁读

  • 有编程基础的开发者:不需要精通某门语言,但要能看懂代码、会用 Git;
  • 刚开始用 AI 编码工具的人:教程从"工具是什么"讲起;
  • 已经天天在用、但总觉得"不太稳"的人:第 4 章以后基本都是给你写的;
  • 要为团队搭建 AI 编码规范的技术负责人:重点看 Part 2、Part 4、Part 6 与附录 C 的检查清单。

学习路线图

        ┌─────────────────────────────────────────────────────────┐
        │  快速上手(半天): 01 → 02 → 03 → 14 → 15                  │
        ├─────────────────────────────────────────────────────────┤
        │  系统学习(一至两周): Part1 → Part2 → Part3 → Part5        │
        │                    → Part7(按顺序读完所有动手练习)          │
        ├─────────────────────────────────────────────────────────┤
        │  团队负责人路线: 04 → 07 → 11 → 12 → 13 → 17 → 20          │
        │              → 附录 B(模板库)→ 附录 C(检查清单)           │
        └─────────────────────────────────────────────────────────┘

全书结构

全书 23 章 + 4 附录,分为七个部分。所有章节围绕一个贯穿项目展开:读书笔记与书评应用**「轻读 BookNest」**(需求书见 附录 D)——每章的"贯穿项目实战"小节,会演示把本章方法应用到这个真实项目上是什么样子。

Part 1 · 心法与基础(第 1–3 章)

建立对 vibe coding 和 AI 编码代理的正确认知:它是什么、边界在哪、怎么跟它说话。

章 文件 内容
01 什么是 Vibe Coding 起源与定义、三种成熟度、适用与不适用场景
02 Agent 工作台 工具地图、核心概念(上下文窗口/权限模式/工具调用)、环境搭建
03 上下文工程 提示词结构、迭代节奏、上下文窗口管理、反模式

Part 2 · 项目配置(第 4–7 章)

把"每次都要重新解释项目"变成"代理开机即懂"。

章 文件 内容
04 项目上下文文件 AGENTS.md / CLAUDE.md 的层级、内容组织与模板
05 规则与记忆体系 多层规则、记忆系统、团队与个人配置的分离
06 MCP 与工具接入 MCP 协议、常用 server、凭据安全、按需启用
07 权限与 Hooks 权限配置、allowlist、hooks 自动化守护

Part 3 · Skill 工程(第 8–10 章)

把可复用的方法论沉淀成代理"随取随用"的技能包。

章 文件 内容
08 Skill 系统原理 SKILL.md 结构、渐进式披露三级加载、触发机制
09 写出优秀 Skill 写作方法论、描述的艺术、结构与资源组织
10 Skill 调试与进化 触发诊断、评测、版本管理与团队 Skill 库

Part 4 · 边界与安全(第 11–13 章)

让代理"敢放手干"的前提是"它干不出格的事"。

章 文件 内容
11 Agent 边界设计 边界类型、权限模式选择、最小权限原则
12 沙箱与隔离 工作目录隔离、Git 隔离、容器化、审批门
13 Hooks 深度实践 拦截写入、凭据扫描、出网防护、审计日志

Part 5 · 任务工程(第 14–16 章)

复杂需求一次说不清?拆,是唯一的方法。

章 文件 内容
14 任务拆分方法论 从需求到任务图、原子性、验收标准、依赖排序
15 计划先行与 Spec 驱动 计划模式、规格文档、先探后改
16 长任务与状态管理 待办清单、里程碑、上下文压缩、断点续作

Part 6 · 多 Agent 协同(第 17–20 章)

从"一个代理"到"一支代理团队"。

章 文件 内容
17 多 Agent 架构模式 主从、流水线、评审者-实现者、扇出汇总等模式
18 Subagent 实战 何时拆代理、自包含提示词、结果回传与并发
19 Workflow 编排 脚本化编排、缓存复用、失败恢复
20 协同规范与治理 文件所有权、冲突避免、检查点与成本治理

Part 7 · 综合实战(第 21–23 章)

把前六部分串成两条完整作战线。

章 文件 内容
21 实战一:从零构建 BookNest 全流程演示:需求 → 配置 → 拆分 → 多 Agent 实现
22 实战二:重构遗留项目 接手陌生代码库:探索、边界收紧、渐进重构
23 反模式与踩坑大全 30 个高频失败模式及其解法

附录

附录 文件 内容
A 术语表 全书术语速查
B 模板库 AGENTS.md / SKILL.md / 任务卡 / Hooks 配置模板
C 检查清单 项目启动、日常开工、发布前、多 Agent 协同四张清单
D 贯穿项目需求书 「轻读 BookNest」产品需求与技术栈

使用说明

  • 每章固定结构:学习目标 → 正文 → 贯穿项目实战 → 常见误区 → 最佳实践 → 小结 → 动手练习 → 导航。时间紧可以只读"正文 + 常见误区";
  • 动手练习是教程的一部分:每章 3–4 题,最后一题是综合题。vibe coding 是肌肉记忆,光看不练会在真实项目里现出原形;
  • 贯穿项目「轻读 BookNest」建议真实跟着做一遍:git init 一个空仓库,从第 4 章开始把每章产物落进项目里,到第 21 章你会得到一个完整作品;
  • 本教程的方法与配置文件格式以 ZCode 为主要示例环境(配置约定与 Claude Code 等主流工具高度互通,差异处会注明)。

技术基准

  • 内容基准时间:2026 年 10 月;
  • 示例工具:ZCode(CLI AI 编码代理),概念兼容 Claude Code、Codex CLI、Cursor、Gemini CLI 等;
  • 涉及的标准:AGENTS.md 项目指令约定、Agent Skills(SKILL.md)开放格式、MCP(Model Context Protocol);
  • 贯穿项目技术栈:React 19 + Vite + TypeScript + Express 5 + SQLite + Vitest + Playwright;
  • 安全约定:所有示例零凭据字面量,密钥一律通过环境变量注入;涉及网络请求的示例均含 SSRF 防护。

下一篇:第 01 章 · 什么是 Vibe Coding

📄 第 01 章 · 什么是 Vibe Coding 01-what-is-vibe-coding.md

2025 年,"vibe coding"(凭感觉编码)一词火遍开发者社区:用自然语言把需求说给 AI 听,代码由 AI 生成,人几乎不逐行审阅。它能让你一小时做出一个能跑的 demo,也能让一个项目在第三周腐化到无法维护。本章帮你建立正确认知:它是什么、边界在哪,以及本教程为什么要把你从 L2 推向 L3。

学习目标

  • 能说出 vibe coding 一词的起源、原始含义与学术界的批评视角;
  • 能区分 L1、L2、L3 三种 AI 编码工作形态,并定位自己当前所处的层级;
  • 能识别无纪律 vibe coding 的三大崩坏信号:改 A 坏 B、依赖幻觉、技术债雪球;
  • 能用决策清单判断一个任务适合 vibe coding、必须工程化,还是处在中间地带;
  • 能对照"五堵墙"地图说清本教程五个核心部分各自拆的是哪堵墙。

1.1 起源与定义

一条推文引发的范式讨论

2025 年 2 月,OpenAI 联合创始人、前特斯拉 AI 总监 Andrej Karpathy 发文描述了一种全新的编码方式:完全交给感觉(give in to the vibes)、拥抱模型能力的指数级增长、甚至忘记代码本身的存在——看到什么就说什么,几乎不读代码,报错就原样贴回去让 AI 自己修,项目就这么"长"了出来。他给这种方式起名 vibe coding。

意译他的原话:"有一种新的编码方式……我'看什么说什么',几乎不真正读代码……我全盘接受所有 diff,报错了就复制粘贴回去,通常这样就能修好。"

短短几个月,这个词从玩笑变成行业热词:有人用它一周做出付费用户过千的小产品,也有资深工程师公开拒绝这种工作方式。为什么偏偏是 2025 年?三个条件同时成熟:模型一次能生成的代码量跨过了"整个功能"的门槛;代理工具让模型能直接读写文件、执行命令;对话成本降到可以"随便聊"的程度。争论的核心其实是一个问题:当生成代码的成本趋近于零,人的价值应该放在哪里?

它到底指什么

vibe coding 有三个特征:

  1. 对话式(conversational):需求以自然语言表达,而不是先写成设计文档;
  2. 凭直觉(intuition-driven):判断"好不好"主要靠跑起来看效果,而不是靠读代码推理;
  3. 少逐行审查(low-review):人审的是行为(程序做对了什么),不是材料(代码怎么写的)。

术语提示:社区里相近的说法还有 "AI pair programming"(AI 结对编程)等。本书约定:vibe coding 特指上述这种以行为验证为主的对话式工作方式;它的工程化升级形态叫 agentic engineering(见 1.2);两个词都不带贬义。

与"AI 辅助编程"的区别

很多人把 vibe coding 和前几年的 AI 辅助编程(AI-assisted programming,代表形态是 GitHub Copilot 式的补全)混为一谈,其实两者方向相反:

维度 AI 辅助编程(copilot 补全模式) Vibe coding(对话生成模式)
谁在写代码 人写,AI 补全 AI 写,人指导
人的注意力 逐行阅读与修改 行为验证与方向把控
上下文单位 当前行 / 当前函数 当前功能 / 当前任务
人对代码的掌握 高(每行都过目) 低(只看结果)
典型失败形态 补全不对,删掉重写 行为不对,整段重问

一句话总结:辅助编程里人是作者;vibe coding 里人是产品经理兼验收员。 角色变了,技能结构也要跟着变。

学术界的冷思考

2025 年 6 月,一篇 arXiv 论文在研究开发者使用 AI 生成代码的行为后,把这种状态命名为 material disengagement(对代码材料的疏离):开发者与代码这个"材料"脱钩了——不知道代码长什么样、为什么这样写、改哪里会出事。论文的结论并非"禁止 vibe coding",而是指出:疏离程度越高,交付质量的方差越大。本书后续所有方法,本质上都是在用工程手段控制这个方差,而不是否认这种新工作方式。

1.2 三种工作形态

按人机分工的深浅,AI 编码可以分成三级。注意:这不是能力鄙视链,而是适用场景不同;同一个工具,用出哪种形态取决于你怎么用它。

L1 · 问答 / 补全式

AI 是一个更聪明的搜索引擎加自动补全。你写代码,遇到不会的问一句,或让它补一段。代码的主导权、结构、质量完全在你手里。典型形态:编辑器内补全、单轮问答。适合:对代码质量要求高、AI 只是"外脑"的日常开发。

L2 · Vibe coding(对话生成)

你说需求,AI 生成整个文件甚至整个模块;你跑起来验证、看 diff(代码差异)、不满意就再对话。人把"写"交了出去,但"把关"仍然完全靠人肉——看 diff、跑应用、试边界。适合原型、脚本、学习实验。风险:把关质量完全取决于你读 diff 的耐心与能力,项目一大就会失效(下一节演示失效过程)。

L3 · Agentic engineering(代理工程)

人退到更上游,守四件事:

  • 规格驱动(spec-driven):先让 AI 产出并确认实现计划,再动手写码;
  • 测试护栏(test guardrails):用测试与 lint 定义"对"的标准,让 AI 自我验证,而不是每次都等人来验;
  • 边界约束(boundary constraints):用权限模式、hooks 限定代理能做什么、不能做什么;
  • 多代理编排(multi-agent orchestration):把大任务拆给多个代理并行或流水线完成,人只守检查点。

三级对照

维度 L1 问答 / 补全 L2 Vibe coding L3 Agentic engineering
人机分工 人写 AI 补 AI 写人验 人定规格与护栏,代理执行,人守检查点
质量来源 人的逐行把控 人对行为与 diff 的审查 规格 + 测试 + 权限边界 + 评审检查点
适用规模 片段、单函数 原型、脚本、小项目 中大型项目、长期维护、团队协作
主要风险 效率上限低 规模一大就失控 前期配置成本,需要学习方法论

本教程的目标:带你从 L2 走到 L3。注意,L3 不是"AI 用得更多、人用得更少",而是人把注意力从代码材料上移到规格与边界上——这是 material disengagement 唯一健康的打开方式。

快速自检:你现在在哪一级

  • 你提交的代码里,AI 生成的一半以上吗?不是 → 你还在 L1;
  • 你读 diff 的时间多于写需求描述的时间吗?是 → 你在 L2 的健康区;最近开始跳过 diff → 你已进入 L2 危险区;
  • 你最近一次让代理"先出计划、确认后再动手"是什么时候?从没发生过 → L3 对你还不存在。

自检结果没有高低贵贱:写一次性脚本时停在 L2 完全正确,做长期项目时还赖在 L2 就是事故前兆。

1.3 为什么"能跑就行"不够

Demo 到产品的距离

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 章反复出现。

崩坏的根因不是 AI,是缺了三样东西

  1. 缺验证:没有测试,"改好了"只能靠手点,点不全也点不快;
  2. 缺约束:没有类型与 lint,AI 的自由度没有下限;
  3. 缺记忆:没有项目约定文件,每次对话 AI 都在重新猜你的项目。

这三样东西,分别对应本教程的任务工程(Part 5)、边界与安全(Part 4)、项目配置(Part 2)。换句话说,崩坏不是"AI 不行"的证据,而是"没给 AI 立规矩"的证据。

读者常问:能不能先用 L2 跑起来,欠的债以后再还?可以,这正是 1.4 的"原型毕业制"。危险的不是欠债,而是不知道自己欠了债——没有验证、约束、记忆三件套,你连账本都没有。

1.4 适用与不适用:决策清单

建议直接 vibe(L2 就够)的场景

  • 原型探索:验证一个交互点子,给老板或客户看方向;
  • 一次性脚本:数据清洗、格式转换、跑一次就扔的任务;
  • 学习实验:学一个新库、复现一篇教程、做编程练习;
  • 个人小工具:只有你自己用,坏了重新生成一个就好;
  • 草稿类产物:测试用例初稿、文档草稿、示例代码。

必须上工程化(L3)的场景

  • 生产核心链路:出事故会直接影响用户或收入;
  • 涉及资金与隐私:支付、认证、个人信息处理——一次密钥泄漏或注入漏洞就够喝一壶;
  • 长期维护的团队代码:半年后接手的人(很可能是一个 AI 代理)要能看懂;
  • 多人或多代理并行开发:没有边界约束会互相踩文件。

动手前的五个问题

  1. 这段代码出 bug 会造成真实损失(钱、数据、信任)吗?
  2. 它会被维护超过一个月吗?
  3. 会有除你之外的人(或代理)继续改它吗?
  4. 它处理敏感数据或调用付费资源吗?
  5. 出问题时你需要快速定位原因吗?

任何一个答"是",就别裸奔。这份清单的用途不是吓退你,而是把"在哪里花纪律"变成一个显式决策——纪律是稀缺资源,要花在刀刃上。

中间地带的处理原则

大量任务落在中间:内部工具、活动页、阶段性版本。三条原则:

  • 原型毕业制:允许 vibe 出原型,但转正前必须补测试、补输入校验、过一遍安全检查,否则不进主干分支;
  • 把 vibe 圈在叶子里:核心域模型与接口先按工程化标准写好,让 vibe 只发生在叶子模块(页面、脚本、文案);
  • 人守关键 diff:可以不逐行读,但涉及鉴权、支付、数据删除的 diff 必须人工过目。

以"给 BookNest 加一个实验性的阅读热力图组件"为例:组件本身是叶子模块,可以放心 vibe;但它依赖的数据聚合接口属于核心链路,必须先按工程化标准补齐校验与测试,再让组件自由发挥。顺序很重要:先地基后叶子,而不是反过来。

1.5 本教程地图:五堵墙与五个部分

本书开篇用"四堵墙"概括纯 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 1(第 1–3 章)打地基:认知(本章)、工具与环境(第 2 章)、与 AI 对话的基本功(第 3 章);
  • Part 7(第 21–23 章)做验收:两个贯穿项目全流程实战(从零构建 / 重构遗留项目),外加反模式大全。

另一个观察视角:五个部分各有明确的"落盘产物"——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

本书所有章节都围绕贯穿项目「轻读 BookNest」(个人读书笔记与书评应用,完整需求见附录 D)展开。本章不写代码,只做一个开局对比:同一份需求,L2 与 L3 的开工方式差别有多大。

动手建议:先把附录 D 全文保存为本地文件(例如 docs/requirements.md),之后与代理的对话都以"先读这个文件"开场——文件化的需求是 L3 的入场券,第 4 章之后它还会升级为仓库里的常驻上下文。

L2 式:一句话开工

你:帮我做一个读书笔记应用,能记书、写笔记、打分,用 React 做,越快越好。

AI:好的!我用 React + localStorage 给你生成了完整应用,包含书架、笔记、评分三个页面……

十分钟后你得到了一个能跑的单体应用,看起来效率碾压。但它埋了这些雷:

  • 存储用了 localStorage——与"数据在自己手里、可全量导出"(FR-5)不符,容量也受限;
  • 没有输入校验与统一错误格式(违反 NFR-1),评分可以填 99;
  • 没有测试、没有类型(违反 NFR-5),后面每章实战都在还这笔债;
  • AI 自作主张的技术栈与需求书 D.4 完全对不上,返工只是时间问题。

一句话 prompt 的问题不是"结果差",而是需求根本没有被表达——AI 只好替你猜。

L3 式:结构化开工

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 是工作方式:工具只是载体,规格与护栏靠人来建

最佳实践

  1. 动手前先分级:这个任务是 L1、L2 还是 L3?30 秒的判断省掉几小时的返工;
  2. 需求先落成文字文件(哪怕只有 10 行)再喂给代理,别只靠口头描述;
  3. 把"验证标准"连同需求一起交给代理:怎样算做完,让它自己先验一遍;
  4. 敏感 diff(鉴权、支付、数据删除、依赖变更)永远人工过目;
  5. 原型转正必须"毕业":补测试、补输入校验、过安全检查后才进主干分支;
  6. 依赖一律以安装结果为准:包名让代理当场用 npm view 核实,杜绝幻觉依赖;
  7. 对崩坏信号保持敏感:改 A 坏 B、反复解释同一问题、字段名混乱——出现任何一条就停下来补护栏,而不是继续堆功能;
  8. 向团队推广时,先统一"什么场景允许 vibe"的边界,再谈工具选型;
  9. 每完成一个里程碑,回头更新需求与约定文档,让 AI 的"记忆"随项目一起生长。

本章小结

vibe coding 是 2025 年由 Karpathy 命名的一种对话式、凭直觉、少逐行审查的编码方式;学术界用 material disengagement 提醒我们它与代码材料脱钩的风险。它不是贬义词,而是一个分级明确的工作谱系:L1 补全问答、L2 对话生成、L3 代理工程。无纪律的 L2 在真实项目里必然撞上改 A 坏 B、依赖幻觉、技术债雪球;而 L3 用规格、测试、边界与编排把质量方差重新管起来。本教程的全部内容,就是带你完成从 L2 到 L3 的迁移。

  • 三种形态的核心差别是人守在哪一层:代码行、行为,还是规格与边界;
  • "能跑就行"只对一次性产物成立,五个问题帮你快速判定场景;
  • 五堵墙(上下文 / 沉淀 / 边界 / 能力 / 协同)对应 Part 2–6 五个部分,是全书骨架;
  • 中间地带用三原则处理:原型毕业制、vibe 圈在叶子里、人守关键 diff;
  • 贯穿项目 BookNest 的正确开局是"先读需求、出计划、等人确认",而不是直接生成代码。

动手练习

练习 1–3 可在半小时内完成;练习 4 的产物请妥善保存,第 2 章会直接用到。

  1. 自我定位:回想你最近一次用 AI 写代码的经历,对照 1.2 的三级划分写下:当时处于哪一级?人守在哪一层?付出了什么代价?
  2. 崩坏复盘:找一个你自己"能跑就行"起手、后来失控的项目(没有就虚构一个),列出它命中了 1.3 节的哪几个崩坏信号,以及缺了"验证、约束、记忆"三样东西中的哪几样。
  3. 决策演练:对以下任务逐个用 1.4 的清单分级并说明理由——(a)给团队群写一个生日提醒脚本;(b)公司官网的登录模块;(c)个人博客的主题魔改;(d)读 CSV 生成周报图表的一次性脚本。
  4. 综合实践:通读附录 D 的「轻读 BookNest」需求书,然后把自己扮演成代理:手写(而不是让 AI 生成)你对 FR-1~FR-7 的复述,以及至少三个需要向需求方澄清的问题。这份文档会在第 2 章的实战里直接用到。

导航

📄 第 02 章 · Agent 工作台:工具、概念与环境搭建 02-agent-workbench.md

第 1 章把 AI 编码分成了 L1/L2/L3 三级,本章解决"用什么工具、懂哪些概念、怎么跑起来"三件事。读完并完成动手练习后,你的机器上会有一台可用的 AI 编码代理,和一个刚刚出生的 BookNest 仓库。

学习目标

  • 能解释 AI 编码代理与补全式工具的本质区别,说清 agent loop 的四个阶段;
  • 能按 CLI 型、IDE 型、异步云型对主流工具分类,并为给定场景做出选型;
  • 能用自己的话讲清上下文窗口、工具调用、权限模式、MCP、Skill、subagent、hooks 七个核心概念;
  • 能安装一款 CLI 代理,并在沙箱目录里跑通第一条指令;
  • 能读懂代理输出的关键信息:它读了什么、执行了什么、为什么;
  • 能在"质量档位 / 速度 / 价格"三角之间,做出符合任务价值的选择。

2.1 AI 编码代理是什么

从补全到代理的跃迁

补全式工具回答的是"下一个词是什么";聊天式助手回答的是"这段代码怎么写";而**代理(agent)**回答的是"这个任务怎么完成"——你给它一个目标,它自己拆解步骤、调用工具、检查结果、修正方向,直到任务完成或需要你决策。

判断一个工具是不是代理,就看它有没有闭环执行能力:不靠人来搬运中间结果,自己把"想法"变成"副作用"(写文件、跑命令),再根据副作用的反馈调整下一步。补全工具离开你的手就瘫痪,代理可以自己走很远。

Agent loop:观察 → 思考 → 行动 → 观察

所有编码代理的引擎都是同一个循环(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 秒该做的事。

2.2 工具地图

三大类型

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 为主线

三个原因。其一,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 里写码 + 终端里放代理跑长任务 + 云端代理清工单"。

2.3 核心概念速成

七个词决定你能不能看懂本书后面的所有章节。每个概念先给定义,再配一个类比,并标注它在本书哪一章展开。现在记不住没关系,它们会反复出现。

上下文窗口(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 章我们先从容器讲起。

2.4 动手搭建

第一步:安装 CLI 代理

以 ZCode 为例(其余工具同理,具体命令以各自官方文档为准):

# 全局安装 ZCode CLI
npm install -g @zcode/cli

# 登录:会引导浏览器完成授权,不要在终端里粘贴任何密钥
zcode login

# 验证安装
zcode --version

安全底线:任何正规工具都不会要求你把 API 密钥写进代码或对话。密钥只应存在于环境变量(例如 ZCODE_API_KEY,代码里用 process.env.ZCODE_API_KEY 引用)或官方登录流程中。

三个常见的安装问题,提前给出排查方向:

  • Node 版本过旧:CLI 代理普遍要求较新的 Node LTS(建议 22.x),node -v 先自查;
  • Windows 终端兼容:建议在 Git Bash 或 WSL 中运行,行为与 macOS / Linux 一致,本书示例也按此假设编写;
  • 公司网络受限:若需配置代理服务器,用环境变量注入,不要把代理地址连同内嵌凭据一起写进 shell 配置文件。

第二步:在沙箱目录跑第一条指令

不要一上来就在真实项目里试。建一个练习目录:

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 包描述文件,声明了项目名与元信息

第三步:读懂代理的输出

上面这段输出包含三层信息,请养成逐层阅读的习惯:

  1. 它读了什么(Read、ls 的结果)——这是它推理的依据,依据错了结论必错;
  2. 它做了什么(每次工具调用的命令与参数)——这是真正产生副作用的时刻,看懂了再放行;
  3. 它为什么这么做(工具调用之间的叙述文字)——这是它的计划,计划歪了及时纠正,比事后返工便宜十倍。

一个立刻可用的习惯:**看到 Bash(...) 里的命令,先默念一遍这条命令的后果,再按确认。**这个习惯在第 7、11 章会被权限配置固化下来,但肌肉记忆从今天开始练。

第四步:模型选择与成本意识

主流 CLI 代理都允许切换底层模型。选型是三个维度的权衡:

  • 质量档位:旗舰模型推理强、返工少;轻量模型快而糙;
  • 速度:轻量模型响应快,适合探索、问答、格式化类任务;
  • 价格:按 token 计费(输入加输出,部分服务对缓存命中的输入打折)。

经验法则:用任务的价值倒推模型档位——架构设计与复杂重构用旗舰档;问路、改名、写注释用轻量档;拿不准时用轻量档探路、旗舰档攻坚。另附三个省钱习惯:任务切换就开新会话(旧上下文会持续抬高每轮输入成本);长会话及时让代理总结状态或压缩历史(多数 CLI 提供 /clear、/compact 之类的内置命令);避免让代理无界重试——连续三次失败就该人来介入。

贯穿项目实战:初始化 BookNest 仓库

现在把第 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 章)

最佳实践

  1. 新工具、新指令一律先在沙箱目录试,形成条件反射;
  2. 每次确认前扫一眼工具调用三要素:读什么、做什么、为什么;
  3. 指令里同时给出"做什么"与"怎样算做好"(验收命令),代理会自我验证;
  4. 密钥只走环境变量与官方登录流程,永远不进代码、不进对话;
  5. 任务切换就开新会话,长会话及时让代理总结状态(第 3 章展开);
  6. 派任务先"只读"后"写入":先让代理理解项目,再让它动手修改;
  7. 模型档位跟着任务价值走,旗舰档留给架构设计与疑难杂症;
  8. 跑通的指令存进自己的 prompt 库,下次直接复用——这正是在为第 8 章的 Skill 做准备。

本章小结

本章把"AI 编码代理"从一个名词变成了你机器上可运行的现实:它是一个以 agent loop(观察 → 思考 → 行动 → 观察)为引擎、具备读写文件、执行命令、联网检索、自我修正四类能力的闭环系统。工具分 CLI、IDE、异步云三型;本书选 CLI 型为主线,因为它的能力最完整、扩展机制最透明、与工程化和多代理编排天然契合。七个核心概念(上下文窗口、工具调用、权限模式、MCP、Skill、subagent、hooks)是全书的词汇表,后续章节会逐一展开。最后,你在沙箱里跑通了第一条指令,并拿到了 BookNest 仓库的出生证明。

  • 代理与补全式工具的分界线:是否具备"行动 - 反馈 - 修正"的闭环;
  • 选型看四个维度:本地文件访问、权限控制、可扩展性、成本;
  • 读懂输出三要素(读什么 / 做什么 / 为什么)是人机协作的底线技能;
  • BookNest 骨架已就位:npm workspaces + 双端 Hello + README,下一章学"怎么跟它说话"。

动手练习

  1. 跑通第一条指令:完成 2.4 的沙箱练习,然后追加一个任务:"把 index.js 改成读取命令行参数并打印"。记录代理的每一步工具调用,并标注哪一步你确认得最快、哪一步你犹豫了。
  2. 概念转述:合上书,用自己的话各写两三句话解释 MCP 和 hooks,假设听众是不了解 AI 工具的同事。写完对照 2.3 检查有没有讲偏。
  3. 选型推演:为以下场景各选一类工具(CLI / IDE / 异步云)并说明理由——(a)批量把 30 个仓库的依赖升级到新的大版本;(b)整天手写业务代码,偶尔让 AI 生成样板;(c)为一个开源项目自动响应 issue 并提交修复 PR。
  4. 综合实践:完整走一遍贯穿项目实战——初始化 BookNest 仓库,让代理生成工程骨架与 README,跑通全部验证命令,并以 Conventional Commits 格式完成首次提交;最后把你使用的开工 prompt 原文保存为 docs/prompts/01-scaffold.md(这是你的第一份 prompt 资产,第 8 章会把它升级成 Skill)。

导航

📄 第 03 章 · 上下文工程:与代理高效协作的语言 03-context-engineering.md

核心观点:AI 编码代理的产出质量,主要不取决于模型有多聪明,而取决于它"看到"了什么。把"给模型看什么"变成一门可操练的手艺——这门手艺叫上下文工程(context engineering)。

学习目标

  • 能说清上下文窗口(context window)与"记忆"的区别,理解"会话结束即遗忘"对工作方式的影响;
  • 能用目标、约束、上下文、验收标准四要素,写出一条一次到位的高质量任务指令;
  • 能运用主动指路、噪音控制、分层供给三种技巧,高效向代理供给项目信息;
  • 能识别代理跑偏的早期信号,并用"复现步骤 + 期望行为 + 已尝试项"模板给出有效反馈;
  • 能识别长会话性能衰退的迹象,主动收尾并用交接文档(handoff)开启干净的新会话;
  • 能为贯穿项目 BookNest 的 FR-1 写出一条可直接开工的优质提示词(prompt)。

3.1 上下文决定一切

3.1.1 模型只"看到"窗口里的东西

大语言模型没有任何"偷偷了解你项目"的渠道。每次生成回复,它能依赖的全部信息,就是此刻上下文窗口里的内容,通常由四部分组成:

  • 系统提示(system prompt):工具方预设的行为规则,比如如何调用工具、如何对待危险操作;
  • 项目指令:如 AGENTS.md 等项目上下文文件(第 04 章的主角);
  • 对话历史:你说过的话、代理回复过的内容;
  • 工具调用结果:代理读过的文件内容、执行命令的输出。

窗口容量以令牌(token)计量——粗略估算,1 个 token 约合半个到一个汉字,或四个英文字符。2026 年主流编码模型的窗口普遍达到 200K–1M token,看起来"能装下半个仓库",但有两个反直觉的事实:

  1. 装得下 ≠ 看得清。模型的注意力(attention)会被稀释:同时塞进 50 个文件,它对每个文件的关注都会变浅,关键约束更容易被漏掉。这和人类"资料堆成山反而看不进去"是同一个道理。
  2. 窗口 ≠ 记忆。上下文窗口是一次会话内的工作记忆(类比内存),不是硬盘(类比磁盘)。会话结束、窗口清空,模型对你们之间发生过的一切归零。

3.1.2 垃圾进,垃圾出

代理不会质疑你给的上下文,它默认收到的一切都是真的。于是"垃圾进,垃圾出"(Garbage In, Garbage Out)在 AI 编码里体现得比传统编程更彻底:

你喂进去的"垃圾" 代理的典型反应
过期注释:"TODO: 之后迁移到 GraphQL" 真的开始按 GraphQL 方向做设计
互相矛盾的约定(文档说 REST,旧代码是 RPC 风格) 随机站队,或各写一半
贴了 3000 行无关日志 答非所问,抓着日志里的细枝末节发挥
半截代码片段(缺 import、缺类型) 补全方式随机,风格与项目不符

反过来,高质量上下文会稳定地把代理推向正确答案。记住这句话:上下文的质量上限,就是代理输出的质量上限。

3.1.3 什么是上下文工程

上下文工程(context engineering):围绕"模型此刻能看到什么"这个第一性问题,系统性地决定——给什么、不给什么、什么时候给、以什么形式给。

它比提示词工程(prompt engineering)的范围更大:单条指令怎么写只是其中一环,还包括文件怎么供给、会话怎么管理、约定怎么固化进项目配置。本教程后续的很多内容——第 04 章的项目上下文文件、第 05 章的记忆体系、第 16 章的长任务状态管理——本质上都是上下文工程在不同时间尺度上的应用。

3.2 高质量指令的四要素

一条高质量的开工指令,几乎总能拆出四个要素:目标(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 就合格了。

3.3 上下文供给技巧

四要素解决"单条指令"的质量;本节解决"项目信息"怎么喂。三种技巧:主动指路、噪音控制、分层供给。

3.3.1 主动指路 vs 自主探索

两种策略各有适用场景:

  • 自主探索:适合"我不熟、你来看"的场合(接手陌生代码库、做整体审查)。缺点是慢,且可能迷路;
  • 主动指路:适合目标明确的开发任务。你指出的每个相关文件,都替代理省掉一段弯路。

ZCode 支持在输入中用 @ 引用文件或目录(Claude Code、Cursor 等也有等价的 @ 引用机制);另一种更通用的句式是"先读 X 再动手":

先读 server/src/db/schema.sql 和 server/src/routes/notes.ts,
理解现有的表结构与 REST 约定后,再动手实现 books 路由。

"先读再动手"的价值不只是省 token——它强迫代理先对齐现状、再输出方案,大幅降低"凭想象写代码"的概率。

3.3.2 控制噪音

注意力是稀缺资源。无关的大文件、整屏的日志、成段贴出的报错,都会稀释代理对关键信息的关注。三个实用做法:

  1. 不要整屏贴日志。只贴关键报错行与前后几行;更好的做法是让代理自己取证据:"运行 npm test,读完失败的输出再修";
  2. 别把无关大文件拉进对话。@ 引用要克制:引 schema 和参考实现即可,不要引构建产物、生成文件或整目录;
  3. 临时调试信息用完即弃。会话里贴过的临时内容,任务结束就该让它们离开上下文——最简单的办法是开新会话(见 3.4.4)。

3.3.3 分层供给:先地图,后街道

新手常犯两个相反的错误:要么开场就贴十几份文件(信息过载),要么全程不给任何背景(代理瞎猜)。正确姿势是分层:

  • 第一层(开场):一句话架构 + 目录树 + 本次任务范围,让代理先拿到"地图";
  • 第二层(任务中):按需给出具体文件、schema、样例数据等"街道级"细节;
  • 第三层(返工时):只针对偏差点补充增量信息,而不是把之前的内容重贴一遍。
【第一层示例】
项目是 Express + SQLite 的读书笔记应用,目录:
server/src(routes 路由 / services 业务 / db 数据库)、web/src(React SPA)。
本次任务只涉及 server/src,先从 books 路由开始。

3.4 迭代节奏

上下文工程不是"一次写好就完",而是不断校准的循环。节奏错了,再好的 prompt 也救不回来。

3.4.1 小步快跑,每步验证

把任务切成可独立验证的小片,每一片走完"生成 → 验证 → 下一步"的循环:

推荐节奏:
第 1 步:schema 与迁移脚本          → 验证:迁移能跑通
第 2 步:POST /books 接口 + 校验    → 验证:curl 一发 + 单测通过
第 3 步:GET /books 列表 + 搜索     → 验证:单测 + curl
第 4 步:前端表单与列表页           → 验证:浏览器走一遍主流程

反面教材是让代理"一口气把前后端全做完":得到 20 个文件的大改动,其中任何一处理解偏差,都会被后续十几处改动放大。

3.4.2 跑偏即停,重新对齐

代理一旦开始往错误方向走,让它继续跑只会浪费 token,并制造更多要回滚的代码。正确动作:

  1. 立即中断(ZCode 中按 Esc 停止当前生成);
  2. 三句话重新对齐:重申目标、指出偏差、说明哪些保留哪些丢弃。
停一下。目标仍然是"GET /books 支持关键字搜索",但你把搜索逻辑写进了
middleware,这不符合我们的分层(业务逻辑应放 services)。
保留你写的 SQL 参数化部分,把查询逻辑移到 services/books.ts。

最忌讳的反馈是连说"不对,重写"——那等于让代理随机再猜一个方向,猜中的概率并不会更高。

3.4.3 "改 A 坏 B"时,如何给有效反馈

修好一个问题却弄坏另一个,是代理编码的高发场景。有效的反馈模板是四段式:

【复现步骤】1. npm run dev;2. 打开 /books 页;3. 点击任一书籍的"编辑"
【期望行为】弹窗内出现该书现有信息,修改后保存成功
【实际行为】弹窗空白,浏览器控制台报 "Cannot read properties of undefined"
【已尝试】重启 dev server 无效;直接访问 /books/3 详情页数据正常,
          说明接口没问题,问题出在编辑弹窗的取数环节

四段各有用途:复现步骤让代理能自己验证;期望行为定义"对";实际行为定义"错";已尝试项帮它排除死路、缩小搜索范围。最后一项最常被忽略,但它最能避免代理把你已经排除的方案再试一遍。

3.4.4 何时应该推倒重来,开新会话

出现以下任一信号,就值得结束当前会话:

  • 同一个错误纠正两次后仍然再犯;
  • 开始忘记已确认的约定(又用上被你否掉的方案);
  • 窗口里堆满失败尝试、报错日志与回滚记录;
  • 任务话题切换到了不相干的区域。

开新会话不是"从头再来":带上三样东西即可无缝衔接——一份交接摘要(见 3.5)、本次任务的目标与验收标准、需要优先阅读的文件清单。干净的上下文加明确的摘要,几乎总比一个被污染的"长寿会话"更快。

3.5 长会话管理

3.5.1 识别性能衰退信号

会话越长,代理表现越可能滑坡。典型信号:

  • 重复犯错:同一个坑掉两次以上;
  • 忘记约定:明明前面说过"不引入新依赖",它又开始执行安装命令;
  • 答非所问:你要它改样式,它动起了数据层;
  • 响应变慢、变贵:历史越长,每次请求携带的 token 越多。

衰退不是模型的"性格缺陷",而是上下文里沉积了太多过时信息与失败尝试的必然结果。

3.5.2 主动收尾:让代理写交接文档

不要等到会话崩溃才被迫开新会话。在状态良好时主动收尾,让代理自己生成交接文档(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,继续完成未完成事项。"

3.5.3 向第 16 章埋下的伏笔

本章的手工流程——识别衰退、写 handoff、新会话续作——已经覆盖了长任务管理的核心思想。第 16 章(长任务与状态管理)将把它工程化:用待办清单(todo list)追踪进度、用里程碑切分会话、用上下文压缩(compaction)延续超长任务。现在先把肌肉记忆练出来,届时只是换更趁手的工具。

贯穿项目实战:轻读 BookNest

目标:为 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 为空时,表单与接口都能正常工作。
请先列出你计划修改的文件清单,我确认后再动手。

第二步,逐要素复盘它为什么这样写:

  • 目标:给出了字段清单、状态枚举值与 API 落点——代理不需要猜"状态"存中文还是英文(这是高频翻车点);
  • 约束:锁死技术栈与依赖(防止自作主张引入 ORM 或 UI 库)、锁死改动范围(防止顺手重构)、复用现有校验(防止另起炉灶);
  • 上下文:三个"先读"分别指向 schema、路由风格、请求层——用最小的信息量完成现状对齐;
  • 验收标准:全部可执行——两条命令、一次 curl、一个边界用例(空封面 URL),完成与否不需要主观判断;
  • 最后一行"先列文件清单"是 3.4.1 小步快跑的开关:清单确认本身,就是你第一次纠偏的机会。

第三步,看一个反例及其恶果:

帮我给 BookNest 加上书籍管理功能,要能增删改查,顺便把样式做好看点,
尽量做得完善一些。
缺失要素 随之而来的恶果
目标模糊("完善一些") 代理擅自实现封面图片上传到本地磁盘——需求只要一个 URL 字段
没有约束 执行依赖安装引入 ORM 和整套 UI 组件库,与项目"不新增依赖"原则冲突
没有上下文 没读 notes.ts,自创一套路由风格和错误格式,与现有代码格格不入
没有验收标准 声称"完成",实际是 15 个文件的 diff、零测试、构建报错

同一件事,好坏 prompt 的差距不是"表达水平",而是返工次数:前者通常一轮通过,后者往往三轮起步。

常见误区

误区 典型现象 纠正方法
把上下文窗口当记忆 新会话里代理"忘了"上次的约定,你被迫从头解释 约定固化到 AGENTS.md 等项目文件(第 04 章),不依赖聊天历史
一条 prompt 塞十个需求 代理每件都浅尝辄止,或只做了前两件 一个会话一个主题;按验收标准切成小步交付
贴巨量日志当"证据" 响应变慢变贵,代理抓着无关细节发挥 只贴关键报错行,或让代理自己运行命令取证据
跑偏后继续"再试一次" 越修越乱,token 烧光,diff 无法收拾 立即中断,按"目标—偏差—保留项"重新对齐
反馈只有"不对,重写" 代理随机换方向,可能换到更离谱的地方 用"复现步骤 + 期望行为 + 实际行为 + 已尝试项"四段式
验收标准缺失或不可执行 代理说"做完了",你不知道怎么验 每条任务配可执行验收:命令、预期输出、边界用例

最佳实践

  1. 开工先给"地图":一句话架构 + 目录树 + 任务范围,再进入细节;
  2. 目标明确的任务主动指路:"先读 X 再动手"是最便宜的对齐手段;
  3. 每个会话聚焦一个主题;话题要切换时,宁可开新会话;
  4. 验收标准必须可执行——写命令和预期输出,不写"质量高一些";
  5. 大改动前先要文件清单或计划,确认后再放行(计划模式的深度用法见第 15 章);
  6. 跑偏即停:中断比补救便宜,重新对齐比回滚便宜;
  7. 反馈给事实与约束,不给情绪;永远附上"已尝试项";
  8. 长会话在状态良好时主动收尾,让代理写 handoff 后再开新会话;
  9. 反复出现的项目约定,尽快从"聊天里说"升级为"文件里写"(第 04 章)。

本章小结

本章建立了一个核心心智模型:模型只能看到上下文窗口里的内容,窗口会清空、注意力会稀释,所以"给什么信息"与"怎么给"直接决定产出质量。四要素(目标、约束、上下文、验收标准)让单条指令一次到位;主动指路、噪音控制、分层供给三种技巧解决项目信息的供给;小步快跑、跑偏即停、四段式反馈构成健康的迭代节奏;而识别衰退信号并主动交接,是长会话的生存技能。下一章我们把"每次都要重复的项目约定"固化进项目上下文文件,从根上消除本章反复出现的痛点。

  • 模型只见窗口:垃圾进垃圾出,会话结束即遗忘;
  • 四要素自检:做什么?不做什么?你都知道什么?怎样算做完?
  • 供给三技:主动指路、控制噪音、分层供给;
  • 节奏三招:小步验证、跑偏即停、结构化反馈;
  • 长会话:识别衰退信号,主动 handoff,干净续作。

动手练习

  1. 重写旧 prompt:找出你最近一次让 AI 编码"翻车"的指令,按四要素重写它。对照 3.2 的四问自检,列出原指令缺了哪几环。
  2. 噪音对照实验:同一个小任务做两遍——第一遍只给相关文件路径,第二遍额外贴入三个无关大文件的内容。对比两次输出的准确度与耗时,写下观察结论。
  3. 反馈急救:假设代理修好了一个空指针崩溃,却把 /books 列表页的排序弄丢了。按 3.4.3 的四段式模板写出你将给出的完整反馈。
  4. 综合实践(BookNest):为 FR-3(评分与标签:1–5 星评分、自由标签、按标签筛选)写一条四要素齐备的开工 prompt,在 ZCode 中实际执行。记录:一次通过率、你中断重对齐的次数、最终 diff 行数,并把这份记录与第 1 题的旧 prompt 做对比。

导航

📄 第 04 章 · 项目上下文文件:AGENTS.md 与 CLAUDE.md 04-agents-md.md

上一章的结论是:代理只能看到窗口里的内容,会话结束即遗忘。那么"项目用什么技术栈、命令怎么跑、哪些文件不能碰"这些每次都要重复的事,就应该写进一个代理开机即读的文件。这个文件就是本章主角:AGENTS.md。

学习目标

  • 能解释项目级指令文件解决什么问题,以及 AGENTS.md 为何能跨工具通用;
  • 能正确决定指令文件的安放层级(仓库根 / monorepo 子目录 / 用户全局)与加载合并规则;
  • 能按"简介、技术栈、命令、约定、目录、禁区"六类内容,为项目写出合格的指令文件;
  • 能识别 wiki 化、文档腐烂等反模式,并用"短、准、可执行"三原则修剪内容;
  • 能用"代理提问法"从零起草一份 AGENTS.md,并按检查点评审它;
  • 能为 BookNest 落地根目录与 web/ 子目录两份成品文件。

4.1 为什么需要项目级指令

4.1.1 每次重复解释的痛苦

回想第 03 章的结论:会话结束即遗忘。没有项目级指令文件时,每个新会话你都要重新交代一遍:

我们用 Express 5 和 better-sqlite3,SQL 必须参数化,错误格式统一为
{ code, message },测试用 Vitest,不要动 migrations 里已发布的文件……

这段话的代价远不止"多打几行字":

  • token 成本:每个会话重复付费,团队里每个人再各付一遍;
  • 一致性风险:口头交代总有遗漏,代理今天记住、明天忘掉;
  • 协作壁垒:这些约定只存在于某个人的聊天记录里,同事接手时无从知晓。

4.1.2 一次写好,开机即读

项目上下文文件打破了这个循环:把约定写进仓库里的一份 Markdown 文件,代理每次启动会话时自动读取。你只需要维护文件本身,"向代理解释项目"的边际成本降为零。

4.1.3 AGENTS.md:跨工具的事实标准

AGENTS.md 是一份纯 Markdown 的开放约定,正迅速成为跨工具的事实标准(de facto standard):

  • ZCode、Codex CLI 等命令行编码代理原生读取仓库根目录的 AGENTS.md;
  • Claude Code 使用 CLAUDE.md,内容组织方式与 AGENTS.md 高度互通;CLAUDE.md 还支持 @路径 导入语法——写一行 @AGENTS.md 即可引用同一份文件,避免两份内容漂移;
  • Cursor、Gemini CLI 等也已支持或兼容这一约定。

结论:团队共同约定写在 AGENTS.md 里,是当前兼容性最好的选择;个人全局偏好与工具私有配置再用各自机制补充(第 05 章展开)。

4.2 放哪里、怎么生效

4.2.1 三个层级

层级 位置 该放什么 是否进 Git
项目根 <repo>/AGENTS.md 项目事实:技术栈、命令、约定、禁区 是(团队共享)
子目录 <repo>/web/AGENTS.md 等 该子目录特有的补充与覆盖 是
用户全局 ~/.zcode/AGENTS.md(Claude Code 为 ~/.claude/CLAUDE.md) 个人跨项目偏好:回复语言、提交习惯等 否(个人)

4.2.2 monorepo 的分层叠加

BookNest 这类 monorepo(server + web 两个工作区)正是子目录层级的典型用户:

  • 代理读取或修改 web/ 下的文件时,根目录与 web/AGENTS.md 叠加生效;
  • 子目录文件适合写"只对本目录有意义"的约定:组件命名、状态管理方式、样式规则;
  • 判断标准很简单——**这条约定换个目录还成立吗?**成立就放根目录,不成立就放子目录。
booknest/
├── AGENTS.md          # 全局事实:技术栈、命令、总体约定、禁区
├── server/            # (约定不多时可以不单设文件)
└── web/
    └── AGENTS.md      # 前端增量:组件、状态、样式约定

4.2.3 加载与合并顺序

各工具的实现细节略有差异(请以所用工具文档为准),但方向一致:

  1. 会话启动时,加载系统提示与用户全局文件;
  2. 进入项目后,加载仓库根的 AGENTS.md;
  3. 当操作深入某个子目录时,叠加该子目录的 AGENTS.md;
  4. 多层内容冲突时,越靠近当前工作文件的层级优先(就近原则)。

由此推出两条纪律:

  • 全局层只放个人偏好,别写项目细节——否则它会污染你的所有项目;
  • 子目录文件只写增量,重复根文件内容必然造成两处漂移。

4.3 应该写什么

六类内容,每类配一段 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.*

4.4 不该写什么(反模式)

4.4.1 三大反模式

  1. 整本 wiki 贴进来。几千字的项目背景、设计史、会议纪要——这些内容每次会话都会进入上下文:行数即成本,还会稀释代理对关键约定的注意力(第 03 章)。背景细节应放 docs/,AGENTS.md 里只留一行链接;
  2. 写代码里已有的信息。依赖版本号、完整命令清单、每个文件的内容概述——这些信息代码里就有权威来源,一旦依赖升级,文档就开始说谎。这叫文档腐烂(doc rot):过时的文档比没有文档更危险,因为代理会坚信它;
  3. 长篇散文式表述。"我们希望代码质量始终保持在较高水平"——代理无法执行一句感受。改写成规则:"PR 前必须通过 lint 与全部测试"。

4.4.2 短、准、可执行三原则

  • 短:目标控制在 30–60 行。每加一条都问:它值得每次会话占据注意力吗?
  • 准:与仓库现状一致。宁可短不可错——一条错误约定的危害,远大于少写十条正确约定;
  • 可执行:命令、规则、清单,而非感受与愿景。检验方法:代理能照着这条做出一个可判定的动作吗?

4.4.3 约定的保鲜

  • 约定变了就更新文件;把"同步更新 AGENTS.md"写进任务的完成定义(Definition of Done);
  • 每个里程碑结束花 10 分钟通读一遍,删掉过期条目;
  • 当代理指出"文档与代码冲突"时,先修文档再继续编码——这往往意味着有人改了约定没留痕。

4.5 从零起草

4.5.1 30 行起步模板

新项目不要追求一步到位,从这个模板起步,用起来再长:

# AGENTS.md — <项目名>

## 简介
<一句话:这是什么、给谁用、核心能力>

## 技术栈
<语言/框架/数据库/测试框架,只写影响写法的关键项与硬性版本>

## 常用命令
- 开发:npm run dev
- 测试:npm test
- 单个测试文件:npm test -- <path>
- Lint:npm run lint

## 代码约定
<3–5 条最重要的规则:命名、错误处理、安全要求>

## 禁区
<3–5 条:不许动的路径、不许引入的依赖、不许跳过的检查>

4.5.2 代理提问法

不想自己憋?让代理反过来采访你。在项目根目录开一个会话:

请先浏览这个仓库的结构和 package.json,然后向我提出 5–8 个问题,
你的任务是收集足够的信息,为这个项目起草一份 AGENTS.md。
问完后给出不超过 40 行的草稿。

它会问出类似这样的问题:"错误响应有没有统一格式?""迁移脚本的命名规则是什么?""哪些目录是生成产物不能改?"——这些问题恰好就是模型最需要知道的事,因为提问者就是消费者。你逐条回答后得到草稿,再做人工评审。这比闭门造车快得多,也准得多。

4.5.3 评审一份 AGENTS.md 的检查点

  • [ ] 每条命令都亲手跑过一遍吗?
  • [ ] 禁区具体到路径与动作了吗?
  • [ ] 有没有与 package.json 或代码重复的易变信息?
  • [ ] 超过 60 行了吗?若有,删掉最弱的一条试试;
  • [ ] 新会话测试:让代理"读完 AGENTS.md 后复述将遵守的约定",核对有无漏读或误读。

贯穿项目实战:轻读 BookNest

目标:落地两份成品文件——仓库根 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 里的版本与命令 依赖升级后文档说谎,代理按旧版本写代码 只写关键选型与硬约束,易变信息指向权威来源
全局文件里写项目细节 个人偏好污染所有项目,换台机器行为不一致 全局层只放个人习惯;项目事实进仓库共享
约定改了文件没更新 代理坚持过时约定,产出与代码库冲突 "宁可短不可错";把更新文件写进完成定义
子目录文件重复根文件内容 两处漂移、互相矛盾,代理随机听谁的 子目录只写本目录特有的增量与覆盖
禁区只写"不要乱来" 代理照样动它不该动的文件 禁区具体到路径与动作,一条一个行为

最佳实践

  1. 从 30 行起步,跑过几个真实任务后再增补;
  2. 每条约定都应能被一次命令或一次 diff 验证;
  3. 命令必须真实可跑——写完自己执行一遍,代理会照着跑;
  4. 禁区清单具体到路径与动作,一条一个行为;
  5. monorepo 分层放置:子目录只写增量,避免两处漂移;
  6. 团队共享的进 Git,个人偏好进全局层(且不进 Git);
  7. 用"代理提问法"起草,用人工评审收尾,用新会话复述法验收;
  8. 把"同步更新 AGENTS.md"写进任务的完成定义(Definition of Done);
  9. 每个里程碑花 10 分钟修剪过期内容,保持"宁可短不可错"。

本章小结

项目上下文文件把第 03 章反复出现的痛点——"每个会话都要重新解释项目"——变成一次性的配置投入。AGENTS.md 已成为跨工具的事实标准,Claude Code 用户用 CLAUDE.md 承载同样的内容约定;文件分仓库根、子目录、用户全局三层安放,冲突时就近优先。内容上坚持六类(简介、技术栈、命令、约定、导览、禁区)与"短、准、可执行"三原则,警惕 wiki 化与文档腐烂两大反模式。起草时用"代理提问法",评审时用检查点,保鲜靠完成定义与定期修剪。至此,代理已经"开机即懂"你的项目;下一章讨论规则与记忆体系——把团队约定与个人偏好进一步分离,并让代理拥有跨会话的记忆。

  • 三层安放:仓库根(团队事实)、子目录(增量)、全局(个人偏好);
  • 六类内容:简介、技术栈、命令、约定、导览、禁区;
  • 三原则:短、准、可执行;两大反模式:wiki 化、文档腐烂;
  • 起草靠代理提问法,验收靠新会话复述,保鲜靠完成定义。

动手练习

  1. 为你自己的项目起草:任选一个你维护的项目,用 4.5.1 的模板写出不超过 30 行的 AGENTS.md,其中禁区至少 3 条、命令至少 4 条(含"跑单个测试文件")。
  2. 代理提问法对比:对同一项目执行 4.5.2 的采访流程得到第二版草稿。对比两版,把更优的条目合并回你的版本,记录"代理发现了而你漏掉"的条目。
  3. 复述验证:把合并后的文件提交进仓库,开一个全新会话让代理复述将遵守的约定(4.5.3 最后一项),核对有无漏读或误读,并据此修订文件。
  4. 综合实践(BookNest):把本章两份成品文件落进你的 BookNest 仓库并提交;然后开新会话,用第 03 章的四要素 prompt 让代理实现 FR-1 的前端表单页。观察它是否遵守子目录约定(不引入 UI 框架、状态走 TanStack Query),记录所有偏差,再把对应的约定改写得更具针对性。

导航

📄 第 05 章 · 规则与记忆体系:分层治理代理的行为 05-rules-and-memory.md

第 04 章解决了"开机即懂":一份 AGENTS.md 让代理开机即掌握项目事实。但用起来你会发现两个新问题:其一,约定开始分家——"SQL 必须参数化"属于团队,"回复用中文"只属于你自己,混在一个文件里会互相污染;其二,代理在会话中"学到"的东西(你的纠正、你的习惯),会话一结束又丢了。本章给出解法:用分层规则回答"谁该遵守什么",用记忆系统让代理"越用越懂你",并守住两者的边界。

学习目标

  • 能说出指令四层模型(全局个人层、项目共享层、项目个人层、子目录层)各自的定位与加载优先级;
  • 能用"换个人、换台机器还成立吗"的判据,把一条约定正确放进共享层或个人层;
  • 能解释自动记忆与 AGENTS.md 的分工(工作笔记 vs 宪法),并完整执行一次"记忆晋升";
  • 能识别并清理错误记忆,避免代理被过时或片面的信息误导;
  • 能为跨会话协作选对持久化手段:交接文档、决策日志、任务进度文件;
  • 能为 BookNest 配齐三层规则文件,使团队规则与个人偏好各归其位。

5.1 指令的分层模型

5.1.1 四层结构

第 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 中引用,被引用的文件随主文件一并加载。按需拆分,不要一开始就建目录。

5.1.2 加载顺序与优先级

会话中四层的加载顺序:

  1. 会话启动,先加载全局个人层;
  2. 进入项目,加载项目共享层(含规则目录中被引用的文件);
  3. 叠加项目个人层;
  4. 操作深入某个子目录时,再叠加子目录层。

多层内容冲突时,越具体、越靠近当前工作文件的层级优先(就近原则,与第 04 章一致):

子目录层 > 项目个人层 > 项目共享层 > 全局个人层

一个冲突示例:全局层写着"我习惯先写测试再实现",项目共享层写着"原型验证类任务可先实现、当天补测试"——在这个项目里,代理应以共享层为准,因为它更具体、且代表团队对"完成"的定义。

由此得到两条纪律:

  • 全局层只放个人偏好——它会被你所有项目读取,写一条项目事实就污染所有项目;
  • 共享层只放团队共识——它会被所有同事读取,写一条个人口味就冒犯所有人。

5.1.3 什么内容放哪层:决策表

内容示例 该放哪层 判断依据
回复用中文、先结论后展开 全局个人层 纯个人沟通偏好,与项目无关
我习惯 TDD(先写测试再实现) 全局个人层 个人工作流,不该强加给同事
SQL 必须参数化、错误格式 { code, message } 项目共享层 团队质量底线,对谁都成立
新增路由必须附带接口测试 项目共享层 团队流程约定(本章实战的晋升示例)
web/ 组件命名与状态管理方式 子目录层 只在前端目录成立
我本机 dev 端口改用 5174(5173 被占) 项目个人层 只与本机环境有关
本机数据库文件的绝对路径 项目个人层 机器相关,进 Git 必错
密钥一律读 process.env.* 项目共享层 安全硬约束,全团队一致

一个补充判据:能交给工具的,别写进任何层。缩进、引号、行宽这类纯格式问题,交给 Prettier/ESLint 的配置文件(进 Git)解决,规则文件只写工具管不了的事——机器能强制的,就不要靠代理自觉。

5.2 团队共识与个人偏好分离

5.2.1 一条判据

换一个同事、换一台机器,这条规则还应该生效吗? 是——进项目共享层;否——进个人层(全局个人层或项目个人层)。

两类内容要区别对待:

  • 团队共识定义"什么算完成":测试要求、错误处理、安全约束、提交规范;
  • 个人偏好定义"我怎么顺手":沟通风格、工作流习惯、本机环境。

对照几个易混的例子:

看起来像 实际是 原因
"提交信息要写清影响范围" 团队共识 评审者要读,对所有人生效
"我说话喜欢极简,别寒暄" 个人偏好 只影响与"我"的会话体验
"测试覆盖率 ≥ 80%" 团队共识 来自需求书 NFR-1,是质量底线
"我喜欢先看 diff 再跑测试" 个人偏好 个人工作节奏,无对错

5.2.2 从 PR 评审视角看污染的危害

假设你把"我喜欢每行不超过 80 字符"写进了团队的 AGENTS.md。从评审视角看,这行字造成四重伤害:

  1. 强加于人:同事的代理开始按你的口味产出代码,PR diff 里充满与功能无关的格式改动;
  2. 权威稀释:当文件出现第一条"我喜欢",大家就不再把整份文件当宪法——劣化从第一条开始;
  3. 评审噪音:个人口味的增删反复出现在 diff 里,掩盖真正的约定变更,评审人疲于分辨;
  4. 无人守卫:个人口味随心情变、不走评审,文件悄悄腐烂(doc rot,第 04 章)。

一次典型的评审拒绝:

 ## 代码约定
 - 所有 SQL 必须参数化
-  我偏好双引号与 80 列行宽,代理写代码时请注意

Reviewer 意见:格式问题请改 Prettier 配置(进 Git,机器强制);个人习惯请放 AGENTS.local.md(不进 Git)。

5.2.3 治理动作

  • 共享层改动一律走 PR:AGENTS.md 的一行改动,可能改变团队每个人的代理行为——值得同等评审;
  • 个人层绝不提交:.gitignore 写明 AGENTS.local.md,提交前瞄一眼 git status;
  • 冲突裁决:个人偏好与团队约定冲突时,项目内以共享层为准;确要覆盖就用项目个人层显式写明,并清楚这是"本机例外";
  • 定期修剪:个人层同样会腐烂,换机、换项目时顺手清理。

5.3 记忆系统

5.3.1 代理的自动记忆

多数现代代理都有自动记忆(memory)机制:把会话中"值得跨会话保留的信息"——项目事实、你的纠正、重复出现的偏好——沉淀到本机记忆文件,并在后续会话中读取。各工具实现细节不同(存放位置与触发策略以所用工具文档为准;ZCode 记忆存于本机用户/项目目录,Claude Code 亦有 memory 机制),但模式一致:

  • 写入时机:出现明确纠正("以后别这样")、重复模式(连续几次相同要求)、环境事实(本机路径)时,代理主动记录;你也可以显式说"记住:……";
  • 读取时机:新会话启动时注入记忆摘要,或相关话题出现时检索对应条目;
  • 存放位置:用户目录或项目本机目录,不进 Git——记忆因人而异,是"私人的"。

一个典型片段:

你:别再用 var 了,这个项目一律 const/let。
代理:明白。已记住:本项目一律使用 const/let,不用 var。

5.3.2 与 AGENTS.md 的分工:宪法 vs 工作笔记

记忆和 AGENTS.md 看似重复,实则分工明确:

维度 AGENTS.md(宪法) 自动记忆(工作笔记)
谁维护 人工显式维护 代理自动维护
进版本库 是,走评审 否,本机私有
权威性 冲突时的最终依据 仅供参考,可能过时
内容 当前有效的规则与事实 会话中沉淀的观察与偏好
生命周期 随项目演进、定期修剪 随会话累积,需晋升或清理

判断口诀:每个人该知道的进宪法,只有我在意的留在笔记。

要警惕记忆的两个天然缺陷:它靠归纳得出,可能以偏概全(你一次例外的要求被当成永久规则);它不会自动过期,可能悄悄过时(项目早已改用新命令,记忆里还是旧的)。

5.3.3 记忆晋升:从笔记到宪法

反复出现的好实践不该永远躺在记忆里——换台机器就丢,同事也享受不到。把它"晋升"(promotion)进规则层,五步流程:

  1. 识别:留意记忆里重复出现三次以上的条目;
  2. 调取:让代理列出相关记忆:"列出你关于测试流程的记忆条目";
  3. 裁决:逐条判断是项目事实/团队规则,还是个人偏好——混在一起时拆开(见本章实战);
  4. 落盘:写入对应层级(团队项目的共享层走 PR),措辞按"短、准、可执行"三原则修剪;
  5. 清源:删除原记忆条目,避免双源漂移——两处说法不一致时,代理会随机采信。

5.3.4 清理错误记忆

症状:代理固执地使用旧命令、旧路径,或引用你早已推翻的决定。处理三步:

> 列出你关于本项目的全部记忆条目,标出与 AGENTS.md 或当前代码冲突的项。
(你逐条核对)
> 删除第 2、5 条;第 3 条改成"迁移文件已推进到 0007"。

原则:把记忆当缓存(cache),把 AGENTS.md 当权威源(source of truth)——缓存可疑时,先对源,再清缓存。

5.4 上下文的持久化补充手段

规则层回答"永远要怎么做",记忆回答"你大概是怎么样的"。还有一类信息两者都不适合承载:这一阵子在做什么、为什么这么定。三个轻量手段,点到为止(第 16 章展开):

手段 解决什么问题 放哪 谁维护
交接文档(handoff) 会话之间"上次做到哪、下一步、有什么坑" docs/handoff/ 等任务目录 代理生成、人工确认
决策日志(轻量 ADR) "为什么这么设计"的来龙去脉 docs/decisions/NNN-标题.md 人工主导
任务进度文件 长任务/多会话的状态与验收 docs/tasks/ 代理维护
  • 交接文档:大任务收尾时让代理生成交接文档,下次会话先读再开工。提示词很简单:
> 本次会话到此为止。请生成交接文档到 docs/handoff/:
  已完成的改动、未完成项与原因、下一步建议、需要注意的坑。不超过 30 行。
  • 决策日志:ADR(Architecture Decision Record,架构决策记录)的极简版——一段背景、一个决定、一句理由。注意分工:AGENTS.md 只装"现在有效"的规则,"为什么"进决策日志,别把宪法写成回忆录;
  • 任务进度文件:任务卡与状态清单,是跨会话(含多代理)协作的共享白板。

贯穿项目实战:轻读 BookNest

目标:配齐 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 宪法长成回忆录,关键约定被稀释 "为什么"进决策日志,进度进任务文件
记忆里存敏感信息 代理把令牌写进记忆文件并长期保留 明确禁止密钥入记忆;定期审查记忆文件
层级越多越好 规则散落四层互相矛盾,行为不可预测 能一层解决不拆两层;冲突时就近优先要可查

最佳实践

  1. 用四层模型放置规则:全局个人、项目共享(AGENTS.md + 规则目录)、项目个人、子目录;
  2. 一句话裁决归属:"换个人、换台机器还成立吗";
  3. 共享层改动一律走代码评审,个人层文件永远进 .gitignore;
  4. 缩进、引号、行宽交给 Prettier/ESLint,规则文件只写工具管不了的事;
  5. 把记忆当缓存:重复出现的好实践按"识别、调取、裁决、落盘、清源"五步晋升;
  6. 每个里程碑让代理列出记忆并核对:过时的删、错误的改、金子般的晋升;
  7. 明确禁止代理把密钥、令牌写进记忆或任何文件;
  8. 长任务用交接文档衔接会话,设计取舍写决策日志,保持 AGENTS.md 的"宪法"纯度;
  9. 层级冲突时就近优先,但要保证任何一层随时可查看、可修改。

本章小结

本章把第 04 章的单一指令文件扩展成一套分层治理体系:全局个人层装跨项目偏好,项目共享层(主文件加规则目录)装团队共识,项目个人层装本机专属配置,子目录层装目录增量,冲突时就近优先。团队与个人的分界用"换个人还成立吗"一句话裁决:共享层走评审、个人层不进 Git,格式问题交给工具。记忆系统让代理跨会话记住项目事实与你的偏好,但它是"工作笔记"而非"宪法"——权威永远在人工维护的 AGENTS.md;反复出现的好实践按五步晋升进共享层,可疑记忆及时清理。交接文档、决策日志、任务进度文件补齐"上次做到哪、为什么这么定"的持久化。至此代理既懂项目、也懂你;下一章换一个维度扩展——给它接入更多工具能力。

  • 四层模型:全局个人 → 项目共享 → 项目个人 → 子目录,就近优先;
  • 一条判据:换个人、换台机器还成立吗?成立进共享层,否则进个人层;
  • 记忆是工作笔记:可晋升(五步)、可清理;AGENTS.md 才是权威;
  • 持久化三件套:交接文档、决策日志、任务进度文件(第 16 章展开)。

动手练习

  1. 审计现有配置:查看你的全局个人层文件与某个项目的 AGENTS.md,逐条标注"团队/个人",把误放的个人口味迁到个人层、误放的项目事实迁到共享层,记录迁移条数与理由。
  2. 记忆清点:在一个用过几次的项目里,让代理列出它记住的全部条目,逐条标注"正确有用 / 过时 / 错误 / 敏感",执行清理,并让代理复述清理结果。
  3. 晋升演练:找出你重复交代过三次以上的某个要求,按 5.3.3 的五步晋升到正确层级(先裁决它是团队规则还是个人偏好),最后开新会话验证代理行为已由文件驱动而非记忆驱动。
  4. 综合实践(BookNest):把本章三个文件落进你的环境并提交共享层改动;然后人为制造一条错误记忆("记住:本项目测试命令是 npm run test:all"——实际是 npm test),观察代理下一步是否被误导,再按 5.3.4 清理;最后运行 git status --ignored | grep AGENTS 确认 AGENTS.local.md 已被忽略、不会进入提交。

导航

📄 第 06 章 · MCP 与工具接入:给代理扩展能力 06-mcp-and-tools.md

前两章解决的是代理"懂不懂":规则让它开机即懂项目,记忆让它越用越懂你。但有些事不是"懂"能解决的——代理生来只有读写文件、跑命令这类内置工具,它没法直接读你的 GitHub issue、查数据库、开浏览器点一遍页面。本章讲 Model Context Protocol(MCP):给代理"装上新器官"的标准方式,以及配套的凭据安全与克制原则。

学习目标

  • 能解释 MCP 是什么、解决什么问题,以及它与 Skill 的分工(能力 vs 知识);
  • 能按"当前任务真正需要什么",从常用 server 清单中做出克制的选型;
  • 能写出合格的 MCP 配置:区分全局与项目作用域,正确使用 command/args/env 字段;
  • 能用环境变量注入凭据、以最小权限账号接入,绝不把令牌字面量写进配置提交;
  • 能说出"什么时候不该接 MCP",并用按需启用策略控制上下文成本;
  • 能为 BookNest 接入 GitHub 与 SQLite 两个 server,并验证其工作正常与只读约束。

6.1 MCP 是什么

6.1.1 一个协议,标准化所有工具接入

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 接口——接口统一之后,"接入"这件事从项目工程变成了配置问题。

6.1.2 代理视角:发现与调用

一个 MCP server(服务器)把一类能力封装起来,对代理暴露统一接口。最常用的是 tools(工具)——可执行的动作,如"列出 issue""执行查询";协议还定义了 resources(可读取的资源)与 prompts(提示模板)。工作流程:

  1. 会话启动时,代理从各 server 发现可用工具清单(名称、描述、参数模式);
  2. 推理过程中判断某任务需要某工具,发起调用;
  3. 拿到结构化结果,继续推理或再次调用。

工具在会话中通常以"server 名 + 工具名"的形式被引用(如 github server 下的 list_issues)。你在对话记录里看到 mcp__github__list_issues 这类标识,指的就是它。

对代理而言,MCP 工具与内置工具在决策时地位相同;对你而言,体验是"代理多会了几件事"。

6.1.3 MCP 给"能力",Skill 给"知识"

决定是否接入前,先问一句:这是能力问题还是知识问题?

  • 能力问题:代理根本做不到——开不了浏览器、连不上数据库、读不了 issue。解法是 MCP(本章);
  • 知识问题:代理做得到,但不知道按什么流程做、有什么坑。解法是 Skill 与规则文件(第 08 章展开)。

类比:MCP 是装到机器上的电动工具,Skill 是老师傅的操作手册。先确认缺的是工具,再谈手册。

6.2 常用 server 一览与选型

以下按生态中的常见类别给出选型参考(同一类别常有官方与社区多个实现,按维护活跃度与权限模型挑选):

  • 文件系统(filesystem):把代理的文件访问约束在指定目录内、或受控地扩展到仓库之外。值得接入的场景:需要读取仓库外的固定资料目录,又不想放开整个磁盘;
  • Git / GitHub:仓库与协作平台操作(issue、PR、CI 状态)。值得接入的场景:让代理直接读 issue 开工、完成后开 PR 回链,替代手工复制粘贴;
  • 浏览器自动化(Playwright):驱动真实浏览器点击、截图、断言。值得接入的场景:验证前端交互、调试 E2E 用例、查看需要渲染才能呈现的页面;
  • 数据库(PostgreSQL / SQLite 等):表结构发现与数据查询。值得接入的场景:让代理自查 schema、验证迁移结果,省去你人肉贴表结构——务必用只读连接(6.4);
  • 检索与文档(fetch / web search / 文档站):联网查资料与最新框架文档。值得接入的场景:依赖库的版本比模型知识截止时间新,需要以官方文档为准写代码;
  • 项目管理(Jira / Linear 等):读任务、改状态、写评论。值得接入的场景:团队流程以任务系统为中心,代理直接"领任务、回进度"。

场景到 server 的对照表:

你的场景 优先考虑 备注
代理按 issue 开发、开 PR GitHub server 配合细粒度令牌(6.4)
验证页面交互、调试 E2E Playwright server 临时启用,用完即关(6.5)
让代理自查数据库结构与数据 数据库 server 只读连接是硬要求
依赖文档比模型知识新 文档 / 检索 server 减少版本幻觉
读仓库外的固定资料目录 filesystem server 只映射必要目录
团队任务流转在 Jira / Linear 项目管理 server 权限给到"读写自己的任务"即可

同一类别常有多个实现,挑选时看三个观察点:

  • 维护活跃度:看仓库近期提交与 issue 响应,归档或长期停更的直接排除;
  • 权限模型:是否支持只读模式、能否限定可访问的目录 / 仓库 / 表——这决定 6.4 的最小权限能否落地;
  • 工具粒度:暴露 5 个高内聚工具的实现,优于暴露 30 个细碎工具的实现——上下文成本与误调用面都更小。

6.3 配置方法

6.3.1 配置结构与字段

以 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 里的环境变量(主流工具支持展开,具体语法以文档为准);
  • 远程 server 则改用 url(认证用 headers),见 6.3.3。

6.3.2 作用域:全局、项目与本机

作用域 位置 适合放什么 进 Git?
全局 用户目录下的 MCP 配置(如 ~/.zcode/) 跨项目通用:文档检索、浏览器自动化 否
项目 仓库根 .mcp.json 项目专属:本仓库的 GitHub、SQLite server 是,走评审
本机覆盖 不进 Git 的本地配置文件 个人路径、本地端口等变体 否

项目级 .mcp.json 可提交共享——团队成员进入项目即获得同一组工具,这是它相对全局配置的核心价值。多数工具在首次加载项目配置时会请求确认(防止陌生仓库偷偷带入恶意 server),对来路不明的项目配置不要随手点允许。若全局与项目配置出现同名 server,以项目配置为准——与规则层的"就近优先"同理。

6.3.3 两种传输方式

  • stdio(本地子进程):代理把 server 作为子进程拉起,经标准输入输出通信。适合文件、数据库、本地脚本类能力;凭据经 env 注入,不落盘到配置;
  • 远程 HTTP(含流式 HTTP):代理直连云端服务(如 GitHub 托管的 MCP 端点)。适合 SaaS 能力,无本地进程开销。

安全提示一句话:远程端点能以你的凭据访问账号资源,务必核对域名是官方域名再配置。

6.3.4 查看与启停

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 克制原则的落地手段。

6.4 凭据与最小权限

6.4.1 凭据:环境变量注入,绝不落盘

先看错误示范——把令牌字面量写进配置再提交,仓库历史里就永远有了这个令牌,吊销前人人可读:

"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:无字面量"

6.4.2 最小权限账号

MCP server 拿到的账号权限,就是代理拿到的影响力——按最小权限原则(least privilege,第 11 章展开)发放:

  • 数据库:只读账号(PostgreSQL/MySQL 用 GRANT SELECT;SQLite 没有账号体系,靠只读连接或只读文件保障,见本章实战);
  • GitHub:使用 fine-grained personal access token(细粒度个人访问令牌),只勾选必要仓库,权限只给所需项(如 Issues: Read and write、Contents: Read)——不要用全仓库全权限的经典令牌;
  • 浏览器自动化:使用独立的浏览器 profile,不要带着个人登录态与 cookie 跑代理任务。

6.4.3 定期审计

  • 列出当前启用的 server 与工具,问一句"这个还在用吗";
  • 核对各令牌的权限范围是否仍然最小,用不到的立即吊销;
  • 用 git log -p -- .mcp.json 检查配置变更历史,确认没人把凭据带进过提交;
  • 把审计结论写进交接文档或决策日志:哪些 server 在用、各自权限是什么、下次审计时间。

6.5 克制原则:什么时候不要接 MCP

MCP 不是越多越好。接入前算三笔账:

  1. 上下文账:每个工具的名称、描述、参数模式都常驻上下文。一个 server 十几个工具,动辄数千 token;三五个 server 常驻,可能吃掉窗口的 5–10%,还稀释代理对关键约定的注意力(第 03 章);
  2. 误调用账:工具越多,代理选错工具的概率越高——本想让它改文件,它顺手调了数据库工具;
  3. 启动账:stdio server 每个会话都要拉起子进程,npx 首次还要下载包,冷启动拖慢开场。

算一笔具体的账:一个 GitHub server 暴露 20 个工具,每个工具的名称、描述与参数模式合计约 150 token,仅它一家就常驻约 3000 token;若同时挂着文档检索(15 个工具)与浏览器(10 个工具),还没写第一行代码,窗口里已躺着近 7000 token 的"工具说明书"。

接入前的决策顺序:

内置工具(读写文件、跑命令)能完成吗?
  ├─ 能 → 不接。把步骤写成说明或脚本即可
  ├─ 不能,但只是一次性任务 → 让代理写个一次性脚本完成,同样不接
  └─ 不能,且是高频、需要结构化访问的外部系统 → 接对应 server,用完即关

"按需启用、用完即关":浏览器调试、数据库检查这类阶段性需求,平时禁用,任务窗口内再启用。团队共享的 .mcp.json 同样要克制——每加一个 server,都值得在评审里问一句"当前任务真的需要吗"。反过来的信号也成立:某个 server 三个月没被任何会话调用过,就是移除它的时候了。

贯穿项目实战:轻读 BookNest

目标:为 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/规则)

最佳实践

  1. 先判断能力问题还是知识问题:缺工具才接 MCP,缺流程写 Skill 与规则;
  2. 项目专属 server 放项目级 .mcp.json 并走评审,跨项目通用的放全局;
  3. 凭据一律环境变量注入(${VAR}),提交前用 grep 自检零字面量;
  4. 给 server 的账号坚持最小权限:数据库只读、GitHub 细粒度令牌只授权必要仓库;
  5. 远程端点只连官方域名;对来路不明的项目配置不随手确认信任;
  6. 接入后先验证再使用:数据库先做 INSERT 拒绝测试,GitHub 先做一次只读列表;
  7. 控制数量:常驻两三个以内,阶段性 server 按需启用、用完即关;
  8. 定期审计:核对 server 清单与令牌权限,用不到的立即吊销,并检查配置提交历史;
  9. 为每个 server 记录"为什么接入、对应哪个任务",三个月未被调用的就移除。

本章小结

MCP 把"给代理装新能力"从私有插件工程变成标准配置:server 暴露工具,代理启动时发现、按需调用,主流编码代理均已支持。选型上牢记分工——MCP 给能力,Skill 给知识;常用 server 覆盖文件、Git/GitHub、浏览器、数据库、检索与项目管理,但每接一个都要付出上下文、误调用面与启动开销,因此"内置够用就别加、按需启用、用完即关"。配置上区分全局、项目、本机三个作用域,command/args/env 描述本地 stdio 进程,url/headers 描述远程端点;凭据坚持环境变量注入与最小权限——数据库只读、细粒度令牌、定期审计。BookNest 实战给出了两个 server 的完整接入与验证流程,其中"INSERT 必须被拒绝"的只读门槛,请当作日后接入任何数据类 server 的固定动作。下一章讨论权限与 hooks:进一步细化代理"能做什么、做之前要不要问你"。

  • MCP 给能力、Skill 给知识,先分清问题类型再选手段;
  • 配置三作用域(全局 / 项目 / 本机),stdio 与远程 HTTP 两种传输;
  • 凭据零落盘:环境变量注入 + 最小权限 + 定期审计;
  • 克制三笔账:上下文、误调用、启动;按需启用、用完即关。

动手练习

  1. 盘点现状:列出你当前工具里已启用的全部 MCP server 与工具总数,按每个工具 50–200 token 估算上下文占用,写下"哪些可以禁用"的结论并执行。
  2. 最小权限改造:检查你现有某个 server 的账号权限(数据库账号或 API 令牌),把它降级到"当前任务所需的最小权限",记录降级前后的权限差异。
  3. 只读验证流程化:为你常用的数据库 server 写一段接入后验证流程(SELECT 成功 + INSERT 被拒 + 记录 server 版本),保存进项目文档,并规定"每次改动 MCP 配置后重跑"。
  4. 综合实践(BookNest):按本章实战完成两个 server 的接入与全部验证;然后故意把 GitHub 令牌权限临时降为只读(Issues: Read only),观察代理在"创建 issue"一步的报错行为;恢复权限后,把本次接入过程写成一份 10 行以内的 docs/handoff/mcp-setup.md 交接文档(呼应 5.4)。

导航

📄 第 07 章 · 权限与 Hooks:让代理既听话又能干活 07-permissions-and-hooks.md

上一章把代理接上了 MCP:能力半径从"读写本地文件"扩展到"连数据库、调外部 API"。能力越大,越需要缰绳。本章是 Part 2 的最后一块拼图——权限规则与 hooks:前者决定代理"默认能做什么",后者在每一次工具调用的生命周期里自动执行你的检查。它们共同回答一个问题:怎么让代理放手干活,又不至于干出格的事。

学习目标

  • 能说出权限系统要解决的核心矛盾,并用 allow / ask / deny 三类行为描述代理的每个动作;
  • 能读写权限规则语法(工具名 + 参数模式匹配),为项目配置一份分层合理的权限清单;
  • 能分清权限规则与 hooks 的分工,并看懂 hooks 的事件时机、stdin JSON 输入与退出码约定;
  • 能独立编写并注册三个入门 hook:拦截危险命令、写文件后格式化、回合结束凭据扫描;
  • 能为团队设计"共享护栏 + 个人增强 + 强制策略"的三层权限治理结构。

7.1 权限的两难与解法

7.1.1 两个极端都不对

AI 编码代理默认处于一种"每动一步都问你"的状态:读个文件、跑个测试、改一行代码,都要人工点确认。把交互强度拉到两个极端,问题立刻显形:

  • 每步确认(最保守):安全,但"确认疲劳"(alert fatigue)来得很快。第十次被问"是否允许 npm test?"时,你已经开始无脑回车——而真正的危险命令恰好藏在第 41 次里。更糟的是,频繁打断会切碎心流(flow),代理一次跑五分钟的任务被拆成十次人工守候,效率归零。
  • 完全自动(最激进):也称 yolo 模式或 bypass 模式,所有动作免确认。效率拉满,但下面这些真实事故只需发生一次:
    • 代理"清理现场"时执行了 rm -rf,删掉的不是构建产物;
    • 为了"修复冲突",直接 git push --force 覆盖了远端历史;
    • 调试时把数据库连接串(含密码)硬编码进了源码并提交;
    • 把 .env 内容当作"配置示例"贴进了会话或文档。

7.1.2 第三条路:把信任变成可配置的规则

权限系统的解法不是在两个极端里二选一,而是把"你信任代理做什么"变成一条条可配置、可审计的规则:

  1. 声明三类行为:允许(allow)、询问(ask)、拒绝(deny)——代理的每个动作必属其一;
  2. 按证据增长信任:从默认询问起步,把反复被你手动放行的动作(跑测试、看日志)固化为 allow,把绝不许发生的事(读密钥、删库)固化为 deny;
  3. 全程可审计:规则写在配置文件里进 Git,谁放行了什么、为什么放行,diff 里一清二楚。

一句话:权限配置 = 把你对代理的信任,从"每次口头判断"变成"一次书面立法"。 这也是第 11 章"边界设计"的地基——本章先解决"怎么写规则、怎么加自动检查",第 11 章再上升到"怎么为一个多代理系统划分边界"。

7.2 权限规则配置

7.2.1 配置文件在哪

以 ZCode 为例(Claude Code 同名同义,格式互通):

层级 位置 是否进 Git 谁维护
项目共享 <repo>/.zcode/settings.json 是(团队护栏,走评审) 团队
项目个人 <repo>/.zcode/settings.local.json 否(写进 .gitignore) 个人
用户全局 ~/.zcode/settings.json 否 个人跨项目偏好
企业托管 系统级托管配置(如 managed-settings.json) — 管理员强制下发

生效优先级从高到低:企业托管 > 命令行参数 > 项目个人 > 项目共享 > 用户全局。也就是说,任何一层都放宽不了上层收紧的红线——这是 7.5 节团队策略的机制基础。

7.2.2 规则语法:工具名 + 参数模式

每条规则形如 工具名(参数模式),常见写法:

写法 含义 示例
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。

7.2.3 一份完整的教学配置

{
  "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/**)"
    ]
  }
}

逐行讲解:

  1. defaultMode: "default"——未命中任何规则的动作一律询问。这是安全的兜底,不要改成全放行模式;acceptEdits(文件编辑免确认)、plan(只读规划)、bypassPermissions(全放行,高危)等模式留到第 11 章按场景选择;
  2. "Read" 与 git status / diff / log、npm run lint、npm test:*——读文件与只读 Git 命令完全放行,验证类命令放行,让代理"随时自查、改完自验"成为默认习惯;
  3. 两条 WebFetch(domain:...)——出网仅限这两个文档站,其余域名落入 defaultMode 询问;
  4. git commit:*——提交必须人审。commit 是"留下永久记录"的动作,适合 ask;
  5. npm install:*——装依赖改变依赖树,供应链风险入口,必须人审;
  6. "Edit"、"Write"——写代码整体保持询问(ask 优先于 allow 的编辑类默认),项目成熟后可再把子路径收进 allow;
  7. 三条 .env 与 secrets/** 的 deny——注意写了三种形态:根目录的 .env、.env.*、以及任意子目录的 **/.env*,防止代理在 server/ 下新建 .env.local 就绕过了规则;
  8. rm -rf:*——递归删除永远不许代理执行;
  9. Edit(migrations/**)——呼应第 04 章 BookNest 禁区:已应用的迁移文件不可修改,规则比"禁区清单"更硬。

7.2.4 清单是"养"出来的

不要试图一次设计出完美清单:弹确认框时选择"始终允许",规则会写回项目个人配置(settings.local.json);每周把沉淀下来的规则评审后上移到项目共享配置。判断标准:幂等可重跑(测试、lint、status)→ allow;留下永久痕迹(commit、install、push)→ ask;涉密或不可逆(.env、force push)→ deny。

7.3 Hooks 机制

7.3.1 生命周期时机

权限规则回答"能不能",hooks 回答"过程中要不要额外做点什么"。Hook 是你注册的 shell 命令,在工具调用的生命周期事件点上自动执行:

用户输入 ─▶ 模型决策 ─▶ [PreToolUse hooks] ─▶ 工具实际执行 ─▶ [PostToolUse hooks] ─▶ 模型继续
   │                      拦截 / 放行 / 改写              反馈 / 记录 / 联动
   └─▶ 会话级事件:SessionStart、Stop(回合结束)等

常用事件一览:

事件 触发时机 典型用途
PreToolUse 工具调用执行前 拦截危险命令、阻止改敏感文件
PostToolUse 工具调用执行后 自动格式化、触发增量检查
Stop 代理回合结束时 收尾扫描(凭据)、审计日志

7.3.2 输入与输出契约

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 是给模型看的纠错通道,把"为什么拦 + 该怎么办"写清楚,模型下一次就会照做。

7.3.3 与权限规则的分工

需求 用权限规则 用 hook
不许读 .env 一条 deny 即可 太重了
提交前扫描暂存区是否含密钥 表达不了(要"看内容") 合适
写文件后自动格式化 表达不了(要"产生副作用") 合适
禁止强推远端 Bash(git push --force:*) 规则够用,hook 做双保险

判断准则一句话:权限规则是静态白名单,hooks 是可编程守门员。 能用一条规则表达的就用规则(声明式、零维护、一目了然);需要理解内容才能判断、或需要产生副作用的,才交给 hook。反过来把所有事都写成 hook,你会得到一套没人看得懂的隐形逻辑。

7.4 三个入门 Hook 实例

以下示例均用 Node 编写(跨平台、与 BookNest 技术栈一致),保存在 <repo>/.zcode/hooks/ 下。第 13 章会对 hooks 做深度实践,本章目标是三个最小可运行版。

7.4.1 PreToolUse:拦截危险命令

// .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 章。

7.4.2 PostToolUse:写文件后自动 lint / format

// .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 兜底。

7.4.3 Stop:回合结束的凭据扫描提醒

// .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 章)。

7.5 团队策略:护栏也是资产

7.5.1 三层治理结构

层 内容 位置 变更方式
强制层 企业红线:公司密钥目录、出网域名总表 管理员托管配置 IT 统一下发,任何人改不掉
共享层 项目 allow/ask/deny + hooks 脚本 .zcode/settings.json 与 .zcode/hooks/,进 Git 走 PR 评审
个人层 个人增强:更多 allow、本地工具放行 settings.local.json / 用户全局配置 自行维护,不进 Git

关键动作只有一个:把权限配置与 hooks 脚本当作代码提交进仓库。于是护栏获得代码的一切好处——人手一份、可评审、可回滚、可追溯;新成员克隆下来就自带同一套安全基线,而不是靠口头交接"哦对了代理不能动 migrations"。

7.5.2 配置也要进评审

护栏的 diff 和业务代码同等重要,甚至更重要——它决定代理能对业务代码做什么:

  • PR 中修改 deny 规则或 hook 脚本的提交,评审人要重点过目;提交信息建议注明 guard: 前缀;
  • 克隆审查:hooks 以你的用户身份执行任意命令,克隆来的仓库自带 hooks 属于供应链风险——首次运行前逐条检查其中注册的命令与脚本(正规工具会在新仓库首次执行 hooks 前提示确认,别惯性点过);
  • 个人层只做"加法增强"(多放行一些自己的习惯命令),永不覆盖共享层的 deny——机制上上层红线也确实覆盖不了,但保持习惯能避免"在 A 项目能跑、在 B 项目被拦"的困惑。

贯穿项目实战:轻读 BookNest

目标:落地 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 防线;
  • 迁移目录只进 ask 而不是 deny:新增迁移是日常操作,改历史迁移才危险,正好对应"已应用文件禁改、新增递增序号"的约定(严格拦截靠 7.2.3 的 deny 思路或第 13 章的按内容判断 hook)。

文件二: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 注册与权限配置

最佳实践

  1. 从默认询问模式起步,把每次手动放行的动作沉淀为 allow——权限清单是"养"出来的,不是一次性设计的;
  2. deny 写红线(密钥、数据目录、破坏性命令),ask 写需要人审的动作(commit/push/install),allow 只写幂等安全动作;
  3. 依赖"deny 优先级最高"这条铁律:红线一律显式 deny,不要指望"没写 allow"来兜底;
  4. 出网工具按域白名单放行,白名单同时充当 SSRF 防线;
  5. 能用一条规则表达的不写 hook;需要"看内容判断"或"产生副作用"的才写 hook;
  6. hook 脚本必须快(亚秒级)、全程容错、输出克制——它跑在每次工具调用的热路径上;
  7. 拦截类 hook 的 stderr 写清"为什么拦 + 建议怎么做",模型会照着自我修正;
  8. 权限配置与 hooks 随代码进 Git、走 PR 评审(guard: 前缀),个人增强永远放个人层。

本章小结

权限与 hooks 是代理的"法律系统":权限规则把信任变成可配置、可审计的静态法条——allow 固化安全动作、ask 保留人审关口、deny 划定不可逾越的红线,且 deny 永远优先;hooks 则是可编程守门员,在 PreToolUse/PostToolUse/Stop 等生命周期时机自动执行你的脚本,靠 stdin JSON 读取上下文、靠退出码与 stderr 表达裁决。两者分工明确:静态规则管"能不能",可编程 hook 管"过程中还要做什么"。团队层面,把护栏提交进仓库共享、个人增强放用户层、企业托管兜底,并让配置本身也接受评审——至此,Part 2 的四件套(上下文文件、规则记忆、MCP、权限 hooks)配齐,代理已经"开机即懂、干活有度"。

  • 三类行为:allow / ask / deny,判定顺序 deny > ask > allow;
  • 规则语法:工具名 + 参数模式(精确、:* 前缀、路径模式、domain、MCP 命名);
  • Hook 契约:stdin 收 JSON,退出码 0/2/其他,stderr 是给模型的纠错通道;
  • 分工铁律:规则是静态白名单,hooks 是可编程守门员;护栏进 Git,配置进评审。

动手练习

  1. 最小权限清单:为自己的项目写一份权限配置:至少 3 条 allow、2 条 ask、3 条 deny(必须含密钥文件)。开会话逐条验证:deny 的动作是否被拒、allow 是否免确认、ask 是否弹出确认框。
  2. 加固黑名单:给 guard-bash.mjs 增加两条黑名单(git reset --hard、chmod -R 777 /),分别用"应拦截"与"应放行"的命令各测试一次,确认 stderr 提示能引导模型改用安全替代方案。
  3. 可容错的格式化 hook:仿写一个 PostToolUse hook——当被改文件位于 web/src/ 时自动执行 npx eslint --fix;要求 lint 失败时只打印提示、退出 0,并用一个故意写错的文件验证它不会卡死会话。
  4. 综合实践(BookNest):把本章的全量配置与三个 hook 脚本落进你的 BookNest 仓库并提交;然后按 7.5.2 的克隆审查流程,开一个新会话扮演"新成员"检查这套护栏(列出它拦截什么、放行什么、扫描什么),根据发现的问题修订配置,再以 guard: 前缀的提交信息记录这次变更。

导航

📄 第 08 章 · Skill 系统原理:可复用的能力包 08-skill-anatomy.md

Part 2 配齐了四件套:代理"开机即懂"(AGENTS.md)、"记得住"(规则与记忆)、"够得着"(MCP)、"干不出格"(权限与 hooks)。但还有一类知识无处安放:那些重要却低频的流程——比如"新增一个 API 端点的完整步骤",塞进 AGENTS.md 会撑爆常驻上下文,口头交代又每次漂移。本章开始 Part 3:把这类方法论打包成代理"随取随用"的能力包——Skill。

学习目标

  • 能解释 Skill 解决什么问题,说清它与 AGENTS.md、MCP 的分工边界;
  • 能解剖一份 SKILL.md(YAML frontmatter 元数据 + Markdown 正文)与 skill 目录的完整形态;
  • 能复述渐进式披露(progressive disclosure)三级机制,并算清每级的上下文成本;
  • 能据此判断一条 description 的好坏,理解它为何决定 skill 的生死;
  • 能说明 skill 的触发与显式调用方式,并按生效范围正确放置个人级/项目级 skill;
  • 能为 BookNest 盘点流程性工作,用四条标准筛选出值得 skill 化的候选。

8.1 Skill 是什么

8.1.1 从"常驻的上下文"到"按需的能力包"

第 04 章立过规矩:AGENTS.md 只放"每次会话都成立"的约定,低频内容塞进去就是 wiki 化反模式。可开发中确实存在这样的知识:

  • "新增一个 REST API 端点"要做五步,细节很多,但只在写端点时才需要;
  • "发布前检查"有 8 项清单,一个月用不了几次,漏一项却可能带病上线;
  • "做数据库迁移"有一套严格纪律,违反的直接后果是数据损坏。

这些知识的特点是:完整版很重要,但不需要时刻在场。Skill(技能)就是为它们设计的容器——一个文件夹,内含 SKILL.md(元数据 + 方法论正文)与可选资源(脚本、参考文档、模板资产)。代理平时只"看到书脊"(一两行简介),识别到匹配任务后才"翻开整本书"按需加载执行。两种状态的体感差别:

没有 skill 时(每个端点都要重新交代):
你:加一个 GET /api/v1/books 的标签筛选接口。
你:(打断)等等,校验要过 validate.ts,测试要先写,
    错误格式统一 { code, message },路由里别写业务逻辑……

有 add-endpoint skill 时:
你:/add-endpoint 给 books 加按标签筛选的接口。
代理:(自动按五步执行,并对照完成定义自检)

8.1.2 与 AGENTS.md、MCP 的分工

三个机制都往代理的上下文里放东西,容易混。用两个维度区分:加载时机(常驻还是按需)与内容性质(知识流程还是外部能力):

机制 形态 何时进入上下文 擅长解决
AGENTS.md / 规则 Markdown 指令 会话启动,常驻 项目事实、全局约定、禁区
Skill 文件夹(SKILL.md + 资源) 任务匹配后按需 可复用的任务方法论(怎么干)
MCP 协议连接的外部服务 工具清单常驻,调用时执行 与外部系统读写交互(碰得到什么)
权限 / Hooks 配置 + 脚本 生命周期强制执行 护栏与自动化(许不许干)

一个例子看清差别:"操作 SQLite 数据库"是能力问题——用 MCP 接一个 sqlite server;"怎么给 BookNest 加一个端点"是流程问题——写成 skill;"端点路由只做参数解析"是约定——进 AGENTS.md。

8.1.3 开放格式:写一次,处处可用

Agent Skills 已是开放格式(open standard,社区称为 agentskills 标准):规范定义了 SKILL.md 的元数据字段与目录布局,任何遵循该标准的工具都能加载同一个 skill。ZCode 与 Claude Code 均按此约定读取——你在本教程写的每个 skill,换到另一个支持该标准的工具里无需改造。这也决定了 skill 的工程属性:它就是纯文本文件夹,可进 Git、可走 PR 评审、可版本化,没有任何专有运行时。

8.2 SKILL.md 结构解剖

8.2.1 一个真实的 SKILL.md

以 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 节展开)
其他扩展字段 否 工具限制、版本兼容说明等,因工具而异,用到时查所用工具文档

正文则是给代理的操作指令:祈使句、编号步骤、可判定的完成标准("缺任何一步视为未完成"),以及——注意最后一句——对资源文件的指路。

8.2.2 目录形态:三类约定资源

一个成熟的 skill 通常长这样:

add-endpoint/
├── SKILL.md                # 必需:元数据 + 方法论正文
├── scripts/
│   └── scaffold.mjs        # 可执行脚本:生成端点样板(代理用 Bash 调用)
├── references/
│   ├── api-conventions.md  # 参考文档:错误码表、校验规则全量细节
│   └── testing-guide.md    # 参考文档:接口测试模板与断言写法
└── assets/
    └── route.ts.tmpl       # 模板资产:直接复制改造的代码骨架

三个约定目录各司其职:

  • scripts/:确定性操作交给脚本——生成样板、校验格式、算统计数字。脚本输出是确定的,比让模型每次"手写"稳定得多;脚本应支持 --help 并返回明确退出码;
  • references/:正文放不下的细节按主题拆文件(错误码表、平台差异、历史决策)。关键纪律:正文里必须指路("动手前先读 references/api-conventions.md"),否则代理不知道这些文件存在;
  • assets/:模板、样板、清单——直接复制改造,避免模型每次凭空重造一份格式漂移的骨架。

不是每个 skill 都要凑齐三个目录:release-check 这类纯清单技能,一个 SKILL.md 就够了。

8.2.3 最小可用骨架

新技能从这个骨架起步,用起来再长:

---
name: <任务导向的英文名>
description: <做什么>。当<用户会怎么提这个任务>时使用;覆盖<关键步骤范围>,
  完成标准是<可判定的产出>。
---

# <任务名>

固定步骤(缺任何一步视为未完成):
1. <步骤,写明涉及的路径>
2. <步骤,写明验证命令>

<某主题> 的全量细节,动手前先读 references/<主题>.md。

自检方法:合上电脑,你能照着这个骨架口述出技能"何时触发、做哪几步、怎么算完成"吗?三问都答不上来的部分,就是还没想清楚的部分。

8.3 渐进式披露:三级加载机制

8.3.1 三级各自何时付费

Skill 系统的核心设计是渐进式披露(progressive disclosure):把内容分成三级,每一级只在需要时进入上下文:

级别 内容 何时进入上下文 典型成本
L1 元数据 name + description 会话启动即常驻 每个 skill 约百 token
L2 正文 SKILL.md 全文 任务匹配、skill 被触发后 数百到数千 token
L3 资源 正文引用的文件 执行中按需读取 用到才付,可按主题控制

一次完整调用的付费时序:

会话启动 ─▶ [L1] 全部 skill 的 name+description 常驻(图书馆索引卡)
    │
任务到来 ─▶ 语义匹配命中 ─▶ [L2] 加载该 skill 正文全文(翻开那本书)
    │
执行中途 ─▶ 正文指路 references/ ─▶ [L3] 按需读取文件(查书末附录)

8.3.2 一笔算术账

假设你装了 30 个 skill:L1 常驻成本约 30 × 100 = 3000 token,可以忽略不计。反过来,如果把这 30 份正文全部塞进系统提示(很多人早期就是这么用 AGENTS.md 的),按每份平均 2000 token 算就是 6 万——还没开始干活,窗口先没了三分之一。第 03 章讲过:上下文是稀缺资源,行数即成本,且塞得越满,模型对关键信息的注意力越稀。

类比:L1 是图书馆的索引卡,L2 是抽出那本书翻开,L3 是书末的参考文献与附录。你不会为了查一个问题把整个图书馆搬回家——但需要时,整座图书馆都在。

8.3.3 决定性结论:description 决定生死

三级结构推出一个容易被忽视的事实:L1 是唯一的常驻入口,任务与 skill 的匹配只能靠 description 这一两行字完成。 由此得出本书最重要的结论之一:

description 写得好坏,直接决定 skill 会不会被触发。写得含糊,任务来了模型对不上号,skill 等于不存在——写得再漂亮的正文也永远没机会被读到;写得精准,正文的质量才有意义。

所以写 skill 的精力分配是"倒挂"的:一半以上花在那一两行 description 上(第 09 章专门讲怎么写),其次才是正文结构(本章 8.2),最后是资源组织。触发失败的诊断与修复则留给第 10 章。

8.4 触发与放置

8.4.1 触发:一次语义匹配

代理在每个任务开始时,把当前任务描述与所有 skill 的 L1 元数据做语义比对,命中则加载正文。三个提高命中率的抓手:

  1. 用任务的语言写 description:用户会说"加个接口",description 里就要同时出现"新增 REST API 端点"这类正式表述和"接口/端点/API"这类口语变体;
  2. 正文开头重申触发条件:"当用户要求……时使用本技能"——任务模糊在边缘时,给模型第二次确认机会;
  3. name 可检索:用任务导向的英文(add-endpoint),别用 helper-v2 这种看了也不知道干嘛的名字。

以 add-endpoint 为例,看"任务的话"如何对上 description 的关键词:

用户随口说 description 里埋的对应词
"帮我加个接口" 接口 / API
"新增一个端点" REST API 端点
"books 要能按标签筛" 资源名 + 功能动词(覆盖路由、服务层……)

8.4.2 显式调用:不确定就点名

隐式触发不保证每次命中(尤其 skill 新写好、还没调试时),显式调用是确定性手段:

你:/add-endpoint 给 books 加一个按标签筛选的接口。
你:用 add-endpoint 技能,为 fragments 实现批量归并到笔记的接口。

斜杠命令 /skill-name 与对话中点名等价:都是绕过语义匹配、直接指定加载。调试新 skill 时先用显式调用验证正文质量,再观察隐式触发是否靠谱——这个两段式验证流程在第 10 章会工具化。

8.4.3 放置位置与生效范围

位置 生效范围 进 Git? 适用
<repo>/skills/<name>/ 本项目所有会话 是(团队共享、走评审) 项目流程:BookNest 的 add-endpoint
~/.zcode/skills/<name>/ 个人所有项目 否 个人方法论:提交信息风格、周报生成

判断标准与第 04 章指令文件的分层完全同构——问一句"换个项目还成立吗?":成立就放个人级,只对本项目有意义就放项目级进 Git。附录 D 的目标仓库结构里已经预留了 skills/ 目录,BookNest 的项目级技能将住在那里。部分工具还支持通过插件内置 skills,加载机制相同,不再展开。

8.5 Skill 生态与全景

8.5.1 开源生态:先搜再造

Skill 是纯文本文件夹,复用成本几乎为零,先找现成的永远比从零写划算:

  • 官方示例库:Anthropic 开源的 anthropics/skills 仓库,含 docx、pdf、pptx 等办公文档技能,是学习结构与写法的最佳范本;
  • 社区清单:awesome 系列收录了大量社区技能,覆盖测试、部署、文档等场景;
  • 团队内部库:公司沉淀的内部技能库,配套版本管理与治理流程(第 10 章)。

心法一句话:先搜再造——找到最接近的现成 skill,fork 一份,改 description 与项目相关细节,比从零写快一个数量级。

8.5.2 全书体系全景图

把 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 加一个按标签筛选的接口":

  1. AGENTS.md 提供分层约定(路由薄、services 厚、SQL 参数化);
  2. 语义匹配命中 add-endpoint skill,正文(L2)加载,拿到五步流程;
  3. skill 的 scripts/scaffold.mjs 生成端点样板(Bash 调用,命令在权限 allow 清单内);
  4. 正文指路的 references/api-conventions.md(L3)按需读入,错误码表就位;
  5. 写文件后,PostToolUse hook 自动跑 Prettier;提交时 ask 弹确认,Stop hook 扫描暂存区凭据。

常驻层给了背景,能力层给了方法与工具,护栏层全程兜底——这正是本书追求的"有纪律的 agentic engineering"形态。

贯穿项目实战:轻读 BookNest

目标:盘点 BookNest 开发中反复出现的流程性工作,筛选出值得做成 skill 的候选,并给出裁决理由。第 09 章将从第一名开始动手写。

第一步:列清单。 把"做过不止一次、下次还会做"的工作全部列出来,宁多勿漏:

□ 新增一个 REST API 端点(FR-1~FR-7 估计 20+ 个端点)
□ 给前端加一个页面或组件(BooksPage、StatsPage……)
□ 做一次数据库 schema 迁移(每个里程碑都要)
□ 为一个模块补接口测试(每个端点都要)
□ 从 CSV 批量导入书单(FR-5,低频)
□ 发布前检查(构建、测试、安全项;M4 起每次发布都要)
□ 排查一次"测试挂了"(原因每次都不同)

第二步:过筛选标准。 四条全满足才值得 skill 化:

  1. 重复频率高——每周至少两三次;
  2. 步骤稳定——流程基本固定,不会每次都不一样;
  3. 有隐性知识——存在"从现有代码看不出来"的细节值得沉淀;
  4. 出错有代价——跳步或做错会返工,甚至弄坏数据。

第三步:逐一裁决。

候选 频率 步骤稳定 隐性知识 出错代价 裁决
新增 REST API 端点 高 高 校验/错误码/测试写法分散在约定里 中 入选 ①
数据库 schema 迁移 中 高 序号递增、禁改历史、需可回滚 高(数据损坏) 入选 ②
发布前检查 低但关键 高 8 项检查的顺序与安全项 高(带病发布) 入选 ③
前端加页面 高 中 多数约定已在 web/AGENTS.md 低 暂缓观察
接口测试 高 中 可并入端点流程 低 并入 ①
CSV 导入书单 低 低 一次性功能 低 不做
排查测试失败 不定 低 因 case 而异 低 不做

三个入选的理由:

  1. add-endpoint(新增 REST API 端点):M1~M3 要写 20+ 个端点,每个都走"路由→服务→校验→测试→错误格式"五步,步骤 100% 固定;而细节(validate.ts 用法、错误码表、测试断言模板)塞进 AGENTS.md 会撑爆常驻上下文——典型的"常驻放不下、口头说不清",skill 的甜点区;
  2. db-migration(数据库 schema 迁移):规则严格(序号递增、已应用文件禁改、必须可回滚、跑完要验证),出错直接威胁"数据 10 年不换"的承诺。适合 checklist 式正文加一个校验脚本(检查序号连续性与文件头标记);
  3. release-check(发布前检查):低频高价值、纯流程零代码——8 项检查(tsc、lint、vitest、build、e2e、环境变量核查、data/ 备份、提交信息规范)写成一页清单,每次发布省一遍脑内回放。

反面教材同样有价值: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/ 脚本

最佳实践

  1. 先搜再造:动手写之前,先去官方示例库与社区清单找最接近的现成 skill 改;
  2. 把一半以上写作精力投在 description 上——它是唯一的触发入口,决定 skill 生死;
  3. name 用任务导向英文并与目录名一致(add-endpoint,而非 helper-v2);
  4. 正文写给代理看:祈使句、编号步骤、可判定的完成标准;
  5. 正文必须指路 references/,否则 L3 永远不会发生,细节等于不存在;
  6. 确定性操作(生成样板、校验格式)写成 scripts/ 脚本,不要让模型每次手写;
  7. 放置遵守"换个项目还成立吗":项目流程进 skills/ 与 Git,个人方法论进用户目录;
  8. 新 skill 先用显式调用(/skill-name)验证正文质量,再观察隐式触发命中率。

本章小结

Skill 把"重要但低频"的任务方法论从常驻上下文中解放出来:一个文件夹(SKILL.md + scripts/references/assets 资源),遵循 agentskills 开放格式,跨工具通用。它靠渐进式披露三级机制兼顾省与深——L1 元数据常驻(每 skill 约百 token)、L2 正文任务匹配后加载、L3 引用文件执行中按需读取;由此推出本部分的核心命题:description 是唯一触发入口,写得好坏直接决定 skill 的生死。与 AGENTS.md(常驻约定)、MCP(外部能力)、权限与 hooks(护栏)合起来,代理的"知识—能力—纪律"三层就此完整。下一章进入动手环节:写出第一个经得起触发考验的优秀 Skill。

  • Skill = 文件夹:SKILL.md(frontmatter + 正文)+ scripts/references/assets;
  • 三级披露:L1 常驻元数据 → L2 按需正文 → L3 按需资源,省上下文又不失深度;
  • description 决定触发:用"做什么 + 何时用"写,混入用户的口语关键词;
  • 放置分层:项目流程进 skills/ 共享,个人方法论进用户目录,先搜再造。

动手练习

  1. 三级标注:从官方示例库(如 anthropics/skills)挑一个 skill,只读它的 SKILL.md,把全部内容归入 L1/L2/L3 三级,并算一笔账:若三级全常驻要花多少 token,实际常驻只花多少。
  2. description 盲测:给一个 skill 写三版 description(口号版、纯流程版、"做什么+何时用"版),开三个新会话用完全相同的任务描述,测试哪个版本会触发,记录命中率差异。
  3. 放置实验:把同一个 skill 分别放入某项目的 skills/ 与 ~/.zcode/skills/,在该项目内、另一个项目里各开一个会话,验证生效范围与 8.4.3 的表格一致。
  4. 综合实践(BookNest):按四条筛选标准盘点你自己的 BookNest 开发流程,产出一份"skill 化候选清单"(含每项的裁决理由),与本章三个入选候选对比——你发现了本章没发现的候选吗?本章的候选在你的实践里该降级吗?把清单与结论存进仓库 docs/tasks/,第 09 章将从清单第一名开始写出第一个真正的 skill。

导航

📄 第 09 章 · 写出优秀的 Skill:方法论与完整案例 09-writing-great-skills.md

上一章留下一个决定性结论:description 是唯一触发入口,写得好坏决定 Skill 生死。本章回答"怎么写好"——先判断什么值得做成 Skill,再依次攻克 description、正文与资源组织三道关,最后从零完整走查一个含校验脚本的真实案例,并在 BookNest 中落地为第一个项目级 Skill。

学习目标

  • 能用三个判据和一张决策清单,判断一项工作是否值得 Skill 化;
  • 能按"做什么 + 何时用"的公式写出可稳定触发的 description,并指出坏版本的病灶;
  • 能用"给新同事写交接文档"的心态,写出命令式、步骤化、可判定的 Skill 正文;
  • 能按 scripts/、references/、assets/ 的职责边界组织资源,并说清"何时用脚本、何时写指令";
  • 能独立从零写出含校验脚本的完整 Skill,并在贯穿项目中落地与触发。

9.1 什么值得做成 Skill:三个判据

9.1.1 三个判据

写 Skill 本身有成本(结构设计加调试,通常 0.5–2 小时),做之前先过三道判据:

判据一:重复出现 ≥ 3 次。 一两次的偶发任务摊不平写作成本;而"重复"本身是流程稳定的经验证据——第 08 章盘点 BookNest 时,"新增端点已被重复执行 11 次"正是从代理的会话历史里数出来的。注意这是下限不是目标:重复 30 次的任务才最值得投入打磨。

判据二:存在固定的最佳流程。 下次做和这次做,步骤基本一样、可枚举、可排序。如果每次都要临场发挥(比如"排查一次测试挂了",原因每次都不同),流程本身就不存在,Skill 只能写成正确的空话。

判据三:含团队私有约定。 即"从公开资料和现有代码都看不出来"的知识:错误码表、validate() 中间件的用法、迁移纪律。模型通识(general knowledge)解决不了的部分,才是 Skill 的价值增量——否则你在给代理重复教它已经会的东西。

三个判据与第 08 章四条筛选标准的对应:判据一≈"频率高"、判据二≈"步骤稳定"、判据三≈"有隐性知识";"出错有代价"则是加权项——代价极高的任务(如数据库迁移,做错损坏数据)即使频率只有中等,也值得做。

9.1.2 两类不值得做的

  1. 一次性任务:写完就不再执行的功能(如 BookNest 的 CSV 批量导入,FR-5 写完即弃)、为处理某次线上事故临时写的脚本。判据一不满足,做成 Skill 只会制造一个永不触发的死文件夹;
  2. 模型通识本就会的:教代理"Express 中间件是什么""怎么写数组去重"。这些是公开知识,正文教程腔反而可能教错。可靠的测试法:"招一个熟悉这个技术栈的新同事,不问人就能做对吗?" 会——就不写。

9.1.3 决策清单

动手前把这张清单过一遍:前四格全勾才做,出错代价高时可放宽频率要求,末格命中即否决:

□ 近一个月这项工作出现过 ≥ 3 次吗?
□ 下次的步骤和这次基本一样吗(能写成固定编号流程)?
□ 有"看代码、搜公开资料都看不出来"的内部约定吗?
□ 新同事不问人大概率做不对吗?(做不对 → 有沉淀价值)
□ 出错的代价高吗?(数据损坏、带病发布 → 可放宽频率要求)
□ 是一次性任务或每次原因都不同吗?(是 → 停,不做)

用 30 秒裁决三个真实候选:

候选 判据一 判据二 判据三 结论
新增 REST API 端点 11 次 五步固定 错误码表分散在约定里 做
把统计页图表换成折线图 1 次 流程不定 无 不做
数据库 schema 迁移 每里程碑 1 次 纪律严格 序号、禁改历史 做(代价加权)

9.2 description 的艺术

9.2.1 唯一入口,决定生死

回顾第 08 章的机制:L1 元数据(name + description)是唯一常驻上下文的入口,任务与 Skill 的匹配只能靠这一两行字完成。它写含糊了,任务来了模型对不上号,Skill 等于不存在——正文写得再漂亮也永远没机会被读到。所以写 Skill 的精力分配是倒挂的:一半以上先花在 description 上。

9.2.2 写法要点

  1. 第三人称写"做什么":description 会被模型当作工具说明书阅读,"为 BookNest 后端新增 REST API 端点"优于"我会帮你加接口"——后者是聊天腔,不是说明书腔;
  2. 写清"何时用":描述用户会怎么提出这个任务,这是触发匹配的另一半依据;
  3. 埋入口语触发词:用用户的词,不只用你的术语。"接口""端点""API""加个筛选"都要在场,语义匹配才有抓手;
  4. 两三句以内:L1 是常驻成本,规范上限 1024 字符是护栏不是目标;装了 30 个 Skill 时,每个多写 100 字符就是一笔可观的常驻开销;
  5. 点出覆盖范围或完成标准(可选加分项):帮助模型在相近 Skill 之间抉择——"加字段"这个词在 add-api-endpoint 和 db-migration 里都可能出现,靠范围描述才能分对。

9.2.3 三组坏 vs 好对比

第一组:口号式——没有动作,没有边界。

# 坏
description: 处理与数据库相关的各种任务。
# 好
description: 为 BookNest 管理数据库 schema 迁移(新增表与字段)。当用户要求
  "加个字段/建表/改表结构/做迁移"时使用;纪律包括序号递增、已应用文件禁改、
  必须可回滚,完成标准是校验脚本通过且迁移可在干净库上重放。

点评:坏版里"数据库""任务"两个词几乎所有项目话题都沾边,却没有一个具体动词,模型无从判断该不该触发。好版第一句是动作(管理迁移),第二句是触发场景(用户的原话),末句给了完成标准。

第二组:太宽泛——来者不拒,误触发成灾。

# 坏
description: 当用户要求添加任何功能时使用,生成符合项目规范的代码。
# 好
description: 为 BookNest 后端新增 REST API 端点(后端路由)。当用户要求
  "加一个接口/端点/API"或实现包含新 HTTP 路由的功能时使用;不适用于纯前端
  页面、一次性数据脚本与数据库迁移。

点评:坏版"任何功能"会把加页面、换图标、写脚本全部吸过来——触发率 100%,几乎全是误触发。好版用两个手段收窄:限定词("后端路由")+ 排除清单("不适用于……")。收窄 description 不会降低它的价值,只会提高命中的纯度。

第三组:术语腔——只有你的词,没有用户的词。

# 坏
description: 用于执行符合分层架构规范的 RESTful 资源创建工作流。
# 好
description: 为 BookNest 后端新增一个 REST API 端点。当用户说"帮我加个接口"、
  "新增一个端点"、"books 要能按标签筛选"这类涉及新路由的任务时使用;
  覆盖路由、服务层、入参校验、测试与统一错误格式。

点评:坏版每个词都"正确",却没有一个词是用户会说出口的。用户说"帮我加个接口",description 里没有"接口""加"这些字眼,语义匹配只能硬凑。好版把用户真实表述直接引号引进去——触发词的来源不是术语表,是你和同事平时说话的录音。

9.3 正文写作法:给代理的操作手册

9.3.1 心态转换:新同事交接文档

写正文前先换脑子:想象你要休长假,把活交接给一个技术不错但不了解你项目的同事——你写的交接纸上不会有"REST 的历史",只会写"第一步去哪个文件、第二步跑什么命令、什么情况要停下来问我"。Skill 正文就是这张交接文档,读者是代理,不是人:人读教程为了理解原理,代理读正文为了立刻行动。

9.3.2 五条写作纪律

纪律一:命令式、步骤化。 祈使句 + 编号,每步一个明确动作:

教程腔(写给学习者):
在 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 永远不会发生,细节等于不存在。

9.4 资源组织:scripts、references、assets

9.4.1 三个目录的职责边界

第 08 章介绍过三个约定目录,这里补全判断细则:

  • scripts/——确定性工作交给脚本。代码生成器、格式校验器、统计工具:脚本的输出是确定的,比让模型每次"即兴发挥"可靠得多;
  • references/——给代理"读"的知识。按主题拆文件,一次只读需要的那份;
  • assets/——给代理"抄"的骨架。模板、脚手架、清单,复制后填空即用。

判别法:需要理解内容才能用 → references;复制改空就能用 → assets。

9.4.2 何时用脚本,何时写正文指令

任务特征 归宿 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 里涉及网络请求的脚本,白名单校验都是不可省略的门槛。

9.5 完整案例走查:从零写出 add-api-endpoint

9.5.1 定名与目录

从第 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           # 路由文件骨架,复制后填空

9.5.2 SKILL.md 全文

---
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 缺字段需要用户决策):停下来说明并等待,禁止跳过该步或自造约定。

9.5.3 逐条解读写作决定

  1. name 与目录名一致:口头引用、斜杠调用(/add-api-endpoint)、目录查找三者对齐,少一层映射就少一类错误;
  2. description 三段式:第一句"做什么"(新增端点+保证约定);第二句"何时用",直接引用用户口语("帮我加个接口");末句给完成标准,帮模型区分相近任务。触发词覆盖了"接口/端点/加筛选/归并"等多个变体;
  3. "何时使用"段:正文第二次确认触发,并划出"只管新增"的边界——修改类任务不至于把五步全部重跑一遍;
  4. 五步编号 + "缺任何一步视为未完成":把完成判定从"感觉做完了"变成"清单核对"。第 08 章示例正是这一段,此处补齐了每步的验证命令(第 4 步);
  5. 路径全部写全:server/src/routes/<资源>.ts 而非"路由文件里"——代理不用猜,猜就是漂移的起点;
  6. 两处指路 references/:错误码与测试断言都是"篇幅大、低频查"的知识,留在正文会撑爆 L2,指路后按需付费(L3);
  7. checklist 里嵌脚本命令:把"完成定义"落到机器可判定(退出码 0),这是 9.4 节"确定性交给脚本"原则的直接应用;
  8. 汇报格式 + 停下来问条款:前者稳定输出形态,后者堵住最危险的失败模式——代理为了"完成任务"而跳步或编造约定。

9.5.4 配套校验脚本 check_endpoint.mjs

设计约定先行:支持 --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 解析),目标是抓"路由没挂校验""业务写进路由层"这类高频疏漏,不追求编译器级的完备。脚本求确定与够用,不求完备——过度工程化校验脚本本身就是在制造新的维护负担。

贯穿项目实战:轻读 BookNest

目标:把 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"

最佳实践

  1. 动手前过 9.1.3 决策清单:三个判据全满足才做,"出错代价高"可加权,一次性任务直接否决;
  2. 把一半以上精力投在 description:第三人称"做什么"+ 用户口语"何时用",两三句内收笔;
  3. description 里放一个"不适用于"排除清单,宁可收窄也不要来者不拒;
  4. 正文开头重申触发条件与边界(何时用/何时不用),给模型第二次确认机会;
  5. 每个步骤写全路径与验证命令,把"完成"定义成可勾选的 checklist;
  6. 能写成"输入→确定输出"的工作交给 scripts/,脚本要有 --help 与明确退出码;
  7. 大而低频的细节外移 references/ 并在正文指路,正文控制在几百行内;
  8. 涉网的脚本一律先做域白名单校验再发请求,零凭据字面量,密钥走环境变量;
  9. 在正文末尾规定汇报格式,并写明"无法完成时停下来说明,禁止跳步"。

本章小结

写出优秀的 Skill 是一条流水线:先用三个判据(重复 ≥ 3 次、流程固定、含私有约定)筛掉不值得做的任务;再把一半精力押在 description 上——它是 L1 唯一入口,要同时说出"做什么"与"何时用",并埋入用户的口语触发词;正文是写给代理的新同事交接文档:命令式、步骤化、判断标准 checklist 化、输出格式约定化,细节外移 references/ 且必须指路;资源按 scripts/(确定性)、references/(深知识)、assets/(骨架)各司其职。9.5 的 add-api-endpoint 走完了全流程,Skill 至此"能用了"——但"能触发一次"与"每次都触发对、做得好"之间,还隔着一整个调试与进化的闭环,这正是下一章的主题。

  • 三个判据定生死前置关:重复 ≥ 3 次、流程固定、含私有约定,出错代价可加权;
  • description 是唯一触发入口:"做什么 + 何时用" + 口语触发词 + 不适用于清单;
  • 正文 = 交接文档:命令式步骤、可判定 checklist、输出约定、几百行内、指路 references;
  • 确定性交脚本(退出码可验证),判断类写指令,涉网脚本必带域白名单;add-api-endpoint 已落地 BookNest,显式调用保底,隐式命中率交给第 10 章定量验证。

动手练习

  1. 判据演练:列出你自己项目最近两周做过的 5 项工作,用 9.1.3 决策清单逐项打分,写出"做/不做"的裁决与理由——至少找出一个"你想做但清单否决"的反例。
  2. description 改写:把 9.2.3 三组坏 description 各改写一版适配你自己的项目(替换资源名与口语词),并为每版列出 3 条"应该触发它的用户原话",检查这些原话的关键词是否真的都在 description 里。
  3. 脚本化演练:找出你最近一次让模型"即兴发挥"的确定性操作(如生成某种样板文件、核对某种格式),把它改写成 30 行以内、带 --help 与退出码约定的脚本,并对比模型手写与脚本输出的一致性。
  4. 综合实践(BookNest):按第 08 章候选清单第二名 db-migration,写出完整 Skill:SKILL.md(触发词、迁移纪律步骤、完成 checklist、指路)+ scripts/check_migration.mjs(校验迁移文件序号连续、历史文件未被改动),落进 skills/db-migration/ 并按 Conventional Commits 提交。第 10 章将对它执行触发测试与修订的完整闭环。

导航

📄 第 10 章 · Skill 调试与进化:让技能库越用越准 10-skill-testing-evolution.md

第 09 章的 add-api-endpoint 已经"能用了",但写完不等于稳定:它可能在该出场时沉默(不触发)、不该出场时抢戏(误触发)、出场了又漏三步(没做好)。Skill 库不是一次写出来的,是持续养出来的——本章给出一套调试、评测、进化与团队治理的完整闭环,让技能越用越准。

学习目标

  • 能区分 Skill 的三种失败模式(不触发 / 误触发 / 触发了没做好),并用"二分定位法"快速归因;
  • 能构造正反例样本集并执行触发测试,量化一个 Skill 的命中率与误触发率;
  • 能建立金标准样例回归与"小步修改"纪律,安全地迭代 description 与正文;
  • 能把失败会话的教训用"下次遇到 X 应该 Y"句式回收进 SKILL.md 或 references;
  • 能按命名规范、评审清单与贡献流程治理团队共享的 Skill 库;
  • 能说清 Skill 与 AGENTS.md、hooks、subagent 的组合分工。

10.1 三种失败模式与诊断

Skill 的失败几乎都能归入三类。诊断的第一原则:先定位失败发生在哪一级——是没进 L2(触发问题),还是进了 L2 但执行不好(正文问题)。

10.1.1 模式一:不触发(该用没用)

现象:你说出典型任务话术,代理却按通用方式自由发挥——五步约定没人执行,错误格式各行其是。

常见根因(按概率排序):description 太弱(口号化、缺关键词,见 9.2.3);用户的表述与 description 词汇脱节(你说"落库",它只写了"数据库");放置位置错误(项目 Skill 放进了个人目录,反之亦然,见 8.4.3);frontmatter 语法错误导致整个 Skill 加载失败。

诊断步骤:

  1. 显式调用 /add-api-endpoint 试一次——能正常加载正文,说明文件本身没问题,问题出在触发匹配(description),转入第 2 步;连显式调用都失败,问题在文件本身:查目录层级、frontmatter 语法、name 是否合法(小写连字符)、放置位置与生效范围;
  2. 触发问题时,逐词对照用户原话与 description:找出原话里"没被接住"的词,补进 description。

10.1.2 模式二:误触发(不该用乱用)

现象:纯前端任务被拉着加了后端接口,写一次性脚本却套上了完整流程。它比不触发更危险——用户稍不留神,产出就悄悄跑偏,甚至改坏无关文件。

常见根因:description 太宽泛("任何功能都找我");示例词撞车(description 里写了"加筛选",前端任务也说"加筛选");缺少"不适用于"排除清单。

诊断步骤:

  1. 找出吸附错误任务的"磁铁词"——通常是 description 里过泛的大名词或过宽的例子;
  2. 加限定词收窄("后端路由""schema 迁移"),并补"不适用于"排除清单;
  3. 重跑反例确认已排除,再重跑正例确认没有误伤——收窄是走钢丝,两头都要验。

10.1.3 模式三:触发了没做好

现象:Skill 加载了,产出仍有缺项——没跑校验脚本、没读 references、漏了测试步骤,每次漏的还不一样。

常见根因:正文有洞(某步写成了"建议"而非"必须");关键细节缺失(错误码表根本没写);指路句缺失导致 L3 没发生;步骤过长导致注意力被稀释。

诊断步骤:复现一次,把代理的实际执行与 SKILL.md 逐条对照——它读了什么、跳了什么:

  1. 跳过的步骤:检查它是否是编号步骤 + 强制语气("禁止跳过"),还是被写成了"可以/建议";
  2. 缺失的细节:该落 references/ 的深知识是否写了、正文是否指路了;
  3. 该确定 yet 靠模型自觉的动作:是否该下沉为 scripts/ 脚本与 checklist 项。

10.1.4 二分定位法总结

两类问题用两刀切开:第一刀用显式调用切分"文件问题 vs 触发问题";第二刀用执行记录切分"没写 vs 写了没执行"。每刀之后,问题空间缩小一半,通常两三轮就能定位到具体行。日常排查先用这张速查表对号入座:

你看到的现象 失败模式 第一检查点
说了典型话术,代理自由发挥 不触发 description 缺哪个关键词
换个项目/目录就不触发 不触发 放置位置与生效范围
不相干任务被拉去跑流程 误触发 description 的磁铁词与排除清单
触发了但每次漏的步骤不同 没做好 步骤是否"必须"语气 + checklist
触发了但细节总出错 没做好 references 是否写了、是否被指路

10.2 评测方法

10.2.1 触发测试:正反例样本集

把"感觉能触发"变成"测量出触发率":

  • 样本集设计:5–10 条真实任务表述——你自己平时怎么说话就怎么写,别编"标准句";正例(应触发)至少 5 条、反例(不应触发)至少 3 条,反例要覆盖最容易撞车的近邻任务;
  • 测试操作:每条样本开一个全新会话(干净上下文,避免同会话历史污染判断);显式调用的样本不计入触发测试——它绕过了语义匹配,测不到 description 的真实水平;
  • 控制变量:同一模型、同一表述、同一会话类型,只让被测的 description 作为变量。

记录表模板(可直接存进 docs/tasks/):

# add-api-endpoint 触发测试记录(2026-10-06,模型:<name>)
| # | 任务表述(原话)                     | 期望   | 实测   |
|---|--------------------------------------|--------|--------|
| 1 | ...                                  | 触发   | 触发   |
| 6 | ...                                  | 不触发 | 误触发 |
结论:正例命中 5/5;反例误触发 1/3 → 待修订。

10.2.2 完成质量回归:金标准样例

触发对了只是及格,做得对才是目的。为关键 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。

10.2.3 小步修改纪律

  • 一次只改一处:description、某个正文步骤、某个脚本,三者分开改、分开验;两处改动混在一起,归因立即失效;
  • 改前记录现象,改后立即验证:先写下"什么任务、什么表现",再动手,改完马上重跑相关样本;
  • 区分改动类型:description 改动影响触发,正文改动影响执行质量——验证手段分别是触发测试与金标准回归,不要混用。

10.3 迭代进化闭环

10.3.1 从失败会话回收改进

最宝贵的改进素材是每一次失败会话——尤其是你纠正代理的那个瞬间(你打断它的时刻,就是现实与 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)+ 金标准回归(期望无漂移)。

10.3.2 修订的节奏与版本意识

SKILL.md 没有强制的版本字段——版本化靠 Git:Conventional Commits 区分改动性质(fix(skills): 修触发、feat(skills): 加步骤、refactor(skills): 重构目录),提交信息写清三件事:改了什么、为什么、跑了哪些样本。节奏上避免两个极端:

  • 想到就改:一天改五处,谁都说不清哪处起效——description 类小修可以当天做,正文类改进攒到每周一次"技能库巡检"统一处理;
  • 从不回头:Skill 库三个月不动,教训全留在聊天记录里——把"巡检"放进每周日程,翻一遍失败记录,逐条回收。

重大重构(目录调整、脚本重写)在提交信息标注影响面,团队库可打 tag 标记里程碑。

10.3.3 description 的 A/B 对比试验

对 description 的改版别凭语感拍板,做 A/B 试验:

  1. 固定一个样本集(正反例各 ≥5/≥3,见 10.2.1);
  2. A 版跑一轮,记录命中数与误触发数;
  3. 换 B 版(新会话、同表述、同模型),同集合再跑一轮;
  4. 数据定稿,并把两版数据一并记入复盘文档。
# 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 版缺失的那条正例词合并进来复测。"——合并后必须第三轮复测,因为两个优点拼在一起未必还是优点。

10.4 团队 Skill 库治理

10.4.1 位置与版本化:进 Git,走评审

团队 Skill 库放 <repo>/skills/,与代码同库同评审(附录 D 的目标结构已预留)。Skill 是纯文本文件夹——评审、回滚、追溯全部复用 Git 与 PR 的既有设施,把它当代码资产管理,而不是文档附件。个人方法论仍放用户目录(8.4.3),不进团队库。

10.4.2 命名规范

  • 动词短语开头:add-、check-、release-、review-——名字即动作,与 description 第一句呼应;
  • kebab-case、64 字符内、与目录名一致(斜杠调用与目录查找依赖这一点);
  • 任务导向:看名字就能猜到用途;禁用 helper、util、misc、v2 这类零信息词;
  • 反例:tool-helper-v2(看不出干嘛)、endpoint(缺动词与对象)、AddEndpoint(大小写破坏约定)。

10.4.3 评审标准清单

团队 PR 的 Skill 评审四查,逐项可判定:

□ 可触发:description 含用户口语触发词?附 ≥5 正例 + ≥3 反例的测试记录?
□ 可执行:每个步骤动词有明确对象、路径写全?完成标准可判定(非"质量良好")?
□ 脚本安全:无凭据字面量(密钥全走环境变量)?涉网脚本有域白名单?
           有 --help 与明确退出码约定?
□ 指路完整:references/ 与 assets/ 的每个文件都被正文指路?
           (没被指路的文件等于不存在,应删除或补指路)

10.4.4 新人贡献流程

五步闭环,与本章方法一一对应:

  1. 搜:先查官方示例库、社区清单与团队内部库(先搜再造,8.5.1);
  2. 草:按第 08 章最小骨架起步,只覆盖一个任务;
  3. 测:显式调用验证正文质量 → 正反例样本集验证触发(10.2.1);
  4. 交:PR 附测试记录表与金标准样例;
  5. 评:维护者按 10.4.3 清单逐项审,通过后合并。

10.5 与其他机制的组合

10.5.1 引用而非复制:AGENTS.md 是单一事实源

Skill 正文常需要引用全局约定(错误格式、分层规则)。纪律是引用,不复制:"错误码表见 references/api-conventions.md;全局分层约定以 AGENTS.md 为准。"理由:同一知识出现两处,必有一处在说谎——AGENTS.md 改了统一错误格式,Skill 里的副本三个月没人动,代理就会读到过期版本。这就是单一事实源(single source of truth)原则在 Skill 库的应用。

10.5.2 skill 教方法,hooks 强制底线

Skill 正文写"密钥零字面量"是教——模型大概率遵守,但那是概率;PreToolUse hook 扫描暂存区并拦截是强制——确定发生。两者不是重复,而是分工:Skill 负责"为什么要这样做、怎么做更对"的方法论,hooks 负责"无论如何不许越过"的硬底线。凡是出错代价不可逆的规则(凭据、删库、外发数据),必须落在 hooks 这类确定性机制上(第 07、13 章),Skill 正文只做解释与指引。

10.5.3 skill 与 subagent 的配合

Skill 回答"怎么做"(方法论注入当前上下文);subagent 回答"谁来做"(独立上下文并行执行、结果回传)。组合拳在多 Agent 场景尤其有效:主代理触发 Skill 拿到标准流程后,把自包含的子任务派给多个 subagent 并发执行——每个 subagent 的提示词里都带上同一条 Skill 规则,保证干法一致。架构模式的展开在第 17 章。

机制 回答的问题 生效方式 BookNest 例子
AGENTS.md 项目是什么样的 会话启动常驻 分层约定、NFR 清单
Skill 这类任务怎么干 任务匹配后按需加载 add-api-endpoint 五步
Hooks 底线如何强制 生命周期确定性执行 提交前凭据扫描
Subagent 这活谁来干 主代理派发自包含任务 多端点并行实现

贯穿项目实战:轻读 BookNest

目标:盘点 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(确定性执行)

最佳实践

  1. 每个 Skill 上线前跑一轮触发测试:≥5 正例 + ≥3 反例,新会话逐条测,记录表入库;
  2. 为关键 Skill 建金标准样例,任何改动后做产出回归对比;
  3. 小步修改:一次只改一处,改前记录现象、改后立即重跑对应验证;
  4. 失败会话当天复盘,"下次遇到 X 应该 Y"必须落到 SKILL.md、references 或脚本里才算完成;
  5. description 改版走 A/B:同一样本集两版各测一轮,数据定稿,合并优点后第三轮复测;
  6. skills/ 进 Git,提交信息写清"改了什么、为什么、跑了哪些样本";
  7. 团队评审四查:可触发、可执行、脚本安全(无凭据、涉网有白名单)、指路完整;
  8. 每周固定一次技能库巡检,翻失败记录逐条回收,避免"想到就改"或"从不回头";
  9. 组合分工记牢:约定看 AGENTS.md、方法看 Skill、底线靠 hooks、并行靠 subagent。

本章小结

Skill 的进化是一个测量驱动的闭环:三类失败(不触发、误触发、触发了没做好)用"二分定位法"两刀归因——显式调用切分文件问题与触发问题,执行记录切分"没写"与"写了没执行";评测靠两件工具——正反例样本集量化触发、金标准样例守护产出质量;进化靠纪律——小步修改保证可归因,"下次遇到 X 应该 Y"把每次失败会话变成 Skill 文件里的新增行,Git 与 Conventional Commits 记录每一次"为什么";团队治理则把个人手艺变成组织资产:skills/ 进库、动词短语命名、四查评审、新人五步贡献。最后,Skill 不是孤岛——约定引用 AGENTS.md 保持单一事实源,底线交给 hooks 确定性执行,并行执行交给 subagent(第 17 章)。至此 Part 3 收官:你的代理已经有了"开机即懂"的配置与"随取随用"的技能库,下一部分转向更根本的问题——如何设计边界,让代理敢放手干又干不出格。

  • 三种失败模式各有诊断路径,二分定位法:先显式调用一刀,再对执行记录一刀;
  • 触发测试(正反例样本集、新会话)+ 金标准回归,把"感觉"换成"数据";
  • 进化闭环:复盘 → 落点(正文/references/脚本/description)→ 小步修订 → 回归 → Git 留痕;
  • 团队治理:进 Git、动词短语 kebab-case、四查评审清单、五步贡献流程;
  • 机制组合:AGENTS.md 管约定(单一事实源)、Skill 管方法、hooks 管底线、subagent 管并行。

动手练习

  1. 样本集构建:为你项目里的某个 Skill 写 8 条正反例(≥5 正、≥3 反),逐条开新会话测试,产出命中率表并存入 docs/tasks/——如果某条结果出乎意料,先按 10.1 诊断再改。
  2. 二分定位演练:故意删掉某个 Skill description 里的核心关键词制造"不触发",按 10.1.1 的两步诊断并修复,写下每一步的依据(第一刀切在哪、第二刀切在哪)。
  3. A/B 试验:给同一 description 写 A/B 两版,用同一样本集各测一轮,记录"命中数 / 误触发数",写出你的定稿结论与理由。
  4. 综合实践(BookNest):对 db-migration 完整执行本章闭环——设计触发测试样本集(含至少一条易撞车反例,如"改一下 books 表数据"这类数据操作而非 schema 变更)→ 依据结果做一处真实修订(diff + 理由 + 复盘记录)→ 建金标准样例并回归 → 按 Conventional Commits 提交,全部记录归档 docs/tasks/。这份档案就是你团队 Skill 库治理的开端。

导航

📄 第 11 章 · Agent 边界设计:信任要有技术护栏 11-agent-boundaries.md

进入 Part 4「边界与安全」。第 7 章回答了"怎么写一条权限规则",本章往后退一步,回答设计层的问题:面对一个能读写文件、执行命令、访问网络、触发外部服务的代理,你的信任要怎么划分、怎么表达、怎么收放? 边界划得好,你才敢单开一个会话让它自主跑半小时;划得不好,每一次放手都是赌博。

学习目标

  • 能解释代理失败的两种形态(能力失败与越权失败),说清为什么"叮嘱代理小心点"不构成护栏;
  • 能用五个边界维度——文件系统、执行、网络、数据、影响——系统性地审视一个代理的权限面;
  • 能按任务阶段选择匹配的权限模式(plan / default / 白名单自动执行 / 完全托管),设计"先紧后松"的放权节奏;
  • 能按最小权限原则(principle of least privilege)落地"默认拒绝、按需开洞、任务结束收权、定期审计"四步;
  • 能说清边界的四层实现(权限规则、hooks、沙箱、流程约束)各自拦截什么、失效后由谁兜底;
  • 能为贯穿项目 BookNest 设计一份三档"边界矩阵",并说清升降档的触发条件。

11.1 为什么需要边界:失败的不是"不会",而是"越权"

11.1.1 两类失败

代理的失败分两种,性质完全不同:

  • 能力失败(capability failure):它不会做。写不出正确的正则、理解错了需求、代码跑不通。这类失败恼人但基本无害——测试会红、页面会白屏,你一眼就看出来,重试或换个说法就好。
  • 越权失败(authorization failure):它会做,但做了不该做的事。删错了文件、推错了分支、把密钥写进代码、把数据发给了陌生人、装了不该装的依赖。这类失败才是真正危险的:它往往"成功执行",不留报错,等你发现时损失已经发生。

一个反直觉的事实:模型越强,第一类失败在减少,第二类反而在放大。 代理越能干,能触达的东西越多——它自己装依赖、自己跑迁移、自己调外部 API;每多一项能力,越权面就大一分。第 2 章讲过,工具调用(tool use)让代理从"聊天"变成"干活",本章讨论的就是干活的半径怎么控制。

11.1.2 叮嘱不是护栏

多数人的第一反应是在 AGENTS.md 里写"请小心,不要删除文件,不要把密钥写进代码"。这有用吗?有用,但不可靠。 项目指令本质上是对模型的"劝说"——模型会尽力遵守,但遵守是概率性的:上下文长了会淡忘、任务复杂了会顾此失彼、遇到一次提示注入(prompt injection,外部内容里藏着诱导代理的恶意指令)就可能整个抛在脑后。

护栏(guardrail)的定义恰恰相反:不依赖模型"想起来",而是让越权动作在技术上无法发生,或发生时立即被拦截。 两者的关系:

叮嘱(AGENTS.md) 护栏(规则 / hooks / 沙箱)
本质 道德劝告 技术约束
执行者 模型自觉 运行时强制
失效方式 遗忘、被诱导 只能被显式修改(走评审)
定位 引导行为倾向 划定不可逾越的红线

结论一句话:边界 = 把信任编码成技术约束。 叮嘱继续写(它决定代理"往哪走"),边界必须另配(它决定代理"不能去哪"),两者不可互替。

11.1.3 边界设计的三个产出物

为一个代理(或一支代理团队)做边界设计,最终落进仓库的是三样东西:

  1. 权限清单:allow / ask / deny 三类规则(第 7 章),回答"默认能做什么";
  2. 边界矩阵:按任务阶段组织的规则集合(见 11.3 与贯穿项目实战),回答"什么阶段松到什么程度";
  3. 升降档规则:什么证据可以放权、什么信号必须收权(见 11.3.3),回答"信任怎么流动"。

11.2 五个边界维度(本章核心)

审查代理权限面最怕"想到哪算哪"。下面五个维度构成一张完整检查单——代理的一切行为都能归进这五类。每个维度先看一个真实感很强的失控故事,再给最小护栏写法。

11.2.1 文件系统边界:能动哪些路径

失控故事:周五晚上,你让代理"清理一下项目里没用的东西"。它翻了一圈,判断 data/ 目录"像是缓存产物",把 data/booknest.db(你三个月的读书数据)连同 scripts/backup.mjs 一起删了。周一你打开应用,一切归零。

分析:代理对"没用"的判断和你不一样——它没有"数据会丢"的概念,只有文件相似性。文件系统边界要回答:读、写、删,分别允许落在哪些路径上? 最小护栏(第 7 章语法):

{
  "allow": ["Edit(server/**)", "Edit(web/**)"],
  "deny": ["Read(data/**)", "Edit(data/**)", "Write(data/**)"]
}

11.2.2 执行边界:能跑哪些命令与脚本

失控故事:代理跑测试时报 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:*)"]
}

11.2.3 网络边界:能访问哪些域名、能否出网

失控故事:调试一个报错时,堆栈里带着你内网接口的 URL 和一段用户数据。代理"想到"可以把日志贴到某个在线解析服务,POST 了出去——内网拓扑和敏感数据就此离开了你的机器。

分析:网络边界要回答:代理能向哪些域名发请求?默认能不能出网? 白名单(allowlist)不仅限流,还是 SSRF(Server-Side Request Forgery,服务端请求伪造)防护的第一道闸门——代理只能访问你点名过的域,而不是任意 URL:

{
  "allow": [
    "WebFetch(domain:nodejs.org)",
    "WebFetch(domain:developer.mozilla.org)"
  ]
}

未列出的域名全部落入默认模式询问,等于"默认不出网"。

11.2.4 数据边界:能读到哪些密钥与敏感文件

失控故事:代理为排查连接问题读了 .env,拿到数据库密码。之后它"贴心地"生成了一篇《部署指南.md》,把连接串原样贴了进去——文档进了 Git,密码进了提交历史,全组可见。

分析:数据边界要回答:代理能读到哪些密钥、凭据与隐私数据?读到了之后能流向哪里? 两个要点:入口上 deny 掉密钥文件;出口上防"复制进代码 / 文档 / 会话"——第 7 章的 Stop 凭据扫描 hook 就是出口闸门:

{
  "deny": ["Read(.env)", "Read(.env.*)", "Read(**/.env*)", "Read(secrets/**)"]
}

密钥永远通过环境变量注入(如 process.env.BOOKNEST_DB_PATH):代理只见到变量名,见不到值。

11.2.5 影响边界:能触发哪些外部状态变更

失控故事:你让代理"帮忙处理一下 issue"。它用自己的 GitHub 凭据,给一个不相关的开源项目发了条措辞客气的评论。又一回,"把新版本部署一下"被理解成直接对生产环境执行了发布脚本。

分析:前四个维度的爆炸半径都在本机,影响边界管的是对外部世界做的不可逆动作:发 issue、发评论、发消息、部署、扣费、删资源——对端是真实世界,撤不回来:

{
  "defaultMode": "default",
  "ask": ["Bash(git push:*)"],
  "deny": ["Bash(git push --force:*)"]
}

外加一条纪律:MCP(Model Context Protocol)工具按需启用(第 6 章)——这周不碰 issue 就别挂载 mcp__github,工具不在场,误用就不可能发生。

11.2.6 五维度检查单

维度 核心问题 失控关键词 首选护栏载体
文件系统 能读写删哪些路径? "清理"、批量修改、误删数据目录 路径级 allow / deny
执行 能跑哪些命令与脚本? 自我修复、sudo、管道脚本 Bash 规则 + PreToolUse hook
网络 能访问哪些域?能否出网? 上传日志、访问陌生域、SSRF WebFetch 域白名单
数据 能读哪些密钥与敏感文件? 读 .env、写进文档、贴进会话 Read deny + 凭据扫描 hook
影响 能触发哪些外部变更? 发 issue、部署、发消息 ask 关口 + MCP 按需启用

新开一个项目、或接入一个新的 MCP 服务时,把这张表过一遍——五分钟,换一份完整的权限面认知。

11.3 权限模式与任务阶段的匹配

11.3.1 四档权限模式

以 ZCode 为例(截至 2026 年 10 月,Claude Code 的 default / plan / acceptEdits / bypassPermissions、Codex CLI 的 suggest / auto-edit / full-auto 与之一一对应):

模式 行为 适用 风险
plan(计划/只读) 只能读与分析,产出方案,不能改 探索陌生代码、定方案 几乎为零
default(默认确认) 未命中 allow 的动作一律询问 日常开发 确认疲劳
白名单自动执行 allow 清单 + acceptEdits 放开编辑 高信任重复任务 清单沉淀出错
bypassPermissions(yolo,全放行) 全部免确认 仅限隔离环境(第 12 章) 一切损失无护栏

11.3.2 阶段 × 模式矩阵

任务阶段 推荐模式 典型动作 关键约束
探索陌生代码、定方案 plan 读代码、画结构、写规格 方案人审通过后才动手(第 15 章)
日常开发(熟悉模块) default + 精选 allow 写代码、跑测试、查文档 commit / push / install 保持 ask
高信任重复任务 白名单自动执行 批量重命名、格式化、生成样板 任务范围封闭、动作幂等、有检查点
隔离环境批量执行 完全托管(yolo) 容器里跑评测、批量迁移演练 爆炸半径由环境封顶(第 12 章)

11.3.3 先紧后松、随任务切换

放权的原则是证据驱动(evidence-based trust):

  1. 新任务、新模块、新依赖 → 从紧(plan 或 default);
  2. 代理连续表现出对项目约定的理解 → 把反复手动放行的动作沉淀为 allow;
  3. 一旦切换上下文(进入陌生领域、接入新工具、出现一次越权)→ 立即回到紧。

要点:松是攒出来的,紧是随时可回的。 把这个节奏写进团队约定,比争论"到底该不该开 yolo"有效得多——正确答案是"看阶段"。

11.4 最小权限原则的落地

最小权限原则(principle of least privilege):任何主体(人、程序、代理)只应拥有完成当前任务所需的最小权限集合。落到代理身上,是四步循环。

11.4.1 默认拒绝,deny 先行

defaultMode 永远设为"默认确认"而不是放行——没有明确允许的,就都需要过问。配置权限清单时按 deny → ask → allow 的次序写:先划红线,再立关口,最后才给便利。理由有二:deny 的判定优先级最高(第 7 章);更重要的是,"先写 deny"会强迫你先想清楚什么绝不能发生,而不是先想代理能帮什么忙。

11.4.2 按需开洞

任务需要什么,才临时放行什么,而且开在最小范围:要装一个包,开 Bash(npm install left-pad:*) 而不是 Bash(npm install:*);要看一个日志,开 Read(logs/2026-10-06.log) 而不是 Read(logs/**)。

11.4.3 任务结束收权

临时开洞的规则放 settings.local.json(不进 Git 的个人层),任务完成就删。经验法则:每场会话开始时,settings.local.json 里不该有上一场任务的残留。 一条规则连续三场任务都被用到,才值得评审后上移到项目共享层。

11.4.4 定期审计权限面

权限面只增不减是常态,所以要定期收:

  • 里程碑收口时:git log -p .zcode/settings.json 复盘每条新增规则,问一句"还需要吗";
  • 让代理自审:让代理读配置并按五维度汇报暴露面(见下方对话示例);
  • 对照红线清单:托管层的 deny 是否仍覆盖全部红线,新接入的 MCP 服务是否还没纳入边界。

对话示例(权限面自审):

你:读一遍 .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 规则删掉。

11.5 边界的分层实现总览

没有哪一层护栏是完美的,工程上的答案是纵深防御(defense in depth):多层机制各管一段,任何一层被突破,下一层接住。

11.5.1 四层职责分工

层 机制 裁决方式 拦得住 失效时谁兜底
L1 权限规则(第 7 章) allow/ask/deny 静态白名单 声明式、零开销 已知的危险模式 语义级绕过 → L2
L2 Hooks(第 13 章) 生命周期脚本守门 可编程、看内容 需理解内容/上下文的危险 环境级灾难 → L3
L3 沙箱与隔离(第 12 章) 分支、容器、密钥分离 环境硬限制 一切规则被绕过后的损失上限 数据丢失 → 备份
L4 流程约束(第 15/20 章) 计划模式、人工检查点 人在回路 方向性错误、系统性偏差 ——(最外层)

11.5.2 纵深防御图

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

目标:为 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;联调期→发布期——覆盖率达标、准备收口发版。
  • 降档触发(回到紧):引入任何新依赖;代理出现一次越权动作(无论有无损失);进入陌生代码(如第 22 章的 legacy 版)。
  • 任何档位都不放开的三条红线: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 复盘每条规则

最佳实践

  1. 按 deny → ask → allow 的次序写权限清单:先划红线,再立关口,最后给便利;
  2. defaultMode 永远保持"默认确认",完全托管模式只属于隔离环境;
  3. 新项目先用五维度检查单(文件系统/执行/网络/数据/影响)做一次权限面体检;
  4. 任务阶段决定模式:探索期 plan、日常 default、重复任务白名单、隔离环境才托管;
  5. 放权靠证据,收权靠直觉:出现异常立即回紧,不需要先证明出了事;
  6. 临时开洞开在最小范围(具体命令、具体文件),任务结束即收;
  7. 叮嘱写进 AGENTS.md,红线写进规则与 hook,两者缺一不可、不可互替;
  8. 边界四层(规则、hooks、沙箱、流程)按"彼此会失守"设计,层层兜底;
  9. 边界矩阵与权限配置随代码进 Git、走评审(guard: 前缀),让护栏成为团队资产;
  10. 定期让代理自审权限面,并对照五维度清单与红线清单逐项核对。

本章小结

本章把"信任代理"从一句口号变成一套可设计、可配置、可审计的工程对象。核心结论:代理的危险不在"不会"而在"越权",叮嘱不是护栏,边界是把信任编码成技术约束;五个维度(文件系统、执行、网络、数据、影响)构成权限面的完整检查单;权限模式要与任务阶段匹配——先紧后松、证据放权、随时回紧;最小权限原则通过"默认拒绝、按需开洞、任务结束收权、定期审计"四步落地;最后,边界不是单点而是纵深,权限规则、hooks、沙箱、流程约束四层各司其职,任何一层失守都有下一层接住。

  • 两类失败:能力失败无害可重试,越权失败"成功执行"才致命;
  • 五维度检查单:文件系统 / 执行 / 网络 / 数据 / 影响;
  • 阶段匹配:plan → default → 白名单 → 托管,先紧后松、随任务切换;
  • 最小权限四步:默认拒绝、按需开洞、任务收权、定期审计;
  • 四层纵深:规则(L1)→ hooks(L2)→ 沙箱(L3)→ 流程(L4),下一章深入 L3。

动手练习

  1. 五维度体检:为你手头的一个真实项目(或某个开源项目)按 11.2.6 的检查单做权限面审计,产出五维度表格,标出三项你认为最危险的暴露面与对应护栏写法。
  2. 失控复盘:从 11.2 的五个失控故事里选一个,假设发生在你的机器上,写出:直接损失、连带损失,以及"规则 / hook / 沙箱"三层各自能拦截它的哪一步。
  3. 设计边界矩阵:为一个"命令行待办事项工具"项目设计两档边界矩阵(功能开发期 / 发布期),覆盖五个维度,并写明升降档触发条件。
  4. 综合实践(BookNest):把本章的三档边界矩阵落成你仓库里的 docs/boundary-matrix.md,把"开发期"档写进 .zcode/settings.json;然后完整演练一次升降档——模拟从开发期切到联调期,让代理对照矩阵列出配置变更清单,人审后应用,并以 guard: 前缀提交全部变更。

导航

📄 第 12 章 · 沙箱与隔离:控制爆炸半径 12-sandboxing.md

第 11 章的边界设计回答"事前允许什么",本章回答"事后怎么兜底":假设规则被绕过、模型被诱导、代理犯下出乎意料的错误,损失如何被限制在可承受的范围内?这就是沙箱(sandbox)与隔离(isolation)的主题——控制爆炸半径(blast radius):任何一次失控,波及的范围都必须有事先画好的上限。

学习目标

  • 能建立沙箱思维:假设代理一定会犯错甚至被提示注入,把设计目标从"不出事故"转为"可回滚、损失有上限";
  • 能把 Git 用成第一道沙箱:专用分支、高频小提交检查点、干净工作区约定;
  • 能用 git worktree(工作树)为并行任务建立互不干扰的工作区;
  • 能实施文件系统与运行时隔离:限定工作目录、容器化、不信任的代码先在临时环境试跑、测试库与生产库分离;
  • 能实施网络与密钥隔离:出网白名单、生产凭证禁跑代理、环境变量注入而非文件复制;
  • 能设计审批门(approval gate)与"观察期 → 限权 → 放权"三阶段渐进放权流程。

12.1 沙箱思维:为失败而设计

12.1.1 假设一定会出事

第 11 章的边界再严,也要接受一个前提假设:代理一定会犯错——不是可能,是一定。 原因有三:

  • 模型本身有出错率,任务越复杂、步骤越长,出错概率越高;
  • 提示注入(prompt injection):代理读到的任何外部内容(网页、issue、README、依赖文档)都可能藏着诱导指令——你信任的是你的提示词,但代理执行的是它读到的所有内容;
  • 你自己也会犯错:权限配错一条、口述需求时少说一个限定词。

所以沙箱思维的设计目标不是"不出事故",而是两条硬指标:

  1. 可回滚(rollback):任何变更都能在几分钟内撤销;
  2. 损失有上限:任何一次失控,波及范围不超过事先画好的圈。

一句话:护栏决定事故的概率,沙箱决定事故的后果。两者要分开设计、分开验收。

12.1.2 纵深防御的四道墙

道墙 拦什么 什么时候救你
① 权限规则(第 7/11 章) 已知危险动作 代理想 rm -rf、读 .env 时直接被拒
② 沙箱与隔离(本章) 规则被绕过后的波及范围 等价命令绕过规则时,破坏被环境封在圈里
③ Git 检查点 错误的代码变更 改坏了,回滚到上一个绿测提交
④ 备份与快照 数据级灾难 数据库文件被删或损坏时恢复

这四道墙是串联的:每道墙都假设前一道会被突破。①被绕过(比如 npm i 绕过了 npm install 的 deny),②的独立环境与容器封住波及面;②内的变更改坏了代码,③的检查点回滚;连数据都被动了,④的备份兜底。本章 12.2–12.4 依次展开②③两道墙的具体搭法;①已在第 7、11 章讲透;④属于运维常规,只需记住一条:代理可达范围内的数据必须有备份,且备份本身对代理不可达(否则等于没备份)。

12.2 Git 作为第一道沙箱

Git 是成本最低、收益最高的隔离手段——你本来就在用,唯一的变化是把它从"版本管理工具"升级为"代理检查点系统"。

12.2.1 永远不在主干工作

约定只有一条:代理的一切变更只发生在专用分支(task branch)上,主干(main)只接受人审后的合并。它的价值常被低估:这条约定把"代理把 main 搞乱了"从一场灾难降级为"删掉分支重来"。

git switch -c task/server-search-api   # 任务分支命名:task/<范围-内容>

再配一道远端保险:在 GitHub / GitLab 上把 main 设为受保护分支(protected branch),禁止直推——本地约定失效时,远端会拒绝。

12.2.2 高频小提交 = 可回滚检查点

检查点(checkpoint)思想来自游戏:打到存档点再前进。给代理定一个提交节奏:

时机 动作 提交信息示例
一个测试由红转绿 立即提交 test(server): 补搜索接口空查询用例
一个完整的小功能 提交 feat(server): 全文搜索 API
大改动拆步执行,每步完成 提交 refactor(web): 抽出 useBookList hook
上下文即将压缩或会话将结束 强制落盘 checkpoint: 搜索接口完成 70%,进度见 docs/tasks/…

原则一句话:每个"确认没坏"的时刻都是一个检查点,宁滥勿缺。 小提交的代价是历史略碎,收益是任何时刻都能 git revert / git restore 精确回退——这笔交换在代理时代非常划算。提交信息遵循 Conventional Commits(BookNest 的 NFR-5),回滚时也更容易定位语义。

12.2.3 干净工作区约定

代理开工前,工作区必须是"干净"的:要么全部已提交,要么显式暂存(stash)。脏工作区会让回滚失去意义——你分不清哪些变更来自代理、哪些是你昨天的半成品。

开工对话示例:

你:继续昨天的任务,把搜索接口写完。
代理:开工检查:当前分支 task/server-search-api,工作区干净,
      上一个提交 checkpoint: 搜索接口完成 40%。
      我先跑一遍现有测试确认基线全绿,再继续实现。

把"开工先 git status 并汇报"写进 AGENTS.md,让代理每场会话都从已知状态出发。

12.2.4 git worktree:并行任务隔离

多个代理(或你与代理)同时在一个仓库里工作时,同一个工作目录就是冲突源。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 实战时,这是并行任务的标准起手式。

12.3 文件系统与运行时隔离

12.3.1 限定代理的工作目录

代理的一切文件操作都应限定在项目目录内。配置上有两手:对项目内路径给 allow;对仓库之外的路径(~/.ssh、系统目录等)不写任何 allow,靠 defaultMode 询问兜底,已知敏感区再显式 deny:

{
  "allow": ["Edit(server/**)", "Edit(web/**)"],
  "deny": ["Read(.env*)", "Edit(.zcode/**)"]
}

../ 相对路径逃逸的规范化校验,交给第 13 章的 PreToolUse hook 做内容级检查——规则管声明,hook 管变体。

12.3.2 容器化运行代理或其产物

容器(container)把文件系统、进程、网络隔成一个可丢弃的小世界,有两层用法:

  • 容器里跑代理:整个代理进程运行在开发容器(dev 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 的措施在容器内同样要配。

12.3.3 不信任的生成代码,先在临时环境跑

来自代理(尤其是读过网页、issue 之后)的代码,执行前默认不可信。原则:生成代码的第一次运行,永远发生在一次性环境里——临时容器(docker run --rm)、一次性虚拟机,或至少是独立于工作区的临时目录;试跑确认行为符合预期,再进正常流程。这条对"让代理照网上方案跑个安装脚本"尤其重要:先 curl 下来人工读一遍(或让另一个代理审一遍),再执行。

12.3.4 数据库隔离:测试库与生产库

规则只有一条但必须绝对:代理永远不连生产库。 落地三件套:

  1. 独立测试库:SQLite 场景就是独立的库文件;连接信息一律来自环境变量,绝不硬编码:
// 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);
  1. 种子数据(seed data):测试库随时可删可重建,内容来自种子脚本;测试永远对着可重建的数据跑,而不是对着"碰巧还在"的数据跑。
  2. 生产凭证不进代理环境:指向生产库的配置只存在于生产机器;本地代理会话里根本不存在这个变量(12.4 展开)。

12.4 网络与密钥隔离

12.4.1 出网白名单与代理设置

第 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 把代理进程的出网统一导向一个带过滤策略的代理出口——应用层规则失效时还有环境层兜底(企业环境的常见做法)。

12.4.2 生产凭证环境禁跑代理

一条物理隔离规则:存有生产密钥的机器、终端与环境,不运行代理。 部署机上不开代理会话;带着生产密钥的 shell 不用于代理工作;CI 里给代理用的 runner 与部署 runner 分开。密钥跟着环境走,不跟着人走——需要"在能碰生产的地方做事",要么人来做,要么走审批过的流水线(第 19 章)。

12.4.3 密钥存放与代理可达范围分离

给密钥分层,让"代理够得着的"和"丢了会疼的"尽量不重叠:

密钥 存放位置 代理可达性
测试用假凭据(可重建) .env.test 可达也无妨(本来就是假的)
个人开发凭据(如 GitHub 令牌) 系统密钥管理器 / git 凭据管理器 不可达:deny 读配置文件,推送走本机凭据管理器
生产部署凭据 CI/CD Secret 本地代理永远不可达

.env 文件本身也要讲卫生:chmod 600(仅本人可读写)、写进 .gitignore、对代理 deny Read(.env*)(第 7 章)。

12.4.4 环境变量注入,而非文件复制

代理需要让服务用上密钥时,正确姿势是注入环境变量,让进程"可用而不可见"——服务代码通过 process.env.XXX 读取,密钥不出现在代理可读的文件里,也就不会被写进代码或文档:

# 由你(人)在自己的终端里执行;代理只负责写代码,不碰值
set -a; source .env.dev; set +a
npm run dev

注意边界:能跑任意命令的代理理论上可以 printenv 读到进程环境,所以严格场景还要配套 deny Bash(env)、Bash(printenv:*),加上第 13 章的 Stop 凭据扫描做内容级兜底——密钥防护从来是组合拳,不是单招。

12.5 审批门与渐进放权

12.5.1 审批门:四类动作永远人审

无论代理多可信、白名单多成熟,四类动作永远保留人工确认(第 11 章的 ask):

  1. 合并主干:merge 到 main / 发 PR;
  2. 部署:任何对真实用户可见的发布;
  3. 删除:数据、分支、资源、批量清理;
  4. 对外发布:发消息、提 issue、发评论——一切离开本机边界的影响动作。

共同点:不可逆,或影响他人。 审批门不是不信任代理,而是把"最后按按钮"的权力留给承担后果的人。

12.5.2 三阶段渐进放权

阶段 适用时机 权限形态 进入下一阶段的条件
观察期 新项目 / 新代理 / 陌生代码库的前 1–2 周 全程 default(几乎全 ask),计划模式开路 连续多场会话零越权、产出稳定
限权期 观察期通过后 放开只读与验证类 allow,写与执行仍确认 白名单动作连续两周无需人工修正
放权期 长期稳定合作后 成熟 allow 清单 + hooks + 沙箱兜底 持续审计;出事即回退上一阶段

第 11 章的"先紧后松"讲的是节奏,这里是它的制度化版本:信任按证据放,出事降级不商量。

12.5.3 云端与远程异步代理的额外隔离

把任务交给云端 / 远程异步代理(挂机跑长任务、定时任务、CI 里的代理)时,人在回路变弱,隔离要更硬:

  • 一次性环境:每次任务独立容器或虚拟机,跑完即销毁,不留持久污染;
  • 最小仓库副本:浅克隆(shallow clone)、不含 .env*、不含 data/——仓库里没有的东西泄露不了;
  • 产物走 PR:远程代理的产出以分支 + PR 回来,永不直推主干;
  • 密钥不随行:平台侧按任务注入所需密钥(最小权限子账号),任务结束即吊销;
  • 超时与预算:设时间与 token 上限,跑飞的任务自动熔断(呼应第 16 章长任务治理)。

贯穿项目实战:轻读 BookNest

目标:把本章的沙箱手段在 BookNest 上落成一份"配置实录"——分支与提交策略、双 worktree 并行、测试库与生产库分离、可选的容器化方案,并做一次回滚演练。

一、分支与提交策略(写进 AGENTS.md)

## 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 关口:人确认后推送

二、双 worktree 并行:server 与 web 各一

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                        # 确认回到全绿

四、容器化(可选):docker-compose 一例

当 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,明天出事全锁死 按三阶段模型攒证据放权,出事降级不商量
备份从未演练过 出事时才发现备份恢复不了 定期演练回滚与恢复,沙箱要真用过一次

最佳实践

  1. 开工先检查工作区干净:stash 或 commit,绝不让代理在脏工作区动手;
  2. 一切代理变更走 task/* 专用分支,主干只接受人审合并;
  3. 高频小提交:每个绿测时点一个检查点,上下文压缩前强制落盘;
  4. 并行任务用 git worktree 隔离工作区,而不是复制仓库或共享目录;
  5. 生成代码的第一次运行放进一次性环境,确认行为后再进正常流程;
  6. 测试永远打独立测试库 + 种子数据;生产库路径的变量不该存在于代理环境;
  7. 生产凭证与代理物理隔离:部署机不跑代理,代理环境没有生产密钥;
  8. 密钥用环境变量注入而非文件复制,零字面量;严格场景再 deny env / printenv;
  9. 合并、部署、删除、对外发布四类动作永远保留人工审批门;
  10. 按观察期 → 限权 → 放权三阶段放权,出事即降级;回滚与备份定期演练。

本章小结

沙箱与隔离回答的是边界设计之后的问题:规则失效时,损失如何有上限。沙箱思维的前提是假设代理一定会犯错(甚至被提示注入),因此目标不是零事故,而是可回滚与损失封顶——护栏管概率,沙箱管后果。四道墙纵深串联:权限规则拦已知危险,沙箱与隔离封波及范围,Git 检查点保代码可回退,备份保数据可恢复,每道墙都假设前一道会被突破。落地手段上:Git 是第一道沙箱(专用分支、高频小提交、干净工作区、worktree 并行隔离);文件系统与运行时靠限定目录、容器化与临时环境试跑;网络与密钥靠出网白名单(同时是 SSRF 防线)、生产凭证物理隔离与环境变量注入;最后用审批门守住四类不可逆动作,用三阶段模型让信任随证据增长。

  • 四道墙:权限规则 → 沙箱 → Git 检查点 → 备份,串联兜底;
  • Git 沙箱四件套:专用分支、小提交检查点、干净工作区、worktree;
  • 运行时隔离:限定目录、容器化、临时环境试跑、测试库分离;
  • 密钥隔离:白名单出网、生产凭证禁跑、环境变量注入;
  • 放权模型:观察期 → 限权 → 放权,四类审批门永不放开。

动手练习

  1. 检查点演练:建一个 task/* 分支,让代理做一次你明知会失败的重构;练习用 git log + git revert(或 git restore)在 3 分钟内回到全绿,记录你总共用了几条命令。
  2. worktree 实战:在你的仓库里开两个 worktree,让两个代理会话分别修改两个不同模块(如 server 与 web),观察两者提交互不干扰;结束后用 git worktree remove 清理。
  3. 密钥卫生审计:在项目里执行 grep -rnE "sk-[A-Za-z0-9]{16,}|ghp_[A-Za-z0-9]{20,}" src docs(或用 gitleaks),确认无明文凭据残留;再检查 .env 是否在 .gitignore 里、代理是否被 deny 读取。
  4. 综合实践(BookNest):落地本章沙箱配置——把 Git 约定写进 AGENTS.md;建双 worktree 并行完成 server 与 web 两个小任务;实现 seed:test 与测试库分离;(可选)补 docker-compose;最后做一次完整回滚演练,把步骤沉淀成 docs/sandbox-runbook.md 提交。

导航

📄 第 13 章 · Hooks 深度实践:可编程守门员 13-hooks-in-practice.md

第 7 章用三个"最小可运行"的入门 hook 完成了原理验证;本章把它们升级为一套生产级守门体系:危险命令拦截、凭据防泄露、出网防护、写后检查闭环,外加工程化治理——脚本放哪、怎么测试、怎么审计。先记住本章的结论:hooks 是把"人肉盯梢"换成"机器盯梢",它是护栏(guardrail),不是安全边界(security boundary)——最后一道防线永远是第 12 章的沙箱。

学习目标

读完本章,你应该能:

  • 说清 hooks 的触发时机与输入输出契约,并用"手动喂 JSON"的方法独立调试 hook 脚本;
  • 编写并注册生产级危险命令拦截器,把黑名单按"类别 + 原因 + 替代方案"结构化维护;
  • 实现写文件前的凭据扫描,理解"软约束(团队约定)+ 硬约束(机器拦截)"双层防线的设计动机;
  • 为出网请求建立域名白名单校验,说清 SSRF 的攻击面与白名单的防护逻辑;
  • 搭建"写后检查"闭环,让 lint 错误以可执行形式回喂代理形成"写错 → 被拦 → 自修"循环,并对 hooks 体系做工程化治理(样本回放、性能预算、审计日志),准确说出它作为护栏而非安全边界的局限性。

13.1 机制回顾与生命周期

13.1.1 时机:挂在工具调用的生命周期上

先复述第 7 章的框架:hook 是你注册的 shell 命令,在代理的工具调用走到特定事件点时自动执行——

用户输入 ─▶ 模型决策 ─▶ [PreToolUse] ─▶ 工具执行 ─▶ [PostToolUse] ─▶ 模型继续 …
              │            拦截 / 放行             反馈 / 格式化 / 检查
              └─▶ 会话级事件:SessionStart / UserPromptSubmit / Stop / SubagentStop …
  • PreToolUse:工具执行前,拿"打算做什么"做裁决。本章三个守门员(危险命令、凭据、出网)都在这里——它们必须抢在后果发生之前;
  • PostToolUse:工具执行后,拿"做成了什么样"做反馈。格式化与 lint 在这里:写入已发生、无法撤销,但可把问题回喂给模型;
  • Stop:一个回合结束时触发。第 7 章的暂存区凭据扫描留在这里做"最后一道关"(分层哲学见 13.3)。

13.1.2 契约:stdin 进 JSON,退出码出裁决

每次触发,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 是给模型看的纠错通道:写清"为什么拦 + 该怎么办",模型下一次就照做;只写一句"不允许",它会换着花样再试。

13.1.3 调试:先手动喂 JSON,再进真会话

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),最后才进真实会话端到端验证。

13.2 实例一:危险命令拦截器

13.2.1 从"正则堆"到"规则表"

第 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); // 放行
});

13.2.2 注册配置

{
  "hooks": {
    "PreToolUse": [
      { "matcher": "Bash", "hooks": [
          { "type": "command", "command": "node .zcode/hooks/guard-bash.mjs", "timeout": 10 } ] }
    ]
  }
}

两个工程细节:timeout(秒)是注册侧的超时保险;正则故意偏保守——黑名单是概率屏障不是全集,rm -r -f 这类散写变体拦不全,漏网的交给 deny 规则与沙箱(补齐变体留作练习)。

13.3 实例二:凭据防泄露

13.3.1 为什么"零字面量约定"还需要机器兜底

AGENTS.md 与 NFR-2 早就写了"密钥零字面量"——但它是软约束,靠人和模型自觉,而软约束管概率、不管底线:人会忘(赶进度时顺手贴一段含 key 的代码);模型会错——长会话经过上下文压缩后早期约定被挤出窗口,训练数据里"示例代码带密钥"的惯性就会冒出来。而且第 7 章的 Stop 扫描发生在回合结束:文件已落盘、甚至已被引用。把扫描前移到 PreToolUse(Write/Edit 落盘之前),让凭据根本进不了文件系统,这才是硬约束。两层都保留:PreToolUse 拦落盘,Stop 扫暂存区——纵深防御。

13.3.2 脚本与注册

// .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 章密钥分离(代理环境里根本没有生产密钥)构成三层。

13.4 实例三:出网防护

13.4.1 白名单:把"能去哪"变成显式清单

一句话说清 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"]
}

13.4.2 脚本

// .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 章的容器隔离此时才是真边界。

13.5 实例四:写后自动检查闭环

第 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。

13.6 工程化与边界

13.6.1 脚本放哪、怎么测

位置:项目 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

13.6.2 性能预算与审计日志

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 行,误报多了收规则,漏报了补样本。日志只记摘要与路径,不抄录完整文件内容,避免日志本身变成泄密点。

13.6.3 失败策略:fail-open 与它的代价

本章所有脚本对自身异常统一 fail-open(解析失败、内部报错一律退出 0 放行并留痕)。反直觉但必要:一个有 bug 的守门脚本若把会话锁死,用户第一反应是删掉它——护栏被拆掉,比护栏偶尔失灵更糟。真正的红线由三层兜住:deny 权限规则(静态、声明式、不依赖脚本存活)、沙箱(第 12 章)、人审;fail-open 的代价(异常期间拦截失效)靠审计日志里的异常痕迹来暴露。

13.6.4 局限性:护栏不是安全边界

必须诚实地说清 hooks 的三条边界:

  1. 字符串匹配可绕过:变量拼接、base64、bash -c 间接执行、写脚本再跑——黑名单永远追不完变形,它是概率屏障;
  2. hooks 自身可被篡改:hook 以你的用户身份运行,能改 .zcode/ 的代理就能自我解锁——所以第 7 章把 Edit(.zcode/**) 设为 deny,克隆来的仓库必须先审 hooks(7.5.2);
  3. 它是护栏不是安全边界:对抗"决心作恶"的执行体,唯一可靠的是第 12 章的沙箱与最小权限——目录限定、容器隔离、密钥分离、网络隔离。

纵深防御的完整链条值得贴在脑子里:

软约定(AGENTS.md) → 权限规则 → hooks 拦截 → 沙箱隔离 → 人工审批 → 备份兜底
   降低发生概率       静态红线     动态内容审查   限制爆炸半径   最终决定权   最后的后悔药

hooks 在这条链上的位置是"高频、零疲劳、留痕"——它不会让坏事绝迹,但让坏事在发生前被喊停、发生后可追溯。

贯穿项目实战:轻读 BookNest

目标:把四个生产级 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> 被误拦 写清"类别 + 原因 + 替代方案";凭据扫描做占位符豁免防狼来了效应

最佳实践

  1. 能用一条权限规则表达的不写 hook;需要看内容判断或产生副作用的才写 hook(7.3.3 铁律);
  2. 每个守门 hook 三要素:快(毫秒级)、稳(自身异常 fail-open 并留痕)、说明白(类别 + 原因 + 替代方案);黑名单按结构化规则表维护,不做无注释的正则堆砌;
  3. 凭据防泄露双保险:PreToolUse 拦落盘(治本),Stop 扫暂存区(兜底);
  4. 出网一律走域名白名单,直接 IP 拒绝;白名单变更走 PR,与权限规则同权评审;
  5. PostToolUse 反馈必须可执行——贴出文件:行:列与规则名,模型才能一轮自修;
  6. 样本 JSON 回放当回归测试:改任何黑名单规则,先跑回放再提交;
  7. 审计日志 append-only、按月分文件、只记摘要,每月复盘调优规则;
  8. hooks 与配置进 Git、走 guard: 前缀评审;克隆陌生仓库先审 hooks 再放行;
  9. 永远记得:hooks 抬高犯错成本、留下审计痕迹,真正的防线是第 12 章的沙箱。

本章小结

第 7 章回答了"hook 是什么、怎么注册",本章回答"生产级的 hook 体系长什么样"。机制上,hooks 挂在工具调用生命周期的 PreToolUse / PostToolUse / Stop 等事件点上,stdin 收 JSON、退出码出裁决、stderr 回喂模型;调试先手动喂 JSON,再进真会话。实战上,四个守门员各司其职:危险命令拦截器把黑名单结构化为"类别 + 原因 + 替代方案";凭据扫描把零字面量约定从软约束升级为硬约束;出网防护用域名白名单补上"绕过 WebFetch 工具直接 curl"的漏洞,构成 SSRF 防线;写后检查把 lint 错误以可执行形式回喂,形成自修闭环。治理上,脚本进 Git、样本回放防回归、审计日志留痕、fail-open 保可用。而最清醒的认知是最后一条:hooks 是护栏不是安全边界,纵深防御的最后一层永远是沙箱与人。

  • 时机与契约:PreToolUse 事前 / PostToolUse 事后 / Stop 收口;stdin JSON 进、退出码 0/2 出,stderr 是给模型的纠错通道;
  • 四实例:危险命令、凭据防泄露、出网白名单、写后检查闭环;
  • 治理:脚本进 Git、样本回放、性能预算、append-only 审计日志、fail-open 策略;
  • 边界:hooks 是概率屏障,沙箱才是安全边界。

动手练习

  1. 调试热身:为 guard-bash.mjs 准备 6 条样本 JSON(3 条 deny、3 条 allow,其中 1 条是 rm -r -f 这类散写变体),手动喂 JSON 观察退出码;为"散写变体未被拦截"补一条规则,或论证为什么可以不补。
  2. 扩展凭据规则:给 guard-secrets.mjs 增加一种密钥模式(如 Google API Key 的 AIza 前缀),验证三类占位符(<your-token>、${VAR}、明显示例值)都不误报,并把新样本加进回放集。
  3. 闭环观察:故意让代理写一个含未使用变量与类型错误的 TS 文件,观察 check-on-save 的 stderr 与代理的后续动作;记录两轮交互,评估反馈是否"可执行",并改进措辞直到模型一轮改对。
  4. 综合实践(BookNest):完成贯穿项目的全部落盘与注册;然后制造三次真实拦截(危险命令、凭据、出网各一次),从审计日志导出三条 BLOCK 记录贴进提交说明,以 guard: 前缀提交;最后按 7.5.2 的克隆审查流程开新会话检查这套护栏,列出拦截清单并补齐漏掉的样本。

导航

📄 第 14 章 · 任务拆分方法论:把大象切成文件 14-task-decomposition.md

第 12、13 章给代理划定了边界——它不会闯祸了;但"不闯祸"不等于"能干成"。复杂需求交给它一次做完,依然会交付一堆积不起来的代码。本章解决 Part 5 的第一个问题:怎么把"做完 BookNest"这种不可能任务,切成一叠一次会话就能完成、验证、提交的任务卡。下一章再回答"拿到卡之后怎么动手"。

学习目标

读完本章,你应该能:

  • 复述"大任务必败"的四个机制,并据此判断一个需求是否必须拆分;
  • 用拆分四原则(单一职责、可独立验证、上下文自足、依赖显式化)审出一张任务卡的毛病;
  • 把一条功能需求切成垂直切片,画出任务依赖图(mermaid),识别关键路径与并行宽度;
  • 按七字段模板写出验收可执行的任务卡,并完成"坏卡改好卡"的改写;
  • 掌握粒度手感:判断一张卡过大或过小,并执行合并与再拆;
  • 把一个里程碑落成任务清单与任务卡、落盘到 docs/tasks/,交给代理逐卡完成。

14.1 为什么大任务必败:一个失败实验

14.1.1 让代理一次做完整个 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.1.2 背后的四个机制

崩坏不是代理"不够聪明",而是"大任务"这个单位本身错了:

  1. 质量随复杂度非线性衰减:每一步的输出都以"前一步正确"为条件,错误率是复利累积的;且模型注意力随任务步数摊薄——前 5 个文件精雕细琢,第 50 个文件已经是"糊完";
  2. 上下文物理装不下:需求细节 + 项目约定 + 已生成代码,轻松超出上下文窗口;一旦触发压缩,丢失的恰恰是"早期决策"这种最要命的信息;
  3. 中间产物无法验证:写到第 40 个文件时,你没有任何手段验证前 39 个对不对——没有可执行验收的大任务等于蒙眼飞行,攒到最后集中爆雷;
  4. 错误会复利:一个 schema 命名漂移被后续 30 个文件引用,返工不是改一行,而是改一片;发现越晚,修复越贵。

结论:拆,不是优化项,是必要条件。 14.2 给出拆分必须遵守的四个原则。

14.2 拆分四原则

14.2.1 单一职责(Single Responsibility)

一张卡只改变一件事——一个用户可见的能力,或一个模块的契约。检验句式:"这张卡完成后,系统多了什么能力?"答案里出现"以及""顺便",就拆开。"实现书籍 CRUD API"是合格的答案;"实现 CRUD API 并顺便升级 Vite"不是——前者出问题回滚一张卡,后者回滚时把无关变更一起拖下水。

14.2.2 可独立验证(Independently Verifiable)

验收标准必须是"可执行命令 + 期望结果",做完当场判对错。"搜索体验良好"不是验收标准;npm test -- search 全绿、curl "/api/v1/search?q=sf" 返回 200 且含《三体》才是。这与第 7 章"权限清单要能验证"、第 12 章"检查点可回滚"是同源思想:不能判对错的东西,无法管理。

14.2.3 上下文自足(Self-Contained)

动手所需的全部信息——相关约定、接口契约、文件路径、明确不做的事——都要写在卡里,执行会话不允许依赖"记得上一个会话聊过什么"。这正是第 18 章"自包含提示词"的提前演练:给 subagent 的任务提示必须自包含,而任务卡就是提前写好的自包含提示词。注意"链接而非复制":契约写"接口以 docs/spec/tags.md 为准",别把 spec 抄进卡里——两处事实源必然漂移。

14.2.4 依赖显式化(Explicit Dependencies)

每张卡声明前置卡编号;没声明的依赖等于不存在——执行代理不会"凭常识"先帮你打地基。依赖图即执行计划:入度为零的卡可以立即开工(可并行的宽度),最长的依赖链决定里程碑的工期下限(关键路径)。第 17 章的多代理扇出,吃的正是这张图里的并行宽度。

14.3 从需求到任务图的操作流程

14.3.1 第一步:垂直切片,而不是水平分层

拿到功能需求(FR),先切成垂直切片(vertical slice):每片是一条用户可演示的端到端路径("录入一本书并能在列表看到"),而不是水平分层(horizontal layering)——"先把所有 model 写完,再写所有 service、所有 route,最后写所有页面"。

维度 水平分层 垂直切片
首个可验证产物 全部层完成之后 第一个切片结束(几小时内)
验证对象 "代码存在" 用户可见行为
错误暴露时机 集成期集中爆发 每片结束即暴露
单任务上下文 吞下全项目 只装本片相关文件
适配 AI 协作 差——会话之间靠猜 好——每片自包含、可验收

垂直切片天然契合 14.2 的四原则:每片可演示(可验证)、片与片职责清晰(单一职责)、片内信息完整(自足)、切片顺序即依赖(显式化)。AI 协作尤其吃这套:每个切片结束,你都能打开浏览器看到真实进展,并立刻把验证结论喂给下一张卡。

14.3.2 第二步:切片内拆卡、画依赖图

切片往往还太大("书籍管理" = 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 这类"汇点"卡往往是集成风险点,验收要最严。

14.3.3 第三步:估算每张卡的上下文规模

卡级上下文 ≈ 任务卡本身 + 链接的约定(AGENTS.md / spec 的相关节)+ 涉及文件的现有内容 + 新增代码。粗估公式:涉及文件数 × 平均 150 行 + 新增 100–400 行,即千行级、一到三万 token——远小于窗口,而且全部与"这一件事"相关。拆分的物理意义就在这里:把与当前任务无关的上下文挡在门外。 估算超限(标准见 14.5)就再拆。

14.4 任务卡模板与好坏对比

14.4.1 七字段模板

## [编号] [卡名]

- **目标**:一句话说清"完成后系统多了什么能力",以及为什么值得做。
- **涉及文件**:新增 / 修改哪些文件(相对路径,标注 新/改)。
- **实现要点**:技术决策、必须遵守的约定(链接 AGENTS.md / spec)、明确不做的事。
- **验收标准**:编号列出"可执行命令 + 期望结果",全部通过才算完成。
- **依赖**:前置卡编号;无依赖时写"无(前置:M0 已就绪)"。
- **预计规模**:文件数 / 新增行数 / 预计会话时长。

(空白模板收录在附录 B;编号建议用 里程碑-序号,如 M1-02。)

14.4.2 坏卡:一句执行会话没法开工的"需求"

### 任务
给 BookNest 加上评分和标签功能,前后端都要做,体验做好一点,
顺便把搜索框也加上吧。

对照四原则逐条体检:三件事混在一张卡(违背单一职责);"体验做好一点"不可判定(违背可独立验证);前端不知道标签接口长什么样,只能猜(违背上下文自足);不知道必须等标签 API 先行,代理会先造一个假接口再用(违背依赖显式化)。这张卡的宿命:一次会话吞不下,做到一半上下文压缩,验收各执一词。

14.4.3 好卡:同一需求的正确打开方式

## 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 改了卡不用改;验收可执行,"完成"不再有解释权之争。

14.5 粒度的手感

14.5.1 基准线与两种病

基准:一张卡 = 一次会话内完成并验证,工作量几十分钟(30–90 分钟);涉及文件 3–8 个;新增 100–400 行。

过大的信号:

  • 涉及文件 > 10 个,或需要同时理解前后端加数据库才能动手;
  • 验收标准写不成可执行命令,只剩"整体效果正确"这类形容词;
  • 实现要点超过一屏,或预计会话超过 2 小时。

过小的信号:

  • 验收退化成"文件存在""能编译";
  • 一张卡只改一个文件,却和相邻卡强耦合——拆了白拆;
  • 卡数超过 15 张的里程碑开始出现"管理税":喂卡、审卡、核销的开销超过收益。

14.5.2 合并与再拆的操作

  • 合并:同一切片内、无相互依赖或共享同一批文件、验收可一次性执行 → 合成一卡(典型:schema 与迁移器天然是一家);
  • 再拆:过大时按三个刀口下刀——"先地基后实现"(schema/类型/骨架先行,单独成卡)、"接口与实现"(先定契约再填逻辑)、"数据与展示"(后端接口与前端页面分开,各自可测)。每切一刀,确认下半张卡仍能独立验收——拆完不能验收的卡是伪拆分。

最后一条手感:拆分本身可以让代理起草("把 FR-5 切成垂直切片并列出任务卡草案"),但依赖与验收必须人审——拆分质量是后续所有会话的地基,在这里省下的十分钟,会在每个执行会话里加倍还回来。

贯穿项目实战:轻读 BookNest

目标:把 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 修订;执行中发现问题先补卡再继续

最佳实践

  1. 先垂直切片后拆卡:每片是用户可演示的端到端路径,每片结束必有可见进展;
  2. 地基卡(schema、骨架、约定)先行且保持小——它们被所有后续卡依赖;
  3. 四原则当审卡清单用:单一职责、可独立验证、上下文自足、依赖显式化,缺一不合入;
  4. 验收标准必须可执行:命令 + 期望结果,禁止形容词;
  5. 卡内契约用链接指向唯一事实源(AGENTS.md / spec / schema),不复制;
  6. 每张卡写明"不做的事"——防蔓延的第一道闸;
  7. 规模超限(文件 > 10、会话 > 2 小时、验收写不清)就再拆;过小就合并,管理税也是税;
  8. 任务图进 Git 随 PR 修订;并行宽度是为第 17 章多代理扇出预留的产能;
  9. 拆分可让代理起草,依赖与验收必须人审;执行中发现问题先改卡再继续。

本章小结

大任务必败有四个机制:质量随复杂度非线性衰减、上下文装不下全部细节、中间产物无法验证、错误复利放大——所以"把大象切成文件"不是效率技巧,而是让 AI 协作成立的前提。方法论三步走:用垂直切片保证每片可演示、可验证;用四原则(单一职责、可独立验证、上下文自足、依赖显式化)保证每张卡的执行会话能独立完成;用七字段任务卡把这一切固化成可落盘、可评审、可修订的资产。粒度手感以"一次会话几十分钟、3–8 个文件"为基准,过大再拆、过小合并。BookNest 的 M1 已被切成 9 张卡躺在 docs/tasks/ 里——下一章解决拿到卡之后的问题:先计划、对齐、写 spec,再动手。

  • 必败四机制:非线性衰减 / 上下文溢出 / 无法验证 / 错误复利;
  • 四原则:单一职责、可独立验证、上下文自足、依赖显式化;
  • 三步流程:垂直切片 → 依赖图(关键路径与并行宽度)→ 规模估算;
  • 资产:任务卡七字段模板落盘 docs/tasks/,卡随 PR 演进。

动手练习

  1. 诊断:下面三张卡各犯了什么病?用四原则命名病灶并各改写一行:(a) "优化 BookNest 性能";(b) "新建 server/src/utils/date.ts,导出 formatDate";(c) "完成 M2 的笔记功能,前后端和测试都做掉"。
  2. 切片练习:把 FR-5(数据导入导出)切成 2–3 个垂直切片,画出依赖图(mermaid),指出关键路径与并行宽度。
  3. 补卡:按 14.4.1 模板把 M1-04「前端骨架与路由」写成完整任务卡(对照附录 D 的仓库结构与技术栈,验收标准至少 3 条可执行命令)。
  4. 综合实践(BookNest):补全 M1 剩余 6 张卡(M1-03/04/05/06/08/09)落盘 docs/tasks/M1-tasks.md 并提交;然后挑一张依赖已就绪的卡真实执行一次:若代理中途问了卡里没写清的问题,把答案补回卡里再继续——用一次真实执行校准你的拆分手感。

导航

📄 第 15 章 · 计划先行:Spec 驱动与计划模式 15-plan-first.md

第 14 章把大需求拆成了任务图与任务卡;本章解决拆分后的下一个问题:拿到一张任务卡,代理应该"上来就写",还是"先对齐再动手"?答案是后者。本章介绍两层计划工具——轻量的计划模式(plan mode)与更上位的规格文档(specification,简称 spec),以及让计划真正管用的审读与维护纪律。

学习目标

读完本章,你应该能:

  • 解释"先计划后动手"的成本逻辑:方向性错误的代价为什么远高于对齐成本;
  • 在 ZCode 中进入与退出计划模式(plan mode),并判断哪些任务值得进、哪些不值得;
  • 按"背景与目标 / 用户故事 / 接口约定 / 数据变更 / 边界与错误处理 / 明确不做"六段结构写出一页 spec;
  • 用"查假设、查验收、查遗漏、自我批判"四步审读代理产出的计划;
  • 建立计划维护机制:出现偏差时先改计划再改代码,计划文件随 PR 落盘留档;
  • 说清 spec、计划、todo 清单三层的分工(战略与战术的分界)。

15.1 为什么先计划后动手

AI 编码代理的默认性格是"急于动手":你话音刚落,它已经开始生成代码。这对 demo 无伤大雅,对真实项目却是最大的浪费来源。

先看一次典型的翻车现场:

你:给 BookNest 加阅读统计功能,做个页面展示数据。

ZCode:好的!(30 秒后开始写代码)
       40 分钟后:新增 8 个文件、约 500 行,还引入了 ECharts 图表库。
       你打开一看:月度统计用的是 created_at(录入时间),
       而你想统计的是"什么时候读完的"——统计口径全错。

这 500 行代码技术上没什么毛病,但方向错了:改口径意味着数据模型、聚合查询、前端渲染全部重写。算一笔账:

  • 对齐成本:让代理先读代码、给出方案,通常 3–5 分钟;
  • 方向错误的成本:半天返工、被污染的 git 历史、烧掉的上下文窗口,以及"这东西怎么又得重来"的挫败感。

方向性错误的代价不是"几行代码",而是全部已写代码。计划先行的本质,就是把发现方向错误的时点从"写完之后"提前到"写一行代码之前"。

正确的顺序是四拍:读代码 → 提方案 → 人审方案 → 执行。前三拍不落一行代码。同样的需求,换成计划先行:

你:给 BookNest 加阅读统计。先别写代码——读一遍数据模型和现有路由,
    给我一份实现计划:统计口径、接口形状、文件改动清单。

ZCode:(只读探索 3 分钟)books 表没有"读完时间"字段,我暂用 updated_at
       近似,可以吗?另外项目约定轻量 CSS,我建议不引图表库,用 CSS Grid 自绘。

你:口径必须精确,加一个 finished_at 字段;不引图表库同意。

ZCode:(产出 40 行计划,你花 2 分钟批准后开工)

注意这段对话里发生了什么:代理的两个关键决定(口径、图表方案)在写代码之前就被你看见了。这就是计划的第二个价值——计划即沟通契约(communication contract):你评审的对象从"几百行代码"变成"一页意图",看意图比看代码快一个数量级。

第三个价值:计划是可评审产物(reviewable artifact)。它可以被转发、被修改、被留档。下一节你会看到,这份产物还能进 git、随 PR 走、被未来会话考古——它是有寿命的资产,不是聊天记录里的一次性输出。

15.2 计划模式(plan mode)的使用

计划模式是 ZCode 内置的一种权限模式:进入后,代理只能只读探索(读文件、全局搜索、列目录),不能写任何文件或执行有副作用的命令;它必须把要做的事整理成一份计划提交给你,批准后才切回执行模式动手。

以 ZCode 为例,在交互界面按 Shift+Tab 可循环切换权限模式(默认 → 自动接受 → 计划模式),也可以用 /plan 指令或启动参数进入;不同版本入口略有差异,以 zcode --help 与官方文档为准。

两种模式的差异一目了然:

能力 默认模式 计划模式
读文件 / 全局搜索 / 列目录 允许 允许(只读探索)
编辑、新建文件 允许 禁止
执行命令(Bash 等) 按权限配置 原则上禁止,只读命令需在计划中声明
产出物 直接修改代码 一份计划,等待人工批准
人的角色 事后审查 事前把关

计划模式的产物通常长这样(骨架,完整示例见本章贯穿项目实战):

【实现计划 · <功能名>】
一、现状勘察    —— 我读了哪些文件,确认了哪些既有事实
二、假设        —— 我没有问你就自己定了的事(重点看这里)
三、改动清单    —— 按文件列出改什么、为什么
四、测试与验收 —— 怎么证明做对了
五、执行顺序    —— 先做哪步后做哪步,每步怎么提交
六、风险        —— 哪里可能出问题

何时该进计划模式

四类场景,命中任何一条就值得进:

  1. 跨文件改动:动 3 个以上文件,或前后端联动——改动面越大,方向错误的返工越贵;
  2. 架构或方案选择:存在多个可行做法(用不用图表库?聚合放前端还是后端?)——这决定应该由人拍板,不该由代理猜;
  3. 陌生代码区:你或代理都没碰过的模块——先探索再动手,避免基于想象写代码;
  4. 需求有歧义:连你自己都说不清"读完时间"到底指什么——写计划的过程会逼出这些问题。

何时别进

计划模式是工具不是仪式,三种情况直接做:

  1. 机械小改动:改错别字、调样式、加一行日志——做法唯一,没有方案可审;
  2. 单文件局部修改:任务卡已经足够原子(第 14 章),范围与做法都清晰;
  3. 已有 spec 的常规实现:需求与接口都在 spec 里定死了,代理照方抓药即可。

判断口诀:有"选择"要拍板才进计划模式,没有选择就是执行。每个三行改动都走一轮"计划—批准",是在给自己交流程税。

审批计划时该看什么

批准不等于扫一眼格式。你只需要盯三件事:代理默默做了哪些假设、验收标准是否可执行、方案有没有明显遗漏。这三件事怎么查,见 15.4。

15.3 Spec(功能规格)文档

计划模式解决"这一刀怎么切",spec 解决更上游的问题:"这个功能到底做什么、为什么这么做"。三者的层级关系:

需求书(附录 D:做什么功能)      —— 产品层
  └─ spec(做什么与为什么)       —— 规格层:本章
       └─ 任务卡(这一刀切哪)    —— 拆分层:第 14 章
            └─ todo 清单(当前一步)—— 执行层:第 16 章

什么时候需要写 spec?三个判据命中其一就该写:

  • 功能会跨多个会话或多个代理完成(对方没有你的聊天记录);
  • 涉及对外约定:API 形状、数据表结构、文件格式;
  • 将来大概率会回头问"当时为什么这么定"。

反过来说,一次纯 UI 微调、一个内部工具函数,不需要 spec。spec 是给"有约定价值"的东西准备的。

六段结构

一页够用的 spec 只需要六段,每段回答一个问题:

  1. 背景与目标:为什么要做这个功能?成功标准是什么?——给"为什么"留档;
  2. 用户故事(user story):以"作为……我想……以便……"句式列出 2–4 条,锚定真实使用场景,防止功能膨胀;
  3. 接口约定(API contract):对外暴露的每个接口,写清方法、路径、参数、返回形状。用 JSON 示例而不是文字描述——形状比形容词精确:
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]
}
  1. 数据变更(schema diff):对数据结构的改动,用迁移脚本语言写,不用自然语言:
-- 0007_add_finished_at.sql
ALTER TABLE books ADD COLUMN finished_at TEXT;  -- ISO 8601 日期,可空
  1. 边界与错误处理:非法输入怎么办?空数据怎么办?哪些值不计入统计?——这是代理最容易漏想、评审最容易漏看的部分;
  2. 明确不做的事(non-goals):最容易被省略、也最能省 token 的一段。"不引入图表库""不回填历史数据",白纸黑字挡住代理的热情式镀金(gold-plating)。

spec 是多会话与多代理的"共同记忆"

spec 写进文件(BookNest 约定放 docs/spec/)后,它的价值才真正开始:文件比上下文窗口活得久。下个会话冷启动时,代理读 spec 与读你的聊天记录,得到的是同一份理解;第 17 章你会看到,当多个代理并行时,它们之间没有共享的记忆,共享的只有仓库里的文件——spec 就是它们对齐口径的锚点。所以 spec 的变更要走 git:改 spec 就是改需求,应当和改代码一样可评审、可追溯。

一个实用约定:把 docs/spec/ 的存在写进 AGENTS.md(第 4 章),让代理"开机即懂":接手一个功能,先找 spec;没有 spec 且任务跨文件,先提议写一份。

体量控制:一个功能一个文件,100–200 行以内。如果你发现 spec 越写越长、开始出现"先查询再分组再缓存"这类内容,说明你在写实现方案——那是计划的事,挪出去。

15.4 计划的审读技巧

批准一份计划平均只需 3 分钟,前提是你知道往哪看。四步法:

第一步:查假设

代理最大的风险不是写错代码,而是默默替你做了决定:字段语义(updated_at 是编辑时间还是完成时间?)、依赖是否已安装(express-validator 在不在 package.json?)、现有代码是否可复用(项目已有校验中间件吗?)。

防御动作:要求代理在计划里专列"假设"一节。如果它列得太薄,追问一句:

你:计划里的"假设"一节太薄了。列出所有"你没有问我就自己定了"的事情,
    包括字段类型、库名、现有代码的用法,一条都不许漏。

然后把每条假设按"对 / 错 / 无所谓"过一遍。贯穿项目实战里你会看到,两处最贵的返工都是被这一步拦下的。

第二步:查验收标准是否可执行

每条验收标准都应该能翻译成"一条命令或一次观察"。对比:

  • 不可执行:"统计页面要美观,性能要好。"
  • 可执行:"空库调用 GET /api/v1/stats/overview 返回 200 且各字段为零值(Vitest 断言);1 万条记录内响应 < 300ms。"

看到形容词(美观、健壮、优雅)就停下来,让代理把它翻译成可测的陈述。

第三步:查遗漏

拿这张清单扫一遍计划:错误处理、迁移失败怎么回滚、并发与重复提交、空数据状态、权限边界、性能口径。比如统计接口虽是只读,但用户连点两次刷新按钮会发出重复请求——这类"不太可能但一定会发生"的事,代理经常想,也经常不说;你问了它才答。

第四步:让代理自我批判

两个屡试不爽的提示词:

你:列出这个计划的三个最大风险,以及每个风险的缓解措施。
你:假设你是一个严苛的代码评审者,这个计划里哪一条会让你拒绝批准?

批判之后人复核:代理的自批有时流于形式(列出的风险恰是它最有把握的部分),你要检查它列的风险有没有对应的缓解动作,没有就继续追问。

最后提醒:批准的是方向,不是免除检查。执行阶段的每一步仍需要检查点——那是第 16 章 todo 清单的职责。

15.5 计划的维护

最常见的计划失败模式不是"没写计划",而是"计划只在开工前看了一眼"。计划是一次性的,很快就会与现实脱节,从资产变成误导源。

偏差处理:先改计划,再改代码

执行中出现偏差只有两类:一是勘察与现实不符(以为要新写校验,结果发现项目已有中间件);二是需求中途变化(统计口径从"录入时间"改成"读完时间")。两类偏差的处理原则相同:停下来,先改计划文件并写明为什么改,再动代码。顺序反过来,计划就死了。

落盘:计划随 PR 走

计划文件放进仓库(BookNest 约定:spec 在 docs/spec/,任务与计划材料在 docs/tasks/),和代码一起提交。三个好处:

  1. 代码评审者能看到"为什么这么改",评审质量更高;
  2. 出问题时可以考古:当时的判断依据是什么、哪一步开始偏的;
  3. 未来会话冷启动有据可依——这是第 16 章状态管理的地基。

在计划文件末尾维护一个"修订记录"小节,成本极低:

## 修订记录
- 2026-10-06 批准前批注 4 条:口径改用 finished_at;不新增校验依赖;
  yearly 聚合移到 SQL;补迁移回滚说明。
- 2026-10-07 执行中偏差:heatmap 组件改用 CSS Grid 而非 table,
  原因:每月格数固定 12 列,Grid 语义更贴合。

分层:计划是战略,todo 是战术

把三样东西各归各位,别混在一个文件里:

spec   —— 需求真相:做什么、为什么(变化最慢)
计划   —— 战略:分几步、什么顺序、每步怎么验收(按 PR 变化)
todo   —— 战术:此时此刻这一步做没做完(分钟级变化,见第 16 章)

计划批准后,第一件事就是把它拆成 todo 清单交给执行会话;计划负责"路线不偏",todo 负责"脚下不乱"。

贯穿项目实战:轻读 BookNest

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 修改再改代码

最佳实践

  1. 跨文件改动一律先说"先别写代码,给我一份计划",把发现方向错误的时点提前到零成本阶段;
  2. 用计划模式的"只读探索"当免费的代码导览:接手陌生模块,先让它出一份现状勘察;
  3. 要求计划必含"假设"小节;假设薄就追问,逐条按"对 / 错 / 无所谓"过一遍;
  4. 验收标准全部翻译成可执行陈述:一条命令、一个断言、一次可观察的行为;
  5. spec 控制在一至两屏(100–200 行),六段结构写全,"明确不做的事"不许空着;
  6. spec 与计划落盘到 docs/spec/、docs/tasks/,随 PR 提交,让评审者看得见动机;
  7. 执行偏差时先改计划再改代码,并在计划末尾追加"修订记录";
  8. 接功能先找 spec,找不到且任务跨文件,先提议写 spec 再动手——把这条写进 AGENTS.md;
  9. 对复杂计划追加一句"列出这个计划的三个最大风险",用代理批判代理,人做终审。

本章小结

计划先行不是流程洁癖,而是把"发现走错路"的时点从写完代码之后提前到写代码之前:计划模式用只读探索加人工批准,拦住中大型改动的方向性风险;spec 用六段结构把"做什么与为什么"固化成可评审、可追溯、跨会话共享的文件;四步审读法(查假设、查验收、查遗漏、自我批判)让 3 分钟的批准真的值 3 分钟;而"偏差先改计划再改代码、计划随 PR 落盘"的维护纪律,决定这份计划是资产还是误导源。

要点回顾:

  • 方向性错误的代价是全部已写代码,对齐成本只有几分钟;
  • 计划模式 = 只读探索 + 产出计划 + 人工批准;有"选择"要拍板才进;
  • spec 回答"做什么与为什么",任务卡回答"这一刀切哪",todo 回答"当前一步";
  • 审读四步:查假设、查验收、查遗漏、自我批判(人终审);
  • 计划是活文档:偏差先改计划,落盘随 PR 走,修订留痕。

动手练习

  1. 模式判断:给下面三个任务分类——进计划模式还是直接做,并说明理由:(a) 把 BookNest 的分页参数从 0 起改为 1 起;(b) 为 FR-6 全文搜索选型 LIKE 查询还是 SQLite FTS5;(c) 修复按钮悬停色与主题不符。
  2. 假设猎手:找一个你最近让代理完成的功能,要求它"列出当时你替我做的全部假设",逐条核对:哪条侥幸对了?哪条已经埋进代码里了?
  3. 写 spec:按本章六段结构,为附录 D 的 FR-7(快速记录:首页灵感速记、稍后归入某本书)写一份 spec,重点打磨"接口约定"与"明确不做的事"两段。
  4. 综合实践:为 FR-3 的"按标签筛选书籍"完整走一遍本章流程:写 spec(30 行以内)→ 计划模式下获取实现计划 → 按四步法批注至少 3 条 → 批准执行 → 完成后在计划文件补"修订记录"。把批注前的计划与批注存档对比,写下你的发现。

导航

📄 第 16 章 · 长任务与状态管理:会话会死,状态要活 16-long-running-tasks.md

第 15 章解决了"动手前先对齐";本章解决对齐之后的下一个问题:任务大到一次会话装不下怎么办?真实项目里,FR-2 这样的功能要动十几个文件、跑几十轮测试,而会话随时可能因为上下文耗尽(context exhaustion)、崩溃或你主动关闭而结束。本章的核心命题只有一句话:会话是易失的(volatile),状态必须外置到文件——会话会死,状态要活。

学习目标

读完本章,你应该能:

  • 识别长任务的三个敌人——上下文耗尽、中途跑偏、状态丢失,并说出对应的防线;
  • 用待办清单(todo)驱动执行:同一时间只保一件进行中,完成即勾选并提交;
  • 编写状态外置三件套:进度文档、handoff 交接文档与决策日志;
  • 执行新会话冷启动标准动作:"先读 handoff 与任务卡,再复述你的理解";
  • 识别会话性能衰退信号并主动收尾换新会话,而不是等它烂掉;
  • 用看板(kanban)方式管理多个并行任务,并为大重构设计分段推进方案。

16.1 长任务的三个敌人

什么叫长任务?不是"写得久的任务",而是装不进一次会话的任务:改动超过十来个文件、要跨几个小时甚至几天、中途必然需要人拍几次板。BookNest 的 FR-2(读书笔记)就是典型——迁移、service、路由、两个前端页面、端到端测试(e2e),一口气做不完。

长任务有三个敌人,每一个都足以让几小时的成果蒸发:

敌人一:上下文耗尽。 上下文窗口(context window)有限:对话越长,最早的约定与中途的纠正越可能被压缩(compaction)或挤出窗口。表现是代理开始"失忆"——你一小时前说过"不引图表库",它现在给你装了 ECharts。上下文不是记忆,是工作台:台面就那么大,堆得越满,碰掉东西越容易。

敌人二:中途跑偏。 任务越长,需求被误解的机会越多。没有检查点的长执行像没有路标的马拉松——跑偏十公里才被发现,返工的不是一段代码,而是整条路线。

敌人三:状态丢失。 会话结束的原因很多:窗口满了、进程崩了、你合上笔记本、或者只是今天想换个思路重开。"我们刚聊到哪了?"——聊天记录里也许还能翻到,但新会话对你们昨天的共识一无所知。最贵的丢失不是代码(git 管着),而是决策与进度:为什么选了方案 B?做到第几步?下一个会话两眼一抹黑。

三类状态、三种管法,先记这张总账:

状态类型 例子 谁管 怎么管
代码状态 改了哪些文件 Git 小步提交,信息可追溯
任务状态 做到第几步、还剩什么 本章:清单 + 进度文档 外置到 docs/tasks/
决策状态 为什么选方案 B 本章:决策日志 + handoff 外置到 docs/

核心原则:把上下文窗口当内存(RAM),把仓库当硬盘。掉电(会话结束)时内存全空,硬盘上的都还在。代理脑内的记忆你控制不了,仓库里的文件才完全由你控制——所以一切重要状态都往硬盘上搬。

16.2 待办清单驱动

清单放哪:内置 todo 与文件清单

ZCode 这类代理内置 todo 工具(任务清单):代理把计划拆成一列条目,标记 pending / in_progress / completed,执行中随时增删。这是会话内的战术视图,好用,但有个致命属性——会话一死,它就没了。

所以长任务需要第二份:文件清单(如 docs/tasks/fr2-notes.md),跨会话持久。两者分工:

  • 内置 todo:会话内的执行节拍器,分钟级更新;
  • 文件清单:跨会话的进度真相源,每完成一项同步一次,会话收尾时两者必须对齐。

文件清单长这样(节选,全文见贯穿项目实战):

<!-- docs/tasks/fr2-notes.md -->
- [x] T1 迁移 0007_create_notes.sql
- [x] T2 note service + 单元测试(12 条)
- [~] T3 路由层 + 接口测试        ← 当前
- [ ] T4 Markdown 渲染与消毒
- [ ] T5 前端两个页面

三条铁律

  1. 同一时间只保一件进行中。 内置 todo 同一时刻只许一个 in_progress;文件清单用 [~] 标记当前项。出现多个"进行中",意味着多个半成品——等于没有进度。
  2. 完成即勾选并提交。 一项做完:勾选 → 跑相关测试 → git commit(提交信息带任务编号,如 feat(notes): T2 note service 与单元测试)。勾选与提交是同一个动作的两半:只勾不提交,清单就在撒谎;只提交不勾,下个会话就会重做。
  3. 清单是进度的唯一真相源(single source of truth)。 "做到哪了"只允许一个答案:看清单。不靠记忆、不靠翻聊天记录、不靠"我记得差不多做完了"。git log 是第二证据(提交即完成的物证),清单是第一入口。

让代理守清单,而不是你播报

把规则写进 AGENTS.md(第 4 章),并在派活时点名:"读 docs/tasks/fr2-notes.md,按清单顺序执行;每完成一项:更新清单、跑测试、提交;遇到清单外的发现,先报告再动手。"之后你只需在每项完成时扫一眼提交记录——清单就从"你喂给代理的备忘"变成"代理自己守的纪律"。

16.3 状态外置三件套

todo 清单回答"做到哪了";长任务还要回答"为什么这么做"与"下一个会话怎么接"。三件套各管一摊,全部落在 docs/tasks/(与第 14 章任务卡、第 15 章计划同住)。

一、进度文档:任务的日志

按任务一个文件(如 docs/tasks/fr2-notes-progress.md),执行中随时追加三类条目:

  • 决定:执行中拍板的小事("时间线含未归书笔记");
  • 偏差:与计划的出入及原因("分页从 offset 改成游标");
  • 剩余工作:值得解释的余额(细粒度交给清单,文档只记"为什么")。

它与 git log 的分工:git log 记"改了什么",进度文档记"为什么这么改、改的时候发现了什么"。前者是物证,后者是口供,缺一不可。

二、handoff 交接文档:会话之间的桥

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 条以内复述你对当前状态与下一步的理解,等我确认。

"复述对齐"是廉价保险:错读在零行代码时被纠正,成本是一轮对话;不纠,成本是半天返工。完整对话实录见本章贯穿项目实战。

16.4 断点续作与上下文压缩

性能衰退信号清单

长会话不是匀速变慢,而是断崖式变烂。四个信号,命中两条就该收尾:

  1. 重复犯错:刚纠正过的问题又犯(消毒忘了、INNER JOIN 又出现了);
  2. 忘记约定:偏离 AGENTS.md 的命名、目录、提交规范;
  3. 答非所问:回答开始泛泛、复读,或与问题无关;
  4. 产出/消耗比恶化:工具调用越来越密,有效改动越来越少,一个测试反复修不过。

衰退的机制很朴素:窗口里堆满过程细节(报错、试探、改了又改的半成品),真正重要的约定被挤到角落。这不是模型的"态度问题",是上下文的物理规律。

主动收尾:四步换血

发现信号不要硬撑,执行标准收尾,然后换新会话:

  1. 停在完整步:把手头这一小步做完或回滚,不留半成品;
  2. 验证:跑全量测试,确保绿灯;
  3. 落盘:更新清单勾选 → 提交 → 写 handoff;
  4. 换会话:新会话走 16.3 的冷启动标准动作。

宁可主动换会话,不要等会话烂掉——前者由你掌控交接质量,后者由崩溃时刻决定。

把关键记忆搬进文件

会话中你说过的每句重要的话,都面临同一个问题:它会蒸发。听到自己说出下面这些话,就顺手落盘:

  • "记得 rating 是 0 到 5" → 约定,进 AGENTS.md 或 spec;
  • "时间线要含未归书笔记" → 决定,进决策日志;
  • "better-sqlite3 的事务别在 route 里开" → 坑,进 handoff 已知坑。

一句话判断:这句话两周后的新会话还需要吗?需要,就写进文件。

上下文压缩:降落伞,不是背包

ZCode 在窗口将满时会自动压缩,也支持手动触发。要认清它的定位:压缩是有损的安全网,保住的是"最近与相关",丢掉的往往是"早期的约定与细节"——而约定恰恰是长任务里最贵的部分。所以:

  • 把压缩当降落伞:坠机时保命,值得庆幸;
  • 别把压缩当背包:指望"反正会压缩,我不用做笔记",等于把重要文件只存在内存里;
  • 确需压缩时,尽量选在干净边界(刚提交完、一个阶段收尾)手动触发,损失最小。

大重构的分段推进法

大重构(比如把 server 从 JavaScript 整体迁到 TypeScript)天然超会话。分段三原则:

  • 每段可验证:段 = 一个能独立跑测试的切片。按 route 逐个迁(每个迁完测试全绿),而不是"先把所有类型定义写完"——后者在完成前寸步不可验证;
  • 每段可提交:段末全量测试绿 → 提交。仓库永远处于可用状态,随时可以被任何人接手;
  • 段间 handoff:哪怕下个会话就在五分钟后,也写三行 handoff。成本极低,且强迫你确认状态真的干净。

段间最忌"顺手多迁一个文件":没验证的贪多,就是给下一段的地基埋雷。

16.5 多任务并行的看板式管理

单个长任务用清单管理;同时推进多个功能(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 限制

WIP(work in progress,在制品)限制是看板的灵魂纪律:doing 列同时最多 2 张卡(单人甚至 1 张),想开新卡,先关一张。理由有三:

  • 每个并行任务都占一个会话的上下文,开多了每个都在衰退;
  • 半成品堆积会让"还剩多少活"变得不可见;
  • 并行分支改动同一区域时,合并冲突随 WIP 上升而指数增长。

与多代理的衔接

本章的看板是"单人多会话":你开两个终端,各守一张卡。第 17 章往前一步——"一人多代理":主代理派生 subagent(子代理)并行干活。到那时你会发现:代理之间同样没有共享记忆,能对齐它们的也只有仓库里的文件。本章练的清单、handoff、决策日志,正是第 17 章多代理协同的底层设施——先把单人跨会话管明白,才轮得上代理编队。

贯穿项目实战:轻读 BookNest

本节实录里程碑 M2 的第一仗:跨三个会话完成 FR-2(读书笔记)。任务卡与 spec 来自第 14、15 章的方法,这里只展示一件事:状态如何跨会话活着。

会话 1:开工与主动收尾

会话 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;同时保留原文 | 存库前消毒:丢原文 |

会话 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:收口

会话 3 冷启动对齐后,完成时间线页(T5 余项)与 T6:Playwright 冒烟用例、lint 全绿、覆盖率 ≥ 80%,合并 feat/fr2-notes。收口后两个动作别省:把 done 卡片归档进看板(16.5);把 handoff 里长期有效的坑("事务包在 service 层")升级进 AGENTS.md——坑修一次,以后所有会话受益。

常见误区

误区 典型现象 纠正方法
靠记忆和聊天记录续任务 新会话问"我们做到哪了",每次答案不一样,还重做过已完成的事 清单是唯一真相源;冷启动先读清单与 handoff
多项同时标"进行中" 五个半成品,哪项都没法提交,测试长期红着 同一时刻只保一件 in_progress;完成即勾选并提交
会话收尾不写 handoff 下个会话只能靠翻 diff 猜进度,把已知的坑重新踩一遍 四步收尾固定化:停完整步 → 验证 → 落盘 → 换会话
把上下文压缩当记忆 压缩后代理忘了早期约定,装了不该装的依赖 压缩是有损安全网;重要约定写进文件,不指望窗口
决策日志只写"选了什么" 两周后没人记得为什么不选另一方案,同一场辩论再吵一遍 每条决策记四列:日期、决定、理由、备选与不选原因
WIP 不设限 四个分支并行,合并冲突铺满屏幕,没有一个能合进去 doing ≤ 2;想开新卡,先关一张

最佳实践

  1. 长任务开工第一件事:建清单文件与进度文档,路径约定写进 AGENTS.md;
  2. 同一时刻只保一件 in_progress;完成即"勾选 + 测试 + 提交"三连;
  3. 每次主动收尾必写 handoff,五段俱全,"已知坑"一栏不许空着;
  4. 决策日志只追加不改写,每条都带上"备选与不选原因";
  5. 新会话冷启动三步:读 handoff 与任务卡 → 复述理解 → 人确认后开工;
  6. 衰退信号(重复犯错、忘记约定、答非所问、产出恶化)命中两条就换会话;
  7. 会话里说出的重要约定,当场落盘到 AGENTS.md / 决策日志 / handoff;
  8. 大重构分段推进:每段可验证可提交,段间 handoff,不贪多;
  9. 多任务用看板管理,doing 列 WIP ≤ 2,想开新卡先关一张;
  10. 收口时把长期有效的"已知坑"升级进 AGENTS.md,让每个坑只踩一次。

本章小结

长任务的三个敌人——上下文耗尽、中途跑偏、状态丢失——共同指向一个原则:会话是易失的,状态必须外置到文件。对内,用 todo 清单驱动执行,同一时间只保一件进行中,让清单成为进度的唯一真相源;对外,用进度文档、handoff 交接文档与决策日志三件套,把决定、偏差与坑都搬进仓库;跨会话时,执行"读 handoff → 复述理解 → 人确认"的冷启动标准动作;会话衰退时,主动收尾换血而不是硬撑;多任务并行时,用看板加 WIP 限制守住专注。git 保住代码,文件保住意图——两者合起来,才构成跨会话、跨代理都读得懂的"项目记忆"。

要点回顾:

  • 三个敌人:上下文耗尽、中途跑偏、状态丢失;防线全部在文件里;
  • 清单三铁律:一件进行中、完成即勾选提交、清单是唯一真相源;
  • 三件套:进度文档(为什么)、handoff(怎么接)、决策日志(为什么这么选);
  • 冷启动三步:读 → 复述 → 确认;收尾四步:停完整步、验证、落盘、换会话;
  • 看板管并行:backlog / doing / done,WIP ≤ 2,先关一张再开一张;
  • 仓库是硬盘,会话是内存:一切重要状态落盘,才活得过会话的死亡。

动手练习

  1. 敌人诊断:回忆一次你"第二天接着做却对不上进度"的经历,对照 16.1 的三个敌人,写出当时分别是哪个(或哪几个)在作祟,缺的是三件套里的哪一件。
  2. 清单改造:把你手头一个正在进行的任务改造成本章格式的清单文件([x] / [~] / [ ] 三态),并给代理下发"守清单"指令,观察它每完成一项后是否会主动勾选并提交。
  3. 衰退信号演习:故意把一个中等任务塞进同一个会话连做两小时,记录四个衰退信号各自出现的时刻;然后立刻执行四步收尾换新会话,用冷启动三步续作,对比前后 30 分钟的产出质量。
  4. 综合实践:为附录 D 的 FR-7(灵感速记)做一次完整跨会话推进:开工建清单与决策日志 → 会话 1 做到一半主动收尾并写 handoff → 新会话走冷启动三步续作 → 收口后归档看板并把长期坑升级进 AGENTS.md。交付物:清单、handoff、决策日志各一份,外加 200 字复盘。

导航

📄 第 17 章 · 多 Agent 架构模式:从单兵到编队 17-multi-agent-patterns.md

第 16 章的看板停在"单人多会话";本章把拼图补全——"一人多代理"。当任务撞上单代理的天花板(窗口装不下、串行太慢、自己审自己失灵),就该把一个代理变成一支编队。本章给出五种可复用的多代理架构模式(architecture pattern):主从编排、流水线、评审者-实现者、扇出汇总、探索-实现分离,每种都配图、适用场景与"什么时候别用",最后用 BookNest M3 完整推演一次选型。

学习目标

读完本章,你应该能:

  • 说出单代理的三种瓶颈,并用判断表决定一个任务值不值得上多代理;
  • 用主从编排(orchestrator-worker)拆分、派发、整合、验收,写出自包含的 worker 提示词;
  • 用流水线(pipeline)把标准化流程交给角色接力,并让中间产物落盘传递;
  • 搭建评审者-实现者(maker-checker)组合与红队(red team)变体提升质量;
  • 用扇出汇总(fan-out / map-reduce)并行处理同构批量任务,并做好汇总去噪;
  • 用探索-实现分离给陌生代码库"先画地图、再施工"。

17.1 为什么要多代理

ZCode 等工具里的多代理,指的是主会话派生 subagent(子代理):每个子代理有独立的上下文窗口,干完活把结果交回主会话(第 18 章讲操作细节,本章讲架构)。听起来像招了几个实习生,但先想清楚:多代理到底解决什么问题?

单代理的三种瓶颈

  1. 上下文装不下。 全库安全审计要读两百个文件;读完前五十个,窗口已经塞满,后面读的把前面挤掉——审计质量随进度下降。这不是提示词技巧能救的,是物理限制。
  2. 串行耗时。 五个互不依赖的模块要从 JavaScript 迁到 TypeScript,单代理只能一个接一个;五个并行 worker 可能把两小时压成半小时。
  3. 角色混淆。 让写代码的同一个上下文顺便自审——既当运动员,又当裁判。作者对自己刚写的代码有天然的"沉没成本偏袒":它倾向证明自己写的是对的,评审自然流于形式。

本质 trade-off:不要为了多而多

多代理的本质是拿两样东西换两样东西(trade-off,取舍):

  • 得到:并行加速(互不依赖的活同时干);上下文隔离(每个 worker 的窗口干净且专用,读再多文件也不污染主会话);
  • 付出:沟通成本(worker 看不到主对话,提示词必须自包含);集成风险(多份产出要合并,可能冲突、可能口径不一);token 成本(每个 worker 都要从零读一遍背景)。

所以多代理是解决特定瓶颈的工具,不是排场。什么时候值得上多代理,对着这张表看信号:

信号(命中越多越值得) 对应瓶颈 适合的模式
可分解为 ≥3 个互不依赖的同构子任务 串行耗时 扇出汇总
子任务读得多、写得少(探索、审计) 上下文装不下 探索-实现分离 / 扇出汇总
写与审需要不同视角 角色混淆 评审者-实现者
流程标准化、角色分明 —— 流水线
总控复杂、细节可下放 上下文装不下 主从编排

反面判据(命中任一条,就留在单代理):任务原子,一个窗口装得下;串行几分钟能干完;拆分边界还没想清楚——边界不清就上多代理,等于把混乱并行化。

17.2 模式一:主从编排(orchestrator-worker)

主代理(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 提示词必须自包含(self-contained)。worker 看不到主对话!提示词要带全四要素:目标(做什么)、范围(允许动哪些文件、禁止碰哪些)、约定(AGENTS.md 之外的特殊约定)、完成定义(DoD,Definition of Done:怎么算干完了)。
  • 结果结构化回传。要求 worker 按固定模板交卷:改动文件清单 / 测试结果(数字)/ 偏差与原因 / 遗留问题。散文式的自由发挥,主代理没法整合。
  • 验收权在主代理。worker 说"做完了"不算完——主代理必须独立跑全量测试再整合。这一条与第 16 章"清单是唯一真相源"一脉相承:报告不算数,验证才算数。

四要素可以直接落成提示词模板,每次派发填空即可:

【Worker 提示词模板 · 四要素】
目标:<一句话说清做什么,指向 spec / 任务卡的文件路径>
范围:仅允许改动 <文件与目录清单>;禁止触碰 <…>;禁止安装新依赖。
约定:<AGENTS.md 之外的特殊约定,逐条列出,宁多勿漏>
完成定义:<可执行的验收:哪些测试全绿、哪些 AC 通过、lint 无错误>
回传格式:改动文件清单 / 测试结果(数字)/ 偏差与原因 / 遗留问题。

适用与反指标

适用:前后端并行实现、多模块同构开发、改动面大但切片清晰的大 PR。

反指标:子任务强耦合(A 的输出是 B 的输入——该串行或改流水线);接口约定还没定就想派活(先写 spec 再拆,见第 15 章)。

17.3 模式二:流水线(pipeline)

把一件事按工序切开,各角色接力,中间产物落盘传递:

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["人工终审 → 交付"]

要点

  • 文件是流水线的传送带。spec.md、design.md、test-report.md 落在 docs/tasks/,下游只读上游的产物,不读上游的对话——和第 16 章 handoff 同一个道理:代理之间没有共享记忆,只有共享文件。
  • 标准化是前提。流水线适合"每次都这么走"的工序(如新模块从需求到交付的固定五站);工序本身都要临场商量的事,套流水线只是昂贵的传话筒。
  • 关键闸门留给人。至少在 spec 批准处设人工检查点——错误传得越远,修起来越贵(第 15 章的方向性错误成本,在流水线里会被逐站放大)。

优缺点

优点:每个角色的上下文精纯(实现代理只看设计文档与相关代码,不背需求讨论的包袱);职责清晰,每站产物可审计、可返工。

缺点:传递损耗(上游一句话的歧义,到下游放大成半天返工);整体延迟是各站之和(没有并行);上游错误的兜底成本随站数上升。

适用:工序固定的重复型交付(新 CRUD 模块、新数据报表);反指标:流程不标准(每单都要重新讨论工序);任务太小(两轮对话能搞定的事走五站流水线,纯属仪式感)。

17.4 模式三:评审者-实现者(maker-checker)

一个写、一个审:实现者(maker)写代码,评审者(checker)只读不写,产出问题清单。

flowchart TB
    I["实现者代理<br/>写代码 + 测试"] -->|"diff + 简要说明"| C["评审者代理<br/>只读,独立上下文"]
    C -->|"问题清单(分级)"| I
    I -->|"修订后再审"| C
    C -->|"通过"| H["人类终审"]

要点

  • 审者的"无利益关系"是质量来源。评审者在独立上下文里只拿到 diff 与任务卡,看不到实现者的自我辩护,没有沉没成本——挑刺才挑得动。
  • 权限上锁死只读。审者只许读代码、跑测试、出报告,不许直接改——改动权留在实现者手里,否则"审"就变成了"抢方向盘"。
  • 问题清单要分级:阻断(必须修)/ 建议(可商量)/ 疑问(请解释)。没有分级的评审意见没法执行。

红队变体

对抗式用法:专门派一个代理当红队(red team),指令是"你的唯一目标就是搞垮这个实现"——找注入点、找 XSS、构造恶意输入、逼近边界条件。适合安全敏感的改动(BookNest 的 NFR-2 全部适用)。红队报告同样回传给实现者修复,人做终审。

适用:核心链路、安全敏感改动、合并前的把关;反指标:三行样式的改动(评审成本大于风险);没有评审标准(审者只能输出"感觉不够好"这类空话);把红队当日常(对抗式评审很贵,按需开)。

17.5 模式四:扇出汇总(fan-out / map-reduce)

同构任务并行:一个模板,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["最终报告 / 迁移完成"]

要点

  • 同构性(homogeneity)是前提:所有切片用同一套指令、同一套产出模板。多文件迁移(JS→TS)、批量文档翻译、全库审计扫描(安全 / 依赖 / TODO 清点)都是典型。
  • 切片必须互斥:按目录或文件名切,谁都不许碰别人的文件——否则并行就变成冲突制造机。
  • 汇总要去噪:worker 报告必然有重复(同一问题被三个人各报一遍)、误报(模板套错对象)、详略不一。主代理要合并去重、按严重度排序,并抽查复核——从报告里抽三条亲自验证,校准整份报告的可信度。

适用:批量同构操作、只需要"各管一段最后拼总"的扫描类任务;反指标:任务不同构(每片都要不同策略——那是主从编排的事);结果需要交叉讨论才能定论(汇总不是简单拼盘)。

统一回传模板是去噪的前置条件,派发时随提示词一起下发:

【扇出统一回传模板】
切片:<编号与范围,如 src/services/a/**>
发现:<每条一行:文件位置 + 问题 + 严重度(高/中/低)>
完成度:<完成 / 部分完成(原因)>
建议:<是否需要人工跟进,具体哪几条>

17.6 模式五:探索-实现分离(explore-then-implement)

只读探索代理先画地图,实现代理按图施工。

flowchart LR
    E["探索代理(只读)"] -->|"ARCHITECTURE.md<br/>勘探报告"| P{"人工确认地图"}
    P --> I["实现代理<br/>按图施工"]
    I --> V["主代理验收"]

要点

  • 探索代理只读:产出勘探报告——模块清单、数据流向、既有约定、风险点、建议改动面;直接写进 ARCHITECTURE.md 随 git 留档。
  • 地图是一次性投资、多次回收:一份勘探报告可以喂给多个实现 worker,也可以喂给两周后的你自己——这正是第 16 章"状态外置"在多代理下的形态。
  • 人工确认地图再开工:地图错了,后面全错。人花两分钟扫一遍报告,是整个模式里性价比最高的两分钟。

适用:接手陌生代码库、跨模块大改动前的勘察;反指标:代码库是你自己刚写的(地图在你脑子里);改动范围一目了然的小活。

这个模式会在第 22 章大放异彩:接手 booknest-legacy(无类型、无测试、单文件巨石的故意劣化版)时,"先勘探后动刀"是保命规则——面对完全陌生的代码库,先花一个只读代理的预算买一张地图,比蒙眼施工便宜一百倍。

勘探报告不必华丽,五个小节足够,关键是让实现代理"按图能施工":

# 勘探报告 · <代码库 / 模块名>
- 模块清单:入口文件与目录职责(一张表,别写成散文)
- 数据流向:请求从哪进、经过哪几层、落在哪里存储
- 既有约定:命名、错误处理、测试组织(含反常之处,如"有一个
  模块例外地没用统一错误中间件")
- 风险点:无测试覆盖的高危区、明显的坏味道、改动禁区
- 建议改动面:本次任务的最小改动路径,与备选路径的取舍

五模式总对比

模式 一句话 并行度 沟通成本 典型场景 最大风险
主从编排 总控派活,worker 干活 高 中(提示词要自包含) 前后端并行、多模块开发 集成冲突
流水线 角色接力,产物落盘 无(串行) 低(文件传递) 标准化工序 传递损耗
评审者-实现者 一个写一个审 低 低 质量与安全把关 评审流于形式
扇出汇总 同构任务并行跑 最高 低(模板统一) 批量迁移、全库审计 汇总噪声
探索-实现分离 先画地图再施工 中 低 陌生代码库 地图过时

选型口诀:看瓶颈选模式,看耦合定并行,看标准定流水。

贯穿项目实战:轻读 BookNest

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

具体理由,条条对应本项目事实:

  1. 切片互斥,合并零冲突:FR-4 只动 stats 相关文件(对 books/notes 只读),FR-5 只动 import/export 相关文件——两个 worker 的文件集交集为空(迁移脚本编号错开即可)。17.2 节最大的风险"集成冲突",被拆分设计直接消掉;
  2. 约定已落盘,自包含提示词成本低:FR-4 有现成 spec(第 15 章产物),FR-5 的导出格式开工前补一页 spec 即可;通用约定由 AGENTS.md 承担,worker 提示词只需四要素,不用科普项目背景;
  3. 验收可自动化:NFR-1(核心链路覆盖率 ≥ 80%)与 Vitest / Playwright 全绿就是硬验收,主代理不必逐行读懂 worker 的代码也能把关——这正是"验收权在主代理"的底气;
  4. 风险面可控:FR-4 全只读;FR-5 的导入有事务与校验兜底,最险,所以收尾时对两份 diff 叠加一轮只读评审。红队暂不启用:个人自部署、无公网暴露面,成本不划算。

派发前一个小插曲,正好验证第 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 走,代码变了就更新

最佳实践

  1. 上多代理前先问撞的是哪堵墙:窗口、速度还是角色——三堵都没撞,就留在单代理加计划模式;
  2. 拆分两原则:切片互斥、接口先行,共享约定写进 spec 再派发;
  3. worker 提示词四要素写全:目标、范围(含禁止事项)、约定、完成定义;
  4. 回传一律结构化:文件清单、测试数字、偏差、遗留——拒绝散文式汇报;
  5. 验收权永远在主代理与人类:worker 报"完成"后必须独立跑全量测试;
  6. 流水线的中间产物落盘,并在 spec 批准处设人工闸门,错误要早拦;
  7. 评审者给独立上下文与只读权限,问题清单按阻断 / 建议 / 疑问分级;安全敏感改动加红队一轮;
  8. 扇出先统一输出模板,汇总时去重、排序、至少抽查三条;
  9. 陌生代码库先派只读探索代理画地图,ARCHITECTURE.md 落盘随 git,人确认后再施工;
  10. 并行 worker 数量从 2 起步,跑顺了再加——规模跟着集成能力走,不跟着热情走。

本章小结

多代理不是排场,是工具:只有当单代理撞上"窗口装不下、串行太慢、自己审自己"三种瓶颈之一时,才值得付出沟通成本与集成风险,去换并行加速与上下文隔离。五个模式各有锋刃——主从编排靠总控拆分、派发、整合、验收;流水线靠标准化工序与落盘产物接力;评审者-实现者用无利益关系换质量,红队是对抗式变体;扇出汇总用同构切片与统一模板换吞吐;探索-实现分离用一张地图换整个实现阶段的轻量上下文。而选型的钥匙始终在前三章手里:任务拆得干净(第 14 章)、约定落了盘(第 15 章)、状态外置成文件(第 16 章),多代理才有得拆、有得传、有得接。

要点回顾:

  • 三瓶颈:上下文装不下、串行耗时、角色混淆;trade-off:并行与隔离,换沟通与集成;
  • 主从编排:worker 提示词自包含(目标 / 范围 / 约定 / 完成定义),结果结构化回传,验收权在主代理;
  • 流水线:文件是传送带,标准化是前提,spec 批准处设人工闸门;
  • 评审者-实现者:审者只读、独立上下文、清单分级;红队按需启用;
  • 扇出汇总:同构、互斥、统一模板;汇总必须去噪并抽查;
  • 探索-实现分离:只读勘探落盘 ARCHITECTURE.md,人确认地图后再施工。

动手练习

  1. 模式识别:给下面三个场景选模式并说明理由:(a) 把 30 篇 Markdown 文档批量翻译成英文;(b) 接手一个没有文档的旧仓库准备重构;(c) 一个涉及支付回调的 PR 合并前把关。
  2. 自包含挑战:把"帮我把 BookNest 的 books 路由加上分页"改写成自包含 worker 提示词,四要素齐全;然后真的发给一个子代理(或开个新会话假装 worker),看它还缺什么信息,缺的补进提示词。
  3. 红队脚本:为 FR-5 的 JSON 导入功能写一份红队代理提示词:攻击面包括恶意构造的 JSON、超大文件、字段类型混淆、编码陷阱;若导入流程涉及拉取封面图 URL,提示词中必须写明域白名单校验(SSRF 防护)。
  4. 综合实践:为 FR-6(全文搜索,LIKE 与 SQLite FTS5 二选一)做一次完整推演:先用探索-实现分离产出勘探报告(现有查询路径、数据规模、两种方案的改动面)→ 人确认地图 → 选定模式实现 → 评审者-实现者收尾。交付:勘探报告、worker 提示词、评审问题清单各一份,外加 200 字复盘:多代理比单代理省在哪、费在哪。

导航

📄 第 18 章 · Subagent 实战:派活的正确姿势 18-subagents-in-practice.md

第 17 章回答了"怎么编队"——五种架构模式各有锋刃;本章回答"怎么派活"——模式是图纸,subagent(子代理)才是施工的人。什么活该派出去、派发提示词怎么写才不算自断生路、回传怎么验收、并发与成本怎么管、什么时候该给团队设"专岗",是本章的五件事。第 19 章再把这套动作固化成可复现的编排脚本。

学习目标

读完本章,你应该能:

  • 说清 subagent 与主会话的关系:独立上下文、临时、只回传结论;
  • 用决策表判断一个任务是"派发"还是"自己干",并守住"已派发不重做"的纪律;
  • 按"自包含五要素"写出派发提示词,识别"按上面讨论的做"式必败写法;
  • 用结构化模板要求并验收子代理回传,把重要结论沉淀成文件;
  • 管好并发与成本:逐批派发、模型分级、token 意识;
  • 定义一个自定义 subagent(如只读评审员),并判断何时值得造专岗。

18.1 Subagent 是什么:独立上下文的临时工

主会话(你正在对话的这个会话)通过 Agent 工具派生出 subagent(子代理;Claude Code 中对应 Task 工具)。它有三个关键词:独立上下文(有自己的上下文窗口,主对话历史不会跟过去)、临时(任务完成即销毁)、只回传结论(干完活交回一份报告,而不是一路的碎碎念)。

最后一点最容易被低估。"只回传结论"意味着:子代理读了 30 个文件、跑了 10 次命令、绕了三次弯路——这些过程全部留在它自己的窗口里,随会话销毁;主上下文只多出一份结论。

# 同一件事,两种上下文代价

【自己干】主会话直接翻 30 个文件找"鉴权在哪实现"
  → 30 个文件的片段、中间推测、走错的岔路全部进入主上下文
  → 窗口被勘探过程填满,后续设计讨论背着一身噪音

【派发】subagent 翻 30 个文件(在它自己的窗口里)
  → 主上下文只多两行:"鉴权在 server/src/middleware/auth.ts,
     JWT 校验,密钥读自 process.env.JWT_SECRET"
  → 主窗口干净如初

两大核心收益

  1. 上下文隔离(context isolation)。探索性的脏活——全库检索、现状盘点、方案比对——读得多、结论小,最适合隔离在子代理窗口里。主会话的窗口留给决策与整合,不留给过程。
  2. 并行加速(parallel speedup)。互不依赖的切片可以同时派给多个 subagent,串行几小时的活在并发 2 的环境里压成一半时间;并发上限更高的环境收益更大。

这两条正是第 17 章 trade-off 里"得到"的那两项——本章的任务是把它们可靠地拿到手,同时把"付出"(沟通成本、集成风险、token 成本)管住。

模式是战术,subagent 是执行单元

第 17 章的五种模式,落到执行层都是同一样东西的变体:

  • 主从编排的 worker、扇出汇总的切片执行者——实现型 subagent;
  • 评审者、红队——只读型 subagent(工具白名单锁死,见 18.5);
  • 探索代理——只读型 subagent 加报告落盘(见 18.4)。

本章讲"用好这个执行单元",第 19 章讲"把用法写成代码"。战术与单元都齐了,编队才算真的能打仗。

18.2 何时派发,何时自己干

派发不是免费的:写自包含提示词要几分钟,等回传、验收又要几分钟。所以"派不派"是一笔账——收益(隔离、并行)盖过开销(沟通、验收)才派。

适合派发的三类任务

  1. 检索勘探类:找代码、画地图、查现状、比对方案。读得多、写得少、结论几行——上下文隔离的最大受益者。
  2. 批量同构类:N 个互斥切片,同一套指令、同一套产出模板(批量迁移、批量扫描、批量翻译)。扇出汇总模式的执行层。
  3. 独立文件实现类:范围清晰、文件集互斥、约定已成文的实现切片(主从编排的 worker)。前提是第 15 章的 spec 已经写好——接口没定就派活,等于把混乱并行化。

不适合派发的任务

需要连续对话上下文的紧耦合任务:边聊边改的调试、需求还没定型的原型、依赖"我们刚才说的那个思路"的活。子代理看不到对话——派发即断链,这类活留在主会话干。另外,两三分钟能收工的小改也别派:派发与验收的固定开销比活本身还贵。

一条纪律:已派发的搜索别自己再做一遍

派出去的活,主代理不要又亲手做一遍。实测里最常见的浪费:派了检索子代理,等回传的间隙手痒,自己又 grep 了一轮同一范围——双倍 token、双倍时间,还可能得到两份口径不一的结论,整合时自相矛盾。派发之后,主代理的精力只花在两处:验收与整合。

派发决策表

任务特征 判断 理由
要读 ≥10 个文件才能下结论,结论本身几行 派发 隔离:勘探噪音不进主窗口
≥3 个互不依赖的同构切片 派发(并发) 并行加速,切片互斥零冲突
实现切片文件集互斥、spec 已成文 派发 worker 上下文精纯,主代理保验收
需要连续追问、高度依赖对话脉络 自己干 子代理看不到对话,派发即断链
两三分钟能完成的小改 自己干 派发与验收开销大于收益
需求未定型、要边看边决策 自己干 决策在你,代理只是手;定型后再派

18.3 提示词自包含原则(本章核心)

本章最重要的一节只有一句话:子代理完全看不到主对话历史。

它能看到的世界只有三样:你的派发提示词;它自己运行中读到的文件与命令输出;系统级注入的项目配置(AGENTS.md、规则文件通常会被带进子代理上下文——第 4 章的投入在这里回本,通用约定不必在提示词里重复)。而你们在对话里临场达成的任何约定——"就按刚才那个方案""老规矩"——一概不在。

自包含派发提示词模板(五要素)

【自包含派发提示词模板 · 五要素】
目标:<做什么 + 怎么算做完(可执行验收,指向 spec / 任务卡路径)>
背景:<这是什么项目、这活从哪来;子代理需要却看不到的上下文全在这里>
相关路径:<必读文件清单;允许改动的文件/目录;禁止触碰清单>
约束:<AGENTS.md 兜不住的约定 + 安全红线:禁装新依赖、代码零密钥字面量
      (一律 process.env 或 <your-token> 占位)、禁止请求白名单外的网络地址>
期望输出:<结构化回传格式,逐字段列清(模板见 18.4)>

典型错误示范,及其必然失败

❌ "按上面讨论的方案做,注意性能。"
   子代理:哪"上面"?(它看不到对话)→ 只能瞎猜方案,或反问中断流程
❌ "参照之前 books 模块的做法,给 notes 也来一套。"
   子代理:books 模块在哪、"做法"是什么都不知道 → 乱翻一通,方向做偏
❌ "还是老规矩,先写测试再实现。"
   "老规矩"只存在于你的对话记忆里 → 被直接忽略

两条纠正路:对话里的方案压缩成三行结论写进提示词;"参照做法"落盘成文件给它一个路径——让子代理去读 server/src/routes/books.ts,比让它"参照之前的约定"可靠一百倍。后者又一次是第 16 章"状态外置"的道理:代理之间没有共享记忆,只有共享文件。

自包含 ≠ 啰嗦

AGENTS.md 能兜底的通用约定不必重复写;五要素通常各一两行就够。检验法很简单:假装把这份提示词发给一个昨天刚入职、没参加过任何会议的同事,他能不能不追问就开工。能,就自包含了。

18.4 结果回传与验收

结构化回传模板

散文式汇报没法比对、没法去重、没法整合。派发时在"期望输出"里就定死四字段:

【结构化回传模板】
结论:<一句话:干成了什么、是否达标;实现类任务附测试数字>
关键文件路径:<改动/新增/需主代理复核的文件,仓库相对路径>
未尽事项:<没做的、没验证的、拿不准的、与指令的偏差;没有就写"无">
建议下一步:<主代理接手后应该做什么>

主代理的验收三动作

worker 说"做完了"不算完——第 17 章"验收权在主代理"在这里落地为三个动作:

  1. 抽查文件:从回传的关键路径里挑 1–2 个亲自读,优先改动最重、风险最高的那个;
  2. 独立跑验证:自己跑测试与 lint,不信回传里的数字——报告不算数,验证才算数;
  3. 逐条比对完成定义(DoD,Definition of Done):目标里的验收标准一条条对;偏差逐条裁决(接受则回写 spec,拒绝则打回),不做"大概没问题"的糊涂账。

会话即焚,文件长存

子代理运行结束,除了回传文本,它的一切都蒸发了。**重要结论不落文件,就等于没发生。**三个习惯:

  • 探索类任务,在派发提示词里直接写明:"最终报告同时写入 docs/explorations/<主题>.md";
  • 实现类回传里的"建议下一步",由主代理转记进任务卡或看板(第 16 章的状态外置);
  • 验收结论(尤其偏差裁决)追加到对应 spec 的修订记录——这是第 17 章"过期计划比没有计划更危险"的解药。

18.5 并发、成本与自定义代理

并发上限与逐批派发

以本书写作时(2026-10)的 ZCode 版本实测:同时运行的 subagent 上限为 2,一次派发第三个会直接收到报错。不同工具、不同版本的上限各异,但稳妥的姿势是同一个——逐批派发:每批 ≤2,等这批全部回传并验收完,再派下一批。这恰好和第 17 章"并行 worker 从 2 起步"对上了:并发上限不只是工具约束,也是和你的验收整合能力匹配的节奏。

模型分级

派发时可以给子代理指定模型(ZCode 的 subagent_model 参数;Claude Code 的 Task 工具同理):

  • 检索勘探、批量扫描——便宜快档:量大但每步都简单,快与省优先;
  • 实现、评审、红队——能力强档:质量敏感,一步出错返工更贵。

提示词的详略也应跟着分级:勘探类的提示词可以很短,实现类的五要素必须写全。

token 成本意识

子代理没有你的上下文红利:它读的每一个文件都从零计费。三个省法——必读清单给路径不给全库("读 AGENTS.md 和这三个文件",而不是"熟悉一下项目");切片小而互斥(范围越小,读的越少);勘探类回传要求一行一条,别让它写散文。

自定义 subagent:给团队设专岗

当同一个角色连同它的提示词被反复派发,就值得把它定义成专职代理。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 章的三个模式。一次性的角色别定义:定义文件本身也是要维护的成本,写进派发提示词即可。

贯穿项目实战:轻读 BookNest

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

最佳实践

  1. 派发前过一遍决策表:检索勘探、批量同构、独立文件实现三类才派;紧耦合对话任务自己干;
  2. 派发提示词五要素写全,检验标准是"新同事不追问就能开工";
  3. 对话里的临时约定要么压缩进提示词,要么落盘成文件给路径——别指望子代理记得"刚才";
  4. 期望输出定死四字段:结论、关键文件路径、未尽事项、建议下一步;实现类结论必须带测试数字;
  5. 验收三动作缺一不可:抽查文件、独立跑验证、逐条比对完成定义;偏差必须裁决并回写 spec;
  6. 重要结论落盘:探索报告进 docs/explorations/,待办进任务卡——会话即焚,文件长存;
  7. 逐批派发,每批 ≤2,验收完再派下一批;并发跟着验收能力走,不跟着热情走;
  8. 模型分级:勘探扫描用便宜档,实现评审用强档;token 成本靠"必读清单最小化"来控;
  9. 同一角色复用 ≥3 次再定义专职代理(评审员/勘探员/红队最常见),只读岗位锁死工具白名单。

本章小结

Subagent 是带独立上下文的临时工:过程留在它自己的窗口里,只有结论回到主会话——这换来上下文隔离与并行加速两大收益,代价是沟通(提示词必须自包含)与验收(回传不算数,验证才算数)。派发是笔账:检索勘探、批量同构、独立文件实现三类活值得派,紧耦合对话任务自己干;派出去的就别再做一遍。提示词五要素(目标/背景/相关路径/约束/期望输出)是本章的心法,结构化回传四字段与验收三动作是本章的手法;并发逐批、模型分级、结论落盘是日常纪律;专职代理留给复用三次以上的角色。

要点回顾:

  • subagent 三关键词:独立上下文、临时、只回传结论;收益 = 隔离 + 并行;
  • 派发三类:检索勘探、批量同构、独立文件实现;纪律:已派发的搜索别自己再做一遍;
  • 自包含五要素:目标、背景、相关路径、约束、期望输出——子代理看不到主对话历史;
  • 回传四字段 + 验收三动作(抽查 / 独立跑 / 比对完成定义);会话即焚,文件长存;
  • 并发实测上限 2、逐批派发;模型分级省钱;token 靠必读清单最小化来控;
  • 专职代理 = 复用 ≥3 次的角色,只读岗位锁死工具白名单。

动手练习

  1. 派发判断:四个场景判断"派发还是自己干"并说理由:(a) 全库找出所有硬编码字符串字面量并归类;(b) 调一个"偶现"的竞态 bug,需要反复试错;(c) 把 12 个组件的 class 命名统一成 BEM 规范;(d) 和产品讨论完立刻改首页布局。
  2. 自包含体检:把这句派发提示词改写到自包含——"帮我把搜索功能优化一下,参照之前 books 列表的做法,注意老规矩";逐条列出你补了什么、为什么。
  3. 设一个专岗:为 BookNest 写一个红队 subagent 定义文件(name/description/工具白名单/系统提示),并真的派发一次:攻击目标是 fragments 导入接口的测试代码(构造恶意 JSON、超大文件、类型混淆);若涉及拉取外部封面 URL,域白名单校验必须出现在系统提示里。
  4. 综合实践:跑通一次"勘探→确认→实现→验收"闭环——派只读探索代理盘点 BookNest 全部路由的错误响应格式(现状是否统一、偏离 AGENTS.md 之处),报告落盘 docs/explorations/error-format.md;你确认地图后,派实现代理统一错误响应中间件;最后按验收三动作收尾。交付:两份派发提示词、一份回传、一段验收记录,外加 150 字复盘:哪一步最省、哪一步最贵。

导航

📄 第 19 章 · Workflow 编排:把多代理流程写成代码 19-workflow-orchestration.md

第 18 章的派发是对话式指挥:主代理临场拆活、临场写提示词、临场验收。灵活,但不可复现——同一场战役打第二遍,还得从头指挥一遍。本章把多代理流程写成 TypeScript 脚本(workflow):子代理是声明,控制流是代码,中间结果是类型,跑过的步骤进缓存。对话式管一次性的仗,workflow 管要反复打的仗。

学习目标

读完本章,你应该能:

  • 判断一个多代理任务值得写成 workflow,还是直接对话派发;
  • 读懂并写出一个 workflow 脚本:声明阶段、控制流、类型化中间结果、日志与交付;
  • 利用 journal 缓存做断点恢复与增量修改,说清"改一处不必全量重付";
  • 把子代理失败分成三类并选对处置:修脚本、重试、换模型,会用 amend 继承缓存;
  • 用伪代码把第 17 章五种模式落成 workflow,并给流程装上三道护栏。

19.1 从对话式派发到脚本化编排

Workflow 是一段 TypeScript 脚本:声明要派哪些子代理(agent 调用),然后用普通控制流——循环、条件、Promise.all 扇出——把它们串成流程;中间结果以类型化数据在步骤间传递;引擎负责调度、并发约束、缓存与日志。写它的心智模型不是"学一个新框架",而是"写一段会派活的普通代码"。

两种打法对比

维度 对话式派发(第 18 章) Workflow 脚本(本章)
灵活性 高:边跑边改、见机行事 低:改流程要修订脚本重新提交
可复现 低:同一场对话无法重放 高:脚本即流程文档,一条命令重跑
断点恢复 依赖会话存续与你的记忆 journal 缓存,续跑只补未完成的步骤
可审计 翻聊天记录,靠回忆拼流程 结构化日志 + 落盘产物,逐步可查
上手成本 低,随聊随派 要写脚本、定类型
适合 一次性、探索性任务 可重复的多步流程

什么时候值得写 workflow

三条判据,命中两条以上再动手:流程会重复(预计跑 ≥2 次);步骤间有需要精确控制的依赖与并发(A 完成才能跑 B、扇出不得超过上限);中间结果需要结构化传递(下游要机器可读的数据,不是散文)。

反过来,一次性的勘探、一个临时的小重构——直接按第 18 章派发,写脚本的时间比活还长。另一个常被忽略的收益:workflow 文件入 git,流程就从"在某人的脑子里"变成"在仓库里"——团队任何人都能看懂、复跑、评审这条流水线。

19.2 编排脚本解剖

一个 workflow 脚本由四个部件组成:

  • 声明阶段:入参(args)、子代理用什么模型、并发上限多少;
  • 控制流:await 顺序即串行接力;Promise.all 即扇出;if/while 即条件与循环;
  • 类型化中间结果:interface 定义步骤间的数据契约——下游拿到的是数据,不是散文;
  • 日志与交付:log() 上报进度(运行中可实时查看),report() 产出最终交付物。

不同工具的 workflow 门面(facade)API 命名略有差异,本章用 ZCode 的简化写法示意,实际以所用版本的脚本文档为准;四个部件的概念完全通用。

完整示例:扇出分析 5 个文件并汇总

// 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 个文件全喂给强模型省一个量级。

19.3 缓存与断点:改一处,不必全量重付

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(修订运行)继承同一套缓存——脚本本身要改时,改完提交新运行,已完成且输入未变的步骤照样命中。

19.4 失败处理:三分类与两条路

子代理失败三分类

失败类型 典型症状 处置
提示词缺陷 产出答非所问、格式不合、范围越界 修脚本(提示词/范围/类型)后重新提交;缓存继承,只重跑受影响步骤
环境问题 网络抖动、命令超时、文件被锁 原样重试(resume),什么都不用改
模型错误 反复幻觉、结构化输出崩坏、同一步骤连败 给该步骤换模型档位(更强或更稳)再跑

盲目重试是头号错误:提示词有缺陷时,重试一百次结果一样。先分类,再动手。

两条路:amend vs 推倒重跑

amend(修订运行):脚本有缺陷时,修订脚本后以新运行提交——引擎自动继承前一次运行的已完成步骤作为缓存,只有改动波及的步骤重付成本。推倒重跑:整个方向错了(需求变了、切片设计错了),才值得清空重来。口诀:脚本错了 amend,方向错了重来。

错误升级:让子代理反问

子代理运行中遇到"答案会改变后续走向"的歧义(切片边界怎么切、疑似数据损坏要不要继续),可以升级(escalate)为问题抛给编排者:该子代理暂停等待,其余子代理与主控流程照常推进;答复原样传回,它从断点继续。反之,鸡毛蒜皮的选择(变量名叫什么、注释怎么措辞)不该问——那是第 18 章自包含原则要堵住的洞。

[subagent fix:group-2 → escalate]
  问:修复 docs/spec/fr5-import.md 时发现 spec 与实现冲突(导入去重策略不同),
      以哪个为准?
[编排者 → 人] 建议:以 spec 为准,实现侧另开 issue;确认?
[人 → 编排者] 确认。
[subagent fix:group-2] 收到,按 spec 修订文档,继续。

19.5 五模式落 workflow 与三道护栏

第 17 章五种模式的伪代码

// 主从编排:总控拆片,扇出实现,独立验收
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));

三道护栏

  1. 并发上限:脚本里显式声明(如并发 2)并据此分批扇出——超发不是"慢一点",是直接报错;
  2. token 预算:给运行设预算上限,超限暂停或降档(勘探步换便宜模型);预算意识会逼你把切片做小;
  3. 人工闸门:关键节点用 escalate 停下等人——地图确认、spec 批准、批量删除前。第 15 章"错误传得越远修起来越贵"在编排层的形态,就是把这些闸门写进脚本,而不是靠人盯聊天窗口。

贯穿项目实战:轻读 BookNest

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 等人确认,写进脚本而非靠盯屏

最佳实践

  1. 写 workflow 前过三条判据:可重复、依赖并发需精确控制、中间结果要结构化——命中两条再写;
  2. 中间结果一律定义类型;下游拿到的应该是数据,不是一段话;
  3. 汇总、去重、分组这类归约用纯计算完成,不花子代理的钱;
  4. 扇出显式声明并发上限并分批;脚本要能在并发 2 的环境里跑;
  5. 模型分级写进脚本:扫描勘探便宜档,修复验证强档,条件分支让贵的调用只花在刀刃上;
  6. 验证步骤独立上下文 + 独立执行(lint/test 自己跑),不信修复者自述;
  7. 失败先三分类再动手;提示词缺陷修脚本 amend,环境问题原样重试,模型问题换档;
  8. workflow 文件入 git:脚本即流程文档,团队可评审、可复跑、可演进;
  9. token 预算与人工闸门写进脚本——编排的成熟度,体现在它什么时候自己停下来。

本章小结

Workflow 把多代理流程从"对话里临场指挥"升级为"仓库里可复现的代码":声明子代理,用普通控制流串接,中间结果以类型传递,进度与交付有日志与落盘。它不是对话式派发的替代——一次性任务依然直接派;它是可重复多步流程的形态。journal 缓存让"改一处不必全量重付",断点恢复与 amend 让失败只付增量;失败三分类(提示词缺陷/环境问题/模型错误)各有一条正解;并发上限、token 预算、人工闸门三道护栏,决定这条流水线是省心的资产还是失控的开销。第 17 章的五种模式全部可以在脚本里落成几行伪代码——至此,多代理协同从战术(模式)、单元(subagent)到流程(workflow),三块拼图齐了。

要点回顾:

  • 对话式派发灵活不可复现;workflow 可复现、可断点、可审计,适合可重复的多步流程;
  • 脚本四部件:声明阶段、控制流(await / Promise.all / 条件)、类型化中间结果、日志与交付;
  • journal 按"步骤名 + 输入指纹"缓存;断点恢复只补未完成步骤,amend 继承缓存只付增量;
  • 失败三分类:提示词缺陷修脚本、环境问题重试、模型错误换档;口诀"脚本错了 amend,方向错了重来";
  • escalate 问答机制:该问的(改变走向的歧义)停下来问,其余子代理与主控照常跑;
  • 三道护栏:并发上限、token 预算、人工闸门——编排的成熟度在于何时自己停下来。

动手练习

  1. 判据演练:三个任务判断"直接派发还是写 workflow"并说明命中了哪几条判据:(a) 给 BookNest 全部页面统一加暗色主题适配;(b) 突发奇想查一下"哪个组件最长";(c) 每次发版前都要跑的"全量回归 + 报告生成"。
  2. 扩写脚本:给 19.2 的 file-insights.ts 加一个阶段——对热点文件各派一个深读代理,产出"重构建议"并定义新的中间结果类型;写出增量代码,并说明哪几步会命中缓存。
  3. 失败处置:给定三条失败日志——(a) fix:group-3 连续三次返回非法 JSON;(b) scan:src/db/pool.ts 报文件被占用;(c) scan:web/src/pages/Home.tsx 的提示词漏了必读文件导致范围越界——分别分类并写出处置动作。
  4. 综合实践:为 BookNest M4 的"端到端测试补齐"写一个 workflow 脚本:阶段 1 扇出勘探各页面清单与关键路径 → 阶段 2 汇总去重并用 escalate 请人确认用例优先级 → 阶段 3 分组编写 Playwright 用例(组间页面互斥)→ 阶段 4 全量回归。实际跑通一次,过程中至少经历一次 amend,交付脚本、运行日志节选与 200 字复盘。

导航

📄 第 20 章 · 协同规范与治理:让编队不内讧 20-collaboration-governance.md

第 19 章把多代理装进了 workflow 脚本:怎么拆、怎么派、失败怎么恢复,机器替你管了。但编队真正会翻车的地方,往往不在"怎么派活",而在"派出去之后"——两个 worker 改同一个文件、各自发明一套接口、单看都能跑合起来就崩。本章讲多代理的"交通规则":文件所有权、契约先行、集成节奏、人工检查点与成本治理。治理的本质只有一句话:用规范换并行度——规范越明确,可放权越大。

学习目标

读完本章,你应该能:

  • 能说出多代理协同的三大事故源(文件冲突、约定漂移、集成失败)及各自的成因与早期信号;
  • 能用文件所有权协议为并行 worker 声明"只准改这些路径",并按三类规则处置共享文件;
  • 能用接口先行(contract-first)在派发前固化契约,并保证 mock 依赖方向正确、变更显式广播;
  • 能设计小批次集成节奏与"每卡一 PR"的代理版持续集成工作流,并按流程处置集成冲突;
  • 能列出必须人工检查点的节点清单、写进 AGENTS.md,并建立 token 成本治理的习惯;
  • 能把本章规范沉淀成团队级《多代理协作章程》,纳入新人 onboarding 并随事故持续进化。

20.1 多代理的三大事故源

第 17 章说过:"边界不清就上多代理,等于把混乱并行化。"本章先把"混乱"具体化——它几乎总是以下三种事故之一或其组合。

事故一:文件冲突(两个代理同时改一个文件)

两种形态,危害天差地别:

  • 同一工作区并行(新手默认形态):两个 worker 在同一个目录里同时跑,A 写盘后 B 用自己内存里的旧版本覆盖回去——连冲突标记都没有,改动无声消失,直到某天测试爆炸才发现;
  • 独立分支并行(推荐形态,第 12 章的 Git 隔离):各自在自己的分支上改,合并时 git 给出冲突标记——可见、可解,代价只是处理冲突的几分钟。

所以文件冲突的第一道防线不是"小心点",而是隔离前提:多代理并行前,每个 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 会不会互相拆台"。

20.2 文件所有权协议

范围是硬约束,不是建议

任务卡(第 14 章七字段模板)里的"涉及文件"、worker 提示词(第 17 章四要素)里的"范围",在多代理语境下必须升格为所有权声明(ownership declaration):只准改清单内的路径,清单外一律只读。执行靠三层防线:

  1. 提示词声明:范围写进每个 worker 的自包含提示词,并用"禁止触碰"句式点名共享文件;
  2. 权限拦截:用 hooks(第 13 章)在工具调用层兜底,越界写入直接阻断:
{
  "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、迁移编号序列。三类处置,按成本从低到高:

  1. 串行修改:把共享文件的修改排进批次计划,一个时间窗内只有一个人改。适合低频修改(如迁移编号,按卡顺序申请顺延);
  2. 编排者代改:worker 在回传里提交"修改申请"(改什么 / 为什么 / 影响面),主代理评估后统一执行并广播,相关 worker rebase 后继续。适合依赖引入、AGENTS.md 更新;
  3. 结构消共享:改代码结构,让"共享"变"独占"——路由自动注册(扫描 routes 目录批量挂载)、跨端类型按域拆文件(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 看清双方意图,不要机械地"取我方"或"取对方";②恢复后把事故记进章程的"事故记录"——一条事故换一条规则;③同一文件两周内冲突两次,触发上面的"结构消共享"。

20.3 接口先行(contract-first)

先固化契约,再并行实现

契约(contract)是并行 worker 之间的"共同协定",必须在派发前定稿。三类契约:

  • API schema:OpenAPI(openapi.yaml)或直接 TypeScript 类型——前后端同构的 TS 项目用类型最省事;
  • 事件名:凡有事件总线、WebSocket 推送、自定义 DOM 事件,事件名与 payload 形状同样是契约;
  • 数据表结构:迁移脚本先行——schema diff 即契约(第 15 章的做法,迁移编号按 20.2 的串行规则申请)。

一个契约文件长这样:

// 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 的人工检查点前置到这里。

mock 依赖方向:前端 mock 后端

契约定了、后端还没实现,前端 worker 怎么开工?前端 mock 后端:按契约起 mock(如 msw 的 handler 返回假数据),前端照常开发。方向不可逆,理由一句话:谁依赖契约谁 mock 对方——前端依赖后端的 API 才需要解耦,后端开发本来就不需要前端存在。方向搞反的典型事故:后端偷懒参照前端随手写的假数据形状去实现,集成时才发现契约早已更新,前端返工两次。

契约变更要显式广播

改契约等于改需求。流程四步:提出变更申请(含理由与影响面)→ 主代理评估(哪些在途 worker 的工作会被作废)→ 批准后修改契约文件并提交 → 广播给所有相关 worker:在途 worker 停机、rebase 新契约、确认影响后继续。悄悄改契约比不改更危险——其余 worker 会带着旧契约继续写,错误要到集成时才放大。

20.4 集成节奏

小批次集成

反面教材:五个 worker 各跑两天,第五天一起合——冲突叠加 bug,每个 bug 都可能是任意两个分支的组合产物,定位是组合爆炸。正面做法:每完成一批任务(2–3 张卡)就合并进主干,跑全量测试,而不是全部做完再合。批次越小,每次合并的冲突搜索空间越小;主干始终是"最近一次确认能跑的状态",任何时刻都能从它重新出发。这与第 12 章"高频小提交 = 可回滚检查点"是同一个思想在分支维度的投影:确认没坏的时刻就是检查点,集成批次就是分支检查点。

集成负责人

每个里程碑指定一名集成负责人(integration lead),由主代理兼任或人工担任,职责四条:定合并顺序(按任务依赖图的拓扑序,第 14 章);每次合并后跑全量测试;跨分支冲突由它裁决(它是唯一同时看得到两份意图的人);集成期间冻结新批次派发(WIP 上限,第 16 章)。

代理版持续集成:每卡一 PR

把"每卡一分支一 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 的"随事故进化"在集成场景的样子。

20.5 人工检查点与成本治理

必须人审的节点清单

并行放大产能,也放大"代理自己拍板"的后果。五类节点必须人审:

节点 为什么必须人 典型动作
架构决策 方向性错误的代价 = 全部已写代码(第 15 章) 批准实现计划与 spec
新依赖引入 供应链风险 + 长期维护成本,装起来快卸起来疼 审依赖理由、维护状态、替代方案
合并主干 多分支改动在此汇流,是最后的拦截点 复核 diff + CI 绿 + 回传一致
发布 对外可见或不可逆 走发布检查单(附录 C)
删除或外发操作 rm -rf、git push --force、向外部服务发送数据 显式确认;权限上默认拒绝(第 11 章红线)

检查点清单化,写进 AGENTS.md

人审节点不写下来,就依赖"主代理记得"——主代理也是模型,也会忘。把清单写进 AGENTS.md 的"人工确认点"小节,代理会在到达节点时主动停下请求确认,而不是自作主张闯过去:

## 人工确认点(到达以下节点必须停下,等待人工批准)
- 引入任何新依赖:给出包名、理由、替代方案,确认后再装;
- 修改 shared/types/** 与 docs/spec/** 下任何契约文件;
- 合并 PR 到 main:CI 全绿 + diff 复核后请求人工批准;
- 执行删除类命令(rm -rf、git push --force);
- 发起任何对外网络请求:仅允许白名单域(SSRF 防护——
  防止代理被诱导向任意地址发数据或拉取恶意内容),
  白名单维护在权限配置的 allow/deny 规则中。

成本治理(cost governance)

多代理的 token 消耗是单代理的数倍,三个习惯把它管住:

  1. token 预算意识:每个里程碑开工前估预算(卡数 × 单卡均值),结束对账。单卡均值跑两个里程碑就有数了(参考量级:一张中等任务卡 10–20 万 token);
  2. 缓存与模型分级:AGENTS.md 与系统提示是 prompt cache 命中率最高的前缀,别频繁改动它们的开头;重活(架构设计、疑难调试)用强模型档位,机械活(批量迁移、格式同步、文档审计)用便宜档位——ZCode 里 subagent 可以按任务指定模型档位,这是一笔直接的汇率差;
  3. 定期复盘单位产出成本:每周或每里程碑算一次"每功能 token 成本 + 人工分钟数",看趋势不看绝对值。某类任务成本畸高,通常是提示词或拆分出了问题——先修方法,再谈预算。
措施 针对的浪费 做法
模型分级 机械活也用强模型 重活强档、批量活低档
缓存复用 重复前缀反复计费 稳定 AGENTS.md 开头,少改
合并派发 同类任务多次冷启动 同构切片合给一个 worker
预算对账 失控后才发现 里程碑开工估、结束对

固化成团队文档:协作章程

本章规范最终要落成一部《多代理协作章程》(team charter):所有权分区、共享文件规则、契约流程、集成节奏、检查点清单。它与 AGENTS.md 的分工要说清:AGENTS.md 管项目事实(技术栈、命令、约定),章程管协作流程(人与代理怎么分工、代理与代理怎么避让)。章程进团队 onboarding——新人(连同新人的代理)第一天就照着跑一遍;每次协同事故后追加一条规则并升版本号,让章程随项目进化。

贯穿项目实战:轻读 BookNest

把本章规范落成 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 不记账 月底用量三倍于预期,说不清花在哪个功能 里程碑预算 + 对账、模型分级、复盘单位成本

最佳实践

  1. 派发即分权:每张任务卡写清"只准改这些路径",提示词、hooks、回传核对三层兜底;
  2. 共享文件先分类:低频的串行、多需的代改、反复争抢的结构消共享;
  3. 契约先行三件套:API schema、事件名、表结构在派发前落盘进仓库;
  4. mock 方向恒定:前端 mock 后端,禁止反向绑架实现;
  5. 契约变更必须显式广播,在途 worker 停机对齐后再继续;
  6. 每卡一 PR + CI 门槛:worker 的"自验"永远要有独立证据链;
  7. 小批次集成(2–3 卡一合)并设集成负责人:定顺序、跑全量、裁冲突、冻结新派发;
  8. 五类节点必人审(架构、依赖、合并、发布、删除与外发),清单进 AGENTS.md 让代理到点主动停;
  9. 成本治理三件事:里程碑预算对账、模型分级、稳定前缀吃缓存,定期复盘单位产出成本;
  10. 章程进 onboarding、随事故进化:一条事故换一条规则,让治理文档越用越厚。

本章小结

多代理的三大事故源——文件冲突、约定漂移、集成失败——没有一个是"模型不够聪明"造成的,全是"规范不够明确"造成的。因此治理的本质不是管住代理,而是把规范提前写清楚:所有权协议回答"谁的文件谁才能改";接口先行回答"大家对齐什么";集成节奏回答"多久对一次账";人工检查点回答"哪些关口必须人来守";成本治理回答"这套编队到底划不划算"。规范与并行度成正比:规范越明确,放权越大;规范每含糊一分,并行就得收一分。

要点回顾:

  • 三大事故源各有对症药:所有权协议 / 契约先行 / 小批次集成,人工检查点兜底;
  • 范围是硬约束:提示词声明 + 权限拦截 + 回传核对,三层执行;
  • 契约落盘为单一事实源;mock 方向前端到后端;变更显式广播;
  • 每卡一 PR、CI 设门槛、按拓扑序合并;集成负责人专职把守,集成期冻结新派发;
  • 五类节点必人审,清单写进 AGENTS.md,代理到点主动请求确认;
  • 成本治理:预算对账、模型分级、缓存复用;沉淀为团队协作章程并进 onboarding。

动手练习

  1. 事故分诊:下面三个现象各属于哪类事故源?给出对应的治理手段:(a) Worker A 的迁移脚本被 Worker B 的同名文件覆盖;(b) 前端页面请求 /notes,后端注册的是 /api/v1/note;(c) 两个分支各自 Vitest 全绿,合并后路由 404。
  2. 所有权表:为 M3(FR-4 统计 + FR-5 导入导出)设计所有权分区表与共享文件清单,标出你认为最容易冲突的文件及处置规则(串行 / 代改 / 消共享三选一,说理由)。
  3. 契约练习:为 FR-7 速记写 shared/types/fragments.ts 契约(Fragment 类型 + 两个接口形状),并写出前端 worker 的 mock 要点:基于契约返回哪些假数据、必须覆盖哪些边界(空列表、未归入、已归入)。
  4. 综合实践:把本章《多代理协作章程》落盘到你的 BookNest(或你自己的项目)docs/team-agents.md,然后真实跑一次双 worker 并行(任选两张互不依赖的卡);发生的任何冲突都按章程处置,并把处置写进"事故记录"。收官时对照:章程比开工时厚了几条?

导航

📄 第 21 章 · 综合实战一:从零构建「轻读 BookNest」 21-capstone-greenfield.md

前 20 章是弹药库:配置(Part 2)、Skill(Part 3)、边界(Part 4)、任务工程(Part 5)、多代理(Part 6)。本章把这些单件武器按一条真实时间线串成一场完整战役——从 git init 到 FR-1~FR-7 全部验收。这是叙事章:不再展开方法论,只在每个阶段标注"用了哪一章、产出了什么、哪里翻了车"。强烈建议边读边在自己的仓库里复刻——本章的顺序,就是你的操作顺序。

学习目标

读完本章,你应该能:

  • 能按开工检查单完成项目接入:工具安装、模型档位、Git 仓库、边界档位、目录骨架;
  • 能按"配置先行"的顺序落地 M0 全部产物,并说清每个产物出自哪一章、如何验收;
  • 能完成从 spec(第 15 章)到任务卡(第 14 章)的规格与拆分全流程;
  • 能以"领卡 → 实现 → 自验 → 提交"的固定节奏用单代理完成一个里程碑;
  • 能在多代理阶段应用接口先行与集成节奏(第 17、18、20 章),并按章程处置真实冲突;
  • 能完成收口三件套(workflow 审计、e2e、发布检查单),并把经验晋升回 AGENTS.md 与 Skill 库。

21.1 阶段零:开工准备(第 0 天)

打地基的事不值得放权——这一阶段由人主导,代理最多当参谋。过一遍附录 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、读取生产密钥。

当晚产出:一个空仓库 + 两份"宪法"雏形。半小时,全程人工——但这半小时是全书回报率最高的半小时,之后的每个代理会话都在吃它的红利。

21.2 阶段一:配置先行(M0,第 1 晚)

原则:**代理干活之前,先让代理"开机即懂、越权即拦"。**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 如期爆雷——请先记住这里。

21.3 阶段二:规格与拆分(M1,第 2 晚)

顺序不可换:先 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 章的反指标:"拆分边界还没想清楚就上多代理,等于把混乱并行化。"

21.4 阶段三:单代理实现 M1(第 3–5 晚)

开工:计划模式先探后改

每个执行会话的第一句话固定不变:

你:先别写代码。读 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 拦住了方向错误,任务卡拦住了范围蔓延,可执行验收拦住了"自说自话的完成"。

21.5 阶段四:多代理实现 M2(第 6–8 晚)

M2 范围:FR-2 读书笔记 + FR-7 快速记录 + 标签完善。两个功能域的文件互不重叠——主从编排(第 17、18 章)的教科书场景,治理规则照第 20 章的章程执行。

接口先行(第 20 章章程生效)

派发之前先落契约:

  • 迁移 0007_notes.sql、0008_fragments.sql(表结构即契约,编号按章程向主代理申请顺延);
  • shared/types/notes.ts、shared/types/fragments.ts(API 形状,变更需主代理批准并广播);
  • mock 方向:Worker B(速记)的"归入笔记"依赖 Worker A 的接口 → Worker B 用 msw 按契约 mock,不被 Worker A 的进度卡脖子。

派发两 worker

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 分钟,但有两点比处置更值得看:

  1. M0 欠的账在此爆雷:如果 21.2 的 PreToolUse 越界写入拦截当时配齐,Worker B 对 app.ts 的写入会在工具层被直接拦下,冲突根本不会发生。当晚补齐 hooks 三件套,此后 M2/M3 零越界事故——治理欠账是会连本带利偿还的;
  2. 治理闭环:事故 → 处置 → 章程加一条 → 广播。下一次派发自动受益,这正是第 20 章"一条事故换一条规则"的落地。

集成收尾

每卡一 PR、CI 绿才合、按依赖图拓扑序合并;两域全部合入后,主代理跑全量测试,并手动过一遍"速记 → 归入某本书 → 笔记时间线可见"的端到端路径——两域的交界处正是集成风险点(第 14 章读图口诀:汇点卡验收最严)。M2 收口,FR-2 / FR-7 可演示。

21.6 阶段五:收口(M3/M4,第 9–10 晚)

M3(FR-4 统计 + FR-5 导入导出)的实现第 17 章已完整推演过(主从编排 + 评审者收尾),此处只记录口动作:

  1. workflow 文档审计(第 19 章):跑一次三代理流水线——spec 与实现一致性检查、AGENTS.md 时效检查、任务卡核销检查。产出:两处 spec 过期(FR-6 的搜索范围已随 M2 扩展到笔记内容)、一张任务卡未核销,全部修正归档;
  2. workflow 回归:全量 Vitest + Playwright 冒烟;利用第 19 章的缓存复用,第二次跑省下大半 token;
  3. e2e 补齐:FR-1~FR-7 每条主路径一条 Playwright 用例,e2e/specs/ 一域一文件(所有权协议延伸到测试目录);
  4. 发布检查单:过附录 C 的「发布前清单」——NFR-3 实测(1 万条列表 187ms,达标)、npm run build && npm start 部署演练、封面图外链仅请求白名单域(SSRF 防护:运行期出网限制在 allow 清单内)、依赖清单复核无弃维护包;
  5. 经验晋升(第 5、10 章):
    • AGENTS.md 追加三条实战规则:路由挂载由主代理统一执行;迁移编号按卡顺延申请;Markdown 渲染一律过消毒函数;
    • 把"BookNest 新模块脚手架"沉淀成项目自有 skill booknest-module(SKILL.md:从迁移、服务、路由到页面、测试的固定五步),下一个新模块直接触发复用——这就是第 10 章"调试 → 提炼 → 进化"的晋升闭环。

21.7 复盘(第 11 晚,一小时)

时间线回顾表

阶段 耗时 关键产出 翻车点
零:开工准备 半晚 空仓库、边界矩阵、目录骨架 无(全程人工)
一:配置先行 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 个晚上(业余时间)。纯编码时间不足一半——另一半花在"看起来不产代码"的配置、规格与治理上,而这正是它们被低估的原因。

"纪律换速度"的三条体会

  1. 配置先行的一晚,是全书回报率最高的一笔投资:M0 之后的所有会话,代理零次问"怎么跑测试"、零次写出自创风格的代码。折算下来每张卡省 5–10 分钟重复解释,11 晚下来就是一整晚;
  2. 小批次集成把最大事故压到了 20 分钟:app.ts 冲突若攒到 M2 结束才发现,两个域的改动混作一团,定位以小时计。每卡一 PR 让它只在"刚刚改过的地方"爆炸,定位成本近似于零;
  3. 人审检查点拦下的不是代码,是方向:M1 两次假设批注、M3 一次 spec 过期审计,任何一次漏网都是半天起步的返工。分工从来是——代理负责把事情做完,人负责确认事情做对。

如果重来会改什么

  • M0 当晚就配齐 hooks 三件套——本项目最贵的一次偷懒,代价是一次真实冲突加半小时排查;
  • M1 就开双 worker 练手:收益不大(M1 本可单代理消化),但能在低风险期把章程磨合顺;
  • spec 再克制一点:FR-5 有一页写成了实现方案("先校验再分块再事务"),后来整段删掉——第 15 章"spec 别写实现细节"诚不我欺;
  • "归入笔记"接口第一天就进契约,而不是 Worker B 开工前夜——速记模块的 mock 因此白写了两天。

贯穿项目实战:轻读 BookNest

本章本身就是贯穿项目的总集成。最终仓库树(对照附录 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 库
复盘只记成绩 时间线回顾表只剩"顺利完成" 翻车点是复盘的核心资产:一条翻车换一条规则

最佳实践

  1. 开工准备全部人工完成:地基不值得放权,半小时换此后每个会话的红利;
  2. M0 产物照清单逐项验收,验收不过不进入业务开发——包括 hooks,一次都别欠;
  3. spec 与任务卡先于代码落盘,"明确不做的事"不许空着;
  4. 单代理阶段固定"领卡 → 实现 → 自验 → 提交"四拍,验收逐条自报、人工抽查;
  5. 一卡一提交,Conventional Commits 带卡号,让回滚粒度等于卡粒度;
  6. 多代理派发前先落契约与所有权分区,worker 提示词四要素一个不少;
  7. 每卡一 PR、CI 设门槛、按拓扑序合并,集成期冻结新派发;
  8. 收口三件套——workflow 审计、全量回归、发布检查单——缺一不发;
  9. 复盘后 48 小时内完成经验晋升:规则进 AGENTS.md,流程进 Skill 库;
  10. 时间线回顾表必须保留"翻车点"列,它是下一部章程与下一篇复盘的素材库。

本章小结

本章把前 20 章的方法按真实时间线走了一遍:阶段零人工打地基(检查单、边界矩阵、目录骨架);M0 配置先行,让代理开机即懂、越权即拦;M1 先 spec 后拆卡,再以"领卡、实现、自验、提交"的四拍单代理逐卡消化;M2 用主从编排双 worker 并行,接口先行解耦进度,一次真实冲突检验了章程并推动它升级;M3/M4 用 workflow 审计、e2e 与发布检查单收口,最后把经验晋升回 AGENTS.md 与 Skill 库。11 个晚上、7 个功能、一次被章程化解的冲突、一次因偷懒放大的事故——结论只有一句:vibe coding 的速度不是来自少做规矩,而是来自把规矩做在前面。

要点回顾:

  • 五阶段节奏:准备(人工)→ 配置先行(M0)→ 规格与拆分(M1)→ 单代理实现 → 多代理实现 → 收口与复盘;
  • 每阶段有硬验收:M0 三连、M1 九卡全绿、M2 契约先行加冲突闭环、M4 发布检查单;
  • 章程随事故进化:一条事故换一条规则;hooks 缺位是本项目最贵的一次学费;
  • 终态资产:可运行的 BookNest、七份 spec、任务卡库、协作章程、项目自有 skill;
  • 复盘的价值在翻车点:它们是下一轮"纪律换速度"的原料。

动手练习

  1. 复刻阶段零与 M0:在你的仓库里按 21.1、21.2 的清单真实走一遍(AGENTS.md、权限 allowlist、至少一条 hook)。验收标准:新开会话让代理"说出本项目怎么跑测试",零追问通过。
  2. 单卡全流程:按第 14 章模板把 M1-08「搜索框与结果页」写成完整任务卡,然后用计划模式开工、按"四拍"完成它,并记录:批注了几条假设?哪条假设如果漏网会返工多久?
  3. 冲突演练:故意制造一次共享文件冲突(两个会话同时修改同一文件并尝试合并),按第 20 章章程处置,把全过程写进你的"事故记录",并回答:这条事故应该换来章程里的哪一条新规则?
  4. 综合实践:完整跑完一个里程碑(M1,或你自己的等价物),产出三件套:时间线回顾表(含翻车点列)、经验晋升 diff(AGENTS.md / skill 的实际改动内容)、以及 200 字复盘——"纪律换速度"在你的项目里的具体数字(省下的返工时长 vs 花在规范上的时长)。

导航

📄 第 22 章 · 综合实战二:用 AI 代理重构遗留项目 22-capstone-legacy.md

第 21 章结尾留了个伏笔:如果第 21 章的全部纪律都缺席,BookNest 三年后会长成什么样?答案是本章的主角 booknest-legacy。接手遗留项目(legacy project)是比 greenfield 更接近日常工作的场景——你无法选择从零开始,只能在别人的地基上施工。本章给出一套四阶段打法:只读勘探 → 安全网先行 → 渐进重构 → 验证与切换。仍用叙事章写法:每个阶段标注用了哪一章、产出了什么、哪里最容易翻车。

学习目标

读完本章,你应该能:

  • 能识别遗留代码库的典型病灶,并复述"先看后动"的接手纪律;
  • 能用只读/计划模式与探索子代理(第 18 章)完成代码库测绘,产出架构文档与风险清单;
  • 能用特征测试(characterization test)为存量行为建立安全网,并以最小代价引入工程化基线;
  • 能按绞杀者模式(strangler fig pattern)制定渐进迁移路线,用小提交保证每一步可回滚;
  • 能组织"实现者-评审者"双代理协作验证重构零行为变化,并完成影子对比与切换;
  • 能为遗留项目划出四条专属边界纪律,并说清每条纪律背后的代价。

22.1 场景设定与心法:接手 booknest-legacy

场景:一个平行宇宙的 BookNest

设定如下:某个团队三年前也想做读书笔记工具,但没有经历第 21 章的任何纪律——没有 AGENTS.md、没有 spec、没有任务卡、没有测试。一个后端在三天里"vibe"出了全部功能,之后三年边修边补,所有代码堆在一个文件里。今天这个活儿交到你手上,附赠一句管理层名言:"功能都是好的,就是改不动。"

它的典型症状,做过遗留维护的人都会会心一笑:

  • 上帝文件(god file):app.js 一个文件 3000 行,路由、SQL、业务逻辑、工具函数全在里面;
  • 魔法数字(magic number):if (retry < 43)——43 是什么?没人知道,原作者也忘了;
  • 复制粘贴函数:getBookList、getBookList2、getBookList3 三胞胎,各改了一半;
  • 硬编码路径:备份目录写死在 /home/legacy/backup,换台机器就静默失败;
  • 藏着的高危写法:手写 SQL 拼接用户输入、按 URL 参数任意出网拉图、某个函数里还躺着一串疑似 API 密钥的字面量。

巨石节选(感受一下你要面对的东西):

// 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);

接手陌生代码的心法

三条心法,比任何工具都重要:

  1. 先看后动。接手第一周的唯一目标不是"改点什么",而是搞清楚"它现在是什么"。你在第 21 章 M1 做过的事反过来做:先勘探、后规划、再动手。
  2. 勘探优先于修改。没有测试保护的代码,任何修改的影响面都是未知的。改动本身就是对勘探现场的一次污染——你看到的将不再是原貌。
  3. 遗留代码的怪行为多半是隐性需求。那个"看起来像 bug"的排序规则,可能支撑着某人的月度报表。所以重构的第一原则是不改变行为——修 bug 是带验收的独立任务,不是重构的顺手之事。

一句行业名言送给你,也送给你的代理:"遗留代码就是没有测试的代码。"(Michael Feathers,《Working Effectively with Legacy Code》)

22.2 阶段一:只读勘探——先画地图,再动第一刀

只读勘探的三种姿势

ZCode 提供三种"只看不动手"的方式,按需组合:

  1. 计划模式(plan mode):Shift+Tab 切入,代理只读探索、产出计划、等批准。适合小范围勘探(单个模块);
  2. 只读权限会话:把权限档位设为只读,代理可自由读仓库,写操作被工具层拒绝。适合全库扫描;
  3. 探索子代理(第 18 章):派发自包含的只读子代理并行勘探,各管一个维度,回传结构化报告。适合大库与多维度。

booknest-legacy 选了第三种:三个只读子代理并行,分别负责路由面、数据面、依赖面,提示词四要素齐全(第 18 章),且都显式写入一条铁律:

【约束】本次为只读勘探:禁止创建、修改、删除任何文件;
禁止执行任何写入或出网命令;所有发现以报告形式回传。

勘探的五个维度

维度 要回答的问题 本项目产出
入口点 程序从哪启动?有哪些进程/定时任务? 启动链路:仅 node app.js
路由清单 对外暴露了哪些接口?各自干什么? 路由表 20 条(books/notes/tags/stats/backup/import)
数据访问 表结构?SQL 在哪?有无拼接与注入风险? 23 处手写 SQL,5 处拼接用户输入
外部依赖 出网到哪?装了哪些包?有无弃维护包? 1 处任意出网(cover 拉图);34 个依赖,2 个弃维护
雷区 哪些地方改了会炸?密钥/路径/危险写法在哪? 风险清单 5 条,按危害排序(见贯穿实战原件一)

两条纪律

  1. 勘探阶段禁止边读边改——发现"顺手就能修"的问题,唯一正确的动作是记进风险清单。原因在 22.1 说过:改动污染现场,而且此刻你没有任何安全网,改错了都不知道;
  2. 报告落盘进仓库:docs/ARCHITECTURE.md + docs/LEGACY-RISKS.md。勘探成果属于仓库而不属于某个会话——三个月后下一个接手的人,以及之后每一个代理会话,都靠这两份文件"开机即懂"(第 4 章思想在遗留场景的延伸)。

报告节选见本章贯穿项目实战。

22.3 阶段二:安全网先行——给旧行为拍快照

特征测试:快照,而非理想

特征测试(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    # 此后任何一步都能一键回到起点

从此,"行为变了没有"不再是感觉问题,而是测试问题。

22.4 阶段三:渐进重构——绞杀者模式

绞杀榕如何在代码库里生长

绞杀者模式(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 行,仅启动与挂载)

三个要点:

  1. 新旧共存于一个进程:迁移期间 app.js 通过 require 挂载新模块——不存在"第二套系统",也就不存在数据分叉与双份维护;
  2. 按域推进,不按层推进:一次迁一个路由域(books → tags → notes → …),每个域连同它的 service、SQL 一起走。这正是第 14 章"垂直切片"在重构场景的镜像——水平切片的集成灾难在这里同样成立;
  3. 迁移时只做"机械等价变换":SQL 拼接改参数化查询(parameterized query)、魔法数字提常量、复制粘贴函数合并——这些有唯一正确答案的变换可以随迁;任何带判断的行为变更都另开任务。

出网类的等价变换要格外当心:巨石里 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 章的评审者-实现者模式,但给它一个遗留场景特有的收窄:评审者不评风格,只做两件事——

  1. 跑全量测试(特征测试 + 已迁移模块的迁移用例);
  2. 对照 diff:确认改动只发生在任务卡声明的文件里,迁出代码与被删代码逐行同构。

风格意见在重构评审里是噪音——行为不变才是唯一标准。完整实录见贯穿项目实战。

22.5 阶段四:验证与切换——影子运行、切换与回滚

四步收口

  1. 全量回归:特征测试 + 迁移用例 + lint + tsc --noEmit 全绿。这是切换的唯一准入;
  2. 影子运行(shadow run)对比:旧服务(3001)与新服务(3000)并行,把固定请求集(含空值、超长、特殊字符等边界输入)同时打到两个端口,对响应做 JSON 深比较,差异必须为零。只比状态码是不够的——字段级差异要在切换前抓完;
  3. 切换与观察:入口切换做成一行配置(STARTER=legacy|modular),切到新入口后观察 48 小时:错误日志、5xx 率、慢查询。观察期旧入口保持可用;
  4. 回滚预案:回滚 = 改一行环境变量。预案必须演练过一次才算存在——"应该能回滚"和"验证过能回滚"是两回事。
// 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 更新为迁移后状态,已消除的雷区逐条销项归档;
  • AGENTS.md 重写:新目录结构、新约定(路由不进入口文件、SQL 一律参数化、出网一律白名单),以及最重要的新禁区——"禁止向 app.js 添加任何路由";
  • 巨石退役:剩余启动代码更名 server.js,保留 legacy-baseline tag 归档历史。

22.6 遗留项目专属边界纪律

四条纪律,每条背后都是真实代价:

  1. 禁止大爆炸重写(big bang rewrite),除非体量很小(经验值:500 行以内)。重写丢掉的不是代码,是隐性需求——那些没有文档、却支撑着生产的怪行为。绞杀者模式慢,但它让每个怪行为在迁移时被"看见"一次;重写让它们集体消失,然后在生产环境里还魂;
  2. 禁止顺手改行为:重构提交只搬移(refactor:),行为变更另开任务(fix: / feat:)。混在一个提交里的搬移与修改,会让 diff 对照失效、回滚粒度报废、特征测试失去意义;
  3. 发现疑似密钥:先轮换,再清理。顺序不可颠倒——删除字面量不能撤销泄露,密钥可能早已进入 git 历史与日志。正确顺序:确认影响面 → 轮换密钥 → 清理代码 → 必要时清理历史。第 13 章的凭据扫描 hook 应该在勘探期就上岗;
  4. 跑 legacy 代码用隔离环境(呼应第 12 章)。未知代码可能写文件、出网、起子进程——首次运行一律放进容器或一次性环境,出网默认拒绝(白名单域例外),文件系统挂载最小化。别让"跑一下看看"变成事故起点。

贯穿项目实战:轻读 BookNest

本章整体即 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',
    ]);
  });
});

原件三:一次绞杀迁移全过程(RL-03:迁出 books 路由)

任务卡(沿用第 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 深比较并覆盖边界输入,零差异才允许切换

最佳实践

  1. 接手第一周只勘探不改码:ARCHITECTURE.md 与风险清单是第一周的唯一交付物;
  2. 勘探交给只读子代理并行做,人工只做抽查与合并:重点核对路由清单与雷区清单的完整性;
  3. 特征测试按风险排序:先锁导入导出、备份、统计、删除等"钱与数据"路径,再锁普通查询;
  4. 工程化工具先"只读体检":tsc --noEmit、不带 --fix 的 ESLint,任何自动改写都延后到迁移完成;
  5. 基线提交打 tag:legacy-baseline 之后,任何一步都能一键回到起点;
  6. 绞杀迁移按域推进:一次一个路由域,"迁出 → 全量测试 → 提交"三拍,绝不跨域并刀;
  7. 评审者子代理只跑测试与 diff 对照,不发表风格意见——重构评审的唯一标准是行为不变;
  8. 影子对比的请求集覆盖边界输入:空值、超长、特殊字符、越权 ID,用真实流量的脱敏快照最好;
  9. 回滚预案演练过才算存在:切换前真实执行一次回滚,再执行切换;
  10. 收尾必须晋升:AGENTS.md 写入新结构与新禁区("禁止向入口文件加路由"),雷区逐条销项归档。

本章小结

本章走完了接手遗留项目的完整四阶段:只读勘探先画地图(架构文档 + 风险清单,勘探期禁改);安全网先行用特征测试锁住存量行为,以"只报不修"的最小工程化建立基线提交;渐进重构按绞杀者模式逐域生长新模块、瘦身巨石,用评审者-实现者双代理守住"零行为变化";最后以全量回归、影子对比、一行回滚开关完成验证与切换,并把勘探成果晋升为新的项目约定。加上四条专属纪律——禁大爆炸、禁顺手改行为、密钥先轮换、隔离环境——你会发现:重构遗留项目和从零构建共享同一套底层纪律,只是把"防犯错"换成了"防惊吓"。

要点回顾:

  • 四阶段节奏:只读勘探 → 安全网先行 → 渐进重构(绞杀者)→ 验证与切换;
  • "勘探禁止边读边改"的两个根因:改动污染现场 + 无安全网的修改影响面未知;
  • 特征测试断言现状而非理想,错的行为也先锁住;修 bug 是带验收的独立任务;
  • 迁移期只做机械等价变换(参数化、提常量、去重),一切行为变更另开任务;
  • 三周零事故的底气:小提交可回滚、评审者只对照 diff、影子对比零差异。

动手练习

  1. 勘探演练:挑一个你自己的旧项目(或开源小项目),用只读模式或派一个只读子代理产出 ARCHITECTURE.md 节选与雷区清单,全程禁止写入代码。验收:报告能回答"怎么跑起来、有哪些对外接口、哪里最危险"三问。
  2. 特征测试:为项目里一个无测试模块写 3 条特征测试,其中至少 1 条锁定一个"看起来不对但一直如此"的行为,并说明你为什么先锁住它而不是修掉它。
  3. 绞杀单域迁移:把一个路由(或等价职责单元)迁出巨石文件,完整走一遍"任务卡 → 实现 → 评审者对照 → 提交",记录 diff 统计(净删/净增行数)与评审者抓到的问题(如果有)。
  4. 综合实践:完成一次完整的四阶段重构(或在 booknest-legacy 上复刻本章全程),产出四件套:迁移前后对照表(行数、测试数、依赖数)、一份影子对比差异报告(应为零差异)、更新后的 AGENTS.md diff,以及 200 字复盘——你的"勘探、安全网、小提交"三项纪律各自拦下了什么。

导航

📄 第 23 章 · 反模式与踩坑大全 23-antipatterns.md

前 22 章告诉你"该怎么做",本章告诉你"不这样做会发生什么"。30 条反模式(antipattern)按六类组织——上下文、配置、边界、任务、多代理、流程——全部来自真实 AI 协作场景的高频失败模式,每条标注详细出处,需要展开时跳回对应章节。读法建议:不要一次通读。把它当"事后字典":每次翻车后用 23.1 的症状速查表反查条目,比事前背诵有效十倍——坑这种东西,没踩过的时候读多少遍都是纸上谈兵,踩过之后再来看,每一条都会让你后背一凉。

学习目标

读完本章,你应该能:

  • 能用症状速查表把 AI 协作中的异常现象反查到具体反模式条目;
  • 能复述六类反模式(上下文/配置/边界/任务/多代理/流程)各自的共同根因与"第一解法";
  • 能按"机制 > 提示词 > 口头约定"的优先级为具体反模式选择落点;
  • 能针对一条反模式完成"反查 → 定根因 → 机制化 → 验证"的完整纠正闭环;
  • 能把一次真实翻车沉淀为一条团队规则,让同一个坑只踩一次;
  • 能用 BookNest 三坑的复盘框架(见贯穿项目实战)复盘自己的项目并统计"坑型分布"。

23.1 症状速查表

先看现象,再查病根。左列是你在 AI 协作中大概率亲眼见过的场景——如果你一条都没见过,恭喜,也请留着这张表:AI 协作的坑不以人的意志为转移,只以会话数为单位发生。

你看到的现象 大概率是 条目
"帮我做个搜索",产出能用但完全不是你要的 需求信息量为零 A1
会话越聊越"失忆",早先的约定被悄悄违背 上下文超限还在硬撑 A3
每个新会话都要重新解释一遍项目背景 约定没落进项目配置 A5 / B1
代理改了没让它改的文件,甚至删了东西 无 deny 底线 + prompt 当边界 C2 / C4
代码里出现密钥字面量或内网地址 凭据管理失守 B4
大功能一次交付,一半是想象出来的 任务没拆就硬吞 D1
两个并行代理互相覆盖,合并冲突一片 所有权没分区 E3
子代理自称"完成",一验收全是坑 派发不自包含 / 结果没验收 E2 / E4
合并后测试变红、页面 404 没读 diff / 测试后置 F1 / F2
长任务跑一小时不停,token 账单起飞 无预算上限与停机条件 E5

使用本表的四个提醒:

  1. 速查表只负责"入围":同一种症状可能对应多个根因("产出不符预期"既可能是 A1 也可能是 D3),入围后回到对应类别细读,不要停在第一印象;
  2. 反查的目的是找自己的缺口:六类问题里,A/B/D 三类的根因几乎都在输入侧与机制侧,先别急着怪模型;
  3. 反查产出必须落到纸面:现象、条目、换来的规则,三行即可——只查不记,同一坑必然复发;
  4. 未遂事故也走这张表:被 hook、批注或评审者拦下的那次,同样反查条目——它验证的是"机制在岗"。

十条现象背后是六类病根,先给一张总览地图,按需跳读:

类别 条数 共同根因 第一落点 主战场
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 章

"主战场"列不是让你现在去重读——它是反查后的入口:确认条目之后,跳到对应章节把该章"最佳实践"重读一遍,比整章重读省一半时间。

23.2 上下文与配置类反模式(A / B 类)

这两类放在一起讲,因为它们是同一场事故的两个阶段:上下文(A 类)决定这一次会话的质量,配置(B 类)决定每一次会话的起点——A 类坑每次踩都是新的,B 类坑踩一次会反复收租。

顺带说清"上下文"与"配置"的分工:上下文是你这一次给代理的东西(提示、报错、文件),配置是仓库里预先替你给代理的东西(AGENTS.md、规则、hooks)。前者的坑靠临场纪律修,后者的坑靠落盘与审计修——两边的解法不能互相替代。

A 类 · 上下文(6 条)

上下文工程(第 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 类问题的根因几乎都在输入侧。

B 类 · 配置(5 条)

配置(第 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 是趋势,不审计就一定腐烂。

心法:配置文件会撒谎,而且专挑新会话的第一分钟撒——定期审计是唯一的解药。

23.3 边界与任务类反模式(C / D 类)

C 类是"代理能做什么没管住",D 类是"代理要做什么没说清"——前者管权限的下限,后者管指令的质量,一个保安全、一个保效率,都缺不得。

C 类 · 边界(5 条)

边界设计(第 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 恳求当安全边界:它最像"做了安全工作"——提示词写上了、心里踏实了,实际拦截力为零。安全感的错觉,比没有防护更糟。

心法:边界的强度以"最疲惫时刻的你"为准——无脑回车的你,就是边界要防的那个人。

D 类 · 任务(5 条)

任务工程(第 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)。

心法:切得越垂直越好验,切得越原子越好滚——任务卡的颗粒度决定返工的粒度。

23.4 多代理与流程类反模式(E / F 类)

E 类是"一支代理团队没带好",F 类是"收尾关口没守住"——E 类问题发生在派发与并行期,F 类问题发生在集成与交付期,恰好是协作生命周期的前后两半。

E 类 · 多代理(5 条)

多代理(第 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 并行代理改同一文件:它是唯一让"并行收益变负"的条目——冲突排查与返工的时间,通常超过并行省下的时间。

心法:派发之前先问一句"这个子代理能只靠自己把事做完吗"——答案是否,就先补上下文再派。

F 类 · 流程(4 条)

流程(第 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 与产品之间隔着的不是功能,而是那套"看起来不产代码"的流程——省掉它,就是省掉产品。

23.5 从反查到落地:把坑变成机制

清单的价值不在"知道",在"落地"。一条反模式从被认领到被消灭,固定四步:

  1. 反查:用 23.1 定位候选条目(可能两三个),回到类别表确认症状与根因都吻合;
  2. 定根因:症状相同、根因可能不同。用最朴素的时间线还原——"什么时候发现的?之前发生了什么?哪一环本该拦住它?";
  3. 机制化:按"机制 > 提示词 > 口头约定"选落点,产出物三选一——hook、权限规则、章程/检查清单条目;
  4. 验证与回访:48 小时内构造一次违规,确认真的拦得住;两周后回访是否复发。

完整走一遍的例子——现象:"依赖清单里多了三个没见过的 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 任务 → 零冲突,挂载变更以申请单形式回传。
回访:此后两个里程碑零合并冲突。

三个例子形状相同:都把"人的自觉"换成了"模板与拦截"。给团队负责人的落地节奏建议:

  • 第一周:C 类三条红线 deny + 凭据扫描 hook——事故成本最高的一类先封顶;
  • 第二周:A 类规范进 AGENTS.md——提示模板、报错模板、文件引用规范;
  • 第三周:D 类任务卡模板与提交纪律入章程,同时启动 B5 的双周文档审计;
  • 之后:每次翻车按四步闭环走,规则库自然长厚,清单越用越薄。

贯穿项目实战:轻读 BookNest

前两章的实战里埋着几个真实的翻车点。本节把本教程作者在 BookNest 全程踩过的三个坑完整摊开——叙事化还原现场,再反查到本章条目。复盘有个反直觉的原则:未遂事故与真实事故同样值得记录——被拦下的那一次证明机制在岗,没被拦下的那一次证明机制缺位。BookNest 全程恰好两类都有。先剧透结论:三个坑,没有一个能怪罪于"模型不够聪明"。

坑一:最贵的一次偷懒——hooks 只配了一半

现场还原(第 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 章"如果重来会改什么"清单上的第一条。教训一句话:治理欠账会连本带利地还,越晚还利息越高。

坑二:白写两天的 mock——契约晚到

现场还原(第 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 一天都不用白写。教训一句话:并行开发的成本不在并行本身,而在被并行放大的信息缺口。

坑三:一页写歪的 spec——从"写过头"到"没跟上"

现场还原(第 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 与章程

最佳实践

  1. 把症状速查表贴进团队 wiki:异常现象先反查、再开新会话,避免带着误诊硬跑;
  2. 每次翻车写三行事故记录:现象、反查到的条目、换来的规则——第 20 章"一条事故换一条规则"的最小实现;
  3. 解法永远按"机制 > 提示词 > 口头约定"排序:能变成 hook 的不要写进章程,能写进章程的不要靠"下次注意";
  4. 每条新规则落地 48 小时内验证:构造一次违规,确认真的拦得住(第 13 章的 hooks 测试法);
  5. 每两周重读一次自己的 AGENTS.md:B5 漂移是慢性病,定期审计是唯一的处方;
  6. 多代理派发前默念 E 类五条:必要性、自包含、所有权、验收、预算,缺一不派;
  7. 预算上限是默认配置:长会话与子代理一律先设 token / 时间上限,再谈产出;
  8. 发布检查单里写死 demo 与产品的分界线:错误处理、测试、密钥管理、回滚方案,缺一不交;
  9. 按项目裁剪本章条目进附录 C 的四张检查清单,让"防坑"发生在事前而不是事后;
  10. 每季度做一次"坑型分布"统计:你翻车最集中的类别,就是你下一轮该补机制的方向。

本章小结

本章用 30 条反模式把前 22 章的正面纪律从失败视角重述了一遍:A 类上下文问题根因在输入侧,解法是喂足信息、少贴大文件;B 类配置问题的根因是快照失真,解法是落盘分层加定期审计;C 类边界问题的根因是约束悬空,解法是下沉到权限、hook 与沙箱;D 类任务问题的根因是切分不当,解法是垂直切片加可执行验收;E 类多代理问题的根因是治理缺位,解法是自包含派发、所有权分区与预算闸门;F 类流程问题的根因是验证外包,解法是把人审与测试放回硬门槛。贯穿项目里作者的三个真实坑(hooks 欠账、契约晚到、spec 失真)进一步印证:翻车极少因为模型笨,几乎都是机制缺——反模式清单的终点不是"知道",而是把每个坑变成一道拦截、一份契约或一次审计。

要点回顾:

  • 用法是"事后字典":症状速查表反查,比事前通读有效;
  • 六类根因速记:输入不足、快照失真、约束悬空、切分不当、治理缺位、验证外包;
  • 解法优先级恒定:机制 > 提示词 > 口头约定;
  • 纠正闭环四步:反查 → 定根因 → 机制化 → 验证与回访;
  • 每类记住"最危险的一条":A3、B5、C4、D1、E3、F4——优先给它们上机制;
  • 未遂事故与真实事故同样入账:被拦住的证明机制在岗,没拦住的证明机制缺位;
  • BookNest 三坑全部指向机制缺口——一道拦截、一份契约、一次审计,缺什么补什么;
  • 读完本章的下一步不是开始读附录,而是回到你上一次翻车的会话,把三行事故记录写下来。

动手练习

  1. 反查练习:回忆你最近一次不愉快的 AI 协作会话,用 23.1 的速查表定位反模式条目,写出根因分析与一条纠正动作,并标注它属于"机制 / 提示词 / 口头约定"哪一层。
  2. 机制化练习:从 B 类或 C 类挑一条你最容易踩的坑,把它做成一个 hook 或一条权限规则,并按第 13 章的方法构造"应拦 / 应放"用例各一条验证有效。
  3. 速查表本地化:基于你自己的项目补充 3 条本章没有的"症状 → 条目 → 解法",沿用本章表格格式,并为每条新条目标注对应正文章节(或注明"本书未覆盖,需自建规则")。
  4. 综合实践:完整复盘你跟随本教程(或自己的一个项目)的全过程:翻出全部翻车点与未遂事故,逐条归入 A–F 类并统计"坑型分布";选出占比最高的两类,各产出一条可执行的团队规则(规则文件、章程条目或 hook 任一形式)并落地;两周后回访是否复发,把结论追加进你的事故记录。

导航

📄 附录 A · 术语表 A-glossary.md

全书 55 个核心术语的分组速查表,按"模型与上下文 → 工具与协议 → 工程机制 → 多代理协同 → 安全与治理"五个主题排列。每个术语给一句话解释与首次系统讲解的章节;正文各章技术术语首次出现处均括注英文原词,本表同时充当中英对照索引。

A.1 本表的用法

  • 选词标准:只收"全书多处复用,或被某章系统性讲解"的术语;仅出现一次的名词不收;
  • 章节引用:"详见章节"指该术语被系统性讲解的位置,不是唯一出处——多数术语会在后续章节反复使用;
  • 检索建议:先按主题定位分组(A.3–A.7),再用编辑器全文搜索直达术语行;拿不准两个近义术语的区别时,直接翻 A.8 的辨析;
  • 推荐顺序:概念地图(A.2)→ 分组表(A.3–A.7)→ 辨析(A.8)→ 缩写表(A.11),四步定位不超过一分钟。
  • 约定说明:解释中的"代理"均指 AI 编码代理;涉及 ZCode 特定配置路径时以 ZCode 为准,概念与 Claude Code 等主流工具互通;
  • 与附录 C 的配合:检查清单大量引用本表术语,动工前把清单过一遍,等于顺手验收了对术语的理解;
  • 口径演进:术语以本教程成文时(2026 年 10 月)的主流用法为准;工具实现迭代快,名称对不上时以所用工具的官方文档为最终依据。

A.2 全书概念地图

五十多个术语不是平铺的,它们挂在同一棵"代理能力树"上。先看树再查表,每个术语就知道自己服务于哪一层:

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 的两场综合实战。查表前先定位自己在哪一层,能省一半时间。

A.3 模型与上下文

一切方法的地基:模型能看到什么、会记多久、会犯什么错。这组词不理解,后面的所有配置都只是背口诀。 主线在 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 章

A.4 工具与协议

代理的"外挂装备"与它们的标准接口。 这组的主题是四条供给线:能力怎么接进来(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 章

A.5 工程机制

从"聊天生成代码"升级为"工程化协作"的一组机制。 四个经典问题各有对应词:权限怎么配(第 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 章

A.6 多代理协同

从一个代理到一支编队。 五种架构模式解决"怎么编队"(第 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 章

A.7 安全与治理

放手让代理干活的底气来自这组词。 前四个词负责"限"——限权、限网、限爆炸半径、层层设防;后四个词负责"管"——管凭据、管记录、管节点、管预算。主线在第 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 章

A.8 易混淆术语辨析

十一组最容易混的近义术语,每组几句话分清。这些配对在实战里经常被互换使用,而互换的代价就是配置错位。

  1. vibe coding vs agentic engineering(代理工程)——前者指凭感觉、少逐行审查的对话式工作方式,做 demo 快但撞墙也快;后者是其工程化升级形态:规格驱动、测试护栏、人工检查点、沙箱隔离。 本教程的全部内容,就是带你完成从前者到后者的迁移(第 1 章)。
  2. AGENTS.md vs SKILL.md——前者常驻:每个会话开头自动进入上下文,写"项目是什么、规矩是什么";后者按需触发:任务匹配才加载,写"某类任务怎么做"。 把低频流程塞进 AGENTS.md,等于让每个会话背几千字用不上的说明书,是第 8 章点名的反模式。
  3. MCP vs Skill——第 6 章一句定音:MCP 给能力(数据库、浏览器这些"手"),Skill 给知识(某类任务的做法这个"脑")。 判断"该装 server 还是写 Skill",先问缺的是能力还是方法:连不上数据库是能力问题,每次加接口都出错是方法问题。
  4. 权限模式 vs 权限规则——模式是全局姿态(default、plan、acceptEdits、bypassPermissions),决定整体放行基调;规则是逐动作条目(allow / ask / deny),做精细裁量。 模式选宽了用规则来收,规则收得太碎就用模式调——两者配合而非二选一(第 7、11 章)。
  5. 权限规则 vs hooks——权限规则回答"这个动作能不能做",是静态闸门;hooks 回答"做的过程中与之后还要额外做什么",是动态检查。 拦危险命令靠 deny 规则,落盘前扫密钥、写完自动格式化靠 hooks——两者互补而非互替。
  6. 上下文窗口 vs 记忆系统——窗口是一次会话的工作记忆,会话结束即清空;记忆是跨会话的持久沉淀,后续会话自动带回。 前者靠上下文工程省着用,后者靠第 5 章的规则与记忆体系管着用——把重要约定托付给聊天历史,是新手最贵的一课。
  7. 权限规则 vs 沙箱——权限规则是事前闸门(动作发生之前拦截),沙箱是事后围墙(动作发生之后限制波及范围)。 第 11 章的结论是两者都要:任何单层护栏都会被绕过,安全来自纵深而不是单点。
  8. 任务卡 vs spec vs handoff——任务卡说"这张卡做什么、怎么算完成";spec 说"这个功能做成什么样、契约是什么";handoff 说"上个会话干到哪、下个会话从哪续"。 三者分别服务单卡执行、功能契约与跨会话续作,混写成一份就会互相淹没。
  9. subagent vs workflow——一次性任务直接派 subagent(第 18 章,对话式、灵活);预计要跑两遍以上的多步流程写成 workflow(第 19 章,可复现、可断点、可审计)。 判据:命中"会重复、有依赖控制、要结构化传递"两条以上,就上脚本。
  10. 主从编排 vs 扇出汇总——同构批量任务(每个切片同一策略)选扇出汇总;任务不同构、需要分别下派时选主从编排——第 17 章选型表的原话是"任务不同构是扇出汇总的反指标"。 拿不准就先按主从设计,确认切片同构后再退化为扇出提速。
  11. 幻觉 vs 提示注入——幻觉是模型"自发"犯错(编造不存在的库与 API),提示注入是"被教唆"犯错(恶意指令藏在它要读的内容里)。 前者靠核验流程兜底(npm view 核实、跑测试),后者靠边界设计防御(只读、域白名单、沙箱)。

A.9 口头叫法对照

社区与团队口语里的常见说法,与本书标准术语的对应关系:

你可能听到的说法 本书标准术语 说明
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 调试时定量验证隐式命中

A.10 按角色的检索路径

你是谁 建议路径
刚上手的开发者 先读第 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 章反模式大全对照使用,多数"不对劲"都能对号入座

A.11 缩写对照

缩写 全称 含义与出处
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 渐进式披露的三级加载,按上下文区分

导航

📄 附录 B · 模板库:可直接复制的八个模板 B-templates.md

本附录把全书出现过的八份关键模板收成"空白可复制版"。占位符约定:项目名一律写 YOUR_PROJECT,域名写 YOUR_DOMAIN,尖括号 <…> 表示需要你填写的说明位。安全硬约束:所有模板零凭据字面量——密钥、口令、连接串一律环境变量注入(如 ${MY_KEY} 或 process.env.MY_KEY),复制后切勿把真实值写进任何进 Git 的文件。

B.0 索引与使用说明

  • 每份模板前有两行"什么时候用 / 怎么改",先读说明再复制;
  • 模板是起点不是终点:落进你的仓库后必须按对应章节做一次验证(见 B.10);
  • 配置类模板(B.7、B.8)放 .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 章

B.1 模板一 · AGENTS.md 起步模板

什么时候用:新项目初始化(贯穿项目的 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 规则去硬约束。

B.2 模板二 · monorepo 子目录 AGENTS.md 模板

什么时候用:monorepo 里某个子包有"只在该目录成立"的约定(如前端子包的组件规则),放根目录会污染其他目录的约定。 怎么改:只写增量,不复制根文件内容;每一条都自问"它只在本目录语境下成立吗";超过 15 行说明这是低频流程,该按第 8 章判据抽成 Skill 而不是继续加行。

# AGENTS.md — YOUR_PACKAGE/(子目录补充约定)

根目录 AGENTS.md 的约定在本目录同样生效,本文件只写本目录特有的增量。

## 补充约定
- <本目录的命名 / 结构约定,如:组件一律函数式,文件名 PascalCase>
- <本目录专属的数据获取约定,如:服务端状态统一走 src/api/client.ts>
- <本目录的安全红线,如:渲染用户内容前必须消毒>
- <本目录的禁用项,如:不引入 UI 组件框架>

两份文件的分工:根文件管"全仓库都成立"的约定,子文件管"只在这里成立"的增量。验证动作与 B.1 相同——让代理读两份后复述,核对有无漏读误读。

B.3 模板三 · SKILL.md 模板

什么时候用:某类任务已重复出现 ≥ 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 章的样本集定量验证,不凭感觉宣布。

B.4 模板四 · 任务卡模板

什么时候用:把垂直切片拆成卡、落盘到 docs/tasks/ 时(第 14 章交付);给 subagent 派活前,任务卡就是提前写好的自包含提示词。 怎么改:题头"编号与卡名"是第 1 字段,编号建议用"里程碑-序号"(如 M1-02);验收标准必须是"可执行命令 + 期望结果",写不出命令就说明卡还没拆到位;契约信息用"链接而非复制",两处事实源必然漂移。

## [编号] [卡名]

- **目标**:一句话说清"完成后系统多了什么能力",以及为什么值得做。
- **涉及文件**:新增 / 修改哪些文件(相对路径,标注 新/改)。
- **实现要点**:技术决策、必须遵守的约定(链接 AGENTS.md / spec,契约
  用"链接而非复制":写"以 docs/spec/YOUR_SPEC.md 为准",勿把 spec 抄进卡);
  以及明确不做的事。
- **验收标准**:编号列出"可执行命令 + 期望结果",全部通过才算完成:
  1. `npm test -- <对应测试文件>` → 全部通过;
  2. `<验证命令,如 curl 接口或打开页面>` → <期望结果>。
- **依赖**:前置卡编号;无依赖时写"无(前置:M0 已就绪)"。
- **预计规模**:文件数 / 新增行数 / 预计会话时长。

写卡口诀:目标写能力不写任务、验收写命令不写形容词、依赖写编号不写"应该已经好了"。编号连续性与依赖图在拆卡评审时统一检查。

B.5 模板五 · 功能 Spec 模板

什么时候用:计划先行(第 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)用"链接而非复制"指向它;修订走版本号,不原地覆盖已评审的历史。

B.6 模板六 · Handoff 交接文档模板

什么时候用:每次主动收尾时(不是"想起来才写"),落盘 docs/tasks/ 并随代码提交;会话可能随时死掉,状态必须活在仓库里而不是聊天记录里。 怎么改:"已知坑"是全文最值钱的一段,务必写具体;提交号、分支、测试状态用真实值;写完让接手会话"先读 handoff 与任务卡 → 复述理解 → 人确认后开工"。

# Handoff · <任务名> · <会话 N → 会话 N+1>

- 时间:YYYY-MM-DD HH:mm  作者:<哪个会话 / 谁写的>
- 下手前先读:<任务卡路径>、<spec 路径>、<决策日志路径>

## 已完成
- <做了什么>(commit <短哈希>)

## 当前状态
- 分支 <分支名>;工作区 <干净 / 有未提交改动:逐条列出>;
- 测试 <全绿 / 有失败:列出>;任务清单勾至 <T 几>。

## 下一步(按顺序)
1. <具体到"改哪个文件的哪部分">;
2. <……>

## 已知坑
- <踩过的坑与绕法——全文最值钱的一段,别省略>

## 关键文件
- <带路径列出,让接手者不必全库搜索>

与决策日志(第 16 章)配合使用:handoff 管"干到哪",决策日志管"为什么这么定";两者都只追加不改写,推翻旧结论就追加新条目并注明取代谁。

B.7 模板七 · 权限配置模板

什么时候用:项目 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),不合格的删掉——清单是养出来的,不是一次设计出来的。

B.8 模板八 · Hooks 配置模板

(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),每次改规则后回放一遍,防止"修一个误拦、放走三个危险"。

B.9 八个模板怎么协作

一份"作战地图",说明八份模板在一次真实开发里的接力顺序:

  1. M0 工程骨架:B.1(根 AGENTS.md)+ B.2(子目录增量)+ B.7(权限护栏)+ B.8(hooks 硬约束)——配置先行,代理开机即懂、出手有界;
  2. 每个功能:B.5(Spec 定契约)→ B.4(切片拆卡)→ 逐卡执行、逐卡验收;
  3. 每次收尾:B.6(Handoff)随代码提交,状态活进仓库;
  4. 重复 ≥ 3 次的任务:按第 9 章三判据立项,用 B.3 沉淀成 Skill,进入"随取随用"的技能库;
  5. 新会话冷启动:读 B.6 的 handoff 与对应 B.4 任务卡 → 复述理解 → 人确认后开工。

B.10 使用模板的三条纪律

  1. 模板是起点不是终点:落进你的仓库后必须做对应章节的验证——AGENTS.md 用新会话复述测试(第 4 章),权限规则用样本动作验证放行 / 拦截(第 7 章),hooks 先手动喂 JSON 再进真实会话(第 13 章);
  2. 最小起步、增量生长:权限清单从默认询问起步"养"出来;Skill 先过三判据再立项;deny 与禁区宁紧勿松,allow 宁少勿多——放宽随时可以,收紧要趁早;
  3. 一切进 Git 走评审:约定、护栏与流程只有成为仓库里可评审、可回滚的一部分,才是团队资产;个人例外写本机覆盖文件(settings.local.json、AGENTS.local.md)并加入 .gitignore;
  4. 模板会过期:本附录基准时间 2026 年 10 月,工具配置格式迭代快;套用前与所用工具的当前文档核对字段名,以官方文档为最终依据。

导航

📄 附录 C · 检查清单:开工前、收工前、派活前 C-checklists.md

附录 B 给了八份模板——"东西长什么样";本附录给四张清单——"什么时候必须逐项核对"。 四张清单对应代理工程的四个高风险切换点:项目接入代理(一次性)、每个工作日开工(日常)、 任务收工与合并(每卡)、向 subagent 派活(每次派发)。清单与模板配套使用:每一项几乎都 能在附录 B 找到对应模板,勾选时顺手核对模板是否真的落了地。

安全硬约束与全书一致:零凭据字面量——任何一项核对中只要发现密钥、口令、连接串以字面量出现, 一律先修再谈合并(密钥走环境变量注入见第 6 章,hooks 凭据扫描是最后一道闸见第 13 章)。

C.0 为什么需要清单,以及怎么用

记忆靠不住。三次教训全部来自第 21 章的真实时间线:

  • M0 当晚"hooks 三件套只配了凭据扫描,越界拦截等像样了再补"——这笔账在 M2 爆雷成 app.ts 双改冲突;
  • spec 定稿几晚后范围已变,没人记得回去改——两处过期 spec 靠收口阶段的 workflow 审计才抓出来;
  • 派活提示词写了"共享文件只读",却没写清 app.ts 挂载怎么办——一次真实冲突换来章程第 2.3 条。

三次的共同点:**道理都懂,但轮到执行的那一分钟没人核对。**清单的作用就是把"懂"变成"勾":

  • 清单是外部记忆:把散落在第 4–20 章的检查动作固定成勾选序列,不依赖当天的状态与心情;
  • 勾选即留痕:逐项勾选并落盘,事后可审计——出了问题能回溯到当时哪一项没勾;
  • 清单会生长:复盘换来的新规则优先沉淀为新勾选项——一条事故换一条清单项(见 C.6)。

怎么读:赶时间只看每张清单前的"什么时候用 / 预期耗时"两个加粗行;第一次接入项目把清单一逐项 过完;之后把清单抄进自己的仓库(见 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 的"什么时候用")。

C.1 清单一 · 项目接入 AI 代理检查单(一次性设置)

什么时候用:新项目 git init 之后、首次给仓库接入 AI 代理之前;接手陌生仓库(第 22 章)同样先过一遍。这一阶段全程由人主导,代理最多当参谋——地基不值得放权。 预期耗时:30–60 分钟。这是回报率最高的半小时,之后的每个代理会话都在吃它的红利。

仓库与 Git

  • [ ] Git 仓库已初始化并有首个空提交(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 章);
  • [ ] monorepo 子目录的增量 AGENTS.md 按附录 B.2 就位,只写"只在本目录成立"的约定,不复制根文件内容(第 4 章);
  • [ ] 规则分层检查通过:共享层(进 Git)、团队约定、个人偏好三层各改各的、互不覆盖,个人层已进 .gitignore(第 5 章);
  • [ ] 验证动作已做:开一个全新会话,让代理"读一遍 AGENTS.md,用 5 条要点复述你将遵守的约定",核对一致才算数(第 4 章)。

权限与边界

  • [ ] 权限配置按附录 B.7 落地:从默认询问起步,defaultMode 保持 "default",判定口径 deny > ask > allow(第 7 章);
  • [ ] deny 规则覆盖 .env 三种形态(.env、.env.*、**/.env*)、secrets/**、rm -rf、git push --force 与已应用的迁移目录(第 7、11 章);
  • [ ] 边界档位矩阵落盘 docs/boundary-matrix.md:开发 / 联调 / 发布三档各允许什么、禁止什么;三条红线任何档位不放开——force push、rm -rf、读取生产密钥(第 11 章);
  • [ ] 测试库就绪:测试数据库与生产隔离,代理可随意重置,跑失败也不碰真实数据(第 11 章)。

工具与 MCP

  • [ ] 工具链与骨架就绪:zcode --version 可运行;npm run dev 起得来、npm test 冒烟全绿、npm run lint 零告警——M0 验收三连(第 2、21 章);
  • [ ] hooks 按附录 B.8 配置:先脱离会话手动喂样本 JSON 调通脚本,再进真实会话端到端验证(第 13 章);
  • [ ] hooks 样本集回放通过:allow 样本必须退出 0(误拦即 FAIL),deny 样本必须退出 2(漏拦即 FAIL)(第 13 章);
  • [ ] 凭据扫描 hook 在位:构造一个含假密钥的提交,确认会被拦下(第 13 章);
  • [ ] MCP server 按需启用:只开本项目用得上的(如 GitHub 查 issue / PR、SQLite 只读),不开"以后可能用"的(第 6 章);
  • [ ] MCP 凭据全部环境变量注入,配置与日志零字面量;出网白名单只放行点过名的域——SSRF 防护第一道闸(第 6、13 章)。

团队共享

  • [ ] 配置全部进 Git 走评审:AGENTS.md、权限、hooks 是仓库里可评审、可回滚的团队资产;个人例外进 local 文件并加 .gitignore(第 5、7 章);
  • [ ] 团队同步完成:一次宣讲加演示,每位成员各自跑一遍"新会话复述"验证(第 5 章);
  • [ ] 本清单勾选结果已存进 docs/(如 docs/onboarding-checklist.md),注明日期与执行人(第 16 章);
  • [ ] 模板逐个对照过附录 B:B.1 / B.2 / B.7 / B.8 落盘到模板指定位置,字段名与所用工具的当前文档核对过(附录 B.10)。

配套模板:B.1(根 AGENTS.md)、B.2(子目录增量)、B.7(权限)、B.8(hooks)——各项的具体写法、落盘位置与验收方式见对应模板。

漏勾的典型代价(第 21 章实录):

  • 跳过 hooks 样本集验证 → app.ts 双改冲突就是这么被放大的(M0 欠账,M2 连本带利还);
  • deny 漏掉 .env 的某一种形态 → 代理在子目录新建文件绕过,凭据扫描形同虚设;
  • 跳过"新会话复述"验证 → 看似配好了,代理其实没读懂,每个会话都在重复解释技术栈。

C.2 清单二 · 日常开工检查单(每个工作日)

什么时候用:每个工作日(或每个工作时段)开工的第一分钟、写第一句话之前。 预期耗时:3–5 分钟,全部是"确认"而非"设置"——设置类的事已在清单一做完。

  • [ ] git status 干净:无未提交改动、无未跟踪文件;昨天的活已收口,或已按附录 B.6 写好 Handoff(第 12、16 章);
  • [ ] 昨日状态已恢复:读了最新 Handoff 与决策日志的增量,向代理复述理解、核对一致后再动手(第 16 章);
  • [ ] 今天的边界档位已明确:处于开发 / 联调 / 发布哪一档,按 docs/boundary-matrix.md 收紧或放开;三条红线一条没松(第 11 章);
  • [ ] 任务卡在手:要做的卡(编号如 M1-02)已打开,目标、涉及文件、验收标准三个字段已读——无卡不开工(第 14 章,附录 B.4);
  • [ ] 卡里没写清的地方先补卡:代理问出"卡里本该写清"的问题时,先改卡再动代码,答案写回 docs/tasks/(第 21 章);
  • [ ] 上下文供给已备好:该读的文件列进第一句话——AGENTS.md、相关 spec、相邻代码、Handoff,不让代理自己全库翻(第 3 章);
  • [ ] 第一句话是计划不是代码:"先别写代码。读 X/Y/Z,给出实现计划:改动清单、假设、测试与验收、执行顺序"——先探后改(第 15 章);
  • [ ] 本次"不做的事"已写进第一句话:顺手不做哪些模块、不装什么依赖,防范围蔓延(第 15 章);
  • [ ] todo 清单已建:代理把本次会话的步骤建成待办清单,逐项推进、逐项勾销,看板同步推进(第 16 章);
  • [ ] 上下文窗口健康:要干的事超过窗口七成就先压缩或拆会话接力,别硬塞——中途失忆比慢更贵(第 3、16 章);
  • [ ] 模型档位与任务匹配:设计类用强档,机械批量可降档跑;拿不准就从强档开始(第 2、20 章);
  • [ ] 今天的活里有要派给 subagent 的吗:有,先过清单四再派,不边干边派(第 18 章)。

漏勾的典型代价:

  • 不看 git status 就开工 → 昨天未收口的改动混进今天的提交,回滚粒度全乱;
  • 跳过"先计划后动手" → 假设无人批注直接开写,半天返工起步(第 15 章:3 分钟批注换半天返工);
  • 上下文塞太满 → 中途被压缩,先前的约定丢失,代理开始"失忆式发挥"。

C.3 清单三 · 任务完成/合并前检查单(收工前)

什么时候用:每张任务卡宣布"完成"之前、每次提 PR 或合并之前;发布场景以本清单为底版(即第 21 章的「发布前检查单」)——发布时在全量测试之上再过一遍,并追加全量回归与 e2e 冒烟。 预期耗时:代理逐条自报约 5 分钟;人工抽查 diff 另计(按卡规模 5–20 分钟)。分工是——代理负责逐条自报,人负责抽查,不是反过来(第 21 章)。

功能与验收

  • [ ] 验收标准逐条勾:卡内每条"可执行命令 + 期望结果"都真实跑过并原样贴出结果,没有一条是"应该没问题"(第 14 章);
  • [ ] 对照 spec 验收标准(AC-X)全部通过;范围未蔓延——spec"明确不做的事"没有被顺手做掉(第 15 章);
  • [ ] 偏差显式声明:与卡、与 spec 不一致之处逐条列出并说明原因,"偏差:无"也要显式写出来(第 21 章);
  • [ ] 主路径人工过一遍:改动涉及端到端主路径的,手动走一遍关键用户路径——两域交界处最值得走(第 21 章)。

测试

  • [ ] 全量测试通过:npm test(或项目等价命令)全绿,不是只跑了自己改的那几个文件(第 7 章);
  • [ ] 新行为有测试:本次新增或修改的行为都有对应用例;修 bug 的先有复现用例再修(第 14 章);
  • [ ] 静态检查通过:lint 与类型检查(tsc --noEmit)零错误零告警(第 21 章)。

安全

  • [ ] 零凭据字面量:本次 diff 无任何密钥、口令、连接串字面量,敏感值一律环境变量注入(第 6、13 章);
  • [ ] 敏感文件未动:.env*、secrets/**、已应用的迁移文件零改动——deny 规则之外再人工核对一遍(第 7、11 章);
  • [ ] 输入与外链已设防:新接口有输入校验并走项目统一错误格式;新增外部请求只打白名单域(SSRF 防护)(第 13 章);
  • [ ] 依赖变更已审:新装或升级的依赖是点过名的包,无弃维护、无可疑来源(第 21 章发布检查项)。

代码与提交

  • [ ] diff 全部读过:人逐行看过本次改动——代理自报不算数,看不懂的行让代理逐行解释到位(第 1、21 章);
  • [ ] 错误处理与回滚考虑过:失败路径齐全且走统一错误格式;出了问题怎么回滚、回滚粒度多大,心里有数(第 12 章);
  • [ ] 提交信息合规:Conventional Commits 格式、卡号入尾(如 feat(server): add books CRUD API (M1-02)),一卡一提交、大卡拆数个提交(第 21 章)。

文档回写

  • [ ] AGENTS.md 要更新吗:本次沉淀的新约定(如"路由挂载由主代理执行")已晋升进根或子目录 AGENTS.md(第 5、21 章);
  • [ ] spec 与任务卡要回写吗:实现中的口径变化已写回 spec 与卡——卡是活的,答案写回卡里,后续会话不再问(第 14、15 章);
  • [ ] 收尾物已落盘:Handoff(附录 B.6)已更新,看板 / todo 已勾销,决策日志有增量已追加——只追加、不改写(第 16 章)。

配套模板:B.4(任务卡——验收标准即第一组)、B.5(Spec——AC 是第二道验收)、B.6(Handoff——收尾物落盘)。

漏勾的典型代价:

  • 只跑自己改的文件 → 合并后连带打挂别人的模块,CI 红了才发现;
  • diff 没全读就合并 → "自说自话的完成"直接进主干,spec 也拦不住方向性漏网;
  • 文档不回写 → 两周后 spec 过期、卡口径漂移,下一个会话拿旧地图走新路(第 21 章收口审计抓出两处)。

C.4 清单四 · 多代理派发前检查单(派活前)

什么时候用:主代理每次向 subagent / workflow 派活之前——由主代理按条起草提示词与安排,人最终批准后才派发。 预期耗时:10–15 分钟,大头在把提示词写到自包含;省下的都是后半夜排查冲突的时间。

派发决策

  • [ ] 值得派发三问全过:任务是否自包含、边界是否清晰、结果是否可验收——任一答不上来就先自己拆,不硬派(第 18 章);
  • [ ] 拆分边界已想清:按功能域垂直切、任务域之间文件不交叠——"拆分边界还没想清楚就上多代理,等于把混乱并行化"(第 17 章);
  • [ ] 并发不超上限:WIP 从 2 起步、贴着章程上限;第一个里程碑先用单代理练节奏,再放开并行(第 17、21 章)。

提示词质量

  • [ ] 四要素齐:目标(做什么)、背景(读哪些文件 / 卡 / spec)、约束(范围与禁区)、输出格式(回传什么)一个不少(第 17、18 章);
  • [ ] 自包含可独立执行:不依赖聊天历史,路径全部写全;契约"链接而非复制",指向 spec 与任务卡(第 18 章,附录 B.4 / B.5);
  • [ ] 范围与禁区写死:仅允许改动哪些路径逐条列出,共享文件标只读;安全约定(消毒、密钥零字面量)随卡下放(第 20 章);
  • [ ] 歧义处理规则已写明:遇到卡里没写清的情况,停下回传问题,禁止自行其是或自造约定(第 14、18 章);
  • [ ] 回传验收标准已写明:完成定义可执行(测试全绿 + spec AC 全过 + lint / tsc 通过),回传格式固定为"改动文件清单 / 测试结果(数字)/ 偏差与原因 / 遗留问题"(第 18、21 章)。

所有权与契约

  • [ ] 文件所有权分区已成文:每个 worker 的可写路径互斥,测试目录也纳入所有权协议(一域一文件)(第 20 章);
  • [ ] 共享文件修改规则已声明:app.ts、package.json、shared/types/** 等谁只读、谁有权改、改走什么流程(回传"修改申请",或一律由主代理执行)(第 20、21 章);
  • [ ] 接口契约已固化:迁移编号顺延规则、共享类型、API 形状先落盘 shared/types/**,变更需主代理批准并广播(第 20、21 章);
  • [ ] 下游依赖已用 mock 解耦:下游 worker 按契约 mock 上游接口(如 msw),不被对方进度卡脖子(第 21 章)。

资源与收口

  • [ ] 预算与档位已设:每个 subagent 的模型档位、token 与时长上限明确;批量机械任务降档跑(第 20 章);
  • [ ] 边界随之下放且不放松:worker 继承主会话的权限与 hooks 护栏,deny 红线不因派发而放开(第 11、13 章);
  • [ ] 结果落盘安排已定:产物写到哪个路径、走 PR 还是补丁、看板如何更新——结果进仓库,不活在对话里(第 16、19 章);
  • [ ] 失败预案已备:subagent 卡死、超预算、输出不合格时怎么处置(重试、缩小范围、回收人工)(第 19 章);
  • [ ] 集成节奏已排:每卡一 PR、CI 绿才合、按依赖图拓扑序合并;集成期冻结新派发(第 20、21 章)。

配套模板:B.4(任务卡=提前写好的自包含提示词)、B.5(Spec——契约的单一事实源)。

漏勾的典型代价(第 21 章实录):

  • 所有权没分区 → 两个 worker 同时改 app.ts,定位与合并吃掉一晚上的顺风局;
  • 契约没先落盘 → 下游 worker 的 mock 白写两天,苦等上游接口;
  • 回传标准没写明 → 代理自评"完成"但验收命令一个没跑,返工全发生在合并之后。

C.5 四张清单怎么接力:一个工作日的样子

以第 21 章的 M1-02「书籍 CRUD API」为例,四张清单在一天里的接力:

  1. 开工:过清单二(3 分钟)——git status 干净、边界档位=开发期、M1-02 在手、第一句话进入计划模式;
  2. 中途:代理问"编辑弹窗要不要封面预览"——卡里没写清,先补卡再继续(清单二第 5 项);中途想并行 处理一个独立小任务?先过清单四再派,不边干边派;
  3. 收工:过清单三(10 分钟)——验收命令逐条跑、diff 全读、提交信息带卡号、Handoff 更新、看板勾销;
  4. 合并后:清单三"文档回写"组若更新了 AGENTS.md 或 spec,第二天的清单二立刻吃到红利——新会话 不再追问这个口径;
  5. 接入新项目的那天:多花 30–60 分钟过清单一,此后每天只花 3–5 分钟——这就是"把规矩做在前面"的复利。

清单不是四座山,是四个开关:C.1 一次拧紧,C.2 每天打开,C.3 与 C.4 按需扳动。

C.6 使用建议:让清单长在流程里

清单只在"长在流程里"时才起作用——躺在书签夹里的清单,第三次就不会被打开。八条建议:

  1. 落盘进项目 docs/:四张清单复制进 docs/checklists/,随代码进 Git 走评审,与附录 B 的模板互为表里——模板给"长什么样",清单给"何时核对";个人可另建精简版,团队版以仓库为准(呼应第 5 章的分层思想);
  2. 勾选即留痕:清单二的勾选结果写进当天 Handoff;清单三在 PR 描述里贴一份勾选记录;清单四随任务卡归档——事后可审计,冲突可回溯;
  3. 高频清单晋升成 Skill:清单四"每次派活前逐项过"完全命中第 9 章三判据——重复 ≥ 3 次、步骤固定、含项目私有约定。把它做成 dispatch-subagent 一类的 skill,SKILL.md 的"完成定义"就是逐项勾选清单四;清单三同理可做 pre-merge-review;
  4. 做完按第 10 章验证:清单类 skill 的完成标准天然可判定(逐项勾选结果 + 关键命令退出码),隐式触发是否稳定用样本集定量验证,不凭感觉宣布生效;
  5. 清单随事故生长:复盘后 48 小时内的经验晋升(第 21 章)多问一句——"这条翻车要不要变成新勾选项?"一条事故换一条清单项,清单会越来越像你的项目本身;
  6. 赶时间至少勾两组:清单三的安全组与清单四的所有权组是代价最高的两组,跳过的代价以半天计——没时间全勾,先勾这两组;
  7. 发布日多一步:发布前除清单三外加跑全量回归与 e2e 冒烟(第 21 章收口三件套),NFR 类指标实测后留痕;
  8. 每周一次清单体检:评审本周弹确认框沉淀的权限规则(第 7 章养清单的节奏),顺手检查四张清单有无要增删的条目。

清单 Skill 的最小骨架(以清单四为例)

按附录 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 条)

导航

📄 附录 D · 贯穿项目需求书:「轻读 BookNest」 D-capstone-project.md

本教程所有章节的"贯穿项目实战"小节均基于本项目。写作时请保持与本文档设定一致;读者则应把本文档当作一份"可直接喂给 AI 代理"的需求书范例来学习。

D.1 产品愿景

「轻读 BookNest」是一个个人读书笔记与书评应用:记录读过的书、写读书笔记、给书打分,并按标签和年份回顾阅读轨迹。单用户自部署(先不做多用户),目标是"5 分钟部署,10 年不换"——数据永远在自己手里。

选择它的原因:体量适中(前后端加起来约 20–30 个源文件),既简单到初学者能跟上,又有足够的功能面让"任务拆分、边界设计、多 Agent 协同"有真实的用武之地。

D.2 功能需求

编号 功能 说明
FR-1 书籍管理 录入书籍(书名/作者/封面 URL/分类/状态:想读·在读·读完),支持编辑与删除
FR-2 读书笔记 针对某本书创建笔记(Markdown 正文),支持时间线浏览
FR-3 评分与标签 1–5 星评分;自由标签(如"科幻""年度十佳"),可按标签筛选
FR-4 阅读统计 总量/年度统计、月度阅读热力图、平均分分布
FR-5 数据导入导出 导出/导入 JSON 全量备份;支持从 CSV 批量导入书单
FR-6 全文搜索 按书名/作者/笔记内容关键字搜索
FR-7 快速记录 首页"灵感速记":先存为碎片,稍后归入某本书的笔记

D.3 非功能需求

  • NFR-1 可靠性:后端 API 全部有输入校验与统一错误格式;核心链路测试覆盖率 ≥ 80%;
  • NFR-2 安全:所有 SQL 使用参数化查询;无 XSS(笔记渲染需消毒);密钥零字面量,一律环境变量;
  • NFR-3 性能:列表页 1 万条记录内响应 < 300ms(本地 SQLite 足够);
  • NFR-4 可移植:npm run dev 一键起本地环境;npm run build && npm start 可部署到任意 Node 主机;
  • NFR-5 可维护:TypeScript 严格模式;ESLint + Prettier;Conventional Commits。

D.4 技术栈

层 选型 说明
前端 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

D.5 仓库结构(目标形态)

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

D.6 数据模型(核心三表 + 一表)

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)   -- 灵感速记

D.7 里程碑

里程碑 范围 对应教程章节
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 仓库浏览 →