拆.
上下文与记忆——AI 怎么'记得' · 第 03

AGENTS.md / CLAUDE.md

用户怎么给 AI'长期记忆'

4500读完约 23 分钟craft:B+发布于 2026-06-21

一、原理:用户级 instruction 文件的需求

每个项目都有自己的"规矩"——技术栈、代码风格、测试约定、提交格式、命名规则。新人入职这个项目,要花几天熟悉这些规矩。

AI Agent 加入这个项目,也需要知道这些规矩。否则它会"做对了功能但风格不对"——按它训练数据里的"通用风格"写代码,跟项目格格不入。

具体例子:

  • 项目用 TypeScript,但 AI 写出了 let x = 5(应该用 const
  • 项目禁用 any 类型,但 AI 用了 let data: any
  • 项目测试用 vitest,但 AI 写了 jest 风格
  • 项目 commit message 要求格式 "feat: ...",但 AI 写了 "Added user login"

这些不是 LLM 能力问题——它"知道"怎么写好代码。是它不知道这个项目要的风格

解决方案是给 LLM 一份项目说明书——告诉它这个项目的规矩。

这份说明书有几个特性需求

需求 1:跨 session 持久

用户开一个 session、说一遍规矩、AI 学会。下个 session 重新开,AI 又忘了。说明书必须能跨 session——存在文件里。

需求 2:项目级而非全局

不同项目有不同规矩。React 项目跟 Go 项目规矩完全不同。说明书必须能放在项目里——一个项目一份。

需求 3:用户可读可改

用户要能看到说明书内容、可以编辑、可以版本控制。markdown 格式是最合适的——既人类可读又机器可读。

需求 4:AI 自动发现

用户不用每次开 session 都告诉 AI"看一下我的说明书"——AI 应该自动发现并加载。

把这 4 个需求合起来,约定俗成的方案是:在项目根放一个 markdown 文件,AI Agent 启动时自动找、读、加入 system prompt。

这个文件叫什么?业界有几个约定:

  • AGENTS.md——通用 AI Agent 约定
  • CLAUDE.md——Anthropic Claude 项目的约定(Claude Code 的项目说明书)
  • CONTEXT.md——一些产品用这个名字
  • README.md——人也看的(不专属 AI)

opencode 支持前三种。我们看具体怎么处理。

二、案例:opencode 的"只取第一个匹配"判断

opencode 加载 instruction 文件的代码在 packages/opencode/src/session/instruction.ts

它的逻辑大致是:

function systemPaths(): string[] {
  const globalFiles = [
    "~/.opencode/AGENTS.md",
    "~/.claude/CLAUDE.md"
  ]
  
  const projectFiles = findUp([
    "AGENTS.md",
    "CLAUDE.md",
    "CONTEXT.md"
  ])
  
  const configFiles = config.instructions || []
  
  return [...globalFiles, ...projectFiles, ...configFiles]
}

逻辑分三块。

第一块:全局文件

~/.opencode/AGENTS.md~/.claude/CLAUDE.md——用户级别的 instruction。这两个文件放在用户家目录,跨所有项目生效。

适合"我自己的偏好"——比如"我用 4 空格缩进"、"我喜欢函数式风格"。

第二块:项目级文件

findUp 是关键。它从当前工作目录开始,向上查找直到找到第一个 AGENTS.md / CLAUDE.md / CONTEXT.md(任一个)。

注意细节:只取第一个匹配,不合并多个

如果你的目录是 /Users/lee/projects/my-app/src/components/,opencode 会按这个顺序查找:

  1. /Users/lee/projects/my-app/src/components/AGENTS.md ?
  2. /Users/lee/projects/my-app/src/AGENTS.md ?
  3. /Users/lee/projects/my-app/AGENTS.md ?
  4. /Users/lee/projects/AGENTS.md ?
  5. /Users/lee/AGENTS.md ?
  6. /Users/AGENTS.md ?
  7. /AGENTS.md ?

找到第一个就停。

如果在 /Users/lee/projects/my-app/AGENTS.md 找到了——就用它。不会再找父目录的。

这个"只取第一个匹配不合并"是个有意的产品判断。为什么?

判断理由 1:避免合并歧义

如果同时找到 /Users/lee/projects/my-app/AGENTS.md/Users/lee/AGENTS.md——怎么合并?

按顺序拼接?前者优先?合并去重?每一种都有边界情况:

  • 顺序拼接——可能导致后面的覆盖前面的("用 TypeScript" + "用 Python")
  • 前者优先——但用户可能期望后者补充
  • 合并去重——markdown 不容易去重(不同的 section 内容)

opencode 选择最简单的方式——只取第一个匹配。用户清楚知道"我设置了哪个文件、AI 看哪个"——没有歧义。

判断理由 2:尊重最近原则

findUp 从当前目录开始——这意味着用户在哪个目录工作,就用那个目录的说明书。

如果一个 monorepo 里有多个子项目,每个子项目自己的目录有 AGENTS.md——用户进哪个子项目,就用哪个子项目的 AGENTS.md。

这个"最近原则"符合用户直觉——"我在哪干活就用哪的规则"。

判断理由 3:避免重复加载

如果合并多个 AGENTS.md,子项目的 AGENTS.md 可能包含和父项目重复的内容——LLM 看到重复指令既浪费 token 又可能困惑。

不合并避免这个问题。

判断理由 4:用户能精确控制

用户想要不同 scope 的指令——可以放在不同位置。想要项目级——放项目根。想要全局——放 ~/.opencode/

判断理由 5:用户能 override

如果父目录有 AGENTS.md,子项目想覆盖——子项目放自己的 AGENTS.md 就行。findUp 会在子项目就停。

这种层级覆盖让用户精细控制。

加载到的 instruction 文件被怎么用?我们看 instruction.ts 第 155-169 行(简化版):

function system(): string {
  const paths = systemPaths()
  return paths
    .filter(exists)
    .map(readFile)
    .map(content => `# Instructions from: ${path}\n\n${content}`)
    .join("\n\n")
}

每个文件的内容前面加一行 marker # Instructions from: <path>——这告诉 LLM "下面这段来自这个文件"。如果 LLM 看到多个 instruction(global + project + config),它知道哪段是哪来的。

然后用 \n\n 拼起来——所有 instruction 拼成一长段,塞进 system prompt 的步骤 2。

注意这里全局和项目级会一起出现——global 先、project 后。两者不冲突——global 是用户偏好、project 是项目规则。

项目级只取第一个匹配——不像 global 那样把多个 global 文件都加(其实 global 只查 2 个固定位置)。

这是 opencode 的微妙设计——global 的"叠加"和 project 的"取第一个"——按 instruction 的"自然 scope"做不同处理。

三、设计启示:合并 vs 不合并的产品判断

这一章的核心论点:多源 instruction 的合并策略是产品判断——选哪种取决于你的用户场景

设计 AI 产品的"用户级 instruction"时,下面几条原则有用:

1. 明确 instruction 的 scope 层级

至少分两层:

  • 全局(用户偏好,跨项目)
  • 项目(项目规则,本项目)

更复杂的产品可能加:

  • 组织(公司规则,跨项目)
  • 子项目(monorepo 子模块)
  • session(一次对话临时)

每一层独立来源、独立加载。

2. 默认"最近优先 + 不合并"

冲突时按"最近原则"——离当前工作位置最近的 instruction 优先。取第一个、不合并——避免歧义。

如果用户想合并多个——让他们显式做(比如在自己的 AGENTS.md 里 @include 其他文件)。

不要默认做"智能合并"——边界情况多、用户难预期。

3. 文件名要约定俗成

不要发明新名字("MyAIInstructions.md")。用业界约定(AGENTS.md、CLAUDE.md、CONTEXT.md)——用户从其他工具迁移过来不用学。

如果你的产品想推自己的约定("OURNAME.md")——也要同时支持业界约定作为 fallback。

4. 用 markdown 不用专有格式

人类可读可编辑、git 可版本控制、用 markdown 标题分 section。

不要用 JSON / YAML——人类不易读、容易语法错。

5. 标 source 让 LLM 知道哪段是哪来的

# Instructions from: <path> 这种 marker 让 LLM 看到内容时知道出处。万一冲突,LLM 自己能判断"项目级优先还是全局优先"。

6. 加载时机要稳定

每次 LLM 请求都重读 instruction 文件——保证用户改了文件立刻生效。

不要"启动时读一次缓存"——用户改文件后要重启 session 才生效,体验差。

7. 文件大小要合理

instruction 文件不能太长——会吃 context。1000-3000 字算合理。超过 5000 字 LLM 会忽略中间段。

如果用户写得太长——产品可以警告"文件超过 X 字,部分内容可能被 LLM 忽略"。

8. 让用户能 debug

让用户能问"你看到了哪些 instruction"——AI 应该能回答"我看到了 /path/to/AGENTS.md 这些规则"。

让用户验证"我的 instruction 真的被加载了"——避免他们困惑"为什么 AI 没按我的规则做"。

最后一个观察。AGENTS.md / CLAUDE.md 是 AI 产品的用户级控制接口。它让用户能"配置 AI 的行为"——不需要改产品代码、不需要写 plugin、只需要写一份 markdown。

这种无需编程的可配置性是开发者工具的关键。给用户太多控制不行(产品行为不可控)——给用户太少不行(用户感觉被锁定)。

AGENTS.md 是个平衡点——用户可以约束 AI 的风格、规则、约定,但不能改 AI 的核心能力。这种**"风格层"开放、"能力层"封闭**的设计是开发者工具的好范式。

下一章 4.4 我们看一个更微妙的话题——Context Epoch。当用户在对话中段切换了 mode、改了环境、换了模型——AI 怎么平滑过渡?