ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

AI Agent记忆机制全解析:从失忆到跨会话长期记忆

AI Agent记忆机制全解析:从失忆到跨会话长期记忆 1. 从“每次重新认识”到“一次就记住你”如果你折腾过几轮 AI Agent大概率遇到过这种让人抓狂的场面上午刚跟 Agent 交代清楚“我的项目是智能家居控制台代码在 /home/me/smarthome编译用 make build”下午再开一轮新会话它一脸茫然地问你“您好请问您想做什么”。这不是模型不行而是 Agent 的默认设定里根本没有“记忆”这回事。大模型本身是“无状态”的每一次对话对它来说都是全新的开始。你之前喂给它的项目背景、代码路径、偏好习惯、甚至你骂过它哪句话全都像没发生过。早期我做 AI Agent 时也被这一点坑得很惨后来才慢慢摸清楚所谓“让 Agent 记住你”本质上是自己动手给 Agent 搭建一套记忆机制。这期是“走进 AI Agent”第三篇前两篇我们聊了 Agent 的底层运行逻辑和搭建思路这篇专门解决一个实际问题怎么让 Agent 在多次对话、多个会话之间持续记住用户信息、项目上下文和个人偏好。说白了就是从“每次重新自我介绍”进化到“一次认识长期有效”。这篇文章适合亲手搭过 Agent、但被“失忆”问题折磨过的开发者也适合正准备从零搭一个“带记忆”的 Agent、想知道该从哪里下手的初学者。我会把记忆机制的常见方案、具体实现步骤、以及我踩过的坑都拆开讲清楚保证你看完能直接照着重现。2. 想让 Agent“记住”先搞懂记忆到底存在哪里2.1 为什么大模型天生没有记忆要解决记忆问题第一步是理解为什么大模型天生记不住事。模型在训练时把海量文本压缩成了参数这些参数里保存的是“世界知识”不是“你的个人信息”。你每次输入一段对话模型只是根据当前输入的上下文做推理并不会因为这个输入而修改自身参数。举个例子你把“我的数据库密码是 abc123”发给模型模型当时能记住这句话是因为这句话在“上下文窗口”里。一旦上下文被新内容挤出去或者会话关闭、窗口清空模型就彻底忘记这句话。这不是 bug而是 Transformer 架构设计本身决定的上下文窗口决定了模型“短期记忆”的容量上限而模型权重又不会因为对话而更新所以“长期记忆”必须由外部机制来补。理解了这一点你就明白市面上所有“带记忆的 Agent”本质上都在做同一件事在模型外面加一层存储和读写逻辑让“该记住的东西”在会话结束后还能被取回来。2.2 记忆机制的三种主流形态根据我自己的实践和对开源社区方案的拆解目前主流的 Agent 记忆机制大致可以分成三种形态会话内记忆短期记忆靠大模型的上下文窗口实现。Agent 把历史对话、工具返回结果、中间推理过程都拼进 prompt 里让模型“看到”之前的对话内容。这种方式实现简单但受上下文长度限制对话一长就会爆掉或者丢失早期信息。跨会话记忆长期记忆在外部存储里保存用户画像、项目摘要、历史对话记录在每次会话开始时把相关信息加载回来。这是“让 Agent 记住你”的核心也是本文重点。混合记忆短期长期结合同时使用上下文窗口和外部存储。短期记忆负责当前对话的连贯性长期记忆负责跨会话的稳定身份和偏好。成熟的 Agent 框架如 LangChain、AutoGen、自研框架都会提供这种方案。三种形态并不是互斥的我在生产环境里基本都是混合使用。短期记忆解决“聊得下去”的问题长期记忆解决“越用越懂你”的问题。2.3 什么时候需要认真设计记忆这里想多说一句个人体会不是所有 Agent 都需要复杂的记忆机制。如果你的 Agent 只是做一次性的问答、翻译、代码解释那短期记忆就够用了强行加长期记忆反而增加系统复杂度。但如果你做的是这几类场景记忆机制就是刚需私人助手类 Agent需要记住用户的日程偏好、工作习惯、常用工具链。开发辅助 Agent需要记住项目结构、技术栈、代码风格、常用命令。知识库问答 Agent需要记住用户之前问过什么问题、对哪些领域感兴趣以便后续答复更精准。自动化工作流 Agent需要记住任务状态、执行进度、依赖关系否则重启一次就全乱套。判断标准很简单如果用户在第二次使用时需要重新向 Agent 解释一遍上次已经说过的话那就说明你的 Agent 需要记忆功能了。3. 记忆系统设计核心从存储方案到读写逻辑3.1 记忆存储选型向量库、关系型数据库还是文件记忆系统的底层存储方案直接决定了系统的复杂度、查询效率和可维护性。我试过三种方式各自有明确的适用场景存储方案推荐场景优点缺点向量数据库如 Chroma、FAISS、Milvus、Qdrant记忆内容量大、需要语义检索能根据“语义相似度”召回相关内容即使表述不完全一致也能找到需要做嵌入Embedding多一层模型调用部署相对复杂关系型数据库如 SQLite、PostgreSQL记忆结构清晰如用户画像、项目元信息查询精确、事务可靠、适合保存“事实型记忆”不支持语义模糊检索记得太死板本地文件JSON、Markdown、SQLite 文件个人项目、低并发、快速原型零依赖、可读性强、调试方便数据量一大就难管理不支持复杂查询我的习惯是混合使用事实型记忆比如用户姓名、项目路径、偏好配置放关系型数据库或者 JSON 文件经验型记忆比如历史对话摘要、解决问题的思路、用户提问模式放向量库。举个例子我做的一个代码助手 Agent用户画像表存在 SQLite 里包括用户 ID、语言偏好、常用框架而历史对话的摘要则经过 Embedding 后存进 Chroma会话开始时按当前输入的语义去召回最相关的几条历史记忆。3.2 记忆的写入策略不是所有内容都值得记记忆系统最容易犯的错就是“什么都记”。刚开始做的时候我把用户每一轮对话都存进去结果向量库很快被无关信息塞满召回时经常把不相关的东西带出来反而干扰模型判断。后来我总结了一套写入策略核心原则是只记录对后续对话有复用价值的信息。用户画像信息用户在对话中明确表达的偏好、背景、身份信息比如“我是一名前端工程师”“我喜欢用 Python 写脚本”“我们团队用 GitLab 做协作”。项目关键信息项目路径、技术栈、编译命令、启动方式、常用部署流程。任务结果记录某个任务执行完成后的关键结果比如“上周已经实现了登录模块”“数据库连接串配置在 config/db.yaml”。用户纠偏信息当用户纠正 Agent 的错误时这个纠正本身非常有价值。比如用户说“不是 Postgres我们用 MySQL”这条必须记下来。写入时机上我一般采取“会话结束时统一提取”或者“关键信息即时提取”两种模式。即时提取响应快但容易把不完整的信息写进去会话结束统一提取可以用更长的上下文做摘要质量更高。现在主流做法是先用模型做一次“记忆提取”判断再写入存储避免什么都塞进去。3.3 记忆的读取策略决定 Agent 能不能“想得起来”写入只是第一步读取策略决定了记忆能不能被有效利用。这里有个核心矛盾如果你把全部记忆都塞进 prompt上下文窗口会爆炸如果你只塞部分记忆又可能漏掉关键信息。我的做法是**“分层加载 语义召回”**基础身份记忆每次会话开始时固定加载比如用户姓名、常用语言、默认工作目录。这些是稳定信息量小、必须要。动态语义记忆把当前用户输入的问题做 Embedding去向量库里召回最相关的 5-10 条历史记忆拼进 prompt。这样既能覆盖“用户之前提过类似需求”的场景又不会带来太多噪声。项目级记忆如果 Agent 绑定的是某个具体项目把项目摘要和最近操作记录直接加进去。这类 Agent 通常上下文需求单一固定加载即可。读取策略一定要配合“记忆排序”也就是召回的优先级。新记忆比旧记忆更重要带纠正性质的记忆比中性描述更重要涉及用户身份的信息比具体操作细节更重要。我一般会在存储每条记忆时打上类型标签和 timestamp召回后按规则重排。3.4 记忆的新鲜度和遗忘机制长期运行的 Agent记忆会越积越多也会越来越“旧”。一个很现实的例子用户上个月用 Vue2这个月项目升级到 Vue3如果 Agent 还把 Vue2 当成用户偏好那它越“记住”越帮倒忙。所以记忆系统必须设计更新机制和遗忘机制。更新机制相对好做每当用户提供了新的偏好或信息时先查找旧记忆如果存在同类型的冲突信息就覆盖或者标记为“已过期”。遗忘机制则有两种常见做法时间衰减给每条记忆加上最后访问时间超过一定时间没有使用的记忆在召回时降权甚至直接清理。手动清理提供 API 或者界面让用户查看当前记忆、手动删除敏感或错误的条目。这两种方式我都用过时间衰减适合自动化程度高的场景手动清理适合对隐私敏感的场景。成熟的产品应该两者都提供。4. 实操为一个带记忆的 Agent 写一套可运行的最小实现4.1 方案选型与系统结构这部分我会给出一套可以直接跑起来的最小实现。它不依赖复杂框架只用 Python OpenAI 兼容 API SQLite Chroma就能实现“跨会话记住用户”的核心能力。选这套组合的原因是依赖少、可读性强、方便理解记忆机制的每一步。系统结构大概是这样SQLite保存用户画像、项目信息、偏好配置等事实型记忆。Chroma Embedding 模型保存历史对话摘要的向量索引用于语义召回。Agent 主循环每次收到用户输入时先加载基础记忆再做语义召回把记忆拼进 prompt最后调大模型生成回复并在会话结束后提取新记忆写入。提示下面的代码是完整可运行的示意实现。实际项目中你需要根据自己的模型 API 地址、Embedding 模型和业务场景做调整。4.2 定义记忆数据结构在动手写逻辑之前先定义好数据结构。我的经验是先把“记忆类型”想清楚后面读写的代码都会好写很多。# memory_types.py from dataclasses import dataclass, field from typing import Optional from datetime import datetime MEMORY_TYPES [user_profile, project_info, task_result, user_correction, conversation_summary] dataclass class MemoryItem: memory_type: str content: str user_id: str timestamp: float field(default_factorylambda: datetime.now().timestamp()) metadata: Optional[dict] Nonememory_type 表示这条记忆的类别content 是记忆正文metadata 可以放额外的信息比如项目名称、关联任务 ID。我建议不管用什么存储都统一用这个结构方便后面做类型过滤和排序。4.3 SQLite 实现事实记忆的读写事实型记忆的特点是“量小、精确、必须准”。用户姓名错了不行、数据库地址错了更不行所以它们应该放在强一致性的关系型数据库里。# sqlite_memory.py import sqlite3 import json class SQLiteMemory: def __init__(self, db_pathagent_memory.db): self.conn sqlite3.connect(db_path) self._create_table() def _create_table(self): self.conn.execute( CREATE TABLE IF NOT EXISTS memory ( id INTEGER PRIMARY KEY AUTOINCREMENT, user_id TEXT NOT NULL, memory_type TEXT NOT NULL, content TEXT NOT NULL, timestamp REAL NOT NULL, metadata TEXT ) ) self.conn.execute( CREATE INDEX IF NOT EXISTS idx_user_type ON memory (user_id, memory_type) ) self.conn.commit() def add_memory(self, item: MemoryItem): self.conn.execute( INSERT INTO memory (user_id, memory_type, content, timestamp, metadata) VALUES (?, ?, ?, ?, ?) , (item.user_id, item.memory_type, item.content, item.timestamp, json.dumps(item.metadata or {})) ) self.conn.commit() def get_memories(self, user_id, memory_typeNone, limit10): if memory_type: rows self.conn.execute( SELECT memory_type, content, timestamp, metadata FROM memory WHERE user_id ? AND memory_type ? ORDER BY timestamp DESC LIMIT ? , (user_id, memory_type, limit) ).fetchall() else: rows self.conn.execute( SELECT memory_type, content, timestamp, metadata FROM memory WHERE user_id ? ORDER BY timestamp DESC LIMIT ? , (user_id, limit) ).fetchall() return [ { memory_type: r[0], content: r[1], timestamp: r[2], metadata: json.loads(r[3]), } for r in rows ] def delete_memory(self, user_id, content_prefixNone): if content_prefix: self.conn.execute( DELETE FROM memory WHERE user_id ? AND content LIKE ?, (user_id, content_prefix %) ) else: self.conn.execute(DELETE FROM memory WHERE user_id ?, (user_id,)) self.conn.commit()get_memories 方法支持按用户 ID 和记忆类型过滤按时间倒序返回。这样基础记忆加载就很简单了每次会话开始时把 user_profile 类型拉出来即可。4.4 Chroma 实现语义记忆的召回事实型记忆搞定了但有个问题用户可能不总是用一模一样的表述提问。比如上次用户说“我的项目在 /home/me/agent-demo”这次他问“帮我看看 agent-demo 附近的代码”如果只靠 SQLite 的精确匹配根本查不到。这时候就要靠向量库做语义召回。# vector_memory.py from typing import List import chromadb from chromadb.utils import embedding_functions class VectorMemory: def __init__(self, collection_nameagent_memory, model_nameBAAI/bge-small-zh-v1.5): self.client chromadb.Client() # 这里使用本地嵌入模型也可以用 OpenAI 的 text-embedding-ada-002 self.ef embedding_functions.SentenceTransformerEmbeddingFunction( model_namemodel_name ) self.collection self.client.get_or_create_collection( namecollection_name, embedding_functionself.ef, metadata{hnsw:space: cosine} ) def add_texts(self, ids: List[str], texts: List[str], metadatas: List[dict]): self.collection.add( idsids, documentstexts, metadatasmetadatas ) def search(self, query: str, top_k: int 5, memory_type: str None): where_filter None if memory_type: where_filter {memory_type: memory_type} results self.collection.query( query_texts[query], n_resultstop_k, wherewhere_filter ) return results这里用到了 Chroma 的余弦距离作为相似度度量。Cosine 是向量检索里最常用的度量它对向量模长不敏感适合文本语义相似度计算。如果你用的是 OpenAI 的 Embedding 接口只需要把 embedding_functions 换掉即可。需要特别说明的是Embedding 模型的选择会影响召回效果。我测试过 bge-small-zh-v1.5、m3e-base、text-embedding-ada-002 等模型中文场景下 bge 和 M3E 表现不错英文场景 OpenAI 的效果更稳定。选模型时还要考虑部署成本因为向量化这一步会经常调用。4.5 让 Agent 使用记忆拼接记忆到 Prompt存储和召回写好之后真正的关键是“怎么让记忆影响 Agent 的输出”。这一步就是把召回的记忆拼接到 system prompt 里让模型在生成回答时能够“看到”这些记忆。# agent_with_memory.py from sqlite_memory import SQLiteMemory from vector_memory import VectorMemory from memory_types import MemoryItem, MEMORY_TYPES import openai class MemoryAgent: def __init__(self, openai_api_key, modelgpt-4o-mini): self.sqlite_mem SQLiteMemory() self.vector_mem VectorMemory() self.model model openai.api_key openai_api_key def _get_base_prompt(self, user_id): # 固定加载基础身份记忆和项目信息 profiles self.sqlite_mem.get_memories(user_id, memory_typeuser_profile, limit5) projects self.sqlite_mem.get_memories(user_id, memory_typeproject_info, limit3) base_lines [你是我的 AI 助手请结合你的记忆来回答我的问题。] base_lines.append(以下是你对这个用户已有的长期记忆) for item in profiles projects: base_lines.append(f- [{item[memory_type]}] {item[content]}) return \n.join(base_lines) def _get_semantic_memories(self, user_id, query, top_k3): results self.vector_mem.search(query, top_ktop_k) lines [] if results and results[documents]: for doc, meta in zip(results[documents][0], results[metadatas][0]): lines.append(f- 相关历史记忆{doc}) return \n.join(lines) def chat(self, user_id, user_input): base_prompt self._get_base_prompt(user_id) sem_memories self._get_semantic_memories(user_id, user_input) full_prompt ( base_prompt \n\n 以下是与你当前问题相关的历史记忆\n sem_memories \n\n 用户说 user_input ) resp openai.ChatCompletion.create( modelself.model, messages[ {role: system, content: full_prompt}, {role: user, content: user_input} ], temperature0.3 ) return resp[choices][0][message][content]这段代码的核心思路是把长期记忆写进 system 消息而不是和用户消息混在一起。这样做的好处是模型会把记忆当作“背景知识”而不是“必须接话的内容”回答会更自然。另外我特意设置 temperature0.3因为带记忆的场景通常需要更稳定、更贴合事实的回答太高的 temperature 会让模型自由发挥偏离记忆内容。4.6 会话结束后提取新记忆最后一步是写入新记忆。这一步我建议在会话结束之后做用一次额外的模型调用把整段对话摘要成几条记忆。这样写入的信息更精炼不会把每一轮闲聊都存进去。# extract_memory.py import openai def extract_memories_from_conversation(history_text, openai_api_key, modelgpt-4o-mini): prompt f 请从以下用户与 AI 助手的对话中提取出值得长期记住的信息。 只提取对后续对话有帮助的事实型信息比如用户的身份、偏好、项目信息、纠偏内容。 请以 JSON 数组输出每条包含两个字段 - memory_type: 取值为 user_profile / project_info / task_result / user_correction - content: 提取出的具体信息保持第一人称表述 对话内容 {history_text} resp openai.ChatCompletion.create( modelmodel, messages[{role: user, content: prompt}], temperature0.0, response_format{type: json_object} ) return resp[choices][0][message][content]提取函数返回 JSON 后你只需要逐个解析并写入 SQLite 和 Chroma。这里有一个小细节需要给每条记忆生成一个唯一 ID我在实践中一般用“user_id timestamp hash”来生成避免重复插入。5. 四个让记忆系统“翻车”的常见问题5.1 上下文窗口被记忆塞爆了这是最常见的坑。加了记忆系统后每次会话都把所有历史记忆都加载进来刚开始还好用户聊了十几次之后记忆内容越来越多最终把上下文窗口挤爆模型反而丢失了当前对话的核心信息。排查思路检查加载记忆时是否做了数量和长度限制。如果发现每次加载的记忆量超过上下文窗口的 30%就需要做裁剪或摘要。我的经验值是给记忆预留的 token 数不要超过窗口总大小的 25%剩余空间必须留给用户当前的输入和模型回答。解决方案限制固定记忆条数为 5-10 条超过的丢弃或者做摘要合并。语义召回时设置 top_k 上限一般 5 条以内足够。对召回的记忆做 token 截断超过 100 token 的单条记忆先压缩再使用。5.2 记忆被错误信息污染有一类情况特别隐蔽用户随口说了一句“我好像用的是 Python 3.8”但其实项目用的是 3.10。如果记忆系统不加辨别地全盘记录后面 Agent 就会带着错误信息回答问题越帮越乱。排查思路检查记忆写入策略是否过于宽松。如果你是直接把用户所有陈述都写入记忆那就需要加一道“信息核验”环节。解决方案对关键信息在提取过程中要求模型判断“这是否是明确而稳定的信息”。比如用户情绪化的表达、临时状况的描述不应该写入长期记忆。建立“记忆纠偏”机制。当用户明确说“不是这样”的时候优先删除或覆盖旧记忆而不是新增一条矛盾记忆。定期人工审核。如果系统面向生产我强烈建议提供一个“记忆管理后台”让用户能看到 Agent 记住了什么能手动删除不正确的条目。5.3 多用户/多项目的记忆串号在开发环境里很多人图省事全局只用一个 user_id。结果用户在 A 项目里说的内容是“我的项目是任务管理系统”切到 B 项目后 Agent 还带着这套记忆回答牛头不对马嘴。排查思路检查记忆查询时是否带了 user_id 和 project_id 这两个维度的过滤条件。如果只有一个 user_id那记忆自然全混在一起。解决方案数据模型上增加 user_id project_id 组合维度。查询记忆时同时按两个维度过滤。如果 Agent 服务了多个用户要注意鉴权确保用户只能访问自己的记忆。5.4 记忆的隐私与安全风险这个可能很多人一开始没想到但真正上线后非常重要。记忆系统存储的是用户的个人信息、项目信息、甚至可能有敏感的内部文档路径。如果存储层没有做权限隔离或者日志里泄露了记忆内容就是安全事故。排查思路检查存储层是否有访问控制、API 是否有鉴权、训练日志是否包含记忆内容。解决方案存储层加密至少做到数据库文件权限隔离。提供记忆导出和删除功能这是很多隐私合规场景的硬性要求。不要在模型调用日志里记录完整记忆内容只记录 memory_id。用户撤回授权后要能级联删除该用户的所有记忆向量和事实数据。6. 进阶思路让记忆具备“层级”和“迁移”能力基础和进阶的读写机制说完我还想多说两个值得思考的方向这也是我目前在做的探索。第一个是记忆层级。不是所有记忆都该一视同仁有些记忆是用户级适用于用户所有项目有些是项目级只适用于某项目有些是任务级只在特定任务中有意义。设计记忆架构时如果能在写入时明确记忆层级读取时按层级匹配效果会比全部混在一起好很多。第二个是记忆迁移。当 Agent 从一个任务切换到另一个任务时很多记忆其实是可以迁移的。比如用户“偏好使用 Python 而非 Java”这个信息做任何任务都应该生效“登录模块已经在 3 月完成”则只对项目开发任务有效。一个好的记忆系统应该能区分哪些是通用能力、哪些是特定场景知识并把通用能力迁移到新任务中。这两个方向没有标准答案但如果你在做严肃的 Agent 产品建议一开始就把记忆的层级模型设计好不然后面数据一多再改成本会非常大。7. 几件我希望一开始就知道的事写了这么多最后分享几点实操中的体会。第一别指望一行代码解决记忆问题。记忆系统不只是一个数据库或者一个向量索引它是由写入策略、召回策略、更新机制、遗忘机制共同构成的完整链路。任何一个环节偷懒都会在长线使用中暴露问题。第二记忆质量比记忆数量重要得多。我后期用下来真正让 Agent 表现改善的是那几十条精心提取的高质量记忆而不是几千条无差别的对话记录。一定要花时间设计记忆提取的 prompt让模型只记真正值得记的东西。第三一定要给用户“忘掉你”的能力。这是我在做产品后才意识到的。用户对“AI 记住自己”这件事既期待又警惕。提供一个清晰的“查看记忆/删除记忆”入口反而会增加用户的信任感。第四记忆系统的可观测性要提前做。建议在每条记忆写入时打印日志、在每次召回时记录命中情况。等出问题的时候这些日志能帮你快速定位到底是没写入还是没召回还是回召了但 prompt 拼接错了。这比瞎猜高效得多。希望这篇能帮你把自己的 Agent 真正变成一个“越用越懂你”的助手。如果你也在折腾 AI Agent 的记忆机制欢迎带着你的踩坑经历来交流我们下篇接着聊。
返回列表