官网

Harness Engineering 基础入门:让仓库可读、让会话连续、让任务有边界

·23 分钟阅读·
目录

Learn Harness Engineering 课程的学习笔记(基础篇)

基础入门解决什么问题

上一篇说过,Harness 有五个子系统:指令、工具、环境、状态、反馈。基础入门解决的是最前面的三件事:

  1. 让仓库可读 —— 把关键信息写进仓库并合理拆分指令,让它看得懂;
  2. 让会话连续 —— 让跨会话任务接得上,别每次从头摸索;
  3. 让任务有边界 —— 一次只做一件事,并且用功能清单定义"做完"。

下面分别展开。


一、让仓库可读:指令子系统

一个残酷的事实:agent 只能看见仓库

一个团队的架构决策散落在 Confluence、Slack、Jira、和几个资深工程师的脑子里。对人类来说这勉强够用,你可以问同事、搜聊天记录、翻文档,实在不行还能去茶水间堵人。但对 AI agent 来说,不在仓库里的信息等于不存在

Agent 的输入只有三样东西:系统提示和任务描述、仓库里的文件内容、工具执行的输出。你的 Slack 历史、Jira 工单、Confluence 页面、和周五下午跟同事聊的架构决定,agent 全都看不到。它不能"去问一下",也不能"搜一下聊天记录"。它的整个工作世界就是仓库本身,仓库外面的事它一概不知。

所以问题变成了:你给不给它一张足够好的地图?

知识可见性与系统记录

OpenAI 把这称为 - "仓库即规范"原则 ——仓库本身就是最高权威的规范文档。Anthropic 则强调持久化状态是长任务连续性的必要条件,而这些状态必须存在于仓库中,因为那是 agent 唯一稳定可访问的存储。

几个关键概念:

  • 知识可见性缺口:项目总知识中不在仓库里的比例。缺口越大,agent 失败的概率越高。把脑子里关于项目的隐性知识全算上,再看有多少写进了仓库,两者的差距就是可见性缺口。
  • 系统记录(System of Record):代码仓库作为项目决策、架构约束、执行状态和验证标准的权威信息源。仓库说了算,别的地方说了不算。如果"此路不通"这个信息只在老张的脑子里,那每次都得问老张。写进仓库,谁都不用问。
  • 发现成本:agent 为了在仓库里找到一条关键信息需要消耗多少上下文。信息放得越隐蔽,发现成本越高,留给实际任务的预算越少。
  • 知识衰减率:仓库中单位时间内变得过时的知识条目比例。文档和代码脱节是最大的敌人,比没有文档更危险的是过时的文档——它会让 agent 走错方向还以为自己是对的。

这不是"写更多文档"的问题,而是"把决策信息放到正确的位置"的问题。一份在 src/api/ 目录下、50 行的 ARCHITECTURE.md,比一份在 Confluence 里、500 页但没人维护的设计文档有用得多。

怎么检验地图画得好不好:"全新会话测试"

开一个全新的 agent 会话,只让它看仓库内容,看它能不能回答五个基本问题:

  1. 这是什么系统?
  2. 怎么组织的?
  3. 怎么运行?
  4. 怎么验证?
  5. 现在进度到哪了?

如果它答不上来,说明地图上有空白。空白的地方,agent 就得自己猜,猜错了就是 bug,猜多了就浪费上下文。每个新会话都要猜一遍,猜的成本远高于一开始就把地图画好。

画地图的三条原则

  • 知识靠近代码。 一条关于 API 端点认证的规则,应该放在 API 代码旁边,而不是藏在一个巨大的全局文档里。模块目录本身就是天然的索引,agent 读到代码就能读到约束。
  • 用标准化的入口文件。 AGENTS.md(或 CLAUDE.md)是 agent 的"着陆页",50-100 行就够了。它不需要包含所有信息,但必须能让 agent 快速回答"这是什么项目"、"怎么跑"、"怎么验证"。
  • 最小但完备 + 和代码一起更新。 删掉某条规则不影响决策质量,那条规则就不该存在;但全新会话测试中的每个问题都必须有答案。把知识更新跟代码变更绑定,最简单的方法是把架构文档放在对应模块目录下。

用 ACID 原则管理 agent 状态

这个类比来自数据库的事务管理:

  • 原子性:每次"逻辑操作"(比如"添加新端点并更新测试")用一个 git commit 原子化。要么全做,要么不做,没有"做了一半"。
  • 一致性:定义"一致状态"的验证谓词,比如所有测试通过、lint 无报错。Agent 每次操作后跑验证,不一致的中间状态不要 commit。
  • 隔离性:多个 agent 并发工作时,每个 agent 用独立的进度文件,或者用 git 分支隔离。并发写入同一文件是出问题的常见原因。
  • 持久性:关键的项目知识用 git 跟踪的文件持久化。脑子里的不算,写在纸上的才算。

我踩过的坑:巨型指令文件

你开始认真对待 harness 了,建了个 AGENTS.md,把能想到的所有规则都塞了进去。一个月后 300 行,两个月 450 行,三个月 600 行。然后你发现 agent 的表现反而变差了。这就是"巨型指令文件"陷阱,它带来五个问题:

  1. 上下文预算被吃掉了。 假设 agent 有 200K tokens 的窗口,一个膨胀的指令文件可能占掉 10-20K(8-15%)。到真正需要理解代码的时候,预算已经不够了。
  2. 中间迷失。 Liu et al. 2023 年的《Lost in the Middle》证明:LLM 对长文本中间部分的信息利用效率显著低于两端。埋在 600 行文件第 300 行的安全硬约束,几乎注定被忽略。
  3. 优先级冲突。 硬约束、设计指导、历史教训混在一起,格式和位置一模一样,agent 没有可靠信号区分红线和建议。
  4. 维护衰减。 删除指令的后果不确定,加新指令无成本,文件只增不减,信噪比持续下降。
  5. 矛盾累积。 不同时期加的指令开始互相矛盾,agent 每次随机选一条遵循。

正确的拆分姿势

核心一句话:常用信息放手边,偶尔用的收起来,用不上的别带。

入口文件 AGENTS.md 控制在 50-200 行,只放最常用的东西:

# AGENTS.md

## 项目概览
Python 3.11 FastAPI 后端,PostgreSQL 15 数据库。

## 快速开始
- 安装:`make setup`
- 测试:`make test`
- 完整验证:`make check`

## 硬约束
- 所有 API 必须走 OAuth 2.0 认证
- 所有数据库查询必须用 SQLAlchemy 2.0 语法
- 所有 PR 必须通过 pytest + mypy --strict + ruff check

## 专题文档
- API 设计规范(docs/api-patterns.md)— 添加新端点时必读
- 数据库操作约束(docs/database-rules.md)— 涉及数据库修改时必读
- 测试标准(docs/testing-standards.md)— 编写测试时参考

每个专题文档 50-150 行,按主题放在 docs/ 目录下或对应模块目录旁,agent 只在需要时才去读。还有些信息直接放在代码里更合适(类型定义、接口注释、配置文件说明),agent 读代码时自然能看到,不用在指令里重复。

每条指令都应该标明:来源(为什么加这条规则)、适用条件(什么时候需要)、过期条件(什么情况下可以删掉)。定期审计,像管理代码依赖一样管理指令。如果某条指令必须在入口文件里,放顶部或底部,不要放中间——"中间迷失"效应告诉我们,重要信息放在文件两端被记住的概率更高。

实际数据:一个 SaaS 团队的 AGENTS.md 从 50 行膨胀到 600 行,简单 bug 修复任务中 agent 花大量上下文处理无关部署指令,安全约束"所有数据库查询必须用参数化查询"埋在中间经常被忽略。拆分重构(入口裁剪到 80 行 + 专题文档)后,同一任务集的成功率从 45% 提升到 72%,安全约束遵循率从 60% 提升到 95%。


二、让会话连续:状态子系统

那个让人崩溃的时刻

你让 Claude Code 帮你实现一个完整的功能,它跑了 30 分钟,做了大部分工作,但上下文快满了。你开个新会话继续,然后发现:它不记得上次做了什么决策、为什么选了方案 A 而不是方案 B、哪些文件已经改过、测试跑到什么状态了。它得花 15 分钟重新探索一遍项目,而且可能跟上次的做法不一致。

这就是"断片",也是长任务最让人崩溃的地方。

为什么"更大的窗口"解决不了

上下文窗口是有限的。这不是一个可以通过模型升级解决的问题,即使窗口大小增长到 1M tokens,复杂任务依然会用完。因为 agent 不只是在生成代码,它还要理解代码库、跟踪自己的决策历史、处理工具输出、维护对话上下文。这些信息加起来增长得比窗口扩容快得多。

更深层的问题在于,agent 产生的信息不是均匀重要的。中间推理步骤包含决策的"为什么"——为什么选了方案 A 而不是 B,为什么用了这个库而不是那个库。最终输出只包含"是什么",即代码本身。压缩策略通常保留后者但丢了前者——下一个会话看到代码却不知道为什么这么写,可能会"优化"掉一个有意为之的设计决策。

Anthropic 还观察到一个现象:当 agent 感觉上下文快满了,它会表现出"赶工收尾"的行为——匆忙结束当前工作、跳过验证步骤、选简单的方案。这被称为**"上下文焦虑"**。

连续性断了以后会发生什么

  • 决策漂移:上个会话花了大量预算分析三种方案,最终选了方案 B。新会话不知道分析过程,基于不完整信息重新决策,可能选了方案 A。同样的信息,不同的结论。
  • 重复劳动:新会话不确定某项工作是否已完成,重新做了一遍;或做了一半发现跟已有实现冲突,需要返工。
  • 方向偏离:几个会话累积下来,每个新会话对项目目标的理解都略有偏差,偏差一层层叠加,最终结果可能跟最初的意图相去甚远。
  • 验证缺口:上个会话的验证结果没记录,新会话得重新跑一遍验证才能了解当前状态,每次都浪费宝贵的上下文。

我的解法:把它当成"会失忆的工程师"

核心思路:把 agent 当成一个每次会话都会清空短期记忆的工程师来管理。 每次它要"下班"之前,必须把关键信息写下来,让下一个"接班"的 agent 能快速上手。

工具 1:进度文件(PROGRESS.md),记录"做完了什么、正在做什么、卡在哪":

# 项目进度

## 当前状态
- 最新 commit: abc1234 (feat: add user preferences endpoint)
- 测试状态: 42/43 通过 (test_pagination_edge_case 失败)
- Lint: 通过

## 已完成
- [x] 用户模型和数据库迁移
- [x] 基础 CRUD 端点

## 进行中
- [ ] 分页功能 (90% - 边界条件测试失败)

## 已知问题
- test_pagination_edge_case 在空结果集时返回 500

## 下一步
1. 修复分页边界条件 bug
2. 添加"是否包含已删除用户"的查询参数
3. 更新 API 文档

工具 2:决策日志(DECISIONS.md),记录"什么决策、为什么、什么时候做的"——不需要详细设计文档,几行说清楚就行:

# 设计决策

## 2024-01-15: 使用 Redis 缓存用户偏好
- 原因: 读取频率高(每次 API 调用都需要),数据量小
- 否决方案: 用 PostgreSQL 物化视图(变更频率高,物化视图维护成本不划算)
- 约束: 缓存 TTL 设为 5 分钟,写入时主动失效

工具 3:git 提交作为检查点。 每完成一个原子工作单元就提交,commit message 要说清楚做了什么和为什么。这是免费的、自动版本化的状态快照。

工具 4:init.sh 或初始化流程。AGENTS.md 里写明每次"上班"和"下班"的流程:

## 每次会话开始时(上班)
1. 读 PROGRESS.md 了解当前状态
2. 读 DECISIONS.md 了解重要决策
3. 跑 make check 确认仓库处于一致状态
4. 从 PROGRESS.md 的"下一步"部分继续工作

## 每次会话结束前(下班)
1. 更新 PROGRESS.md
2. 跑 make check 确认一致状态
3. 提交所有已完成的工作

压缩 vs 重置:两种上下文管理策略

Anthropic 在 2026 年 3 月的研究揭示了上下文焦虑的具体表现,也给出了两种应对策略:

  • 压缩(Compaction):在同一个会话里把早期对话摘要化。优点是保留连续性,agent 能看到"是什么"。缺点是"为什么"经常在摘要中丢失。更关键的是,压缩并不能消除上下文焦虑——agent 知道上下文曾经很大,心理上仍然倾向于加速收尾。
  • 重置(Context Reset):完全清空上下文,开新会话,从持久化工件重建。优点是干净的心理状态,没有"我快没时间了"的焦虑。缺点是依赖交接工件的完备性。

Anthropic 的数据:对 Sonnet 4.5,上下文焦虑足够严重,压缩单独不够用,重置成为关键组件;但对 Opus 4.5,这种行为大幅减弱,可以不依赖重置。这意味着 harness 设计需要对目标模型有具体的理解,而不是套用通用模板。

混合策略:短任务(30 分钟以内)可以在同一个会话里完成,长任务(跨会话)必须用进度文件和决策日志维持连续性。判断标准:如果任务需要的上下文超过窗口的 60%,就开始准备交接。

实际数据:一个 agent 被要求实现带用户认证的博客系统,12 个功能点,预计 5 个会话。没有状态持久化时,到会话 5 只完成 7 个功能点,其中 3 个有隐含正确性问题;有状态持久化后,所有 12 个功能点完成且通过验证。重建时间减少约 78%,功能完成率从 58% 提升到 100%,隐含缺陷率从 43% 降到 8%。

初始化要独立成一个阶段

一个常见的低效模式:让 agent 直接开始做功能,它上来就写代码,但很快发现测试框架没配好、环境有问题、结构不清晰,大量时间花在"搞清楚这个项目怎么运作"上。

初始化和实现的优化目标完全不同:实现阶段的目标是最大化已验证功能的数量和质量,初始化阶段的目标则是最大化后续所有实现的可靠性和效率。 混在一起,agent 面临一个多目标优化问题,自然倾向于写代码(直接可见的产出)而牺牲基础设施(价值只能在后续会话中体现)。

正确做法:第一个会话只做初始化,不写业务代码。 产出一套基础设施:

  1. 可运行的环境——项目能启动、依赖都装好
  2. 可验证的测试框架——至少有一个示例测试能通过
  3. 启动就绪清单文档——启动命令、当前状态、项目结构
  4. 任务分解——把项目拆成有序任务列表,每个任务有明确验收标准
  5. git 提交作为检查点——初始化完成后提交一个干净的 checkpoint

验收就四个字:能启动、能测试、能看进度、能接手下一步。

## 初始化验收清单
- [ ] `make setup` 从零开始能成功
- [ ] `make test` 至少有一个测试通过
- [ ] 新的 agent 会话能只看仓库回答"怎么跑"和"怎么测"
- [ ] 任务分解文件存在且有至少 3 个任务
- [ ] 所有内容已提交到 git

热启动策略:不要从空目录开始。用项目模板(create-react-app、fastapi-template 等)预置好标准的目录结构、依赖配置和测试框架,只留下项目特有的初始化工作。

Anthropic 的实验数据:使用独立初始化阶段的项目,多会话场景中的功能完成率比混合方式高 31%,初始化阶段投入的时间在后续 3-4 个会话中就能完全收回。有人担心"单独初始化是不是浪费了一个会话",我的体会恰恰相反——前期把地基打牢,后面反而更快。


三、让任务有边界:约束与验证

Agent 总想做太多

让 agent 加个用户认证,它同时开始改数据库 schema、写路由、改前端组件,还顺手重构了错误处理中间件。两个小时后一看,12 个文件被修改,800 行新代码,但没有一个功能是端到端跑通的。

这是两个相关的问题:

  • 过度延伸(Overreach):agent 在一次会话中激活的任务数量超过最优值。可以量化:同时做 5 个功能但 0 个跑通,就是 overreach。
  • 不足完成(Under-finish):已启动的任务中,通过端到端验证的比例低于阈值。写了代码但没跑通测试,就是 under-finish。

这两个问题互相加剧,形成恶性循环:overreach 导致注意力分散,注意力分散导致 under-finish,under-finish 留下的半成品代码增加系统复杂度,进一步导致下一个任务的 overreach。

注意力是有限的资源

这本质上是数学问题。假设 agent 的上下文容量为 C,同时激活 k 个任务,每个任务平均获得 C/k 的推理资源。当 C/k 低于完成单个任务所需的最小阈值时,所有任务都做不完。

Anthropic 的实验数据直接支持这一点:使用"小下一步"策略(等价于 WIP=1)的 agent,任务完成率比使用宽泛提示的 agent 高 37%。更有意思的是,agent 生成的代码行数和实际完成的功能数量呈弱负相关——写得越多,完成得越少。

用 Kanban 的语言说,Little 法则告诉我们 L = λ × W:如果在制品数量 L 过大,每个任务的前置时间 W 必然增加,失败概率被放大。

我的解法:WIP=1

"WIP" 是"在制品"的意思,来自看板方法。核心就是:任何时刻只允许一个任务处于"进行中"状态。

CLAUDE.mdAGENTS.md 里明确写:

## 工作规则
- 每次只做一个功能点
- 当前功能点端到端验证通过后,才能开始下一个
- 不要在实现功能 A 时"顺便"重构功能 B

配合上一条:完成必须有"证据"——不是"代码看起来没问题",而是"验证命令跑过了"。

功能清单:让"做完"有标准

这是整个 harness 的基础结构,不是给人看的备忘录。调度器靠它选任务,验证器靠它判完成,交接器靠它生成报告。没有它,这些组件就没有可以依赖的共识。

每个功能项包含三个要素(三元组):

  1. 行为描述:做什么
  2. 验证命令:怎么算做完
  3. 当前状态:现在到哪了

每个功能项有四种状态:not_startedactiveblockedpassing状态转移由 harness 控制,不是 agent 想改就能改——agent 不能直接把状态改成 passing,只能提交验证请求,由验证命令的结果决定。功能从 active 变成 passing 的唯一方式是验证命令执行成功,且这个转移不可逆。

{
  "id": "F03",
  "behavior": "POST /cart/items with {product_id, quantity} returns 201",
  "verification": "curl -X POST http://localhost:3000/api/cart/items -H 'Content-Type: application/json' -d '{\"product_id\":1,\"quantity\":2}' | jq .status == 201",
  "state": "passing",
  "evidence": "commit abc123, test output log"
}

CLAUDE.md 里写清楚规则:

## 功能清单规则
- 功能清单文件: /docs/features.md
- 每次只激活一个功能项
- 功能项验证命令必须通过才能标为 passing
- 不要修改功能清单的状态,由验证脚本自动更新

粒度校准:每个功能项应该是"一次会话能完成"的范围。太粗了做不完,太细了管理开销大。"用户可以添加商品到购物车"是一个好粒度,"实现购物车"太粗了,"创建 Cart 模型的 name 字段"太细了。

实际数据:一个 8 个功能点的 REST API 项目,无约束模式下 agent 第一个会话同时启动 5 个功能,产出约 800 行代码、涉及 12 个文件,端到端测试通过率只有 20%;WIP=1 模式下第一个会话只做用户注册,产出约 200 行代码、涉及 4 个文件,端到端测试 100% 通过。到第 4 个会话结束,完成率 87.5% vs 37.5%。另一个电商平台用结构化功能清单,功能完成率比自由形式高 45%,零重复实现。


总结

  • 仓库:不在仓库里的知识,对 agent 来说就是不存在。把关键信息写进仓库,是最基本的投入。用"全新会话测试"检验仓库质量。
  • 指令:"加条规则"是短期的止痛药、长期的毒药。入口文件是路由器,不是百科,50-200 行;专题文档按需展开。
  • ACID:用原子提交、一致性验证、隔离并发、持久化关键知识来管理 agent 状态。
  • 连续性:上下文窗口是有限资源,长任务一定会跨会话,跨会话一定会丢信息。解法不是更大的窗口,而是更好的状态持久化:进度文件 + 决策日志 + git 检查点。重建成本是关键指标,好的 harness 应该让新会话 3 分钟内恢复工作状态。
  • 初始化:初始化和实现分开。用"启动就绪清单"四条件验收:能启动、能测试、能看进度、能接手下一步。热启动优于冷启动。
  • 边界:WIP=1 是最安全的默认值——做完一个再做下一个,别让它并行。"少做但做完"永远优于"多做但做半"。
  • 功能清单:完成必须可验证:"代码看起来没问题"不算完成,"命令返回 201"才算。它是 harness 的原语,状态转移由 harness 控制。粒度控制在"一次会话能完成"的范围。

下一篇:进阶:可靠验证、可观测性与自动循环

参考资料

相关文章

评论