发布时间

Harness Engineering(1):适用于长时间运行应用程序开发的框架设计

发布时间

篇幅

8,356 字数

模型之外的那一层,正在成为 AI 时代的基础设施

HarnessAgentEngineering

修的几乎从来不是模型

做 Agent 这两年,遇到一个反复出现的模式。

每次 Agent 跑砸了,大家的第一反应是——模型不够聪明。真坐下来修的时候,几乎每次修的都不是模型。

以前的Coding Agent没有什么乱七八糟的MCP,Skills这些东西,就是大家调侃的“左侧与神明对话”,早期的Coding Agent只有Tool和Rules(比如Cursor),那时候去改一个大项目,往往每个模块都有一个功能一致的utils,变相的在项目里堆数个石山代码。

出乎意料的是,大家一致的把问题归咎于“模型不够聪明”的问题,然而事实上并非如此。于是提示词工程开始发力,大家把越来越多的,风格各异的提示词塞给Agent——于是上下文堆积越来越多,问题依然没有被解决,幻觉越来越大了。

在今年早些的时候,我偶然注意到了一篇文章,讲的大致内容是:Cloudfare用ai复制了一个Next.js,然而这个Next.js在源码解读后,实现的很糟糕。有意思的一点是,对于Next.js的测试,几乎是原封不动的搬运过去的。于是我开始尝试在代码中添加测试,使用Vitest,虽然token消耗更高了,但实际上输出的内容几乎完全符合要求——正确率得到了大幅度的提升与确认。于是我制作了Vite-browser,一个针对于给AI进行调试vite应用的自我审查工具CLI,随后更多的工具涌入了我的视野:Agent-browser , next-browser等一系列针对于Agent自我审查以及调试的工具CLI,他们正在让自己的框架更适配AI,我想这就是第一个转变:当人不再写代码后,工具链应该更适配于人。

还有一些有意思的现象:很多的文档页面开始添加两个按钮copy as markdown和open in chat,我认为最早被发现的应该是superpower,spec-kit和ralph。他们都尝试将更多的开发流程适配于Agent,并尝试制定一套规范能让其进行自主的运转。

2026 年这一层有了一个名字。


Agent = Model + Harness

Mitchell Hashimoto 2026 年 2 月写下这个等式之前,很多工程师已经在做这件事,只是没有统一的词。

他观察到自己用 AI Agent 写代码时的一个习惯——每次 Agent 犯错,他不修那次具体的输出,而是在 Agent 的环境里做一个永久性的修复。比如 Agent 总是在 commit message 里漏写 ticket number,他不会每次都提醒 Agent,而是在 AGENTS.md 里加一条规则,让这个错误从此不可能发生。他把这个动作叫 "engineering the harness"。

几周之内,OpenAI 和 Anthropic 相继发了工程博客把这个词展开。Thoughtworks 的 Birgitta Böckeler 把它压缩成一行:

Agent = Model + Harness

Harness 是 Agent 里除了模型之外的所有东西。Tools、prompts、repo 结构、反馈回路、验证机制、执行环境——都属于 Harness。Harness Engineering 就是把这一层当作主要工程对象来设计和优化。

这个定义立得住,是因为它把三层工程关系讲清楚了。

Prompt engineering 作用在单轮对话——你怎么跟模型说话。比如你在一次 API 调用里,把 "review this code" 改成 "act as a senior engineer. Review this code for security vulnerabilities, performance issues, and maintainability. List each finding with severity and suggested fix." 这是 Prompt 工程——在一次请求里把指令讲清楚。

Context engineering 作用在单个任务——模型应该看到什么、看不到什么。比如你在让 Agent 修一个 bug,你决定要不要把整个相关文件放进 context、要不要同时放进 test 文件、要不要放进 git blame、要不要放进最近三次的 commit message。这些决定决定了模型手里有什么牌,但还没到模型怎么出牌。

Harness engineering 作用在整个 Agent 的生命周期——当它自主运行几小时、做几百个决策时,它的整个环境怎么设计。想象一个 Agent 被丢进一个没见过的 repo,要在 6 小时内完成一个有 14 个 feature 的产品原型。它需要知道去哪找需求、按什么顺序做、做完一个 feature 怎么保存进度、下一个 Agent 启动时怎么接着干、测试失败时该重试还是该停下来问人。这些问题没有一个是 "Prompt 怎么写" 能回答的。这是 Harness 的战场。

三者不是替代关系。它们的作用范围逐层放大,但下一层做得再好,也救不了上一层的失败。

比如你可以把 Prompt 工程做到极致——每个 system prompt 都是反复调教过的,带着 few-shot 例子、角色设定、输出格式约束。你也可以把 Context 工程做到极致——RAG 命中率 95%,只把最相关的 chunk 送进模型。但如果你的 Harness 是坏的——比如 Agent 每次重启都丢失上下文、subagent 之间没有共享记忆、失败任务没有结构化的重试机制——Agent 还是跑不稳。它会在每一次单独的调用上都很聪明,但跨调用的那个"更大的任务"永远完不成。

反过来也成立。Harness 做得扎实的环境里,一个能力只算中等的模型能完成令人惊讶的复杂任务。OpenAI 用 Codex(不是他们最强的模型)写了一百万行生产代码,靠的不是模型本身,是他们围绕 Codex 搭起来的那一整套环境。


失败时不要 try harder

OpenAI 在 2026 年 2 月公开了一个内部实验。三个人的团队,五个月,用 Codex 写了一百万行生产代码,1500 个 PR,全程没有人手写代码。平均到每个工程师,每天 3.5 个 PR 合并进主分支。

这个数字本身已经够刺激了,但真正值得读的是他们对"失败"的归因:

"When something failed, the fix was almost never 'try harder.' Because the only way to make progress was to get Codex to do the work, human engineers always stepped into the task and asked: 'what capability is missing, and how do we make it both legible and enforceable for the agent?'"

—— OpenAI, Harness engineering: leveraging Codex in an agent-first world

我把这段话翻译成工程动作。它明确排除了三件事:

不是写更长的 prompt。 你想象一下这个场景——Agent 在一个 monorepo 里找不到某个 workspace 的依赖,你在 prompt 里加一句"记得去 workspace 的 package.json 里找依赖"。下次 Agent 又在一个新 workspace 里犯了同样的错,你又加一句。三个月后你的 prompt 有 2000 字,其中 1500 字是历史上踩过的坑。这不叫修复,这叫打补丁。

不是换更强的模型。 如果你用 Sonnet 失败了,换 Opus 可能暂时好了,但同类问题会在新的复杂度阈值上再次出现。你只是把问题推迟了三个月。

不是多重试几次。 如果同一个任务跑三次有两次失败,问题不在那次偶然成功的运气,在系统性的设计缺陷。重试只是概率上的掩盖。

它要求的是——在 Agent 的环境里装一个永久性的修复,让这类错误不可能再发生

"找不到依赖"这个例子,永久性修复应该是:在 repo 根目录加一个 AGENTS.md 条目 "monorepo 的 workspace 依赖在各自的 package.json 里",或者更强的做法,写一个 agent-callable 的工具 find_dependency(package_name),让 Agent 不需要知道项目结构也能找到依赖。错误从此不可能再发生,因为环境不允许它发生。

Anthropic 的 context anxiety 案例

Anthropic 的长任务 harness 博客里有一个非常具体的应用。

他们在用 Claude Sonnet 4.5 跑一个长任务——让 Agent 用 React + Vite + FastAPI 搭一个 2D retro game maker,包含关卡编辑器、精灵编辑器、实体行为系统、游戏模式。一个 prompt 丢进去,Agent 要连续跑六小时。

他们观察到一种特定的失败模式:**当 context window 接近它认为的上限时,Agent 会开始草率收尾。**不是因为工作做完了,是因为它"感觉到"空间快用完了。Sprint 的最后几个 feature 开始被 stub 掉、测试变得敷衍、文档从详细变成 TODO。Anthropic 管这个叫 context anxiety

如果停在"模型不够聪明"这个归因上,解法就是等下一代模型。但 Anthropic 没有等。

他们改了 harness。具体做法是——Agent 每完成一个 sprint,主动清空上下文,开一个全新的 Agent 继续跑,通过一个结构化的 handoff 文件(记录 sprint 交付物、当前代码状态、未解决的 bug、下一个 sprint 的目标)承载跨会话的状态。新的 Agent 启动时 context 是干净的,不会看到"前面已经写了 80k token"的压力,就不会触发 anxiety。

这套组合他们叫 context reset——直接让 Sonnet 4.5 跑完了原本跑不完的六小时任务。

等到他们迭代到 Opus 4.6,发现模型自己就能 handle 跨会话的任务分解。4.6 在长 context 里不再表现出 anxiety,能自己判断"空间还有多少、应该怎么分配"。所以他们做了一件看起来"倒退"的事——把 sprint 构造整个删掉,Generator 在一次连续会话里跑两个多小时,不切 session。

Evaluator 没删。在 4.6 能独立完成的任务上它确实是 overhead,但在模型能力边缘的任务上它仍然提供关键 lift。比如 Agent 写了一个 DAW 应用,表面上看 UI 漂亮、功能齐全,但 Evaluator 用 Playwright 真的去点了一遍之后发现——"Audio recording is still stub-only. Clip resize by edge drag not implemented."这种深层 bug,模型自己不会发现,必须有外部视角的验证者。

Anthropic 原文那句话值得记下来:

"The space of interesting harness combinations doesn't shrink as models improve. Instead, it moves."

Harness 不是模型变强之后就会消失的脚手架。它是一个会随模型能力往外移的边界作业层——模型能做的事变多,Harness 就在新的边界上做工。Sonnet 4.5 时代的 sprint 分段,在 4.6 时代已经不需要了;但 4.6 时代的 Evaluator,在 4.5 时代甚至可能都 tune 不起来。


Compaction is the devil

Geoffrey Huntley 从一个完全不同的角度看到了同一件事。

Ralph 是他在 2025 年 5 月提出的 Agent coding 技术,到 2026 年初彻底在社区火了起来。它的核心洞察来自 Huntley 在播客里的一句话:

"Compaction is the devil."

Compaction 是 Claude Code 等工具在 context 塞满时自动压缩对话历史的机制。听起来是好事——毕竟 context window 有限,总得有办法处理超长对话。

但 Huntley 观察到一件事。想象 Agent 在做一个长任务,context 里前面有一句关键指令——"所有 API endpoint 必须用 zod schema 做输入验证"。跑到第 80 轮的时候,这条指令被 compaction 压缩成"使用类型验证"。Agent 看到这句模糊的摘要,决定用 TypeScript 的 interface 就够了——毕竟 "类型验证" 嘛。于是后面 20 个 endpoint 全部漏掉了运行时的 zod 验证,但 Agent 自己觉得做得对。

一旦历史被压缩,Agent 就只能依赖自己生成的摘要继续工作。摘要本身是有损压缩,对原始意图的理解开始漂移。漂移一次你看不出来,漂移五次之后 Agent 已经在做一件和你原本要求完全不同的事。

Anthropic 的解法是 context reset + handoff artifact——主动控制清空时机,用结构化文档承载关键状态。Huntley 的解法更极端——**干脆让 Agent 每跑完一个任务就退出,下一轮从头来。**根本不给 compaction 发生的机会。

两个团队,两个完全不同的解法。但归因方式一致——Agent 跑不稳,不是因为模型弱,是因为 Harness 在某个具体位置没把该处理的情况处理掉。找到那个位置,做永久修复。


Ralph 和 Spec-Kit:两种截然不同的工程化路径

在 Harness Engineering 这个词被命名之前,一批独立的工程实践已经在同时涌现。

多个团队在不同场景下撞到同一个问题,做出相似的解法——这本身就是信号。这层工程是真实存在的需求,不是炒作。

Ralph 和 Spec-Kit 是当前讨论度最高的两个实践。它们代表了两个截然不同的走向。

Ralph:执行优先的持久化循环

Ralph 的核心实现简单到看起来不像是一个技术:

code
while :; do cat PROMPT.md | claude -p; done

一个 bash 无限循环。每一轮启动一个新的 Claude Code 进程,让它读 PROMPT.md,做一点事,然后退出。下一轮再来。

简单的地方到此结束。聪明的地方才开始。

Ralph 的状态不在对话历史里,而是在磁盘上的几个文件中:

  • PROMPT.md —— 每轮都会被加载的主指令
  • AGENTS.md —— 项目约定和构建指令
  • specs/ —— 规格文件目录
  • IMPLEMENTATION_PLAN.md —— 当前任务列表,Agent 自己维护

想象一个具体的 Ralph 运行过程。第 1 轮,Agent 启动,读 PROMPT.md 里的指令——"查看 IMPLEMENTATION_PLAN.md,选一个未完成的任务,实现它,跑测试,测试通过就 commit,然后退出"。Agent 读了 plan,看到第一个任务是"实现 user login endpoint",于是写代码、跑测试、测试过了、commit、退出。

第 2 轮,全新的 Claude Code 进程启动——它不知道上一轮发生过什么。但它读 PROMPT.md、读 IMPLEMENTATION_PLAN.md,看到第一个任务已经标记为 done,第二个任务是 "实现 user logout endpoint",于是继续写这个。它看到 src/routes/auth/login.ts 已经存在,就参考这个文件的风格写 logout.ts。知识不在它的 context 里,知识在 repo 里。

这样跑 50 轮、100 轮,整个 feature 就建成了。没有任何一轮 Agent 的 context 超过它能处理的复杂度,但整个项目最终完成。

这个设计的核心洞察 Huntley 自己一句话讲清了:

"carve off small bits of work into independent context windows."

把工作切成小块,每一块有自己的干净 context。它不是让 Agent 跑得更久,而是承认"Agent 跑久了会漂移"这个事实,然后用循环和磁盘状态绕过这个问题。

控制流在三个地方:

  • Scope discipline —— PROMPT.md 明确要求"一次做一件事,测试通过就提交"。Agent 不会试图一次做完所有任务,它的 scope 被 prompt 硬编码成"一次一个"。
  • Backpressure —— 测试或构建失败,Agent 必须修完才能提交。如果 login endpoint 的测试失败,Agent 不能退出,必须继续调试。失败本身就是反馈信号,循环机制自动处理重试。
  • Natural completion —— 任务做完 Agent 自然退出,循环下一轮自然跳到下一个任务。整个系统没有"全局进度控制器",进度就在文件里。

Ralph 的美学在这里——deterministically bad in an undeterministic world(Huntley 原话)。它不追求单轮的正确,它追求在足够多轮之后整体收敛。每一轮可能走错、可能低质量、可能绕远路,但磁盘上的状态一直在累积,测试一直在把关,最终会收敛到目标。

Spec-Kit:规格优先的结构化流程

Spec-Kit 是 GitHub 在 2025 年 9 月开源的工具包,走的方向几乎和 Ralph 相反。它的核心不是循环,是一条严格的前置流程。

想象你要开发一个 feature——一个让用户通过邮件订阅博客的功能。用 Spec-Kit 的流程大概是这样:

**第一步,/specify。**你跟 Agent 说 "我想让读者能订阅博客,每周自动发最新文章"。Agent 不会立刻开始写代码。它会产出一个 spec.md,里面写:"用户故事:作为博客读者,我希望输入邮箱订阅博客,这样我能每周收到新文章。验收标准:① 订阅表单在文章底部 ② 订阅后收到确认邮件 ③ 每周日早上 9 点发一封包含本周新文章的邮件 ④ 用户可以一键退订。非目标:不支持分类订阅、不支持 RSS、不做会员付费。"Agent 可能会反问你"退订链接在邮件里还是在独立页面?"——这种 back-and-forth 是规格阶段的正常迭代。

**第二步,/plan。**Agent 读你定好的 spec,产出 plan.md,里面写:"技术栈:Next.js + Resend。存储:Supabase 里新建 subscribers 表(email, confirmed_at, unsubscribed_at, token)。核心流程:① POST /api/subscribe 接收邮箱,写入数据库,通过 Resend 发确认邮件 ② 确认邮件里的 token 链接到 /confirm?token=xxx,更新 confirmed_at ③ Vercel cron 每周日触发 /api/weekly-digest,查询上周文章,发送给所有已确认订阅者。"同时产出 contracts/subscribe.ts 定义 API schema,产出 data-model.md 描述数据库表结构。

**第三步,/tasks。**Agent 读 spec 和 plan,产出 tasks.md:

code
T1. 建立 subscribers 表迁移
T2. 实现 POST /api/subscribe
T3. 实现 Resend 邮件模板(确认邮件)
T4. 实现 /confirm 页面
T5. 实现 /api/weekly-digest cron [依赖 T1, T2]
T6. 实现 Resend 邮件模板(每周摘要)
T7. 实现退订链接

并标记出哪些任务可以并行(比如 T3 和 T4 没依赖关系,可以并行)。

**第四步,实现。**Executor agent(或者 Ralph 这种循环)开始按 tasks.md 一个一个做。每完成一个任务就标记 done、commit。

还有一个跨 feature 的 constitution.md——项目级的不可协商原则。比如"所有用户输入必须用 zod 验证"、"所有邮件模板必须在 Resend dashboard 里预览过"、"所有 cron 任务必须有 observability"。这些规则不是某个 feature 的,是整个项目的。

GitHub 对这套流程的核心信念讲得很直接:Specs as executable artifacts —— 规格不再是 Confluence 里没人看的文档,而是 Agent 每次运行都会读取、用来决策的 ground truth。

和 Ralph 形成鲜明对比的是,Spec-Kit 在 Agent 开始写代码之前投入了大量时间做规格对齐。但一旦规格写完,Agent 就有了一个清晰的、结构化的合同,能按这个合同高效执行,而不是在实现过程中反复猜测意图。"这个按钮应该放在哪?""发邮件用什么服务?""数据库表怎么建?"——这些问题都在规格阶段就定了,实现阶段 Agent 只需要执行,不需要猜。

差异背后,共享三个底层设计

Ralph 和 Spec-Kit 看起来对立,但它们共享三个底层设计。这三点比差异本身更值得注意。

状态全部落在 git-versioned 的 markdown 文件里。

不在对话历史里,不在内存里,不在某个 orchestrator 的数据库里。就在 repo 里。Agent 读文件,改文件,commit 文件。

这个设计的威力在于 Agent 的工作现在完全符合人类工程流程。你可以 git log 看到 Agent 做了什么、git diff 看到它改了什么、git blame 看到某一行为什么是现在这个样子、git revert 撤销它。PR review 流程不需要改,code review 工具不需要改,CI/CD 不需要改。Agent 就像一个速度很快但需要约束的工程师,它的产出物和人类工程师的产出物完全同构。

都在区分不同生命周期的工件。

Ralph 有 PROMPT.md(每轮必读)、AGENTS.md(稳定约定)、specs/(规格)、IMPLEMENTATION_PLAN.md(动态任务)。Spec-Kit 有 constitution.md(跨 feature 的宪法)、spec.md(单 feature 的 what)、plan.md(技术计划)、tasks.md(任务队列)。

注意它们都在区分不同"变化速度"的信息。constitution.mdAGENTS.md 一年可能只改几次,spec.md 在 feature 开发周期内稳定,tasks.md 每天都在动。把这些信息混在一个文件里,你每天都在 commit 一个 1000 行的文件的微小变更,git history 就废了。分开之后,每个文件的 commit history 都讲一个连贯的故事——某个规则什么时候加的、某个 feature 什么时候完成的、某个 bug 什么时候修的。

它们都意识到同一件事——把所有信息塞进一个文件是错的,因为这些信息的变化速度不一样

都不是 framework。

它们是 convention + scripts + markdown templates 的组合。没有运行时依赖,没有 SDK,没有 "install this library and pass your agent"。

Ralph 的全部实现可以装进 20 行 bash 脚本和几个 markdown 模板。Spec-Kit 是一个 Python CLI,但它做的所有事情就是把模板文件复制进你的 repo,然后注册几个 slash command 让 Agent 能调用。没有 agent runtime、没有 orchestration engine、没有 state machine。

Harness 的工程化目前是靠约定和工件胜出,不是靠代码抽象。这是一个很重要的信号——它告诉我们 Harness 这一层现在还在"发现正确 pattern"的阶段,远没到"封装成 framework"的阶段。任何现在声称自己是"THE Harness Framework"的东西,多半都太早。


三个文件,三种生命周期

社区的实践都停留在单个 feature 的开发流程——spec → plan → task。对一个长期演进的项目,这个粒度还不够。

我在 z0 的开发过程里演化出一套三文件的分工。后来发现 Amazon 的 Kiro 用的是完全一样的三个文件名——requirements.md / design.md / tasks.md

两个团队在不同场景独立收敛到同一个结构。这本身说明这是一个工程上的自然解。

requirements.md —— 这个 feature 要实现什么

从用户视角描述 what,不涉及怎么做。

核心内容是三块:用户故事、验收标准、明确的非目标。

用户故事用 "As a ... I want to ... so that ..." 的形式,比如 "As a blog reader, I want to subscribe with my email, so that I can get weekly updates without visiting the site"。

验收标准具体到"用户能做什么、做完之后看到什么",比如 "用户点击订阅按钮后,3 秒内收到确认邮件;点击确认邮件里的链接后,页面显示'订阅成功'"。

**Non-goals 比 goals 更重要。**这是对后续 scope creep 最强的防御。比如"这个 feature 不支持分类订阅、不支持 RSS、不做付费会员"。每一条 non-goal 都是在 feature 后期挡住一次"要不要顺便做一下"的冲动的护栏。没有 non-goals 的 feature 最终都会膨胀成半个产品。

这个文件在 feature 周期内保持稳定,只在需求变更时改。它的读者是 planner agent、产品 review 的人、新加入的工程师。

design.md —— 技术上怎么实现

从工程视角描述 how。

核心内容是四块:架构决策、关键接口、取舍记录、风险和未解问题。

架构决策包括技术栈选择、数据流、模块划分。比如"前端用 Next.js 16 的 app router,后端用 Supabase Edge Functions。订阅通过 Resend 发送。cron 用 Vercel Cron Job。"

关键接口包括 API schema、数据模型、事件格式。比如订阅的 API 接收 {email: string},返回 {subscribed: boolean, confirmationSent: boolean}。数据库表 subscribersemail, confirmed_at, unsubscribed_at, token, created_at 五列。

取舍记录用 ADR(Architecture Decision Record)的风格:为什么选 A 不选 B。比如"为什么选 Resend 而不是 SendGrid——Resend 的 React Email 模板直接写 JSX,和我们的技术栈一致;SendGrid 需要额外的模板语言。代价:Resend 的送达率数据没有 SendGrid 丰富。"这类记录的价值是三个月后你(或另一个工程师、或另一个 Agent)回来改这部分代码时,知道"当时为什么这么决定",避免盲目推翻。

风险和未解问题要明确标出来,不假装都想清楚了。比如"风险:如果订阅用户超过 10,000,Resend 免费额度不够。未解决:什么时候换到付费 plan 的阈值?"这类标注允许后面的开发者或 Agent 识别出"这里是一个已知但未解决的点",不会误以为这是故意的设计。

这个文件在设计阶段密集更新,进入实现后相对稳定。读者是 executor agent、code review 的人、后来接手的工程师。

tasks.md —— 分解成哪些可独立执行的步骤

从执行视角描述 do what。

核心内容是任务列表,每个任务粒度是"一个 Agent 的一次 session 能完成"。

具体到博客订阅这个 feature,tasks.md 大概长这样:

code
## Phase 1: Infrastructure
- [x] T1. Create subscribers table migration
- [x] T2. Set up Resend API integration
 
## Phase 2: Subscription Flow
- [ ] T3. Implement POST /api/subscribe endpoint [deps: T1, T2]
- [ ] T4. Design confirmation email template [parallel with T3]
- [ ] T5. Implement /confirm?token=xxx page [deps: T1]
 
## Phase 3: Weekly Digest
- [ ] T6. Implement /api/weekly-digest cron handler [deps: T1]
- [ ] T7. Design weekly digest email template [parallel with T6]
- [ ] T8. Configure Vercel cron [deps: T6]
 
## Phase 4: Unsubscribe
- [ ] T9. Implement unsubscribe link and endpoint [deps: T1]

每个任务有四个东西:状态(todo/doing/done)、描述、依赖关系、验收条件

状态让 Agent 知道该选哪个任务——永远选没有未完成依赖的、状态为 todo 的第一个任务。

依赖关系让并行变得安全。T3 和 T4 之间没有依赖,可以交给两个 subagent 并行做;T5 必须等 T1 完成。

验收条件通常是"跑什么测试能通过"。比如 T3 的验收条件是 "pnpm test api/subscribe 通过",这样 Agent 完成任务时知道怎么自己验证,不需要人工 review 才能进入下一个任务。

这个文件每天都在动。它的主要读者是 executor agent,整个工作围绕这个文件展开。


为什么是三个文件,不是一个或五个

**不能是一个。**这三类信息的变更频率差一个数量级。

想象你把这三个文件合并成一个 feature.md。每天 Agent 为了更新任务状态要改这个文件十几次——"T3 标记为 doing"、"T3 完成了标记为 done"、"发现 T3.1 subtask 加进去"。一个月后,这个文件有 500 次 commit,其中 480 次是任务状态变更。

然后你想回头看"当初为什么决定用 Resend 而不是 SendGrid"。git blame 在这一行显示的是最近一次的任务状态 commit,追溯到真正做出架构决策的那次 commit 需要手动翻几百次提交。Git history 事实上已经失效了。

分成三个文件之后,requirements.md 的 commit history 只讲需求变化,design.md 的 history 只讲技术决策演化,tasks.md 的 history 只讲执行进度。每个文件的历史都是一条连贯的叙事。

**不需要五个。**这三层正好对应 Agent 做决策时需要的三个上下文——我要解决什么问题(requirements)、我用什么方式解决(design)、我下一步做什么(tasks)。

加更多文件会让每次加载的 context 更碎。比如有人会建议加一个 risks.md 专门放风险、加一个 decisions.md 专门放取舍记录。但风险和取舍记录本来就是设计决策的一部分,把它们从 design.md 里拆出来,结果是 Agent 做决策时要同时加载三个文件才能拿到完整的设计上下文。每多一个文件,Agent 就多一个"可能漏读"的风险。

三这个数字不是美学选择,是这套信息的自然最小分割。

目录结构

这三个文件不是放在 repo 根目录,而是按 feature 组织:

code
specs/
├── 001-user-auth/
│   ├── requirements.md
│   ├── design.md
│   └── tasks.md
├── 002-payment-flow/
│   ├── requirements.md
│   ├── design.md
│   └── tasks.md
├── 003-email-subscription/
│   ├── requirements.md
│   ├── design.md
│   └── tasks.md
└── ...
AGENTS.md          # 跨 feature 的导航和约束

每个 feature 在它自己的目录下自包含。做 feature-003 的时候,Agent 被限制只加载 specs/003-*/ 下的文件,不会被 feature-001 或 002 的细节污染。它看不到 user-auth 的实现细节——那些细节对它当前的任务不相关,加载进 context 只会浪费空间、稀释注意力。

这是对 Ralph "独立 context window" 洞察在项目层面的应用。Ralph 把每一轮循环做成独立的 context,我们把每一个 feature 做成独立的 context。同一个原则在不同的层次上应用。


AGENTS.md 是路标,不是说明书

这套三文件体系运行在单个 feature 范围内。每个 feature 在 specs/{feature-id}/ 下有自己的一套。

整个 repo 还需要一个跨 feature 的稳定层,这个角色是 AGENTS.md(或 CLAUDE.md)。它回答一个不同的问题——Agent 第一次进入这个 repo 时,应该先知道什么

AGENTS.md 的核心内容只有三类:

  • 项目导航 —— 这个 repo 的结构、去哪找什么东西。比如"apps/web 是前端、apps/api 是后端、packages/ui 是共享组件、specs/ 是 feature 规格"。
  • 核心约束 —— 什么必须做、什么禁止做,跨所有 feature 都适用的。比如"所有 API 必须用 zod 做输入验证"、"不允许用 any 类型"、"提交信息必须引用 ticket 号"。
  • 索引 —— 指向 specs/ 目录、docs/design/ 里的设计决策、各领域的规则文件。

理想情况下 AGENTS.md 应该很短——50 行到 200 行之间。它是路标,不是说明书

OpenAI 在他们的 harness engineering 博客里专门讲了这个教训。他们最早把所有约定塞进一个巨大的 AGENTS.md——测试规范、代码风格、架构原则、debug checklist、常见陷阱,全都在里面。结果 AGENTS.md 长到 3000 行。

问题立刻出现了。每次 Agent 进入 context,这 3000 行先被加载,占掉了大约 15k token。真正的任务、相关代码、历史上下文只剩下很少的空间。Agent 经常漏掉关键约束,或者为 3000 行文件里某条不相关的规则而过度优化。

他们总结了三个失败模式:

"Context is a scarce resource. A giant instruction file crowds out the task, the code, and the relevant docs—so the agent either misses key constraints or starts optimizing for the wrong ones."

"Too much guidance becomes non-guidance. When everything is 'important,' nothing is."

"It rots instantly. A monolithic manual turns into a graveyard of stale rules."

他们的解法是 progressive disclosure——顶层 AGENTS.md 只放导航和核心约束,具体约定分散到子模块的小文件里。Agent 进到 apps/api/ 目录时才会读到 apps/api/AGENTS.md 里的 API 设计规范;进到 packages/ui/ 时才会读到 packages/ui/AGENTS.md 里的组件命名约定。每次加载的信息都和当前任务相关。

这里浮现出 Harness 工件设计的一个通用原则:用目录结构做隐式的上下文加载,比用大文件做显式的全量加载好

目录结构本身就是一种路由。Agent 进到哪个目录,就自动加载哪一层的规则。它不需要知道"我现在应该忽略 AGENTS.md 里哪些规则"——它只会看到和当前层级相关的规则。规则的作用域由文件位置决定,不由文本内容决定。

这是一个关于 harness 的更深原则——把约束从文本移到结构里。同样一条"后端代码必须用 zod 验证",你可以写在根目录的 AGENTS.md 里(文本约束),也可以写在 apps/api/AGENTS.md 里(结构约束)。前者要求 Agent 每次判断"我现在在写后端代码吗?",后者让 Agent 只在写后端代码时才看到这条规则。后者更可靠,因为它把"判断适用性"这一步从 Agent 的推理里移出去了。


接下来

这是系列的第一篇。我只做了一件事——把 Harness Engineering 是什么、为什么出现、社区当前的主流实践是什么讲清楚。

很多重要的东西这篇没有展开。接下来的系列会分别讲:

  • Context Engineering 的工程化 —— context 作为一种有预算、有生命周期、有结构的资源。怎么给一个 Agent 任务分配 200k token 的 context 预算?哪些信息值得常驻,哪些只在需要时加载?
  • MCP in practice —— 工具调用从临时实现沉淀成协议,它真的改变了什么,又没改变什么。从自定义 tool schema 到标准 MCP server,什么时候值得切换。
  • Skill as Rules —— 把项目规则从自然语言描述变成可激活的声明。我们在学校多人 Codex 协作项目里用这个方法把规范违反率降到了显著低于纯 AGENTS.md 的水平,具体数据和 eval 设计会在那篇展开。
  • Skill as Memory —— 把项目上下文做成可以按需加载的记忆单元。一个 skill 不只是规则,它可以是一段领域知识、一个调试流程、一份 API 手册。
  • Memory 分层 —— Rules / Memory / Session 三层各自的存储策略、检索机制、失效规则。为什么不能用一个向量数据库解决所有记忆问题。
  • 领域 Harness —— 前端、后端、交易、合约审计、代码 review,每个领域的约束都有自己的形状。前端的 harness 要管视觉回归,后端要管 API contract,交易要管资金安全边界——这些是同一套理论的不同具象。
  • Planner / Executor 架构 —— z0 里的实际选择。这个分层什么时候值得、什么时候不值得,我踩过的坑和留下来的取舍。

每篇都会带具体实现、evals、以及已经踩过的坑。


参考

  • Mitchell Hashimoto, Engineer the Harness(2026.02)
  • OpenAI, Harness engineering: leveraging Codex in an agent-first world(2026.02)
  • Anthropic, Harness design for long-running application development(2026.03)
  • Martin Fowler / Birgitta Böckeler, Harness engineering for coding agent users(2026.04)
  • Geoffrey Huntley, Ralph Wiggum as a software engineer(2025.05)
  • GitHub, Spec-Driven Development with AI(github.com/github/spec-kit)
  • Amazon Kiro, IDE documentation(aws.amazon.com/kiro)---