
1. 为什么“答非所问”的锅通常不在模型做过 RAG 项目的人大概率都经历过这个场景用户问了一个问题系统检索回来一堆看起来相关的文档模型也认认真真生成了一段话但答的内容跟问题就是差着十万八千里。第一反应往往是“模型不行换个更大的”“提示词没写好再调调”然后花大量时间在 prompt 上反复折腾效果却始终不稳定。我踩过这个坑不止一次。后来复盘发现绝大多数“答非所问”的根因根本不在生成端而在检索端——向量化质量、切分策略、相似度度量、索引配置这几环里只要有一环出了问题后面模型再强也救不回来。这个判断不是拍脑袋而是有明确的排查路径可以验证的。这篇文章想聊的就是这套排查思路当 RAG 出现答非所问时怎么系统性地先查向量链路把问题定位清楚再决定要不要动模型。涉及的核心技术点包括向量化模型选型、Milvus 的索引与度量方式、LangGraph 编排下的检索节点调试以及一套可复用的排查清单。适合正在做 RAG 项目、被检索质量问题困扰的开发者也适合刚接触向量数据库、想搞清楚“向量到底在干什么”的朋友。2. 先搞清楚向量检索这条链路到底发生了什么2.1 从一句话到一次检索的完整路径很多人对 RAG 的理解停留在“把文档塞进向量库然后搜出来给模型”。但真正跑起来从用户输入到最终召回中间至少经过这么几步用户 query 经过 embedding 模型转成一个高维向量这个 query 向量去向量库里跟已存储的文档向量做相似度计算按相似度排序取 Top-K 个结果这些结果拼进 prompt交给 LLM 生成答案问题就藏在这四步里。第 1 步query 和文档是不是用了同一个 embedding 模型第 2 步相似度用的是余弦还是点积两者对归一化的要求一样吗第 3 步Top-K 取多少合适取少了漏召回取多了引入噪声第 4 步召回的片段本身是不是完整的语义单元我见过一个典型案例文档用某个中文 embedding 模型向量化query 却用了另一个英文为主的模型结果就是向量空间根本对不上检索出来的东西完全是随机的。这种问题你调一万遍 prompt 都没用。2.2 向量相似度到底在比什么要排查向量问题得先理解向量相似度的几何含义。这里不堆公式用直觉解释。一个文本被 embedding 模型转成一个高维向量比如 768 维或 1024 维。你可以把它想象成高维空间里的一个箭头箭头的方向代表了这段文本的语义。两段语义相近的文本它们的箭头方向就接近。余弦相似度衡量的是两个箭头方向的夹角夹角越小越相似取值范围 -1 到 1。它只关心方向不关心长度所以对向量做不做归一化不影响结果。点积则同时受方向和长度影响。如果两个向量都做了 L2 归一化长度变成 1那点积就等于余弦相似度。但如果没归一化点积会被向量长度带偏——一个长向量跟任何向量的点积都可能偏大导致检索结果失真。欧氏距离衡量的是两个箭头端点之间的直线距离距离越小越相似。它同样受向量长度影响。这就是为什么在 Milvus 里选度量方式时如果你用的 embedding 模型输出没有归一化却选了内积IP那检索质量很可能莫名其妙地差。这个坑非常隐蔽因为代码不报错只是结果不对。2.3 为什么“先查向量”是最高效的排查顺序从工程效率角度看排查顺序应该遵循“从确定性高、成本低的环节往确定性低、成本高的环节走”。向量链路里的问题大多是确定性的模型用错、维度不匹配、度量方式选错、归一化没做这些都能通过打印中间结果直接验证改起来也快。而模型生成端的问题往往是概率性的prompt 措辞、温度参数、上下文长度调起来费时且效果不稳定。所以正确的顺序是先确认 query 向量和文档向量在同一个空间、度量方式匹配、召回结果确实相关再去怀疑模型。把这条链路走通你会发现大部分“答非所问”在向量层就已经暴露了。3. 向量化环节最容易被忽视的第一道关3.1 embedding 模型选型的三个硬指标选 embedding 模型不能只看排行榜得结合你的实际场景。我一般看三个指标语言匹配度。如果你的知识库是中文为主就必须选中文语料训练充分的模型。很多英文模型在中文上的语义区分度很差相近的中文句子会被映射到几乎相同的向量位置检索时根本分不开。维度与性能的平衡。维度越高表达能力越强但存储和计算成本也越高。768 维和 1024 维在多数场景下够用1536 维以上要谨慎评估是否真的需要。我实测过一个场景把维度从 1536 降到 768检索准确率只掉了不到 2%但索引体积和检索延迟都明显改善。最大输入长度。这个参数决定了你的切分策略。如果模型最大只支持 512 token你却把 2000 字的段落直接塞进去超出部分会被截断语义就残缺了。指标常见取值选型建议语言中文/英文/多语言中文库优先中文模型维度384/768/1024/1536768-1024 性价比最高最大长度512/8192 token决定切分粒度上限归一化是/否影响度量方式选择3.2 切分策略语义完整比长度均匀更重要切分是 RAG 里最考验经验的一环。新手常犯的错是按固定字符数硬切比如每 500 字切一段。这样切出来的片段经常在句子中间断开语义不完整向量化后表达的意思就偏了。我的做法是按语义边界切分优先在段落、标题、列表项这些自然边界处断开再控制单段长度。如果一段太长就在句子边界处二次切分。这样每个片段都是一个相对完整的语义单元向量质量明显更好。还有一个细节是重叠区。相邻片段之间保留 10%-20% 的重叠内容可以避免关键信息正好落在切分点上被割裂。这个比例不是拍脑袋是因为语义的连续性往往跨越边界适当重叠能让检索时更容易命中完整上下文。提示切分长度要和 embedding 模型的最大输入长度对齐。如果模型支持 512 token单段控制在 300-400 token 比较稳妥留出余量避免截断。3.3 归一化一个不做就出事的预处理前面提到点积受向量长度影响解决办法就是在存入向量库之前做 L2 归一化把所有向量长度统一成 1。这样无论你用余弦还是内积结果都一致省去了很多心智负担。归一化的代码很简单但很多人会漏掉。我建议把它做成向量化流程里的固定步骤而不是依赖某个库的默认行为。因为不同 embedding 库的默认行为不一样有的自动归一化有的不归一化混用就会出问题。import numpy as np def normalize(vec): norm np.linalg.norm(vec) if norm 0: return vec return vec / norm这段代码看着简单但它能帮你排除掉一整类“检索结果莫名其妙”的问题。我现在的习惯是只要向量入库先过一遍归一化心里踏实。4. Milvus 索引与度量配置错了结果全歪4.1 度量方式怎么选才不踩坑Milvus 支持多种度量方式常用的有 COSINE、IP内积、L2欧氏距离。选哪个不是随便挑的得跟你的 embedding 模型和归一化策略配套。如果向量已经做了 L2 归一化那 COSINE 和 IP 结果等价选哪个都行。如果没归一化就必须用 COSINE因为只有它不受长度影响。L2 一般用在图像特征这类场景文本检索里相对少用。我见过有人用未归一化的向量配 IP检索结果时好时坏排查了半天才发现是度量方式的问题。改成 COSINE 之后立刻正常。这种问题不打印向量范数是看不出来的。4.2 索引类型对召回率的影响Milvus 的索引类型直接决定了检索速度和召回率的平衡。常用的几种FLAT是暴力检索召回率 100%但数据量大时慢得没法用。适合小数据集或者做效果验证的基准。IVF_FLAT通过聚类把向量分桶检索时只查部分桶速度快但会损失一点召回率。关键是 nlist 参数桶分得越细召回越高但越慢。HNSW用图结构组织向量检索快且召回率高是目前综合表现最好的选择。代价是内存占用大构建索引慢。IVF_PQ在 IVF 基础上做了乘积量化压缩内存占用极小适合超大规模数据但召回率损失明显。索引类型召回率速度内存适用场景FLAT100%慢高小数据/基准测试IVF_FLAT高中中中等规模通用HNSW很高快高追求效果优先IVF_PQ中快低超大规模省内存我的经验是如果数据量在百万级以内直接上 HNSW省心。数据量再大再考虑 IVF 系列。4.3 在 Mac 上用 Docker 跑 Milvus Standalone本地开发调试用 Docker 跑 Milvus standalone 模式最方便。standalone 是单机版包含了所有必要组件适合开发和中小规模部署。# 下载官方 compose 文件 wget https://github.com/milvus-io/milvus/releases/download/v2.4.0/milvus-standalone-docker-compose.yml -O docker-compose.yml # 启动 docker compose up -d # 查看状态 docker compose ps启动后默认端口是 19530。Mac 上要注意 Docker 分配的内存Milvus 至少需要 4GB建议在 Docker Desktop 里把内存调到 8GB 以上否则容易启动失败或者检索时卡顿。连接测试from pymilvus import connections, Collection connections.connect(hostlocalhost, port19530) print(连接成功)如果连不上先检查容器是否正常启动再看端口有没有被占用。Mac 上偶尔会遇到端口冲突换个端口映射就行。5. LangGraph 编排下的检索节点调试5.1 把检索做成独立可观测的节点用 LangGraph 编排 RAG 流程的好处是每个环节都是显式的节点方便单独调试。我一般会把检索拆成两个节点一个负责 query 向量化一个负责向量库查询。这样出问题时能快速定位是哪一步的锅。from langgraph.graph import StateGraph, END def embed_query(state): query state[query] vector embedding_model.encode(query) return {query_vector: normalize(vector)} def retrieve(state): vector state[query_vector] results collection.search( data[vector], anns_fieldembedding, param{metric_type: COSINE, params: {ef: 64}}, limit5, output_fields[text] ) return {docs: [hit.entity.get(text) for hit in results[0]]}把这两个节点分开调试时就能分别打印 query 向量和召回结果一眼看出问题在哪。5.2 在节点里埋入可观测信息光有节点还不够得让节点输出足够的信息供排查。我习惯在每个关键节点里记录输入是什么、输出是什么、耗时多少、召回的相似度分数分布。相似度分数特别有用。如果 Top-5 的分数都在 0.3 以下说明检索质量很差要么是向量化有问题要么是知识库里根本没有相关内容。如果分数都在 0.9 以上但答案还是不对那可能是召回了高度相似但实际不相关的内容需要检查切分和去重。def retrieve_with_debug(state): vector state[query_vector] results collection.search(...) scores [hit.score for hit in results[0]] print(f召回分数: {scores}) print(f最高分: {max(scores):.4f}, 最低分: {min(scores):.4f}) return {docs: [...], scores: scores}这个打印看着土但排查问题时比任何可视化工具都直接。5.3 用 LLM as Judge 做检索质量评估当检索结果肉眼不好判断时可以引入 LLM as Judge让模型来判断召回的内容跟 query 是否相关。这不是用来替代人工而是快速筛出明显有问题的 case。做法是把 query 和召回的每个片段拼成一个判断 prompt让模型输出“相关/不相关”。批量跑一批测试 query统计相关率。如果相关率低于 70%基本可以确定检索链路有问题先别动生成端。注意LLM as Judge 本身也有偏差只适合做粗筛最终判断还是要靠人工抽查。别把它当成绝对标准。6. 常见问题排查速查表6.1 检索结果完全不相关这是最典型的问题。排查顺序确认 query 和文档用的是同一个 embedding 模型打印两者的向量维度对比检查向量是否归一化打印向量范数确认 Milvus 的度量方式跟归一化策略匹配用一条已知答案的 query 做测试看能否召回对应文档如果这四步都正常再去看切分是否合理。我遇到过切分把关键信息切碎导致召不回的情况把切分粒度调大就好了。6.2 检索结果相关但答案不对这种情况说明检索没问题问题在生成端或者上下文组织。检查召回的片段是否包含了回答问题所需的完整信息prompt 里是否明确要求模型基于给定上下文回答上下文是否太长导致关键信息被淹没有时候召回 10 个片段关键信息只在第 8 个里模型注意力被前面的噪声带偏了。这时候减少 Top-K 或者做重排序rerank会有帮助。6.3 相似度分数普遍偏低如果所有 query 的召回分数都低可能是embedding 模型不适合你的领域换一个领域适配更好的知识库内容跟 query 的表达方式差异太大考虑做 query 改写向量维度太低表达能力不足6.4 排查速查表现象可能原因排查动作完全不相关模型不一致/未归一化打印向量维度和范数相关但答错上下文组织问题检查 prompt 和 Top-K分数普遍低模型不适配/维度低换模型或提维度时好时坏度量方式不匹配统一用 COSINE召回漏关键信息切分过碎调整切分粒度7. 几个我踩过的坑和实操心得第一个坑是混用 embedding 模型。早期项目里文档入库用了一个模型后来换了个更新的模型做 query 向量化忘了重新索引文档结果检索质量断崖式下跌。教训是换 embedding 模型必须全量重建索引没有例外。第二个坑是忽略向量维度对齐。有次从 768 维模型换到 1024 维Milvus 的 collection 还是按 768 维建的插入时报错才发现。建 collection 时一定要把维度写死并跟模型对齐别用动态维度。第三个心得是建立检索质量的回归测试集。准备 50-100 条带标准答案的 query每次改动检索链路后跑一遍看召回率变化。这比凭感觉判断靠谱得多。我现在的项目里这个测试集是必建的改任何检索相关配置前先跑基线。第四个心得是别急着上复杂方案。很多人一上来就搞多路召回、重排序、query 改写结果基础链路都没调通。我的建议是先把单路向量检索调到满意再考虑加复杂度。基础不牢加再多组件也是白搭。最后一个体会是关于排查心态。RAG 出问题时人容易焦虑想快速换个模型试试。但换模型是成本最高的操作而且往往治标不治本。养成先查向量链路的习惯把确定性的问题先排除掉剩下的才是真正需要模型层面解决的。这个顺序理顺了排查效率能提升好几倍。这套排查方法我在多个项目里验证过从中文知识库到多语言场景都适用。核心就一句话向量层的问题用向量的方式查别让模型背不该背的锅。