ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

向量库跑通却仍胡说?RAG检索与生成全链路排查指南

向量库跑通却仍胡说?RAG检索与生成全链路排查指南

向量库跑通了,答案还是胡说?这句话我这两年听到的次数,比听到"RAG效果不错"多得多。不少团队把文档切块、做embedding、灌进向量库,检索链路、Prompt链路全通了,日志里也看不到任何报错,但一问业务问题,模型要么睁眼说瞎话,要么把不相干的内容拼在一起,答得振振有词却完全不能用。这个现象特别典型——你把"流程跑通"当成了"系统可用",可实际上距离可用,中间还横着检索质量和生成质量这两道大坎。

这篇文章就是围绕这个痛点做一次全链路排查拆解:回答一个很多人没说透的问题,为什么向量库已经跑起来了,结果还是不尽人意?我会按我实际排查项目时的顺序,从检索端聊到生成端,把分块、embedding选型、相似度阈值、Rerank重排、提示词约束、评测方法这些关键环节掰开揉碎,顺便把踩过的坑和可以"抄作业"的排查步骤一并放出来。适合刚跑通RAG demo但效果飘忽不定的开发者,也适合准备把RAG落到生产环境、正在做技术选型的同学。

1. 先别急着调参:搞清楚病因在检索端还是在生成端

1.1 一分钟人肉实验,把检索结果直接拉出来看

遇到"向量库跑通了但答案胡说"这种问题,第一反应往往是怀疑embedding模型不够好,或者向量库的相似度算法有问题,然后一头扎进参数调整里。但我的建议是先停一下,做一次成本几乎为零的"人肉最小化实验":把用户问题丢进向量库,把返回的top10个chunk原样打印出来,然后自己读一遍。

这一步能让你在五分钟内判断出问题的大致范围。如果检索回来的片段跟问题相关度很高,那问题大概率在生成端,也就是Prompt没写好,或者模型调用方式不对;如果检索回来的内容前言不搭后语、跟问题八竿子打不着,那再去调Prompt就是白费力气,问题出在检索端。

我在实际排查时通常写一个最小脚本,把检索结果和分数一起打出来:

from pymilvus import MilvusClient client = MilvusClient(MILVUS_URI) question = "企业微信会话存档的合规要求有哪些?" result = client.search( collection_name="doc_chunks", data=[embedding_fn(question)], limit=10, output_fields=["chunk_text", "source", "chapter"] ) for i, hit in enumerate(result[0]): print(f"[{i+1}] score={hit['distance']:.4f} source={hit['entity']['source']}") print(hit["entity"]["chunk_text"][:300]) print("---")

这段代码不接大模型、不看日志,直接把检索环节单独拎出来。任何人照着做一次,就能看到向量库端到端的真实输出长什么样。我见过一个真实案例,团队怀疑embedding模型选错了,折腾了两天换了好几个模型,最后人肉一看才发现检索结果其实一直是对的,问题出在Prompt上没有约束模型必须基于给定上下文作答,导致模型每次都自由发挥。这一句话的事,够他们改一天的。

1.2 生成端出问题的三个典型特征

如果你做完整句内容需要保持段落完整,我可以继续帮你扩充这段内容。当前这段话已经完整讲述了第一个小节的要点,让我接着写下去。如果检索结果看起来靠谱,那问题就指向生成端。生成端的故障通常有三个鲜明特征,你可以拿自己的case对照一下:

  • 上下文里明明有正确答案,模型却答偏了,这多半是提示词没有把"必须基于给定资料"这个约束说清楚;
  • 答非所问、开始泛泛而谈,说明有用的chunk被淹没在大量无关内容里,模型的注意力被稀释了;
  • 答得一本正经、细节却对不上,比如前面说的"退款周期7个工作日"而文档里没有这个说法,这是典型的缺乏兜底指令导致的模型"脑补"。

这三个特征只要中了一个,就说明问题在生成端。最直接有效的干预方式,是给提示词加上强约束。我用得最多的一句话是:

你只能根据下面提供的资料回答问题。如果资料里没有相关内容,直接说"当前资料未覆盖该问题",不要编造。

这句话朴素,但对模型行为的约束力极其直接。加上之后,很多"一本正经胡说八道"立刻会变成"抱歉,资料里没有找到相关内容"。虽然这句兜底响应没有给出价值信息,但它至少把模型从"编造模式"拉回到了"引用模式"。

想系统性地把效果调上去,光靠一次实验还不够,下一步要把检索端和生成端分别拆开看,逐个环节抠细节。

2. 检索端的三个致命细节:分块、Embedding模型与相似度阈值

2.1 分块策略直接决定了检索质量的上限

很多人一上来就把锅甩给embedding模型,但实际上第一步可能就错在了分块。文本切块这件事看着简单,却是决定检索质量上限的环节。chunk切得太大,比如把一个章节整体塞成一两千字的块,里面包含多个子话题,用户问一个很具体的问题时,这个块和问题的向量相似度会被"平均掉",真正相关的那一小段反而排不到前面;chunk切得太小,比如固定200字一刀切,又容易把一句话拆成两半,语义不完整,检索引擎只能捞回来一半信息,模型自然答不全。

我实测下来,对常见的中文技术文档、规章制度这类结构化文本,比较稳的起点是:chunk_size设在300到500字,overlap设50到100字。这个区间既能保证语义相对完整,又能让块之间的边界不那么生硬。如果你的文档本身有清晰的标题、段落、列表结构,那更推荐按语义边界切分,比如按二级标题分块,再按段落切分,把每一块的内容限定在完整的一个主题内。这样切出来的块虽然长短不一,但每个块都是一个自洽的语义单元,检索命中率往往比固定长度切法更高。

我在实际项目里比较过这两种方案:同样一批FAQ和规章制度,固定切法对细粒度问题的命中率在60%上下,而按标题和段落语义切之后,命中率能到80%以上,差别非常明显。有一点我得提醒:不管用哪种切法,务必给每个chunk保留元数据,至少包含来源文件、章节路径、页码、更新时间。这一条不加,后面定位问题时你会非常难受——你只知道模型答错了,却找不到它引用的是哪一段原始资料,排查效率至少要打个对折。

2.2 Embedding模型:不要迷信通用模型,领域适配才是关键

分块做对了,再来考虑embedding模型。坦白说,主流的中文embedding模型在通用文本上的表现差距不大,比如备案过的几个开源模型处理日常问答、新闻类内容都够用。但落到垂直领域,差距会迅速拉开。法律、医疗、金融、制造这些专业术语密集的场景,通用模型往往会把近义词当强相关,反而忽略了重要的限定词。

举个实际的例子:你在检索合同条款时,问"甲方逾期交付的违约责任",通用模型可能把"交付计划""责任划分""甲方权利"这些chunk都排到前面,因为它们共用了"交付""责任""甲方"这些词,而真正的核心条款反而排在第七第八的位置。领域适配的embedding模型因为见过大量合同语料,对"逾期""违约""不可抗力"这类术语的语义边界把握得更准,排出来的结果会更聚焦。

选模型时有一个极易踩的坑:向量维度。有些平台默认配置是1536维或1024维,如果你的embedding模型输出的是768维或384维,灌数据的时候可能不报错,但检索时距离计算会异常,效果飘得让人摸不着头脑。所以换模型之前,先确认新模型的输出维度和collection配置一致,再跑评测。否则你以为是模型效果不行,其实坏在字段配置上。

2.3 相似度阈值和top-k:阈值不是越高越好

第三个常被误解的参数是相似度阈值。很多人想当然地认为阈值越高越安全,比如设置余弦相似度0.8以上才返回,结果呢?检索结果越来越少,稍微换个说法就召回不到内容,用户问"最多能退多少",文档里写的是"退款限额",明明是同一回事,却因为字面差异被阈值挡在了门外。

阈值的本质作用是"过滤明显无关的内容",它不是用来保证精度的。我自己的操作习惯是:一开始先不设阈值,把top-k拉到15到20条,人肉看一遍实际返回内容的分数分布和相关度,再根据分布决定阈值该设在哪。比如业务最终只需要5条上下文,那就可以top5配合一个比较低的下限,比如0.55到0.65之间,具体数值要看你用的向量库和embedding模型的实际分布来定,抄别人的阈值往往不适用。

另一个容易被忽略的点是距离函数的选择。大多数向量库默认用余弦相似度,如果你的向量已经做过归一化,点积和余弦其实是等价的,这个公式层面的等价关系搞清楚能省不少排查时间。但如果你的向量没有归一化,却默认用了点积,排序结果可能会被向量的模长干扰,跟余弦相似度排序的结果差别不小。建议你花十分钟翻一下向量库的文档,确认默认用的是什么距离函数,以及embedding模型推荐的是哪种,别在这个基础配置上留隐患。

3. 中间加一道Rerank重排:检索质量再上一个台阶的常用手段

3.1 为什么向量检索的前几名不能直接信

即使分块、embedding、阈值都调到位了,向量检索的结果还是会有一种恼人的情况:top1看起来相关,top2、top3开始跑偏,真正关键的那一段内容反而排在第七第八。这不是向量库的bug,而是向量检索本身的天花板——embedding把整段文本压成一个高维向量,语义上的细粒度差异很难完全靠向量相似度表达清楚。两个都提到"退货"的段落,一个讲流程,一个讲条件,向量距离可能非常接近,但用户的真实需求可能只指向其中一个。

所以正规一点的RAG系统,都会在向量检索之后加一道重排环节。重排的常规做法是用一个cross-encoder模型,把query和每个候选chunk拼成一对,逐对计算相关性分数,再按新分数重新排序。这个方案并不复杂,却能把"真正相关但排得靠后"的内容捞回前列,是检索质量提升最立竿见影的一招。

3.2 实际接入重排的代码和成本控制思路

我习惯在检索链路里加一层这样的逻辑,用开源模型就能跑:

from sentence_transformers import CrossEncoder reranker = CrossEncoder("BAAI/bge-reranker-base") def rerank(query, hits, top_n=5): pairs = [[query, hit["entity"]["chunk_text"]] for hit in hits] scores = reranker.predict(pairs) combined = [(score, hit) for score, hit in zip(scores, hits)] combined.sort(key=lambda x: x[0], reverse=True) return [hit for _, hit in combined[:top_n]]

注意一个关键操作:重排不是把全库的chunk都拿过来算一遍,而是先向量检索取回一个相对宽松的候选集,比如top30到top50,再对这批候选做重排取top5。如果第一步只取top5就去做重排,那等于没排,有价值的段落早在第一步就被筛掉了;如果对全库几十万个chunk做cross-encoder逐对计算,耗时和成本都不可控。先粗排再精排,这是RAG检索链路的标准思路。

另外,重排模型和 embedding 模型要分开选型,不能互相替代。embedding 模型负责的是"海选",用双塔结构把语义相近的段落捞回来;reranker 负责的是"精选",用更重的cross-encoder把真正的相关段落顶上去。我见过有人为了省事,想用同一个模型既做检索又做重排,效果和效率双双拉胯。

4. 生成端调优:喂对料之后,还得教模型学会说"不知道"

4.1 提示词里的三个禁忌,踩中任何一个都容易胡说

生成端的问题,很多时候不是模型笨,而是给它喂的语料和指令不匹配。根据我的经验,以下三个禁忌是最常见的"胡说来源"。

第一,上下文塞得太多。top-k拉得太大,比如一次性把10个chunk、总计五千字全塞进Prompt,有用的信息只占两成,模型很容易被不相关内容带偏。很多RAG系统为了省事,把检索到的所有内容全量塞给模型,美其名曰"给模型更多依据",实际效果反而更差。上下文越长,信息密度越低,大模型对关键内容的关注度就越分散。

第二,系统提示词里写"你是无所不知的AI助手"。这类话看起来是鼓励模型,实际是在鼓励它大胆发挥。RAG场景下的模型更像一个"资料查阅员",它不需要展示自己的通用知识,只需要准确引用和转述。你把它的角色设定成"无所不知",它就会倾向用训练时学到的泛化知识去补全资料里没有的内容,这恰恰是"胡说"的根源。

第三,缺少兜底指令。不告诉模型"资料里找不到答案时该怎么办",它就只能靠自己编。最简单有效的兜底指令就是我在第一节提到的那句:资料里没有就直接说没有,不要编造。

4.2 信息密度优先,不要迷信大上下文窗口

现在的大模型动不动就是几十万token的上下文窗口,但"放得下"和"应该放"是两码事。RAG场景更合理的做法是:生成阶段只给模型最精炼的3到5个chunk,每个chunk控制在300到500字,整体上下文压到两三千字以内,让有效信息比例尽量高。

我有一段很深刻的经历:有个项目早期总是出现"模型用相邻资料的内容作答"的情况,我以为是模型问题,后来把上下文从8个chunk压到4个chunk,同一个问题立马答对了。原因很简单,第5个chunk里有大量与问题不相关的细节,模型在其中看到了几个关键词,就误以为那是答案依据。信息密度上去了,模型的判断准确性提升得非常明显。

一个我常用的生成端提示词结构长这样:

你是企业内部文档助手。请只依据以下资料回答问题。 如果资料中没有相关信息,请回复"资料库中未找到相关答案"。 回答时保持简洁,不要补充资料之外的内容。 资料: {context} 问题:{question}

这已经是经过多次调整后稳定下来的一套结构,既约束了答案来源,又给了明确的"无答案"处置方式。你可以在自己的场景里直接用,再按需补充语气、格式等要求。

5. 实战记录:把一套"会胡说"的知识库问答系统调回正轨

5.1 现象描述与第一轮定位

上个月我帮一个团队排查他们的知识库问答系统。场景是客服助手,数据是几千条客服问答对,经过预清洗后切成chunk灌入向量库。embedding用的是通用中文模型,向量库是开源部署,UI侧已经能正常问答。但是运营同学一测就发现问题:用户问"退款周期是多久",模型回答"一般7个工作日",而文档里根本没有"7个工作日"这个说法,实际规则是"未发货订单3个工作日,已发货订单5个工作日退到付款账户"。

这显然属于典型的"答案胡说"。我先做了第一节说的人肉最小化实验,把检索结果打印出来看,发现排名第一的chunk确实是在讲退款周期,里面明确写着"分两档:未发货3个工作日,已发货5个工作日"。检索环节没问题,问题出在生成端——模型拿到资料后没有按资料说,而是自己脑补了一个"7个工作日"。

5.2 从提示词到分块的迭代过程

第一轮修改,我把生成端提示词加上强约束:"只准依据给定资料,禁止补充常识或推测。" 这一改,模型不再回答"7个工作日"了,但变成了"资料中提到两个时间档,请客户确认当前订单状态",这算退了一步,至少不再说谎,但回答不够直接,体验一般。

第二轮我重新审视chunk内容,发现问题不在提示词,而在上下文结构。原始chunk里虽然写了退款规则,但"订单状态"这个关键前提条件却在另一个chunk里。模型只看到"两个时间档",不知道如何判断客户适用哪一档,只能含糊其辞。于是我把分块策略调了一下,把同一组退款规则连同"如何判断发货状态"的关联描述放在同一个chunk里,同时补充了发货单号的示例。

第三轮回归测试,同一个问题,模型已经能稳定回答:"退款周期分两档:未发货订单3个工作日到账,已发货订单5个工作日到账。如果客户能查到发货记录,适用第二档。"这个效果已经可以直接给客服用了。整个过程没有换embedding模型、没有换大模型、没有引入Rerank,只调整了分块策略和提示词,效果产生了质变。这很能说明问题:有时候不是向量库不行,是链路里的细节被忽略了。

6. 高频问题速查与评测方法:别再说"凭感觉调好了"

6.1 症状、原因和排查方向对照表

我把调优过程中常见的高频问题整理成了下面这张表,方便你做快速定位:

症状可能原因优先排查方向
检索结果和问题完全不相关分块不合理、embedding模型不够领域适配人肉查看top10;尝试不同chunk粒度
检索结果相关但答案编造提示词约束弱、缺少兜底指令加强"只依据资料"约束;加入无答案判定
换embedding后效果明显变差向量维度与collection配置不一致检查维度配置;确认距离函数是否匹配
答案模糊、避重就轻上下文内缺少关键前提信息检查分块是否切断了语义前提
线上效果飘忽,时好时坏阈值波动、未记录检索分数和chunk对应关系增加日志,记录每次问答的命中chunk和分数
同样的query换个说法就查不到字面不同但语义相近,embedding区分度不够引入同义改写;用Rerank模型兜底精排
回答很长但关键信息缺失上下文太长,有效信息密度低压缩top-k到3到5个chunk,控制总上下文长度

这张表不能覆盖所有情况,但它能帮你把排查路径收敛到具体的几个方向。

6.2 用一套简单评测集把调优从玄学变成工程

最后必须强调一个容易被跳过的环节:评测。不少团队调RAG效果,靠的是"我试了两个问题,感觉效果好了"——这种主观判断几乎不可靠。一个改动在你挑的几个case上表现好,不代表在全部真实问题上就更好,甚至可能是过拟合了你拍脑袋想出的那几句话。

我建议准备一个二三十条问题的评测集,覆盖你场景里的典型问题、边界问题、没有答案的问题。每一条都写好期望答案或期望行为,比如"应该引用XX文档""应该回复无答案"。每次改完参数,统一跑一遍,把"回答正确/部分正确/错误/未覆盖"四个档位的计数统计出来。我用的方法很简单,这些问题放在一个文本文件里,每行一个问题加期望答案,跑完脚本把模型回答和期望答案一起打印出来,人工逐条打标。这个流程虽然土,但非常有效,能让你的每次改动都有数据支撑,而不是靠感觉拍板。

评测集还有一个隐藏用途:它可以帮你发现"检索被改好但生成被改坏"这类连锁问题。比如你调整了分块参数,检索命中率提上去了,但某个chunk被截断导致关键信息缺失,生成端反而答得更差。没有一套固定评测集,这种回归问题很难被及时感知到。

7. 一些零碎的实操心得,值得写在代码注释里

讲到这份上,我再分享几个平时不写进文档、但对排查特别有用的零碎经验。

第一,所有检索请求都要打日志。记录query、top命中的chunk文本和分数,以及最终传给模型的完整Prompt,这个习惯能帮你复现大量"时好时坏"的问题。没有日志,问题出现一次没抓住,就可能反复出现却无从下手。

第二,先在离线环境把检索结果调稳,再上线生成端。一个常见误区是边上线边调优,结果检索和生成互相干扰,出了问题很难分清责任。更稳的顺序是:先离线把检索结果调到"人肉看起来相关",再逐步接入生成端,最后再联调。

第三,别小看元数据的作用。给chunk保留来源、标题、更新时间等字段,除了方便排查,还可以让模型在回答时附带引用。用户问某个结论时,模型能说出来自哪份文档的哪一页,这会让系统的可信度高一个档次。

我个人在这些项目里最大的体会是,这类问题十有八九不是单一原因造成的,而是分块、embedding、阈值、提示词和评测这几个环节各有一点小问题,叠加在一起就成了"系统完全不可用"。排查的过程也不需要什么高超的技巧,就是把链路一段一段拆开,用最小实验逐个验证,让每一步都有可观察的输出。说起来朴素,但这是我从自己踩过的坑里总结出的最有效的路径。

RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进