进阶阅读时长 8 分钟·

用 RAG 打造基于知识库的问答系统

大模型不知道你私有的、最新的资料,还容易产生幻觉。RAG(检索增强生成)通过"先检索、再生成",让模型基于可靠的知识库作答,是目前最实用的落地方案之一。

作者:AI Resource Hub

为什么需要 RAG

直接微调模型来注入知识成本高、更新慢。RAG 把知识放在外部数据库里,回答时临时检索相关片段喂给模型,既便宜又能随时更新。

构建流程概览

一个典型的 RAG 系统分两个阶段:离线的"索引阶段"把文档处理成可检索的向量;在线的"查询阶段"根据用户问题检索并生成答案。

文档切分与 Embedding

先把文档切成合适大小的片段(chunk),过大检索不精准,过小又丢失上下文,常见做法是几百 token 并保留一定重叠。

再用 Embedding 模型把每个片段转成向量,存入向量数据库(如 pgvector、Milvus、Qdrant 等)。

向量检索与 Top-K

用户提问时,把问题也转成向量,在数据库里做相似度检索,取出最相关的若干片段(Top-K)。

可以结合关键词检索做"混合检索",并用重排序(Rerank)进一步提升召回质量。

拼接上下文并生成答案

把检索到的片段拼进提示词,并要求模型"仅依据提供的资料回答,若资料中没有则如实说明",能显著减少幻觉。

进阶优化包括:查询改写、引用出处、控制上下文长度,以及对检索结果做去重与过滤。

选择向量数据库

向量数据库是 RAG 的核心。原型阶段用 Chroma 可以在内存中零配置运行;生产工作负载可考虑 Qdrant(Rust 编写)或 Pinecone(全托管),它们面向生产部署设计;具体限制与扩展方式请查阅各自文档;如果已用 PostgreSQL,pgvector 可避免新增基础设施;需要内置向量化模块和 GraphQL API 时 Weaviate 是亮点。

评估 RAG 质量

RAG 系统的好坏取决于检索和生成两端。用 Recall@K 衡量检索(正确的片段是否出现在 Top-K 结果中),用忠实度衡量生成(答案是否扎根于检索到的上下文),用相关性衡量(是否真正回答了问题)。RAGAS 等框架可以自动计算这些指标,方便你在迭代切分策略、Embedding 模型和提示词模板时持续追踪质量。

开始之前:前置条件

你需要一套规模不大但有代表性的文档、使用这些文档的权限,以及能提取干净文本的解析器(PDF 和扫描件往往需要 OCR)。选择一个能在你的环境中运行的 Embedding 模型和向量库,确认它们的向量维度和语言支持。准备 20–50 个真实问题,并为每个问题标注应当回答它的文档或段落。最后,确定文档如何更新、谁能读取哪个集合,因为权限必须在检索时执行,而不只是在界面上控制。

起步不需要很大的语料库。几百页结构良好的文档,就足以在扩大规模前验证整条流水线。

验证:先检查检索,再检查答案

先测检索。对每个已标注的问题执行搜索并检查 Top 结果:预期段落应出现在 Top-K 中,不相关的片段不应占满结果。如果不符合,先调整切分大小、重叠、元数据过滤或 Embedding 模型,再动生成提示词。然后检查答案:模型应引用或摘录检索到的段落,并在资料确实没有答案时明确说明。保留一个固定的评测集,每次改动后重跑,以便发现回归。还要测试:答案不存在的问题、两份相似文档的问题,以及你支持的每种语言各一个问题。

RAG 常见故障排查

按这个顺序排查。如果检索不到任何结果,检查文档是否已建立索引、查询与文档是否使用同一个 Embedding 模型和维度,以及过滤条件是否把集合排除了。如果正确片段存在但排名很低,检查切分边界,并尝试混合关键词检索或重排序。如果答案忽略检索到的上下文,确认片段确实进入了提示词且没有被截断,然后收紧指令:只依据提供的资料回答并引用出处。如果答案过时,确认文档更新后重建索引的任务确实运行了。如果重复片段占满 Top-K,在写入前按文档 ID 或内容哈希去重。最后,确认访问控制过滤在检索时执行,而不是生成之后。

什么情况下不该用 RAG

当整个知识库能轻松放进模型上下文窗口、且很少变化时,RAG 并不合适;直接把文档传进去更简单。当资料不可靠、互相矛盾,或大多是图片而没有可用文本时,效果也会很差。如果任务需要计算、事务或实时状态,应调用数据库或 API,而不是检索相关文字。对于只需要一次权威查询的问题,搜索索引或结构化查询可能更准确。如果没有人维护索引,检索系统会很快退化;一份更小、经过整理的文档可能对用户更有用。

切分这一步

这个函数解决索引阶段、嵌入之前的文档切分。它是起步草稿,不是实测结果。失败:overlap 为 0 时,正好落在切口上的事实两边都存不下,检索就拿不回来。

def chunk_text(text, size=400, overlap=50):
    chunks = []
    start = 0
    while start < len(text):
        chunks.append(text[start:start + size])
        if start + size >= len(text):
            break
        start += size - overlap
    return chunks
RAG向量数据库Embedding知识库

相关教程

相关评测