01-请求入口与 Retrieval Trace:先把证据链照亮
做 RAG 项目时,很容易一上来就想调 chunk 大小、换 embedding 模型、加 rerank、改 prompt。NexaRAG 的施工路线里,第一步却是 Retrieval Trace。
01-请求入口和 Retrieval Trace:先把证据链照亮
做 RAG 项目时,很容易一上来就想调 chunk 大小、换 embedding 模型、加 rerank、改 prompt。NexaRAG 的施工路线里,第一步却是 Retrieval Trace。
这个选择很有代表性。RAG 的问题经常会在证据层提前暴露。trace 能沿着链路回答:用户原问题是什么,改写后变成什么,工具是否被调用,BM25 召回了什么,向量检索召回了什么,融合后谁排在前面,rerank 是否改变顺序,最终进入 prompt 的证据是哪几段,答案是否引用这些证据。
这篇的核心判断是:
RAG 优化的第一步,是让证据链可观察。
一次请求从哪里进入
NexaRAG 当前的聊天入口在 api/chat_router/router.py。这个文件只处理 HTTP 层的事情:校验问题、接收图片、选择普通响应或流式响应,然后把请求交给 ChatService。
典型的文本接口很薄:
@router.post("")
async def chat(req: ChatRequest, request: Request):
chat_service = get_chat(request)
return await chat_service.chat(
validate_question(req.question),
session_id=req.session_id,
user_id=req.user_id,
)
这里刻意收敛 RAG 细节。API 层只负责把用户请求送进应用编排,BM25、向量库和 citation 留给后面的服务层处理。
真正的聊天编排在 core/chat/service.py。ChatService 做的事情比“调用模型”多:它要恢复或创建会话,构造长期记忆上下文,压缩历史消息,创建 AgentInput,运行 Agent 节点链路,最后写回会话、记录 trace、整理响应。
它的核心流程可以压成这样:
chat()
-> _agent_request()
-> ConversationRegistry.get_or_create()
-> MemoryContextBuilder.build()
-> build_agent_chat_history()
-> AgentInput(...)
-> agent_group.run(...)
-> _finalize_turn()
-> finalize_trace()
-> conversation.memory.add_message()
-> record_agent_output()
-> response_from_output()
这条链路说明一件事:NexaRAG 的 RAG 请求比“query -> retrieve -> answer”更长。它带着会话、用户、图片、记忆、工具调用、生成结果和验证结果一起流动。
Agent 节点把请求拆成多个阶段
默认 Agent 编排在 agent/groups/customer_service_group.py。构建函数 build_customer_service_group() 会把多个节点串起来:
VisionNode
-> IntentNode
-> QueryRewriteNode
-> ToolReasoningNode
-> AnswerGenerationNode
-> VerificationNode
每个节点只处理一段职责。
VisionNode 处理图片。
IntentNode 判断意图。
QueryRewriteNode 改写检索问题。
ToolReasoningNode 负责工具调用和知识库检索。
AnswerGenerationNode 基于上下文生成答案。
VerificationNode 检查答案是否被证据支持。
和 Retrieval Trace 最直接相关的是 ToolReasoningNode。它会调用 search_knowledge、search_product_knowledge、search_troubleshoot_docs 这些工具。工具实现放在 tools/_retrieval/builder.py,最终会进入知识库检索,并把检索过程包装成可回传的 trace。
这个设计有一个重要好处:trace 会跟着 Agent state 一起流动。后面生成答案、校验答案、前端展示诊断信息时,都可以继续使用同一份证据链。
Trace 记录证据过程
普通日志更多记录程序是否报错。Retrieval Trace 记录的是证据怎么来的。
NexaRAG 的 trace 基础结构在 rag/_trace/records.py 里。create_retrieval_trace() 会准备一份统一骨架。
后续章节会对字段进行逐个讲解引用
按当前代码,完整字段结构可以写成这样:
retrieval_trace # 一次请求的完整证据链诊断对象
original_query # 用户进入系统时的原始问题
rewritten_query # QueryRewriteNode 改写后的检索问题
intent # 意图识别结果,通常包含 intent、confidence 等信息
needs_retrieval # 当前问题是否需要检索知识库
tool_calls[] # Agent 本轮调用过的检索工具列表
name # 工具名称,比如 search_knowledge
args # 工具调用参数,比如 query、top_k、retrieval_mode
fallback # 这次工具调用是否来自兜底检索逻辑
retrieval # 检索阶段的完整诊断信息
retrieval_mode # 请求指定的检索模式,如 auto、bm25、vector、hybrid
effective_mode # 实际生效的检索模式,比如 vector 关闭后落到 bm25
filters # 本次检索使用的 metadata filter
fallback_used # 检索阶段是否使用了 fallback
vector_enabled # 当前是否启用向量检索通道
bm25[] # BM25 通道召回的候选列表
rank # 当前候选在该列表中的排序
source # 候选所属文档来源
chunk_id # 候选对应的 chunk 编号
title_path # 候选所在文档章节路径
block_type # 候选主要 block 类型
block_types # 候选包含的 block 类型集合
chunk_strategy_version # 生成该 chunk 的切分策略版本
text_preview # 候选正文预览
metadata # 候选原始 metadata
retrieval_source # 候选来源通道标记
bm25_score # BM25 原始或归一化分数
vector_score # 向量检索分数
rrf_score # RRF 融合分数
rerank_score # rerank 后的分数
bm25_rank # BM25 通道中的名次
vector_rank # 向量通道中的名次
rerank_rank # rerank 后的名次
vector[] # 向量检索通道召回的候选列表
rank # 当前候选在该列表中的排序
source # 候选所属文档来源
chunk_id # 候选对应的 chunk 编号
title_path # 候选所在文档章节路径
block_type # 候选主要 block 类型
block_types # 候选包含的 block 类型集合
chunk_strategy_version # 生成该 chunk 的切分策略版本
text_preview # 候选正文预览
metadata # 候选原始 metadata
retrieval_source # 候选来源通道标记
bm25_score # BM25 原始或归一化分数
vector_score # 向量检索分数
rrf_score # RRF 融合分数
rerank_score # rerank 后的分数
bm25_rank # BM25 通道中的名次
vector_rank # 向量通道中的名次
rerank_rank # rerank 后的名次
fused[] # BM25 和向量检索经过 RRF 融合后的候选列表
rank # 当前候选在融合列表中的排序
source # 候选所属文档来源
chunk_id # 候选对应的 chunk 编号
title_path # 候选所在文档章节路径
block_type # 候选主要 block 类型
block_types # 候选包含的 block 类型集合
chunk_strategy_version # 生成该 chunk 的切分策略版本
text_preview # 候选正文预览
metadata # 候选原始 metadata
retrieval_source # 候选来源通道标记
bm25_score # BM25 原始或归一化分数
vector_score # 向量检索分数
rrf_score # RRF 融合分数
rerank_score # rerank 后的分数
bm25_rank # BM25 通道中的名次
vector_rank # 向量通道中的名次
rerank_rank # rerank 后的名次
reranked[] # rerank 真正生效后的候选列表
rank # 当前候选在 reranked 列表中的排序
source # 候选所属文档来源
chunk_id # 候选对应的 chunk 编号
title_path # 候选所在文档章节路径
block_type # 候选主要 block 类型
block_types # 候选包含的 block 类型集合
chunk_strategy_version # 生成该 chunk 的切分策略版本
text_preview # 候选正文预览
metadata # 候选原始 metadata
retrieval_source # 候选来源通道标记
bm25_score # BM25 原始或归一化分数
vector_score # 向量检索分数
rrf_score # RRF 融合分数
rerank_score # rerank 后的分数
bm25_rank # BM25 通道中的名次
vector_rank # 向量通道中的名次
rerank_rank # rerank 后的名次
rerank # rerank 阶段的诊断信息
enabled # 本次是否启用 rerank
provider # rerank provider,比如 noop、keyword_overlap、dashscope
fallback_used # rerank 失败时是否回退到 fused 结果
input_count # 进入 rerank 的候选数量
output_count # rerank 输出的候选数量
rank_changes[] # rerank 前后名次变化列表
chunk_id # 发生排序变化的 chunk 编号
original_rank # rerank 前的名次
rerank_rank # rerank 后的名次
original_score # rerank 前的分数
rerank_score # rerank 后的分数
errors[] # rerank 阶段错误信息
context_builder # ContextBuilder 构造 final context 的诊断信息
input_count # 进入 ContextBuilder 的候选数量
dedup # 去重阶段诊断信息
before_count # 去重前候选数量
after_count # 去重后候选数量
removed_chunk_ids[] # 去重移除的 chunk 编号列表
merge # 相邻 chunk 合并阶段诊断信息
before_count # 合并前候选数量
after_count # 合并后候选数量
merged_groups[][] # 被合并的相邻 chunk id 分组
budget # 上下文预算控制信息
max_evidence_count # 最大 evidence 数量
max_context_chars # final context 最大字符数
max_chunk_chars # 单个 chunk 最大字符数
used_chars # 已使用字符数
truncated_evidence_ids[] # 被截断的 evidence 编号列表
context_truncated # final context 是否发生整体截断
evidence_count # 最终生成的 evidence 数量
final_context[] # 检索阶段写入的最终 evidence 列表
evidence_id # evidence 编号,比如 E1
chunk_id # evidence 对应的 chunk 编号
doc_id # evidence 所属文档编号
source # evidence 所属文档来源
title # evidence 所属文档标题
title_path[] # evidence 所在章节路径
version # evidence 所属文档版本
content # evidence 正文内容
score # evidence 继承的候选分数
metadata # evidence 原始 metadata
truncated # evidence 内容是否被截断
errors[] # 检索阶段错误信息
final_context[] # 顶层最终 evidence 列表,便于后续生成和校验直接读取
evidence_id # evidence 编号,比如 E1
chunk_id # evidence 对应的 chunk 编号
doc_id # evidence 所属文档编号
source # evidence 所属文档来源
title # evidence 所属文档标题
title_path[] # evidence 所在章节路径
version # evidence 所属文档版本
content # evidence 正文内容
score # evidence 继承的候选分数
metadata # evidence 原始 metadata
truncated # evidence 内容是否被截断
answer_policy # 答案策略判断结果
requires_citation # 本轮回答是否要求引用 evidence
refused # 本轮是否触发拒答
asked_followup # 本轮是否触发追问
reason # 策略原因,比如 no_evidence、ambiguous_product
missing_aspects[] # 当前证据缺少的回答要素
refusal # 拒答相关信息
refused # 本轮是否拒答
reason # 拒答原因
missing_aspects[] # 拒答时缺少的关键信息
answer # 最终答案预览文本
verification # 答案校验结果
supported # verifier 判断答案是否被证据支持
confidence # verifier 给出的置信度
unsupported_claims[] # 证据支持不足的声明列表
conflicting_claims[] # 冲突声明列表
numeric_warnings[] # 数字、型号、日期等高风险事实告警
citation_check # 引用检查结果
has_citations # 答案是否包含 evidence 引用
cited_evidence_ids[] # 答案实际引用的 evidence 编号列表
unknown_evidence_ids[] # 当前 evidence list 之外的引用编号
citation_coverage # 关键事实句的引用覆盖率
warnings[] # 引用检查告警列表
action # verifier 建议动作,比如 allow、warn、refuse
verifier_error # verifier 执行过程中的错误信息
reason # 校验结论原因
suggestion # verifier 给出的修正建议
pass # 兼容前端和测试的通过标记
score # 0 到 5 的压缩评分
errors[] # 全局错误信息
timing_ms # 各阶段耗时统计
这里有两个细节值得单独记一下。第一,bm25、vector、fused、reranked 里的候选项来自 normalize_candidate(),实际只会保留前 MAX_TRACE_ITEMS=10 条;某些分数字段只在对应通道产生时出现。第二,正常检索下 final_context 是 evidence 结构;兜底场景里可能只有 index 和 text_preview,用于保留最低限度的上下文线索。
这些字段看起来很多,但它们都服务于同一个目标:当回答出问题时,可以定位是哪一层出了问题。
比如用户问“小米 15 Pro 电池容量是多少”,最后答案不对。trace 可以把问题拆成几种情况:
| trace 现象 | 可能问题 |
|---|---|
| BM25 和 vector 均未召回正确文档 | 文档未入库、query 表达偏离、filter 过窄 |
| 正确文档在 fused 里,但未进 reranked | rerank 或排序策略有问题 |
| 正确证据在 reranked 里,但未进 final context | 上下文预算、去重或截断出了问题 |
| final context 有正确证据,但答案缺少引用 | 生成 prompt 或引用约束需要加强 |
答案引用了未知的 [E99] | citation check 或输出约束需要加强 |
有了 trace,这些问题就能从“RAG 效果差”拆成具体层次,排查也更有方向。
检索工具怎样把证据变成 trace
tools/_retrieval/builder.py 是请求链路里很关键的一层。它会在格式化检索结果时尽量保留结构化 trace。
当知识库支持 search_with_trace() 时,工具会拿到类似这样的 payload:
{
results: [...],
trace: {...}
}
然后 _contextualize_payload() 会把候选结果交给 ContextBuilder().build(results)。如果构造出了上下文,它会把 evidence list 写回 trace:
final_context = [evidence.to_dict() for evidence in context_result.evidences]
trace["final_context"] = final_context
retrieval["final_context"] = final_context
retrieval["context_builder"] = context_result.trace
这一步很重要。RAG 在“检索到了结果”之后,还要把候选结果整理成最终上下文,也就是后面生成答案时真正看到的证据。
trace 同时记录召回结果和 final context,才能覆盖一类常见问题:正确证据确实召回了,但未进入 prompt。
ChatService 在最后补齐诊断信息
Agent 运行结束以后,core/chat/diagnostics.py 会通过 finalize_trace() 把最终结果补进 trace:
trace["rewritten_query"] = output.rewritten_query or trace.get("rewritten_query", "")
trace["intent"] = output.intent or trace.get("intent", {})
trace["tool_calls"] = trace.get("tool_calls") or output.used_tools
trace["answer"] = text_preview(output.answer, 800)
trace["verification"] = output.verification or trace.get("verification", {})
也就是说,trace 同时记录检索阶段、答案和验证结果。这样前端拿到响应以后,可以把“证据是怎么来的”和“答案是否通过验证”放在一起看。
record_agent_output() 还会把 assistant_message、verification_result、retrieval_trace 写进会话记录。这让 trace 既是一次响应里的字段,也可以成为后续复盘和排查的材料。
前端诊断面板的价值
NexaRAG 前端诊断相关代码在 frontend/src/features/diagnostics/。其中 DiagnosticsPanel.tsx 负责总入口,RetrievalTraceSection.tsx 展示检索链路,VerificationDiagnostics.tsx 展示答案校验,其他组件负责候选列表、证据列表、策略信息、图片诊断等。
这类 UI 的价值很实际。RAG 的很多质量问题需要写知识库的人、调 prompt 的人、做评测的人一起看清楚:系统到底拿到了什么证据。
比如:
- 如果 metadata 缺少产品名,诊断面板里会看到 filters 生效困难。
- 如果 query rewrite 加了错误限定,rewritten query 和召回结果会一起偏掉。
- 如果相邻 chunk 太碎,final context 里可能出现一堆重复片段。
- 如果答案没按证据引用,verification 和 citation check 会暴露问题。
前端展示 trace 的意义,就是把“答案错了”变成“证据链哪里断了”。
Trace 会影响后续每一步改造
这也是系列第一篇正文先讲 trace 的原因。
后面做文档入库和 metadata,要看正确文档是否有稳定身份。
做结构化 chunking,要看正确 chunk 是否更容易进入候选池。
做混合检索,要看 BM25、vector、fused 三层分别召回了什么。
做 rerank,要看 rerank 前后排名是否变化。
做上下文工程,要看 final context 是否更短、更清楚、更可引用。
做引用拒答,要看 answer policy、citation check、verification 是否一致。
做 Golden Set,要通过 trace 判断失败发生在召回、重排、上下文还是生成。
有了 trace,每次修改都能留下证据。
小结
Retrieval Trace 是 NexaRAG 施工线的第一步,因为它让 RAG 链路从黑盒变成可检查的过程。
请求从 api/chat_router/router.py 进入,core/chat/service.py 负责应用编排,agent/groups/customer_service_group.py 串起多个节点,tools/_retrieval/builder.py 把检索结果和上下文构建写进 trace,core/chat/diagnostics.py 在最终响应前补齐答案和 verification,前端 features/diagnostics 再把这条证据链展示出来。
下一篇继续往前看证据链的源头:知识进入系统之前,文档身份、chunk 身份和 metadata 怎样先建立起来。
参考材料:
- NexaRAG
api/chat_router/router.py - NexaRAG
core/chat/service.py - NexaRAG
core/chat/diagnostics.py - NexaRAG
agent/groups/customer_service_group.py - NexaRAG
tools/_retrieval/builder.py - NexaRAG
rag/_trace/records.py - NexaRAG
frontend/src/features/diagnostics/DiagnosticsPanel.tsx - 原 RAG 系列
05-一次RAG请求的完整链路-从用户问题到最终答案.md