01 学习交流群
知识库之外,我们还建了「工程化专题交流群」。
进群和大家一起交流 AI 应用从 Demo 到生产落地的工程实践,包括开发流程、架构设计、效果评估、性能优化与稳定性保障,也会同步专题最新更新和实战资料。
扫码加入,一起聊聊怎样把 AI 应用真正做成、上线并稳定运行。

02 智能体运行环境工程是什么
预计阅读时间:25 分钟
本篇目标:理解 Harness Engineering 优化的到底是什么,分清它与 Prompt Engineering、Context Engineering 和 Loop Engineering 的边界,并能对一个现有项目做第一次 Harness 体检。
你大概见过这样的场面:同一个 Agent,在演示视频里能连续工作几个小时,到了自己的项目却连启动命令都找不到;让它改一个筛选功能,它先猜技术栈,再猜目录结构,最后改完一句“已经完成”,却没有真正打开页面验证。
遇到这种情况,最自然的反应是继续改提示词:把要求写得更详细,强调“认真检查”,甚至换一个更强的模型。这样有时有效,但它解决不了一个更基础的问题——Agent 所在的工作环境,究竟有没有给它完成任务所需的条件?
Harness Engineering 关心的就是这个问题。
先把 Harness 想成“Agent 的工作台”
harness 原本有“挽具、线束、控制装置”的意思。在 Agent 系统里,它通常指包围模型、让模型能够实际工作的那层系统:怎样提供上下文,开放哪些工具,把它放进什么环境,怎样收集运行结果,又用什么规则判断它做完了。
如果把模型比作一位刚加入项目的工程师,那么 Harness 就是他拿到的整套工作条件:
- 入职资料是否说清楚产品和架构;
- 仓库是否容易导航;
- 电脑和依赖能否顺利准备好;
- 启动、测试和检查有没有稳定入口;
- 出错时能否看到日志和运行状态;
- 哪些操作允许直接做,哪些必须先确认;
- 怎样证明一项工作真的完成了。
因此,Harness 不是单独一个文件,也不是某款工具的专有功能。它是一组彼此配合的条件。
我们在这套教程中使用下面这个工作定义:
Harness Engineering,是围绕 Agent 的一次实际运行,系统地设计知识、工具、执行环境、反馈、验证和权限边界,让 Agent 能够更可靠地理解任务、采取行动并证明结果。
OpenAI 在 2026 年发布的 Harness Engineering 实践总结 中提到,他们早期推进缓慢,并不只是 Codex 能力不够,而是环境缺少完成高层目标所需的工具、抽象和内部结构。团队处理失败的方式,也不再只是让 Agent “再努力一点”,而是追问缺少了什么能力,以及怎样让这种能力对 Agent 可见并可执行。
这句话非常重要。Harness Engineering 的起点不是“怎样写出一句万能指令”,而是把一次失败当成环境诊断信号。
一次 Agent 运行里,究竟有哪些东西
先看一个简化模型:
┌─────────────────────────────────────────────┐
│ 用户目标 │
└──────────────────────┬──────────────────────┘
│ 指令 + 可发现的项目知识
▼
┌─────────────────────────────────────────────────────────────────┐
│ 模型(决策) │
└───────────────┬───────────────────────────────┬─────────────────┘
│ 调用 │ 观察结果
▼ ▲
┌─────────────────┐ │
│ 工具 │ │
│ 文件 / 终端 / │ │
│ 浏览器 / 服务 │ │
└────────┬────────┘ │
│ 实际执行 │ 反馈
▼ │
┌─────────────────────────────────────────────────────────────────┐
│ 运行环境 与 权限边界(约束能做什么) │
└───────────────┬─────────────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────────┐
│ 反馈层:测试 / 日志 / 页面 / 检查器 │
└───────────────┬─────────────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────────┐
│ 可验证的结果 或 明确停止并求助 │
└─────────────────────────────────────────────────────────────────┘
这里至少有六类 Harness 组件。后面的课程会逐一改造它们。
1. 知识:Agent 能否找到项目事实
Agent 需要知道项目做什么、代码怎样组织、哪些规则不能破坏,以及当前任务怎样才算完成。知识可能出现在 AGENTS.md、架构文档、产品规则、代码注释、Schema 和任务说明中。
关键不只是“有没有写”,还包括能否被发现、是否与代码一致、是否适合按需读取。把三万字规范全部塞进一段系统提示,并不比完全没有文档高明多少。
2. 工具:Agent 能否采取必要行动
只让 Agent 看源码,却不给它终端、文件编辑、浏览器或日志查询能力,它就只能根据静态文本猜测运行结果。工具也不是越多越好:每一种工具都需要清楚的用途、输入、输出和权限。
例如,修改一个网页功能至少可能需要:搜索代码、编辑文件、运行测试、启动服务、操作页面、读取控制台错误。缺少其中任何一环,都可能迫使 Agent 用猜测填空。
3. 环境:工具是否真的能稳定运行
同样一条 npm test,在人类电脑上能运行,不代表 Agent 的隔离工作区也能运行。隐含的环境变量、手工安装的全局依赖、只存在于某个人电脑的数据库和未写入仓库的初始化步骤,都会让 Agent 卡住。
Agent 友好的环境应该能重复创建,并且失败时给出足够明确的原因。
4. 反馈:Agent 能否观察到行动的后果
代码改了不等于行为改对了。Agent 需要看得见测试结果、页面状态、结构化日志、网络请求、性能指标或其他运行证据。
OpenAI 的案例中,团队让应用能够在每个 Git worktree 独立启动,并把 DOM、截图、日志、指标和链路追踪暴露给 Agent。这样,“修复页面故障”才不再只是改完代码后的一句自我判断。
5. 验证:谁来定义“完成”
“请把功能做好并仔细检查”不是验证机制。可靠的 Harness 会把关键验收条件变成可重复执行的测试、静态检查、结构检查或用户行为脚本。
更重要的是,执行 Agent 不能拥有随意降低验收标准的权力。否则它可能通过删除测试、弱化断言或跳过检查,让结果表面上变绿。
6. 权限:Agent 能做什么、不能做什么
Harness 还要决定文件访问范围、网络访问、凭据使用、危险命令和外部系统写操作。权限太少,Agent 无法完成任务;权限无限,错误的代价又会被放大。
好的边界不是笼统地写一句“请谨慎”,而是把允许、需要确认和禁止的操作分开,并尽量由沙箱、命令白名单、凭据隔离或审批机制执行。
把这六类放在一起,你就会发现:AGENTS.md 很重要,但它只是知识入口,不是 Harness 的全部。
Harness Engineering 与三种相邻概念的区别
这几个术语经常混在一起。与其争论唯一正确的定义,不如看它们分别优化哪一层。
| 方法 | 主要问题 | 典型产物 |
|---|---|---|
| Prompt Engineering | 这一轮怎样把意图表达清楚 | 一段更明确的任务指令、示例、输出要求 |
| Context Engineering | 模型在当前决策前应该看到什么 | 检索、记忆、上下文选择与压缩策略 |
| Harness Engineering | Agent 怎样在真实环境里完成并验证一次任务 | 仓库知识、工具、环境、反馈、验证与权限 |
| Loop Engineering | 多次 Agent 运行怎样持续推进、恢复和停止 | 触发器、持久状态、预算、重试、升级与停止条件 |
它们不是互斥关系。
假设你让 Agent 给 ShelfCare 增加“按标签筛选藏书”功能:
- 把需求、限制和验收条件说清楚,是 Prompt Engineering;
- 决定本轮要加载哪些产品规则、相关代码和历史决策,是 Context Engineering;
- 让 Agent 找得到架构说明,能启动应用、操作页面、运行测试并受权限约束,是 Harness Engineering;
- 如果任务失败后要自动记录状态、重新规划、换一个隔离工作区继续,并在三次失败后转人工,这是 Loop Engineering。
本教程只聚焦第三层。我们会让一次 Agent 运行获得更好的工作条件,但不会搭建无人值守的长期任务循环。这样也能避免把“Agent 内部为了完成一项任务而反复调用工具”和“外部系统反复启动多个 Agent 运行”混为一谈。
Harness Engineering 优化的是“一次运行”
这里的“一次运行”不一定只有一个模型请求。编码 Agent 可能在一次任务中连续搜索文件、运行命令、修改代码和重新测试几十次。只要这些动作仍服务于同一个任务,并共享同一段运行上下文,我们就把它看作一次 Agent 运行。
Harness Engineering 想让这次运行具备四个阶段:
理解:找到目标、规则和相关代码
↓
行动:在受控环境中使用合适工具
↓
观察:读取命令、页面、日志和状态反馈
↓
验证:用独立证据判断完成、失败或需要求助
很多糟糕体验并不是 Agent 完全不会写代码,而是链条在中间断了:
- 只有行动,没有理解:改错目录或违背产品规则;
- 只有源码,没有环境:无法复现故障;
- 只有修改,没有观察:不知道页面是否真的可用;
- 只有自述,没有验证:一句“已完成”替代了证据;
- 有工具,没有权限边界:为了通过任务做出越界操作。
所以,衡量 Harness 不能只看最终代码能不能编译。还要看 Agent 是否少走弯路、是否能独立收集证据、是否知道何时停止,以及人在过程中被迫补充了多少隐含信息。
一个很常见的误区:什么都写进 AGENTS.md
当人们意识到 Agent 缺上下文时,第一反应往往是写一份越来越长的 AGENTS.md:安装命令、架构细节、所有业务规则、历史坑点、编码风格和当前任务,全堆在一个文件里。
这比没有入口好,但很快会出现三个问题:
- 重要信息被淹没。 每次任务都加载大量无关内容,真正关键的边界反而不突出。
- 文档会过期。 命令和代码已经变化,长文档里的旧说法仍在影响 Agent。
- 文字无法强制执行。 “不要跨层访问数据库”写十遍,也不如一个结构测试可靠。
更有效的做法是把 AGENTS.md 变成一张短地图:告诉 Agent 从哪里开始、常用命令是什么、不同主题应该继续读哪份文档。能由脚本和测试执行的规则,就不要只停留在文字里。
第三篇会专门完成这项改造。现在先记住一句话:文档负责让规则可见,工具负责让规则可执行。
先给一个项目做 Harness 体检
在接触 ShelfCare 之前,可以先拿你熟悉的任意仓库做一个十分钟练习。不要启动 Agent,只站在“第一次看到这个项目的人”的角度回答下面的问题。
| 检查项 | 你能否在仓库里找到明确答案 |
|---|---|
| 项目解决什么问题,核心用户行为是什么 | 是 / 否 |
| 从零开始需要安装什么,支持哪些运行环境 | 是 / 否 |
| 一条命令能否启动项目 | 是 / 否 |
| 一条命令能否运行与当前修改相关的快速测试 | 是 / 否 |
| 完整检查入口是否清楚 | 是 / 否 |
| 主要目录和依赖方向是否有说明 | 是 / 否 |
| 关键产品规则是否写在仓库中 | 是 / 否 |
| 本地示例数据是否可重复创建 | 是 / 否 |
| 页面故障能否从日志或诊断命令中看到 | 是 / 否 |
| 功能完成是否有机器可检查的验收证据 | 是 / 否 |
| 哪些命令或文件属于高风险区域是否明确 | 是 / 否 |
| 文档、命令和实际代码是否有人持续校验 | 是 / 否 |
这里不需要计算一个看似科学的总分。更有用的是把所有“否”按六类标记:知识、工具、环境、反馈、验证、权限。
如果大部分问题都答不上来,那么 Agent 每次工作都要临时猜测,或者等人类在对话里补充。换一个更强模型可能缓解部分问题,却不会把这些隐含知识自动变成稳定的项目能力。
反过来,如果仓库的入口清楚、环境可复现、反馈可读取、规则可执行,同一个模型往往就能承担更长的任务。这也是 Harness Engineering 最值得投入的地方:改造结果会被后续很多次运行重复使用,而不是随着一次对话结束而消失。
这套教程如何做实战
接下来我们会使用一个叫 ShelfCare 的本地个人藏书管理应用。它不是大型企业系统,功能很直观:记录书名、作者、阅读状态和标签,并支持搜索与筛选。
初始仓库故意保留一些现实中常见的问题:
- 启动方式依赖口头经验;
- 测试命令不够容易发现;
- 业务规则散落在代码和人的脑子里;
- Agent 看得见源码,却看不见页面行为和关键日志;
- 架构边界写在说明里,却没有机器检查;
- “功能完成”的标准模糊。
第二篇,我们会用一个具体任务建立基线:让 Agent 增加“按标签筛选藏书”功能。你需要保留它的操作记录、人工补充、命令失败和最终验证证据。此时不要急着替它修环境,因为我们需要先看清问题。
之后每一篇只改造一层:
第 2 篇:记录失败,建立基线
第 3 篇:整理仓库知识与导航
第 4 篇:统一 setup、dev、test、check 入口
第 5 篇:建立可重复的验收链路
第 6 篇:暴露页面、日志与运行状态
第 7 篇:把架构、安全和偏好变成机器约束
第 8 篇:用原始任务重新测试并做前后对照
最后一篇仍然使用同一个 Agent、同一版模型和同一段任务描述。只有这样,我们才能比较 Harness 改造本身带来的变化,而不是把结果差异偷偷归功于换模型或改提示词。
Harness 不是万能药
最后也要把边界说清楚。
Harness 可以帮助 Agent 找到知识、正确使用工具、观察结果和遵守规则,但它不能把模型没有的能力凭空造出来。一个任务如果本身目标含糊、需要重大产品判断、缺少合法数据,或者超出当前模型能处理的复杂度,环境再完善也不保证成功。
同样,不是每个小项目都需要搭建复杂平台。一个人维护的脚本,可能只需要短 README、稳定命令和几条测试;多人协作且大量使用 Agent 的仓库,才值得投入更完整的日志、结构检查、隔离环境和自动验证。
Harness Engineering 的原则不是“组件越多越专业”,而是让每一项投入都对应一种重复出现的失败:
找不到事实 -> 改善知识入口
命令经常失败 -> 改善环境与脚本
不知道改没改对 -> 增加可观察反馈
完成标准含糊 -> 增加独立验证
频繁破坏边界 -> 增加机器约束
操作风险过大 -> 收紧权限并设置确认点
如果一种失败只出现一次,修好任务就够了;如果它会让后续 Agent 一再跌倒,就应该把修复沉淀进 Harness。
本篇检查
读完后,你应该能用自己的话回答四个问题:
- 为什么 Harness 不等于 Prompt,也不等于
AGENTS.md? - 知识、工具、环境、反馈、验证和权限分别解决什么问题?
- Harness Engineering 与 Loop Engineering 的优化对象有什么不同?
- 当 Agent 失败时,怎样判断应该改提示词、补 Harness,还是承认任务超出模型能力?
如果这四点已经清楚,就不要继续停留在概念里。下一篇我们会故意给 Agent 一个不友好的项目,让它先失败一次。那次失败不是翻车,而是整套改造最重要的基线。
03 先让智能体在糟糕环境里失败一次
预计阅读与操作时间:45~60 分钟
本篇目标:用同一个小功能建立一份可重复的 Agent 工作基线,区分“功能没做出来”和“环境让 Agent 不得不猜”这两类问题。
配套代码说明
本篇的实验起点是 assets/shelfcare-starter/,一份刻意“能运行、但没为 Agent 整理过”的起始项目。它使用 Node.js 自带的 HTTP 与测试模块,无任何第三方依赖,目录结构如下:
shelfcare-starter/
├── data/
│ └── books.json # 8 本藏书样本(id/title/author/status/tags)
├── public/
│ ├── index.html # 页面结构:搜索框、状态下拉、书卡模板
│ ├── app.js # 浏览器端状态、筛选与渲染逻辑
│ └── styles.css # 纯样式,不承载任何产品行为
├── src/
│ └── library.js # 读取 data/books.json,导出 listBooks()
├── tests/
│ └── library.test.js # 两项基础测试(数据可读、状态值合法)
└── server.js # HTTP 服务:/ 静态页 + /api/books 接口
代码实现的功能很直接:
- server.js 用
node:http起一个监听127.0.0.1:3000的服务,GET /返回首页、GET /api/books返回全部藏书 JSON,并对静态文件做了简单的路径穿越防护(越出public/一律 403)。 - src/library.js 是唯一的取数入口,用
path.join(__dirname, "..", "data", "books.json")定位数据文件,listBooks()读并解析后返回数组。 - public/app.js 负责全部交互:监听搜索框
input和状态下拉change,在currentBooks()里用keyword + status做 AND 过滤,通过<template>克隆书卡渲染,并在零结果时显示“没有找到符合条件的书。”。 - tests/library.test.js 用
node:test断言 8 本书可读、且status都落在planned/reading/finished三个合法值内。
这套起始代码的关键特征,正好对应本篇要制造的那些“缺口”:没有 README、没有 AGENTS.md、没有 package.json,启动靠 node server.js、测试靠 node --test tests/library.test.js 这些口头经验,业务规则(如搜索与状态如何组合)只存在于代码里。基线实验的目的,就是看 Agent 在缺少这些“可发现入口”时到底会怎样挣扎。
上一篇把 Harness 拆成了知识、工具、环境、反馈、验证和权限六个部分。这一篇先不改任何一部分。我们要把 Agent 放进一份能运行、却没有为它整理过的项目里,看看它怎样接手任务。
这不是一场“找茬测试”,也不是要证明 Agent 不行。Agent 可能失败,也可能把功能做出来。真正要记录的是过程:它找命令花了多久,做了哪些猜测,需要人补了几次信息,最后拿出了什么证据。
到了第八篇,我们会用相同 Agent、相同模型和完全相同的任务再跑一次。没有这份基线,所谓“改造后更好用”就只是一种感觉。
准备这次实验
课程提供了一份起始项目:assets/shelfcare-starter。它使用 Node.js 自带的 HTTP 和测试模块,不需要下载第三方依赖,避免网络和包管理器把实验带偏。
开始前需要:
- Node.js 20 或更高版本;
- Git;
- 一种已经可以正常使用的命令行编码 Agent;
- 大约 30 分钟不被打断的时间。
先在终端中确认版本:
node --version
git --version
node --version 应显示 v20 或更高版本。你的 Agent 只要能在当前文件夹中读写文件、运行终端命令即可;浏览器操作能力不是本篇的硬性要求,是否具备会成为一项观察结果。
复制一份实验项目
不要直接修改课程自带的起始文件。先把它复制成独立实验目录。
macOS 或 Linux:
cd harness-engineering-tutorial
cp -R assets/shelfcare-starter shelfcare-lab
cd shelfcare-lab
Windows PowerShell:
Set-Location harness-engineering-tutorial
Copy-Item -Recurse .\assets\shelfcare-starter .\shelfcare-lab
Set-Location .\shelfcare-lab
这里的 harness-engineering-tutorial 是你保存本教程文件的目录。如果终端已经位于该目录,就从复制命令开始。
检查当前目录,应看到下面这些内容:
data/
public/
src/
tests/
server.js
你会发现项目没有 README、AGENTS.md 和 package.json。这不是教程漏发了文件,而是初始仓库的一部分:程序可以运行,但很多信息仍靠维护者自己知道。
先由你确认起始项目没有坏
基线实验必须从一个可用项目开始,否则 Agent 的失败可能只是素材损坏。
在 shelfcare-lab 目录运行:
node server.js
终端应该出现:
ShelfCare running at http://127.0.0.1:3000
在浏览器打开 http://127.0.0.1:3000。页面顶部应显示 ShelfCare,下面有搜索框、阅读状态选择框和 8 本书。依次做两个检查:
- 搜索“Martin”,结果应剩下 2 本;
- 清空搜索,选择“想读”,结果也应剩下 2 本。
回到终端按 Ctrl+C 停止服务,再运行已有测试:
node --test tests/library.test.js
你应看到 2 个测试通过,失败数为 0。Node.js 小版本不同,输出排版可能略有差异,关键是末尾的 pass 2 与 fail 0。
这一步是人的预检,不要把命令复制到稍后给 Agent 的任务里。我们已经知道正确入口,实验要观察的是仓库本身能不能让 Agent 找到它们。
冻结实验条件
想比较两次运行,至少要固定四件事:Agent、模型、任务、起始代码。先在课程目录旁边新建一份 shelfcare-baseline.md,不要放进 shelfcare-lab,避免 Agent 在实验前读到观察表。
写下:
# ShelfCare Harness 基线
- 日期:
- Agent 与版本:
- 模型:
- 推理强度或模式:
- 操作系统:
- 浏览器工具:有 / 无
- 自动执行权限:
- 开始时间:
- 结束时间:
如果工具不显示精确模型版本,就记录界面中能看到的名称,不要猜。权限也要写清楚,例如“可读写当前目录,终端命令逐次确认”。第八篇复测时尽量保持一致。
接着把起始代码交给 Git 保存。仍在 shelfcare-lab 目录运行:
git init -b main
git add .
git commit -m "chore: add ShelfCare starter"
git switch -c baseline-attempt
如果较旧的 Git 不支持 git init -b main,改用:
git init
git branch -M main
如果提交时提示没有配置姓名或邮箱,按 Git 给出的提示配置自己的身份后重试。不要随便复制网上的陌生邮箱。完成后运行:
git status --short --branch
预期第一行包含 baseline-attempt,并且下面没有未提交文件。这样无论 Agent 改成什么样,我们都能看到精确差异,也能回到干净起点。
只给 Agent 一段原始任务
在 shelfcare-lab 目录启动你平时使用的命令行 Agent。授予它读取、修改当前目录和运行本地命令的权限,但不要开放课程目录、个人文件、真实凭据或不相关的外部系统。
把下面这段话原样交给它:
请为 ShelfCare 增加按标签筛选藏书的功能。请完成实现并检查结果。
不要补充启动命令、代码位置、界面样式、筛选规则或验收方法。这段任务故意接近真实工作中常见的简短需求:目标听起来清楚,但细节依赖项目知识和工程约定。
运行中尽量不要救场
观察 Agent 的行为,但不要因为它走了一点弯路就立刻提示。重点记下:
- 它先读了哪些文件;
- 是否主动寻找项目说明;
- 尝试了哪些启动或测试命令;
- 哪些命令失败了,失败后怎样恢复;
- 是否询问标签筛选的具体规则;
- 是否启动了应用;
- 是否真正操作了页面;
- 是否运行已有测试或新增测试;
- 最终用什么证据说明“完成了”。
如果 Agent 因权限确认停住,可以批准与当前项目直接相关、可逆的本地操作,并把这次批准记作一次正常权限确认。如果它需要你提供业务规则或启动命令,先把问题记下来,再只回答使实验能够继续的最少信息。
下面几类情况应直接拒绝:读取项目之外的个人目录、索要真实密钥、删除课程或其他仓库、上传代码到外部服务。基线实验不需要这些权限。
给这次运行设置一个边界:最多 30 分钟,或者在 Agent 明确报告完成、明确表示无法继续时结束。不要无限陪它重试。
不要先相信“已完成”,独立检查结果
Agent 停止后,先不要继续追问它。打开另一个终端,在 shelfcare-lab 目录检查实际修改:
git status --short
git diff --stat
git diff
你要确认它究竟改了哪些文件,而不是只看最后一段总结。接着运行起始测试:
node --test tests/library.test.js
然后启动应用:
node server.js
打开 http://127.0.0.1:3000,按下面这组行为逐项检查:
- 页面上能找到标签筛选控件;
- 标签选项来自现有图书数据,至少能选择“科幻”和“技术”;
- 选择“科幻”后,只显示《献给阿尔吉侬的花束》和《你一生的故事》;
- 选择“技术”并把阅读状态设为“在读”,只显示《设计数据密集型应用》和《程序员修炼之道》;
- 搜索、状态和标签三种条件同时生效,而不是互相覆盖;
- 恢复“全部标签”后,标签条件被清除;
- 没有结果时仍显示清楚的空状态,浏览器控制台没有报错。
这些是我们将要逐步写进 Harness 的产品规则。原始任务里没有明说,因此 Agent 如果猜错,不要立刻把它归为“模型不会写筛选”。它更可能暴露了知识与验收条件没有进入仓库。
如果你的 Agent 没有浏览器能力,它无法亲自完成第 3~7 项页面验证,这本身就是基线的一部分。你仍应由人手动检查,并记录“Agent 未获得用户界面反馈”。
检查完按 Ctrl+C 停止服务。
把失败按原因分类
回到 shelfcare-baseline.md,补上结果。可以直接使用下面的表:
## 运行结果
- 是否交付可见的标签筛选:是 / 否 / 部分
- 独立行为检查通过数:__/7
- 原有测试:通过 / 失败 / 未运行
- Agent 新增的验证:
- Agent 提供的完成证据:
## 过程数据
- 人工补充信息次数:
- 权限确认次数:
- 失败命令次数:
- 无效重试次数:
- 修改文件数:
- 是否检查实际页面:
- 是否检查 Git Diff:
## Harness 问题
- 知识:
- 工具:
- 环境:
- 反馈:
- 验证:
- 权限:
分类时不要把所有问题都写成“Agent 不够聪明”。下面是更具体的判断方式:
| 观察到的现象 | 更可能缺少什么 |
|---|---|
| 反复猜启动命令、测试命令 | 可发现的环境入口 |
| 不知道搜索、状态和标签怎样组合 | 产品规则与验收知识 |
| 改了页面但从未真正打开 | 浏览器或用户行为反馈 |
| 只运行语法检查就宣布完成 | 独立的功能验证 |
| 试图读取无关目录或使用外部服务 | 清楚且可执行的权限边界 |
| 已有完整信息和工具,仍无法实现基本逻辑 | 才更像能力、规划或实现问题 |
一次现象可能同时涉及几类。例如,Agent 没有打开页面,既可能是缺少浏览器工具,也可能是项目没有告诉它怎样启动应用。先记录证据,不急着追求唯一答案。
“做成功了”也有基线价值
如果 Agent 一次就通过全部七项检查,不需要为了配合教程而判它失败。继续看过程:它是否花了很多步骤猜命令,是否靠偶然猜中了组合规则,是否遗漏了自动化验证,下一次换个功能还能不能重复成功。
Harness Engineering 追求的不是把偶然成功包装成必然失败,而是让成功更可重复、失败更容易定位。
保存尝试,但把主线恢复到起点
无论结果完整与否,都把这次尝试保存在实验分支上:
git add .
git commit -m "baseline: agent tag-filter attempt"
如果 Agent 没有改动文件,Git 会提示没有内容可提交,这是正常情况。可以只保留 shelfcare-baseline.md 中的记录。
现在切回最初的 main 分支:
git switch main
git status --short --branch
预期位于 main,工作区干净,并且标签筛选功能不在主线代码中。baseline-attempt 分支保留了第一次尝试,第八篇复测时可以随时对比:
git diff main..baseline-attempt
这样做比运行 git reset --hard 更安全:尝试结果没有被删除,后续改造也从统一起点开始。
常见阻塞
端口 3000 已被占用
症状通常是 EADDRINUSE。可以先用另一个端口做预检:
macOS、Linux 或 Git Bash:
PORT=3100 node server.js
Windows PowerShell:
$env:PORT=3100
node server.js
然后打开 http://127.0.0.1:3100。记得在基线里写下端口变化;如果 Agent 不知道这一点,不要在它运行前悄悄替它修好。
页面能打开,但一直显示“正在读取书架”
先直接访问 http://127.0.0.1:3000/api/books。如果看不到包含 8 本书的 JSON,回终端查看服务是否仍在运行以及是否有错误。若 API 正常,再打开浏览器开发者工具的 Console,检查前端错误。
Git 切换分支时提示有未提交修改
不要强制切换。先运行 git status --short,确认修改属于本次实验,再提交到 baseline-attempt。基线的意义就是保留这些痕迹。
Agent 对项目做了过度改造
如果它引入框架、包管理器或大规模重写,也先不要手工修正。记录新增依赖、修改文件数和它给出的理由,提交到基线分支,再切回 main。修改范围失控本身就是值得改造的 Harness 信号。
本篇留下了什么
现在你应该有三样东西:
- 一份可以运行的 ShelfCare 主线仓库;
- 一个保留首次 Agent 尝试的
baseline-attempt分支; - 一份记录人工介入、失败命令、行为验收和六类问题的
shelfcare-baseline.md。
下一篇先不改命令和测试。我们会从最常见的缺口开始:把产品规则、架构和工作入口整理成 Agent 能发现的仓库知识,同时避免把 AGENTS.md 写成一本过期的百科全书。
04 让仓库知识对智能体可见
预计阅读与操作时间:40~50 分钟
本篇目标:为 ShelfCare 建立一套精简、可导航、能随代码一起维护的仓库知识结构,让 Agent 找得到产品规则、架构和当前开发入口。
配套代码说明
本篇加入仓库的是 assets/lesson-03-knowledge/ 下的四个 Markdown 文件,也是整个教程第一次为项目“写知识”。它们不是代码,但结构本身承载了第 03 篇的核心主张——知识分层、渐进式披露:
lesson-03-knowledge/
├── AGENTS.md # 入口地图:导航 + 当前命令 + 仓库边界
└── docs/
├── PRODUCT_RULES.md # 产品事实:状态取值、筛选 AND 组合、验收示例
├── ARCHITECTURE.md # 架构地图:数据流、目录职责、依赖方向
└── DEVELOPMENT.md # 开发事实:启动、测试、端口覆盖、故障入口
各文件实现的功能:
- AGENTS.md 是唯一入口,用 “Start here” 三个相对链接指向三份专题文档,并明确写着 “Read only the document relevant to the task. Do not load every document by default.”(按任务读取,不默认全载)。它还声明了几条仓库边界(不擅自加依赖、不改数据伪造功能、不访问仓库外文件),但此刻这些只是文字。
- PRODUCT_RULES.md 把标签筛选写成可判断的行为:标签选项来自数据、搜索/状态/标签用 AND 组合、清除一个条件不影响其他、空结果显示固定文案,并保留“科幻 → 两本书”“技术+在读 → 两本书”两个验收示例。
- ARCHITECTURE.md 先用一张 ASCII 数据流图画出
Browser → server.js → src/library.js → data/books.json的请求走向,再说明目录职责和三条依赖边界(浏览器不能直读data/、src/library.js不依赖浏览器、CSS 不承载行为)。 - DEVELOPMENT.md 记录“现在怎样做”:Node 版本要求、
node server.js、node --test tests/library.test.js、端口覆盖,以及EADDRINUSE等三个高频故障入口。
一个关键细节:这四份文件的内容与本篇之前 starter 的真实代码、数据、命令逐项一致——例如 PRODUCT_RULES.md 里“科幻”对应 data/books.json 的 book-001 和 book-006。本篇第七节会专门要求你验证这一点,因为“文档说一套、代码做一套”正是知识层最危险的失败。
第二篇结束时,shelfcare-lab 的 main 分支仍是干净的起始项目。你已经知道怎样启动和测试它,也在基线记录里写下了 Agent 不知道什么。
现在要做的不是把这些答案重新告诉下一次 Agent,而是把值得重复使用的知识放回仓库。以后换一个任务、开一个新会话,Agent 仍然能自己找到。
OpenAI 在 Harness Engineering 实践总结 中把这件事称为让仓库成为知识的记录系统。他们也试过把全部规则堆进一个巨大的 AGENTS.md,最后改为用短入口指向结构化文档。原因很实际:上下文有限,长手册会挤占任务和代码,所有规则都强调等于没有重点,而且一大块文字很难检查是否过期。
ShelfCare 规模很小,不需要照搬大型仓库的文档树。我们只建立四个文件:一张入口地图、产品规则、架构地图和本地开发说明。
先分清四种知识
在开始写文件前,先决定不同信息应该放在哪里。
| 知识 | 适合放在哪里 | 例子 |
|---|---|---|
| 长期工作规则与导航 | AGENTS.md |
从哪里读产品规则、修改后怎样验证 |
| 产品事实 | docs/PRODUCT_RULES.md |
标签与状态怎样组合 |
| 代码结构和依赖方向 | docs/ARCHITECTURE.md |
浏览器不能直接读取 data/ |
| 当前开发入口 | docs/DEVELOPMENT.md |
怎样启动、测试和处理端口冲突 |
还有两类内容不应该混进来:
- “本次增加标签筛选”属于一次任务,不是永久仓库规则;
- “先读第 38 行再修改第 72 行”属于临时实现计划,代码变化后很快失效。
入口文件越短,越容易稳定。细节进入专题文档,Agent 需要时再读,这就是渐进式披露:先看到地图,再按当前任务进入相关区域。
把知识文件加入主线
确认终端位于 shelfcare-lab,并且已经切回 main:
git status --short --branch
预期第一行是 ## main,下面没有未提交文件。如果你跳过了第二篇,可重新从 assets/shelfcare-starter 复制项目并建立初始提交;不要在带有基线功能修改的分支上继续。
课程已经在 assets/lesson-03-knowledge 中准备好四个知识文件。把它们复制到实验仓库。
macOS 或 Linux:
cp -R ../assets/lesson-03-knowledge/. .
Windows PowerShell:
Copy-Item -Recurse -Force ..\assets\lesson-03-knowledge\* .
复制完成后,项目根目录应多出:
AGENTS.md
docs/
├── ARCHITECTURE.md
├── DEVELOPMENT.md
└── PRODUCT_RULES.md
在 Windows PowerShell 中,* 不会复制以点开头的文件,但本次四个文件都不以点开头,因此结果相同。
把 AGENTS.md 写成地图,不是百科全书
打开根目录的 AGENTS.md。它只做四件事:
- 一句话说明 ShelfCare 是什么;
- 指向三个专题文档;
- 写出当前启动、测试和完成前检查;
- 说明仓库范围与高风险边界。
其中最重要的部分是导航:
## Start here
- Product behavior and acceptance examples: [docs/PRODUCT_RULES.md](docs/PRODUCT_RULES.md)
- Code layout and dependency direction: [docs/ARCHITECTURE.md](docs/ARCHITECTURE.md)
- Current local commands and troubleshooting: [docs/DEVELOPMENT.md](docs/DEVELOPMENT.md)
Read only the document relevant to the task. Do not load every document by default.
这里没有要求 Agent 每次先读完三篇文档。修一个 CSS 间距,不需要加载产品筛选规则;修改标签行为时,才需要阅读 PRODUCT_RULES.md。OpenAI 近期针对 Agent 指令的建议也强调按任务读取所需文档,避免让长期指令不断膨胀。官方说明
AGENTS.md 还写了几条仓库边界,例如不为简单功能擅自引入依赖、不修改数据来伪造功能效果、不访问仓库之外的文件。它们能让边界对 Agent 可见,但目前仍只是文字。第七篇会把能机械执行的部分升级成真正的检查。
不同 Agent 会不会自动读取它
AGENTS.md 是本教程选择的中立仓库入口,但不同编码 Agent 的自动发现规则并不完全相同。Codex 会读取作用域内的 AGENTS.md;其他工具可能使用自己的项目指令文件,或需要你在开场时说“先阅读 AGENTS.md”。
不要靠猜。查一下你所用 Agent 的项目指令文档,再做后面的检索测试。如果它不会自动加载,本教程后续任务统一在开头加一句:
请先阅读仓库根目录的 AGENTS.md,再开始处理任务。
这不会改变知识结构,只是告诉工具入口在哪里。
产品规则要写成可判断的行为
打开 docs/PRODUCT_RULES.md。它没有写“标签筛选要好用”这种无法验收的话,而是把规则写成 Agent 可以实现、人和测试可以判断的行为:
- 标签选项来自图书数据,不维护第二份硬编码列表;
- 搜索、阅读状态和标签条件使用 AND 组合;
- 清除一个条件时,其他条件继续生效;
- 没有结果时显示固定空状态;
- 修改筛选功能不能靠改
data/books.json伪造结果。
文档还保留了两个具体例子。例如,“技术 + 在读”应该只剩《设计数据密集型应用》和《程序员修炼之道》。例子不是多余解释,它把抽象规则变成了可复查的样本。
注意,我们没有把按钮颜色、控件必须放在第几列之类的实现偏好也写成产品规则。文档应该保护真正影响用户行为的事实,不是替 Agent 做完全部设计选择。
架构文档写边界,不逐文件复述代码
docs/ARCHITECTURE.md 先画出当前数据流:
Browser
├─ GET /, /app.js, /styles.css ──> server.js ──> public/
└─ GET /api/books ──────────────> server.js ──> src/library.js ──> data/books.json
接下来才说明目录职责和依赖方向。对于标签筛选任务,最关键的判断是:当前数据量很小,浏览器已经一次加载全部图书,所以 UI 筛选可以继续留在 public/app.js,不必为了一个下拉框新增服务端 API。
一份有用的架构文档应该回答“代码为什么在这里”和“哪些方向不允许”,而不是把每个函数名重新抄一遍。函数名改动频繁,边界相对稳定;前者更容易腐烂,后者更适合作为长期地图。
文档里目前写了三条边界:
- 浏览器代码不能直接读取
data/; src/library.js不能依赖浏览器代码;- CSS 不承载产品行为。
这些也暂时是说明,不是机器约束。先让 Agent 看得见,之后再根据真实风险决定哪些值得自动检查。
开发文档记录“现在怎样做”
docs/DEVELOPMENT.md 记录的是当前事实:Node.js 版本、启动命令、测试命令、端口覆盖和三个高频故障入口。
它与 AGENTS.md 中的命令看起来有一点重复,但职责不同:入口文件只给最短路径,开发文档负责解释预期结果和故障恢复。等第四篇建立统一脚本后,两处都要在同一次提交里更新。
这条约定已经写进 AGENTS.md:行为或结构变化时,同步更新对应文档。与其给所有文档加一个容易过期的“最后更新日期”,不如明确什么代码变化会触发它更新:
- 筛选行为变化,更新
PRODUCT_RULES.md; - 目录或依赖方向变化,更新
ARCHITECTURE.md; - 命令、环境变量或故障入口变化,更新
DEVELOPMENT.md。
日期只能说明有人动过文件,触发条件才能告诉维护者什么时候必须动它。
先检查文档是不是在说真话
知识进入仓库不代表它自动正确。先做三个层次的检查。
1. 检查链接目标存在
macOS 或 Linux:
for file in AGENTS.md docs/ARCHITECTURE.md docs/PRODUCT_RULES.md docs/DEVELOPMENT.md; do
test -f "$file" || exit 1
done
Windows PowerShell:
$files = @(
"AGENTS.md",
"docs/ARCHITECTURE.md",
"docs/PRODUCT_RULES.md",
"docs/DEVELOPMENT.md"
)
$files | ForEach-Object {
if (-not (Test-Path $_)) { throw "Missing file: $_" }
}
没有输出、退出码为 0,表示四个文件都存在。这只是最低限度,还不能证明内容正确。
2. 用真实代码核对关键陈述
逐项打开对应文件检查:
| 文档陈述 | 代码证据 |
|---|---|
/api/books 返回藏书 |
server.js 的路由 |
数据由 src/library.js 读取 |
server.js 顶部的 require |
数据文件是 data/books.json |
src/library.js 的 dataFile |
| 筛选发生在浏览器 | public/app.js 的 currentBooks() |
| 起始项目有两项测试 | tests/library.test.js |
不要因为课程给了现成文档就跳过这一步。真正迁移到自己的项目时,文档初稿常常来自旧 README、聊天记录或 Agent 总结,最容易在这里带入错误。
3. 运行文档声称可用的命令
node --test tests/library.test.js
应继续显示 2 个测试通过、0 个失败。再运行:
node server.js
打开 http://127.0.0.1:3000,确认仍显示 8 本书,然后按 Ctrl+C 停止。文档变更不应改变应用行为。
最后检查 Markdown 和空白错误:
git diff --check
git status --short --untracked-files=all
git diff --stat
git diff --check 应没有输出;git diff --stat 应只列出 AGENTS.md 和三个 docs/ 文件。如果出现应用代码,说明你没有从干净 main 开始,先用 git status 查清来源,不要直接删除。
做一次“知识检索测试”
现在启动你的编码 Agent,但明确要求它不要修改文件。使用下面这段任务:
不要修改任何文件。请根据仓库中的项目知识回答:
1. ShelfCare 当前怎样启动和运行测试?
2. 标签、状态和搜索条件怎样组合?
3. 标签筛选应该放在哪一层,为什么?
4. 哪些操作超出这个仓库的默认工作边界?
请为每个答案标出依据的文件路径;找不到时直接说明找不到,不要猜。
如果你的 Agent 不会自动读取 AGENTS.md,在最前面加上前文那句入口提示。
一个合格结果应该引用:
docs/DEVELOPMENT.md回答命令;docs/PRODUCT_RULES.md回答 AND 组合与动态标签;docs/ARCHITECTURE.md回答浏览器层;AGENTS.md回答仓库访问、依赖和危险操作边界。
回答内容还要与真实代码一致。只会复述文档却引用错路径,说明导航仍不清楚;不读文档、只凭代码猜产品规则,也不能算通过。
这项测试没有让 Agent 实现功能,却验证了一件更基础的事:换一个全新会话,它能否自行找到此前只存在于人脑和教程里的知识。
提交知识基线
确认检索测试通过后,提交这四个文件:
git add AGENTS.md docs
git commit -m "docs: add repository knowledge map"
git status --short --branch
预期仍在 main,工作区干净。此时不要重新执行标签筛选任务;我们还没解决开发入口、机器验证和页面反馈,过早复测会混淆每一层改造的作用。
常见阻塞
Agent 完全忽略 AGENTS.md
先确认它是否支持自动发现这个文件,以及终端的当前目录是否真在 shelfcare-lab。如果工具使用另一种项目指令文件,不必复制两套完整规则;保留 AGENTS.md 作为仓库地图,在工具专用入口中只写一行指向它。
文档链接存在,但 Agent 总是读错文件
检查入口说明是否写成“所有任务都先读全部文档”。如果是,改成按任务路由,例如“涉及用户行为时读 PRODUCT_RULES.md”。文件名也要表达用途,避免 notes.md、misc.md 这类无法判断内容的名称。
文档与代码发生冲突
不要默认代码或文档哪一个必然正确。运行现有行为,查看 Git 历史和测试,再决定真实产品契约。如果仍无法判断,应把冲突交给维护者,而不是让 Agent自行挑一个喜欢的答案。
想把所有基线观察都写进仓库
shelfcare-baseline.md 是这次课程实验记录,不是产品长期知识,继续留在仓库外即可。只有会影响后续任务的稳定事实,才进入仓库文档。
本篇留下了什么
ShelfCare 的代码还没有增加标签筛选,但项目已经多了一条清楚的知识路径:
AGENTS.md
├─ 用户行为 -> docs/PRODUCT_RULES.md
├─ 代码位置 -> docs/ARCHITECTURE.md
└─ 本地操作 -> docs/DEVELOPMENT.md
Agent 不再需要从文件名和零散代码中猜测全部事实。不过,它仍然要记住原始命令,而且这些命令没有统一的错误提示。下一篇我们会把 setup、dev、test 和 check 做成稳定入口,让人和 Agent 都能用同一种方式启动与验证项目。
05 让开发环境可以被智能体自己启动
预计阅读与操作时间:40~50 分钟
本篇目标:为 ShelfCare 建立跨平台的setup、dev、test和check四个稳定入口,让 Agent 能自行判断环境是否可用、怎样启动项目,以及基础检查是否通过。
配套代码说明
本篇加入 assets/lesson-04-runtime/,把上一篇“写在文档里的低层命令”升级成可执行的稳定入口:
lesson-04-runtime/
├── package.json # 四个 npm scripts + engines(node>=20)
├── scripts/
│ ├── setup.js # 环境自检:Node 版本、必需文件、数据可解析
│ └── check.js # 分阶段组合检查:环境→语法→测试
├── AGENTS.md # 同步更新:稳定工作流 setup→dev→test→check
└── docs/ # 同步更新 ARCHITECTURE.md、DEVELOPMENT.md
各文件实现的功能:
- package.json 只暴露四个稳定契约:
setup、dev、test、check。底层实现以后可变,但 Agent 始终敲同一条命令。engines声明node >=20,但真正的版本校验放在 setup 里,因为engines不保证所有环境都拒绝旧版本。 - scripts/setup.js 做三件事:① 用
process.versions.node检查主版本 ≥ 20;② 逐一确认 11 个必需文件(源码、数据、测试、知识文档)都存在;③ 解析data/books.json确认是非空数组。它只读、不安装、不改环境,可安全反复运行。 - scripts/check.js 用
spawnSync依次跑setup → 三个 JS 语法检查(--check) → 测试(--test)。每一阶段打印[check] <name>,任一阶段失败立即停止并返回对应退出码。关键设计:全部用 Node API 而非 shell 拼接,因此 Windows/macOS/Linux 走同一份实现。 - 同步更新的知识文件:
AGENTS.md不再教 Agent 记底层命令,而是给出npm run setup → npm run dev → npm test → npm run check的工作流;DEVELOPMENT.md同步替换成新入口。
值得注意的两个设计取舍:check 不启动长期服务、也不打开浏览器,所以可安全反复运行——但也因此只能证明“环境、语法、现有测试没问题”,还证明不了“标签筛选符合产品规则”,这个缺口正好引出下一篇。setup 刻意不自动装 Node、不改环境变量、不装系统软件,因为这些副作用不该藏在一个可被 Agent 随时触发的“准备”命令里。
第三篇已经把低层命令写进 docs/DEVELOPMENT.md。Agent 现在能查到 node server.js 和 node --test tests/library.test.js,但这仍然有两个问题。
第一,文档和命令可能分叉。将来测试目录或启动方式变化,每个 Agent 都要重新理解内部细节。第二,命令失败时,原始错误不一定告诉它应该检查什么。一个稳定入口的价值,不是少敲几个字符,而是把项目约定和诊断信息放进可执行代码。
这一篇不会引入构建工具或第三方依赖。ShelfCare 很小,用 Node.js 和 npm 自带能力就够了。
四个入口分别解决什么
先确定它们的职责,避免做出四个名字不同、内容重复的脚本。
| 入口 | 回答的问题 | 是否持续运行 |
|---|---|---|
npm run setup |
当前机器具备运行项目的基本条件吗 | 否 |
npm run dev |
怎样启动本地应用 | 是,直到按 Ctrl+C |
npm test |
当前自动化测试是否通过 | 否 |
npm run check |
提交前的基础检查是否全部通过 | 否 |
setup 不是“盲目安装一切”。本项目没有第三方依赖,它只检查 Node 版本、必要文件和示例数据,并给出下一步。这样的脚本可以重复运行,也不会悄悄改变机器状态。
check 负责组合已有检查,但不假装覆盖还不存在的能力。到本篇结束,它能证明环境、JavaScript 语法和现有测试没有问题,却还不能证明标签筛选符合产品规则。这个缺口会自然引出第五篇。
加入运行资产
确认你位于 shelfcare-lab 的 main 分支,并且第三篇的知识文件已经提交:
git status --short --branch
git log -1 --oneline
工作区应当干净,最近提交应是类似 docs: add repository knowledge map。如果你跳过了第三篇,先按上一课复制 assets/lesson-03-knowledge,否则 setup 会明确报告缺少知识文件。
把本篇资产复制进仓库。
macOS 或 Linux:
cp -R ../assets/lesson-04-runtime/. .
Windows PowerShell:
Copy-Item -Recurse -Force ..\assets\lesson-04-runtime\* .
这次会新增:
package.json
scripts/
├── check.js
└── setup.js
同时更新 AGENTS.md、docs/ARCHITECTURE.md 和 docs/DEVELOPMENT.md。这不是顺手改文档,而是在兑现上一课的规则:项目命令和目录变化时,同一变更必须更新知识来源。
package.json 只暴露稳定契约
打开 package.json,核心只有四个脚本:
{
"scripts": {
"setup": "node scripts/setup.js",
"dev": "node server.js",
"test": "node --test",
"check": "node scripts/check.js"
}
}
以后底层实现可以变化。例如测试可能从 Node 内置测试迁移到其他框架,但 Agent 仍然运行 npm test。稳定入口把“怎样做”藏在项目内部,把“要做什么”留给使用者。
这里选择 npm scripts 还有一个朴素原因:Node.js 发行版通常同时提供 npm,同一命令可以在 macOS、Linux 和 Windows 使用。我们没有用 Bash 脚本作为唯一入口,否则 PowerShell 用户又需要另一套实现。
package.json 的 engines 写了 node >=20,但它本身不保证所有环境都会拒绝旧版本。因此真正的版本检查仍放在 scripts/setup.js 中。
setup 要检查环境,也要给出下一步
运行:
npm run setup
预期看到类似:
> shelfcare@0.1.0 setup
> node scripts/setup.js
[setup] Node 20.x.x: ok
[setup] Required files: 11/11
[setup] Sample data: 8 books
[setup] Ready. Run `npm run dev` to start ShelfCare.
具体 Node 小版本以你的机器为准。关键是四行都出现,并且命令以退出码 0 结束。
打开 scripts/setup.js,可以看到它做了三类检查:
process.versions.node的主版本至少是 20;- 启动、源码、数据、测试和知识文件都存在;
data/books.json是可解析的非空数组。
它没有自动下载 Node、修改环境变量或安装系统软件。这些操作副作用较大,也经常需要管理员权限,不应该藏在一个可被 Agent 随时运行的“准备”命令里。
观察一次有用的失败
不用破坏文件也能检查错误路径。切到项目父目录再运行脚本:
cd ..
node shelfcare-lab/scripts/setup.js
脚本本身会根据所在文件定位仓库,因此仍应通过。这说明它没有依赖终端恰好位于某个绝对路径。
真正给使用者的入口仍要求从仓库根目录运行 npm run setup,因为 npm 需要先找到 package.json。回到项目:
cd shelfcare-lab
Windows PowerShell 同样使用 Set-Location .. 和 Set-Location shelfcare-lab 即可。
dev 入口必须给出可观察状态
运行:
npm run dev
终端应打印:
ShelfCare running at http://127.0.0.1:3000
而且进程不会立刻结束。这两点共同说明本地服务正在监听:一条明确地址让人和 Agent 知道下一步去哪里,持续运行则说明这不是一次性构建命令。
打开该地址,确认仍能看到 8 本书,然后按 Ctrl+C 停止服务。
如果默认端口被占用,跨平台差异只留在设置环境变量这一步。
macOS、Linux 或 Git Bash:
PORT=3100 npm run dev
Windows PowerShell:
$env:PORT=3100
npm run dev
无论端口怎样变化,项目入口仍是 npm run dev。
区分 test 和 check
先运行当前测试:
npm test
预期是 2 个测试通过、0 个失败。npm test 适合开发过程中快速反馈;Node 的测试发现机制会自动查找符合命名规则的测试文件,因此以后新增测试不需要不断修改脚本参数。
再运行完整基础检查:
npm run check
输出会按阶段展开:
[check] environment
...
[check] server syntax
[check] library syntax
[check] browser syntax
[check] tests
...
[check] All checks passed.
scripts/check.js 没有把所有命令拼成一条很长的 shell 字符串,而是使用当前 Node 可执行文件逐步启动检查。这样有三个好处:
- Windows、macOS 和 Linux 走同一份实现;
- 输出会明确停在哪个阶段;
- 一个阶段失败后立即停止,不用从后续噪声里找第一处错误。
现在的 check 顺序是:环境检查、三份 JavaScript 语法检查、自动测试。它没有启动长期服务,也没有打开浏览器,因此可以安全地反复运行。
让 Agent 自己发现入口
更新后的 AGENTS.md 不再教 Agent 记住底层命令,而是给出稳定工作流:
npm run setup -> npm run dev -> npm test -> npm run check
你可以开一个新 Agent 会话做只读检查:
不要修改文件。请检查 ShelfCare 的本地开发入口:
1. 验证环境是否就绪;
2. 列出可用的项目脚本;
3. 运行不会长期占用终端的自动检查;
4. 告诉我哪些用户行为仍未被这些检查覆盖。
合格结果应当:
- 运行
npm run setup; - 通过
npm run或package.json发现四个脚本; - 运行
npm run check; - 明确指出现有检查没有验证浏览器中的标签筛选行为。
最后一点很关键。好的 Harness 不只是给 Agent 一条绿色命令,也让它知道绿色结果的证明范围。把“语法和两项数据测试通过”说成“标签筛选已经正确”,仍然是假完成。
独立检查实际改动
先看文件范围:
git status --short
git diff --stat
git diff --check
预期新增 package.json、scripts/setup.js、scripts/check.js,并修改三份知识文件。docs/PRODUCT_RULES.md 没有变化,因为本篇没有改变产品行为。
再完整运行一次:
npm run setup
npm test
npm run check
三个命令都应以退出码 0 结束。npm run dev 已在前面通过浏览器独立确认,不要把长期运行命令塞进 check 后让自动流程一直挂住。
确认无误后提交:
git add package.json scripts AGENTS.md docs/ARCHITECTURE.md docs/DEVELOPMENT.md
git commit -m "build: add stable local project commands"
git status --short --branch
预期位于 main 且工作区干净。
常见阻塞
PowerShell 提示不能运行 npm.ps1
这是 PowerShell 执行策略拦截了同名脚本,不代表 ShelfCare 有问题。可以在当前命令中使用 npm.cmd:
npm.cmd run setup
npm.cmd run check
不要为了这门课全局关闭系统执行策略。
npm 提示找不到 package.json
先运行 pwd,Windows PowerShell 使用 Get-Location,确认当前目录是 shelfcare-lab。再运行目录列表检查根目录是否真的有 package.json。这是工作目录问题,不要重复安装 npm。
setup 报 Required files are missing
错误后面会列出缺少的相对路径。若缺少 AGENTS.md 或 docs/,说明第三篇资产没有复制完整;若缺少源码或数据,重新从 Git 状态与历史确认文件为何消失,不要创建空文件骗过检查。
check 停在 browser syntax
先单独运行:
node --check public/app.js
Node 会给出更集中的语法位置。修复第一处语法错误,再重新运行 npm run check。不要从 check.js 里删除这个阶段。
npm test 显示的测试数不是 2
如果你还没有新增测试,先运行 git status 检查是否把其他实验文件留在 tests/ 下。Node 会自动发现测试文件,多出来的临时测试也会参与执行。若是你主动新增的测试,则以全部测试通过为准,不必追求固定数量。
本篇留下了什么
ShelfCare 现在有了四个跨平台项目入口:
setup:确认能不能工作
dev:启动可操作的应用
test:获得快速自动反馈
check:完成提交前的基础检查
Agent 不必再从源码猜命令,也不会只得到一串没有阶段名称的失败输出。但 npm run check 目前只能证明“环境和现有代码没有明显基础错误”,还不能证明“标签筛选满足产品规则”。下一篇,我们会把完成标准变成机器可重复验证的行为,让 Agent 的“做完了”必须带着证据。
06 把完成了变成机器可验证的结果
预计阅读与操作时间:45~60 分钟
本篇目标:把标签筛选的产品规则转成一条可重复运行的验收命令,并建立“先复现、再修改、再验收、再回归”的 Definition of Done。
配套代码说明
本篇加入 assets/lesson-05-validation/,为“标签筛选”建立一条任务专用、先红后绿的验收链:
lesson-05-validation/
├── acceptance/
│ └── tag-filter.contract.js # 八项验收契约(故意不用 .test.js 后缀)
├── scripts/
│ └── accept-tag-filter.js # 验收执行器:受保护文件比较 + 跑契约
├── package.json # 新增 accept:tag-filter 入口
├── AGENTS.md # 声明受保护文件,禁止实施者自行修改
└── docs/DEVELOPMENT.md # 加入六步 Definition of Done
各文件实现的功能:
- acceptance/tag-filter.contract.js 是核心,包含八项检查:① 页面存在
id="tag-filter"控件及“标签”文本;②public/filter-books.js导出filterBooks纯函数接口;③科幻恰好返回book-001、book-006;④技术+reading返回book-003、book-008;⑤all+planned返回book-004、book-007(专门防“清标签连状态一起清”的错);⑥public/app.js真的读取#tag-filter并调用filterBooks;⑦ 不匹配关键词必须排除结果;⑧ 非空关键词、状态与标签共同限制结果。它读取真实data/books.json但不修改数据,且故意不用.test.js后缀,避免被npm test的自动发现误当成已实现功能。 - scripts/accept-tag-filter.js 是执行器:先做受保护文件比较(
git diff --exit-code main -- acceptance/tag-filter.contract.js docs/PRODUCT_RULES.md,不一致就失败),再用node --test显式运行契约文件。它支持ACCEPTANCE_BASE环境变量覆盖基线分支名。 - package.json 新增
accept:tag-filter,与check刻意分开——check是“当前主线应始终满足的回归基线”,accept是“尚未完成的任务目标”,实现前就应该失败。 - AGENTS.md / DEVELOPMENT.md 落地两层防作弊:前者把验收契约和产品规则标为受保护文件;后者把完成标准写成六步 DoD(复现失败→最小实现→不改受保护输入→专项验收通过→回归通过→查 Diff 并人工验证)。
这里最关键的设计是“执行者不能移动终点”:因为 Agent 通常同时拥有仓库写权限,技术上它也能改测试。本篇用三层防护(受保护文件声明 + 运行前与 main 比较 + 人/CI 审查最终 Diff)来约束这件事,同时也诚实地说明——本地脚本是护栏,不是不可攻破的安全边界。
到目前为止,ShelfCare 的 npm run check 可以证明环境、语法和现有两项测试正常。但如果 Agent 只给页面加了一个没有作用的下拉框,这条命令仍会变绿。
这不是测试数量少这么简单,而是验证对象错了:我们验证了项目“没有明显坏掉”,却没有验证任务“真的完成”。
这一篇先不实现标签筛选。我们会把预期结果写成独立验收,让它在当前 main 上明确失败。第八篇 Agent 接到原始任务时,必须把这条红色验收变绿,同时保持回归检查通过。
先分清三层证据
一个功能任务通常需要不止一种检查。
| 层次 | 证明什么 | ShelfCare 中的例子 |
|---|---|---|
| 单元或数据测试 | 小块逻辑与数据约束正确 | 图书数据可读、状态值合法 |
| 任务验收 | 本次需求的关键规则被满足 | 标签与搜索、状态使用 AND 组合 |
| 用户行为验证 | 真正运行时,用户能完成操作 | 页面能选择标签并看到正确书目 |
npm test 目前主要是第一层。本篇增加第二层。第三层需要页面和运行状态反馈,放到下一篇完成。
这三层不是“越往下越高级”,而是证明范围不同。单元测试很快,适合定位;任务验收紧贴需求;真实用户行为最接近最终体验,但执行成本也更高。可靠的完成判断通常需要组合它们。
从产品规则提取验收条件
先打开 docs/PRODUCT_RULES.md。标签筛选至少有这些可判断条件:
- 页面存在标签筛选控件;
- 标签值
科幻只保留两本科幻书; - 标签
技术与状态reading同时生效; - 标签恢复为
all时,只移除标签条件; - 页面代码真的读取标签控件并调用统一筛选函数。
这里故意没有把“控件必须是绿色”“必须放在搜索框右边”写进验收。那些不是当前产品契约,写进去只会把一种实现偏好伪装成正确性。
同样,验收也不应该只检查某段字符串存在。一个静态文本里出现“科幻”,不能证明筛选逻辑工作。我们会把核心逻辑约定成一个纯函数接口:
filterBooks(books, { keyword, status, tag })
纯函数不依赖 DOM,Node 测试可以直接输入图书和条件,再检查输出 ID。页面仍要负责读取控件和渲染结果,但最容易出错的组合逻辑可以快速、确定地验证。
加入一条任务专用验收命令
确认当前位于 shelfcare-lab 的 main 分支,且第四篇已经提交:
git status --short --branch
git log -1 --oneline
工作区应干净,最近提交类似 build: add stable local project commands。
复制本篇资产。
macOS 或 Linux:
cp -R ../assets/lesson-05-validation/. .
Windows PowerShell:
Copy-Item -Recurse -Force ..\assets\lesson-05-validation\* .
新增的核心文件是:
acceptance/tag-filter.contract.js
scripts/accept-tag-filter.js
同时更新 package.json、AGENTS.md 和 docs/DEVELOPMENT.md。新的项目入口是:
npm run accept:tag-filter
它与 npm run check 故意分开。check 代表当前主线应始终满足的回归基线;accept:tag-filter 代表尚未实现的任务目标,在开始工作前就应该失败。如果现在把它塞进 check,后续每一篇 Harness 改造都会因为“功能还没做”而无法获得绿色基线。
先运行一次失败的验收
运行:
npm run accept:tag-filter
当前代码没有 #tag-filter 控件,也没有 public/filter-books.js,所以命令应以非零退出码结束。示例输出会包含多项失败,例如:
not ok ... the page exposes a tag-filter control
not ok ... the browser filter module exports the agreed interface
...
fail 6
Node 版本不同,行号和排版可能不同。判断成功复现的标准不是固定的失败数,而是:失败原因明确指向尚未实现的标签控件、筛选接口和页面集成。
如果这条命令现在反而通过,说明你的 main 很可能还残留第二篇基线分支的实现。运行:
git status --short --branch
git log --oneline --decorate -5
先确认自己没有停在 baseline-attempt,也没有把基线实现合并进 main。
红色验收不是项目损坏
运行下面的回归检查:
npm run check
它应继续以 [check] All checks passed. 结束。这两个结果可以同时成立:
当前项目基线:健康
未来任务目标:尚未完成
这比“所有测试必须永远是绿色”更接近真实开发。关键是团队要清楚哪条命令代表当前回归,哪条命令代表正在实现的目标。
验收测试到底检查了什么
打开 acceptance/tag-filter.contract.js。它使用真实的 data/books.json,但不修改数据,包含八项检查。文件故意不使用 .test.js 后缀,避免 Node 的默认测试发现把尚未实现的任务契约混入 npm test;专项入口会显式运行它。
1. 页面有可定位的标签控件
测试要求 public/index.html 中存在 id="tag-filter",并有“标签”文本。稳定 ID 既方便 JavaScript 绑定,也方便后续浏览器工具定位。
2. 筛选逻辑有明确接口
测试要求 public/filter-books.js 导出 filterBooks。这个文件当前不存在,所以它会失败。明确接口让 Agent 不必猜验收怎样调用功能,也把组合逻辑从 DOM 事件里分离出来。
3. 用固定样本验证产品规则
测试直接检查:
科幻 -> book-001、book-006
技术 + reading -> book-003、book-008
all + planned -> book-004、book-007
最后一个例子专门防止一种常见错误:用户清除标签后,状态筛选也被一起清空。
4. 页面接入了新逻辑
测试还要求 public/app.js 读取 #tag-filter,并调用 filterBooks(...)。这只是轻量集成信号,不能替代真实浏览器行为;它至少能阻止 Agent 只写一个从未被页面使用的纯函数。
这组验收没有规定 CSS 类名、模块内部变量或代码行数。它固定的是输入、输出和必要连接点,把实现空间留给 Agent。
Definition of Done 不等于“一条测试绿了”
更新后的 docs/DEVELOPMENT.md 为标签任务定义了六步完成标准:
1. 复现验收失败
2. 按产品规则做最小实现
3. 不修改受保护的验收输入
4. npm run accept:tag-filter 通过
5. npm run check 通过
6. 检查 Diff,并手动验证受影响页面
它对应一条完整证据链:
先复现 -> 证明测试能发现缺失
再实现 -> 只改变功能代码
专项验收 -> 证明任务规则满足
回归检查 -> 证明旧能力没被破坏
Diff 审查 -> 证明修改范围合理
运行验证 -> 证明真实页面可用
只跑最后一条绿色测试不够。如果没有先看见它失败,就无法确认测试是否真的覆盖了缺失功能;如果不跑回归,可能新筛选正确但旧搜索坏了;如果不看 Diff,Agent 可能通过修改数据或验收标准获得绿色结果。
防止“修改考试题获得满分”
执行功能的 Agent 通常同时拥有仓库写权限,因此它技术上也能修改测试。并非每次修改测试都是作弊——需求变化时测试当然要变——但执行者不能在没有说明的情况下自行降低标准。
本篇用了三层保护。
第一层:把验收输入明确标成受保护
AGENTS.md 写明,实施标签筛选时不得修改:
acceptance/tag-filter.contract.js
docs/PRODUCT_RULES.md
如果产品契约确实要变,Agent 应停止并请求评审,而不是一边改实现一边移动终点。
第二层:运行前与 main 比较
scripts/accept-tag-filter.js 会运行类似下面的 Git 比较:
git diff main -- acceptance/tag-filter.contract.js docs/PRODUCT_RULES.md
在未来的任务分支中,只要受保护文件与 main 不同,验收就先失败。如果你的基线分支名不是 main,可以显式指定:
macOS、Linux 或 Git Bash:
ACCEPTANCE_BASE=your-base npm run accept:tag-filter
Windows PowerShell:
$env:ACCEPTANCE_BASE="your-base"
npm run accept:tag-filter
本教程统一使用 main,通常不需要设置。
第三层:人或 CI 审查最终 Diff
本地保护脚本也在同一个仓库里,拥有完全写权限的 Agent 理论上仍可修改它。真正高风险项目应该让 CI 或外部评审从可信基线运行检查,并限制谁能改验收规则。
ShelfCare 不搭建远程 CI,但仍保留最重要的一步:完成前运行 git diff main...HEAD,确认验收与产品规则没有被悄悄弱化。Harness 的边界应该诚实;本地脚本是护栏,不是不可攻破的安全边界。
验证这条验收链路本身
现在做四个检查。
第一,确认新测试文件语法正确:
node --check acceptance/tag-filter.contract.js
node --check scripts/accept-tag-filter.js
两条命令都应没有输出并返回 0。
第二,确认专项验收按预期失败:
npm run accept:tag-filter
失败应来自缺少功能,而不是 JSON 解析、路径拼写或测试脚本语法错误。
第三,确认主线回归仍通过:
npm run check
第四,先把本篇文件加入暂存区,再查看完整改动范围。这样新文件也会进入 Diff;单纯运行 git diff 不会显示尚未跟踪的文件。
git add package.json scripts/accept-tag-filter.js acceptance AGENTS.md docs/DEVELOPMENT.md
git diff --cached --check
git diff --cached --stat
git diff --cached -- acceptance/tag-filter.contract.js docs/PRODUCT_RULES.md
最后一条应显示新加入的验收文件,而 docs/PRODUCT_RULES.md 没有变化。我们把已有产品规则机器化,没有趁机改需求。若发现不该提交的文件,可以用 git restore --staged <路径> 将它移出暂存区,不会删除工作区内容。
确认后提交:
git commit -m "test: add tag-filter acceptance contract"
git status --short --branch
提交后再次运行 npm run accept:tag-filter,它仍应因为功能缺失而失败,但不再提示受保护文件与 main 不同——因为验收契约本身已经成为 main 的基线。
让 Agent 读懂完成标准
开一个新的只读 Agent 会话,给出:
不要修改文件。请说明“按标签筛选藏书”任务的 Definition of Done:
1. 开始实现前应该运行什么;
2. 哪些文件不能由实施者自行修改;
3. 完成前要提供哪些自动化与人工证据;
4. 当前 main 为什么允许一条验收失败。
请引用仓库内的依据。
合格回答应引用 AGENTS.md、docs/DEVELOPMENT.md、docs/PRODUCT_RULES.md 和 acceptance/tag-filter.contract.js,并分清:
npm run check是当前回归基线;npm run accept:tag-filter是未完成任务的目标;- 第八篇实现任务后,两者都必须通过;
- 页面行为还需要真实运行证据。
如果 Agent 把当前红色验收当成“仓库已经坏了”,说明文档没有让这两个层次足够清楚。先修知识入口,不要靠每次对话口头解释。
常见阻塞
验收失败,但原因是 Cannot find module
如果缺少的是 public/filter-books.js,这是预期的功能缺口;测试会把它转成明确断言。如果缺少的是 data/books.json 或其他已有文件,先运行 npm run setup,这属于项目基线损坏,不是正确的红色验收。
验收先提示 protected inputs differ from main
运行:
git diff main -- acceptance/tag-filter.contract.js docs/PRODUCT_RULES.md
如果改动只是本篇文件尚未提交,先按步骤提交验收契约;如果是在功能分支中被 Agent 修改,审查差异并恢复未经批准的改动。不要为了让命令变绿而关闭比较。
当前仓库的默认分支不叫 main
本教程第二篇已经统一成 main。若你把方法迁移到自己的仓库,设置 ACCEPTANCE_BASE 指向真实基线分支,或在脚本配置中明确默认值。不要在每个人的电脑上靠不同的隐含分支名运行。
想把所有未来需求都提前写成失败测试
不要。只为已经确认、即将实施的任务建立验收。大量长期失败的测试会让红色失去信号价值,也容易在需求变化后变成垃圾。验收是任务契约,不是愿望清单。
Agent 建议直接把验收加入 npm run check
在功能实现完成并进入主线后,可以考虑把稳定的标签回归测试纳入普通测试;但当前阶段还不行。否则第六、七篇只改 Harness 也会一直失败。何时从“任务验收”升级为“长期回归”,应该在功能合并时决定。
本篇留下了什么
ShelfCare 现在同时拥有两条含义明确的验证线:
npm run check
-> 当前主线是否健康
npm run accept:tag-filter
-> 标签筛选任务是否达到完成标准
第二条目前应该是红色,而且失败原因可解释。我们还保护了产品规则和验收输入,避免执行 Agent 通过移动终点获得“完成”。
不过,这条验收仍然没有真正点击网页。它验证了控件标记、纯筛选逻辑和页面接入信号,却看不到浏览器控制台、运行日志和用户实际看到的结果。下一篇会把这些运行状态暴露出来,让 Agent 能从“页面有问题”一路追到可检查的证据。
验收链补充检查
在提交固定验收资产并确认基线存在后,先跑正常实现,再在实验副本中暂时忽略 keyword 条件;新增的非空关键词/无匹配关键词断言必须失败。恢复实现后重新通过。子进程被信号终止、启动失败或基线不存在都应返回非零,不能打印成功。首次接入先按本章提交验收契约,再运行受保护基线比较。
07 让界面日志和运行状态对智能体可读
预计阅读与操作时间:50~65 分钟
本篇目标:让 ShelfCare 暴露健康状态、结构化请求日志、浏览器事件和可定位的 DOM 状态,并用一个可控搜索故障完成从用户现象到代码位置的诊断。
配套代码说明
本篇加入 assets/lesson-06-observability/,为“运行中的应用”补齐四类证据。它改动最多,但每一处都对应一个可诊断的问题:
lesson-06-observability/
├── server.js # 结构化 JSON 日志 + /health 端点 + 请求 ID
├── scripts/
│ ├── doctor.js # 终端健康检查:/health + /api/books
│ ├── setup.js # 必需文件清单加入 OBSERVABILITY.md、doctor.js
│ └── check.js # 新增 diagnostic syntax 阶段
├── public/
│ ├── index.html # 加入 #runtime-status 状态节点
│ └── app.js # 浏览器事件日志 + 显式故障注入
├── docs/OBSERVABILITY.md # 运行证据地图
└── package.json # 新增 npm run doctor
各文件实现的功能:
- server.js 把日志改成稳定的 JSON 事件:
server_started(含url、pid)、request_completed(含requestId、method、path、statusCode、durationMs)、request_failed(含同一个requestId,可与 500 响应关联)。新增GET /health返回{"status":"ok","books":8}。日志只记请求元数据,不记搜索词、图书内容、Cookie 或文件路径。 - scripts/doctor.js 是终端侧诊断入口:读取
APP_URL(默认127.0.0.1:3000),依次请求/health和/api/books,校验两者图书数一致后输出“Server-side diagnostics passed.”。它明确只覆盖服务端,不假装验证了浏览器。 - public/index.html 加入一个视觉上隐藏、但语义可读的
#runtime-status节点:data-state(loading/ready/error)、data-visible-count、data-filters,且是aria-live区域,对辅助技术同样有意义。 - public/app.js 在浏览器侧补上过程证据:
books_loaded、books_rendered(含totalCount、visibleCount、filters)等 JSON 事件;currentBooks()里实现了一个显式故障注入——仅当 URL 带?fault=search-empty且搜索词非空时,把关键词替换成不可能命中的值,并发出client_fault_active警告。这个故障是 opt-in 的,正常地址不受影响,专用于练习“从现象追到代码”的诊断闭环。 - docs/OBSERVABILITY.md 是一张“证据地图”:把每个问题(服务是否启动、请求是否到达、能否读数据、浏览器加载了什么、用了什么筛选条件)对应到具体的日志事件或 DOM 属性。
整套设计的核心是“证据相互印证、互不替代”:/health 返回 200 不代表搜索正确,一张截图也看不出控制台错误;只有把服务端日志、DOM 状态、浏览器事件串起来,才能从“页面显示 0 本书”一路定位到 public/app.js 的 currentBooks()。
第五篇给标签任务加了机器验收,但验收脚本仍然只看文件和纯逻辑。真实页面出问题时,Agent 还可能面对一句非常模糊的报告:“我一搜索,书全没了。”
仅有源码时,它只能猜:是数据没读到,API 返回空数组,还是浏览器筛选错了?如果每次都要人替它打开页面、复制日志、解释当前状态,Agent 就没有真正接手运行中的应用。
OpenAI 的 Harness Engineering 案例中,团队不仅让 Codex 读代码,还让每个工作区的应用、DOM、截图、日志和指标对 Agent 可用。这样 Agent 才能复现用户现象并验证修复。官方案例
ShelfCare 不需要搭一套监控平台。我们只增加足够完成本地诊断的四类证据。
不同证据回答不同问题
先别急着“多打日志”。诊断时最有用的是知道每类证据能证明什么。
| 证据 | 最适合回答 | 不能单独证明 |
|---|---|---|
| 截图 | 用户看到了什么、布局是否异常 | 元素语义、请求原因、内部状态 |
| DOM 与可访问性状态 | 控件、文本、属性和当前页面状态 | 服务端为何返回这些数据 |
| 浏览器控制台 | 前端加载、事件、异常和筛选过程 | 服务是否整体健康 |
| 服务端日志 | 请求是否到达、状态码、耗时和错误 | 用户最终看到什么 |
| 健康与诊断命令 | 关键依赖能否工作、数据是否可读 | 每一条用户路径都正确 |
它们应该相互印证,而不是互相替代。/health 返回 200,不代表搜索正确;一张看起来正常的截图,也不能证明浏览器控制台没有错误。
加入可观测性资产
确认 shelfcare-lab 位于干净的 main,第五篇验收契约已经提交:
git status --short --branch
git log -1 --oneline
复制本篇资产。
macOS 或 Linux:
cp -R ../assets/lesson-06-observability/. .
Windows PowerShell:
Copy-Item -Recurse -Force ..\assets\lesson-06-observability\* .
主要变化包括:
server.js 结构化启动、请求与错误日志;/health
scripts/doctor.js 从终端检查健康与图书 API
public/index.html #runtime-status 状态节点
public/app.js 浏览器事件与可控故障
docs/OBSERVABILITY.md 运行证据地图
package.json 新增 npm run doctor,setup 与 check 也同步知道这些新文件。产品规则没有改变,标签筛选依然尚未实现。
先看服务端是否“活着”
在第一个终端运行:
npm run dev
启动信息现在是一行 JSON,示例:
{"timestamp":"2026-09-23T12:00:00.000Z","level":"info","event":"server_started","url":"http://127.0.0.1:3000","pid":12345}
时间和进程号会不同。结构化日志的价值不是看起来高级,而是字段稳定:Agent 可以直接找 event=server_started 和 url,不用从不同措辞里猜地址。
保持服务运行,在第二个终端进入同一仓库,运行:
npm run doctor
预期输出:
[doctor] Target: http://127.0.0.1:3000
[doctor] Health: ok; books=8
[doctor] API: ok; returned 8 books
[doctor] Server-side diagnostics passed.
第一个终端同时会多出两条 request_completed 日志,分别对应 /health 和 /api/books,包含状态码、耗时与请求 ID。
这条证据链说明:进程已启动,服务能读取 data/books.json,图书 API 返回 8 本书。它仍没有证明浏览器渲染或搜索逻辑正确,doctor 也明确把自己称为 server-side diagnostics。
使用自定义端口
如果服务运行在 3100,诊断命令也要指向同一地址。
macOS、Linux 或 Git Bash:
APP_URL=http://127.0.0.1:3100 npm run doctor
Windows PowerShell:
$env:APP_URL="http://127.0.0.1:3100"
npm run doctor
若忘记同步,doctor 会明确报告连接失败,而不是笼统地说项目坏了。
读懂结构化请求日志
浏览器打开 http://127.0.0.1:3000。服务端终端会出现若干日志,例如:
{"level":"info","event":"request_completed","requestId":"8a2f91c0","method":"GET","path":"/api/books","statusCode":200,"durationMs":1.2}
日志没有记录搜索词、图书内容、Cookie 或文件绝对路径,只保留本地诊断所需的请求元数据。即便是个人项目,也不要养成把完整请求、凭据和用户输入全部打印出来的习惯。
如果 /api/books 读取数据时抛出异常,服务端会先产生 request_failed,返回的 500 响应中也包含同一个 requestId。这样 Agent 可以把一次失败响应与对应日志连起来。
当前应用很小,日志直接写终端就够了。更大项目可以把同样的 JSON 行交给日志系统,但核心仍然是稳定事件名、可关联 ID 和必要字段,不是具体平台。
让页面状态进入 DOM
打开浏览器开发者工具的 Elements 面板,搜索 runtime-status。你会看到一个视觉上隐藏的节点,加载完成后类似:
<p
id="runtime-status"
data-state="ready"
data-visible-count="8"
data-filters='{"keyword":"","status":"all"}'
>
应用就绪,显示 8 本书
</p>
它没有为了自动化污染可见界面,同时也不是毫无语义的隐藏标记:节点使用 aria-live,加载和错误状态可以被辅助技术感知。
浏览器工具或 DOM 快照现在可以直接回答:
- 应用仍在 loading、已经 ready,还是进入 error;
- 当前渲染多少本书;
- 搜索和状态筛选的当前值是什么。
这比让 Agent 根据截图数卡片可靠得多,也比从内部变量猜页面状态更接近用户实际看到的结果。
浏览器事件补上过程证据
切到开发者工具的 Console,刷新页面。应看到 JSON 格式的事件:
{"source":"browser","level":"info","event":"books_loaded","count":8}
{"source":"browser","level":"info","event":"books_rendered","totalCount":8,"visibleCount":8,"filters":{"keyword":"","status":"all"}}
输入“Martin”,第二条事件的 visibleCount 应变成 2,filters.keyword 应是 martin。
DOM 状态告诉你“现在是什么”,事件日志告诉你“怎样走到这里”。两者结合,Agent 可以判断数据已经加载,但某次筛选之后可见数量异常。
如果你的编码 Agent 没有浏览器或 DOM 工具,不要假装它已经拥有这些证据。你可以由人打开页面并把相关 DOM、控制台事件或截图交给它;也可以为工具接入受控浏览器。npm run doctor 只能覆盖服务端,不能替代这一层。
注入一个可重复的搜索故障
现在做本篇的核心练习。不要修改代码,直接打开:
http://127.0.0.1:3000/?fault=search-empty
页面初始仍显示 8 本书。输入“Martin”后,页面会错误地显示 0 本,并出现空状态。
这个故障通过查询参数显式开启,只影响当前 URL;正常地址不受影响。这样每个人都能复现同一种现象,也能随时移除参数恢复。
按下面的顺序收集证据,不要一看到零结果就直接搜索代码。
第一步:确认用户现象
页面结果摘要应为“共找到 0 本书”。截图可以证明视觉结果,但继续检查 #runtime-status:
data-state="ready"
data-visible-count="0"
data-filters 包含 keyword=martin、status=all
这说明应用不是加载失败,而是在 ready 状态下得出了零结果。
第二步:排除服务端数据问题
在第二个终端运行:
npm run doctor
它仍应报告 API 返回 8 本书。服务端日志中的 /api/books 也是 200。问题范围因此缩小到浏览器加载数据之后。
第三步:检查浏览器事件
Console 中应出现:
{"source":"browser","level":"warn","event":"client_fault_active","mode":"search-empty"}
紧接着的 books_rendered 会显示 totalCount: 8、visibleCount: 0。这已经给出很强的判断:数据存在,筛选阶段将结果归零,而且有一个名为 search-empty 的故障模式。
第四步:从稳定事件名回到代码
在仓库根目录搜索:
rg "client_fault_active|search-empty" public
如果没有安装 rg,使用编辑器的全局搜索。结果会定位到 public/app.js 的 currentBooks()。你会看到只有在 URL 显式携带该故障参数且搜索词非空时,匹配关键词才被替换成一个不可能命中的值。
第五步:移除故障并复验
回到正常地址:
http://127.0.0.1:3000/
再次搜索“Martin”,应恢复为 2 本。client_fault_active 不再出现,DOM 的 data-visible-count 为 2。
到这里才形成闭环:复现现象、排除服务端、找到前端事件、定位代码、移除触发条件、验证恢复。
让 Agent 独立走一次诊断链
如果 Agent 有浏览器工具,让它直接处理下面的任务:
不要修改代码。请诊断这个现象:
打开 http://127.0.0.1:3000/?fault=search-empty 后搜索 Martin,页面显示 0 本书。
请依次给出用户界面证据、DOM 状态、浏览器事件、服务端诊断和代码定位;说明故障在哪一层,以及移除故障参数后如何确认恢复。
如果 Agent 没有浏览器工具,就向它提供截图、#runtime-status 节点和两条浏览器事件,再让它运行 npm run doctor 与代码搜索。不要只发一句“页面不工作”。
合格诊断应包含:
- UI 确实为 0 本;
- DOM 处于
ready而不是error; - API 仍有 8 本书;
- 浏览器事件显示故障模式在客户端生效;
- 代码位置是
public/app.js; - 正常 URL 搜索 Martin 恢复 2 本;
- 全程不需要修改数据或重启到另一个不明状态。
验证改造没有破坏主线
先停止开发服务器,再运行:
npm run setup
npm run check
两条命令都应通过。标签任务验收仍然应该失败,因为本篇没有实现标签筛选:
npm run accept:tag-filter
确认失败仍指向缺少标签控件和筛选接口,而不是本篇新增日志或状态节点。
然后审查改动:
git status --short
git diff --check
git diff --stat
git diff -- server.js public scripts docs AGENTS.md package.json
重点确认:没有把 ?fault=search-empty 设成默认状态,没有在日志中加入敏感数据,也没有修改受保护的标签验收和产品规则。
提交:
git add server.js public scripts package.json AGENTS.md docs
git commit -m "feat: expose local runtime diagnostics"
git status --short --branch
常见阻塞
doctor 报 fetch failed
先确认另一个终端里的 npm run dev 仍在运行,再核对端口。若服务使用 3100,APP_URL 也必须是 3100。不要把连接失败误判成 API 返回错误。
服务端终端没有 request_completed
确认访问的是 127.0.0.1 上当前进程打印的 URL,而不是另一个旧服务。可以对照 server_started 中的 pid,停止所有旧 ShelfCare 进程后重新启动。
Console 里看不到 JSON 事件
先刷新页面,确认 Console 没有按级别隐藏 Info 或 Warnings。若页面缓存了旧 app.js,执行一次强制刷新。不要先改日志代码。
DOM 有 runtime-status,但仍是 loading
打开 /api/books 并查看 Console。如果 API 失败,状态最终应变为 error;如果一直 loading 且无错误,检查脚本是否实际加载以及 defer 是否仍在 index.html 中。
用截图能不能代替所有检查
不能。截图适合证明视觉现象,却看不到隐藏状态、请求 ID和异常堆栈。对当前故障,至少还需要 DOM、浏览器事件与服务端诊断中的两类证据来缩小范围。
本篇留下了什么
ShelfCare 现在有了一条可重复的运行诊断链:
用户现象
-> DOM 状态与浏览器事件
-> /health、/api/books 与请求日志
-> 稳定事件名定位代码
-> 移除故障后重新验证
Agent 不再只能从源码推测页面发生了什么,也能明确说出每条证据证明到哪一层。下一篇会处理另一类问题:即使 Agent 看得见一切,它仍可能走捷径、跨过架构层或扩大权限。我们会把这些关键不变量从文字升级成可执行约束。
08 把约束变成可执行规则
预计阅读与操作时间:45~60 分钟
本篇目标:为 ShelfCare 增加一组可运行的仓库护栏,亲眼看到一次越过数据访问层的修改被挡住,并分清自动规则、人工审批和安全沙箱各自负责什么。
配套代码说明
本篇加入 assets/lesson-07-guardrails/,把前几篇“写在文档里的边界”升级成能失败的机器检查:
lesson-07-guardrails/
├── policy/
│ └── repository-policy.json # 机器可读的规则参数
├── scripts/
│ ├── guard.js # 规则执行器:七类检查
│ ├── check.js # check 中并入 guard 阶段
│ └── setup.js # 必需文件清单加入 policy、guard
├── docs/GUARDRAILS.md # 自动操作 / 需审批 / 禁止捷径 三分法
├── .gitignore # 第一层敏感文件保护
└── package.json # 新增 npm run guard,并入 check
各文件实现的功能:
- policy/repository-policy.json 是“参数与逻辑分离”的关键:它存放
runtimeRoots(生产运行代码范围)、allowedDataReaders(只允许src/library.js读数据)、allowedDependencies(空列表=不加依赖)、sensitiveFileNames/Extensions、maxChangedFiles(12)、maxJavaScriptLines(300)、forbiddenPackageScriptFragments(rm -rf、git reset --hard、curl、wget等)。这些阈值可以改,但应由评审而非执行者为了过关去放宽。 - scripts/guard.js 依次跑七类检查并打印稳定名称:
architecture boundary(用正则检查运行时代码是否绕过src/library.js直读data/books.json)、dependency allowlist、sensitive files、package scripts、file size preference、change scope(改动文件数 + 受保护文件与baseRef比较)。它刻意不扫描tests/、acceptance/和检查脚本本身,因为测试读固定样本是合理的,误报会让人习惯性忽略护栏。 - scripts/check.js 把
guard并进通用检查,npm run check会自动执行它;setup.js 相应把policy/、guard.js、GUARDRAILS.md纳入必需文件清单。 - docs/GUARDRAILS.md 用三分法划清边界:可自动做(读仓库、改任务内文件、跑文档命令)、必须审批(加依赖、外网、凭据、部署、破坏性命令)、禁止捷径(改数据/测试/策略来伪造完成)。
- .gitignore 是第一层敏感文件保护,减少误提交
.env、私钥的机会。
本篇最诚实的一处判断体现在代码与文档的配合上:guard.js 能拦住“绕层访问”,但它自己也在这个仓库里、能被拥有写权限的 Agent 修改。因此文档明确写着——“仓库护栏不是安全沙箱”,真正的安全边界要交给可信 CI、主分支保护和工具级权限。逻辑上,guard 是“动作后检查”(改了不合规就失败),而更严格的系统级权限是“动作前拦截”,两者别混为一谈。
前几篇已经让 Agent 知道项目怎么工作,也让它能够启动、观察和验证应用。不过,“知道规则”不等于“必然遵守规则”。
比如 docs/ARCHITECTURE.md 已经写明:服务端应该通过 src/library.js 读取数据。Agent 仍可能为了快速完成任务,直接在 server.js 里读取 data/books.json。功能也许能跑,测试甚至可能通过,但项目里从此出现两条数据访问路径。等缓存、校验或存储方式变化时,其中一条很容易被漏掉。
这不是多写一句“请严格遵守架构”就能稳定解决的。真正重要的边界,需要从文字提醒升级为能失败的检查。
架构文档和架构检查并不重复
同一条规则通常需要两个版本。
文档解释为什么:
server.js -> src/library.js -> data/books.json
可执行检查回答有没有违反:
运行时代码中,只有 src/library.js 可以直接读取 data/books.json。
只有检查,没有文档,Agent 看到失败后不知道设计意图;只有文档,没有检查,违规修改仍可能混进结果。两者配合才完整。
也不是所有偏好都值得变成硬规则。变量名是否“好看”、两个相近函数是否应该合并,往往需要上下文判断;数据访问层不能被绕过、凭据不能进入仓库、验收契约不能被任务代码偷偷改写,则适合机器执行。
一个实用判断是:如果违反某条规则会造成明确、重复而且代价较高的问题,就值得自动检查;如果它高度依赖情境,保留为评审项更合适。
加入仓库护栏
确认第六篇的改动已经提交,工作区没有遗留文件:
git status --short --branch
git log -1 --oneline
然后复制本篇资产。
macOS 或 Linux:
cp -R ../assets/lesson-07-guardrails/. .
Windows PowerShell:
Copy-Item -Recurse -Force ..\assets\lesson-07-guardrails\* .
Copy-Item -Force ..\assets\lesson-07-guardrails\.gitignore .
本次主要增加这些文件:
policy/repository-policy.json 机器可读的规则参数
scripts/guard.js 规则执行器
docs/GUARDRAILS.md 自动权限与人工审批边界
.gitignore 第一层敏感文件保护
package.json 新增 npm run guard,npm run check 也会自动执行它。AGENTS.md、架构和开发文档同步加入了入口与边界,避免出现“脚本已经变了,说明还停在旧版本”的情况。
先运行:
npm run setup
npm run guard
预期最后看到:
[guard] architecture boundary
[guard] dependency allowlist
[guard] sensitive files
[guard] package scripts
[guard] file size preference
[guard] change scope
[guard] All guardrails passed.
这些输出刻意使用稳定的检查名称。Agent 不必阅读整段脚本,也能知道失败属于架构、依赖、安全还是修改范围。
策略文件到底约束了什么
打开 policy/repository-policy.json,会看到当前项目最值得保护的几项不变量。
1. 数据访问方向
runtimeRoots 指定生产运行代码范围,allowedDataReaders 只允许 src/library.js 读取 data/books.json。server.js 和浏览器代码必须通过既有边界拿数据。
这里特意没有扫描 tests/、acceptance/ 和环境检查脚本。测试读取固定样本是合理行为,而生产请求绕过访问层不是。规则越精确,误报越少,团队才不会习惯性忽略它。
在真实数据库项目中,同样的思路可以变成:Controller 不能直接依赖数据库客户端,领域模块不能反向导入 Web 层,跨模块访问只能经过公开接口。实现手段可能是 lint 规则、依赖图检查或架构测试,原则没有变化。
2. 依赖和团队偏好
ShelfCare 目前不需要第三方包,所以 allowedDependencies 是空列表。Agent 如果随手安装一个依赖,护栏会要求先评审,而不是默认接受。
JavaScript 文件当前还设置了 300 行上限。它不是宇宙真理,只是一个团队偏好:文件明显膨胀时,先拆分或解释原因。与其在提示词里反复说“保持文件短小”,不如把项目认可的阈值写在同一个策略文件里。
这些参数都可以改,但不应该由执行功能任务的 Agent 为了通过检查而自行放宽。策略变更和产品代码变更是两类决策。
3. 敏感文件与危险脚本
护栏会拒绝 .env、私钥和 credentials.json 一类文件;.gitignore 同时减少误提交的机会。package.json 脚本中出现 rm -rf、git reset --hard 或下载命令时也会失败。
这不是完整的安全扫描。它只挡住项目已知、容易发生的错误。真正的密钥检测、依赖漏洞检查和系统权限隔离仍需要专门工具。
4. 受保护输入和修改规模
标签筛选任务的契约与产品规则会和 main 比较。执行功能任务时,如果它们发生变化,护栏要求停止并评审。一次变更默认最多触及 12 个文件,超过后也不会假装什么都没发生,而是要求拆分任务或扩大评审范围。
文件数量不是代码质量指标。这个阈值保护的是“任务边界”,防止一个小需求悄悄扩展成仓库级重构。
亲手制造一次架构违规
现在验证护栏不是摆设。在 public/ 下临时新建 direct-data-access.js,内容如下:
const fs = require("node:fs");
const books = JSON.parse(fs.readFileSync("../data/books.json", "utf8"));
console.log(books.length);
这段代码本身也不适合浏览器运行,我们只是用它模拟一个最直白的“绕过数据访问层”修改。执行:
npm run guard
预期命令以非零状态退出,并明确指出:
[guard] ERROR: public/direct-data-access.js reads data/books.json directly; use src/library.js instead.
[guard] Guardrails failed.
这次失败给 Agent 的信息足够具体:违规文件是哪一个,越过了什么边界,应该走哪一层。它不需要猜测“架构不合规”究竟指什么。
删除这个临时文件。
macOS 或 Linux:
rm public/direct-data-access.js
Windows PowerShell:
Remove-Item public\direct-data-access.js
重新运行:
npm run guard
现在应该恢复为 All guardrails passed。如果仍然失败,不要先修改策略;根据第一条 ERROR 检查临时文件是否真的删除,以及本篇开始前是否已有其他未提交改动。
哪些操作可以自动做,哪些必须停下来问
代码规则只能约束仓库内容,无法代替权限设计。docs/GUARDRAILS.md 因此还给操作分了三类。
Agent 可以自行读取仓库、修改任务范围内的文件、运行文档列出的本地命令、查看 Git 状态和 Diff。这些动作可逆,影响也局限在实验仓库。
下列动作必须获得批准:
- 访问外部网络或向外部服务写数据;
- 读取仓库之外的文件,使用凭据或扩大系统权限;
- 安装依赖、修改策略或产品契约;
- 改动持久数据、部署、发布、合并或重写 Git 历史;
- 执行破坏性或难以恢复的命令。
如果任务确实需要其中一项,正确行为不是永远拒绝,而是停下来说明:需要做什么、为什么需要、会影响哪里,以及最小需要什么授权。
这张权限表的价值在于把“自主工作”与“无限权限”分开。Harness 的目标是减少不必要的人类接管,不是取消所有人工决策。
可执行约束不是安全沙箱
还有一个很容易被忽略的事实:如果 Agent 能编辑整个仓库,它通常也能编辑 scripts/guard.js 和策略文件。因此,本地护栏更像高质量的护栏和反馈机制,而不是不可绕过的安全边界。
在团队项目里,关键检查应该再由可信 CI 执行,并结合:
- 主分支保护和必要评审;
- 对策略、测试与部署文件的所有者审核;
- 最小权限的凭据和隔离运行环境;
- 网络、文件系统和外部写操作的工具级限制。
换句话说,仓库规则负责告诉 Agent 什么改动不合格;系统级权限负责限制它根本不能做什么。两者不要混为一谈。
让 Agent 先读边界,再开始工作
可以把下面的任务交给你的编码 Agent:
请先阅读 AGENTS.md,以及与架构和护栏有关的文档。暂时不要修改代码。
告诉我:
1. 运行时代码读取藏书数据必须经过哪一个模块;
2. 哪些操作可以自行执行,哪些必须先申请批准;
3. 完成功能修改前要运行哪些通用检查;
4. 如果 npm run guard 失败,你会怎样处理,而不是怎样绕过它。
请引用对应文件和命令。
合格回答应该定位到 src/library.js、docs/GUARDRAILS.md、npm run guard 与 npm run check,并明确表示不会为了变绿而弱化策略。
这一步看起来没有产出代码,却能确认 Harness 已经把边界交付给 Agent。若它仍只复述通用安全常识,没有引用仓库事实,先修复知识入口,不要立刻把真实任务交给它。
做一次完整复验
运行通用检查:
npm run setup
npm run check
check 中应该出现 repository guardrails 阶段,最后仍是:
[check] All checks passed.
标签功能尚未实现,因此任务专用验收依旧应该失败:
npm run accept:tag-filter
这两个结果并不矛盾。通用检查证明现有应用与仓库规则健康;红色验收证明下一篇要完成的标签功能还没有被伪造出来。
最后审查改动:
git status --short
git diff --check
git diff --stat
git diff -- .gitignore AGENTS.md package.json scripts policy docs
重点确认:guard 已进入 check,策略没有包含真实凭据,受保护的验收文件和产品规则没有改动,临时的违规文件也已经删除。
提交:
git add .gitignore AGENTS.md package.json scripts policy docs
git commit -m "chore: enforce repository guardrails"
git status --short --branch
常见阻塞
guard 说修改文件超过 12 个
先用 git status --short 和 git diff --stat 确认是否混入了上一项任务。若是,先分支或提交前一项改动;若当前任务本来就需要更大范围,拆分任务或请评审者明确批准调整阈值。不要静默改成一个很大的数字。
guard 说受保护文件不同于 main
检查 git diff main -- acceptance/tag-filter.contract.js docs/PRODUCT_RULES.md。如果只是误改,恢复那两处;如果产品契约真的改变,暂停功能实现,让契约变更单独评审。
我的默认分支不叫 main
将 policy/repository-policy.json 的 baseRef 改成真实基线分支,并把这次策略调整纳入评审。临时也可以先创建课程约定的 main 分支,但不要让检查长期指向不存在的基线。
创建 .env 后为什么 guard 直接失败
这是预期行为。真实凭据应放在仓库之外,或由受控的密钥机制在运行时注入。若项目需要环境变量模板,只提交不含秘密的 .env.example。
正常实现被架构规则误报
先确认实现是否真的绕过了 src/library.js。如果架构已经有充分理由改变,不要在业务文件中藏一个技巧绕过正则;同时更新架构文档、策略和检查,并单独评审这项设计决定。
本篇留下了什么
ShelfCare 现在不只“写着一些规则”,而是拥有一条能失败的约束链:
架构与安全意图
-> policy/repository-policy.json
-> npm run guard
-> npm run check
-> 可信 CI 与人工评审
它保护的是少数关键不变量,不替 Agent 决定每个函数怎么写。下一篇我们会回到第二篇完全相同的标签筛选任务,让 Agent 在完整 Harness 中独立完成它,再比较前后两次的过程、结果和人工接管次数。
验证计数与检查失败
git status --porcelain=v1 -z --untracked-files=all 按文件列出未跟踪目录,脚本同时处理带空格的文件名和重命名前后路径。请在副本的 public/new/ 创建超过 maxChangedFiles 数量的文件,预期 guard 失败。基线不存在、Git 读取失败时同样失败;先修正 policy.baseRef 或提交基线,不能把“跳过检查”当通过。
09 综合验收
预计阅读与操作时间:60~90 分钟
本篇目标:用第二篇完全相同的任务做一次受控复测,让 Agent 在完整 Harness 中实现标签筛选,再用自动检查、运行证据和过程记录判断改造是否真的有效。
配套代码说明
本篇的 assets/lesson-08-reference-solution/ 是失败后的恢复路径,不是衡量 Agent 的答案标准——只要另一种实现满足产品契约、架构边界和验证要求,就同样有效:
lesson-08-reference-solution/
├── public/
│ ├── filter-books.js # 新增:可在 Node 与浏览器复用的纯筛选模块
│ ├── app.js # 接入 filterBooks + 动态标签控件
│ └── index.html # 加入 #tag-filter 下拉框、引入 filter-books.js
├── scripts/
│ ├── check.js # 把标签验收并入长期回归
│ └── setup.js # 必需文件清单加入 filter-books.js
└── docs/ # 同步更新架构/开发/可观测性文档
各文件实现的功能:
- public/filter-books.js 是验收契约要求的那个“约定接口”的参考实现。它用 IIFE 包裹,导出
filterBooks(books, {keyword, status, tag}):对每本书做三条件 AND 过滤(关键词匹配 title+author 且忽略大小写、状态等于或为all、标签命中或为all)。它同时做两件事——module.exports导出给 Node 测试用,window.ShelfCareFilters挂到全局给浏览器用,所以同一份逻辑能在两端复用,这是它能被acceptance/tag-filter.contract.js直接 require 的原因。 - public/app.js 的改动有三处:从
window.ShelfCareFilters取filterBooks;activeFilters()增加tag字段;新增populateTags()从data/books.json的 tags 去重、按中文排序后填充#tag-filter下拉框(不维护第二份硬编码标签列表)。故障注入?fault=search-empty逻辑被完整保留,只是改由filterBooks承接过滤。 - public/index.html 在工具栏新增“标签”下拉框(
id="tag-filter",初始只有“全部标签”一项,选项由 JS 动态生成),并在<script>里先加载filter-books.js再加载app.js。 - scripts/check.js 新增
filter syntax和tag-filter acceptance两个阶段,把标签验收升级为长期回归;scripts/setup.js 把filter-books.js、OBSERVABILITY.md、GUARDRAILS.md、policy/等新文件纳入必需清单。
这套参考实现能一次性满足第五篇的八项验收(控件存在、接口可导出、科幻/技术+在读/all 三组样本结果正确、页面真正接入),并且 npm run guard、npm run check 全部通过——我在实际复现时验证过:accept:tag-filter 输出 pass 6 / fail 0,filterBooks(books,{tag:"科幻"}) 恰好返回 book-001、book-006。
第二篇里,我们把一份几乎没有工作说明、稳定入口和任务验收的 ShelfCare 交给了 Agent。现在业务功能仍然没有实现,但它周围的环境已经不同:仓库知识可发现,启动和检查命令稳定,验收先红,运行状态可观察,关键边界也能自动阻止违规。
这一篇不再增加新的 Harness 概念。我们要回答一个更重要的问题:做完这些改造后,同一个 Agent 面对同一句任务,是否更容易稳定交付?
先控制变量,不要把演示当成证明
这次对照尽量只改变 Harness。请保持以下条件与第二篇一致:
- 使用同一种 Agent、同一个模型和相同的思考强度;
- 新开会话,不把第一次尝试的分析或答案复制过来;
- 使用完全相同的原始任务,不追加实现提示;
- 不在执行中途替 Agent 指路,除非它明确请求必要授权;
- 仍然由你独立验收,而不是只接受 Agent 的完成总结。
模型或 Agent 版本若已经升级,也不必放弃实验,但要把变化记下来。一次个人实验不能证明普遍因果,它能证明的是:在你的项目和当前工具条件下,新的工作环境是否减少了猜测、返工和人工接管。
OpenAI 的 Harness Engineering 实践也强调,Agent 端到端完成任务依赖特定仓库的结构、工具、验证和恢复能力,不能脱离这些投入直接推广到所有项目。官方案例
确认复测起点
你现在应该位于 shelfcare-lab 的 main 分支,第七篇改动已经提交,工作区干净。运行:
git status --short --branch
git branch --list
预期能看到 main 和第二篇保留的 baseline-attempt,当前分支是 main。如果 baseline-attempt 不在,也可以继续,只是最终无法查看第一次代码尝试;第二篇保存的过程记录仍可用于比较。
先确认完整 Harness 的健康状态:
npm run setup
npm run check
两条命令都应通过。接着复现任务目标尚未完成:
npm run accept:tag-filter
它应该失败,并指向缺少 #tag-filter、public/filter-books.js 和页面集成。如果此时已经通过,先检查自己是否把第二篇的实现带回了 main;没有红色起点,就无法证明接下来的修改使验收从失败变成成功。
创建第二次尝试分支:
git switch -c harness-attempt
不要删除或重置 baseline-attempt。两个分支分别保存改造前后尝试,main 保存功能尚未实现但 Harness 完整的基线。
把完全相同的任务交给 Agent
在 shelfcare-lab 根目录启动一个新 Agent 会话,只发送第二篇使用过的这句话:
请为 ShelfCare 增加按标签筛选藏书的功能。请完成实现并检查结果。
不要补充“请阅读 AGENTS.md”“请运行某条测试”或“请新建纯函数”。这些信息现在已经进入仓库;如果还需要人每次在对话里重新讲一遍,Harness 就没有真正承担交接工作。
让 Agent 自己完成本轮。你可以观察,但先不干预。重点记录过程,而不是只等最终答案:
- 它是否先查看
AGENTS.md,再按链接读取相关文档; - 是否运行
npm run setup或直接找到稳定入口; - 是否在修改前复现
npm run accept:tag-filter的失败; - 是否保持验收文件、产品规则和数据不变;
- 是否把搜索、状态和标签组合逻辑集中在可测试接口中;
- 是否运行专项验收、通用检查并检查 Diff;
- 有浏览器能力时,是否真的操作页面并读取 DOM 或控制台证据;
- 遇到权限边界时,是说明阻塞并请求最小授权,还是自行扩大范围。
一个健康过程不一定与参考实现逐行相同。Harness 约束的是结果、边界和证据,不应该规定每个变量名或每一行代码。
先审查结果,再运行命令
Agent 结束后,先不要立刻接受它的总结。在仓库根目录运行:
git status --short
git diff --check
git diff --stat
git diff
检查下面几件事:
data/books.json没有为了凑出结果而被修改;acceptance/tag-filter.contract.js和docs/PRODUCT_RULES.md没有被弱化;- 没有新增第三方依赖、凭据或与任务无关的大规模重构;
- 标签选项来自图书数据,不是另存了一份硬编码列表;
- 新筛选不会破坏原有搜索、状态筛选和故障注入。
再单独确认受保护输入与 main 一致:
git diff --exit-code main -- acceptance/tag-filter.contract.js docs/PRODUCT_RULES.md
正确结果是没有输出,退出码为 0。如果这里出现差异,不要继续把其他绿色测试当作完成证据。先审查 Agent 为什么改了考试题或产品契约。
让红色验收变绿
先运行任务专用验收:
npm run accept:tag-filter
预期六项检查全部通过,输出会包含类似:
pass 6
fail 0
具体排版和耗时随 Node.js 版本变化。关键是这六类行为都被证明:控件存在、纯筛选接口可用、科幻结果正确、技术与在读使用 AND 组合、all 只移除标签条件、页面确实接入新逻辑。
接着运行通用检查:
npm run guard
npm run check
两条都必须通过。专项验收绿色证明新任务完成,通用检查绿色证明现有能力和仓库边界没有被破坏。任意一条失败,都不能用另一条替代。
如果 Agent 的实现让专项验收通过,却没有把已经稳定的标签验收纳入日常 npm run check,先记录本次实验结果,不要中途改变测量。完成对照后,再按第八节做项目收尾。
验证真正运行的页面
自动验收还不能替代浏览器。打开第一个终端,运行:
npm run dev
在第二个终端运行:
npm run doctor
doctor 应继续报告健康状态为 ok,API 返回 8 本书。然后在浏览器打开终端打印的地址,通常是 http://127.0.0.1:3000。
逐项检查第二篇定义的七个用户行为:
- 页面出现“标签”下拉框;
- 选项来自现有数据,能看到“科幻”和“技术”;
- 选择“科幻”后,只剩《献给阿尔吉侬的花束》和《你一生的故事》;
- 选择“技术”,再把阅读状态设为“在读”,只剩《设计数据密集型应用》和《程序员修炼之道》;
- 再输入搜索词时,搜索、状态和标签同时生效;
- 恢复“全部标签”后,状态和搜索条件仍然保留;
- 组合出零结果时显示“没有找到符合条件的书。”,Console 没有异常。
打开开发者工具,在 Elements 面板找到 #runtime-status。选择“技术”与“在读”后,data-filters 应同时包含当前 status 和 tag,data-visible-count 应为 2。Console 中的 books_rendered 事件也应给出相同状态。
最后确认第六篇的诊断练习没有被新功能破坏。打开:
http://127.0.0.1:3000/?fault=search-empty
输入任意搜索词后仍应触发 client_fault_active;移除查询参数,搜索恢复正常。验证完按 Ctrl+C 停止服务。
如果 Agent 没有浏览器工具,这部分由你手动完成,并在记录里写“运行验证由人执行”。不要把缺少工具伪装成 Agent 已经检查过页面。
保存第二次尝试和过程数据
回到课程目录,在 shelfcare-lab 旁边新建 shelfcare-harness.md,与第二篇的 shelfcare-baseline.md 放在一起。不要把它放进实验仓库,避免过程记录混入功能 Diff。用相同字段记录结果:
# ShelfCare Harness 复测记录
## 固定条件
- Agent 与模型:
- 思考强度:
- 原始任务:请为 ShelfCare 增加按标签筛选藏书的功能。请完成实现并检查结果。
- 与基线不同的工具或版本:
## 运行结果
- 是否交付可见的标签筛选:是 / 否 / 部分
- 独立行为检查通过数:__/7
- npm run accept:tag-filter:通过 / 失败
- npm run check:通过 / 失败
- Agent 提供的完成证据:
## 过程数据
- 人工补充信息次数:
- 权限确认次数:
- 预期的失败验收次数:
- 非预期失败命令次数:
- 无效重试次数:
- 修改文件数:
- 是否检查实际页面:
- 是否检查 Git Diff:
## Harness 仍然缺少什么
- 知识:
- 工具:
- 环境:
- 反馈:
- 验证:
- 权限:
这里把“预期的红色验收”和“非预期命令失败”分开。先红后绿是证据,不是无效失败;找错命令、重复撞同一个错误才是流程成本。
回到 shelfcare-lab,把实现提交到当前分支:
git add .
git diff --cached --check
git diff --cached --stat
git commit -m "feat: add tag filtering with harness validation"
git status --short --branch
预期当前位于 harness-attempt,工作区干净。不要急着删除两个实验分支,它们是本次课程最有价值的产物之一。
把任务验收升级为长期回归
实验结束后,标签筛选不再是“未来任务”,而是现有产品能力。以后修改搜索或筛选时,npm run check 应该一起检查它。
如果 Agent 已经完成下面三件事,只需核对,不要重复修改:
scripts/check.js包含标签验收阶段;scripts/setup.js把public/filter-books.js列为必需文件;AGENTS.md、架构、开发和可观测性文档反映新的筛选结构与状态。
如果没有,在记录完实验后再给 Agent 一个独立的收尾任务:
标签筛选已经通过独立验收。现在请把它升级为长期回归能力:
让 npm run check 包含标签验收;让 setup 检查新增运行文件;同步更新 AGENTS.md、架构、开发和可观测性文档。
不要修改产品规则、验收契约、样本数据或仓库策略。完成后运行 npm run check 并审查 Diff。
这不是偷偷改变原始实验,而是把一次成功任务产生的新事实写回 Harness。今后的 Agent 将继承这次经验,不必重新猜测标签筛选的接口和验证方式。
如果第二次尝试没有完成,先填写仓库外的失败记录,并保存 Agent 留下的代码,不要覆盖它。确认 Diff 中没有凭据或个人文件后,在 harness-attempt 上提交失败现场;即使只有部分实现,也值得保留:
git add .
git diff --cached --check
git diff --cached --stat
git commit -m "experiment: preserve failed harness attempt"
如果代码没有任何变化,Git 会提示没有内容可提交,保留外部记录即可。工作区干净后,再从 main 新建恢复分支,使用课程提供的参考资产:
git switch main
git switch -c harness-reference
macOS 或 Linux:
cp -R ../assets/lesson-08-reference-solution/. .
Windows PowerShell:
Copy-Item -Recurse -Force ..\assets\lesson-08-reference-solution\* .
然后运行:
npm run accept:tag-filter
npm run check
参考资产只是恢复路径,不是衡量 Agent 的答案标准。只要另一种实现满足产品契约、架构边界和验证要求,就同样有效。
怎样比较两次结果
把 shelfcare-baseline.md 和 shelfcare-harness.md 并排打开,填写一张小表:
| 指标 | 改造前 | 改造后 | 你要观察的变化 |
|---|---|---|---|
| 独立行为通过数 | __/7 | __/7 | 结果是否更完整 |
| 人工补充信息 | __ 次 | __ 次 | 仓库能否自行交接 |
| 非预期失败命令 | __ 次 | __ 次 | 入口是否更稳定 |
| 无效重试 | __ 次 | __ 次 | 反馈是否能推动修正 |
| 自动验证证据 | 有 / 无 | 有 / 无 | 完成是否可重复证明 |
| 页面运行证据 | Agent / 人 / 无 | Agent / 人 / 无 | 工具能否触达用户行为 |
| 越界或过度修改 | 有 / 无 | 有 / 无 | 约束是否守住任务范围 |
不要只比较“最后都做成功了没有”。如果两次都成功,但改造后少了多轮猜命令、主动复现红色验收、给出完整 Diff 和页面证据,这仍是明显改善。Harness 的价值常常体现在成功的可重复性和失败的可定位性,而不只是单次通过率。
如果改造后仍然失败,按证据判断下一步:
- 找不到已有规则:知识入口或渐进式披露仍有问题;
- 命令可运行但看不到页面:工具或运行反馈还不够;
- 所有信息、工具和验收都齐全,仍写不出基本逻辑:更可能是模型能力、规划或实现问题;
- 为通过测试而改契约:受保护输入和可信评审还不够强;
- 频繁请求不必要权限:权限说明或工具默认权限需要调整。
不要因为一次结果不好,就把所有 Harness 推倒重来。修复最大、最重复的一个缺口,再执行下一次代表性任务。
给自己的项目做一张 Harness 体检表
ShelfCare 的改造可以压缩成六个问题:
| 维度 | 最低可用状态 |
|---|---|
| 知识 | Agent 能从短入口找到产品、架构和开发事实 |
| 环境 | 一条稳定命令可以检查环境,一条命令可以启动项目 |
| 验证 | 任务有先红后绿的验收,完成还包含回归与 Diff |
| 可观测性 | Agent 或操作者能读取 UI、日志、健康与错误证据 |
| 约束 | 关键架构和安全不变量能自动失败并给出修复方向 |
| 权限 | 自动动作、需审批动作和禁止捷径有明确边界 |
把这张表带到自己的项目里,不必一次补齐所有工程。先选一个经常让 Agent 失败的小任务,保存基线,然后只补最影响结果的那一层。Harness Engineering 不是先造一套宏大平台再开始工作,而是把每次失败暴露出的缺失能力沉淀回环境。
常见阻塞
第二次尝试一开始就无法创建分支
运行 git status --short。如果第七篇还有未提交改动,先确认内容并提交;不要用强制切换或硬重置清理。实验需要一个可追踪的起点。
Agent 修改了受保护文件
保存 Diff 和它给出的理由,把这次行为记入复测记录。若没有经过契约评审,恢复这两处改动后再验收;不要通过修改测试获得绿色结果。
guard 提示修改超过 12 个文件
先判断 Agent 是否做了与标签无关的重构。如果是,让它缩小范围;如果确实需要扩大修改,结束受控实验并单独评审,而不是当场放宽策略。
自动验收全绿,但页面标签选项为空
打开 Console 和 #runtime-status,再检查 /api/books。这说明静态契约覆盖不够或页面加载流程没有真正接上,也是下一轮应该沉淀的新验收。自动测试绿色不代表观察到的用户问题不存在。
两次尝试使用的模型已经不同
在记录中明确标注,避免把全部变化都归因于 Harness。你仍可以评估当前项目是否更容易交接,但不能把它当成严格的单变量比较。
没有浏览器工具,是否还能完成课程
可以。由人完成七项页面检查,并如实记录人工接管一次。之后再决定是否值得为 Agent 接入受控浏览器;不要为了追求“全自动”跳过真实用户行为验证。
课程结束时,你真正得到的是什么
最终成果不是一个标签下拉框,而是一套可以迁移的工作方法:
保存真实失败
-> 判断缺少知识、工具、反馈、验证还是边界
-> 把缺失能力写进仓库或执行环境
-> 用相同任务复测
-> 把新经验继续沉淀进 Harness
模型仍然重要,提示词也仍然重要。但当 Agent 进入真实项目,稳定交付还取决于它能否看懂环境、采取正确动作、读取结果、证明完成,并在越界前停下来。
如果第二次实验让这些步骤更少依赖你在旁边解释,这套 Harness 就已经开始发挥作用。下一次 Agent 失败时,先别急着换一句更用力的提示词;问一问:它缺的究竟是能力,还是一个本来就应该由环境提供的条件?
10 知名开源智能体Crush是如何做智能体运行环境工程的
本章是迁移练习,不把 Crush 内部的一次 turn 等同于本课程的外部任务调度。预计用 20~30 分钟追踪三条调用链。
先固定所读源码
git clone https://github.com/charmbracelet/crush.git
cd crush
git rev-parse HEAD > ../crush-reading-commit.txt
git status --short
把提交号保存到阅读笔记。本文不声称对未来 main 分支路径永久有效;阅读时从该提交搜索符号,用本地跳转确认调用关系,引用源码时使用含该提交号的永久链接。
配置如何进入一次运行
从配置加载入口追到模型、工具权限和 LSP 的使用处。记录“文件或配置脚本 → 解析 → 校验 → 使用”的实际函数。lsp add、permissions allow/deny 若出现在该版本的配置脚本示例中,就属于配置语言,不是可直接在 shell 执行的通用命令;启动参数只能按本机 crush --help 验证。不要把裸 --yolo 当成一条命令,也不在练习中关闭权限检查。
回答:一个权限条目在哪里被检查?拒绝后调用者收到什么?权限控制决定允许执行什么,不是任务结果验收,也不等于操作系统沙箱。AGENTS.md 是否拆分,应按信息重复、职责和上下文成本判断,不按“一屏”长度。
停止与重复检测
搜索 toolResultOutputString 和循环检测调用者,追踪输入、签名、阈值和返回路径。原始工具文本可能带时间戳或临时路径;不能因为进入签名计算就称为“稳定文本”。用两次相同错误但不同时间戳的输出,判断该版本是否会识别为重复。
内部工具循环发生在一次 Agent turn 内,属于运行时控制;本课程多次运行之间的领取、重试、恢复是另一层。plan/coder 的角色分工也不自动构成独立验证器,必须查看谁制定验收、谁执行以及能否被候选修改。
关联标识与持久化
搜索 WithRunID、RunIDFromContext,追踪 RunID 如何进入事件与日志。它提供关联标识,单靠传递 ID 不提供去重;Mutex 串行化也不保证相同请求只执行一次。真正去重需要检查键、原子领取或持久记录。
再找到一次运行状态写入、加载和恢复的入口。写出一个中断点:此时已保存什么、可能重复哪些副作用、哪些状态必须由人确认。不要从“保存聊天历史”直接推出可恢复业务事务。
独立验收
交付一页阅读笔记:提交号、三条调用链的源码位置、一个重复检测反例、一个中断恢复边界。每条结论至少指向一个调用者与被调用者;没有找到的机制写“未在本次路径确认”,不要用概念相似代替实现证据。
参考:Crush 官方仓库。
11 进阶学习
企业级Agent工程架构实战营
Agent 演示成功之后 怎样让它在真实业务里可靠地工作
一个 Agent 在演示时完成了任务,换一份数据、换一个用户,或者执行到一半断开连接,它还能顺利完成吗?
学完 Harness Engineering,你会开始留意这些问题。Agent 能否找到项目资料、启动程序、读取日志、使用检查工具,都会影响结果。
知识库带你做的,是把这些条件逐步准备好,让任务有可读取的背景,也有可执行的检查。
先检查 Agent 周围的环境
回到自己的项目,可以先做三个小检查,看看任务是否具备可重复执行的基础。

哪一项很难回答,就从哪一项补起:把说明放进仓库,让启动步骤可以重复执行,给关键动作增加记录。
这些工作看起来很细,却能减少开发过程中反复解释和人工补救的次数。
接入业务之后 还要管住真实动作
当 Agent 开始查数据、建工单或调整预算,系统需要承担更多责任。下面四类问题值得提前处理。

比如,创建工单后,还要查询业务状态,确认工单真的存在。中途失败时,先读取已经完成的动作, 再决定从哪里继续,避免重复执行。
后续换模型、改上下文或增加记忆时,也要用相同的任务比较。把失败样例留下来,才能检查新版本有没有把旧问题带回来。
把环境建设继续推进到系统架构
《企业级 Agent 工程架构实战营》面向已经具备大模型应用和基础 Agent 开发经验的工程师,进一步练习架构设计与治理。
课程围绕 DataAgent 项目,讨论什么时候采用固定工作流、什么时候交给 Agent 自主执行,以及怎样组合两者。接着深入任务调度、状态保存、故障恢复,以及上下文、工具和记忆管理。
在预算调整场景中,课程会练习预览、人工审批、受控执行和结果验证;在质量检查上,则包含执行追踪、回放、自动评测和灰度回滚。
如果你已经做出 Agent 项目,正在被长任务、权限或稳定性问题困住,可以带着当前系统来了解课程。把具体问题与大纲对应起来,更容易判断自己需要补哪一部分架构能力。
想了解下一步怎么学
如果你希望进一步了解《企业级 Agent 工程架构实战营》的课程内容与学习要求,可以扫描下方二维码,向学习顾问发送「Harness 架构进阶」,说明自己的基础和想解决的问题,一起判断课程是否适合。
