06-引用、拒答与答案校验:让模型留在证据边界内
前面已经把候选片段整理成了 [E1]、[E2] 这样的 evidence block。到这里,RAG 链路看起来已经很完整:文档有身份,chunk 有结构,检索有候选,rerank 有排序,上下文有证据编号。
06-引用、拒答和答案校验:让模型留在证据边界内
前面已经把候选片段整理成了 [E1]、[E2] 这样的 evidence block。到这里,RAG 链路看起来已经很完整:文档有身份,chunk 有结构,检索有候选,rerank 有排序,上下文有证据编号。
到这里,还要补一道答案边界。
接入 RAG 以后,模型仍然可能幻觉:漏引用、引用未知的 [E99]、把两个冲突证据强行合并、在证据不足时给出肯定答案,或者把价格、容量、日期、型号这类高风险事实说错。
所以 NexaRAG 这一阶段要补的是答案边界:
RAG 要把证据交给模型,也要检查模型是否真的留在证据边界内。
引用先解决“答案根据哪段证据”
引用相关代码在 agent/_citation/:
agent/_citation/parser.pyagent/_citation/formatter.pyagent/_citation/policy.py
最基础的是解析引用编号:
CITATION_PATTERN = re.compile(r"\[(E\d+)\]", re.IGNORECASE)
也就是说,模型答案里出现 [E1]、[E2] 时,系统可以抽取这些 evidence id。然后 known_evidence_ids(evidences) 会根据 final context 里的 evidence list 生成合法编号集合。
check_citations() 做几件事:
- 抽取答案里的 cited evidence ids。
- 判断引用是否都存在于当前 evidence list。
- 抽取关键事实句。
- 计算关键事实句里有引用的比例。
- 给出
missing_citations、unknown_evidence_ids、low_citation_coverage等 warning。
这一步解决的是“答案是否绑定证据”。有了 citation check,模型引用 [E99] 这种未知证据时,系统可以及时发现。
哪些句子必须重点引用
NexaRAG 会重点关注 key fact sentence。
agent/_citation/parser.py 里有一个 RISK_FACT_PATTERN,会识别数字、单位、价格、日期、型号等:
5000mAh
120W
8GB
5999元
2026年7月
iPhone 16 Pro Max
小米 15 Pro
另外,包含“支持、采用、包含、提供、保修、价格、容量、版本、型号、参数”等词的句子,也会被视为关键事实句。
这很符合客服 RAG 的特点。闲聊的引用要求可以更轻,但产品参数、售后规则、故障步骤、价格日期这类事实必须有来源。否则答案看起来顺,风险也会很高。
证据不足时选择拒答或追问
引用只能检查“答了以后是否有证据”,但还有一个更前置的问题:当前证据是否足够回答。
这部分在 agent/evidence/selection_policy.py 和 agent/evidence/refusal_policy.py。
assess_evidence_sufficiency() 会先把 evidence 规范化,然后判断几种情况:
- 缺少 evidence:
reason="no_evidence"。 - 多个产品混在一起,但问题缺少明确产品:
reason="ambiguous_product"。 - evidence 和问题的 token overlap 很低:
reason="no_relevant_evidence"。 - 否则认为证据基本足够。
如果证据不足,answer_policy_from_sufficiency() 会生成策略:
requires_citation
refused
asked_followup
reason
missing_aspects
这一步很重要。RAG 系统更适合根据证据状态选择动作。检索结果为空、不相关、产品歧义时,正确动作可能是拒答或追问。
冲突证据要提示版本差异
NexaRAG 还会检查证据之间是否冲突。detect_evidence_conflicts() 会根据问题关注的方面,比如电池容量、充电功率、价格、日期,从 evidence 中抽取值。如果同一方面出现多个不同值,就构造冲突结果。
比如两个证据分别说:
[E1] 电池容量为 5000mAh
[E2] 电池容量为 5200mAh
这时更稳的做法是提示知识库中存在不一致信息,并建议确认文档版本。
conflict_answer() 会把冲突值和 evidence id 拼出来,让用户知道冲突来自哪些证据:
知识库中存在不一致信息:5000mAh [E1];5200mAh [E2]。建议先确认应采用的文档版本或产品版本。
这也是 RAG 证据链里很容易被忽略的一层。有些问题会遇到多份证据互相冲突;当知识库自身不一致时,模型要把冲突暴露出来。
verifier 做最后一道门
答案校验主入口在 agent/verification/service.py 的 AnswerVerifier。
它会把问题、答案、上下文和 evidence list 交给轻量模型做判断,同时再做本地规则校验:
citation_check = check_citations(...)
numeric_warnings, unsupported_claims = grounding_warnings(...)
最后输出 VerificationResult,里面包括:
supported
confidence
unsupported_claims
conflicting_claims
numeric_warnings
citation_check
action
verifier_error
reason
suggestion
也就是说,verifier 除了使用 LLM 判断,还会结合 citation check、数字/型号/日期等高风险事实检查,以及冲突声明,决定最终 action。
如果答案里出现证据支持不足的数字,agent/verification/fact_matching.py 会尝试抽取高风险事实,并在 evidence 或 context 里查找是否存在。未找到就生成 unsupported_fact:... 这类 warning。
这对 3C 客服尤其重要。容量、功率、价格、型号、日期这些字段一旦说错,答案就属于事实错误。
VerificationNode 把结果写回 trace
Agent 节点里,答案校验发生在 agent/nodes/verification_node.py。它会从 state 里取出 final context:
trace.final_context
retrieval.final_context
然后调用 verifier,把结果写回 state。如果 retrieval_trace 存在,还会同步写入:
trace["verification"] = result
这就让前端诊断面板能看到答案校验结果。trace 同时包含检索链路和最终答案是否被证据支持。
这一步解决的内容和下一步
引用、拒答和答案校验解决的是“答案边界”问题。
它要求模型在有证据时引用证据,在缺少证据时拒答或追问,在证据冲突时提示冲突,在答案里出现数字、价格、型号、日期等高风险事实时进行校验。
下一步要回答另一个问题:这些策略到底是否让系统变好。手工看几条 trace 只能发现个案,证明整体效果的力度有限。要证明改造有效,就需要 Golden Set 和评测体系。
小结
NexaRAG 的幻觉治理把答案约束拆成几层:
agent/_citation/*检查引用是否合法。agent/evidence/*判断证据是否足够、是否冲突、是否应该拒答或追问。agent/verification/*检查答案是否被证据支持,并拦截高风险事实。VerificationNode把校验结果写回 trace。
这样,RAG 链路就从“给模型一些资料”走到了“要求模型基于资料回答,并对回答做校验”。
下一篇继续看评测:怎样用 Golden Set 证明这些改造真的让系统变好,同时避免只让单次 demo 看起来更顺。
参考材料:
- NexaRAG
agent/_citation/parser.py - NexaRAG
agent/_citation/policy.py - NexaRAG
agent/evidence/selection_policy.py - NexaRAG
agent/evidence/refusal_policy.py - NexaRAG
agent/verification/service.py - NexaRAG
agent/verification/fact_matching.py - NexaRAG
agent/nodes/verification_node.py - 原 RAG 系列
08-RAG幻觉治理-引用拒答证据约束和答案校验.md