简介:本教程面向开发者与技术负责人,系统讲解如何为AIAgent构建从短期上下文到长期记忆的完整记忆架构。你将了解记忆分层的设计原理、存储与检索的关键配置、写入与召回流程的实现步骤,以及验证与排查方法,最终让Agent在长周期交互中稳定记住用户关键信息。
当你为用户构建一个持续运行的对话型Agent时,很快会遇到一个典型故障:用户在早期会话中明确告知“我对花生严重过敏”,经过数天、上百轮交互后,当用户询问“今晚吃什么”时,Agent却推荐了含花生的食物。这类问题并非模型推理能力不足,而是架构层面的必然结果——大语言模型(LLM)的推理过程本质上是无状态的(Stateless),每次请求只依赖当前传入的上下文窗口,而真实应用对跨会话、跨时间的有状态(Stateful)交互有强需求。
本教程的目标,是带你完成一套可落地的Agent记忆系统实现:把“短期上下文”和“长期记忆”分层解耦,通过记忆写入、存储、检索、注入的标准流程,让Agent在有限上下文窗口下仍能保持对关键事实、用户偏好和历史事件的持续感知。
在开始实现前,需要具备以下基础条件。以下均为通用技术能力,不绑定任何具体平台。
在动手编码前,先确定三件事,否则后续会频繁返工:
参考认知科学中“感觉记忆—短期记忆—长期记忆”的分层思路,以及冯·诺依曼架构中“寄存器—内存—外存”的层次化存储思想,Agent记忆系统建议设计为三层:
即当前请求的上下文窗口内容,包括系统指令、最近若干轮对话、本轮召回的长期记忆片段。它的特点是容量受token上限约束、生命周期仅限单次请求。
即会话级历史,按会话ID保存完整或摘要化的对话记录,超出窗口部分做滚动摘要。它的作用是保证同一会话内的连贯性。
跨会话持久化的事实与偏好条目,带元数据(来源、时间、置信度、类型),支持语义检索与结构化过滤。它是解决“第五天忘记第一天过敏信息”的关键。
三层之间的关系是:请求到来时,从短期记忆取最近上下文,从长期记忆按当前问题召回相关条目,合并注入工作记忆;对话结束后,由写路径异步抽取新事实写入长期记忆。
做什么:先设计统一的记忆条目结构,作为写路径与读路径的契约。
为什么做:没有统一结构,后续去重、更新、过滤都无从下手。
示例(通用伪代码):
MemoryItem {id: stringnamespace: string // 用户或租户隔离维度type: enum // fact | preference | event | task_statecontent: string // 记忆正文,建议一句话一条embedding: vector // 语义向量source_session: string // 来源会话created_at: timestampupdated_at: timestampconfidence: float // 抽取置信度ttl: duration | null // 过期策略,事实类可永久,事件类可设TTL}
注意点:content字段务必保持“单条单事实”,例如“用户对花生严重过敏”独立成条,不要把多条事实塞进一段长文本,否则召回精度会显著下降。
做什么:在每轮或每若干轮对话结束后,调用模型对对话内容做结构化抽取,产出候选记忆条目。
为什么做:长期记忆的价值取决于写入质量。直接把整段对话存入长期记忆,会导致检索噪声大、token浪费、事实冲突难以消解。
实施要点:
示例伪代码:
function writeMemory(session):candidates = extractFacts(session.transcript) // 模型结构化抽取for c in candidates:if c.confidence < THRESHOLD: continuesimilar = searchMemory(c.namespace, c.content, topK=3)if similar has conflict:markObsolete(similar.conflicting)insert(c)else if similar.highSimilarity > DUP_THRESHOLD:update(similar.best, c)else:insert(c)
做什么:在构造本轮请求前,用用户当前输入去长期记忆中检索相关条目,并做裁剪后注入上下文。
为什么做:长期记忆可能积累成千上万条,不可能全部注入。召回质量直接决定Agent“记不记得住”。
实施要点:
示例伪代码:
function recallMemory(userInput, namespace):vecHits = vectorSearch(namespace, embed(userInput), topK=20)kwHits = keywordSearch(namespace, userInput, topK=10)mustHits = fetchByType(namespace, type=fact, priority=high, limit=5)merged = rerank(dedupe(vecHits + kwHits + mustHits))return truncateToBudget(merged, MEMORY_TOKEN_BUDGET)
做什么:把系统指令、召回的记忆片段、短期对话历史按固定顺序拼接为最终上下文。
为什么做:注入顺序影响模型对信息的注意力分配。建议顺序为:系统指令 → 高优先级用户事实 → 召回的相关记忆 → 最近对话历史 → 当前用户输入。
注意点:记忆片段应以明确标签包裹(如“以下是已知的用户长期信息”),并注明时间,帮助模型区分“历史事实”与“当前对话内容”,减少误用。
做什么:当会话历史超出窗口预算时,对较早的对话做摘要压缩,保留摘要与最近若干轮原文。
为什么做:短期记忆负责会话内连贯性,但完整历史会挤占记忆注入与回答所需的token空间。摘要策略是在“连贯性”和“成本”之间的平衡。
实施要点:摘要触发阈值建议设在窗口使用率的60%~70%;摘要需保留人物、结论、未完成事项三类要素;摘要本身也应作为低优先级条目进入写路径候选,避免信息二次丢失。
做什么:为不同记忆类型配置生命周期策略,定期清理或降权。
为什么做:长期记忆无限增长会推高检索成本、降低召回精度,并带来隐私合规风险。
建议策略:用户事实类长期保留(用户可删除);偏好类设较长TTL并随新偏好更新;事件类设中等TTL;任务状态类在任务完成后归档。所有删除操作应支持按namespace批量执行,满足数据治理需求。
| 配置项 | 含义 | 常见取值逻辑 | 风险提示 |
|---|---|---|---|
| 抽取置信度阈值 | 低于该值的候选记忆不写入 | 从0.6起步,按误写入率调整 | 过高会漏记关键事实 |
| 去重相似度阈值 | 高于该值视为重复并更新 | 0.85~0.95区间调试 | 过低会合并不同事实 |
| 召回Top-N | 单次注入的记忆条数上限 | 5~15条,视窗口大小 | 过多稀释注意力 |
| 记忆token预算 | 注入记忆占用的token配额 | 窗口的15%~30% | 过高挤压对话历史 |
| 摘要触发比例 | 历史占窗口比例达到即触发摘要 | 60%~70% | 过低导致频繁摘要丢失细节 |
| 时间衰减系数 | 时效性在重排序中的权重 | 按业务调整,事实类可设低衰减 | 过高会让旧事实失效 |
完成实现后,用以下方法验证记忆系统是否生效:
问题一:Agent仍然“忘记”早期事实。
排查顺序:确认写路径是否真的产出了该事实条目(查存储)→ 确认该条目type是否被纳入强制召回 → 确认token预算是否把它截断 → 确认注入标签是否被模型正确理解。
问题二:召回结果答非所问。
多为检索策略单一所致。检查是否只用了向量检索而缺少关键词与结构化过滤;检查embedding是否与写入时使用同一模型版本,版本不一致会导致向量空间错位。
问题三:记忆条目重复膨胀。
检查去重阈值是否过低、写路径是否在每轮都全量抽取。建议改为按轮次批量抽取,并加强写入前的相似检索。
问题四:延迟升高。
读路径串行执行多次检索会累积延迟。可将向量检索与关键词检索并行发起,重排序放在合并后执行;对高频用户可缓存其高优先级事实集合。
问题五:敏感信息被长期保留。
检查是否对记忆内容做了分类分级,是否提供按用户删除入口,是否在抽取阶段就过滤了不该长期保存的字段。
本教程围绕“Agent记忆系统从短期到长期”的核心任务,给出了三层记忆架构(工作记忆、短期记忆、长期记忆)的设计思路,并按“定义结构→写路径抽取→读路径召回→上下文组装→摘要压缩→过期清理”的顺序拆解了实施步骤,配套了配置说明、验证方法、排查思路与优化建议。关键结论是:解决Agent“金鱼记忆”的关键不在于扩大上下文窗口,而在于把无状态推理与有状态记忆解耦,用统一的记忆条目契约、高质量的写路径抽取和带保底规则的混合召回,让有限窗口内的信息始终是“当前最该知道的那部分”。后续可继续关注的方向包括:记忆的层次化摘要、多Agent记忆共享与权限模型,以及记忆质量的自评估机制。