引用与可溯源
状态:✅ 已补齐(2026-09-12)
一句话定义:让回答 带出处 (citation),并尽量保证陈述可被检索片段支撑(groundedness);支撑不足则拒答或降级。
大纲
已有相关文档(先读这些)
学习要点(读后应能回答)
一、引用 UX:从角标到原文
Citation /saɪˈteɪʃən/ (Citation,引用)是信任的基础设施:小北的制度问答一旦答错,用户要能追到原文核验,否则「不敢用」本身就是最大的点踩来源。小北上线前的点踩归因里,41% 是「无出处不敢采信」——比答错还多。
引用 UX /ˌjuː ˈeks/ (User Experience,用户体验)分三档,价值递增:
| 档位 | 形态 | 用户能做什么 | 小北实现 |
|---|---|---|---|
| 最低 | 答案末尾列「来源:xxx 文档」 | 知道大概出处 | 不够——无法定位到具体条款 |
| 中等 | 句内角标 [1],悬停显示引用块原文 |
就地核对 | 悬停卡显示块原文 + 文档名 |
| 最高 | 角标点击跳转原文并高亮 | 全文语境核验 | Markdown 跳锚点;PDF 跳页码 |
跳转能力完全依赖 01 篇 埋好的元数据:每块的 docId、markdownBreadcrumbs(锚点路径)、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 块的编号——引用存在且位置合理,内容却是编的。两层校验兜底:
- 结构校验(必做,零成本):每个
citesID 必须存在于本次检索结果集内——编造 ID 直接过滤或重生成; - 语义校验(关键):句子与其所引块做 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 版差旅制度是常态。靠检索层元数据加权解决:块带 effectiveDate 与 superseded(被取代)字段,检索时新版本 × 1.3 加权、已取代版本直接过滤;答案生成时显式声明版本口径(「按 2024 版差旅制度」),让用户知道依据。
4.2 无命中判定
拒答的触发信号是检索质量而非模型自信度:重排 top-1 分数 < 阈值(小北取 0.35,用 04 篇 的标注集标定)或所有召回块 NLI 非蕴含 → 进入拒答分支。把「拒不拒」交给检索分数,把「怎么拒」交给产品话术,是两个解耦的决策。
4.3 无命中的话术设计
这是文首第二个学习问题。拒答话术三原则:
- 不编造替代品:禁止「相关内容是…」硬凑一块擦边的——那是把检索失败转成引用幻觉;
- 给出路:明确说「知识库未覆盖」,附转人工入口或提问建议(「试试换个说法 / 联系 HR」);
- 保留透明度:说明「已检索 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 | 用户体验 |
参考资料
- Enabling Large Language Models to Generate Text with Citations(Liu 等,2023,引用生成)
- RARR: Researching and Revising What Language Models Say(Gao 等,2022,归因与修订)
- Evaluating the Factuality of LLMs (FActScore)(Min 等,2023,事实分解评测)
- 幻觉机理与缓解 · 本仓库
评论
评论加载中…