LLM 的上下文窗口再大,关掉会话就“失忆”。这是当前 Agent 落地时最常见的痛点。很多团队把“上下文窗口”当成“记忆”来用,结果对话一长就开始漏信息、重复提问,甚至把上一轮的错误决策带进下一轮。真正能撑起复杂任务的 Agent,需要一套独立于模型上下文之外的记忆系统:有写入、有检索、有更新、有遗忘。
这次我们来完整拆一遍 Agent 长期记忆架构。文章会先讲清短时记忆与长期记忆的边界,再给出一套可落地的记忆分层模型,然后逐步展开记忆写入、混合检索、遗忘机制的实现思路,最后给出一个可以照着改的工程模块设计和接口示例。内容偏系统设计,也包括可以直接抄的代码骨架。
1. Agent 记忆架构核心思路速览
先给一张总览表,后面所有章节都围绕这张表展开。
| 设计项 | 推荐方案 |
|---|---|
| 记忆分层 | 工作记忆 / 情景记忆 / 语义记忆 / 技能记忆 |
| 短时记忆存储 | 会话上下文、任务栈、Redis 缓存 |
| 长期记忆存储 | 关系型数据库 + 向量索引,或专用向量数据库 |
| 写入触发 | 用户显式表达、任务完成、事件发生、定时回顾 |
| 检索链路 | 向量召回 + 关键词召回 + 时间过滤 + 重排序 |
| 遗忘策略 | 时间衰减、重要性评分、容量上限、显式删除 |
| 接入方式 | 记忆服务 API,或接入 LangGraph 等框架的 memory 节点 |
| 合规要求 | 最小必要存储、支持用户删除、敏感信息脱敏 |
这套结构解决的核心问题是:Agent 不再只有“上下文窗口里的临时文本”,而是拥有一个跨会话、可检索、可管理的事实库。短期记忆负责“当前任务跑得顺”,长期记忆负责“这个用户是谁、之前做过什么、结论是什么”。
2. 为什么 Agent 需要独立于大模型之外的长期记忆
大模型本身是没有状态的。你调用一次接口,传入完整的对话历史,模型才能“知道”前面聊了什么。这也是为什么很多人把上下文窗口当成 Agent 的记忆:因为从表现上看,只要把历史消息全部拼进 prompt,Agent 好像就记得住。
但这种方式有四个硬伤。
第一,成本随轮数膨胀。每一轮对话都把全部历史发给模型,token 消耗持续增长。第二,窗口有上限,超出之后只能截断。截断会把关键结论丢掉,而且往往是对话中期的信息先丢。第三,无法区分“值得记的事”和“临时的一句话”。用户在某轮随口说“我比较喜欢简洁的回复”,这句话如果不在窗口里,下一轮就没了;如果把所有话都留着,又会污染 prompt。第四,不同任务之间无法共享记忆。用户上一个 Agent 里确认过的偏好,下一个 Agent 完全不知道。
所以长期记忆的定位应该是:把对话中真正有长期价值的信息抽取出来,存到记忆系统里;等到下一次需要时,通过检索把相关的记忆片段重新放回 prompt。也就是“唤醒”而不是“全量携带”。这也正是 LangChain、LangGraph 等 Agent 框架开始强调长期记忆能力的原因——模型本身负责推理,外部记忆模块负责记住,两者各司其职。
3. 短时记忆与长期记忆的区分
很多入门设计会漏掉一个重要问题:什么样的信息算短时记忆,什么样的信息必须进长期记忆?区分标准不能只看“时间长短”,还要看信息是否具备跨任务复用价值。
短时记忆的特征是:
- 只在当前任务内有效,例如用户正在填写的表单内容、中间推理步骤、临时文件路径;
- 容量小,生命周期短,任务结束即失效;
- 复用的价值低,绝大多数中间状态不需要进入长期库。
长期记忆的特征是:
- 可以跨会话、跨任务使用,例如用户偏好、项目背景、已确认的业务规则;
- 时效性可能很长,也可能有有效期,例如“用户当前正在使用免费套餐”;
- 从原始对话中抽取出来之后,仍然需要继续维护,包括冲突处理和过期淘汰。
下面用一张表做对比:
| 维度 | 短时记忆 | 长期记忆 |
|---|---|---|
| 生命周期 | 会话内或任务内 | 跨会话、跨任务 |
| 存储位置 | 内存、Redis、会话文件 | 数据库、向量索引 |
| 数据形态 | 原始消息、任务状态、临时变量 | 结构化事实、摘要、用户画像 |
| 写入方式 | 直接追加 | 抽取、去重、冲突合并 |
| 检索需求 | 无,直接读取 | 需要检索,只取相关片段 |
| 遗忘方式 | 会话结束即清理 | 需要显式遗忘策略 |
如果把这个边界搞混,会出现很典型的问题。把临时信息写进长期库,长期记忆会被噪声淹没,检索时永远命中一堆无关记录;把长期信息全部留在短时上下文里,token 成本暴涨,而且会话结束之后这些信息就永久丢失。所以分层设计不是理论洁癖,是实际工程必须做的第一步。
4. 记忆分层:工作区、情景区、语义区、技能区
我建议把 Agent 记忆分成四层。每一层解决一类问题,存储介质、写入触发条件和维护策略都不同。
4.1 工作记忆层(L0)
工作记忆层对应短时记忆,存的是当前正在执行的会话状态和任务状态。包括用户最新输入、Agent 的推理中间结果、待处理队列、工具调用记录等。
实现上不需要复杂数据库,通常用内存对象或 Redis 就行,每个会话一个键。要注意的是必须设置生命周期,例如会话闲置 30 分钟后清理,或者任务完成后压缩。
工作记忆层虽然技术含量不高,但它是整个记忆系统的入口。很多长期记忆的抽取工作,就是从这里拿到原始素材的。
4.2 情景记忆层(L1)
情景记忆层存的是跨会话但没有经过太多抽象的事件记录。例如“用户在 3 月 10 日要求生成一份月度报表”“用户在上一次对话中提到了项目 A 的截止时间是月底”。这些记录是“发生过的事实”,保留原始语义。
情景记忆通常会被转成长期记忆之后再做清理,而不是一直无限堆积。它的价值在于:当 Agent 需要回答“我的项目甲方之前提过什么要求”这种问题时,情景记忆是最终的事实来源。
存储建议使用普通数据库表,每条记录包含时间、事件摘要、关联实体、重要性分数和过期时间。
4.3 语义记忆层(L2)
语义记忆层是长期记忆的核心,存的是经过抽取和抽象之后的稳定事实。例如“用户偏好简洁回复”“用户负责的项目叫 x 项目”“团队使用 PostgreSQL”。这些事实不再依赖某一次对话,而是像用户画像一样长期存在。
语义记忆的写入需要经过 LLM 抽取和冲突处理。比如用户上一轮说“我讨厌长邮件”,下一轮说“重要客户可以发长邮件”,这两条信息需要合并而不是覆盖,因为后者是对前者的补充。
语义记忆的存储一般用关系型数据库加向量索引,或者直接用向量数据库。检索时既要支持按实体属性精确匹配,也要支持按语义相似度召回。
4.4 技能记忆层(L3)
技能记忆层存的是“怎么做”的经验。例如某个工具调用流程在什么条件下有效、某个常见任务的处理步骤、哪些 workflow 在上一次执行时成功了。
这一层和 Agent 的 skill/plugin 机制可以结合。它可以不存自然语言描述,而是存可执行的模板或工作流引用。技能记忆的意义在于,Agent 在遇到重复任务时可以直接复用成功路径,而不是每次重新探索。
四层记忆之间不是孤立的。典型流程是:工作记忆中的对话内容被抽取出事件,进入情景记忆;情景记忆经过摘要和去重,升级为语义记忆;多次成功的任务路径沉淀为技能记忆。这样记忆就有了“从原始到抽象,从临时到稳定”的完整生命周期。
5. 记忆写入:什么时候记、记什么、怎么去重
长期记忆架构里,最容易被忽略的是写入策略。没有好的写入策略,检索做得再漂亮也没用,因为库里装满了噪声。
5.1 写入触发时机
不建议把每一轮对话都写进长期记忆。比较合理的触发点有四个:
- 用户显式表达偏好。例如“以后都用中文回复”“我不喜欢看超过 500 字的总结”。
- 任务完成时抽取关键结论。例如“本次任务生成了 3 份周报,存档位置是 xxx”。
- 跨会话关键事件发生。例如用户创建了一个新项目、修改了截止时间。
- 定时回顾。每隔一定轮数,对近期工作记忆做一次摘要,把有价值信息抽取出来。
5.2 记忆条目数据结构
一条长期记忆应该至少包含下面这些字段:
{ "memory_id": "mem_xxx", "type": "semantic", "subject": "user", "predicate": "prefers_language", "object": "chinese", "importance": 0.9, "created_at": "2025-01-01T10:00:00Z", "last_accessed_at": "2025-01-01T10:00:00Z", "expires_at": null, "source_session": "session_xxx", "content_embedding": [] }subject/predicate/object 结构适合表示“主谓宾”三元组,既方便精确查询,也方便做冲突检测。importance 是重要性分数,遗忘机制会用到。expires_at 是过期时间,适合临时性事实。
这个结构是示例,实际可以根据业务调整。但建议至少保留类型、实体、属性、值、重要性、时间戳这几个核心字段。
5.3 写入去重与冲突合并
写入前先做相似度检查。如果新记忆的向量和已有记忆的相似度超过阈值,就不要直接插入,而是做合并。例如“用户喜欢简洁回复”和“用户喜欢简短回复”语义高度相似,就应该保留一条,或者把两者合并成“用户喜欢简洁、简短的回复”。
合并规则建议按重要性优先:新值重要性更高时更新旧值,否则保留旧值并补充说明。要避免“每次写入都新增一条”,这样长期记忆会快速膨胀,检索时返回一堆重复内容。
代码层面,写入流程可以抽象成这样一个函数:
def remember( memory_store, embedding_model, item: MemoryItem, duplicate_threshold: float = 0.85, ): if not item.content: return query_vec = embedding_model.embed(item.content) item.content_embedding = query_vec # 先查重,再决定新增还是合并 candidates = memory_store.search_similar( vec=query_vec, top_k=5, threshold=duplicate_threshold ) for cand in candidates: if is_semantic_similar(item, cand): memory_store.merge(item, cand) return memory_store.insert(item)这个查重逻辑非常重要。实际运行时,你可以在同一个任务里快速验证:连续写入两条语义相近的记忆,观察最终库里是否只保留了一条(或者变成一条合并后的记录)。
6. 记忆检索:向量 + 关键词多路召回 + 重排序
长期记忆如果只有写入没有检索,就是一个死库。检索的目标是:给定当前用户问题,从记忆库中找出最相关、最有用的记忆片段,组装到 prompt 里。整个检索流程我分成五步。
6.1 查询改写
用户的问题通常是口语化的,例如“帮我把上次给财务的方案找出来”。直接拿这句话做向量检索,语义能命中一部分,但精确命中“财务方案”这种实体关键词会较弱。所以第一步是查询改写,用 LLM 把用户问题转换成适合检索的形式,例如:
- 原始问题:帮我把上次给财务的方案找出来
- 检索 query:财务 方案 上次 时间
- 检索意图:查找用户和财务相关的历史方案
这一步可以简单调用一次 LLM,也可以根据意图模板生成。查询改写不是必须的,但在命中率不足时优先检查这里。
6.2 多路召回:向量检索 + 关键词检索
单靠向量检索不够。向量检索适合语义相似,但不擅长精确匹配人名、编号、专有名词。比如用户问“项目X的负责人是谁”,如果记忆里存的是“项目X由张三负责”,向量检索能命中;但如果存的是“张三 项目X 负责人”,关键字权重较高时,BM25 召回会更准。
所以建议做双路召回:
- 向量召回:embedding 模型编码 query,在向量索引中取 top_k;
- 关键词召回:使用 BM25 或全文索引,按词频和逆文档频率召回包含关键实体的记忆。
热搜里提到的“向量混合检索加 bm25 多路召回”就是在讲这种方案。两面召回之后合并、去重,再进入重排序阶段。
一个简化版的双路召回伪代码如下:
def search_memories(memory_store, query: str, top_k: int = 10): query_vec = embedding_model.embed(query) vector_hits = memory_store.vector_search(query_vec, top_k=top_k) keyword_hits = memory_store.bm25_search(query, top_k=top_k) # 合并候选集,按来源去重 candidates = merge_by_memory_id(vector_hits, keyword_hits) return rerank(candidates, query, query_vec, top_k=top_k)6.3 时间过滤与重要性加权
召回之后的候选集不能直接进 prompt。需要做一轮过滤:已经过期的记忆直接丢弃;距离过期时间很短的记忆降权;重要性分数低的记忆只有在没有其他候选时才返回。
这里引入一个简单的评分公式:
score = semantic_score * alpha + keyword_score * beta + importance * gamma - time_decay * delta其中 alpha、beta、gamma、delta 是权重,time_decay 是距离创建时间或最后访问时间的衰减函数。具体数值要在你自己的数据集上调,先固定一个可用版本,再看命中率调整。
6.4 重排序(Rerank)
多路召回召回的数量可以放大,比如 top 50,然后重排序后再取 top 5。重排序可以使用专门的 rerank 模型,也可以直接用 LLM 对候选记忆打分。对小规模应用,用 LLM 打分成本可控;大规模应用建议用 lightweight rerank 模型,避免每次检索都调用大模型。
重排序的目标不是“找到语义最像的那条”,而是“找到当前任务最需要的那条”。例如用户问“这个项目什么时候截止”,记忆库里有一条“项目截止时间是月底”,还有一条“用户是项目负责人”,前者应该排得更靠前,尽管后者的实体关联更强。
6.5 组装上下文
检索到的记忆要以紧凑文本的形式插入到 system prompt 中,而不是把完整原始对话塞回去。建议给每条记忆加一个元信息前缀,例如:
【记忆-语义】用户偏好:使用简洁中文回复(重要度0.9,记录于2025-01-01) 【记忆-情景】上次对话:用户要求生成了3份周报,存放在 /reports/weekly/这样可以告诉模型“这些是历史记忆”,避免模型混淆记忆和当前对话。同时控制召回条数,例如默认返回 5~8 条,每条记忆控制在一两句话,整体记忆 prompt 保持在几百 token 以内。
7. 遗忘机制:不让记忆变成包袱
长期记忆系统如果没有遗忘机制,早晚会失控。遗忘不是删除,而是对记忆进行“降权、归档、最后删除”的生命周期管理。遗忘机制要解决几个问题:存储无限膨胀、过时信息产生误导、用户隐私数据存留过久。
7.1 遗忘的三个层次
- 软失效:把记忆标记为“不活跃”或“低置信度”,检索时降低权重,但不物理删除。适合暂不确定是否还有用的记忆。
- 归档:把长期不访问的记忆移到冷存储,如从热向量索引迁到离线表。保留记录但不再参与实时检索。
- 硬删除:物理删除记忆记录及其向量索引。适用于过期隐私数据、用户主动要求删除的数据、重复错误数据。
7.2 遗忘评分策略
给每条记忆维护一个遗忘分,定期扫描并处理低分记忆。遗忘分可以由这几个因素决定:
- 重要性(importance):越低越容易被遗忘;
- 最后访问时间(last_accessed_at):越久越容易被遗忘;
- 访问频率:高频访问的记忆要优先保留;
- 是否过期(expires_at):过期记忆直接进入删除或归档流程。
一个简单的遗忘评分函数可以这样实现:
import math from datetime import datetime, timezone def forget_score(item, now=None): now = now or datetime.now(timezone.utc) age_hours = (now - item.last_accessed_at).total_seconds() / 3600.0 # 时间衰减:age 越大,score 越小;半衰期设为 7 天 half_life = 24.0 * 7 time_factor = math.pow(0.5, age_hours / half_life) # 综合分数,importance 越高越不容易被遗忘 return item.importance * time_factor * (1.0 + item.access_count * 0.1)这里的半衰期 7 天是示例参数,真实业务需要按用户习惯和记忆类型调整。语义记忆的半衰期应该比情景记忆长,用户画像比临时任务结论长。
7.3 定期整理与摘要合并
遗忘机制不能只被动等待记录过期,还要定期主动整理。例如每周对情景记忆做一次摘要,将多条相似事件合并成一条语义记忆,然后删除原始事件记录。这样既能压缩存储,又能提升检索质量。
整理任务可以做成一个后台定时任务,例如每天凌晨运行:
- 扫描低遗忘分记忆,标记归档或删除;
- 对相似记忆做聚类合并;
- 对过期记忆执行硬删除;
- 更新用户记忆画像摘要。
注意一点:用户主动要求删除的记忆,必须真实删除,不能只做“降权”。尤其是涉及个人信息的数据,除了删除主记录,还要同步删除向量索引、缓存和归档副本。这是合规底线。
8. 工程实现:记忆模块的落地骨架
下面给出一套可以照着改的工程骨架。这里不绑定具体框架,核心接口是通用的:写入、检索、遗忘、整理。
8.1 存储选型参考
不同规模的项目选型差异很大,给一个保守参考:
| 规模 | 短时记忆 | 长期记忆 |
|---|---|---|
| 单机小项目 | 内存字典 + pickle/JSON 落地 | SQLite + 文件向量索引 |
| 团队中等项目 | Redis + JSON | PostgreSQL + pgvector |
| 大规模服务 | Redis Cluster + 会话缓存 | PostgreSQL + pgvector / Milvus 等向量库 |
选择存储时先不要追求“最先进的向量数据库”。用 PostgreSQL 自带 pgvector,或者 SQLite 加向量扩展,就足够启动一个真实项目。把记忆 schema 设计好,后面迁移到专用向量库并不难。
8.2 核心接口设计
class MemoryStore: def insert(self, item: MemoryItem) -> str: ... def search_similar(self, vec, top_k, threshold) -> list: ... def search_keyword(self, query, top_k) -> list: ... def update(self, memory_id: str, updates: dict) -> None: ... def delete(self, memory_id: str) -> None: ... def bump_access(self, memory_id: str) -> None: ... def get_expired(self, now) -> list: ...8.3 一个简单的写入实现
这里用 SQLite 做示例,方便大家快速跑通。实际生产可以用 PostgreSQL + pgvector,接口保持一致。
import sqlite3 import json class SqliteMemoryStore(MemoryStore): def __init__(self, db_path: str): self.conn = sqlite3.connect(db_path) self.conn.execute(""" CREATE TABLE IF NOT EXISTS memories ( memory_id TEXT PRIMARY KEY, type TEXT, content TEXT, importance REAL, created_at TEXT, last_accessed_at TEXT, expires_at TEXT, access_count INTEGER DEFAULT 0, embedding TEXT ) """) self.conn.commit() def insert(self, item: MemoryItem) -> str: cur = self.conn.execute( """ INSERT INTO memories (memory_id, type, content, importance, created_at, last_accessed_at, expires_at, access_count, embedding) VALUES (?, ?, ?, ?, ?, ?, ?, 0, ?) """, ( item.memory_id, item.type, item.content, item.importance, item.created_at, item.last_accessed_at, item.expires_at, json.dumps(item.content_embedding), ), ) self.conn.commit() return item.memory_id搜索需要把 embedding 存成 JSON 还是用专门向量列,取决于你选的存储方案。生产环境建议直接使用数据库原生向量索引,避免每次全量扫描。
8.4 与 Agent 框架的接入方式
LangGraph 这类框架已经把“长期记忆”放进了官方能力体系里,但核心思路是一样的:你需要注册一个 memory store,在每次对话轮次前后触发写入和检索。典型的接入点是:
- 在对话开始时,读取当前用户的历史记忆,组装到 system prompt;
- 在对话结束时,抽取本次对话的长期记忆,写入记忆库;
- 在工具调用失败或多次重复问题时,主动检索记忆补充上下文。
不要把这个模块做成不可替换的“大胶水”。记忆模块应该只依赖两个输入:当前对话历史和用户标识。这样无论是接入 LangGraph、自研 Agent,还是未来换成另一个框架,都只需要改接入层,不需要重写记忆逻辑。
9. 记忆服务 API 与批量任务
当记忆模块从 Agent 进程里抽离出来,它就是一个独立的记忆服务。这样做的好处是:多个 Agent 可以共享一套记忆,前端可以独立查看和删除记忆,批量整理任务也可以在服务端统一执行。
9.1 接口设计示例
下面给出一组示例接口,字段和路径请按实际项目调整。这里只是为了说明接口形态:
| 方法 | 路径 | 作用 |
|---|---|---|
| POST | /memories | 写入一条记忆 |
| POST | /memories/search | 检索记忆 |
| POST | /memories/forget | 遗忘/删除记忆 |
| GET | /memories/{user_id} | 查看某个用户的记忆列表 |
| POST | /memories/consolidate | 触发一次整理任务 |
搜索接口请求体示例:
{ "user_id": "user_123", "query": "财务方案", "top_k": 5, "include_expired": false }9.2 Python 调用示例
记忆服务启动后,Agent 侧可以通过 HTTP 调用。
import requests url = "http://127.0.0.1:8000/memories/search" payload = { "user_id": "user_123", "query": "财务方案", "top_k": 5, "include_expired": False, } resp = requests.post(url, json=payload, timeout=5) if resp.status_code == 200: memories = resp.json()["items"] for mem in memories: print(mem["content"], mem["importance"])9.3 批量任务设计
批量任务通常有两类。一类是批量写入,例如从历史对话日志中回填记忆。这种任务要求写入接口支持批量和幂等,否则重复回填会产生大量重复记忆。另一类是批量整理,例如每晚对全部用户的记忆执行遗忘扫描和摘要合并。
批量整理任务要特别关注两个问题:
- 失败重试:整理任务涉及删除和合并,中途失败容易造成数据不一致。建议每一步操作先记录操作日志,再执行变更;
- 全量扫描成本:随着记忆量增加,每天全量扫描所有记忆会越来越慢。可以按用户分组,每天只处理一部分,或者按“最近有活跃记录的用户”优先处理。
接口服务的稳定与否,直接决定了记忆模块能不能被其他系统复用。所以最好把写入去重、检索超时、遗忘删除这几个流程都在服务端固定下来,而不是每个调用方各写一遍。
10. 性能观察与常见问题排查
长期记忆系统上线后,不要只关心功能是否“看起来能跑”,要看几个关键指标。
10.1 关键指标
| 指标 | 观察方式 | 说明 |
|---|---|---|
| 写入耗时 | 记录每次 remember 的 P95 耗时 | 太高说明查重或 embedding 调用成为瓶颈 |
| 检索耗时 | 记录 search 的 P95 耗时 | 多路召回后如果超过几百毫秒,需要优化索引 |
| 记忆总量 | 每天记录新增和删除数量 | 持续增长说明遗忘策略没生效或查重没生效 |
| 命中率 | 人工抽样判断检索返回值是否相关 | 可以用“检索是否被 Agent 采用”做间接指标 |
| embedding 调用量 | 记录每日 embedding 接口调用次数 | 写入和检索都依赖 embedding,费用需要关注 |
这些指标不需要一次性全做,但至少要记录日志。否则上线之后系统效果变差,很难定位是记忆写入的问题、检索的问题还是 prompt 组装的问题。
10.2 常见问题排查表
| 问题现象 | 可能原因 | 排查方式 | 解决方案 |
|---|---|---|---|
| 检索结果和当前问题无关 | 向量阈值设置过低,召回大量弱相关记忆 | 检查候选记忆的相关性分数分布 | 调高相似度阈值,增加关键词召回权重 |
| 记忆库快速增长 | 写入去重失效 | 查看是否有大量相似记忆同时存在 | 加强写入前向量查重和合并逻辑 |
| 某些用户记忆过多 | 没有按用户设置容量上限 | 查看单个用户记忆数量 | 按用户设置容量上限,超出触发遗忘 |
| 记忆 prompt 占用过大 | 召回条数过多或单条记忆太长 | 检查最终插入 prompt 的记忆内容长度 | 降低 top_k,控制每条记忆长度 |
| 用户删除记忆后仍被检索到 | 只删除主表,未清理向量索引或缓存 | 检查删除逻辑覆盖范围 | 同步删除向量索引、缓存和归档副本 |
| 检索延迟偏高 | embedding 调用慢或向量索引未优化 | 分段测量查询改写/embedding/检索耗时 | 对向量索引做量化和分片,必要时增加缓存 |
11. 最佳实践与合规边界
长期记忆看起来只是技术模块,但涉及用户数据,必须把合规和隐私问题放在一起设计。
第一,坚持最小必要存储。不要把所有原始对话全部入库,只存储抽取后对任务有复用价值的结构化事实。这样既能降低存储成本,也能减少隐私风险。
第二,敏感信息必须脱敏。用户姓名、电话、地址、身份证号、财务信息等,尽量不要以明文形式存进长期记忆。如果业务确实需要,要做加密存储和访问审计。
第三,支持用户自主查看和删除。Agent 应该允许用户查看“你还记得关于我的哪些信息”,并且允许一键删除全部记忆。这不仅是合规要求,也能提升用户信任。涉及人脸、声音、私有文档等敏感内容时,必须获得明确授权后才能写入记忆库。
第四,内存和数据库权限要隔离。记忆服务如果是对外的,需要做用户身份校验,防止一个用户检索到另一个用户的记忆。即使是内部服务,也建议按用户维度做隔离,避免误查询导致的数据泄露。
第五,介入人工审核。对重要事实和用户画像类记忆,可以在抽取后做低置信度标记,由人工或后台校验后再进入线上检索。不要完全信任 LLM 抽取结果,它在某些场景会幻觉出用户从未说过的信息。
12. 总结与下一步
这套 Agent 长期记忆架构最值得记住的三个设计点是:分层、混合检索、遗忘机制。分层的本质是让不同类型的记忆各归其位,避免把临时信息当成核心事实;混合检索解决的是“语义相似”和“精确匹配”不能互相替代的问题;遗忘机制则是让记忆系统长期可用的前提。
最先应该验证的功能是一个最小闭环:在一个会话里告诉 Agent 一条用户偏好,关闭会话,开启新会话后,问一个需要用到这条偏好的问题,看 Agent 能否通过长期记忆检索正确回答。这个闭环能跑通,架构就已经成立了一大半。
最容易踩的坑是把短时记忆直接当成长期记忆写入,导致记忆库快速膨胀,以及不做遗忘策略,导致越用越慢。这两个问题在系统初期很难发现,等记忆量上去了再改,成本会高很多。
下一步你可以继续扩展的方向:为记忆增加置信度评估和人工审核流程,将多用户的记忆做权限隔离,在不同业务场景里设置不同的记忆 TTL,以及把记忆检索结果做成可观测的日志,方便持续调优检索权重。等这些做扎实了,这个长期记忆模块就可以真正接到生产 Agent 里。