做个简单实验:你让Agent帮你记住一件事,比如“以后周报直接发钉钉群”,Agent当时答应得好好的,你一顿操作猛如虎,然后重启进程,再问它“周报怎么发”,大概率换来一句“我不太确定您指的是什么”。这就是很多Agent项目从demo走向生产时绕不过去的坎——没有持久化,Agent就是一条金鱼,看着聪明,转头就忘。
这个问题在圈子里有个很形象的比喻:“金鱼记忆”。而真正要解决它,不是靠给模型加提示词,也不是靠调大上下文窗口,而是要给Agent补上两样东西:持久化和缓存。持久化解决“重启后还记得什么”,缓存解决“高频访问时还能跑多快”,两者合在一起,才是Agent真正能长时间稳定干活的数据底座。
这篇文章我会结合我自己在Agent开发里踩过的坑,把持久化和缓存这件事拆开讲清楚:Agent的记忆到底分几层、底层该存什么、Redis在系统里到底扮演什么角色、缓存和数据库到底怎么保持一致性,最后给出一套可以直接抄作业的设计方案。内容不追求最前沿,只求能落地。
1. 为什么要给Agent做数据底座:先看懂三层记忆模型
1.1 工作记忆、短期缓存与长期存储的差别
很多刚接触Agent开发的同学会混淆一个概念:以为大模型自己就带记忆。实际上,大模型只有“上下文窗口”,窗口一关,什么都没了。而且就算在窗口里,上下文长度也有限制,内容一多就开始“忘前边的”,这是模型的物理局限。
我在做Agent系统时,习惯把记忆分成三层,参考的就是人脑的机制:
- 工作记忆:就是当前这轮对话里,模型上下文窗口中的内容,相当于你手头正在处理的事。这层数据活在内存里,换一个请求就没了,而且成本高(Token越多,调模型越贵越慢)。
- 短期缓存:比如同一个Agent实例在几小时内的会话状态、临时查询结果、中间步骤输出。这层可以理解为“便利贴”,放在Redis这类高速存储里,过期就丢,目的是避免每次都重复计算。
- 长期存储:用户偏好、历史对话摘要、工具调用结果、任务状态、业务规则。这些必须落到数据库、对象存储或向量库里,进程挂了、服务器重启了、镜像重新部署了,数据都还在。
三层之间不是独立的。长期存储里最常被调用的数据,会提升到缓存层;工作记忆用不完的,总结完沉淀到长期存储;缓存失效了,再回到长期存储里捞。这套机制设计得好不好,直接决定Agent是“聪明但健忘”还是“笨但可靠”。
1.2 数据底座在Agent系统里的真实位置
我画一个我实际项目里的调用链路,你感受一下数据底座是在哪里插进去的:
用户输入进来,先命中一个会话ID,系统从这个会话ID去Redis里取最近几轮的对话记录和状态快照,拼成上下文,发给大模型。模型决定要调用工具,工具返回结果后,系统把这个结果同时做两件事:第一,写入数据库里的“工具调用记录表”;第二,把结果塞回上下文继续推理。一轮结束之后,异步任务把这一轮的对话摘要、关键状态刷新到长期存储,再清理掉过期的缓存Key。
这个过程中,Redis管的是“当前会话跑得快不快”,数据库和向量库管的是“下次还能不能想起来”。二者缺一个,Agent都活不长。
所以你会发现,Agent开发学习路线里,除了模型、提示词、编排框架,还得懂Redis、懂数据库设计、懂缓存一致性。这不是后端同学强行给自己加戏,是Agent应用一旦上生产,这就是绕不开的硬功夫。
2. 持久化设计:让Agent拥有“长期记忆”
2.1 最小可用方案:从SQLite和JSON开始
如果你只是做一个个人项目的Agent,不必一上来就上PostgreSQL加向量数据库,SQLite就是你最好的朋友。一个文件搞定存储,备份就是复制文件,没有任何运维负担。
我最早做的Agent原型就只落了两种数据:会话记录和状态JSON。会话记录存用户每一轮说了什么、Agent回了什么,状态JSON记录Agent内部维护的临时变量,比如“用户所在城市”“当前任务的步骤序号”。代码大概长这样:
CREATE TABLE IF NOT EXISTS conversations ( id TEXT PRIMARY KEY, session_id TEXT NOT NULL, role TEXT NOT NULL, content TEXT NOT NULL, created_at INTEGER NOT NULL ); CREATE TABLE IF NOT EXISTS agent_state ( agent_id TEXT PRIMARY KEY, user_id TEXT NOT NULL, state_json TEXT NOT NULL, updated_at INTEGER NOT NULL ); CREATE TABLE IF NOT EXISTS tool_calls ( id TEXT PRIMARY KEY, session_id TEXT NOT NULL, tool_name TEXT NOT NULL, input TEXT, output TEXT, created_at INTEGER NOT NULL );这个设计看起来简单,但它解决了一个核心问题:Agent的推理状态不再是内存里的临时变量,而是有据可查的持久化数据。进程崩了,恢复时从agent_state里把state_json读出来,接着往下走就完事。
2.2 状态、会话和业务数据要分开存
很多人做持久化的时候容易犯一个错误:把所有的东西都塞进一张大表里,字段叫content或者data,里面是一个超大的JSON。短期demo没问题,数据一多就难受:没法按用户查、没法按时间清理、没法做权限控制、出了问题也没法排查。
我的习惯是把Agent的数据分成四类来建模:
- 用户身份与配置数据:user_id、用户偏好、Agent对该用户的记忆(比如喜欢简洁回答、常用语言)。这些数据变化频率低,放普通关系表里即可。
- 会话与消息数据:每一轮对话的输入输出、中间思考过程、引用的文件。这些数据是流水,量大,但要支持回溯,需要按会话ID建索引。
- 任务与状态数据:Agent正在执行的多步骤任务,现在执行到哪一步,哪些子任务已经完成。这类数据适合存成“任务表+状态字段+上下文快照”。
- 知识记忆数据:Agent从历史交互中提取的经验、用户说过的偏好、外部文档切片。这类数据是需要语义检索的,适合放向量库。
分开建表之后,你会发现每个表的生命周期是完全不同的:消息流水可以定期归档;状态表需要频繁更新但数据量小;知识记忆需要增量写入并做去重。混在一起,后面做缓存和清理会非常痛苦。
2.3 语义记忆:让Agent从“查记录”变成“回想起”
关系型数据库适合存结构化数据,但它有一个天生短板:没法做语义匹配。用户之前说“我喜欢干货多的回复方式”,下次他换了个说法问“你能不能别废话”,你以为这是新需求,其实是同一个意思。要让Agent能“想起来”相关的历史记忆,就得靠向量数据库。
思路是这样的:每当Agent完成一轮有价值的交互,就把用户的关键信息、偏好、结论抽出来,生成一条记忆文本,再用Embedding模型转成向量,存进向量库。下次用户提问时,把当前问题也转成向量,去向量库里做相似度检索,找出Top K条最相关的记忆,拼接到上下文里。
这个方案在实操里有几个很现实的点要提醒:
- Embedding有成本,不要每轮对话都全量生成记忆,要有触发条件(比如用户明确表达偏好、任务完成、发现新实体)。
- 向量检索不是万能的,它适合“模糊回忆”,不适合“精确查询”。你要查“用户上次填的手机号”,应该去关系型数据库查,别用向量库硬搜。
- 记忆要支持“遗忘”。用户说“我之前让你保存的偏好作废了”,你不能光删向量,还要在关系表里标记状态,否则下次又检索出来,等于没删。
2.4 事件快照:让Agent的执行可回放、可恢复
做Agent和做普通接口最大的区别是:Agent执行一个任务往往要跨很多步骤,中间可能调用多个工具、做多次推理。一旦中间崩了,如果只保存最终状态,你根本不知道是哪个步骤出的问题。
所以我在设计持久化的时候,还做了一个日志型的事件表。每执行一个步骤,就记录一条事件:事件类型、输入、输出、耗时、异常信息。这样Agent执行失败的现场是完整的,不光能排查问题,还能在下次恢复时直接从失败的步骤重放,而不是从头再来。
有人可能会说,这不就是日志吗?对,本质上就是日志,但它不是给人看的日志,而是给机器恢复用的“Action Log”。没有这套东西,Agent的“断点续跑”就是一句空话。
3. 缓存设计:给Agent的高速运行装上加速器
3.1 Agent系统里该缓存哪些东西
缓存不是Redis的专利,在Agent系统里首先要想清楚“什么东西值得缓存”。我的经验是三类数据价值最大:
第一类,LLM的响应结果。同一个问题,如果输入完全一样,模型的输出大概率相同,那为什么每次都要跑去调模型、烧Token?把结果按“模型+提示词+参数”的哈希值缓存下来,命中就直接返回,成本能省一半以上。这在圈子里叫“语义缓存”或“提示词缓存”,是降本增效最立竿见影的手段。
第二类,工具调用的结果。比如Agent经常要查天气、查汇率、查库存,外部API不是免费的,也不一定稳定。把按天变化频率低的数据缓存起来,设置一个合理的TTL,既省了外部API的费用,又降低了响应延迟。
第三类,会话状态与上下文快照。多轮对话里,没必要把全部历史消息都发给模型,可以把已经总结过的历史上下文缓存起来,新请求只需要拼增量部分。
这三类缓存一旦设计好了,Agent的整体延迟可以下降得非常明显。我之前一个项目里有台服务要处理大量重复咨询,加了响应缓存之后,模型调用量直接降了接近一半,速度肉眼可见地变快。
3.2 Redis在Agent底座里的三种核心角色
Redis在Agent系统里真的不是“拿来做缓存”这么简单,它至少承担了三个角色:
角色一:高速KV存储。Agent的状态、会话摘要、临时数据放Redis里,读写都是亚毫秒级,比每次查MySQL快一个数量级。配合TTL自动过期,非常适合做短期缓存。
角色二:分布式锁和任务协调。多个Agent实例同时处理同一个任务时,你不能让两个实例同时去改同一个状态。用Redis的SET NX指令做锁,或者用Redlock保证互斥,可以避免并发冲突。Agent系统一旦多实例化,这一步逃不掉。
角色三:消息队列的轻量替代。有些Agent任务是异步的,比如“总结今天的聊天记录”“生成周报草稿”,这些任务可以塞进Redis的List结构里,用BRPOP阻塞消费,实现一个极简任务队列。数据量不大时,完全没必要上Kafka。
Redis本身也支持持久化(RDB快照和AOF日志),但这里要特别说明:Redis持久化是给缓存做恢复兜底的,不能替代业务数据库。缓存的数据丢了可以从源头重建,业务数据库可不能这么玩。
3.3 缓存一致性:先写库还是先删缓存
做缓存一定会遇到缓存和数据库不一致的问题。Agent场景里最常见的例子是:用户修改了偏好设置(比如“以后用英文回复”),你更新了MySQL,但Redis里的旧偏好还在,Agent读到的还是中文,导致“明明改了却不生效”。
业界有几套常见的处理模式,我直接说结论:
- 对于Agent状态这类数据,我推荐Cache Aside模式:读的时候先查缓存,没命中再查数据库并回填;写的时候先更新数据库,再删除缓存对应的Key,而不是去更新缓存。为什么删除而不是更新?因为更新缓存要考虑类型序列化问题,而且要处理并发写,复杂度高,删掉让下次读时回填反而简单稳妥。
- 对于一致性要求较高的数据,比如用户余额、权限信息,光删缓存是不够的,最好在缓存里加一个版本号,读的时候校验版本,版本不一致就回源。或者直接用“延迟双删”:先删缓存,再更新数据库,隔一小段时间再删一次缓存,防止旧数据被并发读回填到缓存里。
- 对于会话摘要这类允许最终一致的数据,别搞复杂的强一致方案,TTL一到自动过期即可。追求强一致性的代价是性能和复杂度,Agent场景里绝大多数数据用最终一致就够了。
我在项目里被这个坑折磨过:用户改完配置,Agent还是老的记忆,排查了半天,最后发现是代码里“先删缓存,再更新数据库”,结果读请求在中间把旧值写回了缓存。改成“先更新数据库,再删缓存”之后,问题立刻消失。
3.4 缓存三兄弟:穿透、击穿、雪崩在Agent场景里的表现
这三个词是缓存领域的经典问题,放在Agent系统里一样存在,而且表现形式更隐蔽。
缓存穿透:Agent收到了一个恶意或者构造的请求,Redis里没数据,数据库里也没有,请求每次都打到数据库上。你以为Agent只是在“思考”,其实数据库已经被打爆了。解决办法是缓存空值加短TTL,或者用布隆过滤器挡住非法请求。
缓存击穿:某个热点Key突然失效,一瞬间大量请求同时打到数据库。在Agent场景里典型的就是某个用户会话的上下文Key过期了,而这时候系统正在并发处理多条消息,全部去数据库捞历史。解决办法是互斥锁,只有一个请求去重建缓存,其他请求等待。
缓存雪崩:大量Key同一时刻集中过期,数据库压力瞬间飙升。解决办法是把TTL做随机化,不要所有Key都设成同一个过期时间。比如设成300秒,实际可以加上一个0到60秒的随机偏移。
这三个问题在普通Web项目里需要专门设计,Agent系统也一样,尤其是当你有多个Agent实例并发处理高吞吐消息时,哪怕只是几十个用户同时用,都可能因为一个Key失效引发连锁反应。
4. 一套能落地的数据底座设计和实操方案
4.1 缓存Key怎么设计、存哪些字段
Redis缓存能否高效,Key命名规范很重要。我的习惯是:用“业务域:对象类型:ID”的格式,冒号做分隔符,方便在Redis客户端里做前缀匹配。
举一个实际的Key设计示例:
agent:{agent_id}:state # Agent当前的状态快照,JSON agent:{agent_id}:session:{sid}:meta # 会话元信息,比如用户ID、创建时间 agent:{agent_id}:session:{sid}:ctx # 已经压缩过的上下文摘要 tool:result:{tool_name}:{hash} # 工具返回结果的缓存 llm:response:{prompt_hash}:{model} # 大模型响应的语义缓存 agent:lock:{agent_id}:{task_id} # 分布式锁,防止并发修改状态Value的格式,我建议能存JSON就存JSON,但是别嵌套太深。会话上下文的缓存里,需要保留“消息的原始内容”还是“只保留摘要”?我的建议是:摘要进缓存,原始内容进数据库。这样缓存的体积小,命中率高,而且就算丢了也能从数据库重建。
4.2 哪些数据必须走持久化、哪些只丢缓存就够
新手最容易犯的错,就是把所有数据一股脑全塞进Redis,想着反正有TTL,过期就清。问题在于,没有数据源头时,缓存过期就意味着永久丢失。
我这里给出一张责任边界表,你可以直接照抄:
| 数据类型 | 存储位置 | 原因 |
|---|---|---|
| 用户身份与偏好 | MySQL/PostgreSQL | 变化少,必须长期保存,还要做权限控制 |
| 对话消息原文 | 数据库或对象存储 | 需要回溯审计,不能因为缓存过期就消失 |
| 会话上下文摘要 | Redis + 数据库双写 | Redis用于快速拼接上下文,数据库用于兜底恢复 |
| 工具调用结果 | Redis为主,特定结果落库 | 短时效数据用缓存,重要结果(如订单、支付结果)必须落库 |
| 临时中间变量 | 仅Redis内存 | 丢了不影响正确性,重算一次就行 |
| 执行事件日志 | 数据库 | 用于回放和排查问题,必须持久化 |
这个表格的价值在于,它逼你在写代码之前先想清楚:这份数据丢了会发生什么。如果丢了会出事故,就必须落库;如果丢了只是多花一次调用,那放缓存就够了。
4.3 从单机到分布式:什么时候引入消息队列和任务队列
很多Agent开发者的项目是从单机脚本开始的,后来用户多了,部署成服务,再后来要支持多实例,发现状态不同步、任务冲突,才开始考虑分布式。
我的经验是:先从单机缓存+数据库做起,别过早引入消息队列和任务队列。绝大多数Agent项目撑不到需要Kafka的程度,你用Redis的List结构做任务队列,搭配几个Worker进程,已经可以扛住不小的流量。
当你发现以下几个信号时,再考虑引入更重的组件:异步任务积压,Redis List消费不过来;需要任务重试、延迟执行、死信队列;多个服务之间需要解耦通信。到那个阶段,可以把任务队列换成RabbitMQ或Kafka,缓存和持久化仍然用原来的方案,只是把消息层的短板换掉而已。
4.4 成本与延迟的双重优化:缓存与持久化怎么配合
Agent系统最烧钱的部分是大模型调用,不是存储。所以数据底座设计的一个核心目标,就是“让模型少干活”。
具体手段:第一,重复查询走缓存,命中就直接返回,不调模型;第二,长对话用“摘要压缩”策略,不是每轮都把所有历史都塞给模型,而是把历史压缩成一两句话的摘要,放在缓存里,新的对话只带摘要加最近几轮消息;第三,周期性任务把结果快照存数据库,下次直接复用快照,而不是重新跑一遍流程。
我在一个客服Agent项目里就是这么干的:用户重复问同一个问题时,直接从缓存返回历史答案,模型调用量下降了接近50%;用户开启新会话时,先用向量库检索历史记忆,只取出相关的几条拼进上下文,而不是把整个历史全盘搬出来。你不用盲目追求最新技术,先把“少调模型、多查缓存、必要时落库”这三点做好,成本就已经优化了一大截。
5. 常见问题与排查技巧实录
5.1 对话一长Agent就“变傻”,怎么办
这不是模型变笨了,而是上下文窗口被无关内容塞满了。你每一轮都把全部历史消息发过去,模型自然会被大量无关信息干扰,导致最近的指令不生效。
我处理这个问题的方法是“分段压缩”:每经过3到5轮对话,就把历史总结成摘要存进Redis,原始消息仍然留在数据库里。新请求拼接上下文时,用“摘要+最近两轮完整消息”的方式。如果用户问起很久之前的事,再单独去数据库或向量库检索详细内容。这么做既控制Token成本,又保留了回忆细节的能力。
5.2 Agent重启后状态丢了,登录态、用户偏好全没了
这是典型的“缓存和持久化边界没划清”的问题。登录态、用户偏好这种数据本来就不该只存在Redis里,你重启服务时Redis数据跟着清空,Agent自然什么都不记得。
排查思路也很简单:先在数据库里手动查一下用户信息和偏好表,看看数据还在不在。如果在,说明是读路径没走数据库,只读了Redis;如果不在,说明写路径就有问题,连数据库都没落。按这个方向去查代码,基本能定位是“哪一层”的问题。修复的方向就是按照第4.2节的责任边界表,把该落库的落库,把该回填缓存的回填。
5.3 用户改了好几遍配置,Agent读到的还是旧值
这个坑我在3.3节里已经详细说过了,核心是缓存一致性的问题。最常见的代码错误是“先删缓存,再更新数据库”,导致并发读请求把旧数据写回了缓存。另一个容易被忽略的原因是:缓存Key设计得不对,更新数据时用的Key和读取时的Key对不上,比如一个用了session_id,一个用了user_id,结果缓存形同虚设。
排查时我会先确认改配置走的是哪条链路,在Redis里查一下对应的Key存的是什么值,再对比数据库的值。如果Redis里的值和数据库不一样,就说明更新链路没有正确失效缓存;如果Redis里压根没有这个Key,说明问题在“读时回填”的逻辑。
5.4 Agent安全:缓存和持久化里的隐私怎么守
Agent会记录用户的大量对话内容,这些数据里有个人信息、交易记录、使用习惯。很多人做了缓存和持久化,却没有做权限控制,结果就是一个越权接口能查到所有用户的对话记录。
我的一点建议:第一,Redis里不要存明文隐私,敏感字段要加密,涉及个人信息的Key要加上用户维度的前缀,做一个用户只能访问自己前缀下的Key;第二,数据库访问层要做用户维度过滤,不能靠应用层自己自觉;第三,Agent的执行事件日志要设置权限和审计,谁在什么时间看了什么数据,都要能追溯。
Agent系统的安全不只是防外部攻击,数据底座的访问控制同样重要。很多Agent项目本身就是奔着“帮用户处理私人事务”去的,数据底座做得不牢,信任就无从谈起。
5.5 常见问题速查表
| 现象 | 可能原因 | 排查顺序 | 解决方案 |
|---|---|---|---|
| Agent重启后忘记用户偏好 | 偏好只存在Redis缓存,未落库 | 查数据库是否有数据 | 偏好落库,缓存只当加速层 |
| 修改配置不生效 | 缓存未失效或缓存Key不一致 | 对比Redis值与数据库值 | 使用“先更库再删缓存”,统一Key规范 |
| 回复变慢,Token暴涨 | 上下文无限增长 | 检查发送给模型的Token数 | 摘要压缩+只带最近几轮消息 |
| 重复问题重复调模型 | 没有结果缓存 | 观察模型调用日志 | 引入LLM响应缓存,按提示词哈希命中 |
| 高并发时数据库被打崩 | 缓存穿透或雪崩 | 看Redis命中率和数据库慢查询 | 缓存空值、TTL随机化、热点Key加锁 |
| 多个实例并发改状态 | 缺少分布式锁 | 检查是否有锁日志 | 用Redis SET NX实现互斥锁 |
| 敏感数据泄露风险 | 缓存和库表无权限隔离 | 检查接口和存储层代码 | 加用户维度权限校验,敏感字段加密 |
结尾:一点个人经验
做Agent开发这几年,我最大的体会是:模型的能力只是下限,数据底座决定上限。你有一个再聪明的Agent,如果记不住用户的偏好、丢了上下文就成“金鱼”,那它带给用户的体验就是不信任。反过来,一个底层数据设计扎实的Agent,哪怕模型不是最顶尖的,也会让人觉得“这Agent靠谱”。
最后再分享一个小技巧:如果你刚开始学Agent开发,别急着上复杂架构,先把“会话写进SQLite、状态存Redis、重启后能从库里恢复”这三步打通,你就能体会到数据底座带来的巨大差异。之后再逐步引入向量记忆、分布式缓存、消息队列这些更高级的能力。数据底座这块投入的时间,会在后面每一个需求迭代里加倍还给你。