☰
RAG 进阶:Query Engine 与 Response Synthesizer 调优实战
2026/9/29 17:40:50 网站建设 项目流程

1. 从检索到生成:Query Engine 到底在 RAG 链路里扮演什么角色

很多人做 RAG 项目,前面检索部分调通了,向量库能返回一堆相似度分数挺高的 chunk,但一到“把检索结果喂给大模型生成答案”这一步就开始翻车:要么答非所问,要么把检索到的原文整段抄下来,要么干脆自己编。问题往往不在检索,而在检索和生成之间那个被忽视的中间层——Query Engine。

我在多个 RAG 实战项目里反复验证过一个结论:检索质量决定 RAG 的下限,Query Engine 和 Response Synthesizer 的配置决定 RAG 的上限。检索给你的是原料,Query Engine 决定怎么把这些原料组织成一顿能吃的饭,Response Synthesizer 决定这顿饭最终以什么形态端上桌。

先把概念说清楚。在 LlamaIndex 这套框架里,Query Engine 是一个面向查询的高层封装,它把“检索器(Retriever)”和“响应合成器(Response Synthesizer)”串成一条完整的流水线。你调用query_engine.query("你的问题")这一行代码背后,实际发生了这些事:查询文本被送进 Retriever,Retriever 从索引里捞出一批相关节点(Node),然后这些节点连同原始问题一起交给 Response Synthesizer,Synthesizer 根据你指定的response_mode决定如何把节点内容和大模型交互,最终产出一个 Response 对象。

这里有个关键认知:Query Engine 不是简单的“检索 + 拼接 + 调用 LLM”三步走。它内部处理了节点去重、上下文窗口管理、多轮调用编排、结构化输出解析等一系列脏活。你如果绕过 Query Engine 自己手写这套逻辑,大概率会在 token 超限、节点顺序、引用溯源这些细节上踩坑。

适合谁来参考这篇内容?如果你已经跑通过最基础的 RAG demo,能理解 embedding、向量检索、chunk 这些概念,但发现自己的 RAG 系统回答质量不稳定、想深入调优生成环节,那这篇就是写给你的。如果你还没接触过 RAG,建议先补一下检索部分的基础,再回来看 Query Engine 这一层,否则容易知其然不知其所以然。

接下来我会从整体设计思路、核心参数拆解、实操配置、问题排查四个维度,把 Query Engine 和 Response Synthesizer 这层彻底讲透。所有代码示例基于 LlamaIndex 的 Python 版本,但思路对 LangChain、Spring AI 等其他框架同样适用,因为底层逻辑是相通的。

2. Query Engine 整体设计与 Response Synthesizer 选型思路

2.1 为什么要把 Retriever 和 Synthesizer 拆开设计

刚接触 LlamaIndex 的人常有一个疑问:为什么不直接做一个retrieve_and_generate函数,非要拆成 Retriever 和 Response Synthesizer 两个组件?我一开始也觉得这是过度设计,直到在一个知识库项目里遇到需求变更——产品经理要求“同一个检索结果,既能生成简洁答案,又能生成带引用的详细报告,还能只返回原文片段”。

如果检索和生成是耦合的,这个需求就得改三处代码。但拆开之后,Retriever 保持不变,我只切换不同的 Response Synthesizer 和response_mode就搞定了。这就是拆分的核心价值:检索策略和生成策略可以独立演进。

从架构上看,Query Engine 处于 RAG 链路的“编排层”。它向下对接索引和检索器,向上暴露统一的查询接口。Response Synthesizer 则是编排层里的“执行器”,负责具体的 LLM 调用逻辑。这种分层让每一层都能单独测试、单独替换。我在实际项目里经常这样干:检索效果不好时,我只调 Retriever 的top_k和相似度阈值;生成质量不好时,我只调 Synthesizer 的response_mode和 prompt 模板。互不干扰,排查问题效率高很多。

还有一个容易被忽视的点:Query Engine 支持异步和流式输出。当你用query_engine.aquery()或者query_engine.query()配合流式回调时,Synthesizer 会在内部处理 token 级别的流式拼接。如果你自己手写这套逻辑,流式场景下的节点引用、多轮调用的中间状态管理会让你非常头疼。

2.2 response_mode 的选型逻辑与适用场景对照

response_mode是 Response Synthesizer 最核心的参数,它直接决定了“检索到的多个节点如何被送进 LLM”。很多人默认用compact就不管了,结果在节点数量多、上下文窗口紧张的场景下频繁报错。我把常用的几种模式整理成对照表,方便你按场景选型。

response_mode核心机制适用场景token 消耗调用次数
refine逐个节点迭代,后续节点在前一轮答案基础上精炼需要高精度、节点间信息互补高N 次(N 为节点数)
compact先尽可能把节点塞进一个 prompt,超限才拆分通用场景,节点数适中中1~N 次
tree_summarize把节点组织成树,自底向上逐层汇总长文档摘要、多节点归纳中高多层多次
simple_summarize把所有节点截断拼成一个 prompt节点少、快速摘要低1 次
no_text只检索不生成,返回原始节点需要自己后处理、调试检索无0 次
accumulate对每个节点独立生成答案再拼接多问题并行、节点间独立高N 次
compact_accumulatecompact 和 accumulate 的结合批量查询、需要逐节点答案高N 次

选型的核心判断依据有三个:节点数量、节点间信息是否互补、你对答案精度的要求。举个例子,如果你检索回来 8 个节点,每个节点讲的是不同子话题,那refine或tree_summarize更合适;如果 8 个节点其实是同一段内容的重复召回,那compact去重后一次调用就够了。

我个人的经验法则是:默认用compact,节点数超过 10 个且信息互补时切tree_summarize,需要逐节点溯源时用no_text自己后处理。refine虽然精度高,但 N 次 LLM 调用的延迟和成本在线上环境往往不可接受,除非你对延迟不敏感。

2.3 节点后处理:被低估的质量杠杆

在 Retriever 和 Synthesizer 之间,其实还有一个可以插入 Node Postprocessor 的位置。这是很多人忽略的优化点。检索回来的节点往往包含大量冗余、低相关度甚至矛盾的内容,直接丢给 Synthesizer 会稀释有效信息。

常用的 Node Postprocessor 有几类:相似度过滤(低于阈值的直接丢弃)、去重(相邻或高度相似的节点合并)、重排序(用 cross-encoder 重新打分)、元数据过滤(按时间、来源筛选)。我在一个企业知识库项目里加了一个基于时间戳的过滤,把过期文档节点剔除后,答案准确率肉眼可见地提升了。

这里有个实操细节:Postprocessor 的执行顺序会影响最终节点集合。一般建议先做元数据过滤(便宜且快),再做相似度过滤,最后做重排序(贵但准)。如果你把重排序放在最前面,等于对一堆马上要被过滤掉的节点做了无用功。

3. Response Synthesizer 核心细节与参数拆解

3.1 文本 QA 模板与自定义 Prompt 的注入点

Response Synthesizer 内部依赖一个text_qa_template,它定义了“如何把上下文和问题组织成给 LLM 的 prompt”。默认模板大致是这样的结构:先给一段上下文,然后说“请基于以上信息回答问题”,最后附上问题。这个默认模板在简单场景够用,但在需要控制输出格式、要求引用来源、限定回答语言时就不够了。

自定义模板的注入方式很直接,在构造 Query Engine 时传入:

from llama_index.core import PromptTemplate qa_prompt = PromptTemplate( "以下是检索到的上下文信息:\n" "---------------------\n" "{context_str}\n" "---------------------\n" "请严格基于上述上下文回答问题,不要使用上下文之外的知识。\n" "如果上下文中没有相关信息,请直接回答“未找到相关信息”。\n" "回答时请标注信息来源的编号,格式如 [1][2]。\n" "问题:{query_str}\n" "回答:" ) query_engine = index.as_query_engine( text_qa_template=qa_prompt, response_mode="compact" )

这里有两个关键变量:{context_str}会被 Synthesizer 自动替换成节点内容,{query_str}替换成用户问题。你不需要手动拼接,Synthesizer 在内部处理了。

我踩过的一个坑是:自定义模板里如果漏掉了{context_str}或{query_str},Synthesizer 不会报错,但 LLM 收到的 prompt 里就没有上下文或问题,结果就是模型开始胡编。所以每次改模板后,务必用no_text模式先看看检索到的节点,再单独打印一次最终 prompt 确认变量替换正确。

另一个细节是refine模式会用到两个模板:text_qa_template用于第一个节点,refine_template用于后续节点。如果你只自定义了前者,后者还是默认的,可能导致多轮精炼时风格不一致。建议两个都自定义,保持 prompt 风格统一。

3.2 上下文窗口管理与节点截断策略

这是 Response Synthesizer 最容易被低估的复杂度所在。LLM 的上下文窗口是有限的,而检索回来的节点总 token 数很容易超限。Synthesizer 需要决定:哪些节点保留、哪些截断、截断多少。

compact模式的策略是:按节点顺序累加 token,直到接近text_qa_template能容纳的上限,超出的节点留到下一轮。这里有个隐藏参数text_qa_template的实际可用 token 数,它等于模型上下文窗口减去max_tokens(输出预留)再减去模板本身的 token。如果你不显式设置,Synthesizer 会用模型的默认值,但不同模型的默认值差异很大。

我建议显式控制这几个参数:

query_engine = index.as_query_engine( response_mode="compact", text_qa_template=qa_prompt, similarity_top_k=6, # 控制单次送进 LLM 的最大 token response_kwargs={"max_tokens": 512} )

similarity_top_k决定了检索节点数,间接影响 Synthesizer 的处理量。很多人盲目调大top_k以为能提升召回,结果 Synthesizer 要处理大量低质量节点,反而拉低了答案质量。我的经验是:先用no_text模式看不同top_k下的节点质量,找到质量拐点后再定值,通常 4~8 是个合理区间。

节点截断还有一个坑:如果单个节点本身就超长(比如一个 chunk 设了 2048 token),Synthesizer 在compact模式下可能直接把它截断,导致节点后半部分的信息丢失。解决办法是在索引阶段就控制好 chunk 大小,或者在 Postprocessor 里做节点切分。我一般把 chunk 控制在 512~1024 token,既保证语义完整,又不会在合成阶段被截断。

3.3 引用溯源与结构化输出的实现路径

RAG 系统要落地到企业场景,引用溯源几乎是刚需。用户不信任一个没有出处的答案。Response Synthesizer 本身不直接产出引用,但你可以通过 prompt 工程 + 后处理实现。

思路是这样的:在text_qa_template里要求 LLM 在答案中标注来源编号,同时让 Synthesizer 返回的 Response 对象保留source_nodes。Response 对象的source_nodes属性包含了本次生成用到的所有节点及其元数据。你可以把 LLM 标注的编号和source_nodes的顺序对应起来。

response = query_engine.query("你的问题") print(response.response) # LLM 生成的答案,含 [1][2] 标注 for i, node in enumerate(response.source_nodes): print(f"[{i+1}] 来源:{node.metadata.get('file_name')}") print(f" 内容片段:{node.text[:100]}...")

这里有个细节:compact模式下如果发生了多轮调用,source_nodes会包含所有轮次用到的节点,但 LLM 标注的编号可能只覆盖了部分。所以更稳妥的做法是让 LLM 在每轮都标注,或者改用refine模式逐节点生成再合并引用。

结构化输出是另一个高频需求。如果你需要 LLM 返回 JSON 格式的答案,可以在 prompt 里明确要求,并配合response_kwargs里的response_format(如果模型支持)。但要注意,不是所有模型都支持强制 JSON 输出,这时候就需要在 Synthesizer 之后加一层解析和校验,解析失败时重试或降级。

4. 实操:从零搭建一个可调优的 Query Engine

4.1 环境准备与索引构建的最小闭环

先把环境跑起来。我假设你已经有了一个文档集合,这里用 LlamaIndex 的标准流程走一遍。

pip install llama-index llama-index-llms-openai llama-index-embeddings-openai
from llama_index.core import VectorStoreIndex, SimpleDirectoryReader from llama_index.core.node_parser import SentenceSplitter # 加载文档 documents = SimpleDirectoryReader("./data").load_data() # 切分节点,控制 chunk 大小 splitter = SentenceSplitter(chunk_size=768, chunk_overlap=100) nodes = splitter.get_nodes_from_documents(documents) # 构建索引 index = VectorStoreIndex(nodes)

chunk_size=768和chunk_overlap=100是我在多个项目里验证过的比较稳的起点。chunk 太小会导致语义碎片化,检索时容易召回不完整的片段;chunk 太大则会在 Synthesizer 阶段占用过多上下文,挤压其他节点的空间。overlap 设 100 是为了避免关键信息刚好被切在边界上。

索引构建完成后,先别急着调 Query Engine,用no_text模式验证检索质量:

retriever = index.as_retriever(similarity_top_k=6) nodes = retriever.retrieve("你的测试问题") for node in nodes: print(f"score: {node.score:.4f}") print(node.text[:200]) print("---")

这一步非常关键。如果检索回来的节点本身就不相关,后面 Synthesizer 再怎么调都是白搭。我见过太多人跳过这步直接调生成,结果在错误的方向上优化了很久。

4.2 配置 Response Synthesizer 的完整参数清单

确认检索质量 OK 后,开始配置 Query Engine。下面是一个我常用的完整配置,带注释说明每个参数的作用:

from llama_index.core import PromptTemplate from llama_index.core.response_synthesizers import get_response_synthesizer # 自定义 QA 模板 qa_prompt = PromptTemplate( "上下文信息如下:\n" "---------------------\n" "{context_str}\n" "---------------------\n" "请基于上下文回答问题,标注来源编号。\n" "上下文无相关信息时回答“未找到”。\n" "问题:{query_str}\n" "回答:" ) # 自定义 refine 模板(refine 模式用) refine_prompt = PromptTemplate( "已有答案:{existing_answer}\n" "补充上下文:\n" "---------------------\n" "{context_msg}\n" "---------------------\n" "请基于补充上下文优化已有答案。\n" "如果补充上下文无帮助,保持原答案不变。\n" "问题:{query_str}\n" "优化后的回答:" ) # 构造 Response Synthesizer synthesizer = get_response_synthesizer( response_mode="compact", text_qa_template=qa_prompt, refine_template=refine_prompt, use_async=False # 调试时关掉异步,方便看日志 ) # 构造 Query Engine query_engine = index.as_query_engine( similarity_top_k=6, response_synthesizer=synthesizer, node_postprocessors=[] # 后续可加 Postprocessor )

这里use_async=False是我调试时的习惯。异步虽然快,但日志交错、错误堆栈不直观,排查问题时很痛苦。等配置稳定后再开异步。

node_postprocessors先留空,等基础流程跑通后再逐个加。我一般按这个顺序加:先加SimilarityPostprocessor做阈值过滤,再加KeywordNodePostprocessor做关键词过滤,最后加SentenceEmbeddingOptimizer做句子级裁剪。

4.3 不同 response_mode 的实测对比与选择

光看文档不够,我拿同一个知识库做了一组对比测试,问题都是“XX 功能的配置步骤是什么”,检索节点数固定为 6。结果如下:

response_mode答案完整度答案准确度延迟(秒)备注
compact中高2.1默认首选,性价比最高
refine高高8.7精度略高但延迟翻倍
tree_summarize高中5.3适合摘要类问题
simple_summarize低低1.2节点多时信息丢失严重
no_text--0.3只返回节点,用于调试

实测下来,compact在大多数场景下是最优解。refine的精度优势在节点数少(3 个以内)时才明显,节点一多,多轮精炼反而容易引入噪声。tree_summarize在“总结这份文档”这类问题上表现好,但在“具体步骤是什么”这类需要精确提取的问题上不如compact。

有个细节值得注意:compact模式在节点总 token 超过单次 prompt 上限时,会自动拆成多轮。这时候它的行为和refine有点像,但第一轮用的是text_qa_template,后续轮次用refine_template。所以如果你自定义了模板,两个都要配好。

4.4 流式输出与异步查询的落地配置

线上环境对首字延迟很敏感,流式输出几乎是标配。LlamaIndex 的 Query Engine 支持流式,但配置方式和普通查询略有不同:

query_engine = index.as_query_engine( streaming=True, similarity_top_k=6, response_synthesizer=synthesizer ) response = query_engine.query("你的问题") for token in response.response_gen: print(token, end="", flush=True)

streaming=True会让 Synthesizer 以生成器方式返回 token。但要注意,compact模式在多轮调用时,流式输出只对最后一轮生效,前面几轮的中间结果不会流式返回。如果你需要全程流式,refine模式更合适,但延迟会更高。

异步查询用aquery:

response = await query_engine.aquery("你的问题")

异步的价值在于批量查询场景。我做过一个测试,100 个问题串行查询耗时 210 秒,用asyncio.gather并发查询(并发度 10)降到 28 秒。但并发度不能无限调大,受限于 LLM API 的速率限制和你的配额。我一般从并发度 5 开始压测,找到不触发限流的上限。

5. 常见问题与排查技巧实录

5.1 答案与检索内容不符的排查路径

这是最高频的问题:检索回来的节点明明包含正确答案,但 LLM 生成的内容却对不上。排查路径我总结成一条链:

第一步,确认节点内容确实包含答案。用no_text模式打印节点全文,别只看前 200 字,有时候答案在节点后半段。

第二步,确认最终 prompt 里包含了这些节点。在 Synthesizer 里加日志,或者临时把text_qa_template改成只输出{context_str},看看实际送进去的上下文是什么。我遇到过compact模式因为 token 计算偏差,把包含答案的节点排到了第二轮,而第一轮生成的答案已经“定型”,第二轮 refine 没能纠正过来。

第三步,检查 prompt 模板的指令是否清晰。默认模板比较宽松,LLM 可能“自由发挥”。加上“严格基于上下文”“不要使用外部知识”这类约束后,符合率会明显提升。

第四步,检查模型本身的能力。有些小模型在长上下文下的指令遵循能力很弱,换一个更强的模型或者缩短上下文往往能解决。

5.2 token 超限与节点丢失的典型场景

token 超限报错通常发生在compact和simple_summarize模式。根本原因是 Synthesizer 对模型上下文窗口的估算和实际不符。常见原因有三个:

  • 模型上下文窗口配置错误。LlamaIndex 需要知道模型的context_window和max_tokens,如果你用的是自定义模型或代理接口,这两个值可能没正确传入。
  • 模板本身占用过多 token。自定义模板写得太长,挤压了上下文空间。
  • 节点元数据也被计入 token。有些节点带大量元数据,Synthesizer 在拼接时会把元数据也算进去。

解决办法:显式设置response_kwargs={"max_tokens": 512}预留输出空间,用SimilarityPostprocessor过滤掉低分节点减少总量,或者改用tree_summarize模式让每层处理的 token 更可控。

节点丢失则更隐蔽。compact模式下如果节点总 token 刚好卡在边界,可能出现某个节点被“跳过”的情况。我的做法是在 Postprocessor 里按 token 数排序,把最重要的节点排在前面,确保它们优先被处理。

5.3 引用编号错乱的修复方法

引用编号错乱一般有两个原因:一是 LLM 没有严格按编号标注,二是source_nodes的顺序和 LLM 看到的顺序不一致。

第一个原因靠 prompt 约束,在模板里明确“每个事实性陈述后必须标注来源编号,格式为 [数字]”。第二个原因需要在代码层面保证:Synthesizer 送进 LLM 的节点顺序,和 Response 对象里source_nodes的顺序要一致。实测下来,compact模式下这两者通常是一致的,但如果你加了 Postprocessor 改变了节点顺序,就可能错位。

稳妥的做法是:在 Postprocessor 之后、Synthesizer 之前,给每个节点打上一个稳定的 ID,然后在 prompt 里让 LLM 引用这个 ID 而不是序号。这样即使顺序变了,ID 也能对应上。

5.4 常见问题速查表

问题现象可能原因排查动作解决方向
答案与检索内容不符节点未进 prompt / 模板指令弱打印最终 prompt调模板、换模式
token 超限报错上下文窗口估算错误检查模型配置设 max_tokens、减节点
节点丢失token 边界跳过打印每轮节点排序、换 tree_summarize
引用编号错乱顺序不一致对比 source_nodes用稳定 ID 替代序号
延迟过高refine 多轮调用看调用次数切 compact、开异步
流式输出中断多轮调用只流最后一轮看 response_gen切 refine 或改架构

5.5 几个我踩过的坑和对应技巧

坑一:盲目调大similarity_top_k。以为召回越多越好,结果 Synthesizer 处理大量噪声节点,答案质量反而下降。技巧:用no_text模式画一条“top_k - 节点相关度”曲线,找到相关度骤降的拐点。

坑二:忽略refine_template的自定义。只改了text_qa_template,结果 refine 阶段风格突变。技巧:两个模板一起改,保持指令风格一致。

坑三:在compact模式下期待逐节点引用。compact会把多个节点合并进一个 prompt,LLM 很难精确区分每个事实来自哪个节点。技巧:需要精确引用时用refine或no_text+ 自己后处理。

坑四:异步查询没做限流。并发度调太高触发 API 限流,大量请求失败。技巧:用asyncio.Semaphore控制并发度,从 5 开始逐步压测。

坑五:流式输出和compact多轮不兼容。用户看到的是最后一轮的流式,前面几轮的等待时间没有反馈。技巧:如果首字延迟是硬指标,考虑用refine或者把检索节点数压到单轮能处理完的量。

6. 从 Query Engine 往外延伸:还能怎么优化

6.1 把 Query Engine 接入 Agent 工作流

Query Engine 本身是一个“一问一答”的组件,但在 Agentic RAG 的场景下,它往往作为 Agent 的一个工具被调用。Agent 会根据用户意图决定是否调用 Query Engine、调用几次、用什么参数调用。

这种模式下,Query Engine 的配置要更“防御性”。因为 Agent 可能传入很奇怪的问题,或者连续调用多次。我一般会做两件事:一是给 Query Engine 加超时控制,避免单次查询卡死整个 Agent;二是给similarity_top_k设一个上限,防止 Agent 传入过大的值导致 token 爆炸。

from llama_index.core.tools import QueryEngineTool query_tool = QueryEngineTool.from_defaults( query_engine=query_engine, name="knowledge_base", description="查询内部知识库,适用于产品配置、故障排查类问题" )

description写得好不好,直接影响 Agent 的调用准确率。我试过把 description 写得很泛(“查询知识库”),Agent 经常在不该调用的时候调用;改成具体的适用场景描述后,误调用率明显下降。

6.2 多 Query Engine 的路由与融合

当知识库分成多个领域(比如产品文档、售后记录、内部规范),可以给每个领域建一个 Query Engine,然后用 Router 做路由。LlamaIndex 提供了RouterQueryEngine,它用一个 LLM 做意图分类,把查询分发到对应的子引擎。

from llama_index.core.query_engine import RouterQueryEngine from llama_index.core.selectors import LLMSingleSelector router_engine = RouterQueryEngine( selector=LLMSingleSelector.from_defaults(), query_engine_tools=[product_tool, support_tool, policy_tool] )

路由的准确率取决于每个工具的 description 是否清晰、是否有区分度。我踩过的坑是:两个领域的文档有重叠,导致 Router 经常选错。解决办法是在 description 里明确写出“不适用于 XX 场景”,用排除法帮助 Router 区分。

如果路由准确率上不去,可以考虑“融合”策略:同时查多个引擎,把结果合并后再交给一个 Synthesizer。这样牺牲一些延迟,换取更高的召回覆盖。

6.3 评估 Query Engine 效果的简易方法

调优不能靠感觉,得有评估。我常用的简易评估流程是:准备 20~30 个“问题 - 标准答案”对,跑一遍 Query Engine,然后用 LLM 做裁判打分(答案是否包含标准答案的关键信息、是否有编造)。

eval_prompt = PromptTemplate( "标准答案:{reference}\n" "模型答案:{response}\n" "请判断模型答案是否包含标准答案的关键信息," "输出 1(包含)或 0(不包含),并说明理由。" )

这个方法的成本不高,但能快速对比不同配置的效果。我一般会对比三组:不同response_mode、不同similarity_top_k、有无 Postprocessor。每组跑完记录准确率,选最优组合。

要注意的是,LLM 裁判本身也有偏差,所以评估集要足够大(至少 20 个问题),并且标准答案要写得明确。如果条件允许,人工抽检 10% 的结果校准 LLM 裁判的可靠性。

6.4 一个容易被忽略的细节:Response 对象的元数据

最后分享一个细节:Query Engine 返回的 Response 对象除了response和source_nodes,还有metadata属性,里面包含了本次查询的耗时、token 使用量等信息。这些数据在线上监控里非常有用。

response = query_engine.query("你的问题") print(response.metadata) # {'total_token_count': 1234, 'query_time': 2.1, ...}

我把这些元数据接入了监控面板,能实时看到 P95 延迟、平均 token 消耗、检索节点数分布。有一次发现某类问题的 token 消耗异常高,排查后发现是检索召回了大量重复节点,加了去重 Postprocessor 后降下来了。这种问题光看答案质量是发现不了的,必须靠元数据监控。

另外,source_nodes里每个节点的score也值得记录。如果某类问题的检索分数普遍偏低,说明索引覆盖不够,需要补充文档或调整 embedding 模型。这些信号都是优化 RAG 系统的重要输入。

我个人在实际操作中的体会是,Query Engine 和 Response Synthesizer 这层看起来只是“调几个参数”,但真正决定 RAG 系统好不好用的,往往就是这些参数的组合和细节处理。检索决定能不能找到,合成决定找到之后能不能用好。把这一层吃透,你的 RAG 项目才算真正从 demo 走向可用。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询