跳到主要内容

路由与检索:把「选哪条路」交给 LLM

DeepSearcher 有两层路由,都用同一个套路:把候选项的「自我描述」列给 LLM,让它选。本章讲清这两层,以及底层检索契约。

5.1 第一层:RAGRouter 选 agent

default_searcher 其实是个 RAGRouter(configuration.py:212)。它手里有 [DeepSearch, ChainOfRAG] 两个 agent,任务是对每个问题选一个

怎么选? 靠 agent 的「自我描述」。每个 agent 类上挂了 @describe_class("...") 装饰器(agent/base.py:7-25),它把描述字符串塞进类的 __description__ 属性:

# agent/base.py — 装饰器把描述挂到类上
def describe_class(description):
def decorator(cls):
cls.__description__ = description
return cls
return decorator

RAGRouter 自动收集这些描述(rag_router.py:46-54),编号后连同问题塞进 RAG_ROUTER_PROMPT,让 LLM 只回一个编号(_route,rag_router.py:56-77)。

解析编号的容错。 LLM 不一定老实只回数字,可能回「The best agent is [2] because...」。所以解析分两层(rag_router.py:62-71):

try:
selected_agent_index = int(self.llm.remove_think(chat_response.content)) - 1
except ValueError:
# 推理型 LLM 常回一段解释,末尾才是数字 → 退而求其次找最后一个数字
selected_agent_index = int(self.find_last_digit(...)) - 1

find_last_digit(rag_router.py:89-93)从字符串尾部倒着找第一个数字。这是个朴素但有效的兜底。

5.2 第二层:CollectionRouter 选集合

一个向量库里可以有多个 collection(比如「产品文档」「客服记录」分开存)。检索前先选「查哪些集合」,这就是 CollectionRouter(agent/collection_router.py)。每个 agent 内部都持有一个(如 deep_search.py:106-108)。

它的逻辑有三档(invoke,collection_router.py:42-98):

情况行为
0 个集合警告并返回空
恰好 1 个集合直接用它,不花 LLM token
多个集合把每个集合的 name + description 列给 LLM,让它选相关的

多集合时还有两条强制兜底(collection_router.py:87-93):

for collection_info in collection_infos:
if not collection_info.description: # 没写描述的集合
selected_collections.append(collection_info.collection_name)
if self.vector_db.default_collection == collection_info.collection_name: # 默认集合
selected_collections.append(collection_info.collection_name)

即:没描述的集合默认集合总是被纳入检索(再去重)。设计意图(inferred):没描述就没法判断相关性,宁可查也别漏;默认集合是「主仓库」,总值得看一眼。

选定集合后,实际检索很直接(deep_search._search_chunks_from_vectordb,deep_search.py:132-137):

query_vector = self.embedding_model.embed_query(query) # 问题 → 向量
retrieved_results = self.vector_db.search_data(
collection=collection, vector=query_vector, query_text=query
)

两个要点:

  • embed_query vs embed_documents 查询走 embed_query(单条),灌库走 embed_documents/embed_chunks(批量)。BaseEmbedding.embed_chunks(embedding/base.py:44-66)按 batch_size 分批并显示进度条。
  • query_text 一并传下去。 不只传向量,还传原始文本——因为有些后端支持混合检索(向量 + 稀疏/全文)。比如 Milvus 实现里 use_hybrid = self.hybrid and query_text,会用 RRFRanker() 融合向量与稀疏检索结果(vector_db/milvus.py:208-224)。RRF = Reciprocal Rank Fusion,按「各路排名的倒数」加权融合多路召回。

5.4 检索结果的统一壳:RetrievalResult

所有后端的检索结果都包成 RetrievalResult(vector_db/base.py:9-55),字段:embedding / text / reference / metadata / score

  • reference 用于在日志里告诉用户「这块来自哪个文件」(deep_search.py:162-165)。
  • metadata["wider_text"] 装句窗的更宽上下文(见 ch.05)。
  • 去重用的就是 text 字段(deduplicate_results,vector_db/base.py:58-77)。