ARTICLE DETAIL

资讯详情

深耕网站建设、视觉设计与SEO优化的一线实战洞察。

Redis在AI Agent中的核心作用:会话记忆、缓存与分布式锁实战

Redis在AI Agent中的核心作用:会话记忆、缓存与分布式锁实战

接手过几个 AI Agent 项目的朋友应该都有同感:模型再聪明,一旦把 Agent 挂到线上,麻烦全在记忆和状态上。每次 LLM 调用都是无状态的,而 Agent 要完成的任务却是有状态、有上下文、有多轮协作的。我自己的项目就是典型:一开始用进程内变量存 session,测试环境一个人玩没问题,部署两个副本之后互相看不到对方的状态,直接乱套;后来换成数据库存会话,逻辑是通了,但响应延迟从 50ms 涨到了 200ms,多轮对话一多,体验立刻变差。

后来我把目光放回 Redis 缓存,但慢慢发现,Redis 在 Agent 系统里扮演的角色远不止"加一层缓存加速"这么简单。这篇内容我打算从架构拆解讲到代码落地,再讲讲我在真实项目里踩过的一系列坑,重点覆盖会话记忆、工具结果缓存、缓存一致性、分布式锁这几个方向。不管你是正在做 agent 开发、想优化现有架构,还是准备面试时被问到"Redis 在 AI 应用里怎么用",这篇文章都值得你花十分钟看完。

1. AI Agent 的缓存与记忆架构拆解

1.1 Agent 的核心痛点:无状态的大模型与有状态的任务

LLM 的 API 设计本质上是一次性的:你给它一段 prompt,它给你一个补全结果,这次调用和上次调用之间没有任何天然的"记忆"关系。想让 Agent 记住用户上一轮说了什么、任务拆解到哪一步、某个工具调用返回了什么结果,只能靠业务方自己在外部保存这些上下文,然后在每次请求时把历史信息重新拼进 prompt。

这个问题的严重性会随着任务复杂度指数上升。一个单轮问答机器人,上下文只有 system prompt 加用户问题,不做缓存也能跑;但一个真正意义上的 Agent,比如"帮我调研竞品并生成一份报告",它需要把用户原始需求记下来、把子任务拆解流程存起来、把多个工具返回的结果合并分析,这些中间状态如果全部放在内存变量里,会带来两件事:一是服务重启即丢失,二是多实例部署时状态互相隔离,导致同一个用户的请求落在不同副本上时,Agent 完全失去连贯性。

我当时仔细算过一笔账:如果用数据库强行存这些中间状态,每次读取和回填都要经过磁盘 IO、SQL 解析、网络传输,在对话高频交互的场景下延迟增长非常明显。但状态又不能丢,怎么办?答案不是单纯的"加缓存",而是要把缓存本身设计成状态层。

1.2 Redis 在 Agent 系统中的三大定位

我整理过一套自己的分类方式,Redis 在 Agent 系统里其实承担了三个完全不同的角色,很多人只用了第一个,后两个才是真正提升系统上限的地方。

第一是短期会话缓存。用户在一次对话窗口里的历史消息、Agent 当前执行的步骤、临时产生的中间结果,用 Redis 的 Hash 结构按 session_id 组织,TTL 设置成对话窗口的有效期,比如 30 分钟到 2 小时。这个角色解决的是"多轮对话上下文"的存储问题,也是大多数人理解里的缓存。

第二是工具结果与知识缓存。Agent 的核心能力是调用工具,而很多工具调用是确定性的:同样的输入参数,返回的耗时和结果差别不大。比如一个"天气查询"工具、一个"汇率换算"工具、一个"公司信息查询"接口,把工具入参 hash 成 key,结果缓存到 Redis,下次遇到相同请求时直接命中,省掉的是工具调用本身的延迟和费用。这个收益在大模型应用里尤其明显,因为除了工具调用费用,还能减少一次 LLM 的 tool call 循环。

第三是协作与并发控制。Agent 部署成多实例之后,会有两个经典问题:同一个任务可能被两个实例同时执行、多个实例需要共享同一个会话的状态。Redis 的分布式锁和原子计数能力正好派上用场。这个角色在单体应用时代几乎用不上,但在 Agent 这种天生分布式的场景里是标配。

1.3 一个容易被忽略的选型问题:不是所有状态都该进 Redis

Redis 虽然好,但它不是万能的。很多人在架构 ChatBot 时容易陷入一个误区:把 Redis 当成唯一存储,什么数据都往里塞。结果就是 Redis 内存爆炸、大 key 频出、热 key 打满网络。

我的做法是把数据分成几层:像工具结果、短期会话、分布式锁这类热数据、小体积数据、对延迟敏感的数据放在 Redis;用户画像、历史对话归档、向量化的长期记忆这类需要复杂查询或者体积较大的冷数据,放在专门的向量库或关系型数据库;而系统本身的元数据、账单记录等,留在业务主库。Redis 负责"快、近、热",数据库负责"全、稳、冷",两者各司其职。

另外还有一个经验:如果系统规模很小,比如单机部署、用户量几十个,用进程内缓存完全够,真没必要强行引入 Redis 增加一个运维节点。Redis 的价值要等到"多实例 + 多任务 + 高并发"三个条件都出现时才会真正体现出来。

2. 会话级缓存:短期记忆与上下文管理的 Redis 实现

2.1 会话缓存的数据结构与 TTL 设计

在设计会话缓存时,我最初犯过的错误是用一个 String key 保存整段 JSON 序列化后的对话历史。这种方式最大的问题是更新成本高:每当多轮对话追加一条消息,都得先 GET 出来、反序列化、改完再整体 SET 回去。对话一长,整个字符串的体积越来越大,每次更新都在制造大 key。

后来我改成了 Hash 结构,每个 Session 对应一个 Hash,字段拆成 history、state、meta 三块。history 存的是消息数组的 JSON 序列化,state 存 Agent 当前的任务拆解状态,meta 存 session 的创建时间、用户 ID、模型版本等信息。这样追加消息时只需要 HSET 更新 history 字段,改任务状态时只需要更新 state 字段,完全互不干扰。

TTL 设计上也要花心思。会话窗口的 TTL 既不能太短——用户可能思考十分钟再回一句话;也不能太长——如果无限保存,Redis 内存会被历史 session 堆满。我的实践是:创建 session 时设置基础 TTL 为 1 小时,每次用户产生新消息时调用 EXPIRE 刷新 key 的剩余时间;如果 Agent 需要执行 5 分钟以上的长任务,再单独检查任务状态,给 session 续期到任务完成。这个"滑动刷新"的逻辑,是让会话缓存既不浪费内存、又不丢上下文的关键。

2.2 从"缓存"升级到"记忆":Redis Stack 的向量能力

很多搞 Agent 的人会忽略一个事实:Redis 不止能做传统缓存,Redis Stack 模块里已经内置了向量索引和向量检索能力。这在 Agent 的长期记忆场景里非常有用。

Agent 的长期记忆本质上是一种检索增强:用户的偏好、历史结论、操作习惯需要被向量化存储,在每轮对话前把与当前问题最相关的记忆片段检索出来,加入 prompt,让 LLM 在推理时"看到"这些历史经验。这套能力的标准做法是使用 Pinecone、Milvus 等专用向量数据库,但如果你不想额外引入一个重组件,Redis Stack 的向量集合同样能顶一阵。

我自己的经验是:当向量规模在百万级以内、检索频率中等时,Redis Stack 的体验很顺滑。它最大的优势是复用现有的 Redis 运维体系,不需要多维护一套分布式集群;但到了几千万级向量、高并发检索的场景,专用向量库在索引延迟和内存效率上优势就更明显了。所以判断标准很简单:先想规模,再想运维成本。

2.3 Token 成本视角:Agent 缓存的隐藏收益

这块很少有人提,但我要说:Redis 在 Agent 系统里最大的隐藏价值,是帮你省 token 费。

LLM 按 token 计费,而 Agent 每轮对话都需要把 system prompt、历史对话、工具调用上下文全部拼进请求。很多内容其实是重复的:同一个工具的说明文本、同一个系统的角色设定、同一段用户偏好描述,如果每次请求都原样传给模型,这笔费用是持续烧钱的。

我的做法是把这些稳定内容拆成"记忆块",在 Redis 里做 Key-Value 缓存,每次请求时先检查缓存,命中就直接拼上,不命中再去向量库或业务库查一次,回填缓存。举个具体例子:一个固定的 system prompt 约 500 token,如果系统每天有 10 万次请求,每次都重传,相当于额外消耗 5000 万 token/天;而用 Redis 缓存后,这部分只要在 prompt 变更时才回源,节省量非常可观。同时这也意味着,设计 Agent 时不能只看"能不能跑通",还要看"跑一遍要烧多少钱"。

3. 缓存一致性治理:双写之后的数据同步策略

3.1 Cache Aside 模式与更新顺序的选择

缓存一致性是 Redis 面试题里的重头戏,在 Agent 场景里同样雷打不动。最常用的模式是 Cache Aside,流程很简单:读请求先查缓存,命中直接返回;未命中则查数据库,之后把结果回填缓存;写请求则先更新数据库,再删除缓存。

为什么写操作要选择"删除缓存"而不是"更新缓存"?原因有两个。一是更新缓存存在写放大问题:一个数据可能被频繁更新,但读取频率并不高,每次更新都要写缓存,白白浪费资源;二是并发问题复杂:如果两个线程同时更新数据库和缓存,后到达的更新可能覆盖先到达的数据,导致缓存里是旧值。删除则简单得多——下一次读请求发现 miss,自然会拿到数据库里的最新值并重新回填。

但在 Agent 场景里,缓存的数据类型比较特殊。比如任务状态、执行步骤这类数据,它们的数据源往往不是关系型数据库,而是另一个 Agent 实例的状态更新。这时候 Cache Aside 需要调整一下:Agent 实例更新完状态后,除了删缓存,还要通过消息通知其他实例"这个 session 的状态已变化",避免其他实例继续使用自己在本地构造的旧上下文。这种"删缓存 + 广播通知"的双通道做法,在大规模 Agent 协作时更可靠。

3.2 延迟双删与版本号:解决"旧值回填"的并发难题

如果只按照"先更新库、再删缓存"的顺序来,还是会遇到一个经典问题:线程 A 更新数据库为"任务完成"并删除了缓存;此时线程 B 发起读请求,发现缓存 miss,于是回源数据库——但线程 B 在更新操作完成之前就读到了旧值"执行中",然后把旧值回填到了缓存。此后一段时间内,所有读请求都会看到"执行中"这个过期状态。

解决这个问题的思路有两种。第一种是延迟双删:更新完数据库后,删除缓存,等几百毫秒后再删除一次。这样即使线程 B 把旧值写回了缓存,第二次删除也会把它清掉,下一次读取时拿到的一定是数据库里的最新值。第二种是版本号机制:缓存里存一份 version,数据更新时 version 加一,读请求回填时带上 version,只有"写入的 version 等于当前缓存的 version"才允许覆盖,否则视为旧数据,不写缓存。

在 Agent 场景里,我通常的做法是二者结合:对任务状态这类强一致敏感的数据,用版本号做兜底;对工具结果这类允许短暂不一致的数据,延迟双删就够了。这里的判断原则是:数据过期对用户产生的影响有多大?任务状态错了会导致 Agent 把"已完成"的任务重新执行一遍,影响很大;工具结果晚几十毫秒刷新,用户根本感觉不到。

3.3 穿透、击穿、雪崩:在 Agent 流量特征下怎么防

缓存三兄弟——穿透、击穿、雪崩,在 Agent 场景里也有各自的变形。

穿透通常出现在恶意或异常请求里:攻击者用随机的 session_id 反复请求,这些 session 在 Redis 里不存在,每次都直接打到数据库或向量库,导致后端压力骤增。我在生产环境里的应对措施是空值缓存:对查不到的 session 也设置一个短 TTL 的占位记录,几秒钟后就清除,避免同一个无效 key 反复击穿。更彻底的做法是加布隆过滤器,把所有合法 session_id 预加载进去,请求进来先做一次存在性判断。

击穿集中体现在热点知识条目上。比如某个工具的使用说明被几乎所有 Agent 会话引用,这个缓存 key 一旦过期,所有请求会同时回源。解决办法是加锁重建:一个请求负责重新构建缓存并加锁,其他请求先等待锁释放,再读取已经构建好的缓存,避免同一时刻大量请求都去回源。

雪崩则是因为大量缓存同时过期导致的集中回源。Agent 系统的会话缓存数量大、过期时间集中,如果 TTL 都设置成相同的 30 分钟,每半小时就会出现一波回源高峰。我处理这个问题的办法很朴素:在基础 TTL 上加上随机量,比如 30 分钟加 0 到 5 分钟的随机值,让过期时间散开。

4. Agent 工具调用中的分布式锁与幂等控制

4.1 竞态场景到底有多常见:多实例同时执行同一个任务

Agent 部署成多实例后,最隐蔽的坑就是竞态。举个例子:用户提交了一个"帮我下单"的指令,网关把请求负载均衡到实例 A;由于网络重试、消息队列重复投递或者用户重复点击,这条指令可能又被投递到实例 B。如果两个实例都执行工具调用,就可能出现重复下单、重复发送邮件、重复创建工单等严重事故。

我采取的方案是给每一条用户指令生成一个全局唯一的执行 ID,由"用户 ID + 任务 ID + 工具名 + 入参哈希"组合而成。每个实例在执行工具调用之前,先尝试在 Redis 里通过 SETNX 写入这个执行 ID;只有写入成功的那个实例才有资格执行工具调用,其他实例看到锁已存在,直接放弃执行或返回"任务已提交"。这套机制同时解决了幂等和去重两个问题,是 Agent 系统里必须有的基础防护。

4.2 分布式锁的正确写法:从 setnx 到 Lua 脚本

很多文章讲分布式锁还停留在"SETNX 加 EXPIRE"两行命令的时代,这在 Agent 项目里是远远不够的。SETNX 和 EXPIRE 不是原子操作:如果 SETNX 执行成功、在设置过期时间之前进程崩溃,这个锁就永远不会被释放,其他实例就会无限期等待,整个任务卡死。

真正靠谱的写法是用一条 Lua 脚本把"加锁 + 设置过期时间"放到一个原子操作里执行,或者在 Redis 6 以上版本直接使用SET key value NX EX ttl这种自带原子语义的单命令。释放锁的时候也不能直接 DEL,要先判断锁的 value 是不是自己写入的那个唯一客户端 ID,避免把别人后来拿到的锁误删。这两条细节是线上分布式锁不出事故的前提。

至于 Redlock 算法,我的态度是"大多数项目用不上":它解决的是多个独立 Redis 节点之间的可靠性问题,引入成本和复杂度都不低,而绝大多数 Agent 系统的 Redis 单节点或主从架构已经完全够用。真正的防护重点应该放在"锁的持有时间是否合理"上:给锁设置一个比任务最大执行时间稍长的 TTL,同时加一个看门狗按周期续约,确保任务还没执行完锁不会提前过期。

4.3 用 Lua 做限流与频控,防止 Agent "失控"

Agent 和普通 API 服务还有一个巨大区别:普通服务是被人调用的,而 Agent 有自主调用的能力。我在做任务规划型 Agent 时发现,如果 Agent 陷进了循环试错,它会在一分钟之内反复调用同一个失败的工具,这时候如果没有频控,工具服务商那边的账单会非常吓人。

我的处理方案是用 Redis 的 INCR 做简单计数器限流:以"用户 ID + 工具名 + 小时"作为 key,每调用一次工具就 INCR,超过阈值直接拒绝。更平滑的做法是令牌桶限流器,用 Lua 脚本维护令牌桶的当前令牌数,每次调用前检查并扣减,原子性由脚本保证。

这里我特别想说一句:为什么限流逻辑要写成 Lua 脚本而不是用 Python 先 GET 再 SET?因为 Agent 系统天然多实例并发,两个实例同时读到"还有 1 个配额",然后同时扣减,结果实际调用了 2 次。Lua 脚本在 Redis 里是原子执行的,整个"检查-扣减"过程不会被其他请求穿插,这才是可靠的实现方式。

5. 实操:一个最小可用 AI Agent 缓存层的代码落地

5.1 环境准备:本地 Redis 的安装与基础配置

如果你还没有本地 Redis 环境,最省事的办法是直接用 Docker 起一个。我习惯用 7.0 以上的版本,性能稳定,而且 Redis 7 对多线程 IO 的支持已经非常成熟。

docker run -d --name redis-agent-test \ -p 6379:6379 \ -e REDIS_PASSWORD=agent_dev_2024 \ redis:7-alpine

如果本地环境没有 Docker,也可以直接下载对应平台的 Redis 可执行文件。启动后建议先验证一下连通性:

redis-cli -a agent_dev_2024 PING

返回 PONG 就说明环境就绪。生产环境建议加上 ACL 用户权限配置,不要让应用直连管理员账号,至少给应用单独创建一个只能访问特定 key 前缀的账号,尽量避免所有业务共享超级权限。

Python 侧的依赖很简单,只需要一个 redis-py 库:

pip install redis

千万注意:redis-py 5.x 版本和 Redis 7 的协议兼容性很好,但如果你还在用 Redis 5,某些新命令可能报错,建议配套升级。

5.2 核心代码实现:AgentSession 缓存类

下面是我在实际项目里精简过的一个会话缓存类,包含了基础 TTL 刷新、历史截断和状态更新三个核心能力:

import json import time import redis class AgentSessionCache: """ 基于 Redis Hash 的 Agent 会话缓存 - history: 多轮对话历史(最近 N 轮) - state: Agent 任务执行状态 - meta: 会话元信息 """ def __init__(self, redis_url: str, ttl: int = 3600, max_rounds: int = 20): self.redis = redis.from_url( redis_url, decode_responses=True, # 注意:统一开启字符串解码 socket_connect_timeout=2, socket_timeout=5 ) self.ttl = ttl self.max_rounds = max_rounds def _key(self, session_id: str) -> str: return f"agent:session:{session_id}" def create_session(self, session_id: str, user_id: str) -> None: key = self._key(session_id) mapping = { "history": json.dumps([], ensure_ascii=False), "state": json.dumps({}, ensure_ascii=False), "meta": json.dumps({"user_id": user_id, "created_at": time.time()}, ensure_ascii=False) } self.redis.hset(key, mapping=mapping) self.redis.expire(key, self.ttl) def get_context(self, session_id: str) -> dict | None: key = self._key(session_id) data = self.redis.hgetall(key) if not data: return None return { "history": json.loads(data.get("history", "[]")), "state": json.loads(data.get("state", "{}")), "meta": json.loads(data.get("meta", "{}")), } def append_message(self, session_id: str, role: str, content: str) -> None: key = self._key(session_id) ctx = self.get_context(session_id) if ctx is None: ctx = {"history": [], "state": {}, "meta": {}} ctx["history"].append({ "role": role, "content": content, "ts": time.time() }) # 只保留最近 N 轮,防止 prompt 无限膨胀 if len(ctx["history"]) > self.max_rounds: ctx["history"] = ctx["history"][-self.max_rounds:] # 高频字段单独更新,避免整个对象读改写 self.redis.hset(key, "history", json.dumps(ctx["history"], ensure_ascii=False)) # 刷新 TTL:每次交互都重置会话存活时间 self.redis.expire(key, self.ttl) def update_state(self, session_id: str, state: dict) -> None: key = self._key(session_id) self.redis.hset(key, "state", json.dumps(state, ensure_ascii=False)) self.redis.expire(key, self.ttl) def delete_session(self, session_id: str) -> None: self.redis.delete(self._key(session_id))

这段代码里有几个容易踩坑的细节,我自己都遇到过。第一,decode_responses=True必须开,否则你从 Redis 里读出来的全是 bytes 而不是字符串,后面做 JSON 解析会处处报错。第二,ensure_ascii=False得写在每个 json.dumps 里,否则 LLM 生成的中文内容存进去之后全变成 \uXXXX 转义序列,排查问题时完全没法看。第三,TTL 刷新必须在用户每次交互时都执行,而不是只在创建 session 时设置一次。

工具结果缓存也很简单,本质上就是"入参哈希 + 命中返回 + 未命中回源并回填":

import hashlib import json def cached_tool_call(tool_name: str, args: dict, ttl: int = 600): key_body = f"{tool_name}:{json.dumps(args, sort_keys=True, ensure_ascii=False)}" cache_key = "agent:tool:" + hashlib.md5(key_body.encode()).hexdigest() cached = redis_client.get(cache_key) if cached: return json.loads(cached) result = real_tool_call(tool_name, args) # 只有标记为可缓存的结果才回填,避免污染缓存 if result.get("cacheable", True): redis_client.setex(cache_key, ttl, json.dumps(result, ensure_ascii=False)) return result

5.3 连接池配置与简单压测

在写代码时,连接池是另一个必须提前处理的问题。redis-py 默认会为每个 Redis 实例创建一个连接,如果你的 Agent 服务是多线程同时处理请求,就需要配置合适的连接池,否则高并发下会出现"连接数被占满、请求排队"的情况。

常用的连接池配置参数包括:max_connections 控制连接池上限;socket_connect_timeout 和 socket_timeout 控制网络超时;retry_on_timeout 决定超时后是否重试。我一般会设成:连接池 50 个连接、连接超时 2 秒、读写超时 5 秒。如果你的 Agent 会有同时上千个并发请求,建议用 Redis Cluster 而不是单纯调大连接池,单实例总有极限。

压测下来,同样的操作在本地网络延迟下,SET 命令的 RTT 大概是 0.3-0.8ms,GET 也差不多。这意味着单个 Redis 实例理论上可以支撑每秒几万级的操作,对于 Agent 系统的会话缓存来说完全是冗余量。但请注意,如果代码里每次请求都新建一个 Redis 连接,而不是复用连接池,这个数字会瞬间跌到原来的十分之一甚至更低。连接开销在局域网环境下容易被低估,它才是大多数 Agent 缓存性能问题的第一嫌疑。

6. Redis 在 AI Agent 场景下的典型坑与排查实录

6.1 序列化与解码:字符乱掉、byte 乱飞

Agent 系统存的内容和普通业务缓存有很大区别:普通缓存存的多是结构化的数字、布尔值,而 Agent 存的是大段的自然语言和 JSON 结构。这意味着序列化问题会暴露得非常明显。

最常见的坑是 redis-py 默认返回 bytes。很多新手在hgetall之后直接拿去 json.loads,结果报出"expected string or bytes-like object",或者中文字符串变成一串乱码。我的统一处理方式是在创建 Redis 连接时开decode_responses=True,让所有返回值自动转成字符串。这个开关要在项目开始时打开,不然后期切换会遇到一堆历史数据编码不兼容的问题。

另一个坑是 JSON 序列化时把 None、int、float 混在一起,或者日期对象没有转成字符串,导致 json.dumps 报错。我在实际项目里会把所有要进 Redis 的数据先用一个统一的数据模型校验一遍,尽量保证"进 Redis 的都是干净的字符串"。

6.2 连接池与超时:RedisTimeoutError 的大规模爆发

有一段时间,我们的 Agent 服务频繁报 RedisTimeoutError,刚开始以为是网络问题,后来排查发现连接池配置完全没做过,默认连接数只有几个,一旦并发一上来,连接池很快被耗尽,请求全部阻塞。

排查方法也很直接:看 Redis 的 clients 连接状态、看应用的日志分布、临时把连接池上限调大验证。最终我把连接池从默认的 10 调到了 50,同时加了retry_on_timeout=True,这个问题才彻底消停。

另外还要注意超时的设置。如果 socket_timeout 设得太短,比如 1 秒,那么 Redis 响应稍慢一点就会误报超时;设得太长,比如 30 秒,一旦 Redis 真正阻塞,所有请求都会排长队。我的经验值是读超时 3-5 秒、连接超时 1-2 秒,同时给重试加上最大次数限制,避免超时之后的无限重试放大故障。

6.3 大 key、热 key 和高内存:Agent 数据膨胀的隐形杀手

Agent 系统特别容易制造大 key。原因在于对话历史会持续增长,如果不在代码层做截断、不在写入时控制大小,某个活跃用户的 session key 可能从几千字节膨胀到几兆。而 Redis 处理大 key 时,哪怕只是一个 HGETALL,也可能产生数十毫秒的阻塞,拖慢整个实例上所有请求。

排查大 key 的方式是使用 Redis 自带的SLOWLOG GET命令,找出执行时间最长的命令;然后使用redis-cli --bigkeys扫描全库,定位体积最大的 key。我在 Agent 项目里的处理方式是双管齐下:一是在代码层限制单字段最大长度,超长的 content 直接摘要化;二是定期扫描,对异常膨胀的 session 单独迁移或清理。

热 key 的问题则通常出在系统的公共知识缓存上。假设某个"工具说明"的 key 被 90% 的会话引用,单个 key 的 QPS 就会非常高,即使 Redis 本身能扛,客户端连接也会被这个 key 堵住。我的做法有两层:应用层再加一层本地内存缓存,把高频访问的 key 在进程内先挡掉一层;如果还不够,就把同一个热 key 在同一 Redis 实例上拆成多个带后缀的副本 key,让请求分散,再靠失效通知或者短 TTL 来保证一致性。

6.4 持久化选择:RDB 还是 AOF?

在 Agent 场景里,缓存数据的持久化策略比传统业务缓存放得更重,因为 session 状态在某种程度上是"不可再生数据"——Redis 重启后如果直接丢光,用户的多轮对话上下文就没了,这个体验损失是不可接受的。

从执行原理上看,RDB 是定期对全库做快照,恢复速度快、文件体积小,但存在丢失最近一次快照之后所有修改的窗口;AOF 是追加记录每个写命令,最多丢几秒数据,但文件体积大、恢复较慢。Redis 7 里可以把两者结合,采用混合持久化模式:以 RDB 为基础快照,同时保留 AOF 增量日志,重启时先加载 RDB 再执行增量 AOF 日志,恢复速度和丢失窗口都能兼顾。

我个人的配置偏好是:对于 Agent 系统,开启混合持久化,AOF 的 fsync 策略设置为每秒一次,这样最坏情况下也就丢 1 秒的会话状态,大多数场景完全可以接受。同时设置 maxmemory 上限和合适的淘汰策略。这里要特别说一句:在 Agent 系统里,淘汰策略建议使用allkeys-lru而不是volatile-lru,因为很多 session 的 TTL 可能被滑动刷新不断拉长,如果只淘汰带 TTL 的 key,那些长期活跃的会话可能撑爆内存。

6.5 一个小技巧:用 RedisInsight 可视化排查内存分布

最后分享一个排查工具层面的小经验:RedisInsight 是 Redis 官方的可视化工具,它能直观展示每个 key 的内存占用、key 的数量分布以及命令调用频率。我在 Agent 项目上线初期,每两天就会打开 RedisInsight 看一次内存排名,确认有没有异常膨胀的 key,观察各 session 的 TTL 分布是否合理。

有一次系统慢查询特别多,我就是在 RedisInsight 里按内存排序,发现了一个存储了完整对话原文的 session key,体积占到了整个实例内存的 30%。当时直接把那个用户的全量历史改成了摘要存储,内存立刻降下来,慢查询也消失了。这个工具对运维排查的帮助非常大,尤其是 Agent 这类数据形态复杂的系统,光看 INFO 统计很难定位到具体 key。

说实话,Redis 在 AI Agent 项目里从来就不只是"缓存层"那么简单,它更像是整个 Agent 系统的中枢神经系统:短期记忆靠它存,工具结果靠它省,多实例之间的协作靠它锁,异常流量靠它挡。我在实际踩坑过程中最深的体感是:把 Redis 当成数据的一个有生命周期的状态流去设计,而不是当成一个被动的存储桶,很多看似复杂的架构问题都会迎刃而解。如果你也在做 Agent 系统,建议先从小处入手,把会话缓存和工具结果缓存跑通,再逐步加上锁和限流,这套组合拳打下来,稳定性和成本都会有非常明显的变化。

返回列表