尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

Agent Memory实战:用Redis构建大模型记忆层

Agent Memory实战:用Redis构建大模型记忆层 如果你刷到过「一周搞定 Agent 应用」「全网最好的 Agent Memory 教程」这类标题先别急着收藏。做过 Agent 开发的人往往会告诉你两个现实第一Agent 的难点从来不是调通一个模型接口而是让它在多轮交互中保持稳定第二真正让 Agent 从玩具变成能用产品的东西不是花哨的工具链而是记忆层。Agent Memory简单理解就是让 Agent 记住用户偏好、对话历史、任务状态的能力。它不是某个独立数据库也不是往 Prompt 里硬塞两段历史记录。它要解决的核心问题是把大模型无状态的推理过程变成有状态的连续服务。用更直白的话说没有记忆的 Agent 是搜索引擎有记忆的 Agent 才是助手。这篇文章不打算复制任何「精品教程」的标题党套路。我按实际做项目时的顺序把 Agent Memory 的分类、存储、修改、检索、遗忘这条链路拆一遍然后用 Redis 写一个最小可用的记忆服务再落到电商场景的代码实战上。最后给出一条一周可以走完的学习路径。少走弯路这件事靠的不是收藏夹是把每一层原理都搞清楚。1. 为什么 Agent 总是“记不住”问题到底出在哪1.1 先从一个让人抓狂的对话场景说起假设你是一个电商平台的购物助手用户连续输入了三轮消息用户我之前在你们店买过一件 M 码的白色卫衣想再买一件黑色的。Agent您好请问您需要什么尺码呢用户M 码我刚才说了。Agent好的请问您之前购买过我们的商品吗走到这一步用户体验已经崩了。用户要的不是一个「每次都从零开始」的客服而是一个能记住“我上周买过什么、我穿什么尺码、我偏好什么风格”的助手。这种问题在真实项目里非常常见。很多团队把 Agent 接入微信、App、网页客服之后第一轮测试发现效果不错但一旦用户连续追问、隔几天再来模型就像失忆一样连自己上一句说过什么都忘了。问题不在模型能力而在记忆层没设计。1.2 无状态模型和有状态服务的本质区别大语言模型本身是无状态的。每一次调用模型接口模型都只根据当前请求里的 Prompt 生成回复它不会记得上一次调用里你说了什么。所谓的「上下文」只是你在这一次请求里把之前的内容再次塞进 Prompt 而已。这里有一个容易混淆的点上下文窗口不是记忆。上下文窗口更像是一张「临时便签纸」只在本次请求期间存在请求结束就消失。如果你不主动保存A 轮对话的信息到了 B 轮就无影无踪。所以要让 Agent 拥有记忆必须在模型外部做一层「有状态」的存储再通过某种方式把存储中的内容重新注入到 Prompt 里。这个外部存储以及围绕它展开的读写改删逻辑就是 Agent Memory 的核心。1.3 记忆不是缓存库而是产品体验的分界线我做项目时有个很深的体感记忆层设计得好不好直接决定了 Agent 是「演示品」还是「产品」。演示品阶段Agent 只需要回答单轮问题用户问什么答什么不需要任何个性化。可一旦进入真实业务用户会有历史、有偏好、有当前正在进行的任务。如果一个购物助手不知道用户喜欢什么风格不知道用户上次买到什么程度那它就只是个稍微聪明一点的搜索框。所以我的主判断是Agent Memory 真正解决的不是把对话内容存下来而是让 Agent 在每次交互时都能带着正确的上下文做出连续决策。这背后涉及记忆的分类、写入时机、更新策略、检索方式和遗忘机制每一层都值得单独设计。2. 先把记忆分好类再谈怎么存2.1 短期记忆别把上下文窗口当成记忆短期记忆通常指一次会话内部的上下文。最朴素的做法是把对话历史直接拼进 Prompt。但这里有个容易被忽略的问题上下文窗口有长度限制而且 token 越多调用成本和响应延迟都会上升。所以短期记忆也要做管理而不是无脑堆积。常见手段包括只保留最近 N 轮对话把太早的对话做摘要用一段总结代替长历史对超长内容做截断。在工程上短期记忆一般用列表结构存储按时间顺序追加超过阈值就做裁剪或摘要。2.2 长期记忆存储与检索才是核心长期记忆用来跨会话保存用户的核心信息比如用户画像、历史订单、购买偏好、地址、尺码、过敏信息等。它必须存在外部存储里不能依赖上下文窗口。实现长期记忆时选择什么存储要看数据类型结构化偏好尺码、风格、颜色、地址适合用 Redis Hash 或关系型数据库非结构化文本用户某次投诉、某段评价适合用向量数据库做语义检索时间序列事件浏览记录、点击记录适合用带时间戳的列表或时序存储。长期记忆的难点不在「能存」而在「什么时候写、什么时候更新、怎么检索出最相关的那一部分」。2.3 工作记忆、语义记忆和情景记忆的工程含义除了短期和长期Agent 领域还会提到几类更细的记忆记忆类型生命周期典型实现典型问题短期记忆单次会话内对话历史、上下文窗口超长截断、 token 成本上升长期记忆跨会话Redis、数据库、向量库写入时机、冲突覆盖工作记忆当前任务期间状态对象、购物车、任务栈状态同步、异常恢复语义记忆长期稳定知识库、RAG 检索知识过时、检索相关性情景记忆长期积累事件流水、用户行为日志数据量大、噪声多工作记忆值得多说一句。比如电商场景里用户正在把商品加入购物车、填写地址、选择支付方式这一系列中间状态就是工作记忆。它和长期记忆不一样用户任务一旦完成或取消这些临时状态就应该被清理。很多 Agent 在任务中途出问题就是因为工作记忆没有单独管理把临时状态和长期画像混在一起存结果越存越乱。3. 用 Redis 做记忆层的读写改最小可运行方案3.1 为什么常用 Redis 做记忆中间层在 Agent 记忆的实践里Redis 是常见的一层。原因并不复杂读写快适合频繁的对话读写数据结构丰富Hash 适合用户画像List 适合对话历史ZSet 适合带时间权重的记忆自带 TTL 过期机制天然支持「临时会话过期」启动成本低适合把原型先跑起来。但 Redis 不是银弹。它是基于内存的存储虽然可以开启 AOF 或 RDB 持久化仍然需要做容量规划和备份。生产环境如果只靠 Redis 存大量长期记忆内存会吃紧这时候通常要配合数据库、对象存储或向量库做分层。3.2 数据模型设计按用户和会话组织设计 Redis 的 key 时建议用统一的命名前缀避免和其他业务数据冲突。一个最小可用的结构可以是agent:memory:user:{user_id}:profileHash存放用户稳定画像字段例如 size、style、coloragent:memory:user:{user_id}:session:{session_id}List存放一次会话内的消息记录agent:memory:user:{user_id}:cartHash 或 JSON 字符串存放当前购物车状态agent:memory:user:{user_id}:eventsZSet存放带时间戳的行为事件可以用来做「最近看过什么」这类推荐。我不建议把所有记忆塞进一个 String key 里存一整段 JSON。原因很简单整段覆盖更新很容易产生并发冲突。你读出来一段 JSON改了其中一个字段写回去的时候另一个请求可能已经改了另外的字段结果被覆盖掉。用 Hash 按字段更新会安全很多。3.3 核心操作代码写入、更新、删除下面用 Python 的 redis-py 写一个最小可运行的记忆服务。这套代码是常见写法适合先跑通流程再按自己的场景调整。import json import time import redis r redis.Redis(host127.0.0.1, port6379, db0, decode_responsesTrue) def save_pref(user_id: str, key: str, value: str) - None: 写入或更新一条用户偏好。字段级写入避免整段覆盖。 r.hset(fagent:memory:user:{user_id}:profile, key, value) def get_pref(user_id: str, key: str): 读取单个偏好字段。 return r.hget(fagent:memory:user:{user_id}:profile, key) def get_all_prefs(user_id: str) - dict: 读取用户全部画像字段。 return r.hgetall(fagent:memory:user:{user_id}:profile) def remove_pref(user_id: str, key: str) - int: 删除一个偏好字段。 return r.hdel(fagent:memory:user:{user_id}:profile, key) def append_message(user_id: str, session_id: str, role: str, content: str) - None: 追加一条会话消息并只保留最近 50 条。 key fagent:memory:user:{user_id}:session:{session_id} msg {role: role, content: content, ts: int(time.time())} r.rpush(key, json.dumps(msg, ensure_asciiFalse)) r.ltrim(key, -50, -1) def recent_messages(user_id: str, session_id: str, limit: int 10) - list: 取出最近若干条消息。 key fagent:memory:user:{user_id}:session:{session_id} items r.lrange(key, -limit, -1) return [json.loads(item) for item in items]这里有几个细节需要解释。第一append_message里用了ltrim目的是防止会话列表无限膨胀。如果对话特别长更合理的方式是在写入前判断长度把最早的若干条压缩成摘要再保存。第二save_pref是「存在即覆盖」的语义。用户第一次说喜欢黑色第二次说其实是深蓝色第二次写入应该直接覆盖而不是同时保留两条矛盾记录。第三写会话记录时role字段建议统一为user和assistant方便后续构造 Prompt 时直接映射。3.4 过期策略、序列化与并发覆盖问题使用 Redis 做记忆层最容易踩的坑有三个。第一个坑会话记录永远不会过期。如果你给会话 key 设置了 TTL比如 24 小时那么用户隔三天再来历史记录应该被清理。如果完全不设置过期Redis 内存会被历史记录一点点吃掉。常见的做法是def set_session_ttl(user_id: str, session_id: str, ttl: int 86400) - None: key fagent:memory:user:{user_id}:session:{session_id} r.expire(key, ttl)第二个坑JSON 序列化没有处理中文。写入消息时用ensure_asciiFalse否则读出来的是\u59d3\u540d这样的转义串既不直观也不利于日志排查。第三个坑同时写入同一个用户画像字段。比如用户在一轮对话里纠正了尺码另一个后台任务也在更新他的积分偏好两个请求可能互相覆盖。这个问题在小规模 demo 里不明显但一旦上线至少要做到「最后一次写入生效」再把更新时间记录下来。更严格的话可以用 Redis 的 WATCH 或 Lua 脚本做原子更新。注意不要一上来就把批量数和并发数拉满先用一条样例确认输入、输出和日志都正常再逐步扩大并发。4. 电商案例实战从需求拆解到代码落地4.1 先拆需求该记住什么、忘掉什么、何时更新电商购物助手是最典型的 Agent Memory 应用场景。但在写代码之前先拆清楚需求。需要记住的信息大致分四类用户画像尺码、颜色偏好、风格偏好、肤质、常用地址会话历史这一轮对话聊过什么用户是否已经选了商品工作状态购物车里的商品、当前填到哪一步、是否已经下单行为流水最近浏览过哪些商品、点击过哪些分类。需要「忘掉」的信息也同样重要过期的促销信息用户明确要求删除的个人信息已经完成的订单的临时中间状态错误推断的偏好。更新时机方面最重要的一个原则是用户的显式纠正是最高优先级的更新信号。用户说“我穿 L 码不是 M 码”Agent 必须立刻更新画像中的尺码字段而不是等下一次再纠正。4.2 一个最小可用的记忆服务模块把上一节的函数封装成一个类会更适合实际项目使用。class MemoryService: def __init__(self, redis_client): self.r redis_client def _profile_key(self, user_id: str) - str: return fagent:memory:user:{user_id}:profile def _session_key(self, user_id: str, session_id: str) - str: return fagent:memory:user:{user_id}:session:{session_id} def update_user_pref(self, user_id: str, updates: dict) - None: 批量更新用户画像字段级写入。 if not updates: return self.r.hset(self._profile_key(user_id), mappingupdates) self.r.hset(self._profile_key(user_id), _updated_at, int(time.time())) def get_user_profile(self, user_id: str) - dict: 读取用户画像过滤内部字段。 prefs self.r.hgetall(self._profile_key(user_id)) return {k: v for k, v in prefs.items() if not k.startswith(_)} def add_message(self, user_id: str, session_id: str, role: str, content: str) - None: key self._session_key(user_id, session_id) msg {role: role, content: content, ts: int(time.time())} self.r.rpush(key, json.dumps(msg, ensure_asciiFalse)) self.r.ltrim(key, -60, -1) def get_recent_context(self, user_id: str, session_id: str, limit: int 12) - list: items self.r.lrange(self._session_key(user_id, session_id), -limit, -1) return [json.loads(x) for x in items]这个模块足够支撑一个单用户、单会话的电商助手原型。它的核心价值不是代码量而是把「写入」「更新」「读取」三个动作收敛到统一的接口里避免业务代码里到处散落r.hset和r.rpush。4.3 把记忆注入 Prompt 的正确姿势有了记忆模块接下来要考虑怎么把记忆注入 Prompt。注入顺序通常是这样System Prompt放系统指令说明角色、业务规则、不允许编造信息用户画像放用户已知偏好按「字段值」拼接最近对话放最近的消息记录保持角色交替当前问题放用户最新的输入。下面是一个构造 Prompt 的示例函数def build_assistant_prompt(user_id: str, session_id: str, memory: MemoryService) - str: profile memory.get_user_profile(user_id) history memory.get_recent_context(user_id, session_id) profile_text .join(f{k}{v} for k, v in profile.items()) history_text \n.join( f{msg[role]}: {msg[content]} for msg in history ) return f 你是某电商平台的购物助手。请基于用户的已知画像和最近对话提供有连续性的回答。 【用户画像】 {profile_text or 暂无} 【最近对话】 {history_text or 暂无} 回答要求 1. 回答尺码、风格、适用肤质等问题时优先使用用户画像中的信息 2. 如果用户明确指出之前的信息有误先向用户确认再记录新信息 3. 不要编造用户画像里不存在的信息。 .strip()注入记忆时有一个常见误区把能查到的所有记忆全部塞进 Prompt。这样做短期看起来方便实际上会引入两个问题一是 token 成本上升二是无关信息干扰模型判断。正确思路是先过滤再注入。小项目可以用「最近 N 条 画像字段」信息量大之后就要引入检索只把与用户当前问题相关的记忆挑出来。4.4 单任务验证与批量场景扩展框架搭好之后先做单任务验证。最应该验证的用例是用户 A 第一轮说“我喜欢黑色穿 M 码”结束当前对话第二轮用户 A 问“推荐一件卫衣”Agent 是否主动提到黑色和 M 码。这个用例能覆盖写入、存储、读取、注入四个环节。如果能通过再把范围扩展到批量多个用户并发写入画像是否会串一个用户多个会话最近对话是否会隔离用户纠正偏好后老信息是否会被覆盖会话超过 50 条后早期信息是否丢失或者被正确摘要。这些测试不需要自动化框架手动写几个脚本就能验证。关键是不要跳过尤其是并发串号问题如果 key 里没有带 user_id几乎所有并发测试都会炸。5. Agent 的个人应用场景不只是客服机器人5.1 个人知识助理除了电商客服Agent Memory 在个人场景里同样有很高的价值。一个典型的用法是个人知识助理它可以记住你收藏的文章、你正在读的书、你记录过的项目笔记。比如你每周都让 Agent 帮你整理一份技术周报Agent 需要记住你关注的技术领域、你上次整理到哪一期、你已经收藏过哪些链接。这些信息如果每次都要重新告诉它效率会非常低。有了长期记忆它才能真正承担「助理」的角色。5.2 日程、学习与健康类跟踪另一个常见场景是个人计划跟踪。学习助手可以记住你学到第几课、上次的疑问是什么、下次该复习哪些知识点日程助手可以记住你每周例会的时间、你对会议时间的偏好健身或健康记录类应用可以记住你的训练计划、身体数据变化。这类场景和电商相比数据结构更简单通常一个用户对应一条或多条记录但隐私要求更高。健康数据、日程数据都非常敏感做这类 Agent 时至少要把记忆数据加密存储并且提供一键清除功能。5.3 个人场景和商业场景的取舍差异个人场景和商业场景对记忆层的工程要求差别很大。个人场景的特点是单用户、低并发、数据量小。此时用 Redis 加一个简单记忆模块完全够用不需要上复杂的向量检索和微服务。核心要关注的是数据备份和隐私。商业场景的特点是多用户、高并发、多角色、有审计需求。此时需要考虑用户的显式授权、记忆删除的合规流程、记忆写入的并发控制、不同业务线之间的数据隔离。直接照搬个人项目的记忆模块会出问题。建议先把个人场景跑通再把同一个记忆模块接到商业场景时一定要补上权限、审计和删除机制而不是只加一个 Redis 集群就上线。6. 从原型到生产还差几块关键拼图6.1 记忆冲突与版本管理记忆一定会遇到冲突。用户上一轮说“我喜欢简洁风格”下一轮说“其实我更喜欢复古风”。对 Agent 来说这不是「存两条」而是必须决定以哪条为准。我的处理习惯是默认最后一条明确写入的数据覆盖旧数据并把修改时间记录下来。这样既能满足大多数场景也方便追溯。如果你要做得更严谨可以给记忆字段加版本号写入时通过 CAS比较并交换或 Lua 脚本保证不会用旧值覆盖新值。但对大多数业务来说简单的 last-write-wins 加时间戳已经足够过度设计反而让代码难维护。6.2 隐私、权限与遗忘机制这是生产环境最容易忽略也最容易出事的地方。首先不要明文存储密码、支付卡号、身份证明这类敏感字段。即使只是做 demo也要养成不落盘的习惯。其次要让用户有「遗忘权」。产品里至少提供「清空我的记忆」入口对应到存储层就是删除该用户所有 key。再次权限要收敛。记忆服务不应该开放任意读写接口内部调用也应该校验调用方身份。最简单的做法是把记忆操作封装成独立服务只暴露白名单接口。6.3 可观测性与调试记忆层的调试问题比模型调试更隐蔽。模型输出错了可以看日志但如果 Agent 没用到记忆问题可能出在「记忆没写入」或「写入但没检索到」或「检索到但 Prompt 里被截断」每一层都要有日志才能定位。建议至少记录四类信息写入日志谁在什么时间写了哪个字段更新日志旧值和新值分别是什么读取日志本次查询检索了哪些记忆、命中了什么Prompt 日志最终发给模型的 Prompt 内容。有了这四类日志用户反馈“Agent 又忘了”的时候你才能判断是存储断了还是检索没命中还是 Prompt 拼接写错了。没有日志就去猜往往要花好几倍时间。6.4 成本控制与检索质量记忆不是存得越多越好。每多注入一段记忆就多消耗 token。一个用户如果积累了几百条画像字段你不可能全部塞进 Prompt。常见做法是分层第一层稳定画像每次必带控制在几百 token 内第二层最近对话带最近 N 条第三层历史记忆按需检索只有当前问题相关才注入。第三层可以通过关键词过滤配合向量检索实现。如果记忆文本量不大直接用 Redis 的全文能力或简单的包含关系匹配就够了。记忆量上去之后再考虑向量库按语义相似度取 top-k。检索质量还涉及过时信息。用户半年不买衣服尺码偏好可能已经失效。过时记忆比没有记忆更危险因为它会让 Agent 自信地给出错误建议。所以长期记忆要么带时间戳要么定期让用户确认要么在业务规则里设置有效期。7. 记忆不生效时按什么顺序排查7.1 先看现象记忆不生效通常表现为四种现象Agent 完全不记得用户信息Agent 记得但用的是旧信息Agent 记错了把 A 用户的记忆用在 B 用户身上Agent 编造出记忆里不存在的字段。现象不同排查方向差别很大。不要一上来就去改 Prompt先定位是哪一层出了问题。7.2 再查存储层如果是记忆完全没生效优先查存储层。先用 redis-cli 看 key 是否存在redis-cli keys agent:memory:*然后检查写入时 user_id 和读取时 user_id 是否一致key 是否设置了 TTL并且已经过期写入的是字符串读取时却当成 JSON 解析多个环境连的是不是同一个 Redis 实例序列化编码是否统一中文是否乱码。这层排查成本最低也最容易定位。多数「记忆没生效」的问题最后都发现是 key 拼错了或者连错了库。7.3 再查检索与注入层如果存储层正常记忆也写进去了但 Agent 表现还是像失忆下一步查检索与注入层。最容易出问题的地方有三个一是lrange的取值范围不对。比如你只取了最后 5 条消息而用户的关键信息在第 8 条那自然读不到。二是 Prompt 太长被截断。模型上下文有限如果前面的系统指令和历史记录占了太多位置用户画像可能被截掉。三是记忆注入的位置不对。有些框架会在内部先拼好系统 Prompt你再追加一段记忆可能被覆盖或忽略。这时候要把最终发给模型的完整 Prompt 打印出来确认记忆内容是否真的在里面。7.4 最后看工具与框架边界如果存储、检索、注入都正常那就要考虑框架和工具本身的限制。一些现成的 Agent 框架内置了自己的记忆模块可能不会调用你自定义的存储逻辑。你需要关闭默认记忆或者把自定义记忆接到框架的钩子上。另外有些产品在模型前面加了缓存层同一个问题可能直接命中缓存根本没有触发新的检索逻辑。此时不是记忆写错了而是缓存把新旧结果挡住了。我把排查方向整理成一个表方便对照现象优先排查方向常见原因记忆完全没生效存储层user_id 不一致、key 过期、写入函数没被调用记忆写了但没用上检索与注入层读取范围太窄、Prompt 被截断、注入位置不对记忆内容是旧值更新链路用户最新纠错没走更新逻辑、并发覆盖记忆张冠李戴存储层key 少了 user_id、缓存串号Agent 编造记忆注入与校验把不存在的字段当事实、缺少校验指令越用越慢、越贵记忆长度未截断、未摘要、注入过多8. 一周学习路径从跑通一个 Agent 到自己动手做8.1 分阶段安排「一周搞定 Agent 应用」这类说法多少带点标题党成分但一周时间确实足够把最小闭环跑通。关键在于分阶段控制范围不要一上来就碰 Agent 框架、向量库和复杂编排。我建议按下面的节奏安排第 1 天理解大模型的无状态特性用 Prompt 模拟「单次对话内的记忆」第 2 天装好 Redis用 Python 写入、读取、删除一条会话记录第 3 天实现用户画像的写入、更新、删除理解 Hash 和 List 的区别第 4 天做电商助手的最小闭环把画像和历史对话注入 Prompt跑通单用户流程第 5 天处理边界包括用户纠错、过期策略、记忆冲突、长对话摘要第 6 天封装一个简单的 Agent 循环加入工具调用比如查订单、查库存第 7 天加日志、做并发测试、写清边界文档整理成一篇可复现的经验记录。这套安排的核心思路是先跑通再优化最后工程化。如果你已经熟悉 Python 和 Redis前三天可以压缩到一天半如果完全没接触过 Redis就不要跳过第 2 天直接去调框架大概率会卡在数据层。8.2 适用边界与我的建议最后说清楚一件事Agent Memory 是很关键的一层但它不是 Agent 应用的全部。一个可用的 Agent 还需要任务规划、工具调用、结果验证、异常处理、日志和评估体系。记忆层解决的是「连续性」问题解决不了「模型能力不足」的问题。还有一点容易被低估记忆不是为了存而存。能稳定记住正确的事情并且在该忘掉的时候忘掉这才是记忆层真正成熟的标准。你可以在评估 Agent 时专门加一类用例用户纠正信息之后Agent 是否在新一轮对话里立刻用上新值而不是继续沿用旧值。这一条能过滤掉很多看起来能跑、实际不可用的实现。如果看完这篇文章只记住一句话我希望是Agent Memory 的难点不在存储技术而在「什么该记住、什么该更新、什么该忘掉、什么该被检索出来」这一整套判断逻辑。把这条链路想清楚了再上手 Redis、向量库和框架你会少走很多弯路。
返回列表