引用与可溯源

状态:✅ 已补齐(2026-09-12)
一句话定义:让回答 带出处 (citation),并尽量保证陈述可被检索片段支撑(groundedness);支撑不足则拒答或降级。

大纲

  1. 引用 UX:从角标到原文
  2. 强制引用 vs 软提示
  3. Groundedness 自动评测
  4. 多源冲突与无命中拒答
  5. 端到端落地示例:四周迭代

已有相关文档(先读这些)

学习要点(读后应能回答)

  • 「有角标」一定等于「说得对」吗?→ 见 2.3
  • 无命中时产品话术怎么设计?→ 见 4.3

一、引用 UX:从角标到原文

Citation /saɪˈteɪʃən/ (Citation,引用)是信任的基础设施:小北的制度问答一旦答错,用户要能追到原文核验,否则「不敢用」本身就是最大的点踩来源。小北上线前的点踩归因里,41% 是「无出处不敢采信」——比答错还多。

引用 UX /ˌjuː ˈeks/ (User Experience,用户体验)分三档,价值递增:

档位 形态 用户能做什么 小北实现
最低 答案末尾列「来源:xxx 文档」 知道大概出处 不够——无法定位到具体条款
中等 句内角标 [1],悬停显示引用块原文 就地核对 悬停卡显示块原文 + 文档名
最高 角标点击跳转原文并高亮 全文语境核验 Markdown 跳锚点;PDF 跳页码

跳转能力完全依赖 01 篇 埋好的元数据:每块的 docIdmarkdownBreadcrumbs(锚点路径)、PDF 的页码——溯源是切分时就该想好的下游需求,不是生成端的补丁

二、强制引用 vs 软提示

2.1 软提示的失败率

「请在回答中引用来源」这种软提示,小北实测引用率仅 62%——模型兴致来了才引,且引用位置随机,前端无从渲染。引用必须是结构化输出,不是文风要求

2.2 强制引用:结构化声明

让模型逐句声明支撑块 ID,渲染层再转成角标:

落地示例:结构化引用生成(Node.js + TS,OpenAI 协议):

export interface CitedAnswer {
  sentences: { text: string; cites: string[] }[]; // cites = 块 ID
  refused?: boolean;
}

export async function generateWithCitation(
  query: string, blocks: { chunkId: string; text: string }[],
): Promise<CitedAnswer> {
  const resp = await client.chat.completions.create({
    model: "gpt-4o",
    temperature: 0,
    response_format: { type: "json_object" },
    messages: [
      {
        role: "system",
        content:
          "基于给定资料逐句回答。每句必须标注支撑它的资料编号(chunkId)。" +
          "资料不足以支撑的句子禁止输出;整题无资料支撑则 refused=true。" +
          '输出 JSON:{"sentences":[{"text":"...","cites":["块ID"]}]}' +
          " 或 {\"refused\": true}。",
      },
      {
        role: "user",
        content: `资料:\n${blocks.map((b) => `[${b.chunkId}] ${b.text}`).join("\n")}\n\n问题:${query}`,
      },
    ],
  });
  return JSON.parse(resp.choices[0].message.content ?? "{}") as CitedAnswer;
}

小北实测:强制引用把引用率从 62% 提到 98.7%,且每句的支撑关系显式可查——这是下一步自动评测的前提。

2.3 引用幻觉:有角标不等于说得对

这是文首第一个学习问题。角标只是 UI 形态,模型完全可能把 A 块的结论标上 B 块的编号——引用存在且位置合理,内容却是编的。两层校验兜底:

  1. 结构校验(必做,零成本):每个 cites ID 必须存在于本次检索结果集内——编造 ID 直接过滤或重生成;
  2. 语义校验(关键):句子与其所引块做 NLI /ˌen el ˈaɪ/ (Natural Language Inference,自然语言推理)判定,非「蕴含」关系判引用错位 → 触发重生成或降级为无引用展示。

落地示例:引用校验器(Node.js + TS):

export interface CitationCheck { valid: boolean; reason?: "badId" | "unsupported" }

export function validateCitations(
  ans: CitedAnswer, retrievedIds: Set<string>,
  nliCheck: (sentence: string, blockText: string) => Promise<boolean>, // 蕴含=true
  blocks: Map<string, string>,
): Promise<CitationCheck[]> {
  return Promise.all(ans.sentences.map(async (s) => {
    if (!s.cites.length || !s.cites.every((id) => retrievedIds.has(id))) {
      return { valid: false, reason: "badId" };            // 结构层:编造或缺失 ID
    }
    for (const id of s.cites) {
      if (await nliCheck(s.text, blocks.get(id) ?? "")) {
        return { valid: true };                             // 语义层:至少一块蕴含
      }
    }
    return { valid: false, reason: "unsupported" };         // 引了但没有一块真支撑
  }));
}

三、Groundedness 自动评测

Groundedness /ˈɡraʊndɪdnəs/ (Groundedness,有据性)衡量「每个陈述是否可被检索片段支撑」。它与检索指标的分工:hit@5 说「找没找到」,groundedness 说「说的对不对」——两者都对,系统才闭环。

3.1 指标定义

指标 定义 小北目标
引用覆盖率 带引用的句子 ÷ 全部答案句 ≥ 95%
引用有效率 通过 NLI 蕴含校验的引用 ÷ 全部引用 ≥ 95%
Groundedness 支撑率 有据支撑的句子 ÷ 全部答案句 ≥ 96%
拒答准确率 该拒答的问题中被正确拒答的比例 4.3

评测跑法:复用 01 篇 的标注集(问题 → 期望块 → 参考答案),生成答案后逐句跑 NLI,按句聚合。自动评测可日跑全量日志抽样(每天 500 条),人工复核只看「非蕴含」判例。

3.2 落地示例:批量评测脚本

export async function groundednessReport(
  cases: EvalCase[], pipeline: (q: string) => Promise<{ ans: CitedAnswer; blocks: Map<string, string> }>,
): Promise<{ coverage: number; citationValid: number; grounded: number }> {
  let total = 0, cited = 0, valid = 0, grounded = 0;
  for (const c of cases) {
    const { ans, blocks } = await pipeline(c.query);
    const checks = await validateCitations(
      ans, new Set(blocks.keys()), nliEntailment, blocks,
    );
    checks.forEach((chk, i) => {
      total++;
      if (ans.sentences[i].cites.length > 0) cited++;
      if (chk.valid) { valid++; grounded++; }
      else if (chk.reason === "unsupported") { /* 人工复核队列 */ }
    });
  }
  return {
    coverage: cited / total, citationValid: valid / Math.max(cited, 1),
    grounded: grounded / total,
  };
}

小北三期数据:上线前(仅软提示)groundedness 88.2%;强制引用后 94.1%;引用校验 + 失败重生成后 96.5%

四、多源冲突与无命中拒答

4.1 多源冲突:新旧制度并存

知识库同时存在 2023 版与 2024 版差旅制度是常态。靠检索层元数据加权解决:块带 effectiveDatesuperseded(被取代)字段,检索时新版本 × 1.3 加权、已取代版本直接过滤;答案生成时显式声明版本口径(「按 2024 版差旅制度」),让用户知道依据。

4.2 无命中判定

拒答的触发信号是检索质量而非模型自信度:重排 top-1 分数 < 阈值(小北取 0.35,用 04 篇 的标注集标定)或所有召回块 NLI 非蕴含 → 进入拒答分支。把「拒不拒」交给检索分数,把「怎么拒」交给产品话术,是两个解耦的决策。

4.3 无命中的话术设计

这是文首第二个学习问题。拒答话术三原则:

  1. 不编造替代品:禁止「相关内容是…」硬凑一块擦边的——那是把检索失败转成引用幻觉;
  2. 给出路:明确说「知识库未覆盖」,附转人工入口或提问建议(「试试换个说法 / 联系 HR」);
  3. 保留透明度:说明「已检索 300 万块文档」,让用户知道系统真努力过。

小北话术模板:「知识库中未找到与『xxx』直接相关的内容。你可以换个说法重试,或 [联系人工支持]。」

两个指标防走偏:该拒不拒(幻觉漏网)与不该拒乱拒(阈值过严伤体验)分开统计——后者超过 5% 就回头松阈值。

五、端到端落地示例:四周迭代

「小北」可溯源改造收官排期(衔接 05 篇 之后:p50 = 0.9s,点踩率 5.8%):

动作 产出 指标
W1 块元数据补齐锚点 / 页码;前端角标 + 悬停卡 中档引用 UX 引用覆盖率 62% → 98.7%
W2 强制结构化引用上线;ID 结构校验接入生成管道 引用校验器 引用幻觉(编造 ID)清零
W3 NLI 蕴含校验 + 失败重生成;groundedness 日跑抽样 自动评测面板 支撑率 96.5%
W4 版本加权过滤;拒答分支 + 话术上线;点踩反查闭环 溯源 v2 全量 点踩率 5.8% → 2.1%

工具速查(全部有开源实现):

环节 工具 备注
NLI 蕴含判定 bge / DeBERTa 类 NLI 模型,或 LLM 判分 批量日跑,单条 < 30ms
结构化输出 OpenAI JSON mode / 国产模型同能力 引用声明进 schema
评测面板 复用 01 篇评测脚本 + source / 句级维度 与检索指标同面板分栏
点踩反查 日志埋点 + 缓存答案标记 误缓存与引用错位共用归因管道

自检清单

  • 答案逐句带块 ID 引用,ID 结构校验强制接入;
  • 引用经 NLI 蕴含校验,错位引用触发重生成而非静默展示;
  • groundedness 日跑抽样,与检索指标同面板分栏;
  • 多版本文档有生效日期与取代关系元数据,答案显式声明口径;
  • 拒答由检索分数触发,话术不编造、给出路,两种拒答错误分开统计。

本文缩写

缩写 音标 全拼 中文
NLI /ˌen el ˈaɪ/ Natural Language Inference 自然语言推理
UX /ˌjuː ˈeks/ User Experience 用户体验

参考资料