← 极客知识库本地镜像
工程化 · 仓库改造 / 可启动 / 可验证 / 可观测

Harness Engineering

把仓库改造成 Agent 能理解、能启动、能验证的环境。

来源 time.geekbang.org/space/wiki/Harness_Engineering 11 讲 工程化

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:安装命令、架构细节、所有业务规则、历史坑点、编码风格和当前任务,全堆在一个文件里。

这比没有入口好,但很快会出现三个问题:

  1. 重要信息被淹没。 每次任务都加载大量无关内容,真正关键的边界反而不突出。
  2. 文档会过期。 命令和代码已经变化,长文档里的旧说法仍在影响 Agent。
  3. 文字无法强制执行。 “不要跨层访问数据库”写十遍,也不如一个结构测试可靠。

更有效的做法是把 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。

本篇检查

读完后,你应该能用自己的话回答四个问题:

  1. 为什么 Harness 不等于 Prompt,也不等于 AGENTS.md?
  2. 知识、工具、环境、反馈、验证和权限分别解决什么问题?
  3. Harness Engineering 与 Loop Engineering 的优化对象有什么不同?
  4. 当 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 本书。依次做两个检查:

  1. 搜索“Martin”,结果应剩下 2 本;
  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,按下面这组行为逐项检查:

  1. 页面上能找到标签筛选控件;
  2. 标签选项来自现有图书数据,至少能选择“科幻”和“技术”;
  3. 选择“科幻”后,只显示《献给阿尔吉侬的花束》和《你一生的故事》;
  4. 选择“技术”并把阅读状态设为“在读”,只显示《设计数据密集型应用》和《程序员修炼之道》;
  5. 搜索、状态和标签三种条件同时生效,而不是互相覆盖;
  6. 恢复“全部标签”后,标签条件被清除;
  7. 没有结果时仍显示清楚的空状态,浏览器控制台没有报错。

这些是我们将要逐步写进 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 信号。

本篇留下了什么

现在你应该有三样东西:

  1. 一份可以运行的 ShelfCare 主线仓库;
  2. 一个保留首次 Agent 尝试的 baseline-attempt 分支;
  3. 一份记录人工介入、失败命令、行为验收和六类问题的 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。它只做四件事:

  1. 一句话说明 ShelfCare 是什么;
  2. 指向三个专题文档;
  3. 写出当前启动、测试和完成前检查;
  4. 说明仓库范围与高风险边界。

其中最重要的部分是导航:

## 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,可以看到它做了三类检查:

  1. process.versions.node 的主版本至少是 20;
  2. 启动、源码、数据、测试和知识文件都存在;
  3. 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

检查下面几件事:

  1. data/books.json 没有为了凑出结果而被修改;
  2. acceptance/tag-filter.contract.js 和 docs/PRODUCT_RULES.md 没有被弱化;
  3. 没有新增第三方依赖、凭据或与任务无关的大规模重构;
  4. 标签选项来自图书数据,不是另存了一份硬编码列表;
  5. 新筛选不会破坏原有搜索、状态筛选和故障注入。

再单独确认受保护输入与 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。

逐项检查第二篇定义的七个用户行为:

  1. 页面出现“标签”下拉框;
  2. 选项来自现有数据,能看到“科幻”和“技术”;
  3. 选择“科幻”后,只剩《献给阿尔吉侬的花束》和《你一生的故事》;
  4. 选择“技术”,再把阅读状态设为“在读”,只剩《设计数据密集型应用》和《程序员修炼之道》;
  5. 再输入搜索词时,搜索、状态和标签同时生效;
  6. 恢复“全部标签”后,状态和搜索条件仍然保留;
  7. 组合出零结果时显示“没有找到符合条件的书。”,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 架构进阶」,说明自己的基础和想解决的问题,一起判断课程是否适合。

原文图片