← 返回蜂巢洞察

如何管理代码库中的上下文文件,从而让人工智能编码助手产生更优质的成果

你向编码助手请求创建一个新的端点,90秒后,这个新的端点就已经可以正常使用了。 然后你查看代码变更内容,发现它引入了一个并不存在于你的`package.json`文件中的验证库;尽管你们的团队在去年春天就已经改用了Node.js的测试框架,但它仍然使用Jest来编写测试用例;此外,由于它不知道代码库中其他处理函数都是通过服务来调用的,所以它直接从路由处理函数内部访问了数据库。 代码可以运行,它编写的测试用例也能通过,但你们还是不得不重写大部分代码。 这些情况都不是模型本身的问题——它确实给出了一个看似合理的解决方案,但它之所以会犯这些错误,是因为没有人告诉它这个特定的代码库是如何运作的。 你们

你向编码助手请求创建一个新的端点,90秒后,这个新的端点就已经可以正常使用了。

然后你查看代码变更内容,发现它引入了一个并不存在于你的`package.json`文件中的验证库;尽管你们的团队在去年春天就已经改用了Node.js的测试框架,但它仍然使用Jest来编写测试用例;此外,由于它不知道代码库中其他处理函数都是通过服务来调用的,所以它直接从路由处理函数内部访问了数据库。

代码可以运行,它编写的测试用例也能通过,但你们还是不得不重写大部分代码。

这些情况都不是模型本身的问题——它确实给出了一个看似合理的解决方案,但它之所以会犯这些错误,是因为没有人告诉它这个特定的代码库是如何运作的。

你们的编码规范存在于团队成员的脑海中、代码评审评论中,以及18个月前做出的那些没有被记录下来的决定里。编码助手看不到这些信息,因此它只能依靠之前训练时所使用的所有代码仓库中的平均规则来生成结果,而这就是你们最终得到的结果。

解决这个问题的方法并不是让提示信息变得更长,因为那样每次使用时你们都得重新输入这些信息,而且不同的团队成员也很可能会编写出不同的版本。正确的解决办法是创建一组存储在代码仓库中的文件,这些文件能够自动加载,并且可以像维护普通代码一样来维护它们。

本教程会教你如何构建这样的文件结构,如何确保这些文件能够在四种或五种不同的工具格式下保持一致性,最重要的是,如何防止这些文件逐渐过时。毕竟,一个描述了6个月前就被删除的代码库的上下文文件,其实比根本没有上下文文件还要糟糕。

这里所有的内容都基于一个可供你克隆并运行的示例代码仓库:github.com/Adeniyikayodee/MCF。这个仓库没有任何依赖项,因此你只需要安装Node 20或更高版本的Node.js就可以使用了。

目录

开始之前你需要准备什么

你应当熟悉Git和终端操作,同时需要安装Node 20或更高版本的开发环境。此外,你还应该至少在某个实际项目中使用过Claude Code、Cursor、GitHub Copilot或Codex等代码辅助工具。

你不需要了解模型内部的运作原理,因为本教程所讲解的内容都与磁盘上的文件有关。

为什么上下文窗口才是真正的限制因素

当辅助工具在执行任务时,它所掌握的所有信息都存储在一个被称为“上下文窗口”的缓冲区中。这个缓冲区会保存系统提示符、你的输入内容、辅助工具打开的所有文件、执行的每条命令,以及这些命令产生的堆栈跟踪信息。

但重要的是要明白,这个缓冲区的容量是有限的,而且它的填充速度会比大多数人想象的要快得多。例如,仅仅一次调试过程,就可能消耗数万个令牌,而辅助工具却可能还没有编写出任何一行代码。

对于本教程来说,关键在于了解当这个缓冲区被填满时会发生什么。Anthropic的工程团队将这种现象称为“上下文失效”,也就是说,随着令牌数量的增加,模型获取特定指令的能力会逐渐下降。模型并不是故意忽略你的输入,而是因为可用的注意力资源有限,越来越多的信息在争夺这些资源。

这个事实彻底颠覆了大多数人对于上下文文件的理解:人们通常认为编写更多的内容会更安全,因为这样就能覆盖更多情况,减少随机因素的影响。但实际上,你添加的每一行代码都在与其它所有代码竞争有限的注意力资源。

Claude Code的文档明确指出了这一问题的后果:过长的指令文件会导致辅助工具忽略其中规定的规则;而文件过长所带来的另一个现象就是,辅助工具会反复违反那些你明明写清楚的规则。

下面大致说明了在实际任务中这些注意力资源是如何被分配的:

系统提示符及工具定义                ~12,000个令牌
启动时加载的上下文文件            ~4,800个令牌
辅助工具打开的三个源代码文件        ~9,000个令牌
包含堆栈跟踪信息的测试运行          ~3,500个令牌

在上面的例子中,那个包含4,800个令牌的上下文文件,与辅助工具为修复错误而需要读取的堆栈跟踪信息正在争夺相同的注意力资源。如果这个文件只有600个令牌,且其中只包含了正确的路径信息,那么辅助工具就有足够的空间去直接阅读源代码本身——而这正是它非常擅长的工作。

在讨论上下文文件的问题之前,我们首先应该认识到:它们本质上是一个关于资源分配的问题。而本教程中的几乎所有改进措施,都是源于对这一问题的认真思考和解决。

三个层次

这种有效的处理方式将上下文信息分为三个具有不同成本的不同层次来进行管理。

始终会被加载的层其实是存储在你的代码仓库根目录中的单个文件。每当会话开始时,代理程序都会读取这个文件,无论当前要执行的任务是修复拼写错误还是进行数据迁移操作。你为每一个请求都需要支付费用来使用这个文件,因此其中仅包含与仓库中所有任务都相关的信息。此外,这个文件的体积很小,几乎可以在一分钟内被完全读完。

限定作用域的文件层由嵌套文件组成,这些文件仅会在代理程序在特定目录内运行时才会被加载。与您的API相关的规则保存在src/AGENTS.md文件中,因此那些仅涉及前端功能的任务根本不会消耗这些规则所对应的资源。

按需加载的文件层则由普通的文档组成,根目录文件是通过路径来引用这些文档的,而不是将它们内嵌到代码中。引用一个路径只需要消耗少量的令牌,而相应的文档可能会消耗两千个令牌,因此代理程序只会在实际需要使用这些文档时才消耗预算中的资源。

这种设计方式与新工程师的工作习惯非常契合——他们在入职第一天并不会记住您的架构文档内容,而是知道这些文档的存在,并在需要的时候再去阅读它们。

在配套仓库中,最终的文件结构如下所示:

MCF/
├── AGENTS.md                          始终会被加载,其资源消耗是经过预算控制的
├── CLAUDE.md                          由AGENTS.md生成的文档
├── .github/copilot-instructions.md    也是由AGENTS.md生成的文档
├── .cursor/rules/testing.mdc          具有全局作用域的文件,由人工编写
├── .claude/
│   ├── settings.json                  用于运行代码检查的工具钩子
│   └── skills/add-endpoint/SKILL.md   工作流程相关的文档,按需加载
├── docs/
│   ├── architecture.md          架构文档
│   ├── testing.md            测试相关文档
│   └── decisions/0001-in-memory-store.md 决策流程相关文档
├── scripts/
│   ├── context-lint.mjs        用于代码检查的工具脚本
│   └── sync-context.mjs       同步上下文的相关脚本
├── src/
│   ├── AGENTS.md                      仅在源代码目录中有效
│   ├── api/tasks.js         API相关任务脚本
│   ├── services/tasks.js      服务相关任务脚本
│   ├── lib/validate.js        验证工具脚本
│   ├── router.js          路由器相关脚本
│   └── server.js          服务器端脚本
└── tests/

无需维护四份副本即可选择合适的格式

不同的开发团队对于同样的功能选择了不同的文件命名方式,这确实有些麻烦,但只要确定哪个版本是官方认可的规范,这些问题就可以得到解决。

AGENTS.md可以说是最接近于大家公认的规范格式。它使用纯Markdown格式编写,没有固定的结构要求;其管理机制由Linux基金会旗下的Agentic AI Foundation负责维护;同时,Claude Code、Codex、Cursor、Copilot、Gemini CLI、Aider、Windsurf、Zed等众多工具都能直接读取这种格式的文件。

嵌套文件也是该规范的一部分,其中距离正在编辑的代码最近的文件会优先被加载;而如果您在聊天框中直接输入内容,这些内容也会覆盖其他所有设置。

尽管如此,特定于某些工具的格式仍然存在。Claude Code会读取CLAUDE.md文件,然后遍历目录结构,将找到的所有相关文件的内容合并起来,并处理@path/to/file这样的导入语句;Cursor则使用位于.cursor/rules/目录下的.mdc文件,这些文件包含YAML格式的前置信息,可以用来指定规则的适用范围,例如tests/**/*.js。这种格式在表达能力上是最强的,但同时也最不具便携性,因为除了Cursor之外,其他工具都无法读取这种格式的文件;而GitHub Copilot则只会读取仓库根目录下的.github/copilot-instructions.md文件。

<实际操作的方法是只需编写一次 `AGENTS.md` 文件,然后利用它来生成其他所需的文件;只有当某些工具提供的功能无法通过这种共享格式来表达时,才需要手动编写单独的文件。在实际应用中,这就意味着可以使用 Cursor 的全局匹配功能来进行文件生成工作。你也可以通过创建符号链接来实现这一目的:

ln -s AGENTS.md CLAUDE.md

虽然符号链接代表了最简洁的路径,但它们会在Windows系统上给贡献者带来麻烦,也会影响某些持续集成环境的检取配置。因此,配套仓库采用了另一种方法:通过一个小型脚本来实现相同的功能。该脚本会在生成的每个文件中添加一段提示信息,这样善意的团队成员就不会误删这些文件,从而避免在下次同步时丢失已做的修改。

// scripts/sync-context.mjs const banner = ``; export const targets = [ // Claude Code会自动处理导入语句,因此它生成的文件中只包含指向原始文件的引用以及一些仅对该工具有效的额外信息。 { path: 'CLAUDE.md', render: () => `${banner}\n\n@${SOURCE}\n\n${CLAUDE_EXTRAS}` }, // Copilot没有导入语法,因此其源代码会被直接嵌入到目标文件中。 { path: '.github/copilot-instructions.md', render: (source) => `${banner}\n\n${source}` }, ];

由于Claude Code会自动处理导入语句,因此它生成的文件体积很小——其中只包含指向原始文件的引用以及一些特定于该工具的额外信息,这样文件的大小也就只有大约130个字符而已,而不会重复存储整个原始文件的内容。

> @AGENTS.md ## Claude Code的特殊说明 - 如果某次修改涉及超过三个文件,建议使用“计划模式”进行操作;而对于只修改一行代码的情况,则可以直接忽略这个规则。 - 建议将代码库的探索工作委托给辅助代理来执行,这样得到的结果会以摘要的形式呈现,而不会在主流程中导致大量文件被读取。

运行该脚本会重新生成这两个文件,如果再次运行则不会有任何效果。这正是我们希望钩子或持续集成任务能够反复执行的操作。

图1:终端显示 `npm run sync:context` 正在生成 CLAUD.md 和 Copilot 指令文件,随后 `git status` 显示这两个文件都被标记为已修改

编写根文件

根文件包含了最重要的内容,但也是人们最容易出错的地方,因为人们的本能反应往往是把所有信息都写进去。

对于每一行你考虑添加的内容,都要问自己这样一个问题:如果删除这一行,会不会导致代理程序出现错误?如果答案是否定的,那么这一行就只是在浪费你的注意力,并且不会带来任何实际好处,因此应该将其删掉。认真运用这个标准,就能去掉人们在这些文件中添加的大部分内容。

下面这种文件正是这种检查机制所针对的目标:

# AGENTS.md ## 关于这个项目 这个项目是一个用于管理任务的REST API。它最初是由平台团队在2023年开发的,后来由核心服务团队负责维护。代码采用现代JavaScript编写,并使用了ES模块规范。 ## 代码风格 - 使用有意义的变量名 - 编写清晰、易于维护的代码 - 遵循DRY原则 - 使用`const`代替`var` - 在代码复杂的地方添加注释 ## 文件结构 - `src/server.js` 包含服务器逻辑 - `src/router.js` 包含路由规则 - `src/api/tasks.js` 包含任务处理函数 - `src/services/tasks.js` 包含任务服务相关功能

那里的每一行代码都未能通过测试。模型早已明白`const`的作用,它也能看出名为`router.js`的文件中确实包含了路由相关的代码;而且,知道2023年这段代码是由哪个团队开发的,并不会改变模型所做的任何决策。

然而,有一件事是智能体确实无法自行弄清楚的:那就是这个项目刻意设置成了没有任何依赖关系的状态,而这一信息在文件中完全没有被提及。

这就是随附仓库中提供的版本:

# AGENTS.md

这个`Task API`被用作教程中讲解如何管理上下文文件的示例。这个文件是智能体指令的唯一权威来源,而`CLAUDE.md`以及`.github/copilot-instructions.md`这些文件都是通过`npm run sync:context`命令从这个文件生成的,因此请直接编辑这个文件,而不要修改那些生成后的文件。

## 命令

- 安装:无需安装任何依赖包,因为该项目根本没有依赖关系。
- 运行测试:`npm test`
- 在3000端口启动服务器:`npm start`
- 检查上下文文件:`npm run lint:context`
- 重新生成特定于工具的上下文文件:`npm run sync:context`

## 一些从代码中看不出来但需要遵守的规范

- 测试运行器是Node内置的运行器,通过`node --test`命令来调用它,因此请不要在这个仓库中添加Jest、Vitest或其他任何测试相关的依赖包。
- 这个项目故意不使用任何依赖包,因此遇到问题时应该依靠Node的标准库来解决,而不是通过添加新的包来解决问题。
- `src/api/`目录中的处理函数会返回`{ data }`或`{ error: { code, message } }`这两种格式的数据,而永远不会直接指定HTTP状态码,因为`src/router.js`文件已经负责将错误代码映射到相应的状态码上。
- 处理函数永远不要直接操作存储系统,因此任何用于读取或写入任务数据的逻辑都应该放在`src/services/tasks.js`文件中。
- 存储系统是一种模块级别的状态机制,它的状态会在不同的测试用例之间保持不变,因此任何创建任务的测试文件都必须在`beforeEach`钩子中调用`resetTasks()`函数来重置存储状态。

## 如何判断任务是否完成

在报告某个任务已完成之前,请先运行`npm test`和`npm run lint:context`命令,然后直接粘贴测试结果,而不是简单地断言测试通过了。

关于各个部分的用途,请注意以下几点:这些命令的存在是因为智能体无法可靠地猜测你的脚本名称,而如果猜错了,就会导致测试失败;那些规范都是因为从代码中看不出来,或者与模型原本会做出的假设相矛盾,所以才被明确写出来的;每个规范都会说明其制定的原因,这样即使遇到规则作者没有预料到的情况,这些规则也能继续发挥作用。最后那一部分列出的只是文件路径,这体现了按需提供信息的机制。

关于哪些内容应该被包含进来,这里有一些大致的指导原则:

应包含的内容 不应包含的内容
代理程序无法猜测到的指令 通过阅读代码就能了解的任何信息
与语言默认规范不同的约定 模型已经掌握的标准规范
测试运行工具及其使用方法 详细的API文档(应提供链接)
分支命名规则及提交请求的注意事项 每个迭代周期都会发生变化的信息
与您的项目相关的具体架构决策 冗长繁琐的解释或教程
环境中的特殊设置及所需的变量 对代码结构的逐文件说明
那些不易被发现的陷阱或问题 诸如“编写简洁清晰的代码”之类的建议

确定正确的规则范围

制定糟糕的规则还有另一种方式,那就是使其具有的具体性水平不当。Anthropic的建议将这种问题描述为:需要找到一个恰当的规则范围——这个范围既不能过于严格,以至于在遇到第一个不符合预期的情况时就失效;也不能过于模糊,导致模型根本不知道该如何行动。

如果规则过于严格,那么一旦遇到不符合要求的处理函数,它就会立即失效:
- 每个路由处理函数的代码长度必须恰好为40行,并且必须在第3行调用validate()函数。

如果规则过于模糊,那么它根本不会改变代理程序的行为:
- 编写清晰、易于维护的代码。

正确的规则范围应该是这样的:
- 路由处理函数负责解析和验证输入数据,然后将其处理结果传递给`src/services/`目录中的相应函数。
  处理函数不应直接修改数据存储结构。具体实现方式可以参考`src/api/tasks.js`文件。

第三种规则制定方式是向代理程序明确说明规则的框架、它不能跨越的界限,以及在哪里可以找到相关的示例代码。这种做法其实就相当于在员工入职的第一天向他们讲解这些基本规则。

将规则限定在特定目录内

任何只在与树结构中的某个特定部分相关的情况下才需要被考虑的规则,都应该被放在嵌套文件中。判断一个规则是否属于这类规则的标准很简单:如果在不同的目录中工作的开发人员根本不需要了解这个规则,那么就应该将其移放到相应的嵌套文件中。

<!-- src/AGENTS.md -->
# 基本规则

以下规则适用于`src/`目录下的所有内容,它们位于根文件`AGENTS.md`之上,而不是取代它。

## 添加一个端点

1. 按照相邻处理函数的编写格式,将相应的处理函数添加到`src/api/tasks.js`文件中。
2. 在`src/router.js`文件中的`routes`数组中添加一条记录,并指定其成功状态。
3. 在`tests/api.test.js`文件中添加测试用例,以覆盖成功情况和失败情况。

## 验证规则

验证函数位于`src/lib/validate.js`文件中。它们会返回一个包含错误信息的数组,而不会直接抛出异常;同时,这些函数会报告所有出现问题的字段,而不会在遇到第一个错误就停止执行,因此调用者可以一次性看到所有的错误信息。

这种验证规则就是一个值得记录下来的好例子,因为仅从代码本身是无法理解其含义的。如果让代理程序直接阅读`src/lib/validate.js`文件,它只会看到一个返回数组的函数,而无法判断这是某种刻意设定的规范,还是某个具体实现中的偶然现象。因此,当代理程序尝试编写新的验证函数时,很可能会犯类似的错误:

// src/lib/validate.js
export function validateTaskInput(input) {
  if (typeof input !== 'object' || input === null || Array.isArray(input)) {
    return ['body must be a JSON object'];
  }

  const problems = [];

  if (typeof input.title !== 'string' || input.title.trim() === '') {
    problems.push('title is required and must be a non-empty string');
  } else if (input.title.length > TITLE_MAX) {
    problems.push(`title must be ${TITLE_MAX} characters or fewer`);
  }

  if (input.done !== undefined && typeof input_done !== 'boolean') {
    problems.push('done must be a boolean when present');
  }

  return problems;
}

采用指向方式而非内联方式

在整个系统中,根文件中的“查找位置”部分所消耗的资源是最少的。只需加载四行路径信息,几乎不会耗费任何成本;而在这四行路径之后,还存储着数千条关于架构设计、测试规范以及决策记录的内容,这些内容只有当任务需要时才会被代理程序读取。

那些与架构设计相关的决策记录,正是用来存放那些否则会使根文件变得臃肿的说明内容的理想场所。配套的代码仓库中有一段解释了为什么任务存储采用普通的`Map`结构而不是数据库,其中最有用的内容是在最后一段:

如果任务没有明确要求使用数据库、对象关系映射工具或持久化层,那么代理程序就不应该添加这些组件;而应当将这种缺失视为一种有意的选择,而非需要填补的空白。

如果没有这样的规定,当有人要求“使API具备生产环境可用的功能”时,代理程序可能会默认添加Postgres数据库。但有了这个规定,代理程序就会知道这种缺失是故意为之的,因此在做出更改之前会先询问相关需求。这段说明在平时可能不会带来任何成本,但一旦它帮助你节省了一个下午的时间,它的价值就显而易见了。

同样的逻辑也适用于那些偶尔才会被使用的工作流程。对于添加端点的具体步骤来说,这些信息确实很有用;但如果将这些内容放在每个任务都会被加载的文件中,就会造成资源浪费。因此,这些步骤应该被保存在专门的技能文件中,只有当有人真正需要添加端点时,这些文件才会被加载:

---
name: add-endpoint
description: 按照这个代码仓库规定的层次结构,在任务API中添加一个新的端点
---

# 添加端点

只有当有人需要添加新的端点时,这个工作流程才会被加载,因此它被保存在这里,而不是放在`AGENTS.md`文件中——因为如果放在那里的话,每次启动程序都会加载这些内容,造成不必要的资源消耗。

如果你还没有阅读过`docs/architecture.md`,请先阅读它,然后再按照以下步骤依次操作:
1. 确定哪个层次结构应该负责实现这个新功能。任何与读取或写入任务相关的内容都应该放在`src/services/tasks.js`文件中;而任何与请求格式相关的内容则应该放在`src/api/tasks.js`文件中。
2. 如果端点需要接收输入数据,那么需要在`src/lib/validate.js`文件中添加或修改验证逻辑,使得该逻辑能够返回一个包含错误信息的数组,这样处理函数就可以一次性报告所有错误。
3. 在`src/api/tasks.js`文件中编写处理函数,成功时返回`{ data }`,失败时返回`{ error: { code, message } }`;如果存在现有的错误代码,可以直接使用这些代码。
4. 在`src/router.js`文件中的`routes`数组中注册这个端点,并指定它应该返回的成功状态码;如果你引入了新的错误代码,也需要将其添加到`STATUS_BY_ERROR_CODE`数组中。
5. 在`tests/api.test.js`文件中至少编写一个成功案例和一个失败案例。
6. 运行`npm test`和`npm run lint:context`命令,然后将它们的输出结果整理到总结报告中。

不要添加任何依赖项,不要引入持久化层,也不要在处理函数中设置状态码。

使上下文文件具备可验证性

到目前为止,所提到的建议都属于比较常规的内容,单独来看,这些建议的时效性也很有限。上下文文件的问题与文档文件的问题本质上是相同的:当这些文件中的信息有误时,实际上并不会引发任何问题。例如,如果你将 `src/services/task.js` 文件重命名为 `src/services/tasks.js`,那么这个上下文文件仍然会继续指向一个已经不存在的路径;而如果你删除了 `typecheck` 脚本,六个月后,系统在尝试运行这个脚本时可能会浪费大量的资源。不过,没有人会注意到这些情况,因为你的开发流程中并没有任何机制来检测这些问题。

因此,你需要在开发流程中加入检查机制,让这些错误能够被及时发现。配套的代码仓库中有一个名为 `scripts/context-lint.mjs` 的工具,它可以执行四项检查;这个工具由大约 150 行不依赖第三方库的 JavaScript 代码组成,你完全可以在一个下午的时间内将其适配到自己的代码仓库中。

第一项检查是针对在程序启动时被加载的所有文件进行的:

// 这些文件会在每次会话开始时被自动加载,无论任务是否需要使用它们。如果某个文件的“令牌预算”超过了上限,就需要将其相关内容移到文档中,并保留原来的路径。 const ALWAYS_LOADED = [ { path: 'AGENTS.md', budget: 800 }, { path: 'CLAUDE.md', budget: 300 }, { path: '.github/copilot-instructions.md', budget: 900 }, { path: 'src/AGENTS.md', budget: 400 }, ]; // 这个数值是针对英文文本的平均值。精确性并不是关键,重要的是要能够检测出那些令牌数量超过预算的文件。 const CHARS_PER_TOKEN = 4; const estimateTokens = (text) => Math.ceil(text.length / CHARS_PER_TOKEN);

每個令牌使用 4 个字符来进行计算,这个数值其实只是一个近似值,并非真正的令牌划分标准。对于那些包含大量代码的文件来说,这个数值可能会显得有些偏高。不过这并没有关系,因为我们真正关心的是文件的“令牌预算”是否超过了上限。如果一个文件的令牌数量从 400 增加到 800,这才是我们需要关注的重点;而即使实际数值超出了预算 8%,也不会影响我们对这种情况的处理方式。

第二项和第三项检查会将你的上下文文件视为普通文本进行验证,确保其中提到的路径确实存在,以及任何 npm 脚本都确实存在于 `package.json` 文件中:

// 首先会去除那些被括号包围的代码块,因此示例中的代码永远不会被视为真实的引用。 function inlineCodeSpans(text) { const prose = text.replace(/```[\s\S]*?```/g, ''); return [...prose.matchAll(/`([^`\n]+)`/g)].map((match) => match[1].trim()); } for (const span of spans) { if (looksLikePath(span)) { if (!existsSync(join(ROOT, span))) { problems.push(`${file} 指向了一个不存在的路径:${span}`); } continue; } const script = span.match/^npm run ([\w:-]+)$/) ?? span.match (^npm (test|start)$/); if (script && !scripts.includes(script[1])) { problems.push(`${file} 提到了一个并不存在于 package.json 中的 npm 脚本:${span}`); } }

在扫描代码之前先去除那些被括号包围的代码块,这一操作的重要性远超你的想象。因为你的文档中有很多示例性内容,这些内容本来就不是为了作为真实引用而存在的;如果一个代码检查工具因为这些示例而报错,那么很可能在一周内就被人们忽略掉了。

第四次检查会以模拟模式重新运行同步脚本;如果生成的任何文件与AGENTS.md的内容不一致,检查就会失败,这样就能及时发现那些无视警告提示、直接修改CLAUDE.md的同事。 在正常的代码仓库中,整个检查过程耗时不到一秒钟: 图2:通过npm run lint:context命令在终端输出的结果,显示了4个上下文文件及其对应的预算限制、在9个文件中检测到的47处引用错误、已同步生成的文件,以及未发现任何问题。 有趣的是,当代码出现问题的时候,这个检查机制会如何反应。如果在AGENTS.md中添加一条看似合理的描述——比如提到某个已被删除的脚本或一个被重命名的文件——就会产生如下结果: 图3:通过npm run lint:context命令在终端输出的结果,显示了3个与代理程序相关的问题:一个未包含在package.json中的npm脚本、一个不存在的路径,以及一个与AGENTS.md内容不一致的生成文件。 该脚本会以非零状态退出,因此将其集成到持续集成流程中只需要四行代码;这样一来,文件的内容就绝对不可能发生悄悄的变化。
# .github/workflows/ci.yml
      - name: 运行测试套件
        run: npm test

      # 每次拉取请求都会检查这些上下文文件,这样就能确保它们始终与所描述的代码保持一致。
      - name: 检查上下文文件
        run: npm run lint:context
图4:MCF仓库的GitHub Actions执行流程,显示验证任务成功完成,测试套件和上下文检查工具都显示为绿色状态。 如果这个教程中的其他内容都必须被舍弃,那么这部分内容我一定会保留下来。一个内容简单但能够准确反映实际情况的上下文文件,总比一个描述了去年架构的、写得再精美的文件要好得多——因为代理程序根本无法区分这两者,它会同样认真地对待它们。

为代理程序提供可供验证的对象

在那个根目录文件中,还有一行内容值得仔细研究,那就是对“工作完成标准”的定义。 当某项工作看起来已经完成时,代理程序就会停止运行;如果没有进行检查,它就可以自行判断“工作是否完成”,而“看起来已经完成”就是它唯一可用的依据。这样一来,你就自然而然地成为了这个验证机制的一部分——所有的错误都会等着你去发现。 为某个命令指定“通过”或“失败”的标准,就能让代理程序根据这些标准自行执行操作:它会编写代码、运行检查、读取结果,然后不断重复这个过程,直到检查结果为“通过”为止。

正因为如此,所以在将某个任务标记为已完成之前,执行 `npm test && npm run lint:context` 这一操作,对于提升代码的输出质量来说,其效果要比你编写任何格式规范指南都要显著得多。要求代理程序直接显示输出结果而非仅仅确认操作是否成功也同样重要,因为查看这些输出结果只需要几秒钟的时间,而如果你自己重新运行验证流程,则可能需要花费数分钟的时间。

不过,上下文文件中的说明本质上只是建议而已,而当具体的使用场景变得越来越复杂时,这些建议往往就会被忽略。如果某些操作必须每次都无条件地执行,那么就应该使用“钩子”机制——这种机制会在代理程序的循环中某个固定的节点处自动运行相应的脚本,而且这种设置是无法被更改的:

{
  "hooks": {
    "PostToolUse": [
      {
        "matcher": "Edit|Write",
        "hooks": [
          {
            "type": "command",
            "command": "npm run lint:context --silent"
          }
        ]
      }
    ]
  }
}

一般来说,任何建议性的内容都应该以普通文本的形式呈现;而任何必须严格执行的操作,则应该通过钩子机制或持续集成流程来确保其得到落实。

检查实际效果是否达标

你不应该盲目相信这些指导原则,其实有一种简单的方法可以在你自己的代码仓库中亲自验证这些规则的有效性。

选择一项结构明显正确的任务,把相应的操作指令记录下来以确保每次执行时这些指令都保持不变,然后分别在你的当前分支以及已经删除了相关上下文文件的分支上各运行一次这个任务。在示例仓库中,“添加一个能够返回未完成任务数量的 `GET /tasks/count` 端点,并为其编写测试用例”就是一个很好的测试对象。

接下来,从四个方面来对比两次测试的结果:在没有你进行任何干预的情况下,测试是否都能通过?你需要进行多少次修改才能让代码正常运行?代码是遵循现有的结构进行编写的,还是直接从数据存储层中获取了所需的数据?是否有新的依赖项被添加进了代码中?

这个例子只是一个参考案例,并不能代表一个全面的测试标准,但你可以通过它来判断你的代码文件是否发挥了应有的作用;同时,当出现问题时,这个例子也能帮助你迅速找出是哪条具体的规则没有被遵守。

图5:通过 `npm test` 运行后得到的终端输出结果,显示了所有路由和验证规则都通过了测试

保持文件的健康状态

对待这些文件,就应该像对待代码一样——一旦发现它们出现了问题,就应该立即进行检查,而不是按照固定的时间表来执行维护操作。

有两种诊断方法可以帮助你解决大部分遇到的问题。如果代理程序不断违反某些明确规定的规则,那么很可能是因为文件的内容太长了,导致那些规则被其他信息掩盖了;在这种情况下,你应该果断地删除冗余的部分,而不要试图通过加强规则的约束力来解决问题。

如果代理程序提出了某个问题,而你的代码文件已经包含了回答这个问题的内容,或者问题的表述不够清晰明确,那么你就应该重新编写相关代码,而不是在原有代码旁边添加额外的代码行。

除此之外,还要删除代理程序未经指示就遵循的任何规则,因为模型的默认设置会在每次更新中得到改进,而去年还被认为是必要的某些规则,现在可能已经变得多余了。

可以通过查看代码检查工具的输出结果中的令牌使用量来大致判断文档的质量状况——如果某个文件的令牌使用量不断接近上限,那就说明其中的某些内容需要被移放到docs/目录中。

值得避免的错误

最常见的错误就是创建那种“杂乱无章”的文档文件:人们会把所有曾经提到过的规范都添加进去,结果文件中的令牌数量会达到数千个,而代理程序实际上只遵循其中的一小部分内容。解决这个问题的方法是严格执行测试规则,而不考虑任何情感因素。

另一个常见的错误是将README文件的内容复制到上下文文档中,这样做只会增加每次代码检查的工作量,却没有任何实际意义,因为这两份文档的受众群体是不同的,代理程序在需要时完全可以直接阅读README文件。

第三个需要避免的错误就是为模型已经能够自行理解的内容编写说明文档。任何描述文件内容而非规定代理程序应如何处理这些内容的条款,都属于这种错误的例子。

第四个错误在于制定一些无法被验证的规则,比如要求代码具有可读性或性能良好,这样的要求听起来合理,但实际上代理程序根本无从判断自己是否遵守了这些规则。

第五个也是最终会导致团队陷入困境的错误,那就是让每个工具都维护自己的文档副本。起初这些副本是相同的,但一个月后就会开始出现差异,最终Cursor和Claude Code可能会在同一代码库中根据相互矛盾的指令来执行操作。因此,建议在持续集成环境中生成这些文档副本,并对其进行验证。

从哪里开始

如果读完这篇文章后只做一件事的话,那就先对你已经拥有的上下文文档进行令牌数量估算,然后逐行检查这些内容,看看删除其中任何一行是否会导致错误。大多数人第一次检查时就会删除文件中三分之一到一半的内容,之后会发现代理程序对剩余的部分能够更可靠地执行操作。

之后,添加一些引用链接,使你的文档更容易被访问且不会增加维护成本;同时将代码检查工具集成到持续集成环境中,这样随着代码库的发展,这些设置也能始终保持有效性。

包括代码检查工具、同步脚本、钩子程序以及持续集成工作流程在内的完整配置信息,可以在github.com/Adeniyikayodee/MCF找到。克隆这个仓库后,运行npm run lint:context来测试其功能;然后试着在AGENTS.md文件中修改一些内容,观察代码检查工具是否会报错。

你可以根据自己的实际需求对代码检查工具进行定制,而不必逐字复制它的配置,因为只有那些能反映你的代码库实际情况的检查规则才具有实际意义。

如果你想自己尝试使用这个工具并进行实验,可以克隆这个仓库。克隆后,你会得到一个可以自由修改的分支,同时也可以随时将后续的变更拉取回来。如果你希望在这些变更被应用后能够及时收到通知,可以在“Fork”按钮旁边选择“Releases”或“All activity”,因为只有这样才能真正接收到通知;而仅仅进行克隆操作的话,你只能获得克隆当天的代码版本而已。

进一步阅读资料

相关文章

技术实践

Cloudflare推出了用于实现源站后缓存控制的缓存响应规则

Cloudflare最近推出了“缓存响应规则”这一功能——这种规则引擎会在源服务器做出响应之后、但在内容被写入Cloudflare的缓存系统之前开始发挥作用。此前,缓存规则仅能针对请求的相关属性进行操作;而“缓存响应规则”则增加了在内容被缓存之前对其响应内容进行评估的环节。 作者:雷纳托·洛西奥

阅读全文
技术实践

如何使用Pydantic AI构建具备生产级功能的智能代理

使用原始的LLM SDK来构建AI代理,在开发原型阶段确实可行,但一旦你需要结构化输出、可测试的代码以及具备生产环境可靠性的系统,这些问题就会显现出来。 这些问题的出现具有很强的规律性。你的笔记本代码可以正常运行,于是你将其应用到生产环境中,并开始添加各种补丁:比如为`json.loads`添加异常处理逻辑,编写辅助函数来去除Markdown格式的标记,使用`if`语句检查字段类型,设置重试机制,以及创建一个将工具名称与对应的可调用函数关联起来的映射函数。这些代码单独来看并不复杂,但当它们汇集在一起时,就会占据你代码库的大部分内容,而真正的代理逻辑反而被这些辅助代码所掩盖。 本文将按照这些问题

阅读全文
技术实践

如何利用人工智能对传统应用程序进行现代化改造,同时又避免对其进行彻底的重写?

我见过一些旧系统的迁移项目被认为取得了成功,因为那些旧的框架已经从代码库中消失了。 但六个月后,团队仍然在面对同样的耦合问题、同样不清晰的业务规则,以及几乎相同的部署难题。 虽然技术已经发生了变化,但整个系统本身并没有发生太大的改变。 人工智能让这个问题变得更加复杂了。 它能够比人类团队更快地翻译代码,能够解释那些不熟悉的类结构,生成测试用例,创建适配器,更新API接口,从而大大减少重复性工作。 但是,如果你让一个人工智能编码工具去处理一个旧应用程序,并简单地要求它将所有内容都迁移到现代的技术架构中,那么很可能会得到你想要的结果: 还是那个系统,只不过被更快地重新编写了一遍而已。 这并不一定算

阅读全文
技术实践

Astro 7:Rust编译器、Rust Markdown处理工具以及Vite 8框架,使得构建速度提升了高达61%。

Astro 7着重提升构建性能,它采用了原生工具以及用Rust语言重新编写的编译器。新版本在处理Markdown格式的内容时效率更高,同时对HTML代码的规范要求也更加严格。最近的更新为该工具添加了高级路由功能及增量构建机制;不过,在用户反馈中也有提到一些关于旧文件兼容性以及依赖项数量计算方面的问题。Astro主要适用于那些依赖少量JavaScript代码的内容驱动型网站。 作者:丹尼尔·柯蒂斯

阅读全文