你大概是在搭 RAG 的时候第一次听到「向量检索」这个词:资料明明放进库了,模型回答还是答非所问,于是有人告诉你「检索那步没调好」。这篇不堆数学,从一条查询从文字变成向量、再被拿去比对、最后捞回几段文本这条线讲下来,告诉你每一步在干什么、参数该怎么设、结果不对时先动哪个旋钮。
你大概是在搭 RAG 的时候第一次听到「向量检索」这个词:资料明明已经放进库里了,模型回答还是答非所问,然后有人告诉你「检索那一步没调好」。这时候你打开代码一看,多出来一堆没见过的名词——嵌入、距离、top_k、重排,每个都像懂又都不太懂。
这篇不堆数学公式,就沿着一条查询从文字变成一串数字、再被拿去比对、最后捞回几段文本这条线讲下来。每一步在干什么、请求该带什么参数、结果不对时先拧哪个旋钮,都按实际操作顺序说清楚。你看完应该能自己搭一条能跑通的检索链路。
一句话说清:它不是找字,是找意思
关键词检索比的是字面重合度,你搜「笔记本进水了」,它只认这几个字出现过没有。向量检索比的是两段文本在语义空间里离得多近,所以「笔记本进水了」有概率召回「电脑被咖啡泼了」这一段。这个「近」是模型训练出来的,不是人工写规则定出来的,这也是它和传统搜索最本质的区别。
- 擅长:模糊提问、同义改写、跨语言表达,用户不会用你的术语时特别有用
- 不擅长:精确匹配订单号、型号、报错码、人名,这类字符对齐的需求它经常翻车
- 判断标准:如果你的用户提问方式是「上次那个报错怎么解决」,向量合适;如果是「查订单 A12345」,关键词更稳
- 什么时候不适用:资料库里全是编号、代码、专有名词,且用户也照着念的时候,纯向量检索往往不如关键词,甚至不如两者混用
一次检索实际发生的五步
很多人把向量检索想成一个黑盒,其实拆开就是入库和查询两条路径,一边建库一边查库。下面这五步走完,一条问答链路就算通了。建议你在读代码的时候对着这五步逐个确认,缺哪步补哪步。
- 入库切片把原始文档切成片段,每片单独送去嵌入模型。验证方法是随机抽几片出来看,是不是完整的一句话或一段话,而不是半截断句。
- 生成向量每片文本过一次嵌入模型,得到一串数字,连同原文和元数据一起写进向量库。验证时看库里存的是不是既有向量也有原文,只存向量后面没法排错。
- 查询嵌入用户问题送进同一个嵌入模型,得到查询向量。关键点:入库和查询必须是同一个模型,换一个等于整库重建。
- 召回打分向量库按距离算出最相近的一批候选,这一步只保证「相关片段在里面」,不保证「排在最前面」。看返回的原文是否包含正确答案。
- 重排截断用重排模型或规则把候选再过一遍,取前几条拼进提示词交给对话模型。验证方法是看最终喂给模型的上下文里,有没有那条正确片段。
落地前要准备的四样东西
动手之前先把这四样凑齐,不然写到一半发现缺东西,返工成本比一开始多想十分钟高得多。这四样没有哪样是可以「先随便凑合」的,尤其是嵌入模型和切片策略,后面想换基本等于重来。
| 要素 | 选的时候看什么 | 中途换的代价 |
|---|---|---|
| 嵌入模型 | 中文场景重点看它对中文和长句的表现,短句和长段落分开测 | 等于整库重新嵌入,之前所有向量作废 |
| 向量库 | 小规模先用轻量方案,数据量大或需要元数据过滤再上专门的库 | 迁移数据加改接口,工作量中等 |
| 切片策略 | 先按段落切,观察召回质量再决定要不要重叠和调整片长 | 可以逐步调,但调一次要重新入库 |
| 元数据字段 | 来源、时间、权限、分类,入库时就得带上,后面补很麻烦 | 过滤和排错全靠它,越晚补越难 |
向量库的选型不用凭感觉,可以到 /stacks/vector-dbs 按你的数据规模和过滤需求对照一遍。中文嵌入模型的对比和现状,站内也有对应的资料库页面可以查当时的评测情况。
一次检索请求长什么样
抛开各家 SDK 的写法差异,一次检索请求的核心就三件东西:查询侧给什么、要返回多少条、要不要加过滤条件。把这个骨架记住,换任何库都是套同一个结构。
- 查询输入
- 可以直接给查询向量,也可以给原始查询文本让库内部帮你转向量,后者省事但要注意用的模型是不是和你入库时一致
- 返回条数
- 一次要捞回来多少候选,不要一上来就设很小,先多召回再截断是常规做法
- 过滤条件
- 按元数据字段缩小范围,比如限定某个目录、某段时间、某类权限,比单纯调相似度阈值有效得多
- 返回内容
- 通常同时给你向量、原始文本和距离分数,排错时优先看分数分布和原文,不要只看条数
接口里哪些字段能省、哪些必填,各家的定义有细微差别,可以对着 /ai-dict 里的术语和接口说明逐个核一遍,避免照着示例代码抄却漏掉关键参数。请求写完之后,建议先把返回的原文打出来肉眼过一遍,这一步能提前发现大部分问题。
召回不准,按这个顺序调
召回不准是最常见的问题,但大多数人第一反应是去调对话模型的提示词,这是方向错了。检索的问题基本都出在检索这一环,按下面这个顺序排查,每次只动一个变量,才不会越调越乱。
- 先看返回的原文对不对:如果压根没召回相关片段,问题在切分或嵌入模型,跟对话模型没关系
- 再调返回条数和相似度阈值:宁可多召回几条让重排去筛,也别在召回阶段就把门槛卡死
- 然后加元数据过滤:用来源、分类这类字段排除明显不相关的部分,往往比调阈值更立竿见影
- 最后再考虑混合检索:向量和关键词一起召回再合并排序,专有名词类的问题提升很明显
- 全程留一组固定问题做对照,改之前跑一遍、改之后跑一遍,不然改完你也不知道是哪一步起的作用
什么时候不适用这套顺序:如果你的查询本身就写错了,比如用户问题里带了明显的错别字或缩写,那再怎么调都是白费力气,得先做查询改写或者纠错这一步。这是前置问题,不属于检索参数能解决的范围。
它贵在哪、慢在哪
谈功能之前先把成本结构搞清楚,不然上线之后账单会让你措手不及。向量检索的成本有两块,一块是一次性的,一块是持续的,两块都得算进去。
| 成本来源 | 发生时机 | 怎么控制 |
|---|---|---|
| 切片嵌入 | 入库时给所有切片做一遍,一次性 | 切片别切太碎,入库条数直接决定这一块的量 |
| 查询嵌入 | 每次用户提问都要做一次,持续产生 | 控制查询频率,短查询可以适当做缓存 |
| 向量库检索 | 每次请求都要扫一遍候选 | 过滤条件用起来,缩小扫描范围 |
| 重排与拼装 | 召回之后过重排模型,再塞进上下文 | 召回条数乘每片长度,很容易把上下文吃满 |
延迟的大头通常在向量库检索和后面的重排,不在嵌入本身这一步。具体量级别猜,拿你自己的数据到 /tools/speed-test 量一条基线,心里才有数。另外切片越碎,入库条数越多,检索时的比对量也越大;切片越大,检索精度又下降。这是取舍,不存在一个对所有场景都最优的答案。
和关键词检索、全文索引怎么配合
很多人一上来就想用向量替换掉原来的关键词检索,这个思路本身就有问题。两者管的不是同一类需求:向量管语义,关键词管精确,成熟的做法是并行召回再融合排序,而不是二选一。
- 纯向量在短查询、口语化提问上更稳,用户不会用你的术语时优势明显
- 纯关键词在编号、人名、代码符号上更稳,字符对齐的需求它不会出错
- 判断该用哪种:拿二十条真实用户问题跑一遍,看你两种方案各自能把正确片段召回到前几名,用数据决定,别凭感觉
- 两种同时上时,注意合并排序的规则,别简单拼在一起导致一方永远压着另一方
什么情况下不适用混合检索:如果你的资料库很单一、用户提问方式也很固定,纯一种方案可能就够了,加混合反而增加维护成本。先看你的实际情况,不要因为「别人都这么做」就照搬。
上线前的一份自检清单
功能跑通不等于可以上线,下面这几条对着过一遍,能省掉后期很多排查时间。建议把这份清单存下来,每次改完检索参数都重新走一遍。
- 查询和入库用的是同一个嵌入模型,版本和配置一致,没有中途换过
- 有至少二十条真实问题组成的对照集,改参数前后都跑一遍,看召回结果的变化
- 召回结果里能看到原文和分数,线上出问题能定位到具体是召回错了还是排序错了
- 切片带上了来源等元数据,答题时能把出处带给用户,方便用户自己核对
- 对「查不到」这个情况有兜底话术,而不是让模型硬编一个看起来合理的答案
这五条里最容易漏的是最后一条。检索没召回相关片段时,模型如果不会说「我不知道」,就会编一个答案出来,而这种错误最难被发现。上线前一定要把兜底逻辑写好。
下一步把你现在用的向量库和嵌入模型,对照站内资料库看一遍当时的选型情况,再拿一条真实查询到测速工具量个基线,你对自己这套检索链路的瓶颈就有判断了。→常见问题
向量检索和全文检索到底有什么区别?
全文检索比的是字面是否出现,适合编号、报错码这类精确内容;向量检索比的是意思是否接近,适合口语提问和同义改写。多数项目最后是两者一起用,再把两路结果合并排序,具体取舍可以对着站内对比文章看。
一定要用向量数据库吗?小项目能不能先用文件存?
数据量小、查询不频繁时,用本地文件或轻量库把向量存起来、在内存里算相似度完全够用。等你遇到数据量上万、需要按条件过滤、或者要求毫秒级响应时再换专门的库,先跑通流程比先选型重要。
为什么我明明把资料放进去了,模型还是答错?
先确认检索那一步有没有把正确片段捞回来:把返回的原文打印出来看一眼。如果没捞到,问题在切片和嵌入模型;如果捞到了但答错,问题在提示词怎么拼、上下文放了什么,跟向量检索本身没关系。
换了嵌入模型,为什么整个库要重建?
不同模型输出的向量不在同一套坐标里,查询用一种、库里存另一种,算出来的距离没有任何意义。所以换模型的代价是把所有切片重新嵌入一遍,这也是为什么一开始就要把模型选稳。
召回条数设多少合适?
没有固定值,取决于切片长度和上下文预算:切片越长、模型上下文越小,能塞进去的条数就越少。常规做法是先多召回一批,再用重排截断到少数几条,然后用你自己的对照集测出那个刚好够用的数。