提一个有意思的现象最近不少做视频生成、多模态项目的朋友都在聊为什么像 Sora2、Veo3 这类顶流模型对外讲的都是生成能力多强对内却在偷偷铺一条完全不同的技术线——向量引擎。我自己前阵子帮某个跨平台系统做知识库升级时也踩了一遍这套东西越做越觉得很多人把注意力全放在模型参数上反而忽略了 RAG 架构里那块真正的拼图向量检索。先说清楚这不是要蹭视频模型的热度。视频生成模型本身并不直接依赖 RAG但它们背后的多模态理解、长上下文建模、以及“从海量片段里快速找到最相关信息”的能力几乎都绕不开向量化检索。换句话说一个生成模型要处理“用户给一段描述系统从素材库 / 知识库 / 历史数据里找出最匹配的内容再生成”这一步做的其实就是向量检索的活。这篇文章就把我从零搭完一套 RAG 实战链路后的完整心得写出来包含源码级实现、参数计算、踩坑记录以及为什么说向量引擎是那块绕不开的最后拼图。1. 先用大白话拆清楚RAG 到底在解决什么问题1.1 生成模型不是“什么都懂”它只是“什么都记得住一点”很多刚接触大模型的朋友会有个错觉模型参数量越大知识就越全。实际用下来你会发现模型对常识性问题的回答很流畅但一旦涉及你所在团队内部的业务文档、某个产品最近更新的接口说明、或者上个月刚发生的线上故障复盘它就开始一本正经地胡说八道。原因很简单大模型的知识来自训练时的语料训练完成后权重就固定了。你没法指望一个已经训练好的模型自动知道“你们公司昨天新上线的那个功能叫什么名字”。这时候有两种解决思路一是拿私人数据继续微调模型成本高、周期长、且每次数据更新都要重新训练第二种就是 RAG——不改变模型权重而是让模型在回答前先“查资料”。具体流程可以类比成开卷考试模型不是把答案背在脑子里而是先给你一本指定的参考书它边翻书边答题。向量引擎就是那本参考书的目录和检索系统——它决定模型能不能快速翻到正确的页码。1.2 Sora2、Veo3 这类模型为什么也需要“查资料”你可能要问了视频生成模型不是做生成的嘛跟 RAG 有什么关系关系还真不小。以视频生成为例一个完整的生成链路通常包含几个阶段用户输入一段自然语言描述系统把描述转换成内部的“语义表征”然后从预先整理好的素材库中检索最相关的视频片段、图片、音频最后再交给生成模型做合成或扩展。这个“从素材库中检索最相关片段”的过程本质上就是一个向量检索任务。素材库里的每个视频片段都被离线编码成一个高维向量用户的文本描述也被编码成向量然后通过近似最近邻搜索找到语义上最匹配的片段。Sora2、Veo3 这类顶流模型在宣传中强调生成效果但支撑其效果的底层链路里向量检索的性能直接决定了生成素材的质量上限。我在实现某图像处理 Demo 时也遇到同样的瓶颈模型本身输出的图像质量不错但如果你想让模型“参考公司历史设计风格来生成新图”没有一个能高效检索历史风格向量的引擎整个流程就会卡在“找到对的东西”这一步上。1.3 RAG 架构的四层拼图向量引擎是最后一块一套完整的 RAG 系统我习惯分成四层看数据接入层负责把各种格式的文档、图片、视频片段、数据库记录统一解析成纯文本或统一模态的数据。向量化层把上面这些数据转换成向量。这里的核心是一套嵌入模型比如常用的 BGE、M3E或者多模态场景下的 CLIP 类模型。向量存储与检索层把生成的向量存进专门的向量数据库并提供快速的相似度搜索能力。这就是标题里说的“向量引擎”。生成与编排层把检索结果拼进 prompt调用大模型生成最终答案或内容。前三层做不好第四层再强也白搭。但现实中大多数团队的精力都花在了调 prompt 和模型选型上对向量化层和检索层的重视程度严重不足。等到回答质量上不去、召回不准的时候才回头怀疑是不是嵌入模型选得不对、向量引擎参数没调好。我个人的经验是RAG 系统的质量短板绝大多数出在“查不到”和“查不准”而不是“生成能力不够”。理解了这一点就能理解为什么顶流模型也要在向量引擎上下重注。2. 向量引擎的底层原理不是“查表”而是高维空间里的最近邻搜索2.1 从“关键词匹配”到“语义匹配”向量到底存的是什么传统搜索比如你用一个数据库的 LIKE 查询或者早期搜索引擎的关键词匹配本质上是字符串级别的比对。你搜“苹果”它只能匹配包含“苹果”这两个字的文档匹配不了“红富士”“iPhone”“库克”这些语义上相关但字面上完全不同的内容。向量检索完全不同。嵌入模型会把一段文本、一张图片、一段视频映射成一个几百到几千维的浮点数组。比如一段关于“苹果营养价值”的文本经过嵌入模型处理后会变成一个 1024 维的向量。这个向量里每一个维度的数值本身没有直观含义但整体上它处在一个“语义空间”中——语义相近的内容在高维空间中的距离就近语义无关的内容距离就远。你可以把这个空间想象成一个巨大的三维地图只不过维度从 3 变成了 1024。每一条数据都是地图上的一个点。用户查询来了现在的问题就变成了在这个地图上找出离查询点最近的 K 个点。这就是最近邻搜索问题的由来。2.2 暴力搜索肯定不行近似最近邻ANN才是正解先看最朴素的做法遍历库里所有 N 条向量逐一计算查询向量与每条向量的内积或余弦距离排个序取前 K 个。精确、简单但问题也摆在明面上——当 N 达到百万甚至千万级别每条向量是 1024 维浮点数一次线性扫描的计算量是千万次浮点数乘加。线上服务动辄要求百毫秒级响应暴力搜索直接出局。实际工程里广泛采用的是近似最近邻搜索也就是 ANN。它牺牲一部分可接受的精度换取几个数量级的速度提升。常见的 ANN 索引结构有几大类基于哈希的方法比如 LSH把相近的向量以一定概率哈希到同一个桶里。基于树的方法比如 KD-Tree、Annoy 里的随机投影树。基于图的方法代表就是 HNSW目前工业界应用最广泛。基于量化的方法比如乘积量化 PQ把高维向量压缩成紧凑编码再计算。我前阵子搭系统的时候做过一组对比同样 100 万条 768 维向量暴力搜索单次查询平均耗时 320 毫秒换用 HNSW 索引后降到 15 毫秒以内召回率保持在 95% 以上。这个差距在线上服务里就是“能不能用”的区别。2.3 HNSW 的工作原理跳表思想在高维空间里的延伸在众多 ANN 算法里HNSW 是我用得最多也最推荐的。它不是某个团队拍脑袋发明的而是把早期 NSW 图算法和跳表的分层思想结合后的产物。HNSW 的核心思路构建一张多层次图。底层包含所有数据点连接最密集越往上层的点数越少连接越稀疏。查询时先从最顶层开始沿着稀疏连接快速找到一个大致的区域然后逐层往下细化直到最底层完成精确的近邻查找。这就像你在一座大城市找人——先确认在哪个区再确认哪条街最后精确到门牌号而不是从头到尾挨家挨户敲门。控制 HNSW 索引精度与速度的关键参数有三个M每个节点的最大连接数。M 越大图越稠密召回越高但内存和构建时间也越高。efConstruction构建索引时动态候选列表的大小。它影响索引质量越大索引质量越高但构建越慢。efSearch查询时动态候选列表大小直接影响查询精度。通常调高 efSearch召回率上升但延迟也上升。我常用的参数起点是 M16、efConstruction200、efSearch64实测在大部分场景下能兼顾召回率和延迟。具体调优方法后面实操部分再展开。2.4 向量数据库选型从轻量级到分布式聊向量引擎绕不开选型。市面上叫得出名字的不少我按使用场景把它们分成三类轻量级嵌入式比如 sqlite-vss、Chroma。适合单机小规模、快速验证、或者嵌入到客户端应用里。优点是部署简单缺点是横向扩展能力弱。独立服务型比如 Milvus、Qdrant、Weaviate。适合生产环境有完整的数据管理、索引构建、分片复制能力。其中 Milvus 对海量高维向量的支持比较扎实我在模拟项目 X 里用的就是它。基于传统数据库的扩展比如 PostgreSQL 的 pgvector、Redis 的向量搜索模块。适合团队已有这套基础设施、不想额外引入新组件的情况但大规模场景下性能和功能都会受限于宿主系统。选型没有绝对标准核心看三个约束数据量级、QPS 要求、团队运维能力。数据百万级以下pgvector 或者 Chroma 完全能扛住千万级以上、QPS 上百老老实实上独立向量数据库。我自己踩过的坑是一开始为了图省事把 500 万条向量塞进了一个轻量级方案结果查询延迟飙到 300 多毫秒后期迁移重建索引的成本远高于一开始就选对方案的代价。3. 从零搭建一套可运行的 RAG 系统完整实操与源码3.1 环境准备与依赖安装先交代一下我这套方案的技术栈Python 3.10、LangChain 做编排、HuggingFace 生态的 BGE 嵌入模型、Milvus 做向量存储、OpenAI 兼容接口做生成。选这套组合的理由很简单组件成熟、社区活跃、出了问题能搜到答案。创建一个虚拟环境并安装依赖python3 -m venv rag_env source rag_env/bin/activate pip install langchain langchain-community langchain-openai pip install pymilvus2.3.7 pip install sentence-transformers2.7.0 pip install bge-m3这里的版本号需要特别注意。LangChain 的 API 迭代很快如果你直接用最新版会发现旧教程里的很多导入方式已经失效。我装的是 0.1.x 系列的 LangChain配合 pymilvus 2.3.x整个链路能跑通。如果你用的是更新的版本建议以官方文档为准不要盲目照抄旧代码。嵌入模型选的是 BGE-M3。它支持中文和英文生成 1024 维向量多语言场景下表现比较稳。如果你的数据是纯中文也可以考虑 M3E 模型向量维度是 768检索速度会快一些代价是英文场景的泛化稍微弱一点。3.2 数据切分RAG 里最容易被低估的一步在我接触的很多 RAG 项目里检索效果差最根本的原因不是向量引擎不行而是进索引的数据切得不对。你可以把切分想象成做菜备料——材料切得太粗下锅不入味切得太细炒完不成形。文本也一样切得太长比如直接把一篇 5000 字的文章整篇塞进索引嵌入模型会把整篇内容压成一个向量结果就是语义过度稠化查询时很难精确命中文章中某一小段的关键信息切得太短比如按一句话一切语义不完整检索出来的片段缺乏上下文喂给大模型后回答会显得支离破碎。我的经验是采用“按语义块切分 片间重叠”的策略。所谓语义块就是尽量在段落、章节的边界上切而不是机械地按字数硬切。重叠的意思是在相邻两个块之间保留一部分重合内容避免跨块的关键信息被拦腰截断。以下是我在项目里用的切分代码from langchain.text_splitter import RecursiveCharacterTextSplitter splitter RecursiveCharacterTextSplitter( chunk_size512, chunk_overlap80, separators[\n\n, \n, 。, , . , , ] ) docs splitter.split_text(original_text)chunk_size512 表示每个块最多 512 个字符chunk_overlap80 表示相邻块重叠 80 个字符。这个配置不是拍脑袋定的我分别测过 256/512/1024 三档512 在中文场景下召回率和回答完整度最均衡。256 太碎检索到的片段经常缺少上下文1024 太长语义被稀释好几个不相关的话题被揉进同一个向量里查询时容易召回“看起来像但其实不是”的内容。3.3 向量化与索引构建把文本变成可检索的高维坐标切分完之后进入向量化环节。用 BGE-M3 把每个文本块转换成向量from sentence_transformers import SentenceTransformer model SentenceTransformer(BAAI/bge-m3-2023) vectors model.encode(docs, normalize_embeddingsTrue)注意 normalize_embeddingsTrue 这个参数。归一化之后向量的内积就等于余弦相似度后续检索时计算更高效。这个细节很多教程不会提但实测对检索结果的稳定性有明显影响——不归一化时向量的模长差异会干扰相似度排序。向量化完成后写入 Milvus。先创建 collection指定向量维度为 1024并配置 HNSW 索引参数from pymilvus import connections, CollectionSchema, FieldSchema, DataType, Collection, utility connections.connect(host127.0.0.1, port19530) fields [ FieldSchema(nameid, dtypeDataType.INT64, is_primaryTrue, auto_idFalse), FieldSchema(namevector, dtypeDataType.FLOAT_VECTOR, dim1024), FieldSchema(nametext, dtypeDataType.VARCHAR, max_length8192), ] schema CollectionSchema(fieldsfields, descriptionrag_demo) collection Collection(namerag_demo, schemaschema) index_params { index_type: HNSW, metric_type: IP, params: {M: 16, efConstruction: 200} } collection.create_index(vector, index_params)这里 metric_type 用 IP 还是 COSINE取决于嵌入向量是否归一化。前面已经做了归一化所以 IP 和 COSINE 等价但 IP 在高性能计算上更有优势。如果你在别的项目里跳过了归一化步骤那就得用 COSINE否则检索结果会失真。数据写入时批量插入效率更高import numpy as np rows [] for i, (vec, txt) in enumerate(zip(vectors, docs)): rows.append({id: i, vector: vec.tolist(), text: txt}) collection.insert(rows) collection.flush()3.4 查询链路从用户问题到最终回答的完整走查索引建好后查询链路分四步走第一步同样用 BGE-M3 把用户问题编码成向量。这里有个容易被忽略的坑查询端的向量必须和文档端的向量由同一个模型生成否则两者的向量空间不一致检索结果会非常离谱。第二步在 Milvus 里做近似最近邻搜索拿到相似度最高的前 K 个文本块。K 的取值根据实际场景调我一般在 4 到 8 之间折中选。K 太小可能漏掉关键信息K 太大无关片段混进来反而干扰大模型的回答。第三步把检索到的文本块按相关度重组拼进 prompt。这里有个技巧像 Milvus 这类引擎返回结果时默认按距离升序排距离越小越相关所以不用重新排序直接按返回顺序拼接就可以。第四步调用大模型生成最终回答。完整代码如下question 这个项目支持哪些格式的图片上传 q_vector model.encode([question], normalize_embeddingsTrue)[0] results collection.search( data[q_vector.tolist()], anns_fieldvector, param{metric_type: IP, params: {efSearch: 64}}, limit5, output_fields[text] ) context \n---\n.join([hit.entity.get(text) for hit in results[0]]) prompt f请根据以下资料回答问题。 资料 {context} 问题 {question} 回答要求只根据资料内容回答资料中没有的信息不要编造。 response openai_client.chat.completions.create( modelgpt-4o-mini, messages[{role: user, content: prompt}], temperature0.2 ) print(response.choices[0].message.content)跑通这条链路之后你就拥有了一套最简 RAG 系统。从文档导入、切片、向量化到检索生成全链路不超过两百行代码。但实跑阶段你会发现效果并没有想象中那么“智能”——接下来这部分才是真正的分水岭。4. 实跑中的坑与调优这些问题网上很难搜到完整答案4.1 召回率低先别急着换模型检查这四处遇到检索结果不理想我见过太多人第一反应是换更强的嵌入模型。但根据我的排障经验真正的问题绝大多数出在更基础的地方。按优先级排查第一检查切分粒度。如果你检索到的片段“沾边但不精准”先检查 chunk_size 是不是太大。我遇到过把整个章节作为一条向量入库的线上案例查询“接口超时怎么处理”结果召回的全是“系统架构概述”这种八竿子打不着的段落。换成 512 切分后问题立刻缓解。第二检查查询端的嵌入模型和文档端是否一致。这个错误隐蔽性强尤其是在多语言或多模态场景下很容易配置成两个不同的模型。向量空间不一致相似度计算纯属开盲盒。第三检查归一化。很多人写代码时不会留意 normalize_embeddings导致入库向量和查询向量的尺度不一致。特别是用 IP 内积度量时向量模长大会直接主导排序小模长的语义相似向量反而被压到后面。第四检查 efSearch 参数。如果线上查询速度很快但召回明显变差很可能是 efSearch 调太低。默认 64 在某些数据集上会漏检我一般从 128 起步观察召回率和延迟的平衡点。4.2 数据更新了但搜出来还是旧内容RAG 系统上线后还有个大坑是数据更新。很多团队把新文档向量化写入后发现搜索结果里还是旧内容指向的还是老的地址或接口。原因往往出在 Milvus 的删除操作上。Milvus 支持按主键删除但删除后的数据不会立刻从索引中完全消失默认的 consistency_level 也会影响可见性。我在某跨平台系统里就遇到过业务方更新了产品文档线上搜索两小时内依然返回旧版本内容。解决思路有两个方向。一是写入新数据时显式删除旧数据的主键collection.delete(exprfid in {old_ids})二是利用版本化方案每条数据带上 version 或 timestamp 字段查询时通过布尔表达式过滤掉旧版本。相比频繁删除这种方式在需要回溯历史版本的场景下更稳妥但会占更多存储。还有个实用的技巧如果更新频率很高不要把索引建得太重。HNSW 索引在频繁增删场景下会产生碎片影响查询质量。必要时可以定期重建索引Milvus 提供 compact 接口来合并段和清理删除标记。4.3 延迟瓶颈向量检索提速的三个方向系统上线后查询延迟往往成为新的瓶颈。我在 500 万条向量、单机部署的环境下实测过一组数据不优化时平均查询延迟 140 毫秒经过三轮优化后降到 28 毫秒。三个方向分别如下。第一个方向是缩小向量维度。如果你的嵌入模型输出 1024 维可以考虑换成 768 维或 512 维的模型。这个取舍很现实维度砍一半内存占用和计算量都显著下降召回率只会小幅波动。纯中文知识库场景下用 768 维模型配合 HNSW 完全够用。第二个方向是加缓存。高频查询的 vector 检索结果可以在 Redis 里存一份key 用查询文本的固定哈希value 是命中的文档 ID 列表。缓存命中时跳过向量检索直接把对应文档取出来拼 prompt延迟直接从几十毫秒降到个位数。第三个方向是分片与并行。Milvus 支持对 collection 做分片数据量大了之后可以放在多台机器上查询时并行扫描各分片再合并结果。如果暂时没有多机条件也可以手动按业务领域拆成多个 collection查询时并行搜再合并排序。这个思路在数据天然带业务隔离属性时特别有效——不同业务各查各的库互不干扰。4.4 一个真实项目的调优记录召回率从 67% 到 91%为了让你对调优过程有更直观的感受我把某图像处理 Demo 里的一段真实调优记录摘出来。背景约 20 万条产品说明文档混合中英文目标是根据用户自然语言查询找到最相关的产品说明。第一轮默认参数chunk_size1024efSearch64召回率 67%。主要问题中文长文本语义被稠化查询“支持批量处理吗”经常召回到产品概述这类泛泛的内容。第二轮chunk_size 改为 512chunk_overlap 设为 80召回率升到 78%。有改善但检索结果里混入了不少部分匹配的干扰项。第三轮换用 BGE-M3 并打开 normalize_embeddings召回率到 84%。这一步能看出基础模型的选择确实有关系但到不了“换了就脱胎换骨”的程度之前说的工作都做完了模型差异才开始显现。第四轮efSearch 从 64 调到 128召回率升到 89%。延迟从 18 毫秒升到 31 毫秒仍然可接受。再往上调 efSearch 到 256召回率只涨到 91%延迟却飙升到 70 毫秒性价比明显下降。这一轮下来我最深的体会是调优是个循序渐进的过程每项措施贡献一点叠加起来效果才显著。不要指望某个参数一改就质变也不要在基础问题没解决前盲目追求高端模型。5. 进阶玩法混合检索、重排与多模态扩展5.1 混合检索关键词与向量的双保险纯向量检索在冷门词汇、精确编号、代码片段这些场景下并不总是最优解。比如用户搜“接口返回 502”向量检索可能召回到“网关错误”这类语义相近的内容反而匹配不到文档中精确提到 502 的那一段。业界常见的做法是混合检索向量检索跑一路关键词检索跑一路再通过一个合并策略对齐两路结果。关键词检索可以用 ES 或 Milvus 自带的 scalar 过滤也可以用 BM25。合并策略最简单有效的是加权分数线性融合RRF公式是 score 1/(k rank)。我实测过混合检索相比纯向量检索在代码和型号类问答场景下准确率能再提升 5 到 8 个百分点代价是系统复杂度多了一个检索链路。5.2 重排模型让召回结果的排序真正“懂语义”向量检索召回的 Top-K 结果里真正能让大模型用得顺手的可能只有一半。这是因为向量相似度衡量的是“语义空间里的距离”和“回答问题所需的合适程度”并不完全等价。重排模型Reranker就是专门解决这个问题的。它把查询和每个召回片段拼接起来用一个交叉编码器逐对计算相关性分数再按分数重新排序。相比嵌入模型的双塔结构交叉编码器的表达能力更强因为模型能同时看到查询和文档的完整文本而不只是两个独立的向量。在 LangChain 里接入重排模型很直接我用的是 Cohere Rerank 或者 bge-reranker-base。实际经验是向量检索 Top 50 召回再重排取 Top 5比直接向量检索 Top 5 的质量要稳得多。代价是多了一次推理开销在 QPS 不高的内部工具场景下完全可接受。5.3 多模态扩展Sora2、Veo3 背后的同样套路回到标题里提到的 Sora2 和 Veo3。视频生成场景下的向量引擎其实和我前面讲的技术链路共享同一套骨架只是嵌入模型和数据类型换了。文本对应的是文本嵌入模型视频片段对应的是多模态嵌入模型比如开源社区常用的 CLIP 系列。视频的每一帧或者每一个镜头片段会被编码成向量用户输入一段描述同样编码成向量然后在素材库里搜索最匹配的镜头再交给视频生成模型做扩展或合成。前阵子我帮一个团队做过图像素材的语义检索模块用的就是 CLIP 做图片编码、Milvus 做索引和检索。效果比纯标签匹配好不少用户输入“黄昏时分城市街道的氛围感画面”系统能检索出构图和色调相近的图片哪怕这些图片从来没有被打过“黄昏”这个标签。这套方法接下来还能顺理成章地往音频检索、代码检索、甚至行为序列检索上迁移。只要能把数据映射成向量向量引擎就适用。从这个角度看RAG 的尽头不只是文本知识库而是所有可向量化数据的统一检索入口。6. 避坑清单与给大家的实操建议6.1 上线前必须确认的 5 个配置项确认文档端和查询端用的是同一个嵌入模型且模型版本完全一致。版本不同向量空间可能就有偏移即使名字一样。确认入库向量是否归一化并让检索度量方式与归一化状态保持一致。确认 chunk_size 是否和你的数据特征匹配。法律合同、代码仓库、医疗病历的合适切分粒度完全不同。确认 HNSW 的 efSearch 和 efConstruction 参数不是默认值。默认值很多时候是给通用场景兜底用的不等于给你的场景调过。确认线上数据有明确的更新与删除机制。写入和更新走同一套流程避免新旧数据并存。6.2 节省成本的实用小技巧向量数据库的资源占用大头在内存。减少内存占用最直接的办法是把向量改成 float16 存储——代价是极小精度损失换来内存直接减半。 如果你的向量库数据量很大而且业务允许一点精度损失可以上 PQ 量化把向量压缩到原来的四分之一甚至八分之一代价是召回率进一步下降需要配合重排模型把精度救回来。还有一个成本技巧按业务冷热程度拆 collection。热点数据放在内存里冷数据可以放在磁盘上查询慢一点。实际数据更新频率极低的冷库查询量也不大没必要让它占着昂贵的内存资源。6.3 后续扩展思路如果你已经跑通了基础 RAG下一步可以尝试的扩展方向有三个一是接入更多数据源类型比如网页抓取、数据库记录、邮件存档让知识库覆盖面更广。 二是引入知识图谱把实体关系和向量检索结合回答“谁负责什么系统”这类关系型问题。 三是做检索结果的质量评估体系定期抽检一批查询样本人工标注召回结果是否符合预期形成持续回归的评测集。我个人的体会是向量引擎这套东西入门不难但做好需要一轮接一轮的细节打磨。每次调优的收益看起来都不大五六个点、十来个毫秒但这些收益叠起来决定了系统是从“demo 能跑”到“生产能用”的差别。如果你正在搭 RAG 系统建议从选对切分粒度、确认归一化、配好 HNSW 参数这三件事做起把基础打扎实之后再谈模型升级和多路召回。
阅读完成 · 觉得有帮助?