企业知识库问答上线后,业务方问得最多的不是「模型够不够聪明」,而是:**这句话从哪份文档来的?能不能点开原文核对? **
纯 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,信息可能不全 | 问题明确、答案集中在单段 |
| 3~5 | 完整性与精度平衡 | 大多数场景的默认起点 |
| 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 才从「能答」变成「敢用」。