ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

AI Agent 缓存实战:Redis 状态管理与并发治理

AI Agent 缓存实战:Redis 状态管理与并发治理 1. 从一次线上抖动说起AI Agent 为什么绕不开 Redis先说一个我亲身经历的场景。去年帮一个团队调优他们的 AI Agent 服务功能本身跑得挺好——用户提问、Agent 规划任务、调用工具、返回结果链路清晰。但上线第二周开始监控上出现了一个很规律的现象每隔几分钟接口 P99 延迟就从 300ms 飙到 4s 以上然后自己恢复。日志里翻来翻去最扎眼的一行是Redis command timed out; nested exception is io.lettuce.core.RedisCommandTimeoutException。这个报错很多人见过但放在 AI Agent 的语境下它的成因和传统 CRUD 应用完全不是一回事。传统业务里 Redis 超时多半是热点 key 或者大 key 拖垮了单节点而 AI Agent 的超时往往是因为Agent 的思考过程本身是长耗时、多轮次、带状态的这些状态如果无脑往 Redis 里塞很容易把缓存层当成数据库用最后把连接池和内存一起拖垮。所以这篇内容我想聊的不是Redis 怎么装、怎么用这种入门话题而是聚焦一个更具体的问题当 AI Agent 遇上 Redis 缓存哪些东西该缓存、哪些绝对不能缓存、并发上来之后怎么扛、以及那些只有踩过才知道的坑。适合正在搭建 AI Agent、或者已经上线但被缓存问题折磨的开发者。如果你还在纠结 Redis 的五大基本类型这篇也能看但重点不在那儿。先把结论摆前面AI Agent 用 Redis核心不是缓存数据而是缓存可复用的中间态。这个定位一旦搞错后面全是坑。2. AI Agent 的状态到底长什么样决定了缓存怎么设计2.1 Agent 的三类状态会话态、工具态、模型态要设计缓存先得把 Agent 运行过程中产生的数据分清楚。我一般把它分成三类这三类的缓存策略完全不同。会话态Session State用户和 Agent 的对话历史、当前任务进度、已确认的参数。这类数据的特点是读写频繁、生命周期跟会话绑定、必须强一致。比如用户说帮我订明天下午三点的会议室Agent 记下了明天下午三点这个约束下一轮对话里用户改口说改成四点Agent 必须能读到之前的状态并更新。这类数据我建议放 Redis 的 Hash 结构一个 session 一个 key字段存各个槽位。工具态Tool StateAgent 调用外部工具搜索、数据库查询、API 请求的返回结果。这类数据的特点是可能很大、可能重复、有 TTL 需求。比如 Agent 连续三次调用同一个天气 API结果完全一样那第二次第三次就该命中缓存。这类数据适合用 String 存序列化后的 JSON带明确的过期时间。模型态Model StateLLM 的推理结果、embedding 向量、prompt 模板渲染结果。这类数据的特点是计算昂贵、结果确定、可长期复用。比如一段固定的 system prompt 渲染出来的结果或者同一个问题的 embedding缓存起来收益极高。embedding 向量我一般用 Redis 的向量检索能力Redis Stack 的 RediSearch 模块来存普通文本结果用 String。把这三类分清楚你就知道为什么不能一把梭全塞进 Redis 了——它们的读写模式、一致性要求、体积差异太大混在一起必然出问题。2.2 为什么缓存一切是 AI Agent 最危险的想法我见过不少团队图省事把 Agent 每一步的中间结果都往 Redis 写想着反正 Redis 快。结果呢内存暴涨、连接池打满、序列化开销比计算本身还大。这里有个反直觉的点AI Agent 的瓶颈通常不在计算而在 I/O 和序列化。LLM 调用本身是网络 I/O工具调用也是网络 I/O如果你在中间又插一层 Redis 读写而且每次读写都做 JSON 序列化/反序列化那这点开销累积起来非常可观。我实测过一个案例一个 8KB 的 Agent 状态对象用 Jackson 序列化一次大约 0.3ms看起来不多但如果一个请求链路里读写 20 次就是 6ms 纯开销还没算网络往返。所以我的原则是只缓存重新计算成本 缓存读写成本的数据。这个判断需要你对自己的链路有清晰的耗时认知。LLM 调用动辄 1-3 秒那缓存它的结果绝对划算但一个内存里的字符串拼接你缓存它纯属给自己找麻烦。2.3 一个具体的状态结构设计示例光说理论太虚给个我实际用过的结构。假设是一个客服 Agentsession key 设计成agent:session:{sessionId}用 Hash 存import redis import json r redis.Redis(hostlocalhost, port6379, decode_responsesTrue) def save_session(session_id, state): key fagent:session:{session_id} # 只存需要跨请求共享的字段临时变量不要塞进来 r.hset(key, mapping{ history: json.dumps(state[history][-10:]), # 只留最近10轮 slots: json.dumps(state[slots]), # 已确认的参数槽位 stage: state[stage], # 当前任务阶段 updated_at: state[updated_at] }) r.expire(key, 1800) # 30分钟无活动自动清理 def load_session(session_id): key fagent:session:{session_id} data r.hgetall(key) if not data: return None return { history: json.loads(data[history]), slots: json.loads(data[slots]), stage: data[stage], updated_at: data[updated_at] }注意几个细节history 只留最近 10 轮因为更早的对话对当前决策价值极低全存进去只会让每次读写都变慢slots 单独存因为它是 Agent 决策的核心依据需要频繁读取整个 key 设 30 分钟过期避免僵尸会话堆积。这些都是踩过坑之后定下来的。3. 并发一上来就崩AI Agent 的缓存并发治理3.1 缓存击穿在 Agent 场景下的特殊形态缓存击穿这个词大家熟就是某个热点 key 过期瞬间大量请求同时打到后端。传统场景下后端是数据库扛不住就加锁。但 AI Agent 场景下后端是 LLM情况更糟——LLM 调用又慢又贵一旦击穿不仅慢还烧钱。我遇到过一个典型场景某个热门问题的 embedding 缓存刚好过期同一秒来了 200 个请求全部穿透到 embedding 服务瞬间把配额打满后续请求全部失败。这种事故的根因不是 Redis 不行而是没有对昂贵计算做并发保护。解决方案是分布式锁 双重检查。但这里有个坑很多人用SETNX加锁忘了设过期时间一旦持锁进程崩溃锁永远不释放。正确做法是用SET key value NX EX 10这种原子命令value 用唯一标识比如 UUID释放时用 Lua 脚本校验再删避免误删别人的锁。import uuid import time def get_embedding_with_lock(text, compute_fn): cache_key fagent:embedding:{hash(text)} cached r.get(cache_key) if cached: return json.loads(cached) lock_key flock:{cache_key} lock_value str(uuid.uuid4()) # NX 保证只有一个能拿到锁EX 防止死锁 acquired r.set(lock_key, lock_value, nxTrue, ex10) if acquired: try: # 双重检查拿到锁后再看一眼缓存可能别人刚写完 cached r.get(cache_key) if cached: return json.loads(cached) result compute_fn(text) r.set(cache_key, json.dumps(result), ex3600) return result finally: # Lua 脚本保证校验删除原子性 lua if redis.call(get, KEYS[1]) ARGV[1] then return redis.call(del, KEYS[1]) else return 0 end r.eval(lua, 1, lock_key, lock_value) else: # 没拿到锁短暂等待后重试读缓存 time.sleep(0.1) cached r.get(cache_key) if cached: return json.loads(cached) # 兜底直接计算避免无限等待 return compute_fn(text)这段代码的关键在于双重检查和兜底逻辑。双重检查避免重复计算兜底逻辑避免锁竞争时请求全部卡死。实测下来200 并发打同一个 key只有 1 个真正计算其余全部命中缓存或快速兜底。3.2 连接池配置Lettuce 超时的真正原因回到开头那个RedisCommandTimeoutException。很多人第一反应是Redis 挂了其实十有八九是连接池配置不合理。AI Agent 的特点是请求耗时波动极大——简单问题 200ms复杂任务可能 30 秒。如果连接池的command timeout设得太短比如默认的 60ms 在某些客户端里长耗时操作还没返回连接就被判定超时了。但设太长又会导致连接被长时间占用池子很快耗尽。我的经验配置是这样的以 Lettuce 为例参数建议值理由max-activeCPU核数 * 4太高反而增加上下文切换max-idleCPU核数 * 2保持适量热连接min-idleCPU核数避免频繁创建销毁command-timeout500ms覆盖绝大多数正常操作shutdown-timeout200ms优雅关闭关键点command-timeout 要区分操作类型。普通的 GET/SET 设 200-500ms 足够但如果你有耗时的 Lua 脚本或大 key 操作得单独处理不能一刀切。我一般会把耗时操作拆出来用独立的连接配置。3.3 用 Pipeline 和批量操作减少往返AI Agent 一个请求链路里往往要读写多个 key。如果每个 key 都单独发一次命令网络往返次数会非常可观。这时候 Pipeline 就是救星。比如加载一个完整 session需要读 history、slots、stage 三个字段用 Hash 的hgetall一次搞定但如果它们分散在不同 key 里就该用 Pipelinedef batch_load(session_id): pipe r.pipeline() pipe.get(fagent:history:{session_id}) pipe.get(fagent:slots:{session_id}) pipe.get(fagent:stage:{session_id}) results pipe.execute() return results一次网络往返拿三个值比三次单独请求快 2-3 倍。这个优化在 QPS 上千之后效果特别明显。但要注意Pipeline 不是事务中间某条命令失败不会回滚所以只适合读多写少、允许部分失败的场景。4. 缓存什么、不缓存什么一份实战决策清单4.1 强烈建议缓存的四类数据结合我自己的实践AI Agent 里最值得缓存的四类数据是第一embedding 向量。这是收益最高的。同一个文本的 embedding 是确定的而计算一次 embedding 可能要几十到几百毫秒。缓存起来重复查询直接命中。用 Redis Stack 的话还能顺便做向量相似度检索一举两得。第二LLM 的确定性输出。注意是确定性——temperature 设为 0 的场景同样的 prompt 输出基本一致可以缓存。但如果 temperature 大于 0每次输出都不同缓存就没意义了反而会返回过时结果。第三工具调用的幂等结果。比如查询类 API、汇率、天气、知识库检索这些结果在一定时间内稳定缓存 TTL 设个几分钟到几小时都合理。第四prompt 模板渲染结果。如果模板复杂、变量多渲染本身有开销缓存渲染后的结果能省不少 CPU。4.2 绝对不能缓存的三类数据反过来这几类千万别缓存第一带用户隐私的原始对话。除非你有明确的合规方案和加密措施否则原始对话内容缓存到 Redis 风险极高。我一般只缓存脱敏后的结构化槽位不缓存原文。第二实时性要求极高的数据。比如股票价格、库存数量缓存 1 秒都可能出错。这类数据要么不缓存要么 TTL 设到秒级并配合主动失效。第三大对象。单个 value 超过 10KB 就要警惕超过 100KB 基本就是设计问题。大 key 不仅占内存还会阻塞 Redis 单线程影响所有请求。我见过有人把整个对话历史几十 KB塞一个 key结果每次读写都卡顿。4.3 TTL 怎么定一个可落地的计算方法TTL 定多少很多人拍脑袋。我给个可计算的方法TTL 数据可容忍的最大陈旧时间 × 安全系数0.8比如工具调用结果业务上能容忍 5 分钟陈旧那 TTL 设 4 分钟。为什么要乘安全系数因为 Redis 的过期是惰性删除 定期删除不是精确到点就删留点余量避免边界情况返回过期数据。另外TTL 要加随机抖动。如果一批 key 同时创建、同时过期会造成缓存雪崩——同一时刻大量请求穿透。做法很简单import random def set_with_jitter(key, value, base_ttl): jitter random.randint(0, int(base_ttl * 0.1)) r.set(key, value, exbase_ttl jitter)在基础 TTL 上加 10% 的随机量就能把过期时间打散避免集中失效。5. 那些只有踩过才知道的坑5.1 序列化选错性能差十倍这个坑我踩得最深。早期用 Java 的默认序列化JDK Serializable存 Agent 状态结果发现 CPU 占用异常高。后来换成 JSON性能提升明显再后来对高频小对象换成 MessagePack又提升一截。不同序列化方案的对比我实测过一组数据对象约 2KB方案序列化耗时反序列化耗时体积JDK Serializable1.2ms1.5ms3.1KBJSON (Jackson)0.3ms0.4ms2.4KBMessagePack0.15ms0.2ms1.8KBProtobuf0.1ms0.15ms1.5KB结论很清楚能用二进制就别用文本能用紧凑格式就别用臃肿格式。但也要权衡可读性——调试阶段 JSON 方便看上线后可以换。我的做法是开发环境用 JSON生产环境按数据热度分级热数据用 MessagePack。5.2 缓存与数据库的一致性Agent 场景下的取舍缓存和数据库怎么保持一致是经典难题。AI Agent 场景下我的建议是尽量让 Redis 成为唯一数据源而不是数据库的缓存。为什么因为 Agent 的状态数据大多是过程性的不像订单、用户这种需要持久化的核心资产。会话态、工具态这些丢了重新生成就行没必要搞双写一致性那套复杂逻辑。真正需要持久化的比如用户确认的最终结果直接落库不走缓存。如果确实需要双写记住一个原则先更新数据库再删除缓存而不是更新缓存。删除比更新安全因为更新可能因为并发导致旧值覆盖新值。删除的话下次读自然回源最多一次脏读。5.3 内存淘汰策略别让 Agent 把 Redis 撑爆Redis 内存满了会触发淘汰。默认的noeviction策略下写入直接报错Agent 直接挂。所以生产环境一定要配淘汰策略。对 AI Agent 场景我推荐allkeys-lru或volatile-lru。区别在于allkeys-lru对所有 key 淘汰volatile-lru只淘汰设了过期时间的 key。如果你所有缓存 key 都设了 TTL用volatile-lru更安全因为它不会误删那些没设 TTL 的重要数据。但更根本的做法是监控内存使用率提前扩容。我一般设两个告警线70% 预警85% 紧急。到 85% 还没处理就该考虑加节点或者清理冷数据了。5.4 分布式锁的坑锁续期与误删前面提了分布式锁这里补充两个高频坑。锁续期问题如果业务执行时间超过锁的过期时间锁会自动释放别的请求就能拿到锁导致并发。解决方案是看门狗机制——后台起个线程定期给锁续期。Redisson 这类库已经内置了自己实现的话要小心。误删问题A 拿到锁执行超时锁释放B 拿到锁这时 A 执行完去删锁删的是 B 的锁。这就是为什么释放锁必须用 Lua 脚本校验 value。这个坑我见过太多人踩。6. 从单机到集群AI Agent 缓存的扩展路径6.1 什么时候该上集群单机 Redis 扛不住的时候第一反应往往是上集群。但先别急问自己三个问题内存不够了QPS 到瓶颈了还是单纯觉得该上集群了如果是内存不够先看能不能优化数据结构、清理冷数据、压缩 value。很多时候优化一轮内存能省 30% 以上。如果是 QPS 瓶颈先看是不是有大 key 或者慢命令拖累优化掉往往能提升数倍。真正需要集群的信号是单机内存持续超过 80%且优化空间已尽或者 QPS 稳定超过单机处理能力通常 8-10 万。这时候再考虑集群。6.2 集群模式下的 key 设计Redis Cluster 把数据分到 16384 个槽key 通过 CRC16 取模决定落在哪个槽。这意味着跨槽的操作比如事务、Lua 脚本涉及多个 key会失败。对 AI Agent 来说最典型的问题是session 的多个字段如果分散在不同 key可能落在不同槽无法用事务保证原子性。解决方案是用hash tag——用{}包裹 key 的一部分让相关 key 落到同一个槽agent:session:{user123}:history agent:session:{user123}:slots agent:session:{user123}:stage这样{user123}相同的 key 一定在同一槽可以安全地用事务和 Lua。这个技巧在集群环境下几乎是必备的。6.3 主从与读写分离的取舍AI Agent 的读远多于写读写分离能显著提升吞吐。但要注意主从延迟——写完立刻读可能读到旧值。对 Agent 场景我的做法是强一致的读走主节点弱一致的读走从节点。比如 session 状态这种写完马上要读的走主而 embedding 缓存这种写一次读多次的走从完全没问题。配置上Lettuce 支持readFrom策略可以设成SLAVE_PREFERRED优先读从节点从节点不可用时回退主节点。这样既提升吞吐又保证可用性。7. 监控与调优让缓存问题无处遁形7.1 必须盯住的几个指标缓存出问题往往不是突然的而是有征兆的。我一般盯这几个指标命中率低于 80% 就要查原因可能是 TTL 太短、key 设计不合理、或者缓存被频繁淘汰。内存使用率超过 70% 预警配合淘汰策略观察。慢查询slowlog里超过 10ms 的命令都要关注往往是大 key 或复杂命令。连接数接近 maxclients 就要扩容或优化连接复用。网络往返延迟突然升高可能是网络问题或 Redis 阻塞。这些指标用 Redis 自带的INFO命令就能拿到配合 Prometheus Grafana 做可视化基本能覆盖大部分问题。7.2 慢查询的定位与优化SLOWLOG GET 10能拿到最近 10 条慢查询。看到慢查询后按这个顺序排查是不是大 key用MEMORY USAGE key看单个 key 大小。是不是复杂命令比如KEYS *、HGETALL大 Hash、SMEMBERS大 Set。是不是 Lua 脚本太复杂脚本执行是阻塞的越短越好。是不是网络问题看客户端和服务端的延迟。我遇到最多的就是大 key。一个 1MB 的 HashHGETALL一次要几毫秒高并发下直接拖垮。解决方案是拆分——按字段前缀拆成多个小 Hash或者用HSCAN分批读。7.3 一个真实的调优案例复盘最后分享一个我调过的案例。某 Agent 服务QPS 2000 左右P99 延迟 800ms其中 Redis 相关占 300ms。排查发现三个问题问题一session 用 String 存整个 JSON每次读写都要全量序列化。改成 Hash 后只读写需要的字段序列化开销降了 70%。问题二embedding 缓存没有加锁热点 key 过期时大量穿透。加了分布式锁 双重检查后穿透请求从每秒几百降到个位数。问题三连接池 max-active 设成了 500远超实际需要导致连接创建销毁频繁。调到 CPU 核数 * 4 后连接相关开销明显下降。三个问题解决后P99 降到 250msRedis 相关开销降到 40ms。这个案例说明大部分缓存性能问题不是 Redis 本身的问题而是使用方式的问题。8. 写在最后的一点个人体会做 AI Agent 的缓存优化我最大的体会是别把 Redis 当万能药也别把它当黑盒。它快但快是有前提的——前提是你的数据结构合理、key 设计得当、并发保护到位。一旦这些前提不满足Redis 反而会成为整个链路最脆弱的一环。另外AI Agent 这个领域变化太快今天的最佳实践明天可能就过时。比如向量缓存以前大家用普通 String 存现在 Redis Stack 直接支持向量检索玩法完全不一样了。所以保持学习、保持实测比记住任何一条最佳实践都重要。如果你正在做类似的事情建议先从分清三类状态开始把该缓存的、不该缓存的分清楚再逐步优化并发和扩展性。别一上来就追求集群、追求极致性能先把基础打牢后面的事自然水到渠成。
返回列表