← RAG 与知识检索 · 第 1 篇

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。本文为原创工程讲解,教学示例不代表线上实测。