跳到主要内容

双层检索:把问题拆成两层关键词,分走图的点和边

本章是 LightRAG 的灵魂。讲清楚查询时怎么把一个问题拆成「高层词 + 低层词」,这两层分别检索什么,以及四种模式的分叉。

1. 这一步要解决什么

纯向量检索把整个问题压成一个向量,去和块向量比相似度。LightRAG 认为一个问题其实有两个层次的检索意图:

  • 低层(low-level): 具体的实体、专有名词、技术术语(「找到关于 X 实体 的局部信息」)。
  • 高层(high-level): 抽象的主题、概念、关系类型(「找到关于 某种关系/主题 的全局信息」)。

这两层在图谱里对应不同的东西:低层词适合去匹配图的点(实体),高层词适合去匹配图的边(关系)。把它们分开检索再融合,就是 dual-level retrieval

2. 主线:kg_query 五步走

知识图谱查询的总编排在 kg_query(lightrag/operate.py:3786):

kg_query(query, ...)
1. get_keywords_from_query → 拆出 hl_keywords / ll_keywords
2. (空关键词的兜底处理)
3. _build_query_context → 检索 + 融合 + 截断 + 组上下文
4. 组装 system prompt (rag_response 模板)
5. 调 LLM 生成回答(带缓存)

本章讲第 1、2 步(拆词 + 检索);第 3 步里「融合/截断/找块/组 prompt」放到下一章 03-context-assembly.md

3. 第一步:LLM 拆关键词

3.1 思路

关键词不是用规则切的,而是再调一次 LLM 把问题拆成两个数组。提示词 keywords_extraction(lightrag/prompt.py:484)要求 LLM 返回严格的 JSON:

{"high_level_keywords": ["..."], "low_level_keywords": ["..."]}
  • high_level_keywords:总体概念/主题/问题类型。
  • low_level_keywords:具体实体、专有名词、技术术语、产品名。

对「hello」「asdfghjkl」这种无意义输入,要求返回两个空数组(lightrag/prompt.py:500-501)。

3.2 入口与缓存

get_keywords_from_query(lightrag/operate.py:4001):若调用方已在 QueryParam 里手填了 hl_keywords/ll_keywords,直接用、不调 LLM(lightrag/operate.py:4023-4024);否则调 extract_keywords_only(lightrag/operate.py:4156)让 LLM 抽。解析 LLM 返回 JSON 的容错逻辑在 _parse_keywords_payload(lightrag/operate.py:4095),会剥掉可能的 ```json 代码围栏(_strip_markdown_code_fence,lightrag/operate.py:4082)。

3.3 空关键词兜底

拆词可能两边都空。kg_query 的兜底(lightrag/operate.py:3849-3854):若 hl 和 ll 都空,且问题短(<50 字符),就把原始问题当成低层词强行检索;否则直接返回 fail_response

4. 第二步:分层检索(模式分叉)

核心在 _perform_kg_search(lightrag/operate.py:4315)。它先把要用到的文本一次性批量算嵌入(省 2-3 次 API 往返,lightrag/operate.py:4344-4397),再按模式分叉。

4.1 三条检索通道

通道用哪层词查哪个向量库落到图谱的函数
低层ll_keywordsentities_vdb点(实体)_get_node_data(:5144)
高层hl_keywordsrelationships_vdb边(关系)_get_edge_data(:5419)
向量query 原句chunks_vdb原文块(仅 mix)_get_vector_context(:4258)

关键直觉:低层词查的是「实体向量库」,高层词查的是「关系向量库」。 实体向量库里存的是实体名/描述的嵌入;关系向量库里存的是关系描述的嵌入。所以「具体实体词」自然命中点、「抽象主题词」自然命中边。

4.2 模式怎么分叉

模式分叉的真实代码(lightrag/operate.py:4399-4454):

# 示意,贴近 _perform_kg_search 的真实分叉
if mode == "local" and ll_keywords:
local_entities, local_relations = await _get_node_data(...) # 只查点
elif mode == "global" and hl_keywords:
global_relations, global_entities = await _get_edge_data(...) # 只查边
else: # hybrid / mix
if ll_keywords: local_entities, local_relations = await _get_node_data(...)
if hl_keywords: global_relations, global_entities = await _get_edge_data(...)
if mode == "mix" and chunks_vdb:
vector_chunks = await _get_vector_context(...) # 额外加一路原文块

注意两个细节:

  • local 模式查点时,_get_node_data 也会顺带把这些点相连的边带出来(返回 local_entities, local_relations);global 模式查边时同理带出边的两端实体。所以 local/global 都不是纯点或纯边。
  • 只有 mix 模式才加第三路「原文块向量检索」(lightrag/operate.py:4437),这让它在「图谱没覆盖到但原文里有」的问题上不至于漏。

4.3 Round-robin 融合

低层和高层各自检索出实体/关系后,要合成一个列表。LightRAG 用 round-robin(轮流取) 而非简单拼接(lightrag/operate.py:4456-4510):

实体融合: local[0], global[0], local[1], global[1], ... (按名字去重)
关系融合: local[0], global[0], local[1], global[1], ... (按 src/tgt 排序后的对去重)

为什么轮流取? 这样高层和低层的结果在最终列表里交替靠前,后续 token 预算截断时不会一边倒地砍掉某一层。关系去重的 key 是 tuple(sorted([src, tgt]))(lightrag/operate.py:4487-4491),呼应入库时「关系无向」的约定——A→B 和 B→A 视为同一条。

4.4 _perform_kg_search 的产出

返回一个 dict(lightrag/operate.py:4516-4522):final_entitiesfinal_relationsvector_chunkschunk_tracking(块来源追踪)、query_embedding(复用避免重算)。这些原始结果交给下一阶段做截断和组装。

5. 默认参数(影响检索规模)

来自 lightrag/constants.py,可被环境变量覆盖(QueryParam 字段读 env,lightrag/base.py:107-128):

参数默认含义
DEFAULT_TOP_K40实体/关系向量检索各取多少条(constants.py:52)
DEFAULT_CHUNK_TOP_K20原文块初检 + 重排后保留多少(constants.py:53)
DEFAULT_MAX_TOTAL_TOKENS30000整个上下文 token 预算(constants.py:56)
DEFAULT_KG_CHUNK_PICK_METHOD"VECTOR"从实体回溯支撑块时的选块法(constants.py:59)

下一章:检索出的这堆实体/关系/块,怎么在 token 预算内融合、找支撑块、组成最终喂给 LLM 的 prompt → 03-context-assembly.md