数据截至 (上游 commit d89e37cdfbbe)
路由与检索:把「选哪条路」交给 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):没描述就没法判断相关性,宁可查也别漏;默认集合是「主仓库」,总值得看一眼。
5.3 底层检索契约:embed → search
选定集合后,实际检索很直接(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_queryvsembed_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)。