1. 从无状态到有状态:AI 编程范式转换的底层逻辑
1.1 为什么“无状态模型”正在成为瓶颈
过去两年,大多数人使用 AI 编程的方式非常原始:打开一个对话框,粘贴一段代码,问一个问题,拿到答案,关掉窗口。下一次再打开,模型完全不记得你是谁、你在做什么项目、你的代码风格是什么。这就是典型的无状态交互模式。
这种模式在“问一道算法题”或者“写一个正则表达式”的场景下完全够用。但一旦进入真实项目开发,问题就暴露了。你昨天跟模型讨论过的数据库表结构,今天它完全不记得;你上周让它遵循的命名规范,这周它又按自己的习惯来了;你反复强调的“不要用某个库”,它每次都要再犯一遍。每次对话都像在带一个全新的实习生,而这个实习生还有着极强的自信。
我自己的体感是,当项目超过三个文件、涉及两种以上技术栈的时候,无状态交互的沟通成本会急剧上升。你花在“重复交代背景”上的时间,可能比花在“解决实际问题”上的时间还多。这不是模型能力的问题,而是交互范式的问题。
1.2 有状态编程的核心:让模型“记住”项目上下文
所谓 AI 编程的范式转换,核心就一件事:把模型从无状态的计算器,变成有状态的项目参与者。
有状态意味着什么?意味着模型需要知道:
- 这个项目是做什么的,技术栈是什么
- 代码的组织结构和模块划分是怎样的
- 团队遵循哪些编码规范和约定
- 当前正在处理的任务处于什么阶段
- 之前做过哪些决策,为什么这么做
这些信息如果每次对话都靠人工粘贴,那本质上还是无状态的,只是把成本转嫁给了人。真正的有状态,是这些信息以某种结构化、持久化的方式存在,模型每次启动时自动加载,不需要人反复交代。
这就引出了 Memory 工程的概念。Memory 不是简单的“聊天记录”,而是一套项目知识的持久化与注入机制。它要解决的问题是:如何让 AI 在每次交互时,都能快速获得足够的上下文,从而做出符合项目实际情况的决策。
1.3 范式转换的三个阶段
从我观察到的实践来看,这个转换大致经历三个阶段:
第一阶段:手动粘贴上下文。每次对话前,手动把相关文件内容、规范文档复制到对话框里。优点是简单直接,缺点是上下文长度有限,且完全依赖人的判断——你很难确定自己粘贴的是否完整。
第二阶段:脚本化注入。写一些脚本,自动读取项目中的关键文件,拼接成一段 prompt,再发给模型。这比手动粘贴进了一步,但脚本本身需要维护,而且不同项目的注入逻辑可能完全不同。
第三阶段:声明式配置。这就是 AGENTS.md 这类方案要解决的问题。用一个标准化的配置文件,声明项目需要模型知道的所有信息,模型或工具链自动读取并注入。配置和实现分离,不同项目可以复用同一套工具链,只需要写不同的 AGENTS.md。
目前大多数团队处于第一和第二阶段之间,第三阶段是正在成型的实践。这也是为什么“AGENTS.md 声明式配置”这个概念最近被频繁讨论——它试图为 Memory 工程提供一个标准化、可移植的载体。
2. AGENTS.md 到底是什么:声明式配置的核心设计
2.1 从“命令式”到“声明式”的思维转变
要理解 AGENTS.md 的价值,先要理解命令式和声明式的区别。
命令式是你告诉系统“怎么做”:先读 A 文件,再读 B 文件,然后把 C 和 D 拼接起来,最后发给模型。每一步都要你指定。
声明式是你告诉系统“要什么”:这个项目需要模型知道技术栈、目录结构、编码规范、当前任务。至于怎么读取、怎么拼接、怎么注入,那是工具链的事。
这个转变的意义在于:配置和实现解耦。你写 AGENTS.md 的时候,不需要关心底层是用什么模型、什么工具、什么注入策略。你只需要把项目知识写清楚。换一个工具链,AGENTS.md 还是那个 AGENTS.md。
这就像 Dockerfile 和 docker build 的关系。你写 Dockerfile 声明镜像要包含什么,不需要关心底层怎么分层、怎么缓存。AGENTS.md 试图在 AI 编程领域扮演类似的角色。
2.2 AGENTS.md 的典型结构
虽然没有一个强制标准,但从目前社区实践来看,一个有效的 AGENTS.md 通常包含以下几个部分:
项目概览:一句话说明项目是做什么的,目标用户是谁,核心功能有哪些。这部分帮助模型建立“大局观”,避免它在细节里迷失方向。
技术栈声明:列出项目使用的主要语言、框架、库、工具。特别要标注版本号,因为不同版本之间的 API 差异可能导致模型给出错误建议。
目录结构说明:用树形结构展示关键目录和文件的作用。不需要列出所有文件,但要让模型知道“业务逻辑在哪里”“测试在哪里”“配置文件在哪里”。
编码规范:命名约定、注释风格、错误处理方式、日志规范等。这部分越具体越好,比如“变量名用 camelCase,常量用 UPPER_SNAKE_CASE”比“遵循良好命名规范”有用得多。
当前任务上下文:如果 AGENTS.md 是动态生成的,可以包含当前迭代的目标、正在处理的 issue、已知的坑等。这部分让模型知道“现在该关注什么”。
禁止事项:明确列出模型不应该做的事。比如“不要引入新的第三方依赖”“不要修改数据库 schema”“不要用某个已被废弃的 API”。负面约束往往比正面指导更有效。
2.3 为什么是 Markdown 而不是 JSON/YAML
有人可能会问:为什么用 Markdown 而不是更结构化的 JSON 或 YAML?
我的理解是:Markdown 对模型更友好。大语言模型在训练时见过海量的 Markdown 文档,对标题层级、列表、代码块的理解非常自然。而 JSON 和 YAML 虽然结构清晰,但模型在解析时反而容易出错,尤其是嵌套层级较深的时候。
另外,Markdown 对人也是可读的。开发者可以直接打开 AGENTS.md 阅读和编辑,不需要额外的工具。这降低了维护成本——如果配置文件只有机器能读,那它很快就会过时,因为没人愿意去更新它。
还有一个实际考虑:Markdown 的容错性更强。JSON 少一个逗号就解析失败,YAML 缩进错了就报错。Markdown 即使格式不完美,模型也能理解大意。在快速迭代的项目中,这种容错性很重要。
3. Memory 工程实操:从零搭建一套可用的配置体系
3.1 第一步:梳理项目知识资产
在写 AGENTS.md 之前,先要做一次知识梳理。我通常会让团队回答几个问题:
- 新成员加入项目时,需要知道哪些信息才能开始干活?
- 哪些问题是模型反复问、我们反复答的?
- 哪些规范是“不言自明”但模型不知道的?
- 哪些坑是踩过一次就不想再踩的?
把这些问题的答案列出来,就是 AGENTS.md 的素材。这个过程本身就有价值——它强迫团队把隐性知识显性化。
我自己的做法是建一个docs/agents/目录,里面分几个文件:
overview.md:项目概览和技术栈structure.md:目录结构和模块划分conventions.md:编码规范和约定pitfalls.md:已知的坑和禁止事项current.md:当前迭代的上下文
然后 AGENTS.md 作为一个入口文件,引用或汇总这些内容。这样拆分的好处是,不同部分可以独立更新,不会互相干扰。
3.2 第二步:设计注入策略
有了内容之后,下一个问题是:什么时候、以什么方式把这些内容注入给模型?
常见的策略有三种:
全量注入:每次对话都把整个 AGENTS.md 塞进 system prompt。优点是简单,缺点是浪费 token,而且当项目很大时可能超出上下文窗口。
按需注入:根据当前任务类型,选择性地注入相关部分。比如处理数据库相关任务时,只注入数据模型和 schema 相关的部分。这需要工具链支持任务分类。
分层注入:把信息分为“始终需要”和“按需加载”两层。项目概览、技术栈、核心规范始终注入;具体的模块细节、历史决策按需检索。
我目前用的是分层注入。核心的 AGENTS.md 控制在 2000 token 以内,确保每次都能带上。详细的模块文档放在单独文件里,通过关键词匹配或向量检索按需加载。
这里有个经验:注入的信息不是越多越好。模型和人类一样,信息过载时反而会忽略关键内容。我试过把整个项目的所有文档都塞进去,结果模型的表现反而下降了——它被太多细节干扰,抓不住重点。
3.3 第三步:建立更新机制
AGENTS.md 最大的风险不是写不好,而是写了之后没人更新。项目在演进,规范在变化,但配置文件还是三个月前的版本,模型拿到的就是过时的上下文。
我的做法是把 AGENTS.md 的更新纳入开发流程:
- 每次架构决策后,检查是否需要更新 AGENTS.md
- 每次引入新依赖或废弃旧依赖时,同步更新技术栈声明
- 每个迭代结束时,更新 current.md 中的任务上下文
- 代码 review 时,如果发现模型反复犯同一个错误,考虑是否需要在 pitfalls.md 中补充
还有一个技巧:让模型自己维护 AGENTS.md。当模型发现 AGENTS.md 中的信息与实际代码不符时,让它提出修改建议。这听起来有点循环,但实际用下来效果不错——模型对不一致的地方很敏感。
4. 实战中的坑与排查:Memory 工程的常见问题
4.1 上下文污染:当模型“记错”了
最常见的问题是上下文污染。模型把 AGENTS.md 中的信息和其他来源的信息混淆,导致给出错误的建议。
比如 AGENTS.md 中写了“使用 PostgreSQL”,但模型在某个文件的注释里看到了“MySQL”,就可能建议你用 MySQL 的语法。或者 AGENTS.md 中写了“不要用某个库”,但模型在历史对话中见过这个库的用法,又把它捡回来了。
排查这类问题的思路是:检查信息源的一致性。确保 AGENTS.md 中的声明和代码中的实际情况一致。如果代码中还有旧技术的残留,要么清理掉,要么在 AGENTS.md 中明确说明“某些旧模块仍在使用 X,但新代码一律用 Y”。
另一个技巧是在 AGENTS.md 中使用强断言。不要写“建议使用 PostgreSQL”,而要写“本项目使用 PostgreSQL,禁止使用其他数据库”。模型对强断言的遵循度明显更高。
4.2 信息过时:当配置跟不上代码
第二个常见问题是信息过时。AGENTS.md 中写的目录结构还是三个月前的,新增的模块没有反映进去。模型按照旧结构去找文件,自然找不到。
这个问题没有一劳永逸的解法,只能靠流程来保证。我的做法是:
- 在 CI 中加入检查,如果目录结构变化但 AGENTS.md 未更新,给出警告
- 定期(比如每两周)让模型对比 AGENTS.md 和实际代码,列出不一致的地方
- 把 AGENTS.md 的更新作为代码 review 的一部分
还有一个取巧的办法:让 AGENTS.md 尽量少包含易变信息。目录结构、具体文件列表这些容易过时的内容,可以放在单独的、自动生成的文件里。AGENTS.md 只保留相对稳定的部分,比如技术栈、编码规范、禁止事项。
4.3 注入失败:当模型“看不到”配置
有时候问题不在配置内容,而在注入机制。模型根本没有收到 AGENTS.md 的内容,或者收到的是被截断的版本。
排查这类问题,首先要确认注入是否发生。可以在对话开始时问模型:“你知道这个项目的技术栈是什么吗?”如果它答不上来,说明注入失败了。
常见原因包括:
- 文件路径不对,工具链没找到 AGENTS.md
- 文件太大,被截断或跳过
- 注入位置不对,被其他 system prompt 覆盖
- 编码问题,中文内容乱码
我踩过的一个坑是:AGENTS.md 中用了某些特殊字符,导致解析失败。后来统一用纯文本,避免特殊符号,问题就消失了。
4.4 模型“选择性忽略”:当配置被无视
最让人头疼的情况是:配置明明注入了,模型也“知道”,但就是不用。你写了“用 camelCase”,它还是给你 snake_case。
这种情况通常有几个原因:
配置太长,关键信息被淹没。模型在长文本中的注意力是有限的,如果 AGENTS.md 有 5000 字,其中只有 50 字是关于命名的,模型很可能忽略。
配置之间有冲突。比如前面写了“用 camelCase”,后面某个示例代码里用了 snake_case,模型就困惑了。
配置不够具体。“遵循良好命名规范”这种话等于没说。要具体到“变量用 camelCase,常量用 UPPER_SNAKE_CASE,文件名用 kebab-case”。
模型本身的倾向性。有些模型在训练时见过大量某种风格的代码,会不自觉地偏向那种风格。这时候需要用更强的约束,比如在每次对话开始时重申关键规范。
我的经验是:关键规范要重复。在 AGENTS.md 开头写一遍,在相关任务开始时再提醒一遍。重复虽然看起来冗余,但确实有效。
5. 工具链选型:如何选择适合的 Memory 工程方案
5.1 自建 vs 现成工具
Memory 工程的工具链,目前有两条路:自建和用现成的。
自建的好处是灵活,完全按自己的需求来。坏处是维护成本高,而且容易陷入“造轮子”的陷阱。我见过不少团队花了几周写注入脚本,结果发现现成工具已经覆盖了 80% 的需求。
现成工具的好处是开箱即用,社区维护。坏处是可能不完全匹配自己的流程,需要妥协。
我的建议是:先用现成工具跑通流程,再根据实际痛点决定是否自建。大多数团队的需求其实很标准——读取 AGENTS.md,注入 system prompt,按需加载模块文档。这些现成工具都能做。
5.2 关键选型维度
如果决定用现成工具,我通常会看几个维度:
注入方式:是每次全量注入,还是支持按需加载?是否支持分层注入?
文件格式:是否支持 Markdown?是否支持引用其他文件?
更新机制:是否支持动态更新?是否与版本控制集成?
模型兼容性:是否支持多种模型?切换模型时配置是否需要重写?
调试能力:是否能查看实际注入的内容?是否有日志?
社区活跃度:遇到问题是否能找到帮助?是否有持续更新?
这些维度中,我最看重的是调试能力。Memory 工程的问题往往很隐蔽,如果工具链不提供“查看实际注入内容”的功能,排查起来会非常痛苦。
5.3 一个轻量级的自建方案
如果你决定自建,这里有一个我实际用过的轻量级方案:
核心是一个 Python 脚本,做三件事:
- 读取
AGENTS.md和docs/agents/下的相关文件 - 根据当前任务类型(通过命令行参数或环境变量指定)选择要注入的内容
- 拼接成 system prompt,调用模型 API
脚本的核心逻辑大概是这样:
import os from pathlib import Path def load_agents_config(task_type=None): base = Path("AGENTS.md").read_text(encoding="utf-8") parts = [base] if task_type: task_file = Path(f"docs/agents/{task_type}.md") if task_file.exists(): parts.append(task_file.read_text(encoding="utf-8")) return "\n\n---\n\n".join(parts) def build_system_prompt(task_type=None): config = load_agents_config(task_type) return f"""你是一个项目开发助手。以下是项目上下文: {config} 请基于以上上下文回答问题。如果上下文中有禁止事项,严格遵守。"""这个方案的好处是简单、可控、容易调试。坏处是需要自己维护,而且功能比较基础。适合小团队或个人项目。
6. 效果评估与持续优化:怎么知道 Memory 工程有没有用
6.1 可量化的指标
Memory 工程的效果,不能只靠“感觉”,要有可量化的指标。我通常关注这几个:
重复解释次数:统计一段时间内,你重复交代同一背景信息的次数。如果 Memory 工程有效,这个数字应该显著下降。
首次正确率:模型第一次给出的方案就符合项目规范的比例。这个指标直接反映配置的质量。
返工率:因为模型不了解上下文而导致返工的比例。比如它用了错误的库、违反了命名规范、修改了不该修改的文件。
上下文切换成本:从打开对话到开始有效工作的时间。如果每次都要花五分钟交代背景,说明 Memory 工程没做好。
我自己的记录是:在引入 AGENTS.md 之前,首次正确率大概在 40% 左右;引入并优化之后,提升到了 70% 以上。返工率从 30% 降到了 10% 以下。这个提升是实实在在的。
6.2 定性反馈:模型“像不像”项目成员
除了数字,还有一个定性判断:模型的表现像不像一个了解项目的团队成员。
具体来说:
- 它会不会主动问“这个模块的测试在哪里”而不是随便找个地方写测试?
- 它会不会在修改代码前先确认“这个改动会不会影响其他模块”?
- 它会不会在引入新依赖前先问“项目里有没有类似的库”?
如果模型开始表现出这些行为,说明 Memory 工程起作用了。它不再是一个“外人”,而是一个“参与者”。
6.3 持续优化的方向
Memory 工程不是一次性的工作,而是持续优化的过程。我通常从几个方向入手:
精简配置:定期检查 AGENTS.md,删掉过时的、不重要的内容。配置越精简,模型越容易抓住重点。
补充案例:在配置中加入具体的代码示例。比如“正确的命名方式”配一段示例代码,“错误的做法”也配一段。模型对示例的理解比纯文字描述更准确。
收集反馈:每次模型犯错时,问自己“是不是配置里没写清楚”。如果是,就补充;如果不是,就考虑是不是模型本身的问题。
A/B 测试:如果条件允许,可以对比不同配置方案的效果。比如全量注入 vs 分层注入,详细配置 vs 精简配置。用数据说话,而不是凭感觉。
7. 从 AGENTS.md 到更远的未来:Memory 工程的演进方向
7.1 动态 Memory:从静态配置到实时学习
目前的 AGENTS.md 本质上是静态的——人写好,模型读取。未来的方向是动态 Memory:模型在与项目交互的过程中,自动积累知识,更新 Memory。
比如模型发现某个 API 的用法和文档不一致,它可以自动记录这个发现,下次遇到类似情况时提醒。或者模型发现某个模块的代码风格和规范不符,它可以标记出来,供人决定是否更新规范。
这需要模型具备持续学习的能力,而不是每次对话都从零开始。目前的技术还做不到完全自动,但半自动的方案已经在探索中。
7.2 多 Agent 协作中的 Memory 共享
当项目中有多个 Agent 时(比如一个负责前端,一个负责后端,一个负责测试),Memory 的共享就变得重要。
理想情况下,每个 Agent 有自己的专长 Memory,同时共享项目的公共 Memory。前端 Agent 知道后端的 API 约定,后端 Agent 知道前端的调用方式。这样协作时就不需要反复沟通接口细节。
这需要 Memory 的分层与继承机制。公共 Memory 定义项目级规范,专长 Memory 定义领域级知识。Agent 启动时加载公共 Memory 和自己的专长 Memory。
7.3 Memory 的可移植性:跨项目、跨工具
最后一个方向是 Memory 的可移植性。目前 AGENTS.md 还是项目特定的,换个项目就要重写。但很多知识是跨项目通用的——比如“用 TypeScript 时优先用 interface 而不是 type”这种规范,在多个项目中都适用。
未来的 Memory 工程可能会支持继承和组合。基础 Memory 定义通用规范,项目 Memory 继承基础 Memory 并添加项目特定内容。这样新项目启动时,只需要写增量部分。
另一个维度是跨工具的可移植性。AGENTS.md 不应该绑定到某个特定的工具链。换一个编辑器、换一个模型、换一个平台,AGENTS.md 还是那个 AGENTS.md。这需要社区形成共识,制定标准格式。
我个人在实际操作中的体会是,Memory 工程的价值不在于技术有多复杂,而在于是否真正被用起来。一个简单的、持续更新的 AGENTS.md,比一个复杂的、没人维护的系统有用得多。工具是次要的,习惯是主要的。先养成“把项目知识写下来”的习惯,再考虑用什么工具来管理这些知识。