从零理解检索增强生成(Retrieval-Augmented Generation)

标签:RAG · LLM · Embedding · ChromaDB · Python · AI应用开发

前言

大语言模型(LLM)虽然强大,但有一个致命弱点:它的知识是静态的,来自训练数据,无法获取最新信息,也无法直接访问私有文档库。当你问它"我们公司昨天的会议纪要是什么?制度是什么?",它一无所知。

RAG(Retrieval-Augmented Generation,检索增强生成)正是为解决这一问题而生。它的核心思路是:先检索,再生成。把"回答问题"这件事拆成两步:

  • 第一步:从知识库中检索与问题最相关的内容片段
  • 第二步:把检索到的内容拼进 Prompt,让 LLM 基于这些内容生成回答

 一句话理解RAG:给大模型配上"外部记忆",让它能随时查阅资料再回答你的问题,而不是凭空捏造。

本文是 RAG 系列的第一课,带你从零理解 RAG 的完整流程、核心概念、向量数据库选型,以及如何用 Python 实现一个最小可用的 RAG 系统。

一、RAG 整体流程

RAG 的完整流程分为两个阶段:索引阶段(预处理)和检索-生成阶段(在线处理)。

1.1 索引阶段(离线 / 一次性)

这一阶段是对知识库文档的预处理,需要提前完成:

  1. 文档分块(Chunking):将长文档切割成若干小块(Chunk),每块大小由 chunk_size 决定,相邻块之间有 overlap(重叠)约 10%,防止语义在块边界断裂。
  2. 向量化(Embedding):用 Embedding 模型将每个 Chunk 转换为高维数值向量(如 1024 维)。这个向量代表了该文本的语义信息。
  3. 存储到向量数据库:将所有 Chunk 的向量和原文存入向量数据库(如 ChromaDB),供后续检索使用。

关键参数: • chunk_size:每块文本的长度(字符数或 Token 数) • overlap:相邻块之间的重叠长度,通常为 chunk_size 的 10%

1.2 检索-生成阶段(在线 / 每次问答)

用户提问后,系统实时执行以下步骤:

  1. 问题向量化:将用户的问题也用同一个 Embedding 模型转为向量。
  2. 相似度检索:在向量数据库中,用余弦相似度计算问题向量与所有 Chunk 向量的距离,找出最相似的 Top-K 个 Chunk。
  3. 拼接 Prompt:将检索到的 Chunk 原文和用户问题组合成 Prompt,发送给 LLM。
  4. LLM 生成回答:LLM 基于 Prompt 中的参考内容,生成最终回答。

相似度计算公式(余弦相似度): cos(θ) = (A·B) / (|A| × |B|) 值域 [-1, 1],越接近 1 越相似,越接近 -1 越相反。

二、核心概念详解

2.1 Embedding(文本向量化)

Embedding 是 RAG 的灵魂。它的作用是把自然语言映射到高维向量空间,使得语义相近的文本,在向量空间中距离也相近。

例如,"苹果手机很好用" 和 "iPhone 使用体验不错" 虽然字面不同,但 Embedding 后的向量会非常接近;而 "苹果手机很好用" 和 "今天天气不错" 的向量则差距较大。

向量通常包含几百到几千个数字,例如 BAAI/bge-m3 模型输出 1024 维向量,即一段文本被表示为 1024 个浮点数组成的列表:[0.1, 0.2, 0.5, …]

本课使用的 Embedding 模型:BAAI/bge-m3 特点:中英双语,语义理解强,适合 RAG 场景,通过 SiliconFlow API 免费调用。

2.2 文档分块策略

分块是 RAG 中容易被忽视但非常关键的步骤。分块质量直接影响检索效果。常见策略包括:

  • 固定长度分块:按字符数或 Token 数均匀切割,简单直接,适合均匀文本。
  • 递归字符分割(LangChain 的 RecursiveCharacterTextSplitter):优先按段落、句子等自然边界分割,语义保留更好。
  • 语义分块(Semantic Chunking):用 Embedding 判断语义边界,精细度高但计算开销大,适合质量要求高的场景。

2.3 向量数据库

向量数据库专为高效存储和检索高维向量设计,支持语义近邻搜索(ANN)和语义距离计算。以下是常见选项对比:

名称

类型

易用性

适用场景

费用

ChromaDB

嵌入式本地

✅ 极易

适合Demo/原型

开源免费

Milvus

开源分布式

⚡ 中等

生产级大规模

开源,需部署

Pinecone

全托管云服务

✅ 简单

快速上线,无运维

按量付费

Weaviate

开源混合检索

⚡ 中等

语义+关键词混合

开源免费

本课使用 ChromaDB,它可以完全在本地运行,零配置,非常适合入门 Demo 和原型开发。

2.4 相似度检索

检索时系统需要在数百万甚至更多的向量中快速找到最相似的几条,主要有两种度量方式:

  • 余弦相似度(Cosine Similarity):衡量向量方向的夹角,值域 [-1, 1],与向量长度无关,适合文本语义匹配。
  • 欧氏距离(L2 Distance):衡量向量在空间中的绝对距离,适合图像等需要考虑量级的场景。

RAG 场景中,余弦相似度是最常用的选择。

三、代码实战:最小可用 RAG 系统

3.1 环境配置

本课使用 SiliconFlow API,国内可直接访问,无需翻墙,提供 DeepSeek-V3 和 BAAI/bge-m3 的免费额度。(本节主要看get_embedding函数的笔记)

pip install requests chromadb

3.2 基础配置

import os, sys, io, json, requests

# ===== 基础配置 =====

API_KEY   = "sk-xxx"                        # 你的 SiliconFlow API Key

BASE_URL  = "https://api.siliconflow.cn/v1" # API 地址(兼容 OpenAI 格式)

CHAT_MODEL  = "deepseek-ai/DeepSeek-V3"     # 大语言模型名称

EMBED_MODEL = "BAAI/bge-m3"                 # Embedding 模型名称

# ===== 路径配置 =====

BASE_DIR   = os.path.dirname(os.path.abspath(__file__))  # 当前文件所在目录

DATA_DIR   = os.path.join(BASE_DIR, "data")              # 文档存放目录

CHROMA_DIR = os.path.join(BASE_DIR, "chroma_db")         # 向量数据库存储目录

3.3 Embedding 函数实现

Embedding 函数负责将文本列表转换为向量列表。实现原理:向 Embedding API 发送 POST 请求,解析返回的 JSON 数据,提取每条文本对应的向量数组。

def get_embedding(texts: list[str]) -> list[list[float]]:

    """

    将文本列表转为向量列表

    :param texts: 需要向量化的文本列表

    :return: 向量列表,每个向量是 float 数组

    """

    resp = requests.post(

        f"{BASE_URL}/embeddings",    # 专门负责向量化的端点

        headers={

            "Authorization": f"Bearer {API_KEY}",  # 鉴权(相当于门禁)

            "Content-Type": "application/json"      # 内容格式声明

        },

        json={

            "model": EMBED_MODEL,           # 用作向量化的模型

            "input": texts,                 # 需要转换的文本列表

            "encoding_format": "float"      # 返回 float 格式的向量

        },

        timeout=30  # 超时 30 秒

    )

    # HTTP 状态码:200=正常,404=Not Found,500=服务器错误

    if resp.status_code != 200:

        raise Exception(f"Embedding API 调用失败:{resp.text}")

    data = resp.json()  # 解析返回的 JSON,存入 data

    # 提取 data 里面的数字向量并返回

    # 例如 data = {"data": [{"embedding": [0.1,0.2,...],"index":0}, ...]}

    # 用列表推导式把每个 item["embedding"] 提取出来

    return [item["embedding"] for item in data["data"]]

3.4 文档索引(离线构建)

import chromadb

from chromadb.utils import embedding_functions

def build_index(data_dir: str, chroma_dir: str):

    """读取文档,分块,向量化,存入 ChromaDB"""

    # 初始化 ChromaDB(本地持久化)

    client = chromadb.PersistentClient(path=chroma_dir)

    collection = client.get_or_create_collection(

        name="rag_docs",

        metadata={"hnsw:space": "cosine"}  # 使用余弦相似度

    )

    # 读取 data 目录中的所有 .txt 文件

    docs = []

    for fname in os.listdir(data_dir):

        if fname.endswith(".txt"):

            fpath = os.path.join(data_dir, fname)

            with open(fpath, "r", encoding="utf-8") as f:

                docs.append(f.read())

    # 简单分块(固定长度,带 overlap)

    chunk_size = 500

    overlap    = 50

    chunks = []

    for doc in docs:

        for i in range(0, len(doc), chunk_size - overlap):

            chunk = doc[i: i + chunk_size]

            if chunk.strip():

                chunks.append(chunk)

    # 批量向量化

    print(f"共 {len(chunks)} 个 Chunk,开始向量化...")

    embeddings = get_embedding(chunks)

    # 存入向量数据库

    collection.upsert(

        ids=[f"chunk_{i}" for i in range(len(chunks))],

        documents=chunks,

        embeddings=embeddings

    )

    print("索引构建完成!")

    return collection

3.5 检索 + 生成(在线问答)

def rag_query(question: str, collection, top_k: int = 3) -> str:

    """

    RAG 问答:向量检索 + LLM 生成

    :param question: 用户问题

    :param collection: ChromaDB 集合

    :param top_k: 返回最相似的 Top-K 个 Chunk

    :return: LLM 回答

    """

    # Step 1:将问题向量化

    q_embedding = get_embedding([question])[0]

    # Step 2:在向量数据库中检索最相似的 Chunk

    results = collection.query(

        query_embeddings=[q_embedding],

        n_results=top_k

    )

    retrieved_chunks = results["documents"][0]  # 取第一个查询的结果

    # Step 3:拼接 Prompt

    context = "\n\n---\n\n".join(retrieved_chunks)

    prompt = f"""你是一个知识库助手。请仅根据以下检索到的内容回答用户问题。

如果检索内容中没有相关信息,请回答"null",不要自行编造。

【检索到的内容】

{context}

【用户问题】

{question}

【回答】"""

    # Step 4:调用 LLM 生成回答

    resp = requests.post(

        f"{BASE_URL}/chat/completions",

        headers={"Authorization": f"Bearer {API_KEY}", "Content-Type": "application/json"},

        json={

            "model": CHAT_MODEL,

            "messages": [{"role": "user", "content": prompt}],

            "temperature": 0.3  # 降低 temperature 减少幻觉

        },

        timeout=60

    )

    if resp.status_code != 200:

        raise Exception(f"Chat API 调用失败:{resp.text}")

    return resp.json()["choices"][0]["message"]["content"]

# ===== 主程序 =====

if __name__ == "__main__":

    import chromadb

    client     = chromadb.PersistentClient(path=CHROMA_DIR)

    collection = client.get_or_create_collection(

        name="rag_docs", metadata={"hnsw:space": "cosine"}

    )

    # 首次运行时构建索引

    # build_index(DATA_DIR, CHROMA_DIR)

    # 开始问答

    while True:

        q = input("请输入问题(输入 q 退出):").strip()

        if q.lower() == "q":

            break

        answer = rag_query(q, collection)

        print(f"\n回答:{answer}\n")

四、常见问题与调优指南

4.1 检索不到相关内容

症状:明明文档里有答案,但 RAG 就是检索不到。

解决方案:

  1. 调整 chunk_size 和 overlap:块太大语义模糊,块太小上下文不足,建议从 300~500 字符开始调整。
  2. 换更好的 Embedding 模型(换 E 模型):模型的语义理解能力直接影响检索质量,如尝试 BAAI/bge-large-zh-v1.5。
  3. 加 Rerank 重排序:用 Reranker 模型对 Top-K 结果再次精排,进一步提升召回精度。

4.2 回答与检索内容无关

症状:检索到了正确内容,但 LLM 的回答答非所问。

解决方案:

  1. 优化 Prompt 约束规则:明确告诉 LLM "仅根据检索内容回答",禁止自行推断。
  2. 降低 temperature(如 0.1~0.3):减少模型的随机性和"创造力"。
  3. 增加 few-shot 示例:在 Prompt 中提供几个"检索内容→答案"的示例对,引导模型理解任务格式。

4.3 模型产生幻觉

症状:LLM 回答了问题,但答案是编造的,而非基于检索内容。

解决方案:

  1. 在 Prompt 中明确指令:"只基于检索内容回答,检索不到时回答 null"。
  2. 后处理验证:将 LLM 回答与检索到的 Chunk 做相似度比对,低于阈值时拒绝输出。
  3. 降低 temperature:同上,减少随机输出。

最佳实践:将这三个优化结合使用,可以显著提升 RAG 系统的可靠性。

五、高级分块策略简介

基础的固定长度分块适合入门,但在生产环境中,以下更高级的策略可以显著提升质量:

1. 固定分块(Fixed-size Chunking):最简单,按字符数/Token 数均等切割。适合格式均匀的文档,实现成本低。

2. 递归字符分割(Recursive Character TextSplitter):LangChain 提供,优先按段落 → 句子 → 单词顺序递归切割,尽量保留自然语义边界,是目前最常用的生产级方案。

3. 语义分块(Semantic Chunking):用 Embedding 模型计算相邻句子间的语义距离,当语义距离突然增大时在此处切割。精细度最高,但计算开销大,适合高质量文档处理。

六、总结与下一步

本文覆盖了 RAG 基础第一课的完整内容,包括:

  • RAG 的整体架构(索引阶段 + 检索-生成阶段)
  • 核心概念:Embedding、文档分块、向量数据库、余弦相似度
  • 向量数据库选型:ChromaDB / Milvus / Pinecone / Weaviate
  • 完整代码实现:get_embedding → build_index → rag_query
  • 常见问题与调优:检索不到、回答跑偏、模型幻觉的解法

后续计划:

  • 继续分享手写笔记
  • LangChain 快速搭建 RAG Pipeline
  • Rerank 重排序与混合检索实战
  • RAG 评估指标与 RAGAS 框架
Logo

AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。

更多推荐