RAG 的最小闭环:检索、证据、回答
拆解知识检索与生成两条链路,用离线关键词检索示例理解出处、无答案与权限过滤。
画出最小 RAG 链路,并区分没找到资料和有资料但回答错误。
RAG 的核心,是先找到与问题有关的资料,再让模型基于这些资料回答。它把外部知识接入生成过程,但不会自动消除幻觉,也不会自动解决资料权限。
把检索和生成拆开后,你才知道问题出在哪里:是资料没找到,还是模型没有正确使用已经找到的证据。
两条链路,一个闭环
离线链路负责文档导入、解析、切块、元数据与索引更新。在线链路负责接收问题、限制可访问资料、召回、排序、组装证据和生成回答。
先保留原始来源、文档版本和块位置。回答中的引用需要能回到具体证据,而不只是一个无法定位的大文件。
对于多用户或多项目的数据,应在检索过程中施加权限边界。不能先把所有资料交给模型,再要求模型自己忽略不该看到的内容。
最小实现不一定需要向量库
学习阶段可以先用关键词匹配观察流程。它不等于语义检索,也不适合复杂中文分词,但能帮助你分清输入、可用资料与检索结果。
下面使用人工构造的英文句子,以空格分词。代码不会调用模型,重点是先过滤权限,再挑选有匹配词的证据。
documents = [
{"id": "public-cache", "scope": "public", "text": "cache reduces repeated computation"},
{"id": "public-retry", "scope": "public", "text": "retry needs an idempotent operation"},
{"id": "private-note", "scope": "private", "text": "cache configuration is restricted"},
]
def retrieve(question, allowed_scopes):
query = set(question.lower().split())
candidates = []
for doc in documents:
if doc["scope"] not in allowed_scopes:
continue
score = len(query & set(doc["text"].lower().split()))
if score:
candidates.append((score, doc["id"]))
return [doc_id for _, doc_id in sorted(candidates, reverse=True)]
assert retrieve("cache", {"public"}) == ["public-cache"]
assert retrieve("payment", {"public"}) == []
print(retrieve("retry operation", {"public"}))
这里的可访问范围由可信程序提供,不来自模型生成的参数。真实项目还需要与当前用户和资源授权系统绑定。
证据不足时,允许没有答案
召回了一段文本,不意味着它回答了问题。相似度高也不是事实成立的证明。需要检查资料是否涉及问题、是否足够新、是否能支持具体结论。
没有足够证据时,返回明确的“资料不足”,或询问必要信息。不要用训练记忆填补来源空缺,然后把资料列表附在后面当作引用。
引用也应贴近被支持的陈述。用户应该能看出哪条证据支持哪句话。
保留可定位的中间结果
为每次请求保留召回文档 ID、排序结果和最终使用的来源 ID。默认不记录完整用户问题和资料正文,必要的调试样本通过单独的受控流程处理。
如果答案错误,先检查正确文档是否进入候选集,再检查它是否被送进上下文,最后检查生成是否忠于证据。跳过这些步骤直接换提示词,容易掩盖真正的问题。
下一步
用三份公开说明搭建一个小资料集。准备一个有明确答案的问题,一个没有答案的问题,以及一个没有访问权限的问题。
先验证检索和出处,不急着接生成。再进入下一篇,观察切块怎样改变检索结果和引用完整性。
资料来源与延伸阅读
资料核验:2026-11-10。本文为原创工程讲解,教学示例不代表线上实测。