Redis接入AI:会话存储、向量检索与异步任务实战
1. 从一条更新日志说起Redis 接入 AI 到底意味着什么前几天刷社区的时候看到一条消息说 Redis 官方开始往 AI 方向靠了。第一反应是“又来了什么都要蹭 AI”但点进去仔细看完之后发现这次还真不是简单的蹭热度。Redis 接入 AI 这件事本质上是在解决一个非常具体的问题AI 应用的状态管理和上下文存储。我做了七八年后端Redis 从最早当缓存用到后来做分布式锁、做消息队列、做排行榜几乎每个项目都绕不开它。但最近两年接触 AI 相关的项目越来越多发现一个很尴尬的事情大模型本身是无状态的每次对话都要把历史上下文重新塞进去token 消耗大不说响应还慢。而 Redis 恰好就是干“记住东西”这件事最顺手的工具。所以当 Redis 官方开始提供 AI 相关的集成能力时我一点都不意外反而觉得“终于有人把这事儿做规范了”。这篇文章主要想聊清楚几件事Redis 接入 AI 之后到底多了哪些能力、这些能力解决什么实际问题、怎么在自己的项目里落地、以及我在实操过程中踩过的那些坑。适合正在做 AI 应用开发的后端同学也适合想了解 Redis 新特性的运维和架构师。哪怕你之前只用过 Redis 做缓存看完也能明白它在 AI 场景下能扮演什么角色。2. Redis 在 AI 应用中的角色定位与核心思路2.1 为什么 AI 应用需要 Redis先说一个最朴素的场景。你做了一个 AI 对话助手用户问“帮我查一下明天北京的天气”模型回答完之后用户接着问“那后天呢”。如果没有上下文存储第二个问题模型根本不知道“后天”指的是什么。传统的做法是把整个对话历史拼成一个长字符串每次请求都完整发给模型。这么做有三个问题第一token 消耗随对话轮次线性增长成本扛不住第二每次都要从数据库或内存里把完整历史读出来延迟高第三多轮对话的状态管理逻辑散落在业务代码里维护起来很痛苦。Redis 的解法很直接把对话历史、用户偏好、会话状态这些东西存在 Redis 里利用它亚毫秒级的读写性能在每次请求模型之前快速组装上下文请求完之后再把新的对话内容追加进去。更进一步Redis 还可以做语义缓存——如果两个问题语义相似直接返回缓存的结果不用再调模型。这个思路在客服机器人、知识库问答这类场景下特别管用能省下大量重复的模型调用。2.2 Redis 接入 AI 的几种典型模式从我这边的实践来看Redis 在 AI 应用里主要有四种用法。第一种是会话上下文存储就是把多轮对话的历史存在 Redis 的 List 或 Stream 结构里设置合理的过期时间既保证上下文连贯又不会无限膨胀。第二种是向量检索Redis 从 2.4 版本开始支持向量相似度搜索可以把文本 embedding 存进去做语义搜索或推荐。第三种是任务队列AI 推理往往比较慢用 Redis 的 Stream 做异步任务队列前端提交任务后轮询结果体验会好很多。第四种是限流与配额AI 接口通常按 token 计费用 Redis 做用户级别的速率限制和用量统计既准确又高效。这四种模式不是互斥的一个完整的 AI 应用通常会同时用到两三种。比如一个 AI 写作助手用 Redis 存会话上下文用向量检索做素材推荐用 Stream 做异步生成任务用计数器做免费额度限制。Redis 接入 AI 之后官方把这些常见模式封装成了更易用的接口不用再自己从头造轮子。2.3 和传统缓存用法的本质区别有人可能会说这不还是把 Redis 当缓存用吗表面上看是的但本质区别在于数据形态和访问模式。传统缓存存的是数据库查询结果key 通常是业务 IDvalue 是序列化后的对象访问模式是点查。AI 场景下存的是对话历史或向量key 可能是会话 ID 或向量索引value 是列表或二进制向量访问模式是范围查询或相似度搜索。这就对 Redis 的数据结构选择和内存管理提出了完全不同的要求。举个例子传统缓存你可能会用 String 类型存 JSON简单直接。但对话历史用 String 存就不合适了因为每次追加都要读出来、反序列化、追加、再序列化、写回去效率极低。正确的做法是用 List 或 Stream利用 Redis 原生的追加操作O(1) 复杂度。再比如向量检索必须用 Redis 的向量索引功能普通的数据结构根本做不了相似度计算。所以虽然都是“存东西”但里面的门道完全不一样。3. 核心能力拆解会话存储、向量检索与异步任务3.1 会话上下文存储的选型与实操会话上下文存储是 Redis 在 AI 场景下最基础也最常用的能力。我试过三种方案各有优劣。第一种是用 String 存整个对话的 JSON 数组优点是实现简单缺点是每次追加都要全量读写对话长了之后性能下降明显。第二种是用 List每次新消息用 RPUSH 追加读取时用 LRANGE 取最近 N 条性能好很多但 List 不支持按时间范围查询也没法做消息去重。第三种是用 Stream每个消息是一个 entry支持按 ID 范围查询、支持消费者组、支持持久化功能最全但结构相对复杂。我现在的项目里统一用 Stream原因是它天然适合消息追加的场景而且可以给每个会话设置 MAXLEN 做自动裁剪不用担心内存无限增长。具体操作是这样的用户发来消息后先用 XADD 把用户消息追加到会话流里然后调模型拿到回复后再 XADD 追加模型回复。组装上下文的时候用 XREVRANGE 取最近 20 条消息反转后拼成 prompt。这里有个细节要注意XADD 的时候最好显式指定 ID用时间戳加序列号方便后续按时间范围查询。# 追加用户消息 XADD session:12345 * role user content 帮我查一下明天北京的天气 # 追加模型回复 XADD session:12345 * role assistant content 明天北京晴气温 15 到 25 度 # 读取最近 20 条消息 XREVRANGE session:12345 - COUNT 20过期时间怎么设也有讲究。太短了上下文会断太长了内存扛不住。我的经验是普通对话场景设 30 分钟到 2 小时比较合适客服场景可以设 24 小时因为用户可能第二天回来继续问。另外建议给每个会话单独设 TTL而不是全局统一因为不同用户的活跃度差别很大。可以用 EXPIRE 命令在每次追加消息后刷新过期时间实现“活跃即续期”的效果。3.2 向量检索让 Redis 做语义搜索向量检索是 Redis 接入 AI 之后最让我惊喜的能力。以前做语义搜索要么用专门的向量数据库要么用 Elasticsearch 的 dense_vector部署和维护成本都不低。Redis 从 2.4 开始内置了向量相似度搜索虽然功能没有专业向量库那么全但对于中小规模的场景完全够用而且省去了多维护一个组件的麻烦。具体怎么用呢首先要把文本转成向量这个通常用 embedding 模型来做比如 OpenAI 的 text-embedding 或者开源的 BGE 模型。拿到向量之后用 Redis 的 HSET 存进去同时用 FT.CREATE 创建向量索引。查询的时候把查询文本也转成向量用 FT.SEARCH 做 KNN 相似度搜索。整个过程最关键的参数是向量维度和距离度量方式维度必须和 embedding 模型输出一致距离度量通常用 COSINE 或 L2文本场景下 COSINE 更常用。import redis from redis.commands.search.field import VectorField, TextField from redis.commands.search.indexDefinition import IndexDefinition, IndexType from redis.commands.search.query import Query import numpy as np r redis.Redis(hostlocalhost, port6379) # 创建向量索引 schema ( TextField(content), VectorField(embedding, HNSW, { TYPE: FLOAT32, DIM: 768, DISTANCE_METRIC: COSINE }) ) r.ft(idx:docs).create_index(schema, definitionIndexDefinition(prefix[doc:], index_typeIndexType.HASH)) # 存入文档和向量 doc_id doc:1 embedding np.random.rand(768).astype(np.float32).tobytes() r.hset(doc_id, mapping{content: Redis 是一个内存数据库, embedding: embedding}) # 相似度搜索 query_vec np.random.rand(768).astype(np.float32).tobytes() q Query(*[KNN 5 embedding $vec AS score]).sort_by(score).return_fields(content, score).dialect(2) results r.ft(idx:docs).search(q, query_params{vec: query_vec})这里有几个坑我踩过。第一向量必须转成 FLOAT32 的字节串不能直接传 numpy 数组否则会报类型错误。第二HNSW 索引的构建参数 M 和 EF_CONSTRUCTION 会影响检索精度和内存占用M 越大精度越高但内存越多一般设 16 到 64 之间。第三如果数据量超过百万级建议用 IVF 索引而不是 HNSW虽然精度略低但内存占用小很多。第四向量检索的结果 score 是距离值越小越相似别搞反了。3.3 异步任务队列用 Stream 处理慢推理AI 推理有个特点慢。尤其是图片生成、长文本生成这类任务动辄几秒到几十秒。如果让前端同步等待用户体验很差而且容易超时。这时候就需要异步任务队列前端提交任务后立即返回一个任务 ID然后轮询或通过 WebSocket 推送结果。Redis 的 Stream 天然适合做这个支持消费者组、支持消息确认、支持失败重试。我的做法是这样的前端提交任务时用 XADD 往任务流里写一条记录包含任务类型、参数、回调地址等信息返回任务 ID 给前端。后端有多个 worker 用 XREADGROUP 消费任务流处理完之后把结果写到另一个结果流里或者直接更新任务状态。前端轮询任务状态接口接口从 Redis 里读任务状态返回。这里的关键是消费者组的 ACK 机制worker 处理失败时不要 ACK消息会重新投递给其他 worker实现自动重试。# 创建消费者组 XGROUP CREATE task:stream workers 0 MKSTREAM # 提交任务 XADD task:stream * type image_gen prompt 一只猫 callback https://example.com/cb # worker 消费任务 XREADGROUP GROUP workers worker1 COUNT 1 BLOCK 5000 STREAMS task:stream # 处理成功后 ACK XACK task:stream workers message_id任务状态怎么存我一般用 Hashkey 是任务 IDfield 包括 status、result、error、created_at 等。status 有 pending、processing、done、failed 四种。worker 开始处理时把 status 改成 processing处理完改成 done 并写入 result出错改成 failed 并写入 error。前端轮询时直接 HGETALL 拿状态简单高效。任务结果建议设一个较长的过期时间比如 24 小时给前端足够的重试窗口。4. 从零搭建Redis AI 能力的完整落地流程4.1 环境准备与安装配置先说安装。Linux 下最省事的方式是用 Docker一条命令搞定。Windows 用户建议用 WSL2 或者 Docker Desktop原生 Windows 版本的 Redis 版本比较老很多新特性不支持。macOS 用 Homebrew 装也很方便。我这边开发环境用 Docker生产环境用官方提供的 Redis Stack 镜像因为它预装了 RediSearch、RedisJSON 等模块向量检索功能开箱即用。# Docker 启动 Redis Stack docker run -d --name redis-stack \ -p 6379:6379 \ -p 8001:8001 \ -v redis-data:/data \ redis/redis-stack:latest # 验证向量模块是否加载 redis-cli MODULE LIST配置文件这块有几个参数必须调。第一是 maxmemoryAI 场景下数据量比传统缓存大得多建议至少给 4GB具体看业务规模。第二是 maxmemory-policy会话数据用 allkeys-lru 或 volatile-lru 都行但向量索引数据建议用 noeviction因为索引重建成本很高。第三是 appendonly建议开启 AOF 持久化避免重启后会话丢失。第四是 io-threads如果并发高可以设成 CPU 核数的四分之三提升网络吞吐。注意Redis Stack 和普通 Redis 的配置文件格式略有不同模块相关的配置要写在 redis-stack.conf 里不要直接改 redis.conf否则可能不生效。4.2 会话存储的完整实现会话存储这块我封装了一个 SessionManager 类核心方法有三个append_message、get_context、clear_session。append_message 负责追加消息并刷新 TTLget_context 负责读取最近 N 条消息并组装成模型需要的格式clear_session 负责主动清理。这里有个细节组装上下文的时候要注意 token 数量不能无限取我一般设一个 max_tokens 参数从最近的消息往前累加超过就截断。import redis import json import time class SessionManager: def __init__(self, redis_client, max_turns20, ttl3600): self.r redis_client self.max_turns max_turns self.ttl ttl def append_message(self, session_id, role, content): key fsession:{session_id} self.r.xadd(key, {role: role, content: content, ts: int(time.time())}) self.r.expire(key, self.ttl) def get_context(self, session_id): key fsession:{session_id} messages self.r.xrevrange(key, countself.max_turns) messages.reverse() return [{role: m[role], content: m[content]} for m in messages] def clear_session(self, session_id): self.r.delete(fsession:{session_id})TTL 刷新这块有个坑。如果你在每次追加消息后都调 EXPIRE高并发下会有大量 EXPIRE 命令增加 Redis 负载。优化方案是用 Redis 7.0 的 EXPIRE 选项或者干脆不刷新让会话自然过期。我的做法是折中只在用户主动发消息时刷新模型回复时不刷新这样既保证活跃会话不过期又减少了一半的 EXPIRE 调用。4.3 向量索引的创建与查询向量索引的创建分三步准备数据、定义 schema、执行创建。准备数据就是把文本转成向量我一般用 sentence-transformers 库本地跑 BGE-small 模型768 维速度快且免费。定义 schema 时要注意字段类型文本字段用 TEXT向量字段用 VECTOR如果还要按标签过滤再加 TAG 字段。执行创建用 FT.CREATE 命令索引建好后就可以用 FT.SEARCH 查询了。from sentence_transformers import SentenceTransformer model SentenceTransformer(BAAI/bge-small-zh-v1.5) def get_embedding(text): vec model.encode(text, normalize_embeddingsTrue) return vec.astype(np.float32).tobytes() # 批量导入 for i, doc in enumerate(documents): embedding get_embedding(doc[content]) r.hset(fdoc:{i}, mapping{ content: doc[content], category: doc[category], embedding: embedding })查询的时候KNN 的 K 值怎么选我的经验是如果只是做召回K 设 10 到 20 比较合适太小了可能漏掉相关结果太大了会引入噪声。如果后面还有 rerank 环节K 可以设大一点比如 50 到 100让 rerank 模型去精排。另外FT.SEARCH 返回的 score 是余弦距离范围是 0 到 20 表示完全相同2 表示完全相反。实际使用中score 小于 0.5 的基本可以认为是相关的大于 1.0 的基本不相关。4.4 异步任务的调度与状态管理异步任务这块我用的是 Stream 加 Hash 的组合。Stream 负责任务分发Hash 负责任务状态。worker 的数量根据任务类型来定图片生成这种重任务少开几个 worker文本处理这种轻任务可以多开。每个 worker 用 XREADGROUP 阻塞读取拿到任务后先更新状态为 processing处理完再更新为 done 或 failed。def worker_loop(worker_id): while True: messages r.xreadgroup(workers, worker_id, {task:stream: }, count1, block5000) if not messages: continue for stream, msgs in messages: for msg_id, data in msgs: task_id data[task_id] r.hset(ftask:{task_id}, status, processing) try: result process_task(data) r.hset(ftask:{task_id}, mapping{status: done, result: json.dumps(result)}) r.xack(task:stream, workers, msg_id) except Exception as e: r.hset(ftask:{task_id}, mapping{status: failed, error: str(e)}) r.xack(task:stream, workers, msg_id)这里有个重要的经验任务状态一定要设过期时间否则 Hash 会越积越多。我一般设 24 小时给前端足够的轮询窗口。另外如果任务处理时间可能超过 Stream 的 block 时间建议在 worker 里加心跳机制定期更新任务的 heartbeat 字段方便监控哪些任务卡住了。还有XACK 一定要在处理完成后调用不要提前 ACK否则任务失败后无法重试。5. 性能调优与常见问题排查5.1 内存占用分析与优化AI 场景下 Redis 的内存占用比传统缓存大得多主要大头是向量数据。一个 768 维的 FLOAT32 向量占 3KB一百万条就是 3GB再加上 HNSW 索引的额外开销实际可能到 5GB 以上。优化手段有几个第一用 FLOAT16 代替 FLOAT32内存直接减半精度损失很小。第二用 IVF 索引代替 HNSW内存占用能降 60% 以上。第三对向量做降维比如用 PCA 降到 256 维内存降到三分之一。第四定期清理不再使用的向量数据别让垃圾数据占着内存。会话数据的内存优化相对简单主要是控制 Stream 的长度。用 XADD 的 MAXLEN 选项比如 MAXLEN ~ 1000表示每个会话最多保留 1000 条消息超出的自动裁剪。注意用 ~ 而不是 ~ 是近似裁剪性能更好。另外消息内容如果很长可以考虑压缩后再存读取时解压能省不少内存。5.2 延迟毛刺的排查思路Redis 延迟毛刺是运维的老大难问题。AI 场景下毛刺的来源主要有几个第一大 key 操作比如一个会话存了几万条消息LRANGE 一次取出来会阻塞很久。第二向量索引重建FT.CREATE 在大数据量下会阻塞主线程。第三AOF 重写数据量大时重写会占用大量 IO。第四内存碎片整理activedefrag 开启后会有周期性延迟。排查毛刺我一般用这几个工具redis-cli --latency 看整体延迟分布redis-cli --bigkeys 找大 keySLOWLOG GET 看慢查询INFO commandstats 看各命令的调用次数和耗时。如果发现是向量索引重建导致的建议在从节点上建索引建好后主从切换。如果是 AOF 重写导致的可以调大 auto-aof-rewrite-percentage减少重写频率。5.3 常见问题速查表问题现象可能原因排查方法解决方案会话上下文丢失TTL 设置过短或未刷新TTL session:xxx 查看剩余时间调大 TTL追加消息后刷新向量搜索结果不准距离度量方式选错检查索引的 DISTANCE_METRIC文本场景改用 COSINE任务一直 pendingworker 未启动或消费组未创建XINFO GROUPS task:stream创建消费组启动 worker内存增长过快向量数据未压缩或未清理MEMORY USAGE doc:xxx用 FLOAT16定期清理写入延迟高AOF 同步策略过于严格CONFIG GET appendfsync改为 everysec连接数暴涨客户端未使用连接池INFO clients配置连接池复用连接5.4 几个我踩过的坑第一个坑是向量维度不匹配。我用 BGE-small 模型输出 512 维但建索引时手误写成了 768结果插入数据时报错排查了半天才发现是维度问题。教训是建索引前一定要确认 embedding 模型的输出维度最好在代码里加个断言。第二个坑是 Stream 的消费者组重复消费。我一开始没注意 XACK 的时机在任务开始处理时就 ACK 了结果任务失败后消息丢了没法重试。正确做法是处理成功后再 ACK失败时不 ACK让消息重新投递。第三个坑是 TTL 刷新导致的性能问题。我在每次追加消息后都调 EXPIREQPS 高的时候 EXPIRE 命令占了总命令数的 30%白白浪费性能。后来改成只在用户消息时刷新模型回复时不刷新性能好了很多。第四个坑是向量索引的内存估算错误。我以为 100 万条 768 维向量占 3GB结果实际用了 6GB因为 HNSW 索引本身也要占内存。后来换成 IVF 索引内存降到 2.5GB精度只降了不到 2%。6. 一些实操心得和后续扩展方向Redis 接入 AI 这件事我的整体感受是方向对了但别指望它解决所有问题。它最擅长的是状态管理和快速检索不适合做大规模向量存储和复杂推理。如果你的向量数据超过千万级还是建议用专业的向量数据库如果你的任务调度逻辑很复杂建议用专门的消息队列。Redis 的定位是“够用且快”在中小规模场景下能帮你省掉很多组件的维护成本。后续我打算在这几个方向继续折腾一是把语义缓存做得更精细用两级缓存策略热点问题走 Redis冷门问题走向量检索二是把会话存储和用户画像结合起来根据用户历史行为动态调整上下文长度三是探索 Redis 的 JSON 功能把结构化的用户偏好直接存成 JSON省去序列化反序列化的开销。这些等我踩完坑再回来分享。最后分享一个小技巧如果你在用 Redis 做 AI 应用的会话存储建议给每个会话加一个 version 字段每次追加消息时递增。这样在并发场景下可以用 WATCH 加 version 做乐观锁避免两个请求同时追加消息导致顺序错乱。这个技巧在多人协作的 AI 应用里特别有用我实测下来很稳。
上一篇/下一篇内容由系统自动关联
返回资讯列表 →