上下文组装:从检索结果到带引用的回答
本章接上一章的检索结果,讲清楚
_build_query_context的四阶段,以及「怎么从实体回溯到支撑它的原文块」这个关键动作。
1. 这一步要解决什么
上一章拿到了一堆原始检索结果:实体、关系、(mix 模式下的)原文块。但不能直接全塞给 LLM,因为:
- 量太大,会超模型上下文窗口 → 要按 token 预算截断。
- 实体和关系本身是「摘要」,LLM 生成回答还需要原文块作为证据 → 要从实体/关系回溯到支撑它们的块。
- 回答要可溯源 → 每个块要带
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_tokens、max_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):
- 统计每个块在多少个实体里出现(
chunk_occurrence_count)。 - 同一个块只保留在最早出现的实体那里(去重,
lightrag/operate.py:5318-5321)。 - 每个实体内部,按「块的出现频次」从高到低排(被越多实体引用的块越重要,
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)。上下文分四块:
- Knowledge Graph Data (Entity) —— 实体 JSON。
- Knowledge Graph Data (Relationship) —— 关系 JSON。
- Document Chunks —— 每个块带一个
reference_id,可选content_headings(标题路径)。 - Reference Document List ——
reference_id→ 文档,供生成引用。
然后 kg_query 用 rag_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=True | content = 上下文字符串 |
only_need_prompt=True | content = 完整 prompt |
stream=True | response_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。