☰
deep-searcher深度检索:多路召回与精排迭代实战
2026/9/26 21:04:01 网站建设 项目流程

1. 从“搜得到”到“搜得准”:deep-searcher 到底在解决什么麻烦

做 RAG 应用的人都有一个共同的痛:向量检索把语义相近的片段捞回来了,但捞回来的东西经常“答非所问”。你问“这份合同里违约金的计算基数是什么”,它给你返回三段都在讲“违约责任”的段落,可偏偏没有那句写着“以合同总金额为基数”的关键句。问题不在于向量模型不行,而在于单一向量召回天然缺少“精排”和“多跳推理”的能力。

zilliztech/deep-searcher这个项目,就是冲着这个缺口去的。它做的事情可以一句话概括:在向量库之上叠一层“深度检索”逻辑,让检索过程从一次性召回变成有策略的多轮搜索与筛选。你可以把它理解成给向量数据库配了一个“会追问、会排除、会交叉验证”的检索大脑,而不是一个只会做余弦相似度的哑巴索引。

它适合谁?三类人最该关注。第一类是正在做企业知识库、文档问答的工程师,手里已经有 Milvus 或 Zilliz Cloud,但召回质量卡在瓶颈上;第二类是做 Agent 应用的开发者,需要给智能体一个可靠的“查资料”工具,而不是让它瞎编;第三类是研究检索策略的技术人,想看看在向量召回之外还能怎么加杠杆。

需要先说明一点:这个项目的公开资料相对精简,很多实现细节需要结合它的定位和同类系统的通用做法来推断。下面涉及具体机制的部分,我会明确区分“项目本身的设计意图”和“基于行业常见实践的合理补充”,你照着落地时以实际代码为准。

2. 拆开看 deep-searcher 的检索链路:为什么不能只做一次向量召回

2.1 单次召回的三个硬伤

要理解 deep-searcher 的价值,得先看清朴素 RAG 的检索环节到底弱在哪。我把它归纳成三个硬伤,每一个都对应着实际项目里踩过的坑。

硬伤一:查询和文档的语义鸿沟。用户问的是“怎么退订”,文档里写的是“服务终止流程”。向量模型能把这两个映射到相近空间,但相近不等于精确。一旦文档里有大量“终止”“取消”“关闭”混在一起,召回结果就会互相污染。

硬伤二:Top-K 的 K 值是个玄学。K 设小了,关键片段可能排在第 11 位被截断;K 设大了,噪声片段涌入,反而把真正有用的信息稀释掉。我见过太多项目把 K 拍脑袋定成 5 或 10,然后抱怨“模型答不准”——其实问题出在检索端。

硬伤三:缺少查询改写和结果验证。用户的一句话查询往往信息量不足。比如“上个季度的营收”,到底指哪个季度、哪家公司、哪个口径?单次检索没有机会去澄清或扩展这些维度。

deep-searcher 的思路,就是针对这三点分别加机制:查询侧做改写与分解,召回侧做多路并行,结果侧做重排与过滤。这三步串起来,才叫“deep”。

2.2 深度检索的通用架构分层

虽然项目没有把架构图直接摊开,但按这类系统的通行设计,deep-searcher 大概率遵循这样的分层:

层级职责常见实现手段
查询理解层改写、分解、扩展用户查询LLM 改写、HyDE、子问题拆解
召回层多路并行取回候选片段向量检索 + 关键词检索 + 元数据过滤
精排层对候选做相关性重排Cross-Encoder、LLM 打分、RRF 融合
验证层判断结果是否足够回答LLM 自检、覆盖度评估、迭代触发

这个分层的核心思想是:把“检索”从一个函数调用,变成一个可以迭代的控制流。传统 RAG 里检索是线性的——查一次、拿结果、塞给模型。deep-searcher 里检索是有状态的——查完发现不够,可以换个问法再查,或者对已有结果做二次筛选。

提示:分层不是越多越好。每加一层都意味着额外的 LLM 调用和延迟。实际落地时,查询改写和精排是性价比最高的两层,验证层要看你的场景对准确率的容忍度。

2.3 和普通 RAG 检索的本质区别

很多人会问:这不就是加了个 rerank 吗?不完全是。普通 rerank 是在同一批候选里重新排序,候选集本身没变。deep-searcher 的关键差异在于候选集的动态扩展——它可能根据第一轮结果,生成新的查询去捞新的片段,把候选池做大之后再精排。

打个比方:普通 rerank 像是把一筐苹果重新按大小排好;deep-searcher 像是发现这筐苹果不够,又去别的树上摘了几筐,然后混在一起挑最好的。前者优化的是排序,后者优化的是召回的上限。这个区别在复杂查询上体现得特别明显——当答案分散在多个文档里时,单批候选根本凑不齐,必须靠多轮检索去拼。

3. 查询改写与分解:让检索从“问一句”变成“问对几句”

3.1 为什么原始查询往往不能直接拿去检索

用户输入的查询,和适合向量检索的查询,经常不是一回事。我总结了几种典型情况,你看看是不是很眼熟。

第一种是指代不明。“它的参数是多少”——这个“它”在对话历史里,但检索系统只拿到这一句,根本不知道查什么。第二种是口语化冗余。“我想问一下那个关于报销流程的事情大概是怎么弄的”——去掉废话之后核心就四个字“报销流程”。第三种是复合问题。“A 产品和 B 产品在价格和售后上有什么区别”——这其实是四个子问题缠在一起,单次检索很难同时命中。

deep-searcher 在查询侧要做的,就是把这些“不适合检索的查询”翻译成“适合检索的查询”。这一步的投入产出比极高,因为查询改写的成本只是一次 LLM 调用,但它能同时提升召回率和精排效果。

3.2 查询改写的几种落地策略

按我的实践经验,查询改写可以分几个层次来做,复杂度递增,效果也递增。

最轻量的是同义扩展。把“退订”扩展成“退订 取消订阅 终止服务”,用扩展后的词做多路检索再合并。这个用 LLM 一句话就能生成,成本极低。

中等复杂度的是 HyDE(假设文档嵌入)。让 LLM 先“假装”写一段能回答这个问题的文档,然后用这段假文档去检索。原理是:假文档的措辞和真实文档更接近,比原始查询的向量更“像”目标片段。这个方法在问答类场景里效果很稳。

最重的是子问题分解。把复合查询拆成多个独立子问题,每个子问题单独检索,最后合并结果。这需要 LLM 判断查询是否复合、怎么拆、拆完怎么合并。deep-searcher 作为“deep”定位的系统,大概率支持这种分解能力。

# 查询分解的伪代码示意,帮助理解控制流 def decompose_query(user_query, llm): prompt = f"""判断以下查询是否包含多个独立子问题。 如果是,拆分成子问题列表;如果不是,返回原查询。 查询:{user_query} 以 JSON 列表格式返回。""" sub_queries = llm.invoke(prompt) return sub_queries # 每个子问题独立检索,结果合并去重 all_candidates = [] for sq in decompose_query(user_query, llm): all_candidates.extend(vector_search(sq, top_k=10)) deduped = deduplicate(all_candidates)

3.3 改写带来的副作用与规避

查询改写不是免费的午餐,它有两个副作用必须防。

副作用一是语义漂移。LLM 改写时可能“自作主张”加入原查询没有的限定条件。比如你问“退款政策”,它改写成“7 天无理由退款政策”,凭空多了个“7 天”。如果文档里写的是“15 天”,这次改写就把正确答案排除了。规避办法是保留原始查询作为一路召回,改写结果作为补充路,最后合并。这样即使改写跑偏,原始路还能兜底。

副作用二是延迟叠加。每次改写都是一次 LLM 调用,如果再做子问题分解,可能一次查询要调三四次模型。在交互式场景里,用户等 3 秒和等 8 秒的体验天差地别。我的做法是对改写做缓存——相同或相似的查询直接命中缓存,不重复调用。另外,简单查询走轻量改写,只有检测到复合结构时才触发分解。

注意:改写策略要和你的文档特征匹配。如果你的文档本身就是短句、关键词密集(比如产品参数表),过度改写反而有害,因为原始查询的关键词已经足够精确了。

4. 多路召回与结果融合:把候选池做厚再谈精度

4.1 向量检索之外为什么还要关键词检索

纯向量检索有个被低估的短板:对精确匹配不敏感。用户查一个产品型号“XR-2000”,向量模型可能把“XR-2100”“XR-200”都召回来,因为它们在语义空间里太近了。但用户要的就是精确的那个型号。

这时候关键词检索(BM25 或稀疏向量)就补上了。它对精确 token 的匹配是硬性的,型号、编号、专有名词这类查询,关键词路往往比向量路更准。deep-searcher 作为深度检索系统,多路召回几乎是标配——向量路负责语义泛化,关键词路负责精确命中,两条路的结果融合后,覆盖面比单路宽得多。

4.2 RRF 融合:一个不需要调参的合并方案

多路召回之后,怎么把不同来源的结果合并成一个排序?最常用的是RRF(Reciprocal Rank Fusion,倒数排名融合)。它的公式很简单:

RRF_score(d) = Σ 1 / (k + rank_i(d))

其中rank_i(d)是文档 d 在第 i 路召回里的排名,k 是平滑常数,通常取 60。这个方案的好处是不需要知道各路分数的绝对量级,只看排名。向量相似度是 0 到 1 的小数,BM25 分数可能是几十上百,直接加权平均根本没法比。RRF 绕开了这个问题。

融合方案优点缺点适用场景
加权分数平均简单直观需归一化,量级敏感同类型召回源
RRF无需调参,鲁棒丢失分数细节异构召回源
Cross-Encoder 重排精度最高慢,成本高候选少、精度要求高

我的经验是:先用 RRF 做粗融合,把候选压到 20 到 30 条,再用 Cross-Encoder 或 LLM 做精排。这样既保证了覆盖面,又控制了精排的成本。

4.3 元数据过滤:被很多人忽略的召回加速器

多路召回之外,还有一个几乎零成本但效果显著的手段:元数据过滤。如果你的文档带有时间、来源、类型、权限等结构化字段,检索时先按这些字段过滤,能把候选集缩小一个数量级。

举个实际例子。一个法律知识库,用户问“2023 年之后的劳动合同法解释”。如果不做元数据过滤,向量检索会在所有年份的文档里捞,然后靠语义去区分年份——这恰恰是向量模型的弱项。但如果文档入库时就把“年份”存成元数据,检索时加一个year >= 2023的过滤条件,候选集瞬间从十万级降到千级,召回精度和速度双提升。

deep-searcher 这类系统通常会在召回层支持这种过滤,因为它和“深度”并不冲突——深度检索不等于无脑扩大搜索范围,而是在正确的范围内做聪明的搜索。

5. 精排与迭代验证:决定“搜够了没有”的关键一环

5.1 精排模型怎么选:Cross-Encoder 还是 LLM 打分

候选池做厚之后,就要做精排了。这里有个选型问题:用专门的 Cross-Encoder 重排模型,还是直接用 LLM 打分?

Cross-Encoder 的优点是快且专。它把 query 和 document 拼在一起过一遍模型,输出相关性分数,一次能处理几十条,延迟可控。缺点是它只判断“相关不相关”,不理解“能不能回答问题”。

LLM 打分的优点是理解力强。你可以让 LLM 判断“这段内容是否足以回答用户问题”,这是 Cross-Encoder 做不到的。缺点是慢、贵,候选多了扛不住。

我的实践方案是两级精排:先用 Cross-Encoder 把 30 条压到 8 条,再用 LLM 对这 8 条做“可回答性”判断和排序。这样兼顾了效率和精度。deep-searcher 作为深度检索系统,很可能也是类似的两级思路。

5.2 迭代检索:什么时候该“再搜一轮”

深度检索和普通检索最大的区别,就是它允许迭代。第一轮检索完,系统要判断:现有结果够不够回答问题?不够的话,是换个查询再搜,还是扩大范围再搜?

这个判断逻辑通常交给 LLM:

def should_iterate(query, retrieved_docs, llm): prompt = f"""用户问题:{query} 已检索到的内容: {format_docs(retrieved_docs)} 判断:这些内容是否足以完整回答问题? 如果不足,指出还缺什么信息,并生成一个补充查询。 以 JSON 返回:{{"sufficient": bool, "missing": str, "next_query": str}}""" return llm.invoke(prompt)

这个机制的价值在于避免“检索不足就硬答”。很多 RAG 系统明明没搜到关键信息,还是硬着头皮让 LLM 生成,结果就是幻觉。迭代检索给了系统一个“承认没搜到,再搜一次”的机会。

5.3 迭代的终止条件与成本控制

迭代不能无限进行,必须设终止条件。我一般设三个:

  • 轮次上限:最多迭代 2 到 3 轮,再多延迟受不了。
  • 信息增益阈值:如果新一轮检索没有带来新的有效片段,就停。
  • 置信度判断:LLM 判断现有信息已足够,就停。

成本控制上,迭代检索的 LLM 调用次数是普通检索的好几倍。我的建议是只在必要时开启迭代——简单查询走单轮,只有检测到查询复杂或首轮结果置信度低时,才触发迭代。这样大部分请求还是快的,只有少数难查询付出额外成本。

提示:迭代检索的日志一定要打全。记录每轮的查询、召回数量、LLM 判断结果,这样出问题时能复盘是哪一轮跑偏了。我踩过的坑就是没打日志,结果线上效果波动时完全不知道从哪查起。

6. 落地 deep-searcher 时的工程细节与踩坑记录

6.1 向量库选型与索引参数

deep-searcher 出自 Zilliz,和 Milvus 生态天然亲近。如果你已经在用 Milvus 或 Zilliz Cloud,接入成本最低。索引类型上,我一般用HNSW,因为它在召回率和延迟之间平衡得最好。关键参数两个:M控制图的连接度,efConstruction控制建索引时的搜索范围。

参数常用值调大影响调小影响
M16-32召回率高,内存占用大召回率降,内存省
efConstruction200-400索引质量高,建索引慢建索引快,质量降
ef (查询时)64-128召回率高,查询慢查询快,召回降

我的经验是:数据量在百万级以下,M 取 16、efConstruction 取 200 足够。别一上来就拉满,先跑通再调优。另外,ef是查询时参数,可以动态调——对精度要求高的查询临时调大,普通查询用默认值。

6.2 分块策略对深度检索的影响

分块(chunking)是 RAG 里最容易被低估的环节。分块大小直接决定了检索的粒度。块太大,一个块里混了好几个主题,向量被平均掉,检索不准;块太小,上下文断裂,LLM 拿到片段也拼不出完整答案。

我的实践是分层分块:先按文档结构(标题、段落)做粗分,再对超长段落做细分,同时保留父子关系。检索时命中子块,返回时带上父块作为上下文。这样既保证了检索粒度,又保证了生成时的上下文完整。

deep-searcher 做深度检索时,分块策略尤其重要。因为多轮检索会命中多个块,如果块之间没有关联信息,合并时就会丢失逻辑。带上父块 ID 或章节路径,能让合并后的上下文更连贯。

6.3 我踩过的三个真实坑

坑一:改写查询把关键实体改没了。早期我用 LLM 做查询改写,没保留原始查询,结果一次线上事故:用户查一个内部项目代号,LLM 觉得这个代号“不规范”,改写成了通用描述,导致检索完全跑偏。后来改成原始查询必留一路,再没出过这个问题。

坑二:精排模型和向量模型不匹配。向量模型用的是多语言模型,精排却用了个纯英文 Cross-Encoder,结果中文查询的精排分数全是乱的。教训是召回和精排的模型要在同一语言空间里,别混搭。

坑三:迭代检索陷入死循环。有一次 LLM 判断“信息不足”,生成的补充查询和原查询几乎一样,第二轮检索结果没变化,又判断不足,差点无限循环。后来加了查询相似度检测,如果新查询和历史的太像,直接终止迭代。

6.4 效果评估:别只看召回率

评估深度检索系统,光看召回率是不够的。召回率只衡量“该捞的捞到没有”,不衡量“捞到的有没有用”。我建议至少看三个指标:

  • 召回率:标准答案所在片段是否在候选集里。
  • 精排后命中率:标准答案是否排在 Top-3。
  • 端到端答案准确率:最终生成的答案是否正确。

这三个指标是递进关系。召回率高但精排差,说明融合或重排有问题;精排好但端到端差,说明生成环节或上下文组织有问题。分开看才能定位瓶颈。

7. 这套深度检索思路还能往哪延伸

deep-searcher 代表的方向——把检索从单次函数调用升级为有策略的控制流——其实可以延伸到很多场景。

比如多模态检索。现在很多知识库里有图有表,纯文本向量检索覆盖不了。把图片描述、表格结构也做嵌入,纳入多路召回,是自然的延伸。

再比如带记忆的检索。同一个用户连续问几个相关问题,系统应该记住上一轮检索到了什么,避免重复劳动,也能做增量补充。这需要给检索加一个会话级的状态管理。

还有检索结果的可解释性。深度检索因为有多轮和精排,其实天然带有更多中间信息——哪一路召回的、精排分数多少、哪一轮补进来的。把这些暴露出来,用户能知道“为什么给我这个答案”,信任度会高很多。

我在实际项目里的体会是:深度检索的收益在复杂查询上最明显,在简单查询上反而是负担。所以别一刀切,做个查询复杂度判断,简单的走快路,复杂的走深路。这个分流逻辑本身,可能比任何单点优化都值钱。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询