Redis 原生向量检索实战:从 HNSW 索引到语义缓存与 Agent 记忆

发布时间:2026/11/3 17:17:26
Redis 原生向量检索实战:从 HNSW 索引到语义缓存与 Agent 记忆

Redis 和 AI,这两个词放在一起的时候,过去大家第一反应都是“AI 应用把 Redis 当缓存用”。但这次不一样,Redis 官方把 AI 能力真正做进了自己的核心体系里:向量检索不再需要额外挂一个模块当玩具,Redis Stack 里已经提供了开箱即用的向量索引和相似度搜索,Redis 8 还带来了原生的 vector set 类型,能把向量计算直接跑到数据库内核里。这个消息对于我这种后端老油条来说,第一反应不是“又要学新东西”的烦躁,而是“终于可以把架构图里的组件删掉一个”的踏实。

这篇文章不发新闻稿,就讲大白话。我会把自己实际验证过的东西写出来:Redis 接入 AI 到底接在哪儿、HNSW 索引背后是怎么回事、怎么用 Docker 五分钟跑一个语义搜索服务、在业务里做语义缓存和 Agent 记忆要注意什么,最后是我踩过的一堆坑。适合正在做 AI 应用后端、想在架构里省组件的团队,也适合那些刚把 Redis 当缓存用、想往前再走一步的朋友。

1. 这次“正式接入 AI”,接的到底是哪条线

1.1 从缓存组件到 AI 数据底座,Redis 完成了一次身份转换

Redis 在大家心里的定位一直是“内存缓存”。早年我们用它做页面缓存、Session 存储、接口防重,最多再加个分布式锁。它解决的问题很纯粹:把热数据放到内存里,让访问速度从几十毫秒降到几毫秒。

但 AI 应用带来了一类新数据:向量。不管是大模型的 embedding、图片的特征向量,还是推荐系统里的用户画像向量,本质上都是几百上千维的浮点数组。这类数据的特点是量大、内存敏感、查询方式不是“等于”而是“找最像的”。传统的 Redis 数据结构面对这种查询毫无办法,你总不能把一百万个 768 维向量全拉出来在应用层逐个算余弦距离吧。

所以 Redis 官方的打法很清晰:把向量检索做成数据库的一等能力。你在 Redis 里存普通业务数据的同时,可以给某个字段建向量索引,然后执行“找和这个向量最接近的 K 条记录”的查询。索引、距离计算、排序全部在 Redis 进程内完成,不用把数据导给别的组件。

1.2 面向 AI 应用的三类核心能力

我梳理了一下,Redis 现在的 AI 相关能力基本分成三块:

第一块是向量索引与相似度搜索,基于 Redis Stack 的 RediSearch 模块。你可以在 Hash 或者 JSON 数据类型上创建向量字段,配合 HNSW 索引做近邻检索。一套命令下来,效果和一个专门的向量数据库差不多。

第二块是语义缓存。这一点很多人容易忽略,但它其实最实用。大模型接口按 token 收费、按响应时间计费,如果同样的用户问题能命中缓存直接返回结果,成本和延迟都会明显下降。Redis 做语义缓存的逻辑是:把文本变成向量存进 Redis,新请求来了先算距离,命中一个相似度很高的历史缓存就直接复用。这是一个把缓存老本行和 AI 结合得最自然的方向。

第三块是 Agent 会话记忆。多轮对话、AI Agent 处理复杂任务时,往往需要把上下文、中间状态、历史结果存下来。Redis 的 String、Hash、Stream 数据结构天然适合做这件事,向量字段再加一层,还能支持“根据语义召回相关的历史对话”这种高级记忆能力。

1.3 为什么这件事值得后端团队认真看

我的判断是,Redis 接入 AI 最大的价值不是“又多了一个向量数据库”,而是让架构少了一个组件。

很多团队做 AI 应用时,目前的标配是:PostgreSQL 存业务数据,Milvus 或 Elasticsearch 存向量做检索,Redis 做缓存,中间再用消息队列串起来。组件一多,运维成本、同步延迟、故障点全都上来了。Redis 如果能把高频热数据的向量检索也承接住,那么中小团队完全可以先不引入专门向量数据库,直接用 Redis 撑住上线初期的搜索、召回、去重场景。

尤其要注意的是,向量检索在很多 AI 场景里并不是“海量全量召回”,而是“限量召回候选集”。比如先从一个分类里召回 Top 50 条,再交给大模型精排。这种场景对召回规模要求不高,但对延迟和并发要求极高,正好是 Redis 的强项。用大炮打蚊子不一定准,Redis 这种快刀切小菜的方式反而更对味。

2. 先把原理吃透:向量化、HNSW 与相似度检索

2.1 为什么 AI 场景需要“相似度搜索”

传统数据库的查询逻辑是精确匹配,但 AI 场景的信息形态不是精确的。比如用户问“推荐一款续航长的耳机”,数据库里并没有哪个字段恰好等于这句话,能做的只有把用户问题和商品描述都转成向量,然后看哪个商品向量离这句话向量最近。这就是相似度搜索的由来。

向量化的过程一般靠 embedding 模型完成。文本、图片、音频都可以通过模型映射成固定维度的浮点数组,比如 OpenAI 的 text-embedding-3-small 是 1536 维,很多开源中文模型是 768 维。维度越高,表达的信息越丰富,但存储和计算开销也越大。

Redis 在这儿扮演的角色不是生成向量,而是存向量 + 算距离。你负责用模型生成向量,Redis 负责在毫秒级找到最相似的 K 个。

2.2 HNSW 索引为什么能让检索这么快

如果老老实实把每一对向量都算一遍距离,那就是暴力全扫描。一百万个 768 维向量,单机算一遍余弦距离,延迟直接奔着秒级去了,这在线上没法用。所以需要一个近似最近邻算法,Redis 默认推荐的方案是 HNSW。

HNSW,全称 Hierarchical Navigable Small World,分层可导航小世界图。可以理解成给向量数据建了一张多层的“社交网络图”:底层有海量节点,每往上一层节点越少,最高层只有少数几个“明星节点”。查询的时候,从最高层开始,像顺着熟人网络往下打听一样,先粗粒度定位区域,再一层层细化到最底层找精确邻居。

这个设计的精妙之处在于,它不需要访问全部数据,通常是几十次比较就找到目标区域。实际效果是,百万级数据量的近邻检索单次耗时常常在几毫秒到十几毫秒之间,完全能扛住在线接口的压力。

2.3 距离度量怎么选:余弦、内积还是欧式距离

很多人栽在第一步:距离函数选错了,召回结果怎么看怎么不对劲。Redis 里支持三种常见距离度量:COSINE(余弦距离)、IP(内积)和 L2(欧式距离)。

文本 embedding 场景,绝大多数情况用 COSINE。因为文本向量的绝对大小受句子长度、模型习惯影响,我们更关心的是方向上的相似度,余弦距离正好把这个语义关系捕捉住。我自己的习惯是,凡是用开源的 sentence-transformers 或者 OpenAI embedding 的文本场景,无脑选 COSINE 基本不会出错。

IP 内积在使用归一化向量时,数学上和余弦距离是等价的,但计算效率略高一点。如果你的 embedding 来源已经做了归一化,可以换 IP 省一点 CPU。L2 更适合图像特征这类向量范数有实际意义的场景,比如颜色直方图、SIFT 特征。

一个反直觉的点:COSINE 距离度量下,向量字段在 Redis 内部会做归一化处理,不影响最终查询结果,但会稍微增加写入开销。数据量特别大的时候可以考虑直接预先归一化,然后选 IP。

2.4 索引参数 M、efConstruction、efSearch 怎么设

HNSW 不是零参数算法,建索引的时候有两个关键参数,查询的时候还有一个。

M 是每个节点的最大连接数,默认为 16。M 越大,图越稠密,召回率越高,但内存和索引构建时间也越高。文本检索场景我一般设 16 到 32,超过 48 之后收益就很低了。

efConstruction 是建索引时的动态候选列表大小,默认 100。建索引时调大它,能提高图的质量,但会显著拉长建索引时间。我的建议是数据量百万以内保持默认,离线批量构建场景可以调到 200 到 300,在线增量写入别乱调。

efSearch 是查询时控制搜索广度的参数,默认也是 100。它越大,召回越准,但查询延迟会上升。这个参数可以在每次查询时动态覆盖,比如召回阶段用 100,精排阶段用 200。线上调优时先锁定这个参数,比调 M 见效更快。

这里必须多说一句:参数调优的本质是权衡,没有绝对正确的值,只有适合你数据量的值。如果你发现召回率一直上不去,先不要怀疑模型,先看 efSearch 是不是被压得太低。

3. 实操:5 分钟跑通 Redis 语义搜索

3.1 环境准备:用 Docker 快速部署 Redis Stack

最省事的方式是用官方镜像,不需要自己编译模块。我建议直接用 redis-stack-server 镜像,它已经把 RediSearch 等扩展模块打包好了。

docker run -d --name redis-ai \ -p 6379:6379 \ -v redis-ai-data:/data \ redis/redis-stack-server:latest

启动后先验证一下服务状态,用 redis-cli 查看模块是否加载成功:

docker exec -it redis-ai redis-cli MODULE LIST

如果输出里能看到 search 或 vecsim 相关的模块,说明环境没问题。这一步很多新手会翻车:如果用普通 redis:7 镜像,没有加载任何扩展模块,后面所有向量索引命令都会报错。所以确认镜像带模块是第一要务。

3.2 创建向量索引:命令与参数逐项解释

向量索引和普通索引一样,要先定义好 Schema。下面我用 Hash 类型来演示,因为 Hash 比较贴近传统 Redis 使用习惯。

FT.CREATE idx:code ON HASH PREFIX 1 code: SCHEMA title TEXT content TEXT embedding VECTOR HNSW 6 TYPE FLOAT32 DIM 768 DISTANCE_METRIC COSINE

逐项拆解一下:

  • ON HASH表示索引的是 Hash 类型的数据,PREFIX 1 code:表示只索引 key 以code:开头的记录。
  • SCHEMA后面定义索引字段。title和content是普通文本字段,用于后面的全文过滤。
  • embedding是向量字段,VECTOR HNSW 6中的数字 6 是后续参数个数,紧接着的TYPE FLOAT32表示向量元素是 32 位浮点,DIM 768是向量维度,DISTANCE_METRIC COSINE是距离度量方式。

这里最容易出错的点是“参数个数”。HNSW 6后面的参数必须正好是 6 个,也就是TYPE FLOAT32、DIM 768、DISTANCE_METRIC COSINE这 6 个 token。如果加了别的参数比如M 32,总数就要从 6 改成 8。数错一个,命令直接报语法错误。

3.3 写入向量数据:别把 embedding 存成字符串

向量写入 Hash 的时候,需要把 float32 数组转成二进制字节流,不能直接存成字符串。我用 Python 演示一下标准写法:

import redis from sentence_transformers import SentenceTransformer model = SentenceTransformer("BAAI/bge-large-zh-v1.5") r = redis.Redis(host="localhost", port=6379, decode_responses=False) text = "一款适合通勤的降噪耳机" vec = model.encode(text).astype("float32").tobytes() r.hset("code:1", mapping={ "title": "降噪耳机", "content": text, "embedding": vec })

注意三个细节:第一,模型输出的 numpy 数组必须转成 float32,float64 会导致维度对不上和内存翻倍;第二,.tobytes()之后才能写入 Redis;第三,decode_responses必须设为 False,否则 redis-py 会尝试把二进制流解码成字符串,写入时就会出错。

3.4 相似度检索:手写 KNN 查询语句

查询时用FT.SEARCH配合KNN语法:

vec_q = model.encode("通勤路上用的耳机推荐").astype("float32").tobytes() res = r.execute_command( "FT.SEARCH", "idx:code", "embedding => [KNN 5 @embedding $vec]", "PARAMS", "2", "vec", vec_q, "DIALECT", "3", "RETURN", "3", "title", "content", "vector_score", "SORTBY", "vector_score", "LIMIT", "0", "5" )

这里的关键是KNN 5表示取最相似的 5 条,$vec是查询向量参数,PARAMS 2表示接下来有 2 个参数。DIALECT 3是搜索方言版本,不写这个低版本方言可能不识别向量查询语法。vector_score是 Redis 返回的相似度分数,COSINE 度量下分数越低表示越相似。

我第一次写这条命令的时候没加DIALECT 3,结果报了一堆莫名其妙的语法错误,后来才发现是方言版本不匹配。这一点真的会浪费新手半小时。

3.5 一个端到端的小案例:文档语义检索

光查询不够,我直接给一个完整的文档检索思路,这套结构可以直接搬到很多业务里。

整体流程是:文档入库时切块,每个块生成向量存 Redis;查询时把问题转成向量,从 Redis 召回最相关的 Top K 块;把召回的块拼起来作为上下文送给大模型。伪代码如下:

def recall_documents(question, top_k=3): qv = model.encode(question).astype("float32").tobytes() res = r.execute_command( "FT.SEARCH", "idx:doc", "embedding => [KNN 3 @embedding $vec]", "PARAMS", "2", "vec", qv, "DIALECT", "3", "RETURN", "2", "content", "vector_score" ) # 解析结果,按 vector_score 排序,返回 content 列表 return parse(res)

这个案例的运行效果,我实测在 10 万级文档块的数据量下,单次召回延迟在 5 毫秒以内。对大模型应用来说,这个速度非常舒服,检索开销已经低到可以忽略。

4. 在业务里落地的三个脑洞

4.1 语义缓存:给大模型套一个省钱缓存

大模型接口的延迟和费用一直是痛点,尤其是客服、问答这类高频重复问题场景。传统缓存只能精确命中,用户把问题换一个说法,缓存就失效了。语义缓存解决的就是这个问题。

实现思路很直接:先把用户问题向量化,在 Redis 里检索历史问题向量,如果找到相似度足够高的记录(比如距离小于阈值 0.2),直接把当时的答案返回;没有命中才调大模型,然后把这个问答对写回 Redis。这个方案我在实际项目里验证过,重复问题占比高的时候,接口成本可以降一半以上,延迟也会从秒级降到毫秒级。

这里有一个容易被忽略的问题:阈值设置在 0.2 还是 0.5,决定的是“省钱的胆量”。阈值太严,命中率低,省不了多少钱;阈值太松,答非所问的概率大涨,用户会明显感知到体验变差。我建议先用一批真实用户日志做一个离线命中测试,根据准确率和覆盖率画一个曲线,再选一个平衡点。

4.2 Agent 会话记忆:上下文不再靠拼字符串

AI Agent 处理复杂任务时,最大的问题是上下文管理。把所有历史消息都塞给大模型,成本高且容易被无关信息干扰。更好的办法是分层记忆:短期记忆用 Redis Stream 存最近几轮对话,长期记忆把历史对话向量化存入 Redis,需要时按语义召回相关片段。

Stream 数据类型特别适合做短期记忆,因为它天然支持追加和范围读取,还能设置消息上限。长期记忆则可以直接复用上一节的向量索引。每次 Agent 需要回忆“之前有没有提过某个需求”时,就去向量索引里查一遍。

这个方案我已经在一个内部工具里跑了好几个月,最大的体会是:向量召回的记忆片段,比单纯把全部历史拼进 prompt 的效果好得多,token 消耗也小得多。

4.3 向量场景与传统缓存治理的碰撞

AI 场景不是凭空出现的,它依然躲不过缓存穿透、击穿、雪崩这三座大山。

穿透在 AI 场景里表现得很隐蔽:如果用户反复问一个“库里没有”的问题,每次都在生成向量、查索引、然后调大模型,成本比普通接口高得多。解决办法是给这种“空结果”做缓存,缓存里不但可以存答案,还可以存“这个问题没有标准答案”的标记,配合短 TTL 避免查库。

击穿问题也很有意思:如果某一个热门知识点突然被大量用户同时问,大家生成向量后可能同时去检索,然后同时去调大模型。这时候需要把大模型调用保护起来,最简单的做法是给相同语义的请求加一个分布式锁,只让一个请求去真正调模型,其他请求等结果。

Redis 的分布式锁在这里依然好用,和普通场景没有任何区别。真正要改的是思路:以前锁保护的是数据库,现在锁保护的是大模型 API。

5. 踩坑实录:这些坑我帮你先踩了

5.1 索引建了查不了?一半是数据类型问题

最典型的报错是Unknown field或者Syntax error,原因往往不是语法问题,而是数据类型和索引定义对不上。比如你建索引的时候声明的是 JSON,但数据写到了 Hash,或者反过来。还有前缀对不上也会导致索引“建了但永远搜不到”。

另外,如果先写入了数据、后建索引,Redis 只会索引创建时间之后写入的数据。很多人测试的时候先 HSET 了几条测试数据,再 FT.CREATE 索引,结果查询始终是空的。正确顺序是:先建索引,再写数据。已经写进去的数据要么重写,要么删除后重建索引。

5.2 内存怎么会涨这么多?算账才踏实

向量数据的存储开销比普通 KV 大得多,很多团队第一次上线就吃了内存爆炸的亏。这里给一个快速估算公式:一条 768 维 float32 向量的本体大小是 768×4 字节,约 3 KB。再加上 HNSW 图结构的额外开销(通常达到向量本体的 1.5 到 2 倍),实际单条记录占用约 7 KB 到 9 KB。

所以一百万条这种向量,内存消耗直奔 8 GB 以上。如果还存了业务字段和文本内容,只会更夸张。这不是 Redis 的问题,是向量数据本身的物理属性决定的。我建议上线前先算一笔账,再决定单机内存规格、要不要用 M 参数降内存、或者是不是该只索引核心数据子集。

5.3 HNSW 参数调优的实践值

我用一个表格把经验和问题汇总一下:

现象可能原因处理办法
FT.SEARCH 报语法错误未加 DIALECT 3查询命令追加 DIALECT 3
查询一直空结果建索引前已写入历史数据重建索引,保证索引覆盖已有数据
写入时报类型错误embedding 存成了字符串用 float32 数组转 bytes 后写入
召回结果明显不相关efSearch 太小或模型维度不匹配调大 efSearch,检查 DIM 是否和模型一致
内存涨得离谱float64 存储或索引参数过大确认类型是 float32,降低 M
响应延迟飙升向量索引扫描量大但无过滤条件增加 TAG/TEXT 前缀过滤,缩小检索范围

5.4 额外提醒:别忽视客户端连接池

向量查询命令的一次往返时间可能比普通命令长一点,尤其是 KNN 查询涉及大量计算时。如果客户端连接池太小,高并发下会在 Redis 侧堆积大量等待,表现为接口整体延迟变高。我遇到过不止一次这种情况:Redis 自身 CPU 不高,但就是慢,排查下来是应用侧连接池被占满了。

解决办法是给 AI 相关接口单独开一个连接池,大小按峰值 QPS 和单请求耗时估算。比如单请求平均 5 毫秒,每个连接每秒能处理约 200 个请求,峰值 2000 QPS 就需要至少 10 个连接,再加 50% 冗余,开 15 个比较稳妥。

6. 写到最后:我对 Redis“接入 AI”的一点真实体会

说实话,刚看到这个消息时我是有点怀疑的:Redis 做缓存做得再熟,向量数据库是另一个赛道,能行吗?但真正动手测了一轮之后,我的结论变了。

Redis 接入 AI 最有价值的不是去替代 Milvus 这类专用向量数据库,而是填补 AI 应用和传统内存存取之间的空白。你的 AI 应用里,向量检索往往只需要在百万级数据里帮大模型先筛一遍,Redis 做这件事的延迟和稳定性,比任何外部组件的引入成本都更可控。先用 Redis 撑住,数据规模真到了千万级、亿级,再考虑迁移专门向量数据库也不迟,而且迁移路径并不复杂。

还有一点是我自己在实操中的体会:别把所有向量都一股脑塞进 Redis。热数据放 Redis 做快速召回,冷数据放对象存储或数仓里归档,这个原则和传统缓存治理一模一样。很多人一看到新能力就想全量接入,结果内存爆了、性能降了,最后得出结论“这方案不行”。其实不是方案不行,是使用姿势不对。

我的建议是,从语义缓存和 Agent 记忆这两个场景开始试水,它们对 Redis 来说属于顺势而为,改动小、收益直观。等这两块跑顺了,再考虑把文档检索、搜索召回这些业务逻辑逐步接进来。找一套最合适你团队现状的方案,比追着新功能跑重要得多。