RAG 检索结果附带来源片段的组装方案
来源:17golang原创
时间:2026-10-17 11:13:44 407浏览 收藏
RAG 的检索结果不要直接拼成一段无标识的 prompt。更稳妥的做法是先把每个命中切片整理为统一记录:保留原文、来源文档、定位信息和检索分数;组装时再从同一批记录投影出模型上下文与前端引用。这样模型看到的是紧凑文本,读者看到的是可回溯的来源片段,两者不会因为排序或截断发生错位。
参考资料入口:https://huggingface.co/docs/transformers/main/chat_template_tools_and_documents
先把来源当成切片的一部分
一个切片至少要回答三个问题:它来自哪份文档、在文档中的什么位置、为什么被召回。只有正文文本而没有这些字段,后续即使生成了“参考来源”,也只能凭标题猜测,无法定位到真正的段落。
建议把内部记录设计成稳定的数据对象。source_id 用于跨切片归并,chunk_id 用于定位单个切片,locator 可以是页码、章节、段落编号或 URL 片段。score 只用于排序,不应被直接当成答案可信度。

只拼文本的旧写法为什么容易丢引用
最短的实现通常是遍历命中结果,把每个 text 用换行连接起来,再把连接后的字符串交给模型。这种写法能快速看到回答,却把标题、页码、URL 和切片边界一起丢掉了;当两个切片来自同一文档时,前端还可能展示两条重复来源。
另一个问题是排序与展示各做一遍。上下文层按分数截取前五段,展示层又按文档标题排序,模型回答中的“来源 2”就不一定对应界面上的第二条来源。解决方式不是让模型自己输出编号,而是由组装层先分配稳定的引用编号。
from dataclasses import dataclass
from html import escape
@dataclass
class ChunkRecord:
# 统一记录让上下文和引用共享同一份来源信息。
chunk_id: str
source_id: str
title: str
text: str
locator: str
score: float
def assemble_sources(hits: list[ChunkRecord], limit: int = 5) -> tuple[str, list[dict]]:
# 先按分数排序,再用 chunk_id 保证相同分数时结果稳定。
ordered = sorted(hits, key=lambda item: (-item.score, item.chunk_id))
chosen: list[ChunkRecord] = []
seen_sources: set[str] = set()
for hit in ordered:
# 同一文档优先保留最高分切片,避免引用列表重复占位。
if hit.source_id in seen_sources:
continue
chosen.append(hit)
seen_sources.add(hit.source_id)
if len(chosen) == limit:
break
context_parts: list[str] = []
citations: list[dict] = []
for index, hit in enumerate(chosen, start=1):
citation_id = f"S{index}"
# 编号由组装层产生,模型只需在回答中引用已有编号。
context_parts.append(f"[{citation_id}] {hit.title}\n{hit.text}")
citations.append({
"id": citation_id,
"source_id": hit.source_id,
"title": hit.title,
"locator": hit.locator,
"snippet": escape(hit.text[:180]),
})
return "\n\n".join(context_parts), citations
这段代码刻意把 context 和 citations 放在同一个循环里生成。实际项目中可以按业务需要保留同一文档的多个互补片段,但应让去重规则明确写在组装层,而不是交给模板或前端临时判断。
把一批记录投影成两种结果
检索记录是内部模型,模型上下文和界面引用则是两个不同的投影。上下文需要标题和片段正文,重点是短、清晰、能被模型识别;引用对象需要标题、来源标识、定位信息和短摘录,重点是能让读者回到原文。两者使用同一个 citation_id,但不要把内部所有字段都塞进 prompt。
如果使用支持文档列表的模型模板,可以把每条记录投影为包含标题和内容的文档对象;如果使用普通聊天模板,也可以把编号写进上下文文本。无论采用哪一种形式,编号都应由本地组装逻辑产生,不能依赖模型临时编造。

截断和缺失元数据要有明确规则
上下文预算不足时,应该先减少低分切片,再缩短单个片段,最后才考虑丢弃引用字段。不要先把所有正文截断后再重新编号,否则前端保存的摘录可能已经不是模型实际看到的内容。
缺少标题时可以使用文档 ID 作为临时显示名,缺少定位信息时显示“未提供定位”,但不要虚构页码或 URL。缺少 source_id 的记录不适合进入可回溯引用列表;可以保留在内部调试数据中,或在组装前过滤掉。
还要把“检索分数”和“来源可信度”分开。分数表示当前查询下的相似程度,来源可信度需要由文档级策略、更新时间、权限范围等业务信息决定。把二者混成一个百分比,很容易让用户误以为相似度就是事实正确率。
采用这套组装方式时记住三点
- 切片记录先完整,模型上下文和界面引用后投影。
- 去重、排序、截断和编号只在一个组装层决定。
- 缺失字段要诚实标记,不能为了好看补造定位或可信度。
常见问题
为什么不直接让模型输出来源链接?
模型可以根据上下文复述来源,但不适合负责生成真实链接和稳定编号。链接、定位和权限应来自检索记录,由应用层把模型引用编号映射回来源对象。
同一篇文档的多个切片是否一定要去重?
不一定。若多个切片分别包含定义和例外条件,可以都保留;去重的目标是避免相同来源无意义占位,而不是强行让每篇文档只出现一次。规则应根据上下文预算和引用展示空间决定。
引用摘录要不要和模型上下文完全一致?
核心事实应一致,但展示摘录可以更短,并保留足以定位的标题和位置。若上下文经过了二次裁剪,应该重新生成摘录,避免界面显示一段模型实际上没有读到的文字。
-
225 收藏
-
378 收藏
-
494 收藏
-
421 收藏
-
174 收藏
-
408 收藏
-
324 收藏
-
261 收藏
-
263 收藏
-
237 收藏
-
501 收藏
-
167 收藏
-
349 收藏
-
359 收藏
-
292 收藏
-
383 收藏
-
278 收藏
-
- 前端进阶之JavaScript设计模式
- 设计模式是开发人员在软件开发过程中面临一般问题时的解决方案,代表了最佳的实践。本课程的主打内容包括JS常见设计模式以及具体应用场景,打造一站式知识长龙服务,适合有JS基础的同学学习。
- 立即学习 543次学习
-
- GO语言核心编程课程
- 本课程采用真实案例,全面具体可落地,从理论到实践,一步一步将GO核心编程技术、编程思想、底层实现融会贯通,使学习者贴近时代脉搏,做IT互联网时代的弄潮儿。
- 立即学习 516次学习
-
- 简单聊聊mysql8与网络通信
- 如有问题加微信:Le-studyg;在课程中,我们将首先介绍MySQL8的新特性,包括性能优化、安全增强、新数据类型等,帮助学生快速熟悉MySQL8的最新功能。接着,我们将深入解析MySQL的网络通信机制,包括协议、连接管理、数据传输等,让
- 立即学习 500次学习
-
- JavaScript正则表达式基础与实战
- 在任何一门编程语言中,正则表达式,都是一项重要的知识,它提供了高效的字符串匹配与捕获机制,可以极大的简化程序设计。
- 立即学习 487次学习
-
- 从零制作响应式网站—Grid布局
- 本系列教程将展示从零制作一个假想的网络科技公司官网,分为导航,轮播,关于我们,成功案例,服务流程,团队介绍,数据部分,公司动态,底部信息等内容区块。网站整体采用CSSGrid布局,支持响应式,有流畅过渡和展现动画。
- 立即学习 485次学习