☰
持久化记忆-基于Redis向量搜索的AI长期记忆
2026/10/27 2:07:22 网站建设 项目流程

的向量数据库(如Qdrant、Milvus、pgvector等)。

环境准备

安装Redis Stack

向量搜索需要Redis Stack(包含RediSearch模块)。最简单的方式是用Docker:

docker 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.Empty<float>(); // 向量数据 public Dictionary<string, 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 Dictionary<string, 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 = 8KB
  • COSINE:余弦距离度量,与第二篇的余弦相似度对应

向量序列化

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(List<VectorModel> 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中执行KNN(K-Nearest Neighbors)向量搜索:

public Task<List<QueryVectorItemDto>> 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 List<QueryVectorItemDto>(); 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); }

关键细节:

  1. 查询语法:*=>[KNN 6 @embedding $query_vector AS __embedding_score]
    • *表示不过滤,@user_id:xxx可按用户过滤
    • KNN 6表示找最近的6个邻居
    • @embedding是向量字段名
    • $query_vector是参数化的查询向量
  2. 距离转相似度:Redis返回余弦距离(0-2),1 - distance转换为相似度(-1到1)
  3. Dialect 2:向量搜索必须使用Dialect 2查询引擎

持久化记忆的完整对话流程

将Redis向量存储接入对话系统:

async Task RedisVectorMemoryAsync() { // 初始化AI客户端和Embedding生成器(同前两篇) IChatClient chatClient = ...; IEmbeddingGenerator<string, Embedding<float>> 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. 构建上下文 List<ChatMessage> 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 List<VectorModel> { new() { Id = Guid.NewGuid().ToString("N"), Data = userInput, Embedding = userVector, UserId = "default", Metadata = new Dictionary<string, 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 Dictionary<string, 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

小结

这篇文章我们学习了:

  1. Redis向量搜索的完整实现:索引创建、向量序列化、KNN搜索
  2. HNSW算法和COSINE距离的配置
  3. NRedisStack的RediSearch API使用

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询