Command Palette

Search for a command to run...

0

给 AI Agent 设计的项目文档模板:Agent Workspace RefArch 实战

AGENTS.md + HANDOVER.md + 三层架构 + ARID 原则。Anthropic 的渐进式披露、OpenAI 的 agents.md、加上我们自己的上下文工程实践——揉到一起就是一份给 AI Agent 读的项目文档标准。

每次新 session,它都不记得你是谁

一切从一个很现实的问题开始。

你在一个项目上干了三周。技术栈摸清了,目录结构烂熟于心,测试命令张口就来,甚至养成了一套固定的工作流——先读 HANDOVER.md、再看 AGENTS.md、然后开工。

然后你关掉终端,吃了个饭,回来重新打开。

AI agent 一脸茫然地看着你,像第一次见面。

"我来看看这个项目……嗯,这是一个 Next.js 项目……"

你三周建立的心智模型,它在一瞬间归零了。

这不是模型智商的问题。这是上下文的问题。每个新 session 都是一张白纸,而你的项目在那张白纸上只是一堆文件名。agent 不知道哪些文件重要,不知道你上次做到哪了,不知道这个项目有什么坑在等着它。

我试过几种办法。

一开始我写了一个巨长的 system prompt,把项目结构、技术栈、注意事项全塞进去。然后我发现每次改 prompt 都要开新的 session 验证,调一次十分钟,而且 prompt 越来越长,到最后 AI 读 prompt 的时间比干活的时间还长。

后来我试过把文档写在项目 README 里。但 README 是给人看的,给 AI 看的文档要求不一样——它需要结构化、可检索、而且最好是 agent 自己能维护的。

折腾了大半年,我最终攒出了一套叫 Agent Workspace RefArch 的东西。

不是什么了不起的发明。就是把 Anthropic 在 Claude Code 里的"渐进式披露"思路、OpenAI 的 agents.md 实践、加上我们自己团队踩坑踩出来的上下文工程经验——揉到一起,变成一份给 AI Agent 读的项目文档标准。

版本号我写了个 v1.2,因为前面失败的那几次我懒得编号了。

从一篇 AGENTS.md 到三层架构

最早的想法很简单:在每个项目根目录放一个 AGENTS.md,告诉 agent 这个项目是什么、怎么跑、有什么坑。

我写了第一个版本,大概 80 行。效果还行,agent 至少不会问"这个项目用什么包管理器"这种基础问题了。

但很快问题就来了。

当你有 10 个项目共存在一个 workspace 里,每个项目都有自己的 AGENTS.md,你需要在最上层也有一份地图——告诉 agent 这个 workspace 里有哪些项目、它们之间怎么依赖、哪个是主项目。

而且 AGENTS.md 本身在变长。项目做久了,技术债、已知问题、特殊配置……全都想塞进去。一个文件从 80 行涨到 200 行,agent 每次加载要读完整个文件才能开始干活。

信息密度和加载速度之间,存在一个根本性的矛盾。

Anthropic 在 Claude Code 里用了一个思路叫"渐进式披露"——不是一次性把所有信息塞给模型,而是按需加载,先给索引,再给详情。

我抄了这个思路,改成了三层结构:

workspace/
├── AGENTS.md              ← Layer 1: 项目地图 + 依赖
├── .workspace.yaml        ← MCP / 工具配置
├── project-a/
│   ├── AGENTS.md          ← Layer 2: 技术栈 + 命令 + 约束
│   ├── HANDOVER.md        ← 会话日志(80 行自动归档)
│   ├── docs/decisions/    ← ADR
│   └── ...
└── hooks/                 ← Layer 3: SessionStart/End 自动维护

说说每一层的设计心思。

Layer 1: 项目地图

根目录的 AGENTS.md 不超过 60 行。它只回答两个问题:这个 workspace 里有什么?它们之间怎么关系?

# Workspace: gh.l-web

项目: 个人技术博客 (Next.js 16, Static Export, 中英双语)
依赖: 无

子项目:
  - gh.l-web/      → 主博客项目
  - shared-assets/ → 公共资源

入口: gh.l-web/

就这些。agent 读了之后知道该往哪个目录钻,别的先不关心。

60 行的硬限制是我试出来的。超过这个长度,agent 在第一层就开始走神。

Layer 2: 项目详情的 AGENTS.md

每个子项目的 AGENTS.md 是核心,上限 150 行。它包含:

  • 技术栈与关键命令
  • 目录结构与文件约定
  • 代码规范与约束
  • 边界三段式(下面细讲)
  • 陷阱表(下面也细讲)

150 行是我反复调整后的结果。太少的话信息不够,agent 还是会问低级问题;太多的话它读不完就开始干活,反而漏掉关键约束。

Layer 3: Hook 驱动的自动化

这一层是最重要的但也最容易被忽视的。

AGENTS.md 是人写的,但HANDOVER.md 必须让 agent 自己维护

每次 session 结束时,一个 SessionEnd hook 自动把本次 session 的变更摘要、决策、未完成事项追加到 HANDOVER.md,并裁剪到 80 行以内。下次 session 开始时,SessionStart hook 自动加载 HANDOVER.md 的最后 10 行——agent 立刻知道"上次我做到哪了"。

80 行的裁剪线也有故事。试过 50 行,丢太多上下文。试过 120 行,agent 读得慢而且容易把旧日志当成当前状态。80 行是个甜点。

为什么我选择了 ARID 而不是 DRY

做软件工程的人对 DRY(Don't Repeat Yourself)有近乎宗教般的信仰。我第一次设计这套模板时也试图复用——在根目录 AGENTS.md 里写一份通用的约束列表,然后各个子项目引用它。

结果很糟。

Agent 不是编译器,它不会自动跨文件解析引用关系。当你写"参考根目录的约束 #3",agent 大概率不会真的去翻根目录——它凭训练数据里的"常见约束"继续干活,把你自定义的那条跳过去了。

所以我接受了一个事实:这份文档的核心读者是 LLM,不是人类编译器。 LLM 的阅读模式是线性的、一次性的。它不会像程序员一样 Ctrl+Click 跳转引用。

这就引出了我的核心原则——ARID: Accept Repetition In Documentation

听起来像在开倒车。实际上是有意为之。每个子项目的 AGENTS.md 都可以(也应该)包含那些共性的约束,哪怕它们在根目录已经写过了。LLM 读 A 项目的 AGENTS.md 时,它不需要知道 B 项目的存在;它只需要知道 A 项目自己那一亩三分地的规则。

重复不是 bug,是 feature。

这块我想讲清楚一点,因为它跟很多人的直觉相反。

DRY 的出发点是"一个概念只在一个地方定义,改一处全改"。这个原则在代码里无比正确——你不想维护 5 个相同逻辑的 copy-paste。但在文档里,尤其是给 AI 读的文档里,维护成本不在"改一处要改多处",而在于"信息躺着但 agent 读不到"。

重复的代价是写入时多敲几行字。不重复的代价是 agent 每次漏掉一条约束,产生一个 bug,你花半小时排查。孰轻孰重,算一笔账就清楚了。

不是说完全放弃模块化。架构决策记录(ADR)这种需要全局可见的东西,放在 docs/decisions/ 里共享。但日常的操作约束、边界规则、陷阱提醒——每份 AGENTS.md 都应该自包含。

边界三段式的诞生

AI agent 执行任务时最怕什么?

不是它能力不够,而是它太积极了。

给它一个"改个 CSS 颜色"的任务,它可能顺手帮你升级了依赖版本、重构了组件结构、改写了测试用例——因为它的训练数据告诉它"好的工程师会顺手改进代码"。

但你不能怪它。你没告诉它边界在哪。

所以我设计了一套"边界三段式",放在每个 AGENTS.md 里:

  • Always: 哪些事可以放手去做(跑测试、更新 HANDOVER.md、修 lint)
  • ⚠️ Ask: 哪些事必须先问(加依赖、改公共接口)
  • 🚫 Never: 哪些事绝对不要做(硬编码密钥、删测试)

这个三段式不是我拍脑袋想出来的。它来自一个很惨的教训。

有一次我让 agent "优化一下 Dockerfile",结果它把基础镜像从 node:18-alpine 换成了 ubuntu:latest——因为"最新版的 Ubuntu 更安全"。然后整个 CI 炸了。错在它吗?不。我没告诉它"不要换基础镜像"。

从那以后我加了 Never 段。然后发现光有 Never 不够,agent 会陷入"那我到底能做什么"的焦虑——所以又加了 Always。Ask 段是后来补的,因为有些事不是绝对不能做,但要人确认后才能做。

三段式现在是我所有项目的标配,缺一段都不行。

陷阱表的设计心思

比边界三段式更进一步的是陷阱表。

每个 AGENTS.md 末尾都有一张 markdown 表格,格式固定:

| 症状 | 原因 | 解决 |
|------|------|------|

这个格式是我精心选的。

三个列,不多不少。症状是 agent 能看到的现象("build 报错 Module not found"),原因是它需要知道的根因("路径别名在 tsconfig 里配了但 webpack 不认识"),解决是它可以直接执行的命令或步骤。

我试过四列(加上"预防"),但 agent 在推理时很少主动看预防列——它总是在掉坑之后才查表。也试过两列(去掉"原因"),但 agent 理解不了症状和解决之间的因果关系,下次换个相似但不同的症状又掉进去了。

三列是甜点。不多解释,直接上数据,agent 查一次就明白。

这里有个细节:陷阱表里的"原因"要用 agent 能理解的语言写。不要写"ESM/CJS 互操作性问题"——agent 知道这个,但它不会把这个问题和"build 报错"关联起来。要写"webpack 打包时遇到混合模块格式,因为 xxx 配置默认只支持 CJS"。越具体越好。

当前 context-engineering-system 的陷阱表有 12 条,覆盖了从 build 到 deploy 的常见坑。每条都是从真实事故里提炼的。

实际效果怎么样

说了一堆设计理念,看看数据。

我拿 context-engineering-system 这个项目做了前后对比。改造前,Agent Workspace 没有用这套 RefArch。改造后完整部署了。

指标改造前改造后
Skill description 总字符~15,0002,322
平均 description 长度~190 chars34 chars
未用 skill 在索引中5910 archived

总字符从 15,000 降到 2,322,降了 84%。所以每次 session 加载时,agent 少读了 12,000 多个字符的垃圾信息。

更关键的指标我没法量化但体感明显:"低级问题"的发生频率大幅下降。 以前 agent 经常问"这个项目的测试命令是什么"——现在不问了,因为 AGENTS.md 第一段就写了。以前 agent 偶尔会去改 .env 文件——现在不改了,因为 Never 段写得明明白白。

这套架构目前被 10 多个项目采用。从我个人维护的 side project(baby-harness, agent-search-mcp, crawlweaver)到多人协作的仓库(headroom, qq-group-bot),都有它的影子。

我并没有强制推广。只是有一次在团队里分享了之后,隔壁组的人跑来跟我说"你那套文档模板能不能给我一份"。

那我当然给了。

升维:给 AI 写文档,是在训练它如何读你的项目

最后聊点更大的。

我越来越觉得,给 AI agent 写文档这件事,说到底就是训练一种阅读模式

你写的每一条边界、每一个陷阱、每一段约束,都在告诉 agent:"这个项目的正确打开方式是这样的。"

它在训练 agent 的注意力——哪些信号重要(AGENTS.md 里的约束),哪些信号可以忽略(node_modules 里的十万个文件)。

传统的软件开发文档是给人写的。人是有判断力的——你告诉程序员"不要在构造函数里做耗时操作",他能举一反三到"那初始化时也不应该做"。但 agent 不会。你说"不要在构造函数里做耗时操作",它就只知道这一条。你得把"初始化时也不应该做"也写出来。

所以给 AI 写文档,要接受一个事实:文档的粒度必须粗到每条规则都可以独立理解和执行。

这个粒度比给人写的文档粗多了。

但有趣的是,一旦你习惯了这个粒度,你会发现它反过来帮到了人——新成员 onboarding 时拿到 AGENTS.md,扫一眼就知道什么该做什么不该做。不需要像以前一样"你先读一遍代码库熟悉一下"。

坦率的讲,给 AI 写的文档,最终也让人类读得更明白。

因为我写的不是文档。我写的是这个项目的操作手册——只不过同时用两种语言:人话和 token。


这套 RefArch 目前的完整实现在 context-engineering-system(抱歉这是个内部项目,暂未开源)。模板文件和 hook 脚本在我的 dotfiles 里持续迭代。

如果你也在跟 AI agent 的"失忆症"做斗争,从一个小东西开始:在项目根目录放一个 AGENTS.md,写上三行边界。 不用三个层,不用陷阱表,甚至不用 100 行。

看看 agent 的行为有没有变化。

我赌有。