Agents with Receipts · 有据可查:agentic coding 实践手册
RECEIPT #01 · /practices/01-memory-files.html 在 GitHub 查看源文件

01 — Memory 文件:AGENTS.md / CLAUDE.md 的写法与维护

适用工具:Claude Code · Codex · Cursor · GitHub Copilot · 验证于 2026-09

memory 文件是 agent 每次会话都要付费加载的常驻上下文:写少了 agent 重复犯错,写多了反而淹没关键指令。本章给出从起步、写法、调优,到跨工具共享与 CI 门禁的完整维护路径。每条做法都对应到四家官方文档的原文。

1.1 从 20-30 行起步,只写 agent 推断不出的事实

场景:新项目第一次写 memory 文件,或者 /init 生成了一大篇目录描述和依赖清单,不知道该留什么。

做法:

  1. 对每一行套用 Claude 官方的删行判据:”删掉这一行,agent 会犯错吗?”——不会就删。目录结构、依赖列表、语言通用约定都属于 agent 能从代码推断的内容,删。
  2. 起步只保留三类,20-30 行足够:一句话项目概述、无法从代码猜出的命令、与工具默认不同的约定。Claude Code 官方给出的目标是单文件 200 行以内(原文 “target under 200 lines per CLAUDE.md file”),超过会降低遵从度;硬跳过线是 4 MiB。

    # weibo-chat-auto — Agent 指南
    
    Node 20 CLI,抓取微博私信并生成回复草稿;核心状态机在 src/session.mjs。
    
    ## 常用命令
    | 任务 | 命令 |
    | --- | --- |
    | 全部测试 | `npm test` |
    | 单文件测试 | `node --test test/session.test.mjs` |
    
    ## 约定
    - 所有网络请求走 src/http.mjs 的封装,不直接 fetch
    - 提交信息用中文,首行不超过 50 字符
    
  3. 之后按”同一个错误犯第二次才补一条”的节奏增长,而不是预先写全。

依据:Claude Code 官方原话:”For each line, ask: would removing this cause Claude to make mistakes? If not, cut it”,并明确警告臃肿的 CLAUDE.md 会让真正的指令被无视(Claude Code 官方指南);Cursor 官方同样把”复制整份 style guide”和”记录 agent 已知的常用命令”列为要避免的写法(Cursor Rules)。

边界:删行判据是”会不会犯错”,不是行数本身——出过事故的硬契约再长也要留;组织级 managed CLAUDE.md 的合规条款由安全团队定,不适用此判据。

反模式:把 /init 或 LLM 生成的全文直接提交——Claude Code 的 /doctor 检查会主动提议删掉其中能从代码推断的目录布局、依赖列表和架构综述(memory)。

1.2 命令写到 flag 和路径,让 agent 拿来即跑

场景:agent 每次自己猜测试命令,猜错了浪费一轮;或者明明只改了一个文件却跑全量测试。

做法:

  1. 用”任务 → 命令”两列表格组织,每条命令带 flag、带路径、带占位符示例:

    ## 常用命令
    | 任务 | 命令 |
    | --- | --- |
    | 全部测试 | `pnpm turbo run test --filter <package>` |
    | 单个用例 | `pnpm vitest run -t "<test name>"` |
    | Lint | `pnpm lint --max-warnings 0` |
    | 本地运行 | `pnpm dev`(端口 5173) |
    | 重建快照 | `pnpm test -u`(警告:只在确认 UI 改动符合预期后跑) |
    
  2. 高危命令在同一行注明什么情况下不要跑(如上表最后一行)。
  3. 每条命令在当前仓库真实执行一遍再入表——引用不存在的 npm script 会被 agentsmd-lint 的 dead-script 规则报 error(见 1.6)。

依据:Claude 官方以对比例证说明精确度要求——”Run npm test before committing” 优于 “Test your changes”(memory);agents.md 官方示例同样精确到 pnpm vitest run -t "<test name>",且 FAQ 确认 agent 会自动执行文件里列出的检查命令并修复失败(agents.md)。

边界:git、npm 的基础用法不用写——Cursor 官方明确 “Agent already knows common tools like npm, git, and pytest”(Cursor Rules),写了只是消耗上下文。

反模式:”改完请跑测试并确保通过”——没有命令,agent 只能猜 runner 和入口。

1.3 用 always / ask first / never 三层写边界

场景:agent 改了生成物目录、碰了密钥文件、擅自加了运行时依赖;或者反过来,每一步都停下来问你。

做法:

  1. 把行为边界写成三个固定小节,每条 never 附一句原因:

    ## 边界
    ### Always
    - 改完 JS 文件后运行 `npm test`
    - 提交前运行 `npm run lint`
    
    ### Ask first
    - 新增运行时依赖
    - 修改 .github/workflows/ 下的任何文件
    - force push
    
    ### Never
    - 修改 dist/ 与 src/generated/(构建产物,源头在 codegen 配置)
    - 读取或打印 .env* 文件内容(含凭据)
    
  2. 对必须 100% 拦截的 never 条目,再配一层 hooks 或 permissions deny 兜底——memory 文件是 context,不是强制配置。

依据:Codex 官方示例文件本身就使用这三种句式——”Always run npm test…”、”Ask for confirmation before adding new production dependencies”、”Never rotate API keys without notifying the security channel”(Codex AGENTS.md);Claude 官方明确 memory 文件”treats them as context, not enforced configuration”,要无条件阻止某动作需用 PreToolUse hook(memory)。

边界:never 列表超过十条说明约束放错了层——可机检的(路径、命令模式)挪进 hooks 与 permissions(见 05 章),memory 里只留需要判断的边界。

1.4 像调 prompt 一样调 memory 文件

场景:规则明明写了,agent 就是不遵守。

做法:

  1. 先确认文件真的加载了。Claude Code 在会话里跑 /context,检查 Memory files 列表;Codex 用一条命令让它复述:

    codex --ask-for-approval never "Summarize the current instructions."
    
  2. 排查冲突:两条规则矛盾时 Claude 官方说明它”may pick one arbitrarily”——找到旧的那条,删除。
  3. 对关键条目加强调词提升遵从,这是 Anthropic 官方确认有效的手法:

    - IMPORTANT: src/db/migrations/ 下已合并的文件 NEVER 修改;要改 schema 就新建迁移
    
  4. 每次修改后观察 agent 行为是否真的变了;没变就说明措辞含糊或文件太长,回到 1.1 的删行判据修剪。

依据:Claude Code 官方原话:”Treat CLAUDE.md like code: review it when things go wrong, prune it regularly, and test changes by observing whether Claude’s behavior actually shifts”,并确认 “adding emphasis (e.g., ‘IMPORTANT’ or ‘YOU MUST’)” 可提升遵从(Claude Code 官方指南)。

边界:强调词会通胀——满篇 IMPORTANT 等于没有 IMPORTANT。一条规则反复强调仍不生效、且它必须在固定时点执行(每次提交前、每次编辑后),官方的建议是改写成 hook 而不是继续加粗(memory · Troubleshoot)。

1.5 用根级 AGENTS.md 做单一事实源,CLAUDE.md 软链过去

场景:团队混用多个工具,仓库里同时躺着 AGENTS.md、CLAUDE.md、.cursor/rules/,内容开始互相漂移。

做法:

  1. 根级 AGENTS.md 做主文件:这是 60k+ 开源项目采用的开放格式,Cursor 与 Copilot 原生读取(agents.md、Cursor Rules、Copilot 支持矩阵)。
  2. Claude Code 只认 CLAUDE.md,软链过去:

    ln -s AGENTS.md CLAUDE.md
    git add AGENTS.md CLAUDE.md
    

    需要追加 Claude 专属内容(或在 Windows 上,建 symlink 需要管理员权限)时,改用 import:

    @AGENTS.md
    
    ## Claude Code
    - src/billing/ 下的改动先用 plan mode
    
  3. 新会话跑 /context,确认 CLAUDE.md 出现在 Memory files 下。
  4. monorepo 想放嵌套文件之前,先弄清四家”就近”语义并不相同:

    工具 官方定义的加载语义
    Claude Code 从 cwd 向上收集每层 CLAUDE.md 全部拼接,越近越靠后;cwd 之下的子目录文件按需加载(memory)
    Codex 从项目根向下走到 cwd,每目录至多取一个文件拼接,越近越靠后;按 cwd 而不是被编辑的文件(AGENTS.md discovery)
    Cursor 嵌套 AGENTS.md 与父目录合并,更具体的优先(Cursor Rules)
    Copilot VS Code 里最近的 AGENTS.md 优先;CLI 文档只列出读取 AGENTS.md/CLAUDE.md/GEMINI.md,未定义优先级(仓库指令 · 支持矩阵)

依据:ln -s AGENTS.md CLAUDE.md 与 @AGENTS.md import 都是 Claude 官方文档给出的方案,包括 Windows 例外(memory · AGENTS.md)。

边界:Codex 的就近按启动目录算——编辑 services/x.ts 不会加载 services/AGENTS.md,除非从那个目录启动;依赖嵌套文件做关键约束之前,先在目标工具里验证它真的加载了(1.4 的第 1 步)。

反模式:每个工具各写一份内容相似的文件——三个月后它们描述的是三个不同的项目。

1.6 用 lint 把约定变成门禁

场景:memory 文件随时间腐烂:模板占位符没填就提交、表里的命令随重构失效、写了标题没填内容。

做法:

  1. 把本手册的 tools/agentsmd-lint(零依赖单文件,Node ≥ 20)复制进你的仓库,本地检查:

    node tools/agentsmd-lint/index.mjs AGENTS.md
    node tools/agentsmd-lint/index.mjs --max-lines 150 AGENTS.md CLAUDE.md
    

    五条规则:max-lines(超长降低遵从)、placeholder(未填的模板占位符)、vague(agent 无法执行的模糊措辞)、dead-script(引用了 package.json 里不存在的 npm 脚本)、empty-section(空标题节)。

  2. 发现 error 时退出码为 1,直接接进 CI:

    # .github/workflows/agentsmd.yml
    name: agentsmd-lint
    on:
      pull_request:
        paths: ["AGENTS.md", "CLAUDE.md"]
    jobs:
      lint:
        runs-on: ubuntu-latest
        steps:
          - uses: actions/checkout@v4
          - uses: actions/setup-node@v4
            with: { node-version: 20 }
          - run: node tools/agentsmd-lint/index.mjs AGENTS.md
    
  3. 修复顺序:先 dead-script(命令必须真实可跑,对应 1.2),再 vague(把模糊措辞换成具体条件与数字,对应 1.4)。

依据:Claude 官方要求定期 review memory 文件、移除过期与冲突指令(memory · Consistency);agents.md 官方 FAQ 把该文件定位为 “living documentation”(agents.md),活文档需要门禁防腐。

边界:linter 只查得出形式问题(占位符、死命令、空节),查不出”这条规则本身还对不对”——内容层面的修剪仍靠 1.4 的行为观察。

反模式:warning 长期挂着不修——门禁只对 error 生效时,warning 堆积等于没有门禁。

1.7 Claude 的 auto memory:让 agent 自己记,你定期审计

场景:你纠正过一次”用 pnpm,别用 npm”,下个会话它又忘了;或者反过来,担心 agent 自己悄悄写下一堆从没人看过的笔记。

做法:

  1. 分清两套 memory:CLAUDE.md 是你写的契约;auto memory 是 Claude 按你的纠正与偏好自己写的笔记(官方原话 “notes Claude writes itself based on your corrections and preferences”)。两套都在会话开始时载入。
  2. 知道它写在哪:~/.claude/projects/<project>/memory/,MEMORY.md 是索引(一行一条),细节在各自 topic 文件里;同一 git 仓库的所有 worktree 与子目录共用一个 auto memory 目录,且只在本机。
  3. 用 /memory 审计:浏览、打开、删除,或者直接关掉 auto memory;会话里出现 “Saved N memories” / “Recalled N memories” 就是它在读写。想确认当前会话真正载入了哪些文件,跑 /context 看 Memory files。
  4. 想让某条进 CLAUDE.md 而不是 auto memory,明确说 “add this to CLAUDE.md”,或者自己用 /memory 打开文件编辑。

依据:auto memory 的定位、目录结构、载入上限(MEMORY.md 前 200 行或 25KB)、机器本地与 worktree 共享语义、modified 时间戳与 /memory 入口均出自 Claude memory 官方页。

边界:auto memory 是 agent 自己写的,不是契约——关键约束仍要落在 CLAUDE.md 里或用 hook 强制(见 1.3)。它的索引有独立的 200 行 / 25KB 读限:超限的部分下次加载直接丢,所以一条一行、细节挪 topic 文件,别把索引写成长文。

反模式:把 auto memory 当成”agent 会自己记住一切”,于是什么规则都不写——它记的是它认为值得记的,而且接近读限时官方会提示它合并或丢弃过期条目。


位置:轴一 · 读得懂 — 上一章 00 从零配齐(可跟做) · 下一章 02 机制选型

相关:02 机制选型 · 06 上下文管理

对应检查:agentsmd-lint 的 max-lines · placeholder · vague · dead-script · empty-section · agents-doctor 的 agents-md · claude-md