第 9 章:上下文问答与引用溯源
能讲清 RAG 最后一环怎么把检索结果"翻译"成有据、可溯源的回答,会用 30 行代码实现"带引用 + 会拒答"的问答,并讲清引用溯源两条路线和幻觉检测
📊 学习时长:50-65 分钟 🎯 完成后能力:能讲清 RAG 最后一环怎么把检索结果"翻译"成有据、可溯源的回答,会用 30 行代码实现"带引用 + 会拒答"的问答,并讲清引用溯源两条路线和幻觉检测 🔗 关联面试题:5 道(覆盖 5 个不同角度,见 Part 3)
这一章你会学到什么
- ✓ 零基础也能跟着做:用一段零依赖代码,看到"有依据就带出处答、没依据就拒答"
- ✓ 看懂上下文问答:LLM 是基于检索到的资料作答,而不是凭记忆
- ✓ 学会 Prompt 怎么拼(仅依据资料 / 编号 / few-shot / 长度语气)
- ✓ 理解回答控制:拒答、防越权、多段融合
- ✓ 掌握引用溯源两条路线(Prompt 内生成 vs 后处理归因)及取舍
- ✓ 理解幻觉检测:答案每句话能不能在原文里找到支撑
本章按三段式组织,不同基础的读者各取所需:
段落 给谁看 内容 占比 🚀 Part 1 · 主线实战 零基础,想先跑起来 概念白话 + 带引用问答 demo ~45% 🎯 Part 2 · 面试深度 想讲透、备战面试 Prompt + 回答控制 + 溯源 + 幻觉检测 ~45% 🏆 Part 3 · 验收串题 检验学到位没 关联面试题 + 自检清单 ~10%
🚀 Part 1 · 主线实战 | ~45% 这部分零基础也能跟着做。先用一段不依赖任何库的代码,看到"答案怎么带上出处、没依据时怎么拒答"。
在开始之前
你需要:
- ✅ 装好 Python(3.9+),能看懂基础 Python(列表、字典、函数)
- ✅ 本章 demo 零依赖,直接跑
- ✅ 最好先读过第 3 章(chunk 的
section_path元数据)和第 5、6 章(检索 + 重排),这一章用的就是它们的产出
先看一个生成失败演练(这是本章的"为什么")
下面使用合成条款和问题说明生成环节的两类风险,不是客户项目或线上事故。检索、重排之后,系统还要把检索到的条款交给 LLM 组织答案:
- 它会编。 知识库没有依据时,模型仍可能生成看似确定的答案。
- 它说不清凭什么。 即使答案方向正确,如果给不出出处,使用者仍无法核验。
可以给生成环节加两道硬约束:① 仅依据检索到的资料回答,没有依据就拒答;② 为关键结论标出处 [编号],并能点回原文。这两件事,就是本章的主题:上下文问答和引用溯源。约束效果仍需用无答案集、引用正确率和人工复核验证。
一句话:引用与拒答让关键结论更容易核验。对部分受监管、需要审计或风险较高的场景,它们常是重要的风险控制手段;是否强制、覆盖哪些输出,应以适用法律、监管要求和业务制度为准。
这一章,我们就先用一段零依赖代码,把"带出处作答 + 没依据拒答"跑出来。
3 个核心概念(先白话,再术语)
概念 1:什么是"上下文问答"?
前 8 章都在为这一步做准备:解析、切分、检索、重排,最后捞回来一小撮最相关的片段。上下文问答(也叫"知识增强回答")就是这最后一环:LLM 拿着这些片段当"参考资料",生成对用户问题的回答。
关键词是**"基于上下文":模型应该照着检索到的资料答,而不是凭它自己记忆里那点东西瞎答**(那就退化成第 1 章的"闭卷考试"了)。
概念 2:什么是"引用溯源"?
引用溯源(Citation),就是让答案里的每个结论都标明"出自哪条资料",像论文的参考文献一样标 [1] [2],用户能顺着标号点回原文。
为什么值得做?增强可信(关键结论有证据)、支持审计(部分受监管或高风险场景需要留痕)、方便核查(用户能点开原文自己判断)、降低风险(发现证据不足时可以拒答或转人工)。具体场景是否要求引用、引用覆盖到什么粒度,应以适用规则和业务风险评估为准。
概念 3:什么是"拒答"?
拒答,就是当检索到的资料里没有相关内容时,老老实实说"我没找到",而不是硬编一个。
这听起来简单,却是防幻觉最有效的一招:没有依据,就别给答案。一个会拒答的系统,比一个"什么都敢答"的系统可信得多。尤其在保险、医疗、法律这类高风险场景,"答错"的代价远大于"答不上来",用户对"这个我没查到,建议您咨询人工"是能接受的,但对一个一本正经编出来的错误答案,可能直接照着做、然后出事。所以拒答不是系统能力差,而是一种主动的风险控制。
🛠️ 跟着做:30 行实现"带引用 + 会拒答"的问答
Step 1:写带引用的回答函数
新建 citation_demo.py(完整文件见 code-snippets/citation_demo.py,这里是核心):
def answer_with_citation(query, chunks):
"""有依据就照着答并标 [编号],没依据就拒答(绝不编)。"""
hits = retrieve(query, chunks) # 先检索(简化版;真实用第5、6章那套)
if not hits: # ① 没检索到相关片段
return "【依据不足】资料里没有相关内容,无法回答。", [] # → 拒答,而不是瞎编
# ② 有依据:把每条片段当作答案,并在句末标上它的来源编号 [id]
body = ";".join(f'{c["text"].rstrip("。")}[{c["id"]}]' for c in hits)
refs = [(c["id"], c["src"]) for c in hits] # 编号 → 来源,供前端映射回原文
return "答:" + body + "。", refs
Step 2:运行
python citation_demo.py
你应该看到(知识库固定,结果确定):
=== 有依据:照着答 + 标出处 ===
问:意外受伤的医疗费用报销吗?
答:本保险承保意外伤害导致的医疗费用,予以赔付[1]。
引用:[1] 第2条 保险责任
=== 库里没有:老实拒答(不编) ===
问:癌症能赔吗?
【依据不足】资料里没有相关内容,无法回答。
🐛 跑不通看这里
- 第一个问题也"依据不足"?检查
retrieve的threshold(默认 3),它要求问题和片段共享至少 3 个字才算命中,改了CHUNKS文本可能就够不到。- 第二个问题没拒答、反而编了答案?确认
threshold没被调到 0/1:阈值太低,什么都算"命中",就拒不了答了。KeyError: 'id'?确认CHUNKS每条都带id/src/text三个字段。
Step 3:看懂这组对照
- 有依据:"意外医疗费"命中第 2 条,于是照着条款答,并在句末标
[1],下面列出[1]的来源:答案可溯源; - 没依据:"癌症"在资料里没有,系统拒答而不是编一个看起来很像的答案,这就是防幻觉。
Step 4:🛠️ 动手实验,把阈值调成 0,看它开始"乱答"
把 retrieve(..., threshold=3) 改成 threshold=0,再跑"癌症能赔吗?":
- 你会看到它不再拒答,而是硬扯一条最沾边的条款当答案,这就是"什么都敢答"的系统在制造幻觉。
- 把阈值调回去,拒答就回来了。这一个参数,就是"敢不敢拒答"的开关。
亲手切一下,你就懂了:拒答不是模型"不行",而是工程上主动选择"没把握就不答"。
🧭 你刚才做的,等于真实系统的什么?
- 你的
answer_with_citation(照抄片段 + 标[id])= 真实系统里让 LLM 基于上下文生成 + 标引用那一步,只不过真实的是模型生成自然语言,不是直接照抄;- 你的"
threshold不够就拒答" = 生产里的拒答策略(没有足够依据就不答);- 你那个
[id] → src的映射 = 引用溯源里把标号映射回原文的环节,真实系统映射的是第 3 章给 chunk 存的section_path。 你手搓的是规则版;Part 2 讲真实系统怎么用 prompt 引导 LLM、怎么用 NLI 做更可靠的溯源和幻觉检测。
这一节你掌握了什么?
- ✅ 上下文问答是什么、为什么要"基于资料"而不是凭记忆
- ✅ 引用溯源是什么、哪些场景会把它作为重要的上线控制措施
- ✅ 亲手实现了"带出处作答 + 没依据拒答",看清了拒答怎么防幻觉
🎯 Part 2 · 面试深度 | ~45% 这部分讲透 Prompt 怎么拼、回答怎么控、引用溯源两条路线、幻觉怎么检测。
2.1 Prompt 怎么拼
上下文问答的 prompt,不是把片段一股脑塞进去就行,有讲究:
- 角色 + 硬约束:开头就讲清"你是保险客服,只能依据下面的资料回答",这是防幻觉的第一道闸;
- 检索片段带编号:每条片段标上
[1] [2],模型引用时才能对应得上,为溯源打底; - 输出要求:要求在引用处标
[编号]、找不到就说"资料中没有"; - few-shot 示例(可选):给一两个"根据资料回答 + 标来源"的范例,模型更容易照着做;
- 长度 / 语气控制:用 prompt 直接要求"三句话总结""面向没有保险知识的用户"。
业内行话:这里最值钱的一句约束是 "仅依据提供的资料回答,资料中没有就明确说没有"。它一句话同时干了两件事:奠定可溯源的基础(答案都来自给定片段)+ 挡住一部分幻觉(不许凭空发挥)。面试讲生成环节,先把这句 prompt 约束讲出来,就赢了一半。
2.2 回答控制:不只是"答",还要会"不答"和"取舍"
- 拒答 + 防越权:资料里没有就说"找不到",不编;遇到敏感、越权(比如让它给出不该给的承诺)的问题,直接拦截或给安全话术。
- 多段融合:检索回来多个片段时,要让模型汇总;片段相互矛盾时,倾向更可信/更相关的那条(工程上可以通过调整片段顺序、过滤低可信片段来影响模型)。
- 自检:生成后让模型(或一个小验证模型)检查"有没有没出处的断言",有就补一句"该部分资料中未提及"。
业内行话:防幻觉是两道闸:生成前用 prompt 严格约束"仅依据资料",生成后做风控过滤(规则/分类模型拦截违规、不可信内容)。只靠 prompt 不够,因为模型偶尔还是会"自信地越界";只靠后处理也不够,因为有些幻觉很像真的。两道一起上,才稳。
2.3 引用溯源的两条路线
怎么让答案带上出处?业内两条主流路线:
- 方法 1:Prompt 内生成,让 LLM 边答边标
[1] [2]。优点:一个 prompt 就能完成。缺点:模型可能漏标或标错编号,而且它编出来的内容也可能带一个看似合理的来源,挡不住幻觉。遗漏率必须在自己的评估集上测量。 - 方法 2:后处理式归因,先让模型生成答案,再用算法逐句找出处:对答案里每句话,用 NLI(自然语言推理)模型验证"检索到的原文是否蕴含这句话"。优点:更可靠,能发现"没出处的句子",顺带把幻觉揪出来。缺点:多一道模型,复杂、慢一点。
工程提示:两条路线可以组合:先让 LLM 在生成时标,再用后处理或 NLI 检查证据是否真的支撑结论。面试如果只答"让大模型自己标",还要能解释标错、漏标和伪引用怎样检测。是否需要后处理,应由你的引用正确率和失败样本决定。
2.4 幻觉检测与处理
引用溯源的方法 2,本质上也是一套幻觉检测机制:
- 检测:把答案拆成一句句,逐句问"检索到的原文,能不能支撑(蕴含)这句话?"(用 NLI 模型或规则)。能支撑 → 标上出处;找不到支撑 → 大概率是模型编的。
- 处理:对找不到支撑的句子,可以删掉 / 标"存疑" / 触发拒答 / 回退重新检索,而不是原样吐给用户。
业内行话:可以把"关键事实是否有证据支撑"设为一条保守的验证目标。找不到支撑时,先把该句标为证据不足、需要复核并触发删除、拒答、转人工或重新检索;它是幻觉风险信号,但不能只凭一次未匹配就断言内容一定是编造的。
2.5 引用怎么展示:接回第 3 章的元数据
最后一步,是把 [1] 这个标号,变成用户能点开、能核对的出处:
用户点 [1],系统就把它映射回那条 chunk 的元数据,还记得第 3 章给每个 chunk 存的 section_path 吗?(如 第2条 保险责任):展示来源文件、章节定位、原文片段,甚至高亮跳转到原文。
业内行话:这一章最爽的地方,是前几章埋的伏笔在这里全收口了:第 3 章给 chunk 存
section_path,当时只说"用于答案溯源",现在你看到它怎么变成用户能点开核对的"出处"。面试时如果能把"切分时存元数据 → 检索时带着走 → 生成时映射成引用"这条全链路讲通,面试官会知道你不是只懂单点,而是有系统观。
🏆 Part 3 · 验收串题 | ~10% 学到这一步,做几道题验证一下,知道自己学到位没。
关联面试题(5 道,覆盖 5 个角度)
- 【上下文拼 Prompt】 生成阶段如何基于检索文档和用户查询,把上下文整合进 Prompt 来引导大模型作答?
- 【检索对了生成还差】 检索已返回相关文档,生成仍不理想,可能的原因有哪些(提示工程/上下文建模/微调)?
- 【RAG 缓解幻觉】 RAG 如何通过引入外部知识缓解幻觉?实现机制和效果边界是什么?
- 【输出真实性验证/溯源】 生成后有哪些后处理技术验证回答真实性(事实核查、引用溯源、一致性检测)?
- 【生成控制】 生成流程里人工干预与控制机制能起什么作用?怎么用反馈/规则影响准确性与合规性?
学完这一章你应该能干嘛
跟着做(Part 1):
- 能跑通"带引用 + 会拒答"的问答 demo,讲清拒答怎么防幻觉
- 能说清上下文问答为什么要"基于资料",引用溯源为什么重要
讲深度(Part 2):
- 能讲清生成 Prompt 的关键约束(仅依据资料 / 编号 / few-shot)
- 能讲清回答控制的拒答、防越权、多段融合,以及防幻觉的两道闸
- 能讲清引用溯源两条路线、怎样测引用正确率,以及什么时候需要组合
- 能讲清幻觉检测的核心思想(每句话能否回指原文)
4 项以下 → 回去 Part 1 重做;4–5 项 → Part 2 再看一遍;6 项以上 → 进入下一章。
完整代码 + 数据集
本书每章的完整可运行代码 + 测试样本,都在配套 GitHub 仓库:
🔗 github.com/MisterBooo/rag-from-zero
- 跟着教程 clone 下来就能跑
- 本章的"带引用 + 会拒答"问答 demo(零依赖)也在里面
- 欢迎 Star ⭐ / Issue 反馈
导航