十年匠心定制 · 商业建站与技术教学双线并行 咨询热线:400-886-1026 service@lmnt.cn
ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

用Milvus搭建AI长期记忆库:从Embedding到RAG的完整实践

用Milvus搭建AI长期记忆库:从Embedding到RAG的完整实践 金鱼的记忆只有七秒很多AI应用在对话里也像金鱼——换个话题就忘光。想让AI拥有长期记忆最务实的方案是向量数据库而Milvus是当前绕不开的名字。上篇铺垫了embedding和RAG的基本思路这篇直接落到Milvus把整座“图书馆”从地基开始盖起来。我会按自己搭建时的真实顺序来写先讲清楚为什么不能只靠上下文窗口再说Milvus的核心概念接着用Docker Desktop在Windows上把服务跑起来然后用Python写入第一批“记忆”最后聊几项调优和踩坑记录。整个过程不需要GPU普通的笔记本就能复现。1. 先回答一个基本问题为什么上下文窗口不是长期记忆1.1 上下文窗口的本质一次对话的“草稿纸”大模型的上下文窗口本质上是模型在处理输入时一次性“看到”的token数量上限。把Token理解成字词的碎片假设一个模型窗口是8K那么它一次最多只能“读”8K左右的碎片。听起来不短但真正用来记历史对话时很快就不够用了。我做过一个测试让一个8K窗口的模型读一篇2000字的文章再连续追问十几个问题到后面它就越来越“健忘”甚至会把前面回答过的结论推翻。原因很简单每次请求都只携带最近的一部分历史最早的细节早就被挤出了窗口。这不是模型变笨了而是它在每次请求里能看到的“草稿纸”就这么长写满之后前面的字自然被擦掉。更麻烦的是token成本也会跟着涨。如果每次对话都把全部历史塞进大模型调用次数越多、历史越长费用就越高。很多聪明的做法是把历史“摘要压缩”再塞进去但摘要仍然是有限度的而且会丢失细节。长期记忆的真正解法不是无限拉长窗口而是把内容存到外部存储里要用的时候再把相关的片段捞出来。1.2 从Embedding到Milvus把知识变成可检索的记忆要让AI“想起”很久以前看过的一段话最常用的方式是向量化。所谓向量化就是将一个文本转成一串浮点数比如512维的向量。维度里的每个数值都在刻画文本的语义特征。两个文本如果语义接近它们的向量在高维空间里的距离就近。上篇我把这个过程比作“给书籍写索引卡”每张卡片写着“这本书大概讲什么”。嵌入模型Embedding Model就是那个写索引卡的人。但索引卡写完之后放在哪里如果只是塞进一个Python列表或者CSV文件数据量小的时候能忍一旦有几百万条记忆每次查询都得全量算一遍相似度计算开销很大。这时候就需要一个专门的引擎来管理这些向量。Milvus就是干这件事的它可以把向量和原始文本一起存下来建立专门的向量索引收到一个Query向量后快速返回最相似的Top-K条记录。它的查询过程对上层应用来说就像在数据库里执行一条SELECT区别是这里比较的不是精确数值而是语义距离。1.3 自己写个list也能存向量为什么非要Milvus听到这里有人会问我直接用numpy算余弦相似度不就行了数据量小的时候确实可以我自己早期也是这么干的。但一旦你想把它当成正式的AI记忆库会遇到几个非常现实的问题数据持久化、并发读写、元数据过滤、索引更新、水平扩容。举个例子用户A在问“上次说的那个项目后续怎么做”你应该只去检索用户A自己的历史记录而不是把全库所有人的记忆都翻一遍。这就需要在向量检索之外还能按userId、sessionId这些普通字段做过滤。你当然可以用numpy实现一套但索引、分片、持久化、备份都要自己造轮子成本远高于想象。Milvus的价值在于它把这些能力打包成了开箱即用的服务。它是CNCF孵化项目支持常用的索引算法提供了Python/Java/Go等SDK而且部署方式不复杂。对一个想认真做AI应用的人来说与其从0写草稿不如直接站在这个轮子上面。2. Milvus的核心概念和工作流程从Collection到Index一口气说清2.1 Collection就是图书馆里的一排书架Milvus里最核心的概念是Collection可以把它理解成数据库里的一张表或者说图书馆里的一排书架。所有记忆都存在Collection里一个应用可以按业务建多个Collection比如“用户聊天记录”“商品知识库”“文档笔记”。Collection里的每一行数据叫Entity相当于一本书。每个Entity由若干个Field组成Field就是书的基本属性比如标题、作者、正文、向量。下面是一个典型的记忆Collection结构Field类型说明idINT64主键每条记忆的唯一编号user_idVARCHAR这条记忆属于哪个用户contentVARCHAR原始文本内容vectorFLOAT_VECTOR文本对应的嵌入向量维度按模型而定建Collection时你需要为每个Field指定Schema就像建表时写CREATE TABLE语句。向量字段必须声明维度dim例如Embedding模型输出768维那dim就填768。如果搞错之后插入数据会直接报错这点后面踩坑章节还会细说。2.2 向量索引与相似度度量怎么决定“哪句话和当前问题最像”光把向量存进去还不够还要给向量字段建索引。索引的作用是加速搜索没有索引的Milvus会退化成暴力全量扫描数据量一大就慢得没法用。Milvus支持的索引类型很多比如FLAT、IVF_FLAT、IVF_PQ、HNSW。对新手来说最友好的两个选择是FLAT完全不压缩暴力计算每条记录与查询向量的相似度。适合几十万条以下的数据集准确率最高但最慢。HNSW基于图的近似最近邻搜索算法速度快召回高是目前做AI问答时最常见的索引选择。它需要配置M和efConstruction参数。相似度度量一般是L2欧氏距离或COSINE余弦相似度。文本语义检索里我习惯用COSINE因为它的结果不受向量模长影响更适合Embedding模型。如果你用的是OpenAI、BGE等新一代模型很多模型本身建议用COSINE具体以模型文档为准。2.3 Milvus在RAG链路中的位置把Milvus放进完整的AI应用里就构成了一个典型的RAG检索增强生成链路。问题来了用户提问时系统先用和建库时相同的Embedding模型把Question转成向量然后拿这个向量去Milvus里检索最相似的几条记忆最后把命中的原始文本拼接到Prompt里交给大模型生成答案。这里我特别想强调一点检索用的嵌入模型必须和建库时完全一致甚至连同款模型的不同版本都不能混用。道理很简单换一个Embedding模型向量空间就变了之前存的向量和新Query向量不在同一个语义坐标系里检索结果会乱套。这个坑我亲眼见过同事踩过换了本地模型没重刷数据结果召回的内容和问题完全对不上。3. 在Windows上把Milvus跑起来Docker Desktop部署实录3.1 环境准备Docker Desktop和端口规划Milvus服务端支持Linux、macOS和WindowsWindows上最省心的是用Docker Desktop跑容器。安装Docker Desktop时记得把后端设为WSL2模式这是目前在Windows下性能最好、兼容性最稳的方式。端口规划同样重要。Milvus默认用到几个端口19530是gRPC客户端入口9091是管理健康检查端口另外还有etcd的2379和MinIO的9000/9001。如果你的机器上已经有服务占用这些端口可以先改掉再启动。我本机曾经被一个旧的MinIO占用了9000端口第一次启动直接失败改一下映射端口就好。3.2 用docker-compose启动Milvus StandaloneMilvus的Standalone版适合开发环境官方提供了一个docker-compose.yml模板。如果你不想用curl下载也可以直接建一个文件内容大致如下version: 3.5 services: etcd: image: quay.io/coreos/etcd:v3.5.5 environment: - ETCD_AUTO_COMPACTION_MODErevision - ETCD_AUTO_COMPACTION_RETENTION1000 - ETCD_QUOTA_BACKEND_BYTES4294967296 - ETCD_SNAPSHOT_COUNT50000 volumes: - ${DOCKER_VOLUME_DIRECTORY:-.}/volumes/etcd:/etcd command: etcd -advertise-client-urlshttp://127.0.0.1:2379 -listen-client-urls http://0.0.0.0:2379 --data-dir /etcd minio: image: minio/minio:RELEASE.2023-03-20T20-16-18Z environment: MINIO_ACCESS_KEY: minioadmin MINIO_SECRET_KEY: minioadmin volumes: - ${DOCKER_VOLUME_DIRECTORY:-.}/volumes/minio:/minio_data command: minio server /minio_data healthcheck: test: [CMD, curl, -f, http://localhost:9000/minio/health/live] interval: 30s timeout: 20s retries: 3 standalone: image: milvusdb/milvus:v2.4.13 command: [milvus, run, standalone] environment: ETCD_ENDPOINTS: etcd:2379 MINIO_ADDRESS: minio:9000 volumes: - ${DOCKER_VOLUME_DIRECTORY:-.}/volumes/milvus:/var/lib/milvus ports: - 19530:19530 - 9091:9091 depends_on: - etcd - minio文件保存为docker-compose.yml然后在同目录打开终端执行docker compose up -d首次启动会拉取几个镜像耗时取决于网络状况。如果之前装过其他ETCD或MinIO容器建议先停止冲突容器避免端口打架。3.3 健康检查和资源确认启动完成后可以用下面的命令看容器状态docker compose ps三个容器里standalone状态是running基本就成功了。再访问健康检查接口可以进一步确认curl http://localhost:9091/healthz返回htmlbodyOK/body/html就说明服务正常。Milvus的Standalone版会把它所有的元数据存在ETCD里数据对象存在MinIO里这一点和单机数据库不太一样后期备份要同时考虑这三部分数据别漏了MinIO里的数据文件。3.4 部署阶段最容易卡住的几个问题很多新手在部署时会卡在几个地方。第一Docker Desktop需要启动WSL2后端如果用的是旧版Hyper-V模式可能出现Linux容器无法正常访问宿主机端口的情况。第二端口被占用时容器不会直接崩溃但客户端连接会反复超时排查起来很绕。第三docker compose ps看起来正常但Python连接报错fail to connect这时候先查防火墙有没有放行19530端口。还有一个小细节如果在公司网络或者网络受限环境里拉取镜像很慢可以多等一会或者换一个Docker源但不管怎么换都要确保你使用的镜像源可信。Milvus官方镜像名是milvusdb/milvus别随意使用来路不明的第三方镜像容器里跑着你的数据安全不能省。4. Python实操给AI建一座可用的长期记忆库4.1 先装pymilvus再搞定Embedding客户端和服务端都就绪后在Python环境里安装依赖pip install pymilvus sentence-transformerspymilvus是Milvus的官方Python SDKsentence-transformers用来把中文文本转换成向量。以下示例我使用本地模型BAAI/bge-small-zh-v1.5它的输出向量维度是512具体以你下载的模型为准。如果网络不方便也可以先用几行随机数代替向量先把链路跑通再接真实Embedding。from pymilvus import connections, CollectionSchema, FieldSchema, DataType, Collection connections.connect(hostlocalhost, port19530) print(connected)连接这一步能跑通说明服务器没问题接下来就可以设计Collection结构了。4.2 定义Collection结构告诉Milvus你要存什么我建的“记忆库”包含四个字段主键、用户ID、原始文本和向量。原始文本用VARCHAR类型需要指定max_length向量字段用FLOAT_VECTOR维度填512。fields [ FieldSchema(nameid, dtypeDataType.INT64, is_primaryTrue, auto_idFalse), FieldSchema(nameuser_id, dtypeDataType.VARCHAR, max_length64), FieldSchema(namecontent, dtypeDataType.VARCHAR, max_length65535), FieldSchema(namevector, dtypeDataType.FLOAT_VECTOR, dim512), ] schema CollectionSchema(fields, descriptionAI long-term memory library) collection Collection(nameai_memory, schemaschema) print(collection.name, collection.schema)FieldSchema里有个容易被忽略的参数is_primary。主键必须指定否则创建Collection可能报错。auto_id如果设为True插入时可以不传主键让Milvus自动生成但如果你希望在后续更新时能精确定位到某条记忆建议自己生成主键比如把时间戳和一个自增序列拼起来。4.3 写入第一批记忆并创建索引接下来生成一批示例文本和向量。真实场景里这些文本可能来自用户聊天记录、笔记、文档摘要等。为了让示例跑通我用几条简单的句子from sentence_transformers import SentenceTransformer model SentenceTransformer(BAAI/bge-small-zh-v1.5) texts [ 用户上次提到他最喜欢的编程语言是Python, 项目的上线日期定在下个月5号, 用户不喜欢喝咖啡只喝绿茶, 数据库迁移方案已经确认使用Milvus存储向量, ] vectors model.encode(texts).tolist() ids [i 1 for i in range(len(texts))] user_ids [user_001] * len(texts) collection.insert([ids, user_ids, texts, vectors]) collection.flush() print(total entities:, collection.num_entities)批量插入完成后调用flush()是为了把内存里的数据落盘。注意插入的数据字段顺序必须和Schema里的顺序一致很多新手在这里踩坑少传一个字段或者顺序调错报错信息往往让人一头雾水。然后给向量字段创建索引index_params { index_type: HNSW, metric_type: COSINE, params: {M: 16, efConstruction: 200} } collection.create_index(field_namevector, index_paramsindex_params) print(index created)创建索引后需要执行load()把数据加载到内存中才能查询collection.load()4.4 用search接口把记忆捞回来查询时同样把用户的问题向量化然后调用search。我们来看一个完整示例query 帮我回忆一下上线时间 query_vector model.encode([query]).tolist() results collection.search( dataquery_vector, anns_fieldvector, param{metric_type: COSINE, params: {efSearch: 64}}, limit3, output_fields[content, user_id], ) for hits in results: for hit in hits: print(hit.distance, hit.entity.get(content))执行后返回的相似度越接近1说明和问题语义越接近。这里limit3表示最多返回3条记忆。output_fields用来指定除了向量之外还需要返回哪些原始字段否则默认只给主键和距离。4.5 把Milvus接到Agent对话流程里实际接入AI对话时不需要把所有历史一股脑塞进Prompt而是先检索再拼接。下面是一个简化版的伪代码思路def build_prompt(user_query): query_vec model.encode([user_query]).tolist() results collection.search( dataquery_vec, anns_fieldvector, param{metric_type: COSINE, params: {efSearch: 64}}, limit5, output_fields[content], ) memory_list [h.entity.get(content) for h in results[0]] memory_text \n.join(memory_list) prompt f 以下是与用户相关的历史记忆 {memory_text} 用户当前问题{user_query} 请基于以上记忆回答问题。 return prompt把检索结果拼进Prompt大模型就能基于“外部记忆”生成答案。这样做有几个好处上下文窗口不会被无限撑大无关历史不会被塞进去每次对话只花必要的token。这也是RAG被广泛采用的理由。5. 从“跑通”到“好用”我感觉最值的几项调优5.1 索引参数nlist、nprobe和efSearch应该怎么调很多索引参数看起来是给专业DBA准备的但做AI应用的人都该懂一点。以HNSW为例M控制每个节点的连接数影响内存和查询速度efConstruction控制建图时的搜索范围越大建图越慢但质量更高查询时efSearch越大检索质量越高速度越慢。我的经验是在文本检索场景里M取16左右efConstruction取200efSearch取64是一个兼顾质量和速度的起点。如果你的数据条数到了千万级别再考虑IVF系列或者量化类索引但在1亿级别以下HNSW通常不会让人失望。IVF_FLAT里面也有两个参数nlist控制聚类中心数量查询时nprobe控制要搜索多少个聚类桶。一般建议nlist约等于4 * sqrt(N)nprobe从16开始往上试。每次改参数都要先drop_index再重新创建然后重新load在真实数据上跑一遍召回别只看文档。5.2 给记忆打标用标量字段过滤缩小搜索范围如果把所有用户的记忆都放在一个Collection里查询时必须加上过滤条件不然A用户会搜到B用户的私密内容。Milvus的search接口支持expr参数来过滤标量字段。results collection.search( dataquery_vector, anns_fieldvector, param{metric_type: COSINE, params: {efSearch: 64}}, limit5, expruser_id user_001, output_fields[content], )这一步很关键它相当于在检索之前先划定“只在这位用户的书架里找”。如果你的数据量很大只靠过滤还不够那就要考虑分区键Partition Key功能让同一个用户的记录落在同一个物理分区查询时只扫描相关分区速度还能再上一个台阶。不过分区键一旦设置就不能随意更改设计表结构时要提前想清楚。5.3 观察查询耗时和召回效果的实操方法调优不能靠感觉。我在本地数据量到50万条时经常做这样一组测试准备20个问题每个问题由人工标注至少1条应命中的记忆然后分别用不同参数跑检索统计Top5里能命中的比例再看平均查询耗时。Milvus的search返回结果本身不打印耗时但你可以在调用前后用Python的time.time()包一层。import time start time.time() results collection.search(...) print(query time:, time.time() - start)如果召回率低于预期优先检查是不是Embedding模型不适合领域文本。这不是Milvus的问题很多召回差都差在“语义表示”上向量都表示错了检索算法再强也白搭。5.4 数据量上来之后的内存与磁盘观察Milvus的HNSW索引会把图结构加载到内存所以数据量越大内存占用越高。我自己的经验是512维向量、100万条数据仅向量本身大约占2GB加上索引和原始字段建议预留4GB以上内存。如果只做开发测试几十万条数据对笔记本压力不大。如果内存紧张可以考虑把部分字段设置为“不加载”查询时只返回需要的字段减少内存里的驻留对象。还可以参考官方文档启用MMap文件映射让部分数据按需从磁盘读入但这属于偏进阶的功能建议先把主链路跑顺。6. 我实际踩过的坑版本、维度、索引和清理6.1 pymilvus版本与服务端不匹配刚用Milvus时我按网上的老教程装了pymilvus1.0结果连2.4的服务端直接报协议不兼容。自2.0之后pymilvus和服务端需要保持小版本相近最稳妥的方法是查看官方版本对应表。安装时尽量指定版本比如pip install pymilvus2.4.13不要盲目装最新版也不要因为“兼容所以装老版本”这纯属给自己制造麻烦。6.2 维度写错插数据直接报错创建Collection时我把dim设成384后来换了一个输出维度为768的Embedding模型插入第一行数据就报维度对不上。Milvus的Schema不允许动态修改向量维度唯一办法是换个名字重新建Collection。所以模型选定后第一件事就是打印一下向量维度vec model.encode([测试]).tolist() print(len(vec[0]))然后再去建Collection。这个习惯能帮你省掉很多次删表重建的折腾。6.3 没建索引就查询慢到怀疑人生有一回我在测试环境导入了几万条数据忘记创建索引就直接search结果一个查询要好几秒和我想象的“毫秒级”差了很远。原因就是没建索引时Milvus只能全量扫描。补上create_index并load之后查询时间立刻降到几十毫秒。如果你发现查询特别慢先确认两件事索引是否已创建Collection是否已加载。6.4 反复调试后记得做这些清理开发过程中我经常建了很多测试Collection它们会一直占着MinIO里的存储。如果不需要了记得删除from pymilvus import utility utility.drop_collection(ai_memory)同时容器中的日志文件也会越堆越多定期用docker compose down停掉服务并使用docker system prune清理无用镜像和卷。不过要小心如果数据卷被删除之前导入的记忆也会一起没掉。所以删除前想清楚开发环境可以随意折腾生产环境请先备份。Milvus在我看来最舒服的一点是它把“向量检索”这个原本偏学术的东西做成了可以日常使用的数据库服务。你不需要懂太多索引算法细节也能通过SDK把自己的AI应用从“金鱼记忆”升级到“有图书馆可查”。等我把这套体系再跑一段时间后面会接着整理多租户权限和备份恢复的部分那又是另一层值得记录的经验了。
返回列表