我的 RAG 问答系统总在一些问题上答非所问,明明知识库里就有正确答案,它却引用了一堆看着相关、其实跑题的片段编出似是而非的答案,查了半天发现我只用向量相似度取了 top-k 就直接喂给大模型、压根没做重排序的深度复盘

我做了个 RAG 问答系统:把文档切片转向量存进向量库,提问时把问题也转向量,检索最相似的若干片段连同问题喂给大模型作答,demo 效果不错就上线了。可上线后系统总在某些问题上答非所问,明明知识库里就有标准答案,它却引用一堆看着沾边、实则跑题的片段编出似是而非的答案;我把正确答案文档翻出来核对,它确实在库里且高度相关,换更强的大模型也没用。折腾很久才把矛头指向我以为最不会出错的检索:打印实际召回的 top-k 一看,真正含标准答案那条相似度只有 0.76、排在第 7 位,被一堆相似度更高却跑题的片段挤出了 top-5。根因是向量相似不等于真正相关:向量检索只擅长粗筛召回、不擅长精排,我把召回的 top-k 直接当最终答案喂进去,缺了重排序这关键一步。正解是召回放宽多捞候选,再用 reranker(cross-encoder 联合看问题和片段判真相关性)精排,只把真正靠前的喂给大模型,并配合混合检索(向量+BM25)和评估集量化命中率。这篇复盘从故障现场讲到相似为何不等于相关、召回与精排为何是两步、怎么诊断,再到加重排序、混合检索、量化评估的完整正解,以及只用向量漏精确术语、切片切坏、embedding 不匹配、全凭手感等坑,和找到一批看着相关的不等于选出真正对的、看着像只是入围不是当选、要分清召回别漏与精排选对的认知。

我的 RAG 问答系统总在一些问题上答非所问,明明知识库里有正确答案,它却引用了一堆看着相关、其实跑题的片段,查了半天发现我只用向量相似度取了 top-k 就直接喂给大模型、压根没做重排序的深度复盘

这是一次让我对"检索到"和"检索对"的区别有了刻骨认知的事故。我做了一个基于 RAG(检索增强生成)的问答系统:把公司文档切片、转成向量存进向量库,用户提问时把问题也转成向量,从库里检索出最相似的若干片段,连同问题一起喂给大模型,让它依据这些片段回答。demo 阶段效果不错,我信心满满上线了。

可上线后,用户反馈接二连三:系统总在某些问题上答非所问。明明知识库里就有标准答案,它却像没看见一样,引用了一堆看着沾边、实则跑题的片段,然后一本正经地基于这些跑题片段编出一个似是而非的答案。我把那些问题的正确答案文档翻出来核对——它确实在知识库里,而且和问题高度相关。答案就在库里,系统却没能把它捞出来用上。我一度怀疑是大模型不行,换了更强的模型,问题依旧。折腾了很久才把矛头指向我一直以为"最不可能出错"的那一步:检索。

故障现场:答案在库里,却被"看着相似"的片段挤掉了

我打印出每次检索实际召回、喂给大模型的 top-k 片段,和我心里"正确答案应该在的那个片段"一对比,真相大白:

用户问题: "我们的退货政策,生鲜类商品过了几天就不能退了?"

我的检索(只按向量相似度取 top 5):
  片段1 [相似度0.82]: "关于退货政策的总体说明,本公司一向重视..."  ← 沾"退货政策"的边,但没说生鲜
  片段2 [相似度0.81]: "生鲜类商品因其特殊性,在仓储和运输环节..."  ← 沾"生鲜",但讲仓储不讲退货
  片段3 [相似度0.80]: "商品的退换货流程一般包括申请、审核..."  ← 讲流程,不是时限
  片段4 [相似度0.79]: "政策可能随时调整,请以最新版本为准..."  ← 几乎是废话
  片段5 [相似度0.78]: "几天内的订单可以查询物流状态..."  ← "几天"撞词,完全跑题

  ✗ 真正的答案片段在哪? 它的相似度只有 0.76, 排在第 7 位, 没进 top 5!
    片段7 [相似度0.76]: "生鲜、冷冻类商品自签收起 24 小时内、且不影响二次销售的可申请退货,超过则不予退货"
                        ↑↑↑ 这才是标准答案! 却因为"相似度"略低被挤掉了

大模型只拿到了 1-5 这堆"看着相关其实跑题"的片段,
当然答不出"24小时",只能含糊其辞或编造。

看着这个对比我愣住了:真正含有标准答案的片段,明明语义上最该被选中,它的"向量相似度"却不是最高的,反而被一堆"关键词撞了、语义沾边、实则没回答问题"的片段挤出了 top-k。我一直默认"向量相似度高 = 内容相关 = 该选它",可现实狠狠打脸:相似,不等于相关;能被检索到,不等于检索对了。

第一件事:搞懂为什么"向量相似度最高"≠"最该被选中的答案"

冷静下来,我去补了 RAG 检索这一环的课,才明白我对"向量检索"的理解太天真了:

【向量检索(召回)在做什么,它的局限是什么】

向量检索的原理:
  - 把文本用 embedding 模型转成向量(一串数字),语义相近的文本向量也相近
  - 检索时算问题向量和每个片段向量的相似度(余弦相似度等),取最高的 top-k

它的优势: 快、能跨词面理解语义(不像关键词匹配那么死板)、适合从海量片段里"粗筛"

但它的关键局限——【相似 ≠ 相关】:
  1. embedding 是"通用语义"的压缩, 它捕捉的是"整体上像不像",
     而不是"这段文字到底有没有回答这个具体问题"
  2. 一段文字可能和问题"主题相似"(都在讲退货、都提生鲜),
     但并没有给出问题要的那个具体信息(时限是24小时)
     → 它相似度高, 但其实不相关(没回答问题)
  3. 反过来, 真正含答案的片段, 可能因为措辞、句式、信息密度等原因,
     整体向量相似度反而没那么突出 → 被挤出 top-k
  4. embedding 还会被表面信号带偏: 撞关键词("几天")、长度、句式……

核心认知:
  向量检索擅长"召回"(把可能相关的一批粗筛出来, 保证答案大概率在这批里),
  但它【不擅长精排】——在这批里精准判断"哪个才是真正回答了问题的",
  恰恰不是相似度数值大小能可靠决定的。

  我的错误: 把"召回"这一步的相似度排序, 直接当成了"精排"的最终结果,
            top-k 取了就喂给大模型, 中间缺了"重排序(rerank)"这关键一步。

这一下点醒了我:我把一个"粗筛"工具(向量召回)的输出,当成了"精选"的最终答案直接用了。向量检索的职责是"把可能相关的几十上百条粗筛出来,保证真答案大概率在这批里";而"从这批里挑出真正回答了问题的那几条",是另一项需要更强相关性判断的工作——重排序(reranking)。我缺了这一环,等于让粗筛直接拍板,真答案自然容易被"看着像"的片段挤掉。RAG 的回答质量,上限取决于喂给大模型的片段质量;检索没捞对,大模型再强也是基于错料硬编。

第二件事:正解——召回放宽 + 重排序精排,让"真正相关"的浮上来

找到根因,正解就清晰了:把"召回"和"精排"分成两步——召回这一步把范围放宽(多捞一些候选),再加一个"重排序(rerank)"模型在候选里做精准的相关性打分排序,最后只把重排后真正靠前的几条喂给大模型。

# 错误做法: 向量召回 top-5 直接喂大模型(粗筛当精选)
def rag_bad(question):
    q_vec = embed(question)
    chunks = vector_db.search(q_vec, top_k=5)   # 只按相似度取5条
    return llm_answer(question, chunks)          # 直接喂 → 跑题片段进来了

# 正解: 召回放宽到 top-N, 再用 rerank 模型精排, 取精排后的 top-k
def rag_good(question):
    q_vec = embed(question)
    # 1) 召回: 故意多捞(top 30~50), 目的是"别把真答案漏在范围外"
    candidates = vector_db.search(q_vec, top_k=30)
    # 2) 重排序: 用 cross-encoder / rerank 模型, 对每个候选和问题做
    #    "真正的相关性"打分(它能联合看问题和片段, 判断"有没有回答")
    scored = rerank_model.score(question, [c.text for c in candidates])
    # 3) 按 rerank 分数重新排序, 取真正靠前的几条
    reranked = [c for c, _ in sorted(zip(candidates, scored),
                                     key=lambda x: x[1], reverse=True)]
    top = reranked[:5]
    return llm_answer(question, top)             # 喂给大模型的是"真正相关"的

关键在于重排序模型(reranker,通常是 cross-encoder)和向量召回的本质不同:向量召回是把问题和片段各自单独编码成向量再算相似度(快但粗);而 reranker 是把"问题 + 片段"一起喂进模型,联合判断"这段到底有没有回答这个问题",精度高得多(但慢,所以只用在召回后的少量候选上)。两者配合:召回保证查得全(真答案在候选里),重排保证排得准(真答案排到前面)。

【RAG 检索质量的几个抓手,按性价比排】

1. 加重排序(rerank)——本次的核心解, 召回放宽 + reranker 精排
   召回 top 30~50(宁可多捞), reranker 打分后取真正靠前的 top 3~5

2. 召回阶段就用"混合检索"(hybrid): 向量检索 + 关键词检索(BM25)融合
   向量擅长语义、关键词擅长精确术语/专有名词, 互补, 召回更全

3. 评估检索本身, 而不只看最终答案:
   对一批"问题→应命中片段"的标注集, 算召回率/命中率/MRR,
   把"检索对不对"量化出来, 别凭感觉

4. 切片策略: 片段别太大(稀释)也别太小(断章), 带上标题/上下文

5. 真要的是"答对", 不是"检索到": 检索是手段, 时刻盯着端到端效果

第三件事:其他"看着检索到了、其实没检索对"的坑

顺着"相似 ≠ 相关、召回 ≠ 精排"这条线,我把 RAG 检索里同类的坑都梳理了一遍,它们都在我"以为捞对了"的地方埋着:

第一个,只用向量、漏了精确术语。用户问里有产品型号、专有名词、错误码这类必须精确匹配的词,纯向量检索可能因为"语义相近"反而把型号相近但不对的捞上来。这正是要加关键词检索(BM25)做混合检索的原因。

第二个,切片切坏了。把一段完整答案从中间切断,问题命中了前半句、关键信息在后半句没被一起召回;或者片段切得太大,真答案被一大段无关内容稀释、整体相似度被拉低。切片粒度和重叠要调。

第三个,embedding 模型和场景不匹配。用了个通用领域的 embedding 模型去做专业领域(医疗/法律/代码)检索,它对领域内细微语义差别不敏感,"看着都相似"分不出来。该用领域适配或更强的 embedding。

第四个,没有评估、全凭手感。我最初就是没有一套"问题→该命中哪个片段"的标注评估集,检索好不好全靠抽查几个 case 的主观感觉,问题被掩盖到上线才爆发。检索这一环必须能被量化评估。

第四件事:向量召回 vs 重排序,职责到底有什么不同

我把这次踩坑的核心——召回和精排两个阶段的区别,整理成一张对照表,这是我现在设计任何检索系统都会先想清楚的:

维度 向量召回(retrieval / 粗筛) 重排序(rerank / 精排)
编码方式 问题、片段各自单独编码成向量 问题 + 片段拼一起喂进模型联合判断
判断的是 整体语义"像不像" 这段到底有没有回答这个问题
精度 较粗(相似≠相关) 高(真正的相关性)
速度 快,可对海量片段算 慢,只能对少量候选算
职责 查得:真答案别漏在范围外 排得:真答案排到最前
处理量级 全库 → top N(几十) top N → top k(几条)
缺了会怎样 答案根本没被捞出来(漏召回) 答案捞到了却被挤掉/排在后面没用上

看懂这张表就明白:它俩不是二选一,而是接力——召回负责"别漏",重排负责"选对",合起来才是一个靠谱的检索。我之前只有召回、没有重排,等于只做了"别漏"、跳过了"选对",真答案捞到了候选里却被相似度数值挤出了 top-k。

第五件事:我对"检索到 = 检索对"的几个想当然

这次事故,本质是我对"检索这件事"抱了一堆没经检验的想当然。把它们列出来,每一条都值得警惕:

我曾经的想当然 事故教我的真相
"向量相似度高就是相关,选它准没错" 相似≠相关;语义沾边但没回答问题的,相似度也可能很高
"答案在知识库里,系统就能用上它" 在库里 ≠ 被召回 ≠ 被排到前面 ≠ 被喂给模型;每步都可能掉队
"top-k 取相似度最高的几条就够了" 那只是粗筛结果;真答案常不是相似度最高的,需要重排精选
"答得不好,是大模型不够强" 常是喂进去的料就错了;检索没捞对,换再强的模型也白搭
"检索这一步最简单,不会出问题" 恰恰是最该被量化评估的一环;它的质量决定 RAG 的上限
"一个通用 embedding 走天下" 专业领域、精确术语场景,通用向量分不出细微差别,需混合/适配

第六件事:做 RAG / 任何检索系统,我现在的自检习惯

现在每当我做检索增强、或排查"明明库里有却答不对",我都会先按这张图问自己:

这张图的精髓,是"先确认答案在不在库、再分召回(查全)和重排(排准)两关、用评估集量化别凭手感"排查就顺着"在库→被召回→排到前→喂给模型"逐环看是哪环掉队、设计就召回放宽+重排精排+混合检索+评估量化这套习惯,让我从"向量一捞就直接喂、答不好怪模型"变成了"把检索拆成查全和排准两关、各自负责、量化评估"——核心始终是:向量相似≠真正相关、能召回≠排得对;向量召回只擅长粗筛保证查全、不擅长精排;须加重排序(reranker 联合看问题+片段判真相关)做精选,配合混合检索(向量+BM25)和评估集量化,才能让真正相关的片段浮上来喂给大模型。

我立下的几条规矩

这场"答案在库里却被相似片段挤掉"的事故,换来了我做检索增强系统时,刻进骨子里的几条铁律:

  1. 向量相似度高 ≠ 内容真正相关;语义沾边但没回答问题的片段,相似度也可能很高。
  2. "在知识库里"到"被大模型用上",中间隔着 被召回→排到前→喂进去 好几关,每关都可能掉队。
  3. 向量召回擅长"粗筛/查全"、不擅长"精排/选对";别拿粗筛的 top-k 当最终答案直接喂。
  4. 加重排序(reranker):召回放宽多捞候选,再用 cross-encoder 联合看问题+片段打真相关性分精选。
  5. 召回阶段用混合检索(向量+BM25),语义与精确术语互补,先保证真答案在候选里。
  6. 检索这一环必须能被量化评估(问题→应命中片段标注集,算召回率/命中率/MRR),别凭手感。
  7. RAG 答不好,先怀疑喂进去的料对不对,而不是先怪大模型不够强。

附:我现在落地"召回 + 重排"的最小检索骨架

这是我现在新起一个 RAG 项目时,检索这一环的最小骨架——刻意把"召回(查全)"和"重排(选对)"两步分开写,再挂一个简单的命中率评估,免得又退回"一捞就喂"的老路:

class Retriever:
    def __init__(self, vec_db, bm25, reranker):
        self.vec_db = vec_db          # 向量库
        self.bm25 = bm25              # 关键词检索
        self.reranker = reranker      # 重排序模型(cross-encoder)

    def retrieve(self, question, recall_n=30, final_k=5):
        # 1) 召回: 混合检索(向量 + 关键词), 故意多捞, 保证"查得全"
        vec_hits = self.vec_db.search(embed(question), top_k=recall_n)
        kw_hits = self.bm25.search(question, top_k=recall_n)
        candidates = dedup(vec_hits + kw_hits)     # 去重合并候选

        # 2) 重排: reranker 联合看 问题+片段, 打"真正相关性"分, 保证"排得准"
        scores = self.reranker.score(question, [c.text for c in candidates])
        ranked = [c for c, _ in sorted(zip(candidates, scores),
                                       key=lambda x: x[1], reverse=True)]
        return ranked[:final_k]        # 只把重排后真正靠前的喂给大模型

def eval_recall(retriever, dataset):
    """ dataset: [(question, gold_chunk_id), ...] 量化检索命中率, 别凭手感 """
    hit = 0
    for q, gold in dataset:
        got = [c.id for c in retriever.retrieve(q)]
        if gold in got:
            hit += 1
    return hit / len(dataset)          # 命中率: 真答案被排进 final_k 的比例

骨架虽小,但它把我这次踩坑的两条教训钉死在了结构里:召回这步用混合检索且故意多捞(别让真答案漏在范围外),重排这步用 reranker 联合判断(别让真答案被相似度挤掉),最后还有个 eval_recall 把"到底捞对了没"量化成一个能盯着看的数字。有了这个数字,检索好不好不再是"感觉还行",而是"命中率 92%、上周还是 78%"——能被度量,才能被改进。

后来我把那个最早暴露问题的退货政策问题又跑了一遍:召回放宽到 30 条、过一遍 reranker,那条相似度只有 0.76、原本排在第 7 位的标准答案,这次稳稳排到了第 1 位,大模型一字不差地答出了 24 小时。同一个知识库、同一个大模型,只是在中间补上了被我漏掉的那一步,结果就从答非所问变成了精准命中——这件事我记到现在。

写在最后

回头看,这场由"只召回不重排、相似当相关"引发的"答案在库里却答非所问"事故,真正教给我的,远不止"加个 reranker、用混合检索"这一个技巧。它让我对"找到一堆'看起来符合'的东西, 和真正找到那个'对的'东西, 完全是两码事; 而我们常常在'找到了一堆看着像的'这一步就停下、误以为已经找对了",有了一次刻骨的体会。我栽跟头,是因为我把'初步筛出来的、看着相似的一批',直接当成了'精挑细选后、真正对的那个'——我用一个'粗筛工具'(向量相似)得到了一批'看着都沾边'的候选, 然后跳过了'从候选里精挑出真正对的那个'这一关, 就拿着粗筛结果当答案用了;而那个真正对的, 恰恰因为'看着不是最像的', 被一堆'看着更像、其实跑题'的挤掉了这让我领悟到一个关于"筛选、相似与相关"的深刻认知:"找到一批看起来相关的"(召回/粗筛)和"从中选出真正相关的"(精排/精选)是两个不同的环节, 各需不同的手段; "看起来像/沾边/相似'是一种廉价、易得但粗糙的信号, 它能帮我们快速缩小范围、却不足以拍板;真正的"对/相关/回答了问题", 需要更精细、更联系语境的判断(像 reranker 那样把'问题'和'候选'放一起深究, 而非各看各的);如果我们止步于'粗筛出一批看着像的'、把它当成最终答案, 就会被那些'表面相似度高、实则跑题'的东西误导, 而真正对的反而被埋没这给了我一种看待"检索、筛选乃至判断"时的清醒:每当我"搜出/筛出/找出"一批东西、准备据此下结论或行动时,要分清自己是停在了"粗筛(看着相关)"还是真的做到了"精选(确认相关)"——"看着相似/沾边/匹配关键词"只是入围的门票, 不是当选的资格;越是重要的判断, 越要在'一批看着都对的'里, 用更联系语境、更深究'到底回答了问题没有'的方式, 把真正对的那个挑出来, 而不是被表面相似度的高低牵着走;"区分'召回(别漏)'与'精排(选对)'、不让粗筛冒充精选",是做对检索、也是做对任何'从候选中决断'之事的关键认清相似不等于相关、召回不等于精排、看着像只是入围而非当选——这,是我用一次 RAG 答非所问的事故,换来的、关于检索、也关于如何从一堆"看着像"里找到那个"真正对"的、最朴素也最深刻的领悟。如果这篇复盘,能让你下次发现检索系统"明明库里有却答不对"时,先别急着怪大模型,而是回头看看自己是不是"只粗筛、没精选",那我对着那个"答案就在第7条却没被用上"的 bug 折腾的大半天,就值了。

—— 别看了 · 2026
声明:本站所有文章,如无特殊说明或标注,均为本站原创发布。任何个人或组织,在未征得本站同意时,禁止复制、盗用、采集、发布本站内容到任何网站、书籍等各类媒体平台。如若本站内容侵犯了原著者的合法权益,可联系我们进行处理 邮箱1846861578@qq.com。
技术教程

我的代码在测试环境跑得好好的,一上生产就行为不对、还报了测试环境从没出过的错,折腾半天发现是两个环境的某个配置和依赖版本不一致,而这种差异散落在一堆没人管的地方的深度复盘

2026-6-3 3:30:51

技术教程

我的核心下单服务好端端的突然大面积超时崩了,排查半天发现罪魁祸首竟是一个无关紧要的猜你喜欢推荐功能——它依赖的服务挂了,而我没做熔断降级,卡住的调用把整个服务的线程池占满、连下单都瘫了的深度复盘

2026-6-3 3:40:41

0 条回复 A文章作者 M管理员
    暂无讨论,说说你的看法吧
个人中心
购物车
优惠劵
今日签到
有新私信 私信列表
搜索