4.2.7 Query Engine引用溯源

4 阅读8分钟

企业知识库问答上线后,业务方问得最多的不是「模型够不够聪明」,而是:**这句话从哪份文档来的?能不能点开原文核对? **

纯 LLM 对话无法验证来源,容易产生幻觉,也无法满足合规审计。LlamaIndex 的 Query Engine 在检索 + 生成之外,会把参与回答的文档片段保存在 response.source_nodes 里——文件名、相似度分数、正文摘要都能拿出来展示,这就是 RAG 场景里的引用溯源

下面从 Query Engine 工作流程、核心参数调优,到 source_nodes 实战解析,给出完整可运行代码。


Query Engine 在 RAG 链路中的位置

索引建好之后,用户提问的入口就是 Query Engine。它负责把「自然语言问题」变成「带来源的答案」,内部大致分五步:


用户提问 → 问题向量化 → 向量检索 → Top-K 筛选 → 上下文组装 → LLM 生成回答

                                                              ↓

                                                    source_nodes 记录引用片段

index.as_query_engine() 一行即可创建;真正决定回答质量与成本的,是 ****** **similarity_top_k** ** 和 ****** **response_mode** ** 两个参数。


核心参数怎么选

similarity_top_k:一次检索几个片段

控制向量检索返回多少个最相关文档块:

| 取值 | 特点 | 适用场景 |

| --- | --- | --- |

| 1 | 最快、最省 Token,信息可能不全 | 问题明确、答案集中在单段 |

| 35 | 完整性与精度平衡 | 大多数场景的默认起点 |

| 10+ | 信息更全,但噪声和 Token 消耗上升 | 需跨多段综合的复杂问题 |

response_mode:多个片段如何合成答案

| 模式 | 机制 | 速度 | 质量 | 适用 |

| --- | --- | --- | --- | --- |

| compact | 压缩上下文,一次调用 LLM | 最快 | 良好 | 默认推荐 |

| refine | 首段生成初稿,后续段逐段 refine | 慢 | 可能更好 | 对质量要求高、片段间有递进关系 |

| tree_summarize | 分层递归摘要 | 中等 | 中等 | 片段多、上下文长 |

推荐起步配置:


query_engine = index.as_query_engine(

    similarity_top_k=3,

    response_mode="compact",

)


引用溯源解决了什么问题

传统 LLM 问答:无法验证来源、难以追溯引用、幻觉风险高。

Query Engine 的 Response 对象在 .response(文本答案)之外,还提供:

| 字段 | 含义 |

| --- | --- |

| response.source_nodes | 本次回答参考的文档片段列表 |

| source_node.score | 片段与问题的相似度分数 |

| source_node.node.text | 片段正文 |

| source_node.node.metadata | 文件名、分类等自定义元数据 |

前端可以把 source_nodes 渲染成「参考来源」折叠面板,用户点击即可核对原文——这是企业 RAG 建立信任的关键一环。


环境准备


uv add llama-index llama-index-embeddings-siliconflow llama-index-llms-siliconflow python-dotenv

.env


SILICONFLOW_API_KEY=你的密钥

全局配置(后文示例共用):


import os

from dotenv import load_dotenv

  


from llama_index.core import Settings

from llama_index.embeddings.siliconflow import SiliconFlowEmbedding

from llama_index.llms.siliconflow import SiliconFlow

  


load_dotenv()

  


Settings.llm = SiliconFlow(

    model="deepseek-ai/DeepSeek-V3.2",

    api_key=os.getenv("SILICONFLOW_API_KEY"),

)

Settings.embed_model = SiliconFlowEmbedding(

    model="BAAI/bge-m3",

    api_key=os.getenv("SILICONFLOW_API_KEY"),

)


创建 Query Engine 并提问

最小示例:从文档建索引,创建 Query Engine,执行一次查询。


from llama_index.core import Document, VectorStoreIndex

  


documents = [

    Document(text="LlamaIndex 的数据连接器可以从本地文件、数据库、API 和云存储加载数据"),

    Document(text="VectorStoreIndex 会把文档转换成向量,方便后续做语义检索"),

    Document(text="Query Engine 会先检索相关文档片段,再调用大模型生成回答"),

    Document(text="source_nodes 保存了本次回答参考过的文档片段,便于引用溯源"),

]

  


index = VectorStoreIndex.from_documents(documents)

query_engine = index.as_query_engine()

  


response = query_engine.query("Query Engine 的作用是什么?")

print(response)

典型输出:


Query Engine 会先进行相关文档片段的检索,然后调用大模型来生成回答。


对比实验:similarity_top_k 对回答的影响

用 5 条能力描述文档,分别设 top_k = 1 / 3 / 5,观察回答完整度与引用片段数:


from llama_index.core import Document, VectorStoreIndex

  


documents = [

    Document(text="LlamaIndex 的数据连接器负责从各种数据源加载数据"),

    Document(text="LlamaIndex 的索引负责把文档组织成适合检索的结构"),

    Document(text="LlamaIndex 的查询引擎负责检索文档并生成回答"),

    Document(text="LlamaIndex 支持引用溯源,可以查看回答来自哪些文档片段"),

    Document(text="similarity_top_k 控制一次查询检索多少个相关片段"),

]

  


index = VectorStoreIndex.from_documents(documents)

question = "LlamaIndex 有哪些核心能力?"

  


for top_k in [1, 3, 5]:

    query_engine = index.as_query_engine(similarity_top_k=top_k)

    response = query_engine.query(question)

    print(f"\n--- top_k={top_k} ---")

    print(f"回答: {response}")

    print(f"引用片段数: {len(response.source_nodes)}")

实测规律(文档块与问题一一对应时):

| top_k | 引用片段数 | 回答特点 |

| --- | --- | --- |

| 1 | 1 | 只提到索引/组织文档,信息明显不全 |

| 3 | 3 | 覆盖索引、查询引擎、数据连接器 |

| 5 | 5 | 最完整,额外包含引用溯源、参数控制 |

结论:文档块粒度细、问题覆盖面广时,top_k=1 容易漏信息;top_k=3 是多数场景的合理默认。


对比实验:response_mode 的差异

固定 similarity_top_k=4,遍历三种合成模式:


from llama_index.core import Document, VectorStoreIndex

  


documents = [

    Document(text="compact 模式会尽量合并检索到的内容,再一次性生成回答,速度较快"),

    Document(text="refine 模式会先基于第一个片段生成初始回答,再逐段 refine 修正"),

    Document(text="tree_summarize 模式会对多个片段做分层摘要,适合上下文较多的场景"),

    Document(text="不同 response_mode 需要在速度、成本和质量之间做权衡"),

]

  


index = VectorStoreIndex.from_documents(documents)

question = "LlamaIndex 有哪些 response_mode?"

  


for mode in ["compact", "refine", "tree_summarize"]:

    query_engine = index.as_query_engine(

        similarity_top_k=4,

        response_mode=mode,

    )

    response = query_engine.query(question)

    print(f"\n--- {mode} ---")

    print(response)

三种模式的典型特征:

  • compact:一次 LLM 调用,速度最快、Token 最省,日常问答首选

  • refine:多轮 LLM 调用,逐段修正,质量可能更高,延迟和成本随之上升

  • tree_summarize:分层摘要,适合片段多、总上下文长的场景


引用溯源:读取 source_nodes

溯源的关键是在 Document 上写入 metadata,查询后遍历 source_nodes 展示来源。


from llama_index.core import Document, VectorStoreIndex

  


documents = [

    Document(

        text="VectorStoreIndex 是最常用的向量索引,可通过 Embedding 实现语义检索",

        metadata={"file_name": "vector_index.txt", "category": "索引"},

    ),

    Document(

        text="Query Engine 负责接收用户问题,检索相关文档并调用 LLM 生成回答",

        metadata={"file_name": "query_engine.txt", "category": "查询"},

    ),

    Document(

        text="引用溯源可以展示回答参考了哪些文档片段,帮助判断回答是否可靠",

        metadata={"file_name": "source_tracking.txt", "category": "引用溯源"},

    ),

]

  


index = VectorStoreIndex.from_documents(documents)

query_engine = index.as_query_engine(similarity_top_k=3)

  


response = query_engine.query("Query Engine 如何让回答可追踪?")

print(f"回答: {response}\n")

print("引用来源:")

  


for i, source_node in enumerate(response.source_nodes, 1):

    node = source_node.node

    file_name = node.metadata.get("file_name", "未知")

    category = node.metadata.get("category", "未知")

    score = source_node.score

    snippet = node.text[:80]

    print(f"\n[{i}] 文件: {file_name} | 分类: {category} | 相关度: {score:.4f}")

    print(f"    内容: {snippet}...")

示例输出:


回答: Query Engine 通过引用溯源功能展示回答所依据的文档片段,使答案可验证、可追踪。

  


引用来源:

  


[1] 文件: query_engine.txt | 分类: 查询 | 相关度: 0.5649

    内容: Query Engine 负责接收用户问题,检索相关文档并调用 LLM 生成回答...

  


[2] 文件: source_tracking.txt | 分类: 引用溯源 | 相关度: 0.5298

    内容: 引用溯源可以展示回答参考了哪些文档片段,帮助判断回答是否可靠...

  


[3] 文件: vector_index.txt | 分类: 索引 | 相关度: 0.4220

    内容: VectorStoreIndex 是最常用的向量索引,可通过 Embedding 实现语义检索...

相关度分数越高,说明该片段与用户问题越接近。前端可按分数排序,低于阈值(如 0.3)的片段可标记为「弱相关」或不展示。


参数优化思路

调参不必凭感觉,可以固定一组测试问题,从四个维度对比:

| 评估维度 | 关注点 |

| --- | --- |

| 回答完整性 | 是否覆盖问题涉及的所有要点 |

| 回答准确性 | 是否与原文一致、有无编造 |

| Token 消耗 | refine / 大 top_k 的成本 |

| 响应速度 | 生产 SLA 是否满足 |

建议实验矩阵:

  • Top-K:1、3、5、10

  • response_mode:compact、refine、tree_summarize

大多数业务场景, ****** **similarity_top_k=3** + **response_mode="compact"** ** 是性价比最高的起点;质量不够再试 refine,上下文特别长再考虑 tree_summarize


踩坑提醒

1. metadata 在建索引前就要写好

SimpleDirectoryReader 加载的文件会自动带上 file_name 等字段;手动 Document 需自己设 metadata,否则溯源只能看到正文,无法定位文件。

2. 别把 source_nodes 当「绝对真理」

低分片段仍可能被检索进来;展示给用户时建议带分数,并设阈值过滤。

3. top_k 越大不一定越好

噪声片段进入 Prompt 后,LLM 可能「综合」出错误结论;复杂问题优先调分块策略,而不是无脑加大 top_k

4. response 是对象,不是纯字符串

print(response) 只输出文本;溯源必须访问 response.source_nodes。若需要字符串,用 str(response)response.response

5. refine 模式成本翻倍

每个额外片段可能触发一次 LLM 调用,生产环境要监控 Token 用量。


小结

Query Engine 是 LlamaIndex RAG 的查询入口:as_query_engine() 创建引擎,similarity_top_k 控制检索广度,response_mode 控制合成策略。

企业场景真正拉开差距的是 引用溯源——response.source_nodes 提供片段正文、相似度分数和 metadata,让每一句回答都有据可查。开发阶段用 top_k=3 + compact 快速迭代;上线后在 UI 层把来源展示出来,RAG 才从「能答」变成「敢用」。