封面图:RAG 引用与归因系统把答案断言、证据片段、来源元数据和审计校验连成一条证据链

系列:生产级 LLM 应用方法论 05
日期:2026-06-19
适合读者:正在做企业知识库、文档问答、客服助手、研究助理、合规问答或内部 Agent 的工程师、产品负责人和技术负责人。

摘要

上一篇讲 RAG 的检索质量怎么评测,核心观点是:不要只看最终答案,要把召回、排序、上下文质量和答案归因分开评测。

这一篇继续往下走:RAG 的引用与归因到底应该怎么做?

很多系统会在答案末尾放几个“来源链接”,但这不等于归因可靠。真正有用的引用要回答三个问题:

  1. 这句话是谁说的?
  2. 它依据的是哪一段证据?
  3. 这段证据是否真的支持这个结论?

如果只显示一个文档名,用户无法知道答案里的哪一句来自哪里;如果只显示一个 chunk id,用户无法判断来源是否可信、过期或越权;如果只让模型自己写引用编号,却不做后验校验,引用很容易变成装饰。

本文给出一套工程化设计:把 RAG 输出拆成 claim、citation、evidence span、source metadata 和 audit record;用结构化输出约束模型;用 claim-to-citation checker 做后验校验;在 UI 里把来源做成可点击、可高亮、可审计的证据链。

目录

1. 引用不是装饰,而是产品契约

在生产系统里,引用至少承担四个责任。

责任 问题 失败后果
可验证 用户能不能点开证据,看见原文? 答案无法核查
可归责 答案中的结论来自哪个来源? 出错后无法复盘
可审计 系统是否记录了当时检索到的证据? 线上事故无法还原
可治理 来源是否过期、越权、低可信? 合规和安全风险

所以引用不是“美化答案”的格式,而是 RAG 系统对用户的承诺:答案里的关键结论必须能回到证据。

OpenAI File Search 文档里可以看到,使用文件搜索时,响应会包含 file_search_call 以及带有 file_citation annotation 的消息;如果要分析检索结果,还可以显式 include file_search_call.results。这说明引用不应该只停留在自然语言层,而应该成为系统输出的一部分。

Web Search 文档也强调,面向用户展示 web 结果或其中信息时,inline citations 应该清晰可见且可点击。这个原则同样适用于企业 RAG:引用要能被用户看到、点开、核查。

2. 三个概念:来源、证据、断言

引用设计最常见的问题,是把 source、evidence 和 claim 混在一起。

2.1 Source:来源

Source 是原始资料单位,例如:

  • PDF 文件;
  • Notion 页面;
  • 合同版本;
  • 工单记录;
  • 数据库行;
  • 网页 URL;
  • 内部政策文档。

它回答“这份材料是什么”。

Source metadata 至少应该包含:

{
  "source_id": "refund_policy_2026_03",
  "title": "退款政策 2026-03 版",
  "source_type": "policy_doc",
  "version": "2026-03",
  "updated_at": "2026-03-18",
  "owner": "finance_ops",
  "access_scope": ["customer_service", "finance"],
  "canonical_url": "kb://policies/refund/2026-03",
  "content_hash": "sha256:..."
}

2.2 Evidence:证据

Evidence 是能支撑某个结论的最小文本或结构化片段。它可以是一个 chunk、一段原文、一个表格单元格、一条数据库记录,或一个文档区域。

它回答“哪一段材料支持这个说法”。

{
  "evidence_id": "refund_policy_2026_03#chunk_09",
  "source_id": "refund_policy_2026_03",
  "page": 4,
  "section": "跨境退款",
  "span": {
    "start_char": 1832,
    "end_char": 1938
  },
  "quote": "跨境支付订单需进入人工审核,审核完成后按原支付渠道退款。",
  "retrieval_score": 0.82
}

2.3 Claim:断言

Claim 是答案里的一个可判断真假的关键结论。

例如:

跨境支付订单需要进入人工审核。

它不是整段答案,也不是一个主题。它应该足够小,小到可以问:“这句话能不能被某一段证据支持?”

FActScore 这类工作把长文本拆成 atomic facts,再判断每个事实是否被可靠知识源支持。这个思路对 RAG 很有启发:引用校验也应该尽量做到断言级,而不是段落级。

3. 引用粒度怎么选

不同风险场景需要不同引用粒度。

粒度 例子 适合场景 风险
文档级 “来源:退款政策 2026-03 版” 低风险摘要、内部知识浏览 用户要自己翻文档
段落级 引到某个 section 或 chunk 普通知识问答、客服助手 chunk 太长时仍难核查
句子/片段级 高亮原文中的一句话 政策、合同、医疗、财务 实现成本更高
字段级 引到表格单元格或数据库字段 报表、合同字段、订单状态 需要结构化摄取
断言级 每个 claim 绑定 citations 高风险问答、审计场景 需要后验校验

我的建议是:

  • 对低风险内部知识库,至少做到段落级引用。
  • 对政策、合同、客服流程,做到关键结论的片段级引用。
  • 对法务、医疗、财务、合规、对外承诺,做到断言级引用,并保留审计记录。

不要一开始就追求所有句子都带引用。更好的方式是标出“关键断言”:

  • 数字;
  • 日期;
  • 条件;
  • 例外;
  • 权利义务;
  • 操作步骤;
  • 对用户产生影响的结论。

这些地方必须有可核查证据。

4. 一套推荐的数据结构

RAG 的引用系统可以用四类对象组织。

4.1 Evidence Pack

进入模型前,把检索结果整理成 evidence pack。注意,给模型看的 evidence id 必须稳定、短、可引用。

{
  "query_id": "refund_001",
  "evidence_pack": [
    {
      "evidence_id": "E1",
      "source_id": "refund_policy_2026_03",
      "title": "退款政策 2026-03 版",
      "version": "2026-03",
      "updated_at": "2026-03-18",
      "section": "跨境退款",
      "quote": "跨境支付订单需进入人工审核,审核完成后按原支付渠道退款。",
      "access_scope": ["customer_service", "finance"],
      "source_hash": "sha256:abc..."
    },
    {
      "evidence_id": "E2",
      "source_id": "refund_policy_2026_03",
      "title": "退款政策 2026-03 版",
      "version": "2026-03",
      "updated_at": "2026-03-18",
      "section": "到账时间",
      "quote": "退款通常在 1-5 个工作日内退回原支付渠道。",
      "access_scope": ["customer_service", "finance"],
      "source_hash": "sha256:abc..."
    }
  ]
}

这里不要只给模型一大段文本。短 id、标题、版本、quote 和来源元数据都很重要。

4.2 Answer Contract

让模型输出结构化答案,而不是只输出 Markdown。

{
  "answer": "跨境支付订单需要进入人工审核。审核完成后,退款通常会在 1-5 个工作日内退回原支付渠道。",
  "claims": [
    {
      "claim_id": "C1",
      "text": "跨境支付订单需要进入人工审核。",
      "citations": ["E1"]
    },
    {
      "claim_id": "C2",
      "text": "审核完成后,退款通常会在 1-5 个工作日内退回原支付渠道。",
      "citations": ["E1", "E2"]
    }
  ],
  "insufficient_evidence": false
}

这种结构带来三个好处:

  • 可以逐条校验 claim;
  • 可以把引用渲染成用户可点击 UI;
  • 可以记录审计日志。

4.3 Citation Object

最终展示给用户时,citation 不应该只是 E1。它应该解析成可读来源。

{
  "citation_id": "E1",
  "label": "退款政策 2026-03 版,跨境退款",
  "source_id": "refund_policy_2026_03",
  "quote": "跨境支付订单需进入人工审核,审核完成后按原支付渠道退款。",
  "locator": {
    "page": 4,
    "section": "跨境退款",
    "start_char": 1832,
    "end_char": 1938
  },
  "url": "kb://policies/refund/2026-03#page=4",
  "updated_at": "2026-03-18"
}

4.4 Audit Record

线上出问题时,不能只看用户看到的答案。要能还原当时系统看到了什么。

{
  "trace_id": "rag_20260619_000431",
  "user_id_hash": "u_...",
  "query": "跨境订单退款多久到账?",
  "retrieval": {
    "queries": ["跨境订单退款多久到账", "跨境支付退款人工审核"],
    "filters": {"department": "customer_service"},
    "top_k": 8,
    "result_ids": ["E1", "E2", "E3"]
  },
  "answer_contract": "...",
  "citation_check": {
    "unsupported_claims": [],
    "missing_citations": [],
    "stale_sources": []
  },
  "model": "production-answer-model",
  "generated_at": "2026-06-19T17:57:23+08:00"
}

注意隐私和合规:日志里不一定保存完整用户身份或敏感正文,但必须保存足够的 hash、source id、版本、引用和校验结果,方便复盘。

5. 让模型输出可校验的答案

模型很擅长写自然语言,但自然语言不适合直接做审计。一个可用的生成指令应该明确三件事。

5.1 只允许引用 evidence pack 里的 id

你只能引用 evidence_pack 中出现的 evidence_id。
不要编造 citation id。
如果没有足够证据,设置 insufficient_evidence=true,并说明缺什么证据。

这能减少模型凭空造来源。

5.2 每个关键断言必须有 citations

关键断言包括数字、日期、条件、例外、步骤、限制、权限、法律/财务/医疗影响。
每个关键断言必须放入 claims 数组,并绑定一个或多个 citations。
不要把多个无关事实塞进同一个 claim。

这能减少“整段答案只有一个引用”的问题。

5.3 不确定时降级

如果证据之间冲突,必须指出冲突来源,不要自行选择一个结论。
如果证据不足,必须输出 insufficient_evidence=true。
如果答案需要外部最新信息,而 evidence_pack 没有,必须说明无法基于当前资料回答。

这能减少“引用包装过的幻觉”。

6. Claim-to-Citation 校验怎么做

模型输出引用之后,还需要后验校验。一个基础 checker 可以分四步。

6.1 解析 claims

先把答案拆成 claim。最稳的是让模型直接输出 claims 数组;如果历史系统只能输出 Markdown,可以后处理抽取关键断言。

6.2 校验 citation id 是否存在

检查每个 citation 是否来自 evidence pack。任何不存在的 id 都是硬错误。

6.3 检查 quote 是否支持 claim

支持关系可以分三层:

方法 适合场景 局限
字符串/字段规则 精确数字、日期、订单状态 对改写不友好
结构化比对 表格、数据库、合同字段 需要摄取时结构化
LLM judge / NLI 开放文本、复杂归纳 需要校准和抽样人工复核

OpenAI Graders 文档虽然处在平台迁移期,但其中的思想仍然有价值:评测可以用 string check、text similarity、score model、Python grader 等方式组合。对 RAG 引用来说,也可以把规则校验和模型校验组合起来。

6.4 对失败 claim 做策略处理

不要只把 checker 当日志。它应该影响产品行为:

校验结果 推荐处理
citation id 不存在 拒绝展示该答案,重试或降级
claim 无 citation 删除该 claim 或要求模型重写
citation 不支持 claim 删除、重写或标记为证据不足
source 过期 使用新版本重检索,或提示版本冲突
source 越权 立即阻断,并记录安全事件

在高风险场景里,citation check 失败不应该“带病上线”。

7. 复杂场景:多来源、冲突、表格、摘要和权限

7.1 多来源支撑

有些结论需要多个证据共同支撑。

{
  "claim_id": "C3",
  "text": "跨境退款需要人工审核,审核后仍按原支付渠道退回。",
  "citations": ["E1", "E2"],
  "support_type": "joint"
}

这种 claim 不能只检查单个 citation。要允许 joint support:E1 支持“人工审核”,E2 支持“原支付渠道退回”。

7.2 冲突证据

如果两个来源冲突,不要让模型沉默处理。

{
  "claim_id": "C4",
  "text": "关于跨境退款到账时间,当前资料存在版本冲突。",
  "citations": ["E2", "E7"],
  "conflict": true,
  "resolution": "E2 是 2026-03 版,E7 是 2025-10 版;应优先使用 E2。"
}

冲突不是坏事。坏的是系统假装没有冲突。

7.3 表格和数据库字段

表格引用不要只引用整张表。最好保存单元格、行列名和筛选条件。

{
  "evidence_id": "E12",
  "source_id": "pricing_table_2026_q2",
  "locator": {
    "row_key": "enterprise",
    "column": "sla_credit",
    "sheet": "SLA"
  },
  "value": "10%",
  "quote": "enterprise / sla_credit = 10%"
}

否则用户点开一个大表,仍然不知道答案依据哪里。

7.4 摘要引用

摘要最容易出现“每句话都像有根据,但具体找不到原文”的问题。

推荐做法是:

  • 先为原文证据建立 citation;
  • 摘要句引用原文证据,而不是引用摘要中间产物;
  • 如果必须引用中间摘要,也要保留摘要生成时使用的 source ids。

7.5 权限和租户隔离

引用系统不能泄露用户无权看到的 source title、文件名或 quote。

错误例子:

根据 VIP 客户折扣表,ACME 的折扣为 27%。

即使答案没有展开文档内容,文件名本身也可能泄露敏感信息。

正确做法是检索前强制权限过滤,展示前再次检查 citation 的 access_scope。权限失败时,不应该把 citation label 发给前端。

8. UI 怎么展示引用

好的引用 UI 不是把脚注堆在答案末尾,而是让用户能快速核查。

8.1 Inline citation

关键句旁边显示引用标记:

跨境支付订单需要进入人工审核 [1]。审核完成后,退款通常会在 1-5 个工作日内退回原支付渠道 [1][2]。

标记要能点击。点击后展示来源卡片。

8.2 Source drawer

来源卡片至少展示:

  • 文档标题;
  • 版本;
  • 更新时间;
  • section/page;
  • 原文 quote;
  • 权限范围或可见性说明;
  • “打开原文”链接;
  • “该引用支持哪一句话”。

8.3 原文高亮

如果来源是 PDF、网页、知识库页面,点击 citation 应该跳到原文位置并高亮引用片段。用户不应该在 80 页 PDF 里手动找答案。

8.4 不确定性展示

如果证据不足或冲突,不要用绿色勾号假装确定。

可以用三种状态:

状态 说明
已支持 引用片段直接支持 claim
部分支持 引用支持部分事实,仍缺少另一部分
证据不足 没有足够来源支撑

8.5 引用不要遮挡阅读

引用 UI 的目的不是炫技。正文要顺畅,证据要能展开。对移动端尤其要避免大段脚注打断阅读。

9. 线上日志和审计记录

RAG 出错后,最怕只剩一句用户投诉:

它引用了错误政策。

但你不知道当时:

  • 用户问题是什么;
  • 检索 query 被改写成什么;
  • top-k 结果是什么;
  • 哪些证据进了上下文;
  • 模型输出了哪些 claim;
  • citation checker 是否通过;
  • 知识库当时是什么版本;
  • 前端最终展示了哪些来源。

所以线上 trace 至少记录:

字段 用途
trace_id 关联用户反馈、日志和评测
query 原始问题
rewritten_queries 复盘检索
filters 复盘权限和版本
retrieved_evidence_ids 看召回和排序
context_evidence_ids 看最终进模型的证据
answer_claims 看断言拆分
citations 看引用关系
citation_check_result 看是否带病展示
source_hashes 复盘知识库版本
model_and_prompt_version 回归测试

如果数据敏感,保存 hash 和 locator,必要时通过受控后台重新取证据。不要为了省日志,把系统变成不可审计。

10. 如何评测引用质量

ALCE 提出把带引用的长答案从 fluency、correctness、citation quality 等维度评测;WebGPT 也把收集 references 作为帮助用户评估事实准确性的设计之一;RARR 则展示了为已有模型输出补充归因并修订不支持内容的思路。这些研究共同提醒我们:引用质量要单独评测,而不是藏在答案满意度里。

一套实用指标可以这样设计:

指标 含义
Citation Coverage 关键 claim 中有 citation 的比例
Citation Support Rate citation 实际支持 claim 的比例
Unsupported Claim Rate 没有证据支撑却被输出的 claim 比例
Missing Citation Rate 应该引用但没有引用的 claim 比例
Citation Precision 被引用证据中真正相关的比例
Citation Recall gold evidence 中被引用到的比例
Over-citation Rate 一个简单 claim 绑定过多弱相关来源的比例
Stale Citation Rate 引用过期来源的比例
Permission-safe Citation Rate 引用没有越权来源的比例

评测集要覆盖这些样本:

  • 单证据直接支持;
  • 多证据联合支持;
  • 引用部分支持;
  • 引用不支持;
  • 答案正确但引用错误;
  • 答案错误但引用看似相关;
  • 新旧版本冲突;
  • 权限不足;
  • 无答案问题;
  • 表格字段问题;
  • 摘要型问题。

如果你的 eval 里没有“答案对但引用错”的样本,系统很容易学会用相关文档当装饰。

11. 一个最小校验器示例

下面这个例子只做基础规则校验:citation id 是否存在、claim 是否有引用、引用 quote 是否覆盖 claim 中的关键短语。真实系统里还需要语义 entailment、字段比对、LLM judge 和人工抽检。

import json
import re
from pathlib import Path


def normalize(text):
    return re.sub(r"\s+", "", text.lower())


def keyword_overlap(claim, quote):
    claim_norm = normalize(claim)
    quote_norm = normalize(quote)

    # 真实项目应使用领域词表或实体抽取,这里只保留一个最小示例。
    tokens = re.findall(r"[\u4e00-\u9fff]{2,}|[a-zA-Z0-9%.-]+", claim)
    important = [t for t in tokens if len(t) >= 2]
    if not important:
        return 0.0

    hits = sum(1 for token in important if normalize(token) in quote_norm)
    return hits / len(important)


def check_answer(answer_contract, evidence_pack, threshold=0.5):
    evidence_by_id = {item["evidence_id"]: item for item in evidence_pack}
    errors = []
    warnings = []

    for claim in answer_contract.get("claims", []):
        claim_id = claim.get("claim_id", "<missing>")
        claim_text = claim.get("text", "")
        citations = claim.get("citations", [])

        if not citations:
            errors.append({
                "claim_id": claim_id,
                "type": "missing_citation",
                "message": "关键断言没有引用"
            })
            continue

        support_scores = []
        for citation_id in citations:
            evidence = evidence_by_id.get(citation_id)
            if evidence is None:
                errors.append({
                    "claim_id": claim_id,
                    "type": "unknown_citation",
                    "citation_id": citation_id
                })
                continue

            score = keyword_overlap(claim_text, evidence.get("quote", ""))
            support_scores.append(score)

        if support_scores and max(support_scores) < threshold:
            warnings.append({
                "claim_id": claim_id,
                "type": "weak_support",
                "best_score": round(max(support_scores), 3),
                "message": "引用可能不足以支持该断言,需要语义校验或重写"
            })

    return {
        "passed": not errors,
        "errors": errors,
        "warnings": warnings
    }


if __name__ == "__main__":
    answer_contract = json.loads(Path("answer_contract.json").read_text(encoding="utf-8"))
    evidence_pack = json.loads(Path("evidence_pack.json").read_text(encoding="utf-8"))
    print(json.dumps(check_answer(answer_contract, evidence_pack), ensure_ascii=False, indent=2))

这个 checker 很粗糙,但它说明了一个重要原则:引用必须经过程序检查,而不能完全相信模型自己说“我引用了”。

对于高风险场景,可以再加一个 LLM judge:

你是引用校验器。判断 Claim 是否被 Evidence 直接支持。

Claim:
{{claim_text}}

Evidence:
{{quote}}

只输出 JSON:
{
  "supported": true | false | "partial",
  "reason": "一句话说明",
  "missing_fact": "如果 partial 或 false,说明缺什么"
}

LLM judge 也要评测。不要让 judge 变成新的不可控黑盒。

12. 常见误区

12.1 有引用就等于可信

引用可能不相关、过期、越权、断章取义。真正要检查的是 support relation。

12.2 只在答案末尾列来源

末尾来源列表适合进一步阅读,不适合证明每个关键结论。关键断言需要 inline citation。

12.3 让模型自己编 citation id

citation id 必须来自系统提供的 evidence pack。模型不能发明来源。

12.4 引用整个 PDF

引用到 80 页 PDF 不能算合格归因。至少要到 section、page 或 chunk;高风险场景要到 quote/span。

12.5 忽略表格和结构化数据

很多业务答案来自表格、数据库字段或配置项。不要把它们转成一大段文本后丢掉行列信息。

12.6 引用 UI 不可点击

不可点击的引用无法核查。用户最终只能选择相信或不相信模型。

12.7 不保存当时的 source 版本

知识库会更新。今天点开的来源,不一定是模型昨天回答时看到的来源。审计日志必须记录版本或 hash。

13. 落地路线图

第 1 周:把 evidence id 标准化

  • 为进入上下文的每个 chunk 分配短 id;
  • 保留 source id、title、version、quote、locator;
  • 在 prompt 中只允许引用这些 id。

第 2 周:改输出契约

  • 让模型输出 answer + claims + citations + insufficient_evidence
  • 对关键断言做粒度约束;
  • 失败时允许证据不足,而不是强行回答。

第 3 周:加引用 checker

  • 检查 citation id 是否存在;
  • 检查每个关键 claim 是否有引用;
  • 检查 source 是否过期或越权;
  • 对弱支持 claim 做重写、删除或降级。

第 4 周:改 UI 和日志

  • inline citation 可点击;
  • source drawer 展示 quote 和 locator;
  • 原文高亮;
  • trace 保存 retrieval、context、claims、citations、checker result。

第 5 周:做引用质量 eval

  • 标注一批 claim-to-evidence 样本;
  • 评测 Citation Coverage、Support Rate、Unsupported Claim Rate;
  • 把用户反馈和线上失败样本回流。

14. 发布前自检清单

上线前至少检查这些问题:

  • 每个进入上下文的证据是否有稳定 evidence_id
  • 模型是否只能引用 evidence pack 中的 id?
  • 关键断言是否被拆成 claims
  • 每个关键 claim 是否有 citation?
  • citation 是否能解析到 source、quote、locator 和版本?
  • 是否有 checker 检查 citation id、缺失引用、弱支持、过期和越权?
  • citation check 失败时是否会阻断、重写或降级,而不是照常展示?
  • 前端是否能点击引用并高亮原文?
  • 审计日志是否保存 source hash、模型版本、prompt 版本和 checker 结果?
  • eval set 是否包含“答案对但引用错”“答案错但引用看似相关”“多来源联合支持”“无答案”和“权限不足”样本?

如果这些问题答不上来,引用就还只是格式,不是工程能力。

15. 总结

RAG 的引用与归因,不是给答案加几个脚注。它是一套从数据摄取、检索、上下文构造、模型输出、后验校验、前端展示到审计日志的完整链路。

最小可用设计可以概括为:

Source -> Evidence Span -> Evidence Pack -> Claim -> Citation -> Support Check -> UI Highlight -> Audit Record

只要这条链路断了,RAG 就会回到“看起来有来源,但无法验证”的状态。

下一篇建议继续写:RAG 的知识库摄取与 Chunking 怎么做:从原始文档到可检索证据单元

参考资料