“Redis 已正式接入 AI”,这阵子到处都是类似的标题。我刚开始以为又是一篇通稿,直到自己做 LLM 应用时认真看了 Redis 生态的进展,才发现这句话落在工程上其实是三个方向:一是 Redis 开始支持向量检索,让数据查询从“关键词匹配”进化到“语义相似”;二是 Redis 生态里出现了托管模型推理的模块,虽然现在用得不多,但思路很超前;三是反过来,开发者和运维开始用大模型辅助 Redis 的日常治理,比如写 Lua 脚本、分析慢日志。这篇文章不聊玄的,只聊我实际配置过的命令、踩过的坑,以及一套可以直接抄走的语义缓存实现。
1. Redis 在 AI 应用里到底扮演了几个角色
1.1 越用越重的边缘缓存层
做 LLM 应用的人应该有一个共同感受:Redis 几乎成了默认依赖。用户请求进来,先查 Redis 缓存,没命中再调大模型接口;模型返回结果后,再写回 Redis。这个模式跟传统 Web 缓存一模一样,但价值完全不同——大模型接口按 token 计费,延迟动辄几秒,一次缓存命中省下的不只是时间,还有真金白银。
我在项目里通常会在 Redis 里做两层:第一层是“请求-响应缓存”,用户问过的问题,短时间再次出现就直接返回;第二层是“中间数据缓存”,比如把 embedding 向量、知识库分块结果、会话上下文都放进去。这两层没有 Redis 撑着,系统并发一上来,大模型接口会被打爆,账单也会先炸。
1.2 向量索引能力补齐了 AI 检索的短板
传统 Redis 的查询是精确匹配或正则匹配,这对普通业务够了,但对 AI 场景远远不够。用户说“怎么退订会员”,知识库里存的是“取消订阅服务套餐”,关键词完全对不上,传统查询只能返回空。Redis Stack 引入向量检索之后,可以把 text、图片 embedding 成向量,再按向量距离做 KNN 搜索,从“字面匹配”变成“语义匹配”。
这个能力让 Redis 在 AI 应用里不再只是缓存,而是一个轻量级的向量数据库。对比专门的向量库,Redis 保留了原有的缓存、队列、分布式锁能力,一套基础件搞定多个角色,对小团队来说省了很多运维成本。
1.3 模型推理托管的历史尝试与现在的取舍
可能有人记得 Redis 生态里有个 RedisAI 模块,可以直接在 Redis 上加载 PyTorch、TensorFlow、ONNX 模型,通过命令做推理。它的设计思路是把“数据”和“算力”放在同一个地方,减少数据搬移,思路在当时很惊艳。但实际用下来,模型训练和推理框架迭代太快,把模型跑在数据库里的灵活性不够,社区活跃度也明显不如专门的推理服务。
所以现在的常见做法是:推理交给外部模型服务,Redis 负责向量存储、缓存、会话状态和限流。RediSearch 的向量搜索成为主流入口,RedisAI 更多是一个思路参考。如果你在旧文档里看到 AI.MODELSET 这类命令,知道有这回事就行,新项目我不建议再往这个方向投入。
2. 向量检索接入:让 Redis 读懂语义的第一步
2.1 环境准备与模块确认
要启用向量检索,前提是 Redis 实例带 RediSearch 模块。最省事的方式是直接起一个 Redis Stack 镜像,模块都打包好了。我用的是 redis/redis-stack-server 镜像,一条命令就能跑起来:
docker run -d --name redis-stack -p 6379:6379 redis/redis-stack-server:latest跑起来后先确认模块版本:
redis-cli MODULE LIST看到 RediSearch 相关模块就说明环境到位。如果用的是云厂商的 Redis,记得在控制台单独启用搜索模块,而且要确认版本至少是 7.2,太老的版本可能不支持向量能力。这里有个容易忽略的点:向量搜索依赖 DIALECT 2 及以上的查询语法,命令字面上看不出问题,但老客户端默认 dialect 为 1,查询会报语法错误。
2.2 创建向量索引:字段、类型、距离度量
建索引之前要明确两件事:向量维度是多少,用什么距离度量。文本向量一般是 384、768 或 1536,取决于 embedding 模型;距离度量常用 COSINE,适合文本语义相似度,L2 适合图像或数值型特征。
我实际用的建索引命令:
FT.CREATE idx:docs ON HASH PREFIX 1 doc: SCHEMA title TEXT embedding VECTOR HNSW 6 TYPE FLOAT32 DIM 384 DISTANCE_METRIC COSINE这里 HNSW 后面的数字 6 表示后面跟着 6 个参数。HNSW 适合上万条以上的数据,检索速度更好,但内存占用更高;数据量不大可以用 FLAT,暴力计算但内存省。dim 必须和 embedding 模型的输出维度一致,不一致会导致索引写入报错。
2.3 写入向量与 KNN 查询
向量在 Redis 里的存储格式是二进制字节,不是 JSON 数组。我用 Python 写入时,要先把 float 列表转成 bytes:
import redis import struct client = redis.Redis(host="localhost", port=6379, decode_responses=False) def pack_vector(vec): return struct.pack(f"{len(vec)}f", *vec) vec_bytes = pack_vector([0.12, 0.34, ...]) # 384 个 float client.hset("doc:1001", mapping={ "title": "取消订阅会员", "embedding": vec_bytes })查询端用 KNN 语法:
FT.SEARCH idx:docs "*=>[KNN 5 @embedding $vec AS score]" PARAMS 2 vec <vec_bytes> SORTBY score ASC DIALECT 2重点理解 score:在 COSINE 模式里,score 是向量之间的距离,越小越相似,不是通常理解的相似度分数。我第一次用的时候按“分数越大越好”去排,结果返回的完全不对。后来才反应过来,Redis 返回的是余弦距离,1 减去余弦相似度才是这个 score。
2.4 选 FLAT 还是 HNSW
小数据集用 FLAT 没有任何问题,暴力扫描反而省内存。数据集超过几万条,或者查询并发高,就上 HNSW。HNSW 在建索引时可以调 M 和 EF_CONSTRUCTION,M 表示每个节点的连接数,EF_CONSTRUCTION 表示建图时的候选数,调大后召回率提升但内存和构建时间也增加。我的经验是默认参数先跑,等召回率不达标再逐步调 EF_CONSTRUCTION,不要一上来拉满。
3. 反向接入:让大模型在 Redis 日常治理里干活
3.1 用大模型生成可落地的 Lua 脚本
Redis 的 Lua 脚本写了容易,写好很难。我自己最常用的是限流脚本,固定窗口计数。让大模型生成一遍,再人工加两处修正,效率非常高:
local key = KEYS[1] local limit = tonumber(ARGV[1]) local window = tonumber(ARGV[2]) local current = redis.call("INCR", key) if current == 1 then redis.call("EXPIRE", key, window) end if current > limit then return 0 end return 1这个脚本的逻辑很简单,但有几个模型容易忽略的细节:KEYS 和 ARGV 不能混用,所有 key 必须通过 KEYS 数组传入;INCR 之后要立即判断是不是第一次,只有第一次才设置过期时间;limit 和 window 要用 tonumber 转成数字。这些细节用自然语言描述给大模型时一定要点出来,模型知道这些约束后生成的代码基本能直接跑。
3.2 慢查询日志与热 Key 定位
Redis 的慢日志是个宝藏,但平时没人看。我遇到线上延迟突增时,第一件事就是拉慢日志:
redis-cli SLOWLOG GET 50 redis-cli SLOWLOG RESET慢日志输出里有执行时间、命令参数、来源 IP。原始输出很乱,我的做法是把前 20 条慢日志复制给大模型,让它按命令类型、涉及 key 的模式、耗时分布做归类。模型能从一堆日志里看出“某个固定前缀的 key 频繁执行 KEYS *”或“热 key 集中在某几个固定 key 上”,这比人肉一条条看快得多。
定位热 key 还可以用 redis-cli 自带的扫描工具:
redis-cli --bigkeys --hotkeys大模型不太擅长实时采集,但很擅长解析采集结果。把 bigkeys 输出丢给模型,让它根据 key 类型和大小给出“先拆分大 value,再设置过期时间”这类建议,它的建议基本比我文档里翻到的方案还要具体。
3.3 缓存键的命名与数据治理
缓存键乱是 Redis 运维最头疼的问题。项目里动不动就上万种 key,命名方式全靠开发者当天心情。现在我让大模型充当“命名评审员”:把线上采样到的 key 列表交给模型,让它按业务模块分组、标记重复和不规范的 key,给出统一的命名规则。比如规定所有 key 必须用业务:实体:id 这样的冒号分层结构,而且每层语义清晰。
模型还能帮我生成扫描脚本,比如找出超过 30 天未访问的 key,或者清理特定前缀下所有 key。这类治理工作以前需要人工写脚本,现在描述需求加脚本生成,基本十分钟搞定。
4. 完整落地:用 Redis 给 AI 应用做一层语义缓存
4.1 语义缓存解决的核心问题
普通缓存只能命中完全一样的问题,用户换一种说法,缓存就失效了。语义缓存把用户问题先转成向量,在缓存里做相似度搜索,和已有问题语义相近就直接返回历史答案。实测下来,在客服问答场景中能省下 40% 以上的模型调用量,响应时间从 3 秒降到 20 毫秒以内。
这里要注意的是,语义缓存不是替代向量数据库,而是给高频、重复性的问题加一道护城河。相似度阈值设得过高会误命中,设得过低则命中率太低,需要根据业务容忍度慢慢调。
4.2 索引结构与写入流程
先建索引:
FT.CREATE idx:semcache ON HASH PREFIX 1 sc: SCHEMA question TEXT answer TEXT business_tag TAG embedding VECTOR FLAT 6 TYPE FLOAT32 DIM 384 DISTANCE_METRIC COSINE写入时把问题、答案、业务标签、embedding 一起写进 Hash,并设置 TTL:
def cache_answer(biz_tag, question, answer, embedding): key = f"sc:{biz_tag}:{uuid4().hex}" client.hset(key, mapping={ "question": question, "answer": answer, "business_tag": biz_tag, "embedding": pack_vector(embedding) }) client.expire(key, 900)TTL 设 15 分钟比较合理,既能保证时效性,又不会让缓存无限膨胀。
4.3 查询策略与距离阈值
查询时把用户问题向量化,在缓存索引里做 KNN 搜索。由于 COSINE 模式下 score 是距离,所以阈值通常在 0.08 到 0.15 之间。0.08 意味着余弦相似度 0.92,这个值在客服领域比较安全,几乎不会出现语义不相关的误命中;如果希望更高命中率,可以放宽到 0.15,但要接受一部分近义但不同答案的场景走缓存。
def query_cache(biz_tag, question_embedding, threshold=0.10): res = client.ft("idx:semcache").search( f"(@business_tag:{{{biz_tag}}})=>[KNN 5 @embedding $vec AS score]", query_params={"vec": pack_vector(question_embedding)}, dialect=2, ) for doc in res.docs: if float(doc.score) <= threshold: return doc["answer"] return None4.4 缓存写入与命中的协同细节
写入动作不要放在请求关键路径上,否则 embedding 服务的耗时会被用户感知到。我通常把写入放到消息队列或异步任务里,查询场景直接走 Redis。还有一个很容易踩的坑:Redis 的 TTL 到期删除是惰性的,如果缓存 key 被大量创建,内存可能要等到压力上来了才被回收,所以一定要设置 maxmemory 和淘汰策略。我的建议是配 maxmemory-policy allkeys-lru,防止缓存写穿内存。
4.5 实测数据与调参方向
这套结构在我参与的一个客服问答项目里跑了三个月。热门问题集中在几十个固定问题上,embedding 差异不大,阈值 0.10 时命中率在 35% 到 45% 之间。最明显的变化是高峰期的模型调用量明显下降,大模型接口的付费账单不再飙升。碰到的问题是不同用户的提问方式差异太大时,阈值要放宽到 0.15 才有效果,但这时偶尔会把两个相似但不相同的问题混在一起。后来在答案里加了业务版本号,缓存 key 里带上版本维度,问题才解决。
5. 接入 AI 后的几个血泪教训
5.1 不要把 Redis 当专用向量数据库用
Redis 做向量检索的定位是“轻量级、够用就好”。如果你的向量数据超过百万条,且对召回率有很高要求,Redis 的 HNSW 性能和内存效率会明显吃力,这时应该考虑专门的向量数据库。Redis 的优势是缓存、限流、会话、向量一把抓,但对重度向量检索场景,术业有专攻是对的。
5.2 向量维度错误是最隐蔽的问题
embedding 模型换了一个版本,输出维度从 384 变成 768,结果索引建不了,或者老数据全部查不到。更隐蔽的是模型本身对同一句话的输出也可能有小幅漂移。我的建议是:每次升级 embedding 模型,必须重建向量索引,老数据全部重新生成向量,不要指望余弦相似度能自动兼容不同版本的向量。
5.3 分布式集群下的向量索引要额外关注
Redis Cluster 模式下,RediSearch 需要每个分片都有对应的索引。KNN 查询会先在每个分片上求 Top K,再合并结果,跨分片的全局最优排名会比较麻烦。如果业务初期数据量不大,先用单实例或 Redis Stack 模式跑起来,等数据规模上来再考虑集群方案。不要一上来就 Cluster,给自己增加排查难度。
5.4 分布式锁在 AI Agent 场景里的并发问题
AI Agent 任务调度时,多个 agent 实例经常抢同一个任务。我用 Redis 分布式锁锁任务 key,只允许一个实例执行。命令本身很简单:
SET task:lock:<task_id> <owner_id> NX PX 30000但有个问题容易翻车:锁的 value 只存一个随机字符串不行,还要存 owner 标识,释放锁时必须先比对 owner 再删除。否则一个任务执行时间长,锁过期了,第二个实例拿到锁,第一个实例执行完直接 DEL 就把第二个实例的锁删了。多实例场景下,释放锁用 Lua 脚本保证原子性:
if redis.call("get", KEYS[1]) == ARGV[1] then return redis.call("del", KEYS[1]) else return 0 end5.5 安全基线不能因为“只是缓存”就放松
Redis 接入 AI 后,里面存了 embedding、答案、用户会话,数据价值更高。端口不要暴露到公网,设置强密码,生产环境建议把危险命令重命名或禁用,比如 KEYS、FLUSHALL。我的习惯是所有写入数据的接口都过一层鉴权,Redis 本身只在内网允许访问。
写在最后的一点个人体会
Redis 和 AI 的结合,我最大的感受不是某个新模块多神奇,而是“数据基础设施”和“模型能力”终于开始互相成就在一起。Redis 让大模型应用跑得更省、更快、更稳;大模型也让 Redis 的日常维护从手工敲命令变成了提需求。如果你现在正在做 LLM 应用,我建议先别急着上专门的向量数据库,把 Redis Stack 的向量索引和语义缓存用好,大概率能撑过业务前期的所有场景。等技术债积累到不得不换的时候,再迁移也不迟。