25 引用来源:让 RAG 答案可以核对和追溯
引用来源:让 RAG 答案可以核对和追溯
引用来源的核心是让答案中的事实能够回到具体文档、页码、标题和 chunk。模型负责生成回答,来源列表必须由后端根据真实检索结果控制。
[TOC]
1. 本节目标
基础 RAG 已经能够:
检索相关chunks
把chunks交给大模型
生成自然语言答案
如果接口只返回 answer,会遇到几个问题:
用户无法核对答案
模型可能编造文档名或页码
开发无法快速定位答案依据
错误答案很难判断是检索问题还是生成问题
本节要增加:
稳定的上下文编号
结构化sources
答案中的引用标记
引用合法性校验
来源去重和排序
来源点击时的再次鉴权
完成本节后,应能够:
设计Source数据结构
从检索结果生成来源编号
把来源编号加入RAG上下文
限制模型只能引用已有编号
由后端组装和校验sources
处理一条答案对应多个来源
处理无答案和低相关结果
设计前端来源展示方式
避免来源接口泄露无权限内容
2. 引用来源完整流程
graph TD;
A["检索得到 TopK chunks"] --> B["保留文档与定位 metadata"]; B --> C["按去重结果分配 C1、C2、C3"];
C --> D["构造带编号的参考资料"];
D --> E["大模型生成带引用标记的答案"];
E --> F["解析答案中的引用编号"];
F --> G["校验编号是否属于本次检索结果"];
G --> H["后端组装 sources"]; H --> I["返回 answer + sources"];
关键边界:
模型可以选择引用哪个上下文编号
模型不能创建新的文档、页码或chunk
sources的真实数据只能来自后端检索结果
3. 为什么来源不能完全交给模型
如果只在 Prompt 中说“请输出来源”,模型可能生成:
不存在的文档名
错误页码
未检索到的来源编号
看似合理但没有依据的链接
原因是聊天模型输出的是生成内容,不是数据库事实查询。
正确职责划分:
检索服务:提供真实chunks和metadata
上下文构建器:分配本次请求的来源编号
聊天模型:根据上下文生成答案,可使用编号
后端:校验编号并组装真实sources
前端:展示并跳转来源
4. Source 数据结构
推荐结构:
{
"sourceId": "C1", "documentId": "doc_001", "documentName": "产品手册.pdf",
"chunkId": "chunk_003", "chunkIndex": 3, "titlePath": "文件管理 > 上传限制",
"pageStart": 8, "pageEnd": 8, "snippet": "单个上传文件最大为 20MB。",
"score": 0.89}
字段说明:
| 字段 | 用途 |
|---|---|
sourceId |
本次请求中的稳定引用编号 |
documentId |
后端定位文档,不一定直接展示 |
documentName |
用户可识别的文件名 |
chunkId |
后端定位证据片段 |
chunkIndex |
片段在文档中的顺序 |
titlePath |
Markdown、Word 等结构化标题路径 |
pageStart/pageEnd |
PDF 等分页文档定位 |
snippet |
经过长度控制的证据摘要 |
score |
检索或重排分数,是否展示由产品决定 |
第一版至少需要:
sourceId
documentId
documentName
chunkId
snippet
如果解析阶段能够提取页码和标题,应该一起保留。
5. 来源编号设计
推荐使用本次请求内部编号:
C1
C2
C3
不要直接让模型使用:
数据库UUID
文件存储路径
对象存储内部Key
租户ID
服务器绝对路径
编号生成规则:
先完成权限过滤
再完成去重和最终上下文选择
最后按上下文顺序从C1开始编号
同一个请求内编号必须稳定,响应结束后不要求跨请求稳定。
跨请求稳定定位依赖 documentId + chunkId,不是 C1。
6. 带来源编号的上下文
推荐格式:
[C1]
文档:产品手册.pdf
位置:文件管理 > 上传限制,第 8 页
内容:单个上传文件最大为 20MB。
[C2]
文档:接口说明.md
位置:上传接口 > 错误码
内容:超过大小限制时返回 ERR_FILE_TOO_LARGE。
Prompt 规则:
1. 只能依据参考资料回答。
2. 引用资料时使用 [C1]、[C2] 这样的编号。
3. 只能使用参考资料中真实存在的编号。
4. 不要自行生成文档名、页码或来源编号。
5. 资料不足时回答“根据当前知识库资料无法确定”。
示例答案:
单个上传文件最大为 20MB [C1]。超过限制时,接口返回
ERR_FILE_TOO_LARGE [C2]。
7. 后端来源映射
上下文构建时保存映射:
C1 -> RetrievedChunk A
C2 -> RetrievedChunk B
C3 -> RetrievedChunk C
模型返回答案后,从答案中提取:
[C1]
[C2]
然后只从映射中取真实数据:
answer引用C1 -> 返回C1对应Source
answer引用C2 -> 返回C2对应Source
answer引用C9 -> C9不存在,视为非法引用
模型返回的文档名不能覆盖数据库中的真实文档名。
通用引用提取正则可以从简单形式开始:
\[C(\d+)\]
需要注意:
同一个编号可能出现多次
编号顺序可能与上下文顺序不同
模型可能输出全角符号
模型可能没有输出任何编号
模型可能引用不存在的编号
第一版可以统一要求半角 [C1] 格式。
8. 引用校验
8.1 编号合法性
答案中的每个编号必须存在于本次contextMap
非法编号处理:
从答案中移除非法标记
记录citation_invalid指标
不为非法编号构造source
必要时重新生成一次答案
8.2 答案是否有来源
有事实性回答却没有任何引用时,可以:
接受答案但标记citationMissing
重新提示模型补充引用
使用结构化输出强制返回引用编号
在严格场景直接判定生成失败
第一版建议记录问题,不必无限重试。
8.3 来源是否支持答案
编号合法不等于引用正确。
例如:
答案说最大20MB
C1只说明支持PDF上传,没有容量信息
这属于引用与陈述不一致。
可以通过以下方式检查:
人工评测
规则检查关键数字和专有名词
模型评审答案陈述是否被来源支持
固定评测集验证citation faithfulness
9. 来源去重
多个 chunk 可能来自同一文档,甚至内容高度重叠。
去重可以分两层:
9.1 Chunk 级去重
按chunkId去重
同一chunk只返回一次
9.2 文档级聚合
如果 UI 不需要展示每个片段,可以按文档聚合:
{
"documentId": "doc_001", "documentName": "产品手册.pdf",
"locations": [ {"pageStart": 8, "pageEnd": 8}, {"pageStart": 10, "pageEnd": 10} ]}
但后端仍应保留 chunk 级映射,方便审计和调试。
第一版建议响应保留 chunk 级来源,前端再按文档分组展示。
10. Snippet 设计
snippet 是用户核对答案时看到的短证据。
不要返回整个长 chunk。
可以采用:
最多200到500字符
优先保留命中句附近内容
去除多余空白
不改变原文事实
超长时使用省略号
如果关键词命中位置已知,可以截取:
命中位置前80字符
命中内容
命中位置后120字符
向量检索没有明确关键词位置时,可以使用 chunk 开头,或者让应用按句子选择最相关片段。
不要让聊天模型重新改写 snippet,因为 snippet 应尽量保持原文证据。
11. 无答案时的来源
如果资料不足,推荐:
{
"answer": "根据当前知识库资料无法确定。",
"answered": false, "sources": []}
不要为了展示“系统检索过”而把低相关结果当作可靠来源返回给用户。
调试模式可以单独返回:
debugCandidates
但它不等于用户可见的 sources,并且必须受到权限和环境控制。
12. 响应结构
{
"answer": "单个上传文件最大为 20MB [C1]。",
"answered": true, "sources": [ { "sourceId": "C1", "documentId": "doc_001", "documentName": "产品手册.pdf",
"chunkId": "chunk_003", "chunkIndex": 3, "titlePath": "文件管理 > 上传限制",
"pageStart": 8, "pageEnd": 8, "snippet": "单个上传文件最大为 20MB。",
"score": 0.89 } ], "traceId": "trace_xxx"}
建议 sources 顺序:
优先按答案中第一次出现的引用顺序
没有正文引用时按上下文排序
同一个sourceId只返回一次
13. 前端展示
常见展示方式:
答案正文中显示[C1]
答案下方显示来源列表
点击[C1]滚动到对应来源
点击来源打开文档预览
有页码时跳转到指定页
有标题路径时展开对应章节
来源卡片可以显示:
文档名称
页码或标题
证据片段
更新时间
相似度分数主要用于调试,不一定适合直接展示给普通用户。用户通常无法理解 0.82 到底代表多可靠。
14. 来源跳转与权限
用户点击来源时,后端必须再次鉴权:
graph TD;
A["用户点击来源"] --> B["提交 documentId 和 chunkId"]; B --> C["后端读取当前登录身份"];
C --> D["重新校验租户和文档权限"];
D --> E{"是否允许访问?"};
E -- "否" --> F["返回 403"]; E -- "是" --> G["返回原文片段或文档预览"];
不能因为来源曾经出现在回答里,就永久允许访问。
权限可能发生变化:
用户被移出部门
文档从公开改为私有
知识库授权被撤销
会话链接被转发给其他用户
来源 URL 不应包含服务器文件路径,也不应通过可猜测地址绕过后端权限检查。
15. 结构化输出
为了减少引用格式错误,可以让模型返回结构化结果:
{
"answer": "单个上传文件最大为20MB。",
"citationIds": ["C1"]}
后端再渲染:
单个上传文件最大为20MB [C1]。
优点:
容易解析
容易校验非法编号
答案正文与来源列表分离
方便不同前端展示
但仍要进行后端校验,不能因为 JSON 格式正确就相信编号一定合法。
16. 日志与审计
建议记录:
traceId
回答使用的chunkId
上下文编号映射
模型返回的citationIds
最终返回的sourceIds
非法引用数量
缺失引用数量
不要把完整敏感正文重复写入日志。
可以记录摘要或 Hash:
chunkId
contentHash
documentId
这样既能回放来源链路,也能减少敏感内容扩散。
17. 测试设计
17.1 单来源答案
答案只依赖一个chunk
期望:正文引用C1
期望:sources只有C1
17.2 多来源答案
一个来源给出容量限制
另一个来源给出错误码
期望:两句话分别引用正确来源
17.3 重复来源
模型多次引用C1
期望:sources列表只出现一次C1
17.4 非法编号
上下文只有C1和C2
模型输出C9
期望:C9不进入sources并记录异常
17.5 没有来源
检索无可靠结果
期望:拒答
期望:sources为空
17.6 权限变化
生成答案后撤销文档权限
再次点击来源
期望:返回403
18. 常见问题
18.1 每句话都要引用吗
不一定。
事实性陈述应有来源。过渡句、总结句可以共享前后来源,但要避免让用户误解证据范围。
18.2 来源越多越好吗
不是。
过多来源会增加噪声。只返回真正支持答案的来源。
18.3 能不能只返回文档名
可以作为最小版本,但定位能力较弱。
最好同时返回 chunk、页码或标题路径。
18.4 来源可以证明答案一定正确吗
不能。
来源只能提高可核验性。仍可能出现模型理解错误、文档过期或引用与陈述不一致。
18.5 为什么不用检索结果全部作为 sources
检索候选不一定都被答案采用。
sources 应表示最终答案实际引用的证据,而不是全部调试候选。
19. 练习清单
设计Source响应结构
给最终上下文分配C1、C2编号
让模型输出来源编号
从答案或结构化结果中提取citationIds
校验非法和重复编号
由后端组装真实sources
为snippet设置长度限制
完成无答案sources为空的处理
完成来源点击二次鉴权
测试单来源、多来源、非法来源和权限变化
20. 小结


可信引用链路:
retrieved chunks
-> context IDs -> model citations -> backend validation -> structured sources -> authorized source view
本节最重要的结论:
引用编号可以由模型选择,但来源事实必须由后端掌控。任何文档名、页码和 chunk 定位都必须能够回到真实检索数据。