AI编程Memory工程实战:用AGENTS.md构建持久化项目记忆
发布时间:2026/10/16 13:17:19 锦皓数字建站

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 工程的核心不是技术而是习惯。养成“遇到问题先想是不是配置缺了”的习惯比任何工具都重要。工具会换模型会升级但把项目知识结构化沉淀下来的思路是长期有效的。
锦
锦皓数字建站
深耕本土企业品牌数字化升级,专注原创端正雅致商务官网,从视觉设计到稳定运维全程保驾护航。