☰
AI编程Memory工程实战:用AGENTS.md构建持久化项目记忆
2026/10/16 13:17:03 网站建设 项目流程

1. 从无状态到有状态:AI 编程范式转换的底层逻辑

1.1 为什么“无状态模型”正在成为瓶颈

过去两年,大多数人使用 AI 编程的方式非常原始:打开一个对话框,把代码片段贴进去,问一句“帮我改改”,拿到结果,关掉窗口。下一次再问,模型完全不记得你是谁、你的项目用什么框架、你的命名习惯是什么。这就是典型的无状态交互——每次对话都是一张白纸。

这种方式在写小脚本、查 API 用法时还能凑合,但一旦进入真实项目就会暴露三个致命问题。第一,上下文断裂。你昨天让模型帮你设计了一套错误处理规范,今天新开一个会话让它写业务代码,它完全不知道那套规范的存在,生成出来的代码风格和昨天截然不同。第二,重复解释成本极高。每次都要重新交代项目背景、技术栈、目录结构、代码规范,这些信息可能占掉输入窗口的三分之一甚至一半。第三,无法积累。你在项目中踩过的坑、定下的约定、沉淀的最佳实践,全部散落在几十个历史会话里,没有任何机制把它们固化下来。

我自己的体感是,当项目代码超过五千行之后,纯对话式的 AI 辅助编程效率会断崖式下降。不是模型变笨了,而是缺少一个持久化的“项目记忆层”,模型每次都在盲人摸象。

1.2 Memory 工程到底在解决什么问题

Memory 工程这个词听起来很玄,但拆开看其实很朴素:让 AI 在多次交互之间保持对项目的一致认知。它要解决的核心问题可以归纳为一句话——如何把“人脑中的项目上下文”翻译成“模型能稳定读取的结构化信息”。

这里面有几个关键设计决策。首先是存储位置。记忆放在哪里?放在对话历史里?那会话一关就没了。放在外部数据库里?那每次都要额外检索,增加延迟和复杂度。目前社区逐渐收敛到一个共识:把记忆以文件的形式放在项目仓库里,跟代码一起做版本管理。这样做的好处非常直接——记忆跟着项目走,换工具、换模型、换机器,只要仓库在,记忆就在。

其次是记忆的粒度。太粗了没用,比如“这是一个 React 项目”这种信息模型自己也能推断出来。太细了又维护不动,比如把每个函数的实现细节都写进去。比较合理的粒度是约定、规范和关键决策:目录结构约定、命名规范、错误处理模式、依赖选型理由、禁止使用的 API 等等。这些信息量不大,但对生成代码的质量影响极大。

最后是读取时机。记忆不能等到模型犯错之后再补,必须在生成之前就注入。目前主流的做法是在每次请求的系统提示或初始上下文中,自动加载记忆文件的内容。这就引出了下一个话题——用什么格式来写这份记忆。

1.3 声明式配置为什么比命令式指令更适合做记忆载体

早期大家写记忆的方式很随意,有人用自然语言段落,有人用 YAML,有人直接写 Markdown 列表。用着用着就发现,自然语言段落太模糊,模型有时候抓不住重点;YAML 太严格,写起来累,而且不适合表达“建议”类的软约束;纯列表又太散,缺少层次结构。

最后跑出来的是Markdown 格式的声明式配置,也就是标题里说的 AGENTS.md 这类文件。它的优势在于三点。第一,Markdown 本身有结构,标题层级天然表达了信息的优先级和归属关系,模型解析起来很稳。第二,声明式写法聚焦“是什么”而不是“怎么做”,你只需要告诉模型“本项目使用 pnpm 而不是 npm”,不需要写一堆解释,模型自己会推断出后续行为。第三,人也能读,新人加入项目时,看一遍 AGENTS.md 就能快速了解项目约定,这份文件同时服务于人和 AI,维护动力更强。

注意:声明式配置的核心是“约束”而非“教程”。不要在里面写大段的教学内容,模型不需要你教它怎么编程,它需要的是知道你这个项目的特殊约定是什么。

2. AGENTS.md 文件结构设计与核心字段拆解

2.1 一份可落地的 AGENTS.md 应该包含哪些区块

我前后迭代过七八个版本,最后稳定下来的结构大概包含六个区块。不是每个项目都需要全部写满,但区块的划分方式建议保持一致,这样换项目时肌肉记忆能复用。

第一个区块是项目概览,用三到五句话说明这个项目是做什么的、面向什么场景、当前处于什么阶段。这个区块看起来废话,但它能帮模型建立“大局观”,避免生成与项目定位不符的代码。第二个区块是技术栈声明,精确到具体版本和包管理器。第三个区块是目录结构约定,说明每个顶层目录放什么类型的文件。第四个区块是编码规范,包括命名、格式化、注释风格。第五个区块是依赖与工具链,列出核心依赖和禁止使用的替代品。第六个区块是已知约束与禁忌,这是最有价值的部分,记录那些“看起来可以但实际上不行”的操作。

下面是一份脱敏后的示例结构,你可以直接照着改:

# AGENTS.md ## 项目概览 - 项目类型:跨平台桌面应用 - 核心功能:本地文件索引与全文检索 - 当前阶段:功能开发中期,API 已冻结 ## 技术栈 - 语言:TypeScript 5.4+ - 运行时:Node.js 20 LTS - 包管理器:pnpm(禁止使用 npm 或 yarn) - 构建工具:Vite 5 - 测试框架:Vitest ## 目录结构 - src/main:主进程代码,禁止引入浏览器 API - src/renderer:渲染进程代码,禁止直接访问文件系统 - src/shared:共享类型与工具函数 - tests:测试文件,与 src 目录结构镜像对应 ## 编码规范 - 变量命名:camelCase - 类型命名:PascalCase - 常量命名:UPPER_SNAKE_CASE - 禁止使用 any,必要时用 unknown 加类型守卫 - 所有导出函数必须有 JSDoc 注释 ## 依赖约束 - 状态管理统一使用 Zustand,禁止引入 Redux - 日期处理统一使用 date-fns,禁止引入 moment - HTTP 请求统一走 src/shared/http.ts 封装,禁止直接调用 fetch ## 已知禁忌 - 禁止在渲染进程中使用 Node.js 原生模块 - 禁止在循环中执行同步文件读取 - 禁止提交 console.log,使用 logger 工具替代

2.2 每个字段背后的设计意图与常见误区

很多人第一次写 AGENTS.md 会犯一个错误:把文档写成教程。比如在“编码规范”里写“函数应该保持单一职责,每个函数不超过 50 行,因为这样便于测试和维护”。后半句的解释完全是多余的,模型早就知道单一职责原则,它需要知道的只是“你这个项目对函数长度有没有硬性要求”。

另一个常见误区是写得太抽象。“代码应该清晰易读”这种话等于没写,因为没有任何可执行的判断标准。有效的写法是给出可验证的规则,比如“所有异步函数必须显式标注返回类型”或者“组件文件必须与同名测试文件放在同一目录”。

还有一个坑是版本信息缺失。只写“使用 TypeScript”是不够的,因为不同版本的类型系统行为差异很大。写清楚“TypeScript 5.4+,启用 strict 模式,禁用 implicit any”才能让模型生成真正兼容的代码。

实操心得:每次模型生成出不符合预期的代码时,不要只改代码,要回头想想 AGENTS.md 里是不是缺了对应的约束。把这条约束补进去,下次就不会再犯。这份文件是活的,应该随着项目演进不断增补。

2.3 如何让模型稳定读取并遵守这些配置

写好文件只是第一步,关键是让模型在每次生成时都能读到它。目前有几种主流做法,各有取舍。

第一种是手动注入。每次开新会话时,把 AGENTS.md 的内容复制到对话开头。优点是简单直接,缺点是麻烦,而且容易忘记。第二种是工具自动加载。一些 AI 编程工具支持配置“上下文文件”,启动时自动读取指定路径的文件内容并注入系统提示。这种方式最省心,但依赖具体工具的支持。第三种是脚本拼接。自己写一个包装脚本,在调用模型 API 之前,自动读取 AGENTS.md 并拼接到系统提示里。这种方式最灵活,适合有定制需求的情况。

不管用哪种方式,有一个原则必须遵守:AGENTS.md 的内容要放在系统提示或最靠前的位置,不要放在对话末尾。模型对上下文开头和结尾的信息注意力最强,中间部分容易被忽略。把最重要的约束放在开头,能显著提升遵守率。

3. 实操:从零搭建一套带 Memory 的 AI 编程工作流

3.1 环境准备与工具选型

这套工作流对工具的要求其实不高,核心是能自定义系统提示。如果你用的是带“项目上下文”功能的编辑器插件,直接用它加载 AGENTS.md 就行。如果用的是纯 API 调用,那就自己写一层封装。

我自己的配置是这样的:项目根目录放一个AGENTS.md,然后在项目根目录再放一个scripts/ai-context.mjs脚本,负责读取 AGENTS.md 并输出拼接后的系统提示。调用模型时,用这个脚本的输出作为系统消息。整个链路不依赖任何特定平台,换模型只需要改 API 地址和密钥。

依赖方面,只需要 Node.js 18+ 和一个 HTTP 请求库。不需要数据库,不需要向量检索,不需要额外的服务。越简单越稳定,这是我在多个项目里验证过的结论。

3.2 编写上下文注入脚本

脚本的逻辑很直白:读取 AGENTS.md,读取当前打开文件的路径和内容,拼接成一段结构化的系统提示。下面是一个简化版的实现:

// scripts/ai-context.mjs import { readFileSync } from 'fs'; import { resolve, relative } from 'path'; const ROOT = resolve(process.cwd()); const AGENTS_PATH = resolve(ROOT, 'AGENTS.md'); function loadAgentsConfig() { try { return readFileSync(AGENTS_PATH, 'utf-8'); } catch { console.warn('AGENTS.md not found, using empty config'); return ''; } } function buildSystemPrompt(currentFile) { const agentsConfig = loadAgentsConfig(); const relPath = currentFile ? relative(ROOT, currentFile) : 'unknown'; return [ '你是一个严格遵守项目约定的编程助手。', '以下是本项目的配置声明,所有生成内容必须符合这些约束:', '', agentsConfig, '', `当前正在编辑的文件:${relPath}`, '请基于以上配置和当前文件上下文生成代码。', ].join('\n'); } const currentFile = process.argv[2]; console.log(buildSystemPrompt(currentFile));

这个脚本可以直接在命令行运行,输出结果重定向到文件,再作为系统提示传给模型。如果你用的是支持自定义命令的编辑器,可以把它配置成一个快捷键,一键生成当前文件的上下文提示。

3.3 参数选择与上下文窗口管理

上下文窗口是有限资源,AGENTS.md 写得太长会挤占代码本身的空间。我的经验是把 AGENTS.md 控制在 800 到 1500 字之间。这个长度足够覆盖核心约束,又不会喧宾夺主。

如果项目特别复杂,约束特别多,可以考虑分层加载。核心约束放在 AGENTS.md 里始终加载,模块级的约束放在各子目录的 AGENTS.md 里按需加载。比如src/renderer/AGENTS.md只包含渲染进程相关的约束,编辑渲染进程代码时才加载。这样既能保证约束的完整性,又能控制单次注入的长度。

另一个参数是注入位置。如果模型支持系统提示和用户消息分离,AGENTS.md 放系统提示。如果不支持,就放在用户消息的最前面,并用分隔线跟实际任务隔开。实测下来,用---分隔线隔开配置和任务,模型的遵守率会更高,因为它能清楚区分“规则”和“任务”。

3.4 验证配置是否生效的三种方法

写完配置后,怎么知道模型真的在读、真的在遵守?我常用三种验证方法。

第一种是故意违规测试。在任务里故意要求模型做一件被禁止的事,比如“用 npm 安装一个包”,看它会不会拒绝或者提醒你项目约定用 pnpm。如果它照做了,说明配置没生效。第二种是风格一致性测试。连续生成多个文件的代码,检查命名风格、注释风格是否一致。如果风格飘忽,说明配置的约束力不够。第三种是边界测试。给一个模糊的需求,看模型是否会主动引用 AGENTS.md 里的约束来澄清。比如你让它“加一个日期处理函数”,它应该问你是用 date-fns 还是自己实现,而不是直接引入 moment。

注意:如果测试发现配置没生效,先检查注入位置,再检查文件编码,最后检查模型是否支持系统提示。大部分问题出在注入位置上。

4. 常见问题与排查技巧实录

4.1 模型不遵守配置的五个典型原因

在实际使用中,模型不遵守 AGENTS.md 的情况非常常见。我整理了一个排查表,按出现频率从高到低排列:

问题现象可能原因排查方法解决方案
完全忽略配置配置未注入或注入位置太靠后检查系统提示内容把配置移到最前面
部分遵守部分忽略配置太长,中间内容被忽略统计配置字数精简到 1500 字以内
时好时坏配置格式不统一检查是否有自然语言段落混入统一用列表和标题
遵守旧版配置缓存了旧内容检查文件修改时间清除缓存重新加载
故意绕过约束约束表述太弱检查是否用了“建议”“尽量”等词改用“必须”“禁止”

这张表是我踩了无数次坑之后总结出来的,基本上覆盖了九成以上的问题。其中配置太长是最隐蔽的原因,因为模型不会告诉你“我看不过来”,它只会默默忽略中间部分。

4.2 配置冲突与优先级处理

当项目变大之后,很容易出现配置冲突。比如根目录的 AGENTS.md 说“统一使用 camelCase”,但某个子模块的 AGENTS.md 说“本模块使用 snake_case”。这时候模型该听谁的?

我的处理原则是就近原则加显式声明。子目录的配置优先级高于根目录,但子目录配置必须显式声明“本配置覆盖根配置的某某条款”。这样模型在解析时能明确知道哪条规则生效。如果不想搞这么复杂,那就只保留一份根配置,把所有例外情况都写在根配置里,用注释标明适用范围。

另一种冲突是配置与任务冲突。用户明确要求做一件违反配置的事,比如“我知道项目用 pnpm,但这次请用 npm”。这种情况下,模型应该先提醒冲突,再按用户要求执行。这个行为也需要在 AGENTS.md 里声明,否则模型可能直接拒绝执行,影响效率。

4.3 性能与维护成本的平衡

Memory 工程不是免费的。每次注入配置都会消耗 token,配置越长成本越高。我算过一笔账:一份 1200 字的 AGENTS.md,按每次请求都注入计算,如果一天调用 200 次,一个月下来多消耗的 token 量相当可观。

控制成本的方法有三个。第一,按需加载。不是每次请求都需要完整配置,比如纯问答类请求可以只加载核心约束。第二,定期精简。每个月回顾一次 AGENTS.md,删掉已经内化为习惯的约束,只保留真正容易出错的条款。第三,分层缓存。如果工具支持提示缓存,把 AGENTS.md 的内容标记为可缓存,能显著降低重复注入的成本。

维护成本方面,最大的坑是配置腐化。项目演进之后,有些约束已经过时了,但没人记得去改 AGENTS.md,导致模型一直在遵守过时的规则。我的做法是把 AGENTS.md 的更新纳入代码审查流程,每次合并请求如果涉及规范变更,必须同步更新 AGENTS.md。这样能保证配置和项目实际状态始终一致。

4.4 从单文件到多文件:规模扩大后的演进路径

项目小的时候,一份 AGENTS.md 就够了。但当项目膨胀到几十个模块、上百个文件时,单文件配置会变得臃肿且难以维护。这时候需要演进到多文件配置体系。

我的演进路径是这样的:第一阶段,单文件,所有约束写在一起。第二阶段,按目录拆分,根目录保留全局约束,子目录放模块级约束。第三阶段,引入继承机制,子目录配置可以声明继承自根配置,只写差异部分。第四阶段,引入条件加载,根据当前编辑的文件类型动态选择加载哪些配置。

每个阶段都有对应的工具支持需求。第一阶段只需要能读文件就行。第二阶段需要工具支持按路径加载不同配置。第三阶段需要配置解析器支持继承语法。第四阶段需要更复杂的上下文管理逻辑。不要跳级,在单文件阶段就把约束写清楚,比一上来就搞复杂体系要稳得多。

5. 范式转换的边界与个人实践体会

5.1 什么场景下 Memory 工程收益最大

不是所有项目都值得搞 Memory 工程。我自己的判断标准是看三个指标:项目周期是否超过一个月、代码量是否超过三千行、是否有多人协作。三个指标满足两个以上,Memory 工程的收益就非常明显。因为这三个条件意味着项目有足够多的“隐性知识”需要沉淀,而纯对话式交互无法承载这些知识。

反过来,如果是写一次性脚本、做技术验证、或者个人练手项目,搞一套 AGENTS.md 反而是负担。这时候直接用对话式交互效率更高,没必要为了工程化而工程化。

还有一个容易被忽略的场景是代码审查。把 AGENTS.md 作为审查标准注入,让模型按照项目约定来审查代码,比让它自由发挥要准确得多。这个用法在多人协作的项目里特别有价值,能显著降低审查意见的随意性。

5.2 我踩过的三个印象最深的坑

第一个坑是配置写太满。刚开始搞的时候特别兴奋,把能想到的约束全写进去了,结果 AGENTS.md 膨胀到三千多字。模型反而开始忽略中间部分,遵守率不升反降。后来砍到一千字左右,效果立刻好转。这件事让我明白,约束的价值不在于多,而在于精。

第二个坑是用自然语言写配置。早期我觉得自然语言更灵活,写了一大段“我们希望代码保持整洁,函数不要太长,命名要清晰”之类的话。结果模型的理解完全跑偏,它把“不要太长”理解成了“尽量短”,生成了一堆过度拆分的碎片函数。后来改成“单个函数不超过 50 行”这种可量化的规则,问题才解决。

第三个坑是忘记更新配置。项目从 JavaScript 迁移到 TypeScript 之后,AGENTS.md 里还写着“使用 JSDoc 标注类型”,导致模型一直生成冗余的 JSDoc 注释。这个坑让我养成了把配置更新纳入合并请求检查清单的习惯。

5.3 后续可以继续深挖的方向

这套工作流目前跑得比较稳,但还有几个方向值得继续折腾。一个是配置的自动化校验,写一个脚本检查 AGENTS.md 的格式是否规范、是否有冲突条款、是否引用了不存在的路径。另一个是配置的效果度量,统计模型违反配置的频率,找出最容易出错的条款重点优化。还有一个是跨项目的配置复用,把通用约束抽成模板,新项目直接继承,只写差异部分。

这些方向我都在陆续尝试,有进展了再单独写一篇分享。目前最想说的是,Memory 工程的核心不是技术,而是习惯。养成“遇到问题先想是不是配置缺了”的习惯,比任何工具都重要。工具会换,模型会升级,但把项目知识结构化沉淀下来的思路,是长期有效的。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询