现在位置: 首页 > AI 入门教程 > 正文

RAG 检索增强生成

你有没有遇到过这样的场景:

  • 你有一份几百页的产品手册,用户问了一个很具体的问题,你翻了半天也找不到答案在哪。

  • 你公司积累了好几年的内部文档、会议纪要、技术规范,新员工想了解某项业务规则,根本不知道从哪查起。

  • 你有一堆电子书、研究报告、论文,想让 AI 基于这些内容回答问题,但直接把整本书贴给 AI,上下文窗口装不下。

这就是 RAG 要解决的问题。

RAG(Retrieval-Augmented Generation,检索增强生成)是一种让大语言模型基于特定知识库回答问题的技术

RAG 的核心思路很简单:先从你的知识库中找出最相关的内容,然后把这些内容和用户的问题一起送给 LLM,让 LLM 基于这些上下文来回答。

没有 RAG,LLM 只知道它训练数据里的内容;有了 RAG,LLM 就能用上你私有的、最新的数据。


为什么需要 RAG

RAG 不是解决这些问题的唯一方案,但它是目前最实用、成本最低的方案之一。

LLM 知识截止日期问题

  • 所有 LLM 都有一个知识截止日期——训练数据只到某个时间点,之后发生的事它不知道。

  • 比如 GPT-4 的知识截止到 2023 年 10 月,你问它 2024 年发生的新闻,它只能说我不知道。

  • 更重要的是,你的私有数据(内部文档、产品手册、公司规范)从来就没出现在 LLM 的训练数据里,它怎么可能知道?

私有数据无法直接输入 LLM

你可能会想:那我直接把文档贴给 LLM 不行吗?

答案是:短文档可以,长文档不行。

  • 首先,LLM 的上下文窗口是有限的。比如 GPT-3.5 只有 16K tokens,大概是 1 万字左右。一份几百页的手册,根本贴不下。

  • 其次,就算上下文窗口够大,把整份文档都塞进去,效果也不好。LLM 容易迷失在长文本中,找不到真正相关的信息。

  • 最后,成本问题。GPT-4 128K 上下文的价格是输入每百万 tokens 10 美元,每次都把整本书塞进去,钱包会哭的。

RAG vs 微调:如何选择

另一个让 LLM 用上新数据的方法是微调(Fine-tuning)——用新数据继续训练模型。

但微调和 RAG 有本质区别,适用场景完全不同:

对比项RAG微调
数据新鲜度随时更新,立即可用需要重新训练,周期长
适用数据量非常大(百万级文档)中等(几千到几万样本)
引用来源可以显示答案来自哪篇文档无法追溯来源
幻觉问题通过提供上下文减少幻觉仍可能产生幻觉
修改知识直接删除或更新文档即可需要重新训练,难以"遗忘"
技术门槛较低,用现有框架即可较高,需要 GPU 和训练经验
成本主要是向量数据库和 Embedding训练成本高,推理成本也高

一个简单的判断原则:如果是让 LLM "知道"某些事实类知识(比如产品说明、公司规范),用 RAG;如果是让 LLM "学会"某种风格或能力(比如写作风格、代码规范),用微调。


RAG 架构全貌

RAG 分为两个阶段:离线索引阶段和在线检索阶段。

先看一张完整的架构图:

RAG 整体架构流程图

离线索引阶段(数据准备)

这个阶段是在后台运行的,用户不会直接看到。

它的任务是把你的文档处理成可以快速检索的格式。

步骤如下:

  • 1. 文档加载:读取 PDF、TXT、DOCX、网页等各种格式的文档。

  • 2. 文档切分:把长文档分割成小的文本块(Chunk),通常是几百到一千个词。

  • 3. 向量化:用 Embedding 模型把每个文本块转换成向量(一串数字)。

  • 4. 存储:把向量和原始文本一起存入向量数据库。

这个阶段只需要做一次,或者当文档更新时重新运行。

在线检索阶段(用户查询)

这个阶段是用户提问时实时发生的。

步骤如下:

  • 1. 问题向量化:用同一个 Embedding 模型把用户的问题也转换成向量。

  • 2. 相似度检索:在向量数据库中找出和问题向量最接近的几个文本块。

  • 3. 构建 Prompt:把检索到的文本块作为上下文,和用户的问题一起组装成 Prompt。

  • 4. LLM 生成:把 Prompt 送给 LLM,让它基于上下文回答。

  • 5. 返回答案:把 LLM 的回答返回给用户,通常还会附上引用的文档来源。


向量数据库入门

向量数据库是 RAG 的核心组件之一。要理解它,先得理解什么是向量。

什么是向量(Embedding)

向量(Embedding)是把文本转换成的一串数字。

比如"猫"这个词,可能被转换成 [0.23, -0.45, 0.12, 0.89, ...] 这样一个几百维的向量。

关键在于:语义相近的文本,它们的向量在空间中也会离得很近

比如:

  • "猫"和"猫咪"的向量距离很近
  • "猫"和"狗"的向量距离比和"汽车"近
  • "我喜欢猫"和"我爱猫咪"的向量距离很近

这就是我们能做"语义搜索"的基础——不是按关键词匹配,而是按意思匹配。

语义相似度的计算原理

两个向量的相似度通常用"余弦相似度"(Cosine Similarity)来计算。

余弦相似度的范围是 -1 到 1:

  • 1 表示完全相同
  • 0 表示不相关
  • -1 表示完全相反

实际应用中,我们通常只关心正相似度,越接近 1 表示越相关。

当你问一个问题时,向量数据库会快速计算问题向量和所有文档向量的相似度,返回最相关的前几个。

主流向量数据库

市面上有很多向量数据库,各有特点:

数据库类型特点适用场景
Chroma本地/开源轻量、易用、Python 友好原型开发、小规模应用
Pinecone云服务/SaaS托管服务、无需运维、弹性扩展生产环境、大规模应用
Weaviate开源/托管功能丰富、支持 GraphQL需要高级功能的场景
Qdrant开源/托管高性能、Rust 编写性能要求高的场景
Milvus开源/托管功能全面、企业级大规模企业应用
FAISS本地库Facebook 出品、极快不需要持久化的场景

对于初学者,我们推荐从 Chroma 开始——它安装简单,不需要额外配置,非常适合学习和原型开发。


文档处理流程

文档处理是 RAG 中很容易被忽视但实际上非常重要的一环。

你的检索效果好不好,很大程度上取决于文档处理得好不好。

支持的文档格式

LangChain 支持很多文档格式:

  • 纯文本:.txt, .md
  • PDF:.pdf
  • Office:.docx, .pptx, .xlsx
  • 网页:HTML, URL
  • 代码:.py, .js, .java 等
  • JSON:.json, .jsonl
  • CSV:.csv

每种格式都有对应的加载器,使用起来很方便。

文档切分策略

文档切分(Chunking)是把长文档切成小块的过程。

这看起来简单,其实有很多讲究:

  • 切得太小:可能丢失上下文,一个完整的意思被切断了
  • 切得太大:可能包含不相关信息,浪费上下文窗口

常见的切分策略:

策略原理适用场景
按字符数简单按字数切分,比如每 500 个字符一块简单场景、对上下文要求不高
按段落按换行符切分,尽量保持段落完整格式规范的文档
递归切分优先按段落,太长再按句子,最后按词大多数场景的首选
语义切分根据语义相似度切分,保证语义完整对语义要求高的场景

大多数情况下,用 LangChain 的 RecursiveCharacterTextSplitter 就够了。

切分大小的选择技巧

切分大小(Chunk Size)是一个需要根据场景调整的参数。

一些参考原则:

  • 问答场景:500-1000 个字符,刚好能包含一个完整的问答对
  • 摘要场景:1000-2000 个字符,需要更多上下文
  • 代码场景:可以按函数或类切分,保持代码块完整

还有一个重要参数:重叠(Overlap)——相邻两个文本块之间重叠的字符数。

重叠可以防止重要信息被切分在两个块里,通常设为切分大小的 10%-20%。

不要盲目追求"最优"切分参数。先从 500 字符、50 重叠开始,实际测试效果,然后再调整。


嵌入模型(Embedding Model)

嵌入模型是把文本转换成向量的模型。

它的质量直接影响检索效果——好的 Embedding 模型能准确理解语义,把相关的文档排到前面。

什么是 Embedding 模型

Embedding 模型是一个预训练的神经网络,输入是文本,输出是向量。

它的训练目标是:让语义相近的文本,输出的向量也相近。

比如:

  • "如何学习 Python" 和 "Python 学习方法" → 向量接近
  • "猫咪很可爱" 和 "小狗很可爱" → 向量有一定距离但也比较近
  • "如何学习 Python" 和 "猫咪很可爱" → 向量距离很远

OpenAI text-embedding-3 系列

OpenAI 提供了目前最流行的 Embedding 模型:

模型维度价格(每百万 tokens)特点
text-embedding-3-small1536$0.02性价比高,大多数场景够用
text-embedding-3-large3072$0.13效果更好,适合高精度需求

text-embedding-3-small 是目前的首选——它比上一代 text-embedding-ada-002 效果更好,价格更便宜。

开源 Embedding 模型

如果你不想用 OpenAI,或者有数据隐私要求,也有很多开源选择:

模型维度特点
sentence-transformers/all-MiniLM-L6-v2384轻量、速度快、效果不错
sentence-transformers/all-mpnet-base-v2768效果更好,但也更慢更大
BAAI/bge-large-zh-v1.51024专门针对中文优化
Alibaba-NLP/gte-large-zh1024阿里出品,中文效果好

对于中文场景,推荐用 BAAI/bge 系列或 Alibaba-NLP/gte 系列——它们专门针对中文做了优化,比通用模型效果更好。


检索策略

检索不是"找出最相关的文档"这么简单——不同的策略适合不同的场景。

向量相似度检索

这是最常用的检索方式:计算问题向量和所有文档向量的相似度,返回最相关的前 K 个。

优点:能理解语义,同义词、近义词都能匹配。

缺点:对精确术语、专有名词可能不如关键词检索准确。

比如用户问"runoob 的 Python 教程",向量检索能理解"教程"和"课程"是类似的,但如果用户问的是"runoob Python3.10 文档",可能关键词搜索更准确。

关键词检索(BM25)

这是传统搜索引擎用的方法:匹配关键词,根据词频、逆文档频率等打分。

  • 优点:精确匹配术语、产品名、型号等效果好。

  • 缺点:无法理解同义词、语义关系。

比如用户问如何学习编程,关键词检索只能找到包含编程的文档,找不到写着写代码但意思一样的文档。

混合检索(Hybrid Search)

既然两种方法各有优劣,为什么不一起用呢?

混合检索就是同时做向量检索和关键词检索,然后把结果合并、重新排序。

这样既可以利用语义理解,又能保证精确匹配。

很多向量数据库都支持混合检索,比如 Weaviate、Pinecone、Qdrant 等。

如果拿不准用哪种,选混合检索——大多数情况下,它效果最好。


生成阶段优化

检索到相关文档只是第一步,如何让 LLM 基于这些文档生成好的回答,也有很多技巧。

Prompt 模板设计

Prompt 模板是 RAG 中影响输出质量的关键因素。

一个好的 RAG Prompt 通常包含:

  • 角色设定:告诉 LLM 它是什么角色
  • 上下文:检索到的文档内容
  • 任务说明:让 LLM 做什么
  • 约束条件:比如"只基于上下文回答"、"如果不知道就说不知道"
  • 输出格式:希望 LLM 怎么回答

一个经典的模板:

实例

# 一个典型的 RAG Prompt 模板
RAG_PROMPT_TEMPLATE = """你是一个专业的 {role}。
请基于以下上下文回答用户的问题。如果上下文中没有相关信息,请说"抱歉,我没有找到相关信息"。

上下文:
{context}

用户问题:{question}

请用简洁、准确的语言回答:"""

如何减少幻觉

幻觉(Hallucination)是 LLM 编造事实的现象——它会说得有模有样,但全是假的。

RAG 能减少幻觉,但不能完全消除。

一些减少幻觉的技巧:

  • 在 Prompt 中强调"只基于上下文回答"
  • 让 LLM 在回答中引用原文片段
  • 如果检索结果不相关,让 LLM 说"我不知道"
  • 用多个 LLM 交叉验证答案
  • 让 LLM 先从上下文中找出相关内容,再回答

引用来源标注

让用户知道答案是从哪里来的,这很重要——不仅增加可信度,也方便用户查证。

常见做法:

  • 在回答中标注来源,比如"根据文档 A 第 2 页"
  • 在回答末尾列出参考文档的标题、链接
  • 让用户点击可以跳转到原文位置

LangChain 的一些高级框架(比如 LlamaIndex)可以自动帮你做这些。


实战:基于 LangChain 搭建文档问答系统

说了这么多,我们来动手做一个完整的 RAG 系统。

我们将用 LangChain + Chroma + OpenAI 来搭建一个文档问答系统。

环境准备

先安装需要的库:

pip install langchain langchain-openai langchain-chroma python-dotenv

然后在项目目录下创建一个 .env 文件,填入你的 OpenAI API Key:

OPENAI_API_KEY=你的-api-key-在这里

完整代码实现

这是一个完整的 RAG 系统,包含索引创建和查询功能:

实例

# ============================================
# RAG 文档问答系统完整实现
# ============================================

import os
from dotenv import load_dotenv
from langchain.text_splitter import RecursiveCharacterTextSplitter
from langchain_openai import OpenAIEmbeddings
from langchain_chroma import Chroma
from langchain_openai import ChatOpenAI
from langchain.chains import RetrievalQA
from langchain.prompts import PromptTemplate
from langchain.schema import Document


def load_documents_from_directory(directory: str) -> list[Document]:
    """从目录加载文档
    这里为了演示,我们手动创建一些文档
    实际项目中可以用 DirectoryLoader 加载真实文件
    """

    documents = [
        Document(
            page_content="""RUNOOB 教程网站创立于 2013 年,由菜鸟教程团队创建。
网站的目标是为编程初学者提供简单易懂的编程教程。
我们提供了 HTML、CSS、JavaScript、Python、Java、C++ 等多种编程语言的教程。"""
,
            metadata={"source": "runoob_intro.txt", "page": 1}
        ),
        Document(
            page_content="""RUNOOB 的 Python 教程涵盖了 Python 的所有重要知识点。
包括:Python 基础语法、数据类型、控制语句、函数、模块、文件操作等。
教程中提供了大量的代码示例,每个示例都经过精心设计和测试。
我们还提供了在线 Python 编辑器,你可以直接在浏览器中运行代码。"""
,
            metadata={"source": "python_tutorial.txt", "page": 1}
        ),
        Document(
            page_content="""RUNOOB 教程的特点是内容简洁、示例丰富、注重实践。
我们相信"学习编程的最好方式就是写代码"。
因此每个知识点都配有可运行的代码示例,你可以边看边练。
我们的教程完全免费,任何人都可以访问学习。"""
,
            metadata={"source": "features.txt", "page": 1}
        ),
        Document(
            page_content="""除了文字教程,RUNOOB 还提供了视频教程。
视频教程更加直观,适合视觉学习型的用户。
我们的视频教程覆盖了 Python、Web 开发、数据分析等热门主题。
你可以在 B 站和 YouTube 上找到我们的视频。"""
,
            metadata={"source": "video_course.txt", "page": 1}
        ),
    ]
    print(f"已加载 {len(documents)} 个文档")
    return documents


def split_documents(documents: list[Document], chunk_size: int = 500, chunk_overlap: int = 50) -> list[Document]:
    """切分文档"""
    text_splitter = RecursiveCharacterTextSplitter(
        chunk_size=chunk_size,
        chunk_overlap=chunk_overlap,
        length_function=len,
        separators=["\n\n", "\n", "。", "!", "?", ",", " ", ""],
    )
    splits = text_splitter.split_documents(documents)
    print(f"已将 {len(documents)} 个文档切分为 {len(splits)} 个文本块")
    return splits


def create_vector_store(splits: list[Document], persist_directory: str = "./chroma_db") -> Chroma:
    """创建向量数据库"""
    embeddings = OpenAIEmbeddings(model="text-embedding-3-small")
    vector_store = Chroma.from_documents(
        documents=splits,
        embedding=embeddings,
        persist_directory=persist_directory,
    )
    print(f"向量数据库已创建,共 {vector_store._collection.count()} 条记录")
    return vector_store


def load_vector_store(persist_directory: str = "./chroma_db") -> Chroma:
    """加载已有的向量数据库"""
    embeddings = OpenAIEmbeddings(model="text-embedding-3-small")
    vector_store = Chroma(
        persist_directory=persist_directory,
        embedding_function=embeddings,
    )
    print(f"向量数据库已加载,共 {vector_store._collection.count()} 条记录")
    return vector_store


def create_rag_chain(vector_store: Chroma):
    """创建 RAG 问答链"""

    # 自定义 Prompt 模板
    template = """你是一个专业的 RUNOOB 客服助手。
请基于以下上下文回答用户的问题。如果上下文中没有相关信息,请说"抱歉,我没有找到相关信息"。
回答要简洁明了,使用中文。

上下文:
{context}

用户问题:{question}

回答:"""


    prompt = PromptTemplate(
        template=template,
        input_variables=["context", "question"],
    )

    llm = ChatOpenAI(model="gpt-3.5-turbo", temperature=0)

    # 创建检索链
    rag_chain = RetrievalQA.from_chain_type(
        llm=llm,
        chain_type="stuff",
        retriever=vector_store.as_retriever(search_kwargs={"k": 3}),
        chain_type_kwargs={"prompt": prompt},
        return_source_documents=True,
    )

    return rag_chain


def main():
    """主函数"""
    # 加载环境变量
    load_dotenv()

    # 检查数据库是否已存在
    db_exists = os.path.exists("./chroma_db") and os.path.isdir("./chroma_db")

    if db_exists:
        print("发现已存在的数据库,正在加载...")
        vector_store = load_vector_store()
    else:
        print("创建新的数据库...")
        # 1. 加载文档
        docs = load_documents_from_directory("./data")
        # 2. 切分文档
        splits = split_documents(docs)
        # 3. 创建向量数据库
        vector_store = create_vector_store(splits)

    # 4. 创建 RAG 链
    rag_chain = create_rag_chain(vector_store)

    # 5. 测试查询
    test_questions = [
        "RUNOOB 是什么时候创立的?",
        "RUNOOB 的 Python 教程有什么特点?",
        "RUNOOB 有视频教程吗?",
        "如何在 RUNOOB 上学习 C++?",
        "RUNOOB 的教程需要付费吗?",
    ]

    for question in test_questions:
        print(f"\n{'='*60}")
        print(f"问题:{question}")
        print(f"{'='*60}")

        result = rag_chain.invoke({"query": question})
        answer = result["result"]
        sources = result["source_documents"]

        print(f"回答:{answer}")
        print("\n参考来源:")
        for i, doc in enumerate(sources, 1):
            print(f"  [{i}] {doc.metadata['source']} (第 {doc.metadata['page']} 页)")
            print(f"      片段:{doc.page_content[:50]}...")


if __name__ == "__main__":
    main()

运行这个程序,你会看到类似这样的输出:

创建新的数据库...
已加载 4 个文档
已将 4 个文档切分为 4 个文本块
向量数据库已创建,共 4 条记录

============================================================
问题:RUNOOB 是什么时候创立的?
============================================================
回答:RUNOOB 教程网站创立于 2013 年。

参考来源:
  [1] runoob_intro.txt (第 1 页)
      片段:RUNOOB 教程网站创立于 2013 年,由菜鸟教程团队创建。...
  [2] features.txt (第 1 页)
      片段:除了文字教程,RUNOOB 还提供了视频教程。...

============================================================
问题:RUNOOB 的 Python 教程有什么特点?
============================================================
回答:RUNOOB 的 Python 教程涵盖了 Python 的所有重要知识点,包括基础语法、数据类型、控制语句、函数、模块、文件操作等,并且提供了大量经过精心设计和测试的代码示例,还提供了在线 Python 编辑器,可以直接在浏览器中运行代码。

参考来源:
  [1] python_tutorial.txt (第 1 页)
      片段:RUNOOB 的 Python 教程涵盖了 Python 的所有重要知识点。...
  [2] features.txt (第 1 页)
      片段:RUNOOB 教程的特点是内容简洁、示例丰富、注重实践。...

恭喜!你已经成功搭建了一个完整的 RAG 文档问答系统。


进阶优化:Re-ranking、HyDE

基础 RAG 已经能用,但如果你想让效果更好,可以试试这些进阶优化方法。

Re-ranking(重排序)

向量检索返回的结果是按相似度排序的,但相似度高不一定意味着最适合回答问题。

Re-ranking 就是:先用向量检索返回前 K 个结果(比如 20 个),然后用一个更精确的模型对这些结果重新排序,选最好的几个送给 LLM。

常用的 Re-ranking 模型:

  • cross-encoder/ms-marco-MiniLM-L-6-v2
  • BAAI/bge-reranker-large

虽然多了一步,但效果提升很明显,值得一做。

HyDE(Hypothetical Document Embedding)

HyDE 是一个很巧妙的想法:先用 LLM 为用户的问题生成一个"假设的回答",然后用这个假设的回答去检索。

为什么这样有效?因为问题通常很短,信息密度低;而假设的回答更长,更像文档的风格,和真实文档的相似度更高。

比如:

  • 用户问题:"RUNOOB 什么时候创立的?"
  • 假设回答:"RUNOOB 教程网站创立于 2013 年,由菜鸟教程团队创建,旨在为编程初学者提供优质教程。"

用假设的回答去检索,往往能找到更相关的文档。