很多刚开始玩大模型 Agent 的朋友,第一次做出来的机器人像个金鱼,聊完上一句就忘。这不是模型笨,而是我一开始也没搞懂 Agent 记忆技术到底在解决什么问题。这篇指南按照我自己从零到能跑通的路线整理,面向小白程序员,逻辑讲透,代码可以直接抄。我会从原理、分层、代码、参数、踩坑五个角度,把“大模型 Agent 怎么记住东西”这件事拆开讲清楚。你不需要一开始就懂向量数据库,也不需要背一堆新名词,跟着顺序看下来,基本就能在自己的项目里给 Agent 装上“记忆”。
适合谁读?刚接触大模型应用开发、想让自己的聊天机器人具备跨会话记忆、做私人知识助手、或者想在团队里搭一个带记忆的客服问答系统的人。就算你只写过一点 Python,也一样能跟上。
1. 先搞清楚 Agent 为什么需要记忆
1.1 大模型生来就是“无状态”的
大模型本质上是一个函数:给它一段文字,它返回一段文字。它不会在上一次调用结束后保存任何东西。你发两条消息,它不会自动记得第一条聊了什么,除非你把第一条的内容重新塞进第二条的输入里。
这一点和人的对话体验差别巨大。人聊天时会自然记住“刚才聊到了什么”“对方叫什么名字”“他上次说过喜欢简洁”。但模型没有这个概念。所谓“模型有记忆”,实际上是在模型外面加了存储和检索,再把检索到的内容放回输入里。
我刚开始做 Agent 时犯过一个经典错误:以为把历史消息存进数据库,Agent 就有记忆了。结果发现存了等于没存,因为模型根本不会自己去查数据库。它只认当前请求里的内容。所以你真正要做的事情只有一件:在合适的时机,把合适的记忆内容,变成当前请求的一部分。
1.2 记忆的本质是上下文管理,不是单纯存储
很多人把“记忆系统”理解成“聊天记录存储系统”,这是误区。存储只是最底层,记忆系统的核心是上下文管理。
上下文窗口就像桌面,记忆库像档案柜。档案柜再满也没关系,但桌面只有那么大面积。你不能把所有档案都摊在桌面上,不然真正有用的东西反而找不到。Agent 记忆要做的工作是:判断哪些档案现在有用、把哪几份抽出来放到桌面上、用完后再收回去。
换句话说,记忆系统不是“存得越多越好”,而是“放得越准越好”。精确地决定“放什么进 Prompt”,比“存了多少条历史”重要得多。
1.3 这篇指南解决三类问题
一类是“记不住”:用户昨天告诉你他喜欢简短回复,今天你又说了一大篇。这类问题源于跨会话信息丢失。
另一类是“记太乱”:历史记录、用户偏好、临时状态全混在一起,塞进 Prompt 后模型被旧信息带偏,回答前后矛盾。
还有一类是“记不起”:记忆明明存了,但检索的时候找不准。比如用户问“上次那个报错解决了吗”,系统搜不到,因为当时的记录里根本没有“报错”这个词。
后面所有内容,基本都在围绕“怎么记住、怎么存、怎么取、怎么用、怎么忘”这五件事展开。
2. 记忆体系的四层拆分
2.1 第一层:会话工作记忆
第一层是“正在聊的这段对话”,也就是当前 prompt 里的消息列表。它由模型自带的上下文窗口承载,需要我们自己管理长度和裁剪策略。
典型问题:用户连续聊二十分钟后,历史太长,超过窗口上限。简单粗暴的做法是“只保留最近 N 条”,但这样会导致更早的关键信息丢失。稍好一点的做法是把早期内容压缩成摘要,再把摘要放在消息列表最前面。
这一层的管理频率非常高,几乎是每次请求都要做。所以它必须足够轻,不能依赖外部数据库查询,尽量在内存里完成。
2.2 第二层:情景记忆
情景记忆存的是“发生过的事件”。比如“用户上午反馈了支付失败”“上次对话里用户要求换个方案”“上周已经发送过一封确认邮件”。这类记忆的特点是:有时间、有事件、有结果,通常按发生顺序检索。
情景记忆解决的是连续性。没有它,用户下次打开对话时,Agent 完全不知道你们之前发生过什么。有了它,Agent 可以说出“上次你提到支付失败,我查了一下,可能是银行接口超时”。
实现时一般会给每条记忆打上时间戳、事件类型、关联对象,这样既能按时间排序,也能按事件类型过滤。
2.3 第三层:语义记忆
语义记忆存的是“事实和偏好”,不依赖某个具体事件。比如“用户喜欢简洁回复”“项目部署在测试环境”“用户所在行业是教育”。这些内容即使不提时间,也长期有效。
语义记忆通常适合用向量检索。因为用户问法千变万化:“我上次说过的偏好你还记得吗”“回复别那么啰嗦”,和“用户喜欢简短回答”这句话在字面上差别很大,但语义距离很近。向量检索能抓这种近似关系。
语义记忆要特别注意覆盖和去重。同一条偏好可能被写进去三次,如果不去重,检索时会同时冒出三条重复记忆,既占空间又干扰模型。
2.4 第四层:程序性记忆
程序性记忆是“怎么做某件事”的经验,比如“遇到数学计算先调用计算工具,别自己心算”“生成周报时按数据、结论、行动计划三段来写”。
在小项目里,这部分通常被写死在代码逻辑或系统提示词里。但做得复杂的 Agent 会把它沉淀为可检索的技能模板。比如一个 Agent 遇到 Excel 处理任务时,会自动调出“读取表格-清洗-分析-出报告”的流程模板。
程序性记忆的本质是知识复用。它能让 Agent 不去重复摸索流程,而是直接使用已经验证过的方法。
四层记忆可以共存。我的经验是:先把会话工作记忆和语义记忆做扎实,再考虑情景和程序性记忆。因为前两个直接决定用户体验,后两个更多是锦上添花。
| 记忆类型 | 典型内容 | 存储方式 | 主要用途 |
|---|---|---|---|
| 会话工作记忆 | 当前对话上下文 | 内存消息列表 | 保证当前对话连贯 |
| 情景记忆 | 某时某刻发生的事件 | 数据库记录+时间索引 | 跨会话回溯 |
| 语义记忆 | 用户偏好、事实知识 | 向量库+规则过滤 | 回答个性化问题 |
| 程序性记忆 | 任务流程、技能模板 | 配置/模板库 | 自动决定执行路径 |
3. 手把手实现一套可用的记忆组件
3.1 最小版:会话窗口管理
先写一个最简单的会话窗口。作用是控制消息列表长度,超出的部分挤压成摘要,而不是直接扔掉。
import json from collections import deque class SessionWindow: def __init__(self, max_messages=20): self.max_messages = max_messages self.messages = deque(maxlen=max_messages) def add(self, role, content): self.messages.append({"role": role, "content": content}) def to_prompt(self): return list(self.messages) def compress_old(self): # 保留最近一半消息,把更早的文本拼成一条摘要占位 keep_count = self.max_messages // 2 old = list(self.messages)[:-keep_count] recent = list(self.messages)[-keep_count:] summary_text = "之前对话摘要:" + ";".join(m["content"] for m in old if m["content"]) self.messages.clear() self.messages.append({"role": "system", "content": summary_text}) for m in recent: self.messages.append(m)这里用deque(maxlen=...)可以自动丢弃最旧的。但只丢弃不总结,会丢关键信息。compress_old就是把早期内容压成一条摘要,再把摘要放到 system 里。
注意:摘要不能太频繁做。每轮都压缩等于把每次回答都再调用一次模型做总结,成本和延迟都会上去。建议当消息条数达到上限时才触发。
3.2 升级版:向量检索长期记忆
当需要跨会话记住用户偏好和事件时,纯会话窗口不够。我们需要一个“写进去-查出来”的机制。最通用方案是向量检索:把文本变成向量,用余弦相似度找最接近的内容。
下面用一个纯 Python 标准库演示核心逻辑。生产环境一般会用现成的向量库或模型服务提供的 embedding,原理一样。
import hashlib import json def tokenize(text): # 简单按空格和标点切分,真实场景可以用分词库 return [t.lower() for t in text.replace(",", " ").replace("。", " ").split()] def hashed_embedding(text, dim=128): vec = [0.0] * dim for token in tokenize(text): digest = hashlib.md5(token.encode()).hexdigest()[:8] index = int(digest, 16) % dim vec[index] += 1.0 return vec def cosine_similarity(a, b): dot = sum(x * y for x, y in zip(a, b)) norm_a = sum(x * x for x in a) ** 0.5 norm_b = sum(x * x for x in b) ** 0.5 if norm_a == 0 or norm_b == 0: return 0.0 return dot / (norm_a * norm_b) class VectorMemory: def __init__(self, path="memory.json"): self.path = path self.items = self._load() def _load(self): try: with open(self.path, "r", encoding="utf-8") as f: return json.load(f) except FileNotFoundError: return [] def _save(self): with open(self.path, "w", encoding="utf-8") as f: json.dump(self.items, f, ensure_ascii=False, indent=2) def add(self, text, category="general"): embedding = hashed_embedding(text) self.items.append({ "text": text, "category": category, "embedding": embedding, "access_count": 0 }) self._save() def search(self, query, top_k=3): query_vec = hashed_embedding(query) scored = [] for item in self.items: score = cosine_similarity(query_vec, item["embedding"]) scored.append((score, item)) scored.sort(key=lambda x: x[0], reverse=True) return scored[:top_k]这个写法可以跑通,但有个明显缺陷:哈希词袋向量对“语义相似”不敏感,比如“喜欢简短”和“别啰嗦”几乎匹配不到一起。真实项目里应该把hashed_embedding替换成 embedding 模型生成的向量。替换后其他逻辑不用变。
搜索后,把返回结果按指定格式拼进 Prompt 就行:
memory = VectorMemory() results = memory.search("用户喜欢什么风格") memory_section = "\n".join(f"- [{r[1]['category']}] {r[1]['text']}" for r in results) prompt = f"以下是长期记忆:\n{memory_section}\n\n请基于记忆,回答当前问题。"每次命中时最好更新access_count,后面做遗忘策略会用到。
3.3 进阶版:写入与遗忘策略
长期记忆最怕两件事:什么垃圾都存、存了永远不删。所以要加两道流程:写入前过滤,定期遗忘。
写入前过滤举例:
def should_store(text, min_length=10): # 太短的内容没有存储价值 if len(text) < min_length: return False # 一些临时寒暄不值得记 useless = ["你好", "哈哈", "再见", "不知道"] if any(u in text for u in useless): return False return True遗忘策略可以用“低频且长期未访问优先清理”的思路:
import time def clean_memory(memory, max_items=200, max_age_days=30): now = time.time() kept = [] for item in memory.items: last_access = item.get("last_access_ts", now) age_days = (now - last_access) / 86400 # 访问次数低且超过30天未用,准备淘汰 if age_days > max_age_days and item.get("access_count", 0) < 3: continue kept.append(item) memory.items = kept memory._save()这只是示例,生产里更常用加权分数:综合“最近访问时间”“访问次数”“内容重要性”算一个衰减分,低于阈值就删除。
操作心得:遗忘一定要做成定时任务,而不是每轮对话都扫一遍。原因很简单,记忆表小的时候怎么扫都无所谓,但数据到几万条后每次扫描都是性能开销。
4. 记忆链路的四个关键步骤与参数选择
4.1 写入:判断什么值得记
记忆写入最容易犯的错是“全量写入”。把每轮对话都原样存进去,短期内没感觉,时间一长,向量库塞满了“今天天气不错”“好的”“嗯嗯”这类无意义内容,检索质量直线下降。
我自己的筛选标准有三条:
- 包含事实:比如用户所在行业、项目名称、联系方式、产品版本。
- 包含偏好:比如“回复要简短”“不喜欢 emoji”“报告要带数据表”。
- 包含状态变化:比如“支付失败已解决”“计划从下周二开始实施”。
凡是用户明确表达、重复提到、并且后续可能再次使用的信息,才写入。相反,寒暄、临时情绪、一次性的流程确认,通常不写。
写入时还要做归一化。比如用户说“以后叫我老王就行”和“请称呼我为老王”,本质是同一件事,至少要把称呼统一成“用户希望被称呼为老王”。不做归一化,检索时会同时命中两条类似内容,白白浪费上下文空间。
4.2 检索:如何让“相关”变成“精准”
检索是记忆系统里最影响体验的环节。相关不代表有用。用户问“昨天那个问题解决了吗”,检索出十条“昨天聊过的东西”,模型反而不知道具体是哪件事。
通用做法是“先过滤,再排序”。先用元数据缩小范围,比如时间范围、对话场景、记忆分类;然后做向量检索,按相似度排序;最后按分数阈值截断。
相关参数我建议这样设置:
top_k:从 3 开始调。太少容易漏,太多容易杂。min_score:低于 0.2 的记忆不要放进上下文。这个值需要配合你用的 embedding 模型来定,不同模型分数分布差很多。recency_bonus:给近期内容加一点权重。例如最终得分 = 相似度 * 0.8 + 时间新鲜度 * 0.2。
调参时建议每次只改一个变量,别同时动 top_k 和阈值,不然你不知道是哪个改动带来的效果。
4.3 融合:检索结果如何安全放进 Prompt
检索结果进了 Prompt,不代表模型会正确使用。你需要告诉模型“哪些是记忆,哪些是当前对话”。
我常用的格式:
【长期记忆】 - 用户偏好:喜欢简短回复 - 事实:当前项目使用测试环境 【当前对话】 用户:昨天那个报错后来怎么解决的?这样模型可以清楚区分信息来源,不容易把旧记忆当成新输入。如果记忆内容和当前对话冲突,比如记忆说“用户喜欢详细报告”,但当前用户说“别废话直接给结论”,我会在 Prompt 里加一句“当长期记忆与当前对话矛盾时,以当前对话为准”。
融合阶段还要控制体量。我给自己定过一个预算:记忆内容不超过整个 Prompt 的 30%。超过这个比例,模型会迷失重点,回答又臭又长。
4.4 遗忘:记忆系统必须会做减法
遗忘和写入同样重要。没有遗忘,记忆库就是垃圾堆。旧信息会干扰新判断,甚至让 Agent 用过时信息回答当前问题。
我项目里至少维护三种遗忘机制:
- 过期删除:比如“临时任务”类记忆 24 小时后失效。
- 低频删除:超过 30 天没被检索到的记忆,权重下调,最终清理。
- 冲突覆盖:当新记忆与旧记忆语义高度相似且信息更新时,删除旧记录。
遗忘是记忆系统和日志系统的分界线。日志是”全都要存,永不删除“,记忆是”只保留对后续决策有用的信息“。很多人把这两者混在一起,最后系统既不像日志,也不像记忆。
5. 落地实践:五个经典问题
5.1 检索不到,用户觉得你“失忆”了
现象是:用户明确告诉你“我上次说过”,Agent 却说“抱歉,我没有找到相关信息”。
先检查三步:写入有没有成功、检索条件是不是太严、文本有没有分词或编码问题。我踩过一个坑是 embedding 用的是旧模型,新对话里的词分布变化太大,导致相似度普遍偏低。换新 embedding 模型后问题消失。
如果检索确实没问题,但用户换了说法,可以加一层“关键词+向量混合检索”。关键词负责精确匹配,向量负责语义近似,两边各自返回一批候选,最后合并排序。这样能覆盖“字面不同但意思相近”的情况。
5.2 检索出一堆,但全用不上
现象是:明明返回了记忆,模型还是答非所问。原因通常是 top_k 太大,或者检索时没有限定记忆类型。
比如用户问的是项目状态,结果系统把用户闲聊偏好、历史情绪、任务记录全检索出来了。正确做法是按“场景 + 分类”过滤。查询前先识别意图:这是任务咨询,那就只检索“任务类”记忆;这是闲聊,那就只检索“偏好类”记忆。过滤后再做向量排序,准确率和效率都会好很多。
5.3 上下文越来越长,费用吃不住
随着记忆累积,每次请求都会变得越来越长。如果不对上下文做限制,Token 费用会线性上涨。
我的做法是三层存储:
- 热记忆:最近 1 小时内的会话,直接放内存。
- 温记忆:跨天但频繁使用的内容,放向量库,按需检索。
- 冷记忆:一个月前的内容,压缩成摘要后归档。
每次请求前先算一次总 Token。如果超过窗口预算,就降低“温记忆”的召回条数。比如正常取 5 条,超预算时就只取 3 条。优先保当前对话,长期记忆永远让路。
5.4 记忆被污染,越修越错
这是最隐蔽的问题。“污染”指错误的记忆被写入后,模型每次都会基于错误信息回答,用户纠正完后,旧的错误还在,又会被检索出来。
避免污染的关键是更新机制。当出现一条新记忆与旧记忆明显矛盾时,不应“并列存储”,而应该做覆盖。比如旧记忆是“项目部署在测试环境”,用户今天说“项目已经上线生产了”,这时直接删除旧条目或标记为过期,只保留新条目。
另外,必须给用户提供“删除记忆”的能力。哪怕只是提供一个简单的“清除我的所有历史记忆”按钮,也能兜底解决大部分污染问题。
5.5 安全与隐私怎么处理
记忆里往往包含很多个人信息和业务敏感信息。至少要遵守这几条:
- 不记录密码、密钥、Token 等敏感字段。
- 写入前先做脱敏,手机号、身份证号、银行卡号用掩码替换。
- 记忆数据按用户隔离,不能在多用户间串数据。
- 明确告知用户我们存储了哪些信息,并支持一键清除。
合规这块不能等项目上线才补。从第一版设计记忆组件开始,就要把数据隔离和删除机制考虑进去。否则后期改造成本会很高。
| 症状 | 可能原因 | 排查顺序 | 快速对策 |
|---|---|---|---|
| 完全检索不到 | 写入失败或阈值过高 | 先查写入日志,再查检索分数 | 放宽 min_score,启用混合检索 |
| 检索结果不相关 | top_k 过大、未按场景过滤 | 看返回条目的类型分布 | 先按记忆分类过滤,再向量排序 |
| 上下文超预算 | 记忆未分级、没有限制条数 | 统计每次请求 Token 消耗 | 三层存储+限制温记忆召回 |
| 记忆越用越错 | 冲突信息未覆盖、污染写入 | 检查是否有重复矛盾记录 | 增加覆盖机制,提供用户删除入口 |
| 数据安全风险 | 敏感字段明文入库 | 检查入库前是否有脱敏逻辑 | 脱敏+隔离+可删除 |
6. 进阶方向与选型建议
6.1 从向量检索迈向知识图谱
向量检索擅长回答“语义相近”,但不擅长回答“多跳关系”。比如“A 用户负责的 B 项目里,有哪些模块是用 C 技术实现的?”这种问题,如果只靠向量匹配,很难一次命中。知识图谱则可以把实体和关系串起来,支持精确的关系查询。
但知识图谱的构建成本高,需要维护实体、关系、属性和更新逻辑。我的建议是:先跑通向量记忆,等确实遇到“关系推理”类需求再引入图结构。小项目上来就做图谱,大概率会被建图工作量拖垮。
6.2 记忆压缩的两种路线
一是摘要压缩。把长段对话交给模型生成一段摘要,放入长期记忆。优点是保存的信息密度高,缺点是有损,细节会丢失。二是结构化压缩。从对话中抽出结构化信息,如字段、时间、意图、结论,保存成 JSON。这种格式适合程序处理,也适合做条件查询。
一条可行路线是“先结构化,再按需摘要”。重要内容抽成结构化字段,长期不用的内容再合并成摘要。这样兼顾了即时检索和长期归档。
6.3 什么时候该自研,什么时候用现成组件
如果是快速做 Demo、验证想法,直接用现成框架自带的记忆模块最省事。但默认实现通常比较粗糙,比如全量存历史、简单截断、不做遗忘。一旦你的用户量上来,或者对记忆准确率有要求,就值得自己写一套轻量记忆模块。
我个人的判断标准是:记忆逻辑不超过几百行代码时,自研更划算。你能精确控制什么时候写入、检索多少条、如何融合、如何遗忘。而使用重型框架时,光是理解它的记忆生命周期就要花很久。
如果你打算在自己的项目里长期迭代 Agent,我建议至少把“写入过滤+检索召回+遗忘清理”这三个核心组件自己写一遍。这样独立于任何框架,后续迁移也方便。
最后再分享一点个人体会
我最初做的记忆系统很蠢,一个 JSON 文件存所有历史,每次对话都把所有历史塞进 Prompt。结果模型越聊越乱,成本越烧越高。后来慢慢改成“写入前过滤、检索时排序、融合前控制体量、定期做遗忘”,整个 Agent 才稳定下来。
如果你也在做类似项目,我的建议很直白:不要一开始就上重型设施。先在本地跑一个 JSON 存储 + 向量检索的最小闭环,把“写入-检索-融合-遗忘”体会一遍。跑通之后,自然知道哪些地方必须换数据库、哪些地方必须加规则。记忆系统的价值从来不是存得多,而是用得准。希望这份指南能让你少走一点弯路。