
1. 为什么 AI Agent 一上缓存就翻车做过 AI Agent 项目的人大概都有过这种体验本地跑得好好的一上线就各种超时、数据错乱、内存暴涨。我最早做 Agent 的时候也踩过这个坑当时觉得加个 Redis 缓存不就是get一下set一下的事结果上线第二天就被现实教育了。AI Agent 和传统 Web 应用对缓存的需求完全不是一回事。传统接口缓存的是数据库查询结果key 固定、value 结构稳定、过期时间好估。但 Agent 不一样它每一轮对话都可能产生新的上下文、新的工具调用结果、新的推理中间态缓存的东西五花八门生命周期也千差万别。你要是拿传统那套“统一 TTL 五分钟”的思路去套要么缓存命中率低得可怜要么就是拿到过期数据导致 Agent 胡言乱语。这篇内容我打算把 AI Agent 场景下 Redis 缓存这一整套东西拆开讲清楚缓存什么、怎么设计 key、怎么控制失效、怎么扛并发、怎么排查线上问题。适合正在搭 Agent 或者已经被 Agent 缓存问题折磨过的朋友也适合想从传统后端转过来做 AI 应用的开发者。看完你至少能搞清楚一件事——Agent 的缓存不是“加个 Redis”那么简单它是一套需要分层设计的治理体系。2. AI Agent 缓存到底在缓存什么2.1 先搞清楚 Agent 的请求链路要设计缓存得先知道数据从哪来、到哪去。一个典型的 AI Agent 请求链路大概是这样用户输入 → 意图识别 → 上下文组装历史对话 记忆 RAG 检索→ LLM 推理 → 工具调用 → 结果整合 → 返回。这条链路上每一环都有可以缓存的东西但缓存的价值和风险完全不同。我一般把 Agent 的缓存对象分成四类这个分类是我自己踩坑总结出来的比按技术类型分更实用缓存类型典型内容生命周期命中收益风险等级会话上下文历史消息、摘要会话级高中工具调用结果API 返回、检索结果分钟到小时极高高LLM 推理结果相同 prompt 的响应可长可短中极高向量检索结果embedding、召回列表小时到天高低会话上下文缓存是最基础的主要解决多轮对话时反复拼装历史消息的问题。工具调用结果缓存收益最大因为很多外部 API 又慢又贵同样的查询没必要打两次。LLM 推理结果缓存最危险因为 prompt 只要差一个字符结果就可能完全不同缓存错了就是灾难。向量检索结果缓存相对安全因为 embedding 是确定性的同样的 query 召回结果基本一致。2.2 哪些能缓存哪些碰都别碰这里有个原则我一直在用确定性越强的数据越适合缓存带副作用的操作坚决不缓存。确定性强的比如 embedding 计算结果、RAG 召回的文档列表、固定工具的只读查询这些缓存起来收益高、风险低。带副作用的比如“发消息”“下单”“写文件”这类工具调用绝对不能缓存结果否则用户点两次只执行一次或者反过来执行两次都是事故。还有一个容易被忽略的点带用户身份的数据要按用户隔离。我见过有人把工具调用结果用 query 做 key 直接缓存结果 A 用户查到了 B 用户的订单信息。这种问题在 Agent 场景下特别隐蔽因为 Agent 会自动组装上下文你很难一眼看出数据串了。提示判断一个数据能不能缓存问自己三个问题——同样的输入是否永远得到同样的输出这个输出是否包含用户私有信息缓存过期后重新计算代价大不大三个问题都过了再考虑缓存。2.3 缓存粒度怎么定粒度太粗命中率低粒度太细key 爆炸、管理成本高。我的经验是按“最小可复用单元”来切。比如 RAG 检索不要缓存整个“用户问题 → 最终答案”而是缓存“检索 query → 召回文档列表”。因为最终答案受 LLM 随机性影响缓存了也不一定复用得上但召回列表是确定性的复用价值高。再比如工具调用缓存粒度应该是“工具名 规范化参数”而不是“用户原始输入”。用户说“帮我查下北京天气”和“北京今天天气怎么样”规范化之后应该是同一个 key这样命中率能提升一大截。3. Redis 在 Agent 架构里的正确打开方式3.1 别把 Redis 当唯一存储新手最容易犯的错就是把 Redis 当数据库用所有状态都往里塞结果一重启全没了或者内存一满就开始淘汰关键数据。Redis 在 Agent 架构里的定位应该是加速层不是真相层。我的做法是会话的权威数据存在关系库或者文档库Redis 只存热数据的副本。Redis 挂了Agent 降级跑慢一点但不出错Redis 数据丢了从权威存储重建就行。这样设计的好处是容错性强不会因为缓存层故障导致整个 Agent 不可用。具体到配置我会给不同用途的 key 分不同的 Redis 实例或者至少分不同的 db避免互相影响。会话上下文一个实例工具缓存一个实例限流计数一个实例。听起来有点重但线上出问题的时候你就知道隔离的价值了。3.2 数据结构选型别只会用 String很多人用 Redis 就是SET和GET其实 Agent 场景下其他数据结构能解决很多问题。Hash适合存会话上下文。一个会话一个 hashfield 是消息序号或者消息类型这样更新单条消息不用整体重写也方便只取最近 N 条。Sorted Set适合做带时间序的缓存比如“最近 100 条工具调用记录”score 用时间戳取的时候按范围拿天然有序。List适合做消息队列或者固定长度的滑动窗口比如限流用的请求记录。Set适合做去重比如“这个会话已经调用过哪些工具”避免重复调用。我实测下来会话上下文用 Hash 定期裁剪比整体 String 存储省内存 30% 以上而且读取最近几条消息的速度快很多。3.3 序列化方式的选择Agent 缓存的数据结构通常比较复杂嵌套的 dict、list、甚至自定义对象。序列化方式选不对要么占内存要么反序列化慢要么兼容性差。我一般这么选纯文本、简单结构直接用 String不序列化结构化数据、需要跨语言JSON可读性好调试方便高频读写、对性能敏感MessagePack 或者 Protobuf体积小速度快Python 内部对象pickle但要注意版本兼容和安全问题注意pickle 反序列化有安全风险绝对不要反序列化不可信来源的数据。Agent 缓存的数据如果可能被外部影响一律用 JSON。序列化这块还有个坑对象结构变更后的兼容性。你今天存的是{name, age}明天代码改成{name, age, email}老缓存反序列化就会缺字段。我的做法是缓存 value 里带一个 version 字段读取时检查版本不匹配就当作未命中重新计算。4. 缓存 Key 设计与失效策略实战4.1 Key 命名规范Key 设计看着简单其实是缓存治理的地基。我见过太多项目 key 命名混乱最后没人敢删、没人知道哪个 key 对应哪个业务。我的命名规范是{业务}:{对象类型}:{标识}:{版本}。比如agent:session:u12345:v1 agent:tool:weather:beijing:20240101:v1 agent:rag:query:md5hash:v1冒号分隔层级清晰方便用SCAN按前缀批量操作。版本号放在最后结构变更时直接升版本老 key 自然过期不用手动清理。Key 里绝对不要放原始用户输入一是太长占内存二是可能包含特殊字符三是隐私问题。用户输入一律先 hash 再当 key 的一部分。4.2 TTL 设置有讲究TTL 设太短命中率低设太长数据陈旧。Agent 场景下我一般这么设会话上下文跟会话超时时间一致比如 30 分钟无活动就过期工具调用结果按数据变化频率定天气 10 分钟汇率 1 分钟静态配置 1 小时RAG 检索结果1 到 6 小时取决于知识库更新频率LLM 推理结果谨慎一般不超过 5 分钟或者干脆不缓存这里有个技巧TTL 加随机抖动。如果一批 key 同时写入同时过期会造成缓存雪崩大量请求同时打到后端。我一般给 TTL 加 ±10% 的随机值错开过期时间。import random def jitter_ttl(base_ttl): jitter int(base_ttl * 0.1) return base_ttl random.randint(-jitter, jitter)4.3 主动失效 vs 被动过期被动过期就是等 TTL 到简单但不够及时。主动失效是在数据变更时立即删除或更新缓存及时但需要维护失效逻辑。Agent 场景下我建议两者结合常规数据靠 TTL 被动过期关键数据在源头变更时主动失效。比如知识库文档更新了要主动删掉相关的 RAG 缓存用户修改了偏好设置要主动删掉会话上下文缓存。主动失效的难点是找到所有相关的 key。我的做法是维护一个反向索引记录“哪个数据变更影响哪些 key 前缀”变更时按前缀批量删。这个索引本身也可以放 Redis但要注意它自己的一致性。4.4 缓存穿透、击穿、雪崩的应对这三个经典问题在 Agent 场景下更严重因为 Agent 的请求往往更重、更慢。穿透查询不存在的数据缓存不命中每次都打到后端。Agent 场景下常见于用户问了一个知识库里没有的问题。应对方法是缓存空结果用一个特殊标记表示“查过了没有”TTL 设短一点比如 1 分钟。击穿热点 key 过期瞬间大量请求同时打到后端。应对方法是加互斥锁只让一个请求去重建缓存其他请求等待。或者热点 key 干脆不过期靠后台任务定期更新。雪崩大量 key 同时过期。应对方法就是前面说的 TTL 加抖动再加上多级缓存兜底。import redis import time r redis.Redis() def get_with_lock(key, rebuild_func, ttl300): value r.get(key) if value is not None: return value lock_key flock:{key} # 尝试获取锁避免击穿 if r.set(lock_key, 1, nxTrue, ex10): try: value rebuild_func() r.set(key, value, exttl) return value finally: r.delete(lock_key) else: # 没拿到锁短暂等待后重试 time.sleep(0.1) return r.get(key) or rebuild_func()5. 高并发下 Agent 缓存的稳定性保障5.1 连接池配置别用默认值Redis 客户端默认连接池通常很小Agent 高并发场景下根本不够用。我一般这么配import redis pool redis.ConnectionPool( hostlocalhost, port6379, max_connections200, # 按并发量调整 socket_timeout2, # 读写超时别设太长 socket_connect_timeout1, # 连接超时 retry_on_timeoutTrue, health_check_interval30 # 定期健康检查 ) r redis.Redis(connection_poolpool)max_connections怎么估大概是 QPS 乘以平均操作耗时秒再留 50% 余量。比如 1000 QPS每次操作 2ms那就是 1000 * 0.002 2留余量设 10 就够。但 Agent 场景下操作可能更复杂我一般直接设 100 到 200。socket_timeout特别重要千万别用默认的无限等待。Agent 请求本身就有超时缓存操作再卡住整个链路就废了。我一般设 1 到 2 秒超时就走降级逻辑。5.2 超时和降级的处理缓存超时了怎么办绝对不能让它拖垮整个 Agent。我的原则是缓存操作失败一律降级不抛异常。def safe_cache_get(key): try: return r.get(key) except redis.TimeoutError: # 记录监控指标但不影响主流程 metrics.incr(cache_timeout) return None except redis.RedisError as e: metrics.incr(cache_error) return None降级之后Agent 会走正常的计算路径慢一点但能出结果。同时要打监控指标超时率超过阈值就告警说明 Redis 出问题了。5.3 分布式锁在 Agent 里的正确用法Agent 场景下分布式锁主要用在两个地方一是防止同一个会话被并发处理二是防止缓存重建时的击穿。用 Redis 做分布式锁有几个坑锁要设过期时间否则客户端挂了锁永远不释放释放锁要校验持有者不能直接DEL否则可能删掉别人的锁锁的过期时间要大于业务执行时间否则业务没跑完锁就过期了import uuid def acquire_lock(key, ttl10): token str(uuid.uuid4()) if r.set(flock:{key}, token, nxTrue, exttl): return token return None def release_lock(key, token): # Lua 脚本保证原子性 script if redis.call(get, KEYS[1]) ARGV[1] then return redis.call(del, KEYS[1]) else return 0 end r.eval(script, 1, flock:{key}, token)提示如果业务执行时间可能超过锁的 TTL需要加锁续期机制看门狗否则锁提前过期会导致并发问题。但续期逻辑本身要小心别搞成死循环。5.4 内存管理和淘汰策略Redis 内存满了会触发淘汰淘汰策略选不对会删掉不该删的数据。Agent 场景下我一般用allkeys-lru或者volatile-lru。allkeys-lru是所有 key 都可能被淘汰适合缓存数据都可以重建的场景。volatile-lru是只淘汰设了过期时间的 key适合有些 key 不能丢的场景。但更好的做法是主动管理内存别等 Redis 淘汰。我会定期统计各类 key 的内存占用对占用大的类别做优化比如压缩 value、缩短 TTL、减少缓存粒度。# 查看内存使用 redis-cli info memory # 查看大 key redis-cli --bigkeys # 按前缀统计 key 数量 redis-cli --scan --pattern agent:session:* | wc -l6. 线上问题排查与避坑实录6.1 常见问题速查表现象可能原因排查方法解决思路缓存命中率骤降key 设计变更、TTL 太短监控命中率曲线检查 key 生成逻辑内存持续增长大 key、无 TTL、泄漏--bigkeys、info memory拆分大 key、补 TTL请求超时连接池不足、慢查询慢日志、连接数监控扩连接池、优化命令数据不一致主动失效遗漏对比缓存和源数据补失效逻辑、缩短 TTL缓存雪崩TTL 集中过期看过期时间分布加随机抖动锁不释放客户端崩溃、TTL 太长看锁 key 的 TTL缩短 TTL、加续期6.2 我踩过的几个真实坑坑一用用户输入直接做 key。早期我图省事把用户问题直接当 key结果 key 又长又乱还因为包含特殊字符导致 Redis 报错。后来改成先 hash 再拼前缀问题解决。坑二会话上下文整体覆盖写。一开始每次更新会话都是把整个上下文序列化后SET会话长了之后每次写入都是几十 KB网络和内存压力都大。改成 Hash 结构按消息存只更新新增的消息性能提升明显。坑三忘了给缓存加版本号。有次改了数据结构上线后老缓存反序列化全部失败Agent 直接不可用。后来加了 version 字段结构变更时升版本平滑过渡。坑四分布式锁没校验持有者。有次业务执行超时锁自动过期另一个请求拿到锁开始执行第一个请求执行完直接把锁删了导致第三个请求也能拿到锁三个请求并发跑。后来加了 token 校验才解决。6.3 监控指标要盯哪些缓存这块我必看的指标命中率低于 80% 就要查原因平均耗时超过 5ms 要关注超过 20ms 要优化超时率超过 0.1% 要告警内存使用率超过 80% 要扩容或清理连接数接近 max_connections 要调整大 key 数量定期扫描及时拆分这些指标我会接到监控面板上设置阈值告警。Agent 缓存出问题往往不是突然的而是指标慢慢劣化早点发现能避免大事故。6.4 压测怎么做才靠谱上线前一定要压测但 Agent 的压测和普通接口不一样。普通接口压测看 QPS 和延迟Agent 还要看缓存命中率、内存增长、长尾延迟。我的压测方法是先用真实流量回放模拟真实请求分布然后逐步加压观察各项指标重点看缓存命中率是否稳定、内存是否持续增长、P99 延迟是否可接受。压测时要注意缓存预热冷缓存下的性能和热缓存差很多。我会先跑一轮让缓存热起来再开始正式压测。7. 几个容易被忽略的细节7.1 缓存和 Agent 记忆的区别很多人把缓存和 Agent 的长期记忆搞混。缓存是加速用的丢了可以重建记忆是 Agent 的状态丢了就真的丢了。两者存储方式、生命周期、一致性要求都不同不要混在一起设计。我的做法是记忆存持久化存储缓存只存记忆的热副本。Agent 读取时先查缓存没有再查持久化存储并回填缓存。7.2 多租户场景的隔离如果 Agent 服务多个租户缓存 key 一定要带租户标识否则数据串了就是大事故。我一般把租户 ID 放在 key 的最前面方便按租户批量清理。def make_key(tenant_id, category, identifier): return f{tenant_id}:{category}:{identifier}7.3 缓存预热策略冷启动时缓存是空的所有请求都打到后端容易把后端打挂。我的做法是服务启动后异步预热热点数据比如常用工具的配置、高频 RAG 查询的结果。预热要注意别把后端打挂控制并发分批加载。我一般用单独的线程池做预热限速执行。7.4 缓存数据的清理下线功能或者改架构时老缓存要清理否则占着内存还可能被误读。我一般用SCAN按前缀批量删注意别用KEYS会阻塞 Redis。# 用 SCAN 批量删除避免阻塞 redis-cli --scan --pattern agent:old:* | xargs -L 100 redis-cli DEL清理前一定要确认这些 key 真的没用了删错了就是事故。我一般先在测试环境验证再上生产而且分批删观察监控。8. 写在最后的一点个人体会做 AI Agent 的缓存治理这两年我最大的感受是缓存不是加出来的是设计出来的。一开始就规划好缓存什么、怎么存、怎么失效比事后打补丁省事得多。还有一点别迷信“最佳实践”。别人的 TTL 设 5 分钟你的场景可能 30 秒就够别人的 key 设计你的业务可能完全不适用。多观察自己的监控数据用数据说话比抄配置靠谱。最后分享一个小技巧给缓存加一个“调试模式”开启后记录每次缓存操作的 key、命中情况、耗时。线上排查问题时打开能省很多事。平时关掉不影响性能。这个功能我每个项目都会加救过我好几次。