文章目录
引用溯源实战:让生成的每一句话都能点回原文
RAG 答案的可信度建立在三个动作上:检索要准、生成要有据、用户能验证。前两个是机器的事,第三个是产品的事——答案旁边带 [1][2] 编号,点一下跳到原文那段,用户自己判断模型有没有编。这件事不做,RAG 在用户眼里永远是"一本正经地胡说八道"。这篇文章讲引用溯源的完整链路:编号怎么注入、答案格式怎么约定、前端怎么渲染、以及点击定位的工程细节。
一、引用溯源不是锦上添花,是信任机制
先看没有引用的答案和有的答案,用户体感差距:
无引用:
"合同的违约金比例是 5%,争议解决地在上海。"
用户:……真的吗?我去翻合同吧。(信任成本转移到人)
有引用:
"合同的违约金比例是 5%[1],争议解决地在上海[2]。"
用户:点 [1] → 跳到合同第 3.2 条原文,确认。(信任闭环)
引用溯源解决三个层面的问题:用户验证(不用全信模型)、合规要求(金融/法律场景的硬性要求)、调试抓手(答错了能定位是检索问题还是生成问题)。企业 RAG 的验收标准里,"答案带引用"通常和"准确率"同级。
二、编号注入:context 的组织方式
溯源的第一步是给检索到的每个 chunk 一个稳定编号,编号跟着上下文走:
def build_cited_context(hits):
parts = []
citations = [] # 编号 → 原文映射,给前端用
for i, h in enumerate(hits, 1):
parts.append(f"[{i}] 来源:{h.source}({h.metadata.get('section','')})\n{h.text}")
citations.append({
"id": i,
"source": h.source,
"doc_id": h.doc_id,
"page": h.metadata.get("page"),
"url": h.metadata.get("url"),
"text": h.text,
})
return "\n\n".join(parts), citations
生成的 context 长这样:
[1] 来源:采购合同_v3.pdf(第 3.2 条 违约责任)
任何一方违约,应支付合同总额 5% 的违约金……
[2] 来源:采购合同_v3.pdf(第 8.1 条 争议解决)
因本合同引起的争议,提交上海仲裁委员会……
编号规范:从 1 开始连续、按相关性排序(top_k 越靠前编号越小)、不超过 10 个(再多用户看不过来,上下文也浪费)。
三、生成阶段:引用格式的约束
模型不会自动引用——必须在 prompt 里强约束格式,这是整个链路的核心 prompt 工程:
你是一个严谨的问答助手。基于以下资料回答问题。
【回答规则】
1. 每个事实性论断句末必须标注来源编号,格式如:违约金比例是 5%[1]
2. 编号必须引用下面提供的资料段落,禁止编造编号
3. 资料里没有的信息,回答"资料中未提及",不要用你自己的知识补充
4. 多个资料支持同一论断时,标注多个编号:[1][3]
5. 回答控制在 200 字以内,只回答问题本身
【资料】
{numbered_context}
【问题】
{question}
逐条说明为什么这么写:
- "每个事实性论断"必须标注——不这么写,模型只给开头一句带引用,后面放飞;
- 禁止编造编号——模型幻觉的重灾区,明确禁止 + 生成后校验(下一节);
- 没有就说没有——没有这条约束,模型会用训练知识"补全"答案,RAG 的意义就没了。"拒绝回答"是高质量 RAG 的标志,不是缺陷;
- 多编号支持——交叉验证的论断标多个,用户能看出"两条资料都这么说";
- 长度限制——引用标注会拉长答案,限制长度防止引用稀释信息密度。
四、生成后校验:最后一道防线
prompt 约束不是 100% 可靠,引用编号要程序化校验:
import re
def validate_citations(answer: str, n_sources: int) -> dict:
cited = set()
for m in re.finditer(r"\[(\d+)\]", answer):
cited.add(int(m.group(1)))
invalid = cited - set(range(1, n_sources + 1)) # 编造的编号
unused = set(range(1, n_sources + 1)) - cited # 给了但没引用的资料
return {
"valid": len(invalid) == 0,
"invalid_ids": sorted(invalid),
"unused_ids": sorted(unused),
"citation_count": len(cited),
}
处理策略:校验失败(有编造编号)→ 重新生成(temperature 略降)或剔除无效引用标记;连续失败 → 降级为"无引用模式 + 声明生成可能不准确"。
更强校验(引用-支撑关系核对):不只是编号存在,还要核对"这句话的内容真的出自那段资料"——用小模型/规则做句对段的匹配。成本高,只用于高风险场景(法律、医疗、金融答案的入库审查批处理),实时问答用编号存在性校验就够。
五、前端渲染:从文本到可点击
后端给前端的不是纯文本,是结构化数据:
{
"answer": "违约金比例是 5%[1],争议解决地为上海[2]。",
"citations": [
{"id": 1, "doc_title": "采购合同_v3.pdf", "page": 7,
"section": "3.2 违约责任", "url": "/docs/doc_123?page=7#sec-3.2",
"snippet": "任何一方违约,应支付合同总额 5% 的违约金……"},
{"id": 2, "doc_title": "采购合同_v3.pdf", "page": 12,
"section": "8.1 争议解决", "url": "/docs/doc_123?page=12#sec-8.1",
"snippet": "因本合同引起的争议,提交上海仲裁委员会……"}
]
}
前端渲染三件套:
- 答案里的编号变角标链接:
[1]→ 可点击的上标,hover 显示来源摘要(snippet 前 80 字); - 点击行为分两级:hover 显示摘要浮层(轻量预览),点击弹出抽屉展示完整原文段(全文 + 高亮关键句);
- 答案末尾附来源列表:“参考来源:[1] 采购合同_v3.pdf P7 · 3.2 违约责任”——给用户"这个答案有据可查"的整体感知。
移动端注意:角标 + 浮层 + 抽屉三级交互要简化成两级(点击直接弹底部 sheet),小屏放不下 hover 这种交互。
六、点击定位的工程细节
“[1] 点进去跳到原文”——跳转的精准度决定体验:
- 页码级定位:metadata 里有
page就跳 PDF 对应页(url?page=7),PDF.js 预览器直接定位; - 锚点级定位:解析时记了 section/chunk 的 DOM 锚点(
#sec-3.2),HTML 类文档(Confluence 导入的)直接滚到那段; - 关键词高亮:抽屉里打开原文时,用 chunk 里的关键词高亮——用户一眼看到"模型说的就是这段";
- 降级处理:原文已被删除/无权限 → 抽屉显示"原文已下线,以下为检索时快照"(chunk 文本本身还在,至少给看快照)。
快照归档:引用指向的 chunk 文本要随答案存档(答案生成那一刻的原文快照)——原文档后续更新了,答案和它的引用必须还是配套的。用"现在的原文"验证"过去的答案",会得到"模型当时引错了"的假象。
七、常见问题与解法
| 问题 | 原因 | 解法 |
|---|---|---|
| 模型不给引用 | prompt 没强约束 | 规则写细(见第三节),temperature 调低 |
| 引用编号编造 | 模型幻觉 | 生成后校验 + 重新生成 + 降级声明 |
| 引用位置漂移(标在句首句尾乱跳) | 格式自由度过高 | prompt 给定示例格式,post-process 归一化(挪到句号前) |
| 所有论断挤在一个编号 | 上下文组织不清 | 每个 chunk 用清晰分隔线,编号标注格式给示例 |
| 引用点击打开空白页 | 原文定位失败 | 锚点降级到页码级,再降级到快照 |
| 多轮对话后引用丢失 | 历史 answer 没存 citations | citations 随消息持久化,轮次展示都带 |
八、落地检查清单
- context 编号化:每个 chunk 连续编号 + 来源标注;
- prompt 五规则:逐句标注、禁止编造编号、没信息就说没有、多编号、限长;
- 生成后校验:编号存在性必查,句段支撑核对(高风险场景);
- 结构化输出:answer + citations[] 分离,citations 带 snippet/url/page;
- 前端三级交互:角标 → hover 摘要 → 点击抽屉全文 + 高亮;
- 定位精度:锚点级优先,页码级兜底,快照保底;
- 答案快照归档(answer + citation 的 chunk 快照绑定);
- 引用质量指标进监控:引用率(带引用的论断占比)、编号有效率。
九、小结
引用溯源的五句话:
- 编号跟着上下文走:chunk 从检索那刻起就有身份,答案和引用是一起生成的,不是后补的;
- prompt 五规则是核心:逐句标注、禁编编号、没信息就说没有、多编号支持、限长——引用质量八成取决于这段 prompt;
- 程序化校验兜底:编号存在性必查,高风险场景做句段支撑核对,防的是模型幻觉;
- 点击定位三层降级:锚点 → 页码 → 快照,任何一层失败用户都能看到东西;
- 快照归档防"穿越":原文更新后,旧答案的引用仍指向当时的 chunk 快照——否则每次文档更新都在制造假幻觉。
下一篇讲引用约束生成:怎么让模型"句句有据",把幻觉率再压一个量级。
本文系统阐述RAG中引用溯源的完整工程链路:从context编号注入、prompt强约束生成格式,到生成后编号存在性校验;从前端结构化渲染(角标/浮层/抽屉三级交互),到点击定位的锚点级、页码级与快照降级机制;强调快照归档防‘穿越’、引用质量监控等关键实践。核心目标是构建用户可验证、合规可审计、问题可追溯的信任型RAG答案。

666

被折叠的 条评论
为什么被折叠?



