AI Agent记忆系统构建:从存储选型到召回实践

发布时间:2026/9/13 6:07:21

AI Agent记忆系统构建:从存储选型到召回实践 做AI Agent做到第三篇我总算有底气来聊记忆这个话题了。前两篇讲完规划和流程控制评论区问得最多的一句话就是为什么我的Agent像个临时工上午刚聊清楚的需求下午就全忘了明明告诉过它的偏好转头就按默认方式处理。这个问题的本质就是Agent缺一套自己的记忆体系。今天这篇我会把让Agent记住你的完整思路理一遍从记忆分类、存储选型到写入和召回的工程实现再到我实际踩过的坑一次性讲透。适合正在用LangGraph、Coze、Dify这类框架搭Agent又被聊完就断片折磨的朋友。1. Agent为什么记不住问题并不在模型本身1.1 大语言模型天生是无状态的很多人第一次接触Agent时会有一种错觉模型既然这么聪明应该自带记忆吧但实际上大语言模型是彻底的无状态系统。每一次调用API模型接收的只有你本次传过去的输入它既不记得上次说了什么也不知道中间发生过什么。所谓上下文其实是调用方也就是我们写的应用代码把历史记录拼在请求里一起喂给模型的。打个比方模型像一个随时上岗的侦探破案能力很强但天生失忆。你只有把所有案卷都放到他桌上他才知道自己接下来要查什么你放得越全他判断越准。Agent框架做的事情就是不停帮他整理和放案卷。所以记不住这个锅模型不背是我们没有把记忆系统设计好。1.2 现阶段Agent记忆的三个层次业内讨论Agent记忆经常把记忆分成三个层次会话记忆、长期记忆、工作记忆。这三者不是互相替代的关系而是各管一段。记忆类型生命周期典型载体用途会话记忆短单次会话内内存、Redis、上下文窗口保持当前对话连贯性长期记忆长跨会话数据库、向量库、文件记住用户偏好、关键事实工作记忆极短任务执行中状态机、临时变量多步骤任务中的运行状态会话记忆最直观就是上一轮聊到哪了一般由框架自带的上下文管理解决。长期记忆是今天这篇文章的主角它解决的是你上次说的那件事这次见面我还记得。工作记忆更多出现在多智能体协作场景比如子任务跑到哪一步、中间结果存在哪属于状态管理的范畴。在做记忆系统之前先把这个分类想清楚非常关键因为不同层次的记忆存储介质、读写时机、容错策略完全不一样。我见过不少项目把长期记忆当成聊天记录无限往上下文里塞结果token开销爆炸效果却越来越差。这就是分类没做明白的典型后果。2. 存储选型先决定你让Agent把事记在哪2.1 会话记忆上下文窗口内的临时笔记会话记忆的存储相对简单通常就是维护一个消息队列按时间顺序存下用户和模型的消息。但这里有个必须注意的点上下文窗口是有限的不可能无限积累。模型普遍有128k、200k的上下文窗口看着很大但塞满之后老的内容会被系统自动截掉Agent会突然失忆。实际项目中我更推荐做滚动摘要和滑动窗口的混合策略。具体做法是保留最近N轮对话的完整消息比如最近的20轮确保当前话题连贯更早的内容通过LLM总结成一段200字以内的摘要塞进系统提示词。这样既保留了核心信息又控制了token成本。实测下来这种方式在绝大多数对话场景里比简单截断老消息要稳得多至少用户问到我上周提的那个需求Agent还能从摘要里找到线索。2.2 长期记忆结构化数据与向量库各干各的长期记忆的存储选型是记忆系统里最容易被搞复杂的地方。很多团队一上来就上向量数据库觉得能语义检索就是先进结果把简单问题复杂化了。我的建议是先分清你要记什么。用户偏好、基础画像这类事实型信息适合放进结构化数据库。比如用户不喜欢吃辣、习惯用某种格式汇报、倾向于简短回复这些是明确的字段用SQLite、MySQL、PostgreSQL存都行查询精确、成本极低。而上周聊过想买露营装备这种语义型信息它不是一个干净的字段更像一段回忆这时候才轮到向量数据库出场先把这段文本转成embedding向量存进向量库下次遇到相关话题用语义相似度把这段记忆捞出来。记忆类型例子存储方式召回方式事实型用户偏好、禁忌、身份信息关系型数据库SQL精确查询语义型聊过的事、想法、模糊概念向量数据库向量相似度检索混合型带时间线的事件记录关系型向量库结构化过滤语义召回个人项目的轻量方案SQLite加一个本地的embedding模型就足够跑起来。如果项目到了一定的规模需要多人协作和高并发再考虑pgvector或者专门的向量数据库。没必要一上来就上最重的方案记忆系统的瓶颈从来不在存储引擎本身而在写入和召回的策略。2.3 别忽略记忆的更新与删除存储设计里最容易被忽略的是记忆的更新和删除。很多初版记忆系统只设计了写入和读取没有修改和过期机制跑一段时间之后记忆库越来越脏甚至出现互相矛盾的记忆。我现在的做法是每条记忆都带上时间戳、来源会话ID和置信度字段。当新记忆和旧记忆冲突时以时间戳更新的为准当用户明确说我之前说的那个不算了直接做逻辑删除或标记失效。记忆不是永久沉积层它应该像人的记忆一样会遗忘、会被新信息修正。没有更新能力的记忆库最后一定会变成垃圾场召回的还都是陈旧信息。3. 记忆系统的工作流从写入到召回3.1 写入端对话结束后让LLM自己提炼记忆记忆系统最容易犯的错误是把整段对话原文存进去。这样做的后果有两个一是存储膨胀得快二是召回时噪声太大。正确的做法是在对话进行中或结束时让LLM自己完成信息提炼。我会在Agent处理完一轮关键交互后触发一次记忆提取把最近的对话丢给模型让它按固定格式输出三类信息用户的基本画像姓名、身份、偏好、本次对话中明确表达的需求和结论、需要长期记住的事件或事实。输出是结构化JSON直接写入存储。这样做的好处非常明显存储的是提炼后的高价值信息而不是流水账召回时拿到的条目足够精炼不会引入大段废话。写入频率也要控制不是每句话都写一个会话最多写几条关键记忆就够否则又变成刷屏式存储。我通常会做一层去重判断同一类信息已经存在且没有变化就不重复写。3.2 召回端开场就想起来的比临时翻笔记重要召回的设计直接决定用户的第一感受。最好的体验是用户刚打开对话Agent就已经记得他是谁、上次聊了什么而不是等用户主动提起才想起来。所以在会话开始阶段我就用当前用户的ID去检索长期记忆。做法很简单构造一条包含用户身份加当前场景的检索query比如用户张三今天需要规划周末露营行程拿这个query去向量库里做语义检索取回topK条相关记忆同时从结构化数据库里拉取用户的核心偏好。这两部分内容合并作为系统提示词的一部分注入。这里有个参数值得说一下topK到底取多少。取太少相关记忆可能漏掉取太多注入的上下文膨胀反而干扰模型判断。我实测下来topK在3到5之间效果最好加上结构化偏好信息整个记忆块控制在500字以内对模型指令遵循的影响最小。3.3 组装进上下文给模型的记忆要少而准记忆召回到手之后不能一股脑拼进提示词。我习惯把记忆块放在系统提示词靠前的位置用一个明确的标记段包裹比如已知用户历史信息里面按条目列出召回的长期记忆再紧跟当前会话的近期历史。这里有个实践中的小技巧给记忆条目加上时间上下文。比如用户上周五提到希望采用更简洁的周报格式比单纯说用户喜欢简洁周报要更有用。模型能看到时间线就能判断哪些信息可能已经过时不会盲目遵守旧偏好。另外如果新对话里的信息与记忆冲突我会在提示词里显式说明以用户当前表述为准避免模型被旧记忆带偏。4. 工程落地基于MCP和LangGraph的实践记录4.1 为什么优先考虑MCP Memory服务聊完设计说落地。现在构建Agent应用很多人已经在用MCPModel Context Protocol来标准化Agent与外部工具的交互。记忆这块MCP生态里已经有现成人可以选实现一个标准的记忆服务对外暴露保存记忆和检索记忆两个核心接口不管底层是文件、SQLite还是向量库上层调用方式完全统一。我建议优先考虑这类标准化的现成实现原因很简单省事。自己从头写一套记忆存储又要处理持久化、又要处理embedding、又要设计接口协议工作量不小。而且MCP的接口风格已经被工具生态验证过Claude这类支持MCP的工具可以直接配置使用不需要额外开发对接层。如果项目栈里已经用了MCP记忆模块直接接标准服务能省下不少维护成本。4.2 自己写一个轻量记忆服务的示例当然如果你想完全掌控记忆逻辑或者只是做个快速验证自己写一个轻量记忆服务也不难。下面这个示例用Python实现了一个最基础的记忆类结构化偏好存SQLite语义记忆通过embedding向量检索。没有依赖重型框架跑通整个链路不超过半小时。import sqlite3 import numpy as np class MemoryStore: def __init__(self, db_pathmemory.db): self.conn sqlite3.connect(db_path) self.conn.execute( CREATE TABLE IF NOT EXISTS facts ( user_id TEXT, content TEXT, kind TEXT, created_at TEXT ) ) self.conn.execute( CREATE TABLE IF NOT EXISTS vectors ( user_id TEXT, content TEXT, embedding BLOB, created_at TEXT ) ) self.conn.commit() def add_fact(self, user_id, content, kindpreference): # 事实型记忆精确字段下次用SQL直接查 self.conn.execute( INSERT INTO facts (user_id, content, kind, created_at) VALUES (?,?,?,datetime(now)), (user_id, content, kind) ) self.conn.commit() def get_facts(self, user_id): # 召回结构化偏好 cursor self.conn.execute( SELECT content, kind FROM facts WHERE user_id? ORDER BY created_at DESC LIMIT 20, (user_id,) ) return cursor.fetchall() def add_vector(self, user_id, content, embedding): # 语义记忆存向量后续按相似度召回 self.conn.execute( INSERT INTO vectors (user_id, content, embedding, created_at) VALUES (?,?,?,datetime(now)), (user_id, content, embedding.tobytes()) ) self.conn.commit() def search_similar(self, user_id, query_embedding, top_k5): cursor self.conn.execute( SELECT content, embedding FROM vectors WHERE user_id?, (user_id,) ) rows cursor.fetchall() scored [] for content, blob in rows: vec np.frombuffer(blob, dtypenp.float32) score np.dot(vec, query_embedding) / (np.linalg.norm(vec) * np.linalg.norm(query_embedding) 1e-9) scored.append((score, content)) scored.sort(reverseTrue) return [content for _, content in scored[:top_k]]实际使用的时候embedding我建议先接一个本地模型比如常见的bge系列或text2vec系列短文本效果不错还不花线上费用。测试阶段不建议直接上大厂的在线embedding接口因为调试期间反复调用账单容易超预期。等逻辑都跑通了再根据效果决定是否换更强的在线模型。这里补充一句上面的SQLite方案适合单机和低并发场景。如果记忆服务要承接多用户、高并发建议换成带向量检索的PostgreSQLpgvector扩展接口设计原则不变只是存储引擎更抗造。4.3 在LangGraph中接入记忆的节点设计如果你的Agent是基于LangGraph这类图框架搭建的记忆模块的接入位置基本可以固定下来。LangGraph的思想是把Agent流程拆成节点节点之间用条件边连接在合适的位置插入记忆读写整个流程会非常清晰。我的设计一般是三个节点会话开始节点负责召回记忆从MemoryStore里取出该用户的偏好和向量记忆拼进系统提示词主处理节点负责调用模型进行回复会话结束节点负责提炼并写入新记忆。节点之间用状态对象传递数据记忆读写通过一个memory_client完成。写入节点有个容易踩的坑不要在每一轮对话结束都触发写记忆。用户随便说一句今天天气不错没必要提炼成长期记忆。我会加一个条件判断只有当对话中出现了新的用户明确信息比如以后都用表格给我回报或者完成了一次有结论的任务才触发记忆写入。这样做既省钱又避免记忆库被无关信息污染。5. 常见问题与排查经验实录5.1 召不回检索结果不准怎么办记忆系统最常见的故障就是该想起来的想不起来。排查下来原因一般集中在这几处第一embedding模型选得太弱短句语义表示能力不够导致检索结果和query不沾边第二topK设置太小相关记忆没被捞出来第三库里存的语义记忆本身太模糊比如存了用户喜欢户外实际需要的记忆是用户计划购买露营帐篷。我的排查顺序是先看存储端有没有正确写入再单独测embedding效果最后看召回时的topK和过滤条件。如果单靠向量检索效果不好建议上混合检索就是把关键词精确匹配和向量语义召回结合。比如用一个小工具先按关键词把候选集缩小再在候选集里做向量排序召回准确率能明显提升。这也是很多生产级检索系统的基本操作不要迷信单一算法。5.2 记错人多用户玩混了记忆这个问题在早期原型里特别常见因为初版代码经常只测单用户没考虑多租户隔离。一旦多个用户共用同一个Agent外壳又忘了记忆表带user_id过滤轻则用户A看到用户B的偏好重则敏感信息串台。我的建议是从设计的第一天就强制所有记忆操作带上namespace或user_id参数不管是结构化表还是向量表查询时一律按用户维度过滤。这个约束要在存储层做死不能只靠应用层注意。另外写代码的时候把用户ID的注入放在统一入口处理不要让每个节点各传各的否则后期维护很容易漏。记忆串台的故障测试阶段几乎测不出来只有真实多用户跑一段时间才会爆发所以必须防患于未然。5.3 成本爆表Embedding与Token怎么控记忆系统会额外产生两笔费用一笔是提炼记忆时的LLM调用费用一笔是生成embedding向量的接口费用。项目上线初期量不大感觉不到但日活上来之后每轮对话都加一次提炼、一次embedding成本会跑得很快。减排手段我用了三个第一只在有信息增量时才触发提炼用规则初筛后再调LLM第二embedding尽量本地化短文本用一个开源embedding模型就能覆盖多数场景第三如果用的是API型embedding可以考虑异步批处理把同一用户的多个短文本合并成一个请求提交。实测下来这三招能把记忆系统的额外成本压到原先的三分之一以下体感上Agent的记忆能力也没明显缩水。5.4 隐私与合规哪些记忆不该存做记忆系统越久越觉得克制是最高优先级。不是所有聊过的内容都值得记住特别是涉及用户敏感信息的对话。我的原则是身份识别信息、财物信息、健康信息默认不写入长期记忆用户表示不用记这次不往心里去的内容直接跳过写入流程。产品层面我会给用户提供查看我记住了什么和清除我的记忆两个入口。这不仅是合规要求也是产品信任感的关键。用户愿意让Agent记住自己前提是他知道Agent记住了什么、能一键清空。没有这层兜底再聪明的记忆系统也只是一颗定时炸弹一旦涉及隐私争议之前建立的信任会瞬间归零。我在实际使用中还有一个感受记忆系统不是越复杂越好反而是少而准最难得。你不需要把用户所有的碎片信息都存下来只需要保存那些真正影响后续交互的关键事实加上最近一段时间的上下文摘要就已经能带来很明显的体验提升。真正做过一轮完整记忆系统再回头看我的Agent总失忆这个问题很多时候不是模型不行而是我们还没教会它在合适的时候想起来、在合适的时候忘掉。这也正是Agent从玩具走向生产力工具的关键一步。
延伸阅读

更多相关文章

2026/9/13 6:07:21

齐次性本质:尺度不变性与数学建模的底层逻辑

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/9/13 6:07:21

基于GPS轨迹数据的信号灯周期估计:从清洗到频谱分析

简介:2024年华中杯数学建模竞赛B题完整解决方案包,聚焦行车轨迹估计交通信号灯周期问题,面向参赛队伍及建模学习者,提供源代码、论文与配套文档。资源共95个文件,总大小15.66MB,以MATLAB脚本/函数为主&…

2026/9/13 6:07:21

股票投资入门:核心概念与量化交易基础

1. 股票投资入门基础股票市场对许多人来说既充满诱惑又令人畏惧。作为量化交易的基础,理解股票的本质和运行机制至关重要。我刚开始接触股票时也是一头雾水,直到系统学习了这些基础知识才真正入门。股票本质上代表了对一家公司的所有权份额。当你购买某公…

2026/9/13 7:22:24

STM32 HAL库定时器实战:从时钟树到PWM精准控制

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/9/13 7:22:24

AI智能体用户记忆系统:双层架构与工程落地实践

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/9/13 7:22:24

Ax+By+C=0的几何本质:A、B、C如何定义直线的方向与位置

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/9/13 7:22:24

ADK 本地授权码认证全栈演示:authn-adk-all-in-one 实战指南

ADK 本地授权码认证全栈演示:authn-adk-all-in-one 实战指南 【免费下载链接】adk-python An open-source, code-first Python toolkit for building, evaluating, and deploying sophisticated AI agents with flexibility and control. 项目地址: https://gitco…

2026/9/13 7:22:24

Java Web框架性能横评:Spring Boot、Quarkus与轻量级框架对比

1. Java Web框架的江湖地位与选型困境在Java生态圈里,Web框架的演进就像一场没有终点的马拉松。从早期的Struts到后来的Spring MVC,再到如今百花齐放的微服务框架,每个时代都有其标志性的技术选择。作为从业15年的老Javaer,我见证…

2026/9/13 7:17:24

AI Agent跨会话记忆系统架构设计与实战

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/9/13 0:01:16

拯救者Y7000黑屏故障排查与维修实战指南

1. 项目概述:一台黑屏的拯救者Y7000,到底卡在哪一步? 联想拯救者Y7000系列笔记本,从2018年第一代搭载i5-8300H开始,到后来的i7-9750H、i7-10750H、i5-11400H,再到2023年款的R7-7840HS,它始终是学…

2026/9/13 0:01:16

拯救者Y7000黑屏故障排查与维修实战指南

1. 项目概述:一台黑屏的拯救者Y7000,到底卡在哪一步? 联想拯救者Y7000系列笔记本,从2018年第一代搭载i5-8300H开始,到后来的i7-9750H、i7-10750H、i5-11400H,再到2023年款的R7-7840HS,它始终是学…

2026/9/12 6:29:36

USB Type-C PCB布局分区设计:电源、高速信号与PD协议全攻略

做硬件这行,Type-C接口算是典型的“看着简单,做起来全坑”的东西。光引脚就24个,高低速信号、电源、控制线全部塞在一个小小的连接器里,如果PCB布局不做规划,打样回来基本就是“插上没反应”、“高速掉线”、“静电一打…

2026/9/12 14:32:17

系统编程学习原型如何补齐稳定性边界

系统编程学习原型如何补齐稳定性边界预算有限时&#xff0c;我先优化明显多余的复制&#xff0c;而不是猜测性地换容器。用借用传递只读数据通常就能减少分配&#xff1a; fn parse(line: &str) -> Result<Item, Error> { /* ... */ }用基准确认热点确实在分配&am…

2026/9/12 6:37:43

雨花区哪家财务公司代理记账比较好?

在雨花区&#xff0c;企业处理财税事务常常面临诸多挑战&#xff0c;选择一家靠谱的财务公司至关重要。湖南巨勤财务管理咨询有限公司就是本地正规实体财税服务机构&#xff0c;深耕本地工商财税行业多年&#xff0c;熟悉当地工商局、税务局最新政策与申报流程。主营公司注册、…

还想了解更多?直接咨询顾问

免费诊断 + 免费方案 + 透明报价。

全国咨询热线400-8866-253
免费获取方案
咨询二维码