持久化记忆-基于Redis向量搜索的AI长期记忆
的向量数据库如Qdrant、Milvus、pgvector等。环境准备安装Redis Stack向量搜索需要Redis Stack包含RediSearch模块。最简单的方式是用Dockerdocker run -d --name redis-stack -p 6379:6379 redis/redis-stack:latest数据模型设计首先定义向量存储的数据模型public class VectorModel { public string Id { get; set; } Guid.NewGuid().ToString(N); public string Data { get; set; } string.Empty; // 原始文本内容 public float[] Embedding { get; set; } Array.Emptyfloat(); // 向量数据 public Dictionarystring, object Metadata { get; set; } new(); // 元数据如role public string? UserId { get; set; } // 用户标识 public string? Hash { get; set; } public string? AgentId { get; set; } // 生成向量的模型标识 public DateTime CreatedAt { get; set; } public DateTime? UpdatedAt { get; set; } }查询结果需要附带相似度得分public class QueryVectorItemDto { public VectorModel Vector { get; set; } new(); public float Score { get; set; } // 余弦相似度得分 }Metadata字段用于存储额外信息比如这条记录是user消息还是assistant消息——这在构建上下文时非常重要。从零实现RedisVectorStore连接Redis并创建向量索引var connectionMultiplexer await ConnectionMultiplexer.ConnectAsync(127.0.0.1:6379); var redisVectorStore new RedisVectorStore(connectionMultiplexer, chat_memory); // 确保向量索引存在2048维对应embedding-3模型 await redisVectorStore.EnsureCollectionExistsAsync(2048, allowRecreation: false);索引创建的核心逻辑Redis的向量搜索需要先创建索引。这是最关键的一步private Task CreateIndexAsync(ISearchCommands ft, int vectorSize) { var schema new Schema() .AddTextField(id) .AddTextField(data) .AddTextField(user_id) .AddTextField(metadata) .AddNumericField(created_at) .AddNumericField(updated_at) .AddVectorField(embedding, Schema.VectorField.VectorAlgo.HNSW, new Dictionarystring, object { [TYPE] FLOAT32, // 向量数据类型 [DIM] vectorSize, // 向量维度2048 [DISTANCE_METRIC] COSINE // 余弦距离 }); bool success ft.Create(_indexName, new FTCreateParams() .On(IndexDataType.HASH) .Prefix(_keyPrefix), schema); if (!success) throw new Exception(Failed to create Redis vector index.); return Task.CompletedTask; }关键参数解析HNSW算法Hierarchical Navigable Small World目前最主流的近似最近邻搜索算法兼顾速度和精度FLOAT32单精度浮点每个向量占2048 × 4 8KBCOSINE余弦距离度量与第二篇的余弦相似度对应向量序列化Redis存储的是字节数组所以需要在float[]和byte[]之间转换private byte[] SerializeVector(float[] vector) { var bytes new byte[vector.Length * sizeof(float)]; Buffer.BlockCopy(vector, 0, bytes, 0, bytes.Length); return bytes; } private float[] DeserializeVector(byte[] bytes) { var floats new float[bytes.Length / sizeof(float)]; Buffer.BlockCopy(bytes, 0, floats, 0, bytes.Length); return floats; }Buffer.BlockCopy是最快的数组拷贝方式比逐元素转换快几个数量级。插入向量数据public async Task InsertAsync(ListVectorModel vectors, CancellationToken ct default) { foreach (var vector in vectors) { var key ${_keyPrefix}{vector.Id}; var hashEntries new HashEntry[] { new(id, vector.Id), new(data, vector.Data), new(user_id, vector.UserId ?? string.Empty), new(metadata, JsonSerializer.Serialize(vector.Metadata)), new(created_at, vector.CreatedAt.Ticks), new(updated_at, vector.UpdatedAt?.Ticks), new(embedding, SerializeVector(vector.Embedding)) // 向量序列化为bytes }; await _db.HashSetAsync(key, hashEntries); } }每条记录以Redis Hash的形式存储key格式为chat_memory:{id}。KNN向量搜索这是整个系统最核心的部分——在Redis中执行KNNK-Nearest Neighbors向量搜索public TaskListQueryVectorItemDto SearchAsync( float[] queryVector, string? userId null, int limit 100) { var ft _db.FT(); // 构建KNN搜索查询 var queryStr userId ! null ? $user_id:{EscapeRedisQuery(userId)} : *; var searchQuery ${queryStr}[KNN {limit} embedding $query_vector AS __embedding_score]; var fullQuery new Query(searchQuery) .SetSortBy(__embedding_score) // 按距离排序 .Limit(0, limit) .ReturnFields(id, data, user_id, metadata, created_at, __embedding_score) .Dialect(2); // 必须使用Dialect 2 fullQuery.AddParam(query_vector, SerializeVector(queryVector)); var results ft.Search(_indexName, fullQuery); var searchResults new ListQueryVectorItemDto(); foreach (var doc in results.Documents) { var vectorItem ParseVectorModel(doc); var scoreValue doc[__embedding_score]; // Redis返回的是距离转换为相似度1 - distance var score float.TryParse(scoreValue.ToString(), out var s) ? 1.0f - s : 0.0f; searchResults.Add(new QueryVectorItemDto { Vector vectorItem, Score score }); } return Task.FromResult(searchResults); }关键细节查询语法*[KNN 6 embedding $query_vector AS __embedding_score]*表示不过滤user_id:xxx可按用户过滤KNN 6表示找最近的6个邻居embedding是向量字段名$query_vector是参数化的查询向量距离转相似度Redis返回余弦距离0-21 - distance转换为相似度-1到1Dialect 2向量搜索必须使用Dialect 2查询引擎持久化记忆的完整对话流程将Redis向量存储接入对话系统async Task RedisVectorMemoryAsync() { // 初始化AI客户端和Embedding生成器同前两篇 IChatClient chatClient ...; IEmbeddingGeneratorstring, Embeddingfloat embeddingGenerator ...; const int topK 6; const float threshold 0.5f; // 比内存版更高的阈值Redis搜索更精准 while (true) { var userInput Console.ReadLine(); // 1. 生成查询向量 var userEmbedding await embeddingGenerator.GenerateAsync(userInput); float[] userVector userEmbedding.Vector.ToArray(); // 2. 从Redis检索相关历史 var relevantItems await redisVectorStore.SearchAsync(userVector, limit: topK); var filteredItems relevantItems .Where(item item.Score threshold) .OrderByDescending(item item.Score) .ToList(); // 3. 构建上下文 ListChatMessage chatMessages [ new ChatMessage(ChatRole.System, 你是一个有用的AI助手。下面是相关的历史对话记录请参考这些上下文来回答。 如果历史记录与当前问题无关请忽略历史记录直接回答。) ]; foreach (var item in filteredItems) { var role item.Vector.Metadata.TryGetValue(role, out var r) ? r.ToString() : user; var chatRole role assistant ? ChatRole.Assistant : ChatRole.User; chatMessages.Add(new ChatMessage(chatRole, item.Vector.Data)); } chatMessages.Add(new ChatMessage(ChatRole.User, userInput)); // 4. 调用AI流式输出 // ... 同前几篇 ... // 5. 持久化到Redis var vectorsToInsert new ListVectorModel { new() { Id Guid.NewGuid().ToString(N), Data userInput, Embedding userVector, UserId default, Metadata new Dictionarystring, object { [role] user }, CreatedAt DateTime.UtcNow } }; if (sb.Length 0) { var aiEmbedding await embeddingGenerator.GenerateAsync(sb.ToString()); vectorsToInsert.Add(new() { Id Guid.NewGuid().ToString(N), Data sb.ToString(), Embedding aiEmbedding.Vector.ToArray(), UserId default, Metadata new Dictionarystring, object { [role] assistant }, CreatedAt DateTime.UtcNow }); } await redisVectorStore.InsertAsync(vectorsToInsert); } }清除记忆功能持久化存储还带来了新的需求——清除记忆if (userInput.Equals(clear, StringComparison.OrdinalIgnoreCase)) { var existingItems await redisVectorStore.ListAsync(limit: 10000); foreach (var item in existingItems) { await redisVectorStore.DeleteAsync(item.Id); } Console.WriteLine([系统] 已清除所有历史记忆。); continue; }内存版 vs Redis版对比维度内存版第二篇Redis版本篇持久化❌ 重启丢失✅ 持久存储跨会话❌ 不支持✅ 支持搜索方式手动计算余弦相似度Redis原生KNN搜索相似度计算TensorPrimitives.CosineSimilarityRedis引擎内部计算多用户❌ 不支持✅ 通过UserId过滤性能小数据量快大数据量更优部署成本零需要Redis Stack小结这篇文章我们学习了Redis向量搜索的完整实现索引创建、向量序列化、KNN搜索HNSW算法和COSINE距离的配置NRedisStack的RediSearch API使用