跳到主要内容

上下文组装:从检索结果到带引用的回答

本章接上一章的检索结果,讲清楚 _build_query_context 的四阶段,以及「怎么从实体回溯到支撑它的原文块」这个关键动作。

1. 这一步要解决什么

上一章拿到了一堆原始检索结果:实体、关系、(mix 模式下的)原文块。但不能直接全塞给 LLM,因为:

  1. 量太大,会超模型上下文窗口 → 要按 token 预算截断
  2. 实体和关系本身是「摘要」,LLM 生成回答还需要原文块作为证据 → 要从实体/关系回溯到支撑它们的块
  3. 回答要可溯源 → 每个块要带 reference_id,末尾生成引用列表。

2. 四阶段架构

_build_query_context(lightrag/operate.py:5024)是清晰的四阶段流水线(lightrag/operate.py:5036-5106):

_build_query_context
Stage 1 _perform_kg_search → 纯检索(上一章)
Stage 2 _apply_token_truncation → 按 token 预算截断实体/关系
Stage 3 _merge_all_chunks → 找支撑块 + 合并所有块来源
Stage 4 _build_context_str → 拼成最终上下文字符串 + raw_data

两个提前返回的护栏:

  • Stage 1 后若实体和关系都空,非 mix 模式直接返回 None;mix 模式还要看有没有向量块兜底(lightrag/operate.py:5059-5064)。
  • Stage 3 后若块、实体上下文、关系上下文全空,返回 None(lightrag/operate.py:5087-5092)。

3. Stage 2:token 预算截断

_apply_token_truncation(lightrag/operate.py:4525)把实体、关系分别按各自的 token 预算砍:max_entity_tokensmax_relation_tokens(QueryParam 字段,lightrag/base.py:115-123)。因为上一章用了 round-robin 融合,截断时两层关键词的贡献是交替保留的,不会一边倒。

截断后产出 filtered_entities / filtered_relations 以及它们的文本化形式 entities_context / relations_context,还保留 id→原始数据的映射供后面引用(entity_id_to_original 等)。

4. Stage 3:从实体/关系回溯支撑块(关键)

这是最有意思的一步。实体/关系是 LLM 摘出来的二手信息,要给生成模型提供一手原文,就得知道「这个实体/关系是从哪些原文块抽出来的」。

入库时每个实体都记了 source_id(用 <SEP> 拼接的块 ID 列表),这里就是反向用它。_find_related_text_unit_from_entities(lightrag/operate.py:5260)和 _find_related_text_unit_from_relations(lightrag/operate.py:5511)负责这件事,_merge_all_chunks(lightrag/operate.py:4731)把所有来源的块合并。

4.1 去重 + 按出现频次排序

一个块可能同时支撑多个被检索到的实体。选块逻辑(lightrag/operate.py:5309-5335):

  1. 统计每个块在多少个实体里出现(chunk_occurrence_count)。
  2. 同一个块只保留在最早出现的实体那里(去重,lightrag/operate.py:5318-5321)。
  3. 每个实体内部,按「块的出现频次」从高到低排(被越多实体引用的块越重要,lightrag/operate.py:5326-5333)。

直觉:被多个相关实体共同引用的块,更可能是回答的核心证据,优先级更高。

4.2 两种选块法

kg_chunk_pick_method(默认 "VECTOR",lightrag/constants.py:59)决定怎么从候选块里挑:

方法怎么选直觉
WEIGHT按上面的出现频次做线性梯度权重轮询「被越多实体共享的块越优先」
VECTOR按块嵌入与问题的余弦相似度选(pick_by_vector_similarity)「和问题语义最近的块优先」

VECTOR 模式下取的块数是 max_related_chunks * 实体数 / 2(lightrag/operate.py:5344);若没有嵌入函数则回退到 WEIGHT(lightrag/operate.py:5348-5350)。related_chunk_number 默认 5(DEFAULT_RELATED_CHUNK_NUMBER,lightrag/constants.py:58)。

4.3 重排(rerank)

块选出来后,若开了重排(QueryParam.enable_rerank,默认 true,lightrag/base.py:148),会用重排模型对块重新打分排序。重排实现支持多家:cohere_rerank / jina_rerank / ali_rerank / 通用 generic_rerank_api(lightrag/rerank.py:182-475)。README 说明重排自 2025.08 起成为混合查询的默认增强。

5. Stage 4:拼最终上下文 + 引用

_build_context_str(lightrag/operate.py:4839)把实体、关系、块拼成喂给 LLM 的上下文,用模板 kg_query_context(lightrag/prompt.py:442)。上下文分四块:

  1. Knowledge Graph Data (Entity) —— 实体 JSON。
  2. Knowledge Graph Data (Relationship) —— 关系 JSON。
  3. Document Chunks —— 每个块带一个 reference_id,可选 content_headings(标题路径)。
  4. Reference Document List —— reference_id → 文档,供生成引用。

然后 kg_queryrag_response 系统提示(lightrag/prompt.py:334)把上下文包进去,要求 LLM:

  • 只用上下文里的信息,不准编造(lightrag/prompt.py:355);
  • 追踪支撑每个事实的块的 reference_id,末尾生成 ### References 列表(lightrag/prompt.py:363-369),最多 5 条。

6. 产出:QueryResult

最终 kg_query 返回统一的 QueryResult(lightrag/base.py:1009),按 QueryParam 不同字段填不同内容(lightrag/operate.py:3877-3998):

QueryParam 设置QueryResult 内容
only_need_context=Truecontent = 上下文字符串
only_need_prompt=Truecontent = 完整 prompt
stream=Trueresponse_iterator = 流式迭代器,is_streaming=True
默认content = LLM 回答文本

raw_data 里始终带结构化的 references 和 metadata;QueryResult.reference_list(lightrag/base.py:1026-1037)是个便捷属性,直接抽出引用列表。

查询级 LLM 缓存的 key 由一大串参数 hash 而成(compute_args_hash,lightrag/operate.py:3911-3927):mode、query、各种 top_k/token 上限、关键词、是否重排等都进 key——任一变了就不命中旧缓存。

7. naive 模式:退化成普通向量 RAG

作为对照,naive_query(lightrag/operate.py:5740)完全不碰图谱:直接 _get_vector_context 查原文块向量库,按 max_total_tokens 预算截断,用 naive_rag_response 模板(lightrag/prompt.py:388)生成。这就是普通向量 RAG,可作为「图谱到底有没有帮助」的基线对照。

下一章:巧妙之处、边界与局限、横向对比、完整代码地图 → 04-internals.md