这篇文章把 Redis 在 Agent 运行时里干的四件活拆开来看:短期记忆、会话缓存、限流、分布式锁。重点聊每件事的数据结构选型、常见踩坑,以及缓存层与真相层之间那条边界,给 Agent 平台的缓存设计留一份底稿。

一、Agent 运行时为什么把 Redis 放得离模型这么近
大模型没有状态这件事,这个系列已经念叨过两次。总览篇说它逼出了五类中间件,上一篇说 MySQL 怎么接住台账,这一篇轮到离模型最近的那一层。每次请求都要把上下文重新喂给模型,token 按次计费,首字延迟跟着上下文长度走,于是一个朴素的问题浮出来:有些数据明明每个请求都要用,为什么要每次都从数据库里现查现拼?Redis 的位置就卡在这里。
微秒级的读取速度加上原生的过期机制,让它天然适合贴着运行时进程部署,承接访问频繁、生命周期又短的热数据。
不是所有数据都配进 Redis,判断标准可以压成三个问题:丢了会不会出事故、是不是每个请求都要碰、生命周期是不是以分钟计。三个里有两个答案是否定的,这条数据就该留在 MySQL 里。会话的权威记录、任务的状态机、扣费台账,这些是真相,真相不进缓存。反过来说,Redis 是运行时的加速层,不是真相来源,这句话值得在每次设计缓存键的时候默念一遍。
职责划清楚还有一个附带好处:缓存层的故障不再等于事故。Redis 挂了,运行时降级为直查数据库,慢一点但账不乱。这个优雅降级能不能成立,取决于上线前有没有把 Redis 当成可选依赖来设计,而不是当成第二份存储。
算一笔账感受下差距。一次上下文请求,从 Redis 取热数据是微秒级,从数据库现查再拼装是十毫秒级,差距落在用户体验上不算明显,真正的大头在重复构建的成本:提示词模板、工具清单、历史摘要这些东西每个请求都一样,放进缓存一次构建反复使用,排队延迟和构建开销一起省掉。规模上去之后,这一层省的是真金白银。
二、短期记忆不是长期记忆的缩小版
先掰一个容易混的概念。短期记忆和长期记忆服务的时间尺度完全不同。长期记忆在总览篇说过,靠向量库做语义召回,跨度按周按月算;短期记忆是当前会话的工作区,装着最近几轮对话、当前任务的中间结论、刚拿到的工具返回,跨度按分钟算,会话结束就可以扔。放向量库是检索问题,放 Redis 是生命周期问题,混着设计两头不讨好。
数据结构的选择基本是送分题。会话状态用 Hash,用户偏好、当前模式这类字段各占一个 field,改哪个写哪个;最近 N 轮对话用 List,LPUSH 进 LTRIM 出,天然封顶;工具返回用 String 加 TTL,同一个查询短时间内不重复打工具。真正容易翻车的是 TTL 纪律:没有 TTL 的会话键,和内存泄漏是同义词。上线时人人记得加,三个月后没人记得查,键空间悄悄膨胀,内存告警半夜炸响,这类案例线上不新鲜。
容量估算同样值得提前做。一条会话热数据平均几 KB,日活一万的产品峰值并发按千算,也就吃掉几个 GB;怕的是没纪律的键堆积,每天几百万个漏设过期时间的键,一个月就能吃掉几十 GB。上线前算清单键尺寸、日增键量、内存水位三条线,告警阈值定在七成,比事后扩容从容得多。
会话缓存和会话存储也要分清。MySQL 里的会话表是存储,Redis 里的是缓存。缓存放最近会话的热字段,运行时优先读缓存,未命中回源重建。写路径建议用 write-through,先写库再更新缓存,宁可多一次写,也别让缓存变成另一份需要对账的数据。两份数据开始各说各话的那天,排查成本会超出所有人的预期。

三、限流不是可选项,是预算的闸门
Agent 的流量形状和传统接口不一样。用户会手滑重试,Agent 自己会循环,一次自我修正连着调五次模型很正常,多工具并行一开,瞬时并发直接翻倍。没有限流的 Agent 系统等于没有刹车的车,滑的不只是一个接口,是整个 token 预算和下游工具的可用性。
实现上有三个档位。固定窗口用 INCR 加 EXPIRE,两条命令搞定,缺点是窗口边界处可能放过双倍突发;滑动窗口用 ZSET 按时间戳记请求,精度上去了,内存和清理成本跟着来;令牌桶用一段 Lua 脚本,允许小额突发又卡住总量,多数场景它是最平衡的选择。限流维度要从第一天就分开设计,按用户、按租户、按工具、按模型端点各算各的,只设一个全局阈值,等于让所有租户共用一个水箱。
租户配额还有一个公平性问题:大客户一次批量任务可能瞬间占满全局配额,把小客户的请求全部挤掉,这就是为什么配额要按租户隔离再叠一层全局保护。实现上用 Hash 记每个租户的窗口计数,配合键级过期自动清理旧计数器,内存开销几乎可以忽略。
被限流之后的行为同样重要。返回 429 并带上 Retry-After,让运行时把请求放进排队,或者降级到更便宜的模型,比干等到超时体面得多。限流的本质,是把不可控的突发变成可控的排队——排队不可耻,失控才可耻。

四、分布式锁管效率,幂等管正确性
多实例部署之后,有些事必须保证只有一个人干。定时汇总任务不能每个实例都跑一遍,同一条任务不能被两个消费者同时执行,协调这些的传统答案就是分布式锁。Redis 的实现一行命令:SET 资源名 NX EX 过期时间,原子性拿到;释放不能 DEL 了事,得用一段 Lua 先比对持有者标记再删,否则 A 实例超时后释放了 B 实例刚拿到的锁,并发事故从天而降。
过期时间是双刃剑。设短了,长任务跑到一半锁没了,第二个实例进来踩脚;设长了,实例崩溃后其他实例干等。常见解法是看门狗:持有者定期续期,进程死了续期停,锁按 TTL 自然释放,死锁和踩脚两头都顾住。锁的粒度也值得琢磨,粗到全局一把锁省心但吞吐归一,细到每个资源一个键灵活但管理复杂,多数场景按业务对象粒度就够。Redlock 那场著名的争论,一句话总结:绝大多数团队用不上五节点仲裁,单实例 Redis 配合合理 TTL 和幂等设计,覆盖九成需求绰绰有余。
生产环境还有一个真实的坑:锁内逻辑调用外部服务超时,持有者以为自己死了主动释放,外部服务其实还在跑,这时第二个实例拿到锁开始执行,第一个实例的调用又成功返回,一次任务跑了两遍。根因不在锁,在于任务边界没有用幂等键框住。这一节反复强调锁和幂等的关系,是有原因的。
还有一层要摊开说,锁负责效率,幂等负责正确性,两道保险缺一不可。锁总有失效的概率,网络抖动、时钟漂移、GC 停顿都可能让两个持有者短暂共存,这时候兜底的不能是运气,是上一篇说过的幂等键和乐观锁。设计时按锁一定会失效来推演,系统才立得住。

五、当 Redis 变成关键路径,这四件事要想清楚
用着用着,Redis 会从可有可无变成关键路径,四个信号出现时值得停下来想清楚。淘汰策略排第一:allkeys-lru 会连锁和会话一起淘汰,缓存被悄悄清掉是性能问题,锁被悄悄清掉是正确性问题,缓存类和状态类数据建议分实例部署,各配各的策略。持久化排第二:纯缓存可以关,带会话状态的实例至少开 AOF。热 key 排第三:一个爆点键能打满单个分片,本地缓存垫一层或者把键拆散。集群迁移排第四:多键操作和 Lua 脚本在集群下处处受限,键的散列设计要在上量之前想好。
监控这条线也提前铺好:内存水位、键总数、命中率、慢查询四项核心指标接进告警,命中率掉到七成以下先查淘汰配置,慢查询冒头先查大键。大键扫描用 RDB 分析工具定期跑,发现苗头在业务低峰期拆键,比线上内存耗尽之后救火舒服太多。
把缓存当存储的系统,迟早会被一次淘汰策略教育,这句话不是吓唬人。健康的状态是:Redis 全挂,系统变慢但不出错,任务不重不漏,台账分毫不差。想验证就做一次拔线演练,比在文档里写一百遍降级策略都管用。
收尾留一个观察:这一篇四个场景,技术选型都不难,难的是纪律——TTL 必须写、容量必须估、降级必须演练。三件小事立住了,Redis 就是运行时最称手的搭档。