最近在搞 AI 代理开发的时候,我越来越强烈地感觉到一件事:我们正在从一个“无状态模型”的时代,走向一个“Memory 工程”的时代。以前我们调大模型,基本就是“你问我答”,每次对话都是全新的,模型不记得上一秒说了什么,更别说跨会话的长期记忆。而现在,随着 AI 从单纯的聊天工具变成真正能独立完成任务的代理,记忆已经成了刚需。没有记忆的代理,就像一个得了失忆症的实习生——你说什么他都很认真,但转头就忘,永远在重复问你同一个问题。
我接触到的很多开发者朋友,包括我自己在内,都在这块踩过不少坑。传统的做法是把所有上下文拼在 Prompt 里,但随着任务变复杂,Prompt 越来越长,费用越来越高,模型还经常“忘记”中间的关键信息,输出质量直线下降。后来我深入研究并实践了 Memory 工程,慢慢摸索出一套更靠谱的玩法:把记忆做成显式的、结构化的资源,再用一个声明式的配置文件——比如 AGENTS.md——把代理的行为、记忆、工具都定义清楚。
这就是我想在这篇文章里分享的:从无状态模型的局限,到 Memory 工程的实现思路,再到 AGENTS.md 声明式配置的具体用法。我不准备讲太多玄乎的概念,就按我们实际开发中遇到的痛点和解决方案来聊。希望这篇文章能帮你少走点弯路。
1. 无状态模型的困境:为什么我们需要 Memory
1.1 无状态调用的本质
我们平时调用的绝大多数大语言模型接口,本质上都是无状态的。什么意思?就是你每次发请求,把 Prompt 和输入一起扔给模型,模型返回一个输出,然后这条路就断了。模型自己不会保存任何“这段对话发生过”的信息,下一次你再问它,它又从零开始思考。
很多朋友入门时都这么干过:写一个简单的 Python 脚本,循环调用模型接口,每次都把用户的问题原封不动地传进去。这样做在 demo 阶段没问题,但一旦你开始做真正的产品——比如一个能帮你管理日程、发邮件、查资料的代理——你会发现无状态是一个致命的缺陷。因为代理需要理解连续的任务上下文,它需要知道“用户刚才说了什么”“我上一轮已经做了哪几步”“用户的目标是什么”……这些信息如果每次都要靠用户重复,那这个代理体验起来就是个智障。
打个比方,你让一个刚入职的实习生帮你整理文件,你交代了一句“按日期排序”,他整理到一半,你又让他“顺便把客户 A 的文件也加进来”。一个正常的实习生会记住之前的指令,继续做下去。但一个无状态的模型,他会把你第二次的话当成全新任务——完全不知道“刚才那堆文件”指什么,甚至可能重新开始。这就是无状态模型在代理场景下的核心瓶颈:它没有“能力”去维持一个工作记忆,所以也就无法胜任复杂、长周期的多步任务。
1.2 记忆缺失带来的实际痛点
在实际开发中,无状态模型的痛点特别明显,我总结下来主要有这么几个。
第一,上下文断裂。多轮对话里,你刚问完“帮我找一下上周的报表”,下一句“顺便把里面的数字核对一下”,模型很可能不知道“里面的数字”是什么。因为你没有把上一轮的上下文传进去。于是你被迫在每次请求里塞进一大段历史记录,有的框架会帮你自动拼接,但很容易超长。
第二,费用飙升。大模型的定价是按 Token 算的,你把整个对话历史每次都塞进去,100 轮对话之后,光历史就占了极大比重,费用成倍往上翻。我见过一些团队,为了保持对话连贯,把几万字的聊天记录全部塞进 Prompt,一次请求的费用贵得离谱,换来的却是模型在长上下文里“迷失”——表现反而更差。
第三,无法沉淀知识。你希望代理记住你公司的编码规范、你的个人偏好、某些项目的背景信息。无状态模型每次都需要你把所有资料都带在 Prompt 里,这既不现实,又不灵活。比如你想让代理处理一批文件,文件清单是动态的,总不能每次都在 Prompt 里写死吧。这些都需要一个外部的、持久的记忆系统来解决。
因为这三个痛点,我开始认真研究 Memory 工程。不夸张地说,Memory 就是连接“无状态大模型”和“有状态代理”之间最重要的一块拼图。
2. Memory 工程:给 AI 装上持久化的“大容量外脑”
2.1 记忆的层次与存储设计
Memory 工程,简单来说,就是把代理的上下文和背景知识,从“模型内部的一次性计算”变成“外部的持久化数据”。这样模型每次调用时,可以根据需要拉取相关的记忆片段,而不是把整个历史全塞进去。这就像从一个装满杂物的房间,变成一个有索引、有分区的图库一样,存取都高效得多。
在实际设计里,我个人把记忆拆成三个层次。
- 短期记忆:当前会话内的上下文,通常在会话结束后就过期。可以用内存或 Redis 这类缓存来存,重点是低延迟。
- 长期记忆:跨会话的关键信息,比如用户偏好、历史决策、项目状态等。通常存在数据库里,可能用结构化表或者向量索引。这一层是 Memory 工程的核心。
- 工作记忆:某个特定任务过程中的中间状态,比如任务进行到哪一步了、还有哪些待办事项。这种记忆不一定需要永久保存,但它至关重要,因为代理如果失去工作记忆,多步任务的每一步都会变成“无头苍蝇”。
存储方案的选择上,我一开始用过简单的 JSON 文件把记忆序列化起来,方便是方便,但没法检索。后来发现,如果要按语义检索记忆,必须用向量数据库,先把文本向量化,然后通过相似度搜索找到相关内容。常见的技术栈组合是:用 SQLite 或 PostgreSQL 存结构化记忆,用向量数据库存语义记忆,再配合一个 Embedding 模型把文本转成向量。
这一套下来,代理的“记忆力”就不是简单的缓存了,而是一个完整的知识系统。我举个例子,你在做一个用户画像的代理,短期记忆记住本次会话刚聊了什么,长期记忆里存了用户上个月买过什么商品、喜欢什么风格,工作记忆保存了“正在准备推送推荐列表”的状态。有了这些,代理做推荐的时候,只需要从长期记忆提取用户偏好,从短期记忆提取当下的谈话语境,再从工作记忆判断当前任务进度,三者一拼,输出的结果自然就像“真的懂你”一样。
2.2 记忆写入与检索的取舍
Memory 工程不仅仅是“存起来”就完事,关键还在于“怎么写入”和“怎么读出”。写入不当,轻则记忆冗余,重则污染长期记忆;读取不当,则可能把不相关的记忆杂糅进上下文,导致模型判断失误。
写入方面,我会刻意控制“记什么”。不是所有的对话细节都值得长期记。我在实践中定了一条规则:几乎不记录词级的内容,只记录“结构化的事件”。比如用户说“我讨厌吃香菜”,我会把这条信息转成preference: dislike_cilantro存入长期记忆。这种结构化的记忆,既减少了存储体积,又提高了后续检索的命中率。
读出方面,最常见的做法是“检索增强生成”。代理在每次要接管一个任务时,先根据当前的用户请求和对话状态,对记忆库做一次语义检索,取出最相关的 top-k 条记忆,然后把这些记忆作为额外的上下文,加进 Prompt 里。这里有几个参数值得琢磨,一个是 top-k 的取值,我通常取 3 到 5 条,太多会冲淡核心指令;另一个是相似度阈值,低于阈值的结果宁可不取,免得引入无关信息。还有一个是我自己总结的经验——记忆检索结果和当前任务之间最好做一个“一致性过滤”。比如用户之前说喜欢极简风格,但这次问的是商务办公风格,那检索到的极简风格记忆也许不应该被带进上下文。这个过滤可以很轻量地靠规则实现,并不一定要用复杂的模型。
3. AGENTS.md:声明式配置的入口
3.1 为什么是声明式配置
在 Memory 工程的基础上,我们还需要一种有效的方法来“描述”代理的行为和记忆。这个时候,AGENTS.md 这样的声明式配置文件就走进了视野。
传统写代码的方式是命令式——你告诉程序“先做 A,再做 B,如果 C 就做 D”。但在 AI 代理这个领域,事情不一样了:模型的行为不是严格线性可控的,它是由模型推理驱动的。你很难用传统代码把每一步都写死。声明式配置的核心思想是:你描述“代理是什么、能做什么、有什么记忆、用什么工具”,而不是描述“每一步怎么做”。模型自己根据这些声明,结合上下文去规划执行路径。
打个比方,命令式像是在给实习生一页一页地翻流程图:“看到客户 A 就执行步骤 1,看到客户 B 就执行步骤 2。”而声明式配置更像是给这个实习生一本岗位手册:“你是销售助理,你的客户有 A 和 B 两类,你手上有报价单工具,你的任务是处理好报价请求。”剩下的具体路径,实习生的“聪明脑袋”自己会规划。
用 AGENTS.md 这样的配置文件,有三大明显好处:一是配置和代码分离,改代理行为不用碰核心代码,改一个配置文件即可;二是可版本控制,配置文件跟代码一样放进 Git 仓库,变更可追溯,团队协作时能 review;三是可移植性,不同项目之间可以共用一套代理配置模板,快速部署出行为一致的代理。这比在代码里用 if-else 硬写代理逻辑要优雅得多。
3.2 AGENTS.md 的核心字段与配置写法
AGENTS.md 并不是一个官方的、强制统一的标准,但它逐渐成为社区里一种常见的约定。本质上它是一个 Markdown 文件,但它内部的格式是有结构的。我一般会把它写成类似一个“代理简历”的东西,用简单清晰的字段来描述代理的各项能力。
下面是我常用的一份 AGENTS.md 模板,可以说是我踩坑之后总结出来的精华。
--- name: "客户支持代理" description: "负责处理客户咨询,并根据工单状态提供技术建议" role: "support_agent" memory: enabled: true scope: ["customer_name", "order_status", "last_interaction"] storage: "vector_db" retrieval: top_k: 3 threshold: 0.75 tools: - name: "search_orders" description: "查询客户订单状态" required_permission: "read" - name: "update_ticket" description: "更新工单状态" required_permission: "write" permissions: - "read_order" - "update_ticket" - "send_email" execution_policy: max_steps: 5 fallback: "ask_human" budget: max_tokens_per_request: 8000 max_calls_per_task: 20 ---这些字段不是随便拍的,我一个个解释。
name和description:这是给模型和人类看的自我介绍,也是很多元框架用来识别代理身份的关键字段。role:定义代理的“职业身份”,模型会在生成回复时,隐性地参考这个角色来调整语气和策略。memory:定义这是否启用记忆,以及记忆的存储和检索方式。scope告诉代理哪些信息是可以感知的记忆字段,retrieval定义每次检索的数量和阈值。tools:声明代理可以调用哪些外部函数或服务。这里我会特别注意required_permission,因为工具的安全边界是代理开发里最容易出问题的地方。permissions:定义代理具体能做什么,不能做什么。这个字段直接和记忆读写、工具调用挂钩,是安全审计的核心。execution_policy:规定代理在任务执行中最多走几步,如果超出步数或失败,就回调给人类处理。budget:做资源控制,避免代理失控,无限循环调用,花光你的 Token 预算。
我在实际使用中发现,很多开发者会忽略execution_policy和budget,结果就是代理陷入死循环,疯狂调用工具,账单一晚上飙高。所以这两个字段我强烈建议必填。
4. 实操:构建一个带记忆的 AI 代理
4.1 从零搭建项目骨架
光说不练假把式。下面我们动手,把一个带记忆、带工具、完全由 AGENTS.md 配置驱动的代理搭建出来。我假设你已经准备好了 Python 环境,且有一个可调用的大模型接口(无论什么平台,接口格式大同小异,流程一致)。
项目结构很简单,三个主要部分:agents/目录用来放配置文件和代理逻辑,memory/目录用来存放记忆相关的存储模块,main.py作为代理入口。
my_agent/ ├── agents/ │ ├── config.md # AGENTS.md 的配置内容 │ └── agent.py # 核心代理逻辑,读取配置并执行 ├── memory/ │ ├── store.py # 记忆存储层 │ └── retriever.py # 记忆检索层 └── main.py # 命令行入口这里有个小细节:AGENTS.md 是一个统一命名的文件,我把它命名为config.md只是为了让它在项目里更通用一点,但如果你要遵循社区约定,就叫AGENTS.md也行。代理启动时会先从AGENTS.md读取配置,然后初始化 Memory 和 Tools。
4.2 编写 AGENTS.md 声明文件
按我们上面说的模板,我给这个示例代理写一份真实的声明文件。它要做一个“个人日程管理代理”,能帮用户创建日程、查看日程、并根据最近的日程安排给出提醒。
--- name: "日程助手" description: "一个能够管理用户日程、创建提醒、安排会议的 AI 代理" role: "personal assistant" memory: enabled: true scope: - "user_preferences" - "upcoming_events" - "past_tasks" storage: "sqlite+vector" retrieval: top_k: 5 threshold: 0.7 tools: - name: "create_event" description: "创建一个新的日程事件,包含标题、时间、地点" required_permission: "write_calendar" - name: "list_events" description: "查询指定日期范围的所有日程" required_permission: "read_calendar" permissions: - "write_calendar" - "read_calendar" - "send_notification" execution_policy: max_steps: 8 fallback: "ask_user" budget: max_tokens_per_request: 6000 max_calls_per_task: 15 ---这份声明里,我把memory.scope列成了三个字段:用户偏好、未来事件、已完成任务。这告诉代理,在回答问题时,可以从这三个维度去检索记忆,而不是一锅乱炖。在tools部分,我只给了两个工具:创建事件和列出事件。注意我没有开放“删除事件”的权限,因为从安全角度考虑,先不提供破坏性操作,等以后需求明确了再加。
这里要特别讲讲permissions和tools的关系。permissions是一个代理层面的总控,tools里每个工具还会单独声明required_permission。我在跑起来之后发现,这个双层设计能非常有效地防止代理越权调用工具。假设你给代理加了send_email工具,但代理的permissions里没有send_email,那这个工具实际上是不可用的。这种“默认拒绝”的设计思路,在代理开发里非常重要。
4.3 接入 Memory 存储与检索
写好了配置,接下来就是写入记忆。我用一个简单的MemoryStore类来管理记忆,核心就是两个方法:写记忆和读记忆。为了保持文章聚焦,我不会贴完整代码,但会把核心逻辑讲清楚。
写记忆时,我会把从对话中抽取出来的结构信息,存成一条条带元数据的记录。比如用户说“周五下午开会”,我会解析出动作类型create_event、话题meeting、时间Friday 15:00,然后存进upcoming_events这个 scope 里。我会专门给每条记忆加上时间戳和来源会话 ID,方便以后做过期清理。
读记忆时,我会调向量检索,再加上规则过滤。比如用户问“我今天有什么安排”,我先从upcoming_events里查今天的事件,再从user_preferences里查用户偏好的时间格式,最后把这两部分记忆作为上下文,一起拼进 Prompt。这个过程在retriever.py里实现,核心就是连接向量数据库,执行相似度查询。
有一个特别容易被忽略的细节——记忆的去重。如果用户多次说“我不喜欢开会”,模型存了三条一模一样的记忆,你检索的时候会同时命中,结果就是在 Prompt 里塞了三遍“不喜欢开会”,既浪费 Token,又容易干扰模型判断。所以我的MemoryStore在写入时会做一次相似度检查,如果文本之间的相似度超过 0.9,就会把旧的记录合并或者直接跳过,只在原记录上更新时间戳。
全部接好之后,运行main.py,代理就能开始工作了。你会看到这样的现象:连续问它“我周五有什么会”之后,再问“我适合什么时间开会”,它能结合记忆里“周五下午已满”和“用户偏好上午开会”的状态,给出一个合理的建议。这就是 Memory 工程带来的质的改变。
5. 常见问题与排查实录
5.1 配置不生效的检查清单
AGENTS.md 看着简单,但我一开始用的时候也经常找不到配置“没起作用”的原因。后来我总结了一套排查清单,基本能覆盖大部分问题。
- 文件命名和路径对不对。有些框架只认项目根目录下的
AGENTS.md,你放在子目录里它就读不到了。先确认路径。 - 有没有分隔线。
---前后两边的内容,才是 YAML 格式的配置,如果你为了好看在开头加了 BOM 或者多余的空格,YAML 解析可能会直接失败。 - 记忆开关是不是开了。我见过不少朋友配置里写了
score字段,但实际上应该用scope,写错一个字母,记忆就静默失效了。 - 检查
top_k和threshold。如果top_k=0,那肯定一条记忆都检索不出来。如果threshold太高,比如 0.95,那大概率也是空手而归。对大部分语义检索场景,0.7 到 0.8 是一个比较合理的区间。
另外,如果你在改配置后感觉代理行为和以前一样,先别怀疑缓存,大概率是配置里某个字段名字拼错了。用 YAML 解析器把内容读出来打一行日志,一目了然。
5.2 记忆污染与上下文冲突
记忆也并不是越多越好,存得不对反而会成为负担。我踩过的最深的一个坑,就是“记忆污染”。
什么叫记忆污染?就是你的长期记忆里存下了过时的、甚至错误的指令,结果代理每次都被这些脏记忆带偏。比如有一次我把一个临时任务的规则写进了长期记忆,结果之后一周,代理做任何事都先执行那个过期规则,导致输出完全不可用。后来我学乖了,给所有记忆记录都加了valid_until字段,给临时任务设置一个有效期,过期就自动删除。
还有一个常见的冲突场景是:用户明确表达了 A 倾向,但记忆库里有一条旧的“用户喜欢 B”的记录。检索时这两条记忆同时命中,模型就懵了,不知道到底该听哪个。我的解决办法是给记忆记录加“置信度”字段,对最近更新的记忆提高置信度,检索时优先采用置信度高的记忆。如果置信度相当,就同时给模型展示两条记忆,并在 Prompt 里告诉模型“这两条记忆存在冲突,请以最近一次交互为准”,效果立竿见影。
5.3 性能与成本权衡
Memory 工程的最大敌人就是延迟和成本。你每次调用都去向量数据库查一次,势必要多几十到几百毫秒的延时。如果你在代理里定义了多个工具,工具调用本身也会增加往返时间。我实测下来,一个带记忆和工具的代理,单次完整任务的平均耗时比无状态调用要高出不少,但换来的是极高的准确率和任务完成率,这笔交易是值得的。
为了控制成本,我通常会做两件事:第一,把一些常用的记忆直接缓存到本地,比如用户的固定偏好,不用每次去向量库查,直接当作系统提示词的一部分;第二,给记忆检索结果设置一个“最大 token 预算”,比如 500 token,超过就截断或者丢弃低相关的部分。记住,模型不是信息越多越聪明,聚焦才是王道。
很多团队在刚开始做 Memory 工程时,容易陷入“什么都想记”的误区。我自己也犯过,结果就是记忆库膨胀得飞快,检索效果反而变差。最后的经验是:优先记“结构化的事件”和“明确的偏好”,空泛的聊天内容一律不记。记下来的东西,要定期清理和合并,让记忆库保持“小而精”,这比堆砌一堆没用的信息有用得多。
结尾
从无状态模型到 Memory 工程,再到 AGENTS.md 声明式配置,这中间的变化,本质上是我们在把 AI 从一个“没有记忆的工具”变成一个“有记忆的员工”。说实话,刚开始我接触 Memory 工程时,觉得不就是加个数据库嘛,没什么了不起。但真正动手做出来之后,才发现这里面每一个环节都藏有值得琢磨的细节:怎么设计记忆的 scope,怎么定检索的阈值,怎么防止记忆污染……每一个决策都会直接影响代理的成品质量。
我个人在实际操作中最大的体会是,不要一上来就追求“全知全能”的代理,而是先从一个小任务开始,把记忆和配置的闭环跑通,再一点点扩大范围。比如先做一个只读日程的代理,再加删除权限,再加跨工具的协作。每加一个新能力,就更新一次 AGENTS.md,观察记忆系统是否还稳定。这种渐进式的开发方式,能让你在早期就把记忆污染、配置失效这些顽疾扼杀在摇篮里,不至于到最后推倒重来。
另外一个很实用的小技巧:给代理写一份“自检配置”,让代理启动时先跑一个简单的记忆检索自检。你可以写一个命令,让代理调用list_events工具,再检索一下“用户偏好”,如果两个模块都返回正常数据,就说明 Memory 和 AGENTS.md 配置链路是通的。如果其中一环报错,代理可以直接输出错误日志,省得你到处排查。这个自检动作,我在很多项目里都在用,省了不少调试时间。