📚 RAG从入门到精通

检索增强生成完整指南

📚
什么是RAG?
Retrieval-Augmented Generation

RAG = Retrieval-Augmented Generation(检索增强生成),一句话概括:先从知识库里"找资料",再让 LLM 结合这些资料生成回答。

典型流程:用户提问 → 系统把问题转成向量 → 在向量数据库里检索相似文档块 → 把"问题 + 相关文档"一起丢给 LLM → LLM 基于资料生成答案。

价值在于:1) LLM 的"记忆"有限(上下文长度有限,知识可能过时);2) RAG 能把企业/私有知识库"外挂"给 LLM,不需要重新训练模型;3) 目前企业落地大模型,RAG 是最主流、最可控的方案。

为什么需要RAG?
┌─────────────────────────────────────────────────────────────────┐ │ │ │ RAG = 检索 + 生成,让AI基于知识库回答问题 │ │ │ │ ┌──────────────┐ ┌──────────────────┐ ┌──────────────┐ │ │ │ 用户问题 │───▶│ 检索相关文档 │───▶│ LLM生成 │ │ │ └──────────────┘ └──────────────────┘ └──────────────┘ │ │ │ └─────────────────────────────────────────────────────────────────┘
问题 传统LLM RAG
知识时效性固定动态更新
幻觉问题可能产生基于事实
私有数据无法访问可集成
答案来源不可追溯可引用

🔄 RAG完整流程

文档加载
文本分块
向量嵌入
存入向量库
检索匹配
LLM生成

📋 RAG 完整工作流(程序员视角)

1. 数据准备阶段
  • 加载文档(PDF、HTML、Markdown、数据库、API)
  • 文档切分成 nodes/chunks
  • 每个 chunk 通过 Embedding 模型转为向量
  • 将 chunk_id, text, vector, metadata 写入向量数据库
2. 在线服务阶段
  • 用户问题 → 文本 → Embedding
  • 在向量数据库中做相似度检索,取出 top_k 个相关 chunk
  • 把这些 chunk 拼成提示词模板发给 LLM - 这是RAG最关键的步骤,需要精心设计提示词结构
  • LLM 生成最终答案
📝 提示词模板实例:
# 基础RAG提示词模板 基于以下上下文信息回答用户的问题。如果上下文不包含相关信息,请说明你不知道。 上下文: {context} 用户问题: {question} 要求: 1. 基于上下文信息回答,不要编造信息 2. 如果上下文中有多个相关信息,请综合回答 3. 如果无法从上下文中找到答案,请诚实说明 4. 回答要简洁明了 回答: # 带引用的高级模板 你是一个专业的问答助手,请基于以下提供的文档片段回答问题。 参考文档: {document_chunks} 用户查询: {query} 指令: 1. 仔细分析每个文档片段与问题的相关性 2. 只使用提供的文档信息,不要添加外部知识 3. 在回答中注明信息来源(如:根据文档1第3段...) 4. 如果信息不足,请说明需要哪些额外信息 思考过程: 首先,我会分析每个文档片段... 最终答案:
关键要点:
  • 使用 {context}{question} 等占位符动态填充
  • 明确指示LLM只使用提供的上下文
  • 添加格式要求(如引用来源、结构化输出)
  • 设计"思考链"让LLM展示推理过程
  • 处理"不知道"的情况,避免幻觉

🧩 核心组件

📄 文档加载器

PDFLoader, TextLoader, DocxLoader

✂️ 文本分割器

RecursiveCharacterTextSplitter

📐 向量嵌入

OpenAIEmbeddings, HuggingFaceEmbeddings

🗄️ 向量数据库

FAISS, Chroma, Milvus, Pinecone

🔍 检索器

相似度检索, MMR, 混合检索

🤖 LLM

GPT-4, Claude, 开源模型

🔧 核心三要素详解

📄 文档切分(Chunking)

为什么需要切分? 文档太长会超出上下文窗口,检索时太粗粒度会带进大量无关噪音。

  • 固定长度切分:按字符/Token 数切,比如 512/1024 token 一块
  • 按语义结构切:按段落、章节、HTML 结构、Markdown 标题层级切
  • 滑动窗口:chunk 之间有重叠,避免关键信息被切在边界外
程序员要掌握:根据文档类型选择切分策略,保留元数据(文件名、页码、章节等)方便过滤和溯源。

📐 Embedding(向量化表示)

Embedding 是把文本变成向量,让计算机能衡量\"语义相似度\"。

  • 同义句在向量空间距离近,不同主题的句子距离远
  • 使用 OpenAI text-embedding-3、bge、m3e 等模型
  • 问题本身也被同样模型 Embedding,再去向量库里找最近邻
程序员要掌握:不同 Embedding 模型的特点(多语言支持、长文本支持、维度大小),成本与性能权衡。

🗄️ 向量数据库(Vector Store)

专门存\"文本 + 向量\"的数据库,支持高效的向量相似度检索。

  • 常见产品:Pinecone、Weaviate、Qdrant、Milvus、Chroma、pgvector
  • 核心能力:插入、查询、过滤
  • 检索参数:top_k、距离度量(cosine/dot product/l2)、阈值设定
程序员要掌握:如何选型(云托管 vs 自托管、QPS、延迟、成本),如何设计集合/索引。

🗄️ 向量数据库对比

数据库 部署方式 规模 特点
FAISS本地中小免费、快速
Chroma本地/云中小轻量、易用
Milvus本地/云分布式、高性能
Pinecone云服务超大全托管
Qdrant本地/云Rust、高性能
Weaviate本地/云GraphQL接口、模块化
pgvectorPostgreSQL扩展中小与现有PostgreSQL集成

🔍 检索策略

基本检索

  • 相似度检索 - 余弦相似度匹配
  • MMR - 多样性检索
  • 阈值过滤 - 过滤低相似度结果

高级检索

  • 混合检索 - 向量+关键词
  • 重排序 - 二次排序优化
  • 层级检索 - 文档→段落→句子

📋 RAG模式

模式 复杂度 适用场景
Naive RAG简单问答
Retrieval-Read⭐⭐标准RAG流程
HyDE⭐⭐假设文档增强
RRF⭐⭐⭐多检索器融合
Agentic RAG⭐⭐⭐⭐智能路由

📏 评估指标

检索质量

  • Hit Rate - 命中率:Hit Rate@k = (# queries with relevant doc in top k) / (total queries)
  • MRR - 平均倒数排名:MRR = (1/|Q|) × Σ (1/rank_i)
  • NDCG - 归一化折损累计增益:NDCG@k = DCG@k / IDCG@k,其中 DCG@k = Σ (rel_i / log₂(i+1))

生成质量

  • Faithfulness - 忠实度:基于人工评估或LLM评分,衡量生成内容与源文档的一致性
  • Answer Relevancy - 相关性:基于人工评估或LLM评分,衡量回答与问题的相关程度
  • Context Precision - 上下文精确度:基于人工评估或LLM评分,衡量检索上下文与问题的精确匹配程度

⚡ 优化技巧

问题 解决方案
检索不准确调整chunk_size、重排序、使用混合检索
上下文过长摘要压缩、选择性检索
幻觉回答增强提示、引用来源
速度慢缓存、异步处理、向量压缩
切分不理想调整chunk大小、重叠、边界,按语义结构切分
检索噪音多使用重排序、上下文压缩、元数据过滤
多轮对话效果差将历史对话作为上下文参与检索

🎯 程序员需要重点掌握什么?

🗄️ 向量数据库的使用

至少会用一两个(如 Chroma / Qdrant / pgvector),掌握插入、查询、过滤等核心操作。

📐 Embedding 流程

如何调用模型、缓存向量、批量处理,了解不同 Embedding 模型的特点和成本。

✂️ 文档切分与预处理

对不同数据源设计合理的 ETL 流程,根据文档类型选择切分策略。

🛠️ LangChain / LlamaIndex 抽象

掌握 LlamaIndex 的 VectorStoreIndex、Node、Reader 概念,以及 LangChain 的 VectorStore、Retriever、Document 抽象。

💻 代码示例

示例:完整RAG系统实现
"""
RAG完整实现:从文档到问答
- 文档加载与分割
- 向量嵌入
- 向量存储与检索
- LLM生成回答
"""
from langchain_openai import ChatOpenAI, OpenAIEmbeddings
from langchain_community.vectorstores import FAISS
from langchain_community.document_loaders import TextLoader, DirectoryLoader
from langchain.text_splitter import RecursiveCharacterTextSplitter
from langchain.prompts import ChatPromptTemplate
from langchain.schema.runnable import RunnablePassthrough
from langchain.schema.output_parser import StrOutputParser
from typing import List, Tuple
import os

# 1. 文档加载
class DocumentLoader:
    """文档加载器"""
    
    @staticmethod
    def load_directory(path: str, glob_pattern: str = "**/*.txt") -> List:
        """加载目录下所有文档"""
        loader = DirectoryLoader(path, glob=glob_pattern, loader_cls=TextLoader)
        documents = loader.load()
        return documents
    
    @staticmethod
    def load_file(file_path: str) -> List:
        """加载单个文档"""
        loader = TextLoader(file_path, encoding="utf-8")
        return loader.load()

# 2. 文档分割
class TextSplitter:
    """文本分割器"""
    
    def __init__(self, chunk_size: int = 500, chunk_overlap: int = 50):
        self.splitter = RecursiveCharacterTextSplitter(
            chunk_size=chunk_size,
            chunk_overlap=chunk_overlap,
            separators=["\n\n", "\n", "。", "!", "?", ";", " ", ""]
        )
    
    def split(self, documents) -> List:
        """分割文档"""
        return self.splitter.split_documents(documents)

# 3. 向量存储
class VectorStore:
    """向量存储器"""
    
    def __init__(self, embedding_model=None):
        self.embedding = embedding_model or OpenAIEmbeddings()
        self.vectorstore = None
    
    def create(self, documents: List, save_path: str = None):
        """创建向量存储"""
        self.vectorstore = FAISS.from_documents(documents, self.embedding)
        
        if save_path:
            self.vectorstore.save_local(save_path)
            print(f"✅ 向量库已保存到: {save_path}")
    
    def load(self, path: str):
        """加载向量存储"""
        self.vectorstore = FAISS.load_local(path, self.embedding)
    
    def search(self, query: str, k: int = 4) -> List:
        """相似度检索"""
        if not self.vectorstore:
            raise ValueError("向量库未初始化")
        return self.vectorstore.similarity_search(query, k=k)
    
    def search_with_score(self, query: str, k: int = 4) -> List[Tuple]:
        """带分数的检索"""
        return self.vectorstore.similarity_search_with_score(query, k=k)

# 4. RAG链
class RAGChain:
    """RAG问答链"""
    
    def __init__(self, llm, vectorstore: VectorStore):
        self.llm = llm
        self.vectorstore = vectorstore
        
        # 提示词模板
        self.prompt_template = """基于以下上下文回答问题。如果上下文中没有相关信息,请说明你不知道。

上下文:
{context}

问题: {question}

回答:"""
        
        self.prompt = ChatPromptTemplate.from_template(self.prompt_template)
        self.chain = None
        self.build_chain()
    
    def build_chain(self):
        """构建RAG链"""
        retriever = self.vectorstore.vectorstore.as_retriever()
        
        self.chain = (
            {"context": retriever, "question": RunnablePassthrough()}
            | self.prompt
            | self.llm
            | StrOutputParser()
        )
    
    def invoke(self, question: str) -> str:
        """执行问答"""
        return self.chain.invoke(question)

# 5. 混合检索增强
class HybridRetriever:
    """混合检索器 - 向量+关键词"""
    
    def __init__(self, vectorstore: FAISS):
        self.vectorstore = vectorstore
        self.search_kwargs = {"k": 10}
    
    def search(self, query: str) -> List:
        """混合检索"""
        # 1. 向量检索
        vector_results = self.vectorstore.similarity_search(query, k=5)
        
        # 2. MMR检索(增加多样性)
        mmr_results = self.vectorstore.max_marginal_relevance_search(
            query, k=3, fetch_k=10
        )
        
        # 3. 合并去重
        seen = set()
        combined = []
        for doc in vector_results + mmr_results:
            doc_id = doc.metadata.get("source", "") + doc.page_content[:50]
            if doc_id not in seen:
                seen.add(doc_id)
                combined.append(doc)
        
        return combined[:4]


# 使用示例
def main():
    # 准备测试文档
    os.makedirs("docs", exist_ok=True)
    with open("docs/ai_intro.txt", "w", encoding="utf-8") as f:
        f.write("""人工智能(AI)是一门研究如何让机器具有智能的学科。
机器学习是AI的核心技术之一,让计算机从数据中学习规律。
深度学习是机器学习的一个分支,使用神经网络模型。
大语言模型(LLM)是深度学习的应用,可以理解和生成人类语言。
RAG技术可以增强LLM的知识,让AI回答私有知识库的问题。
""")
    
    with open("docs/rag_guide.txt", "w", encoding="utf-8") as f:
        f.write("""RAG(检索增强生成)是一种结合检索和生成的技术。
RAG的工作流程:加载文档 -> 分割文本 -> 向量化 -> 存储 -> 检索 -> 生成。
RAG的优点:1) 可以使用最新信息 2) 减少幻觉 3) 可以引用来源。
常用的向量数据库有FAISS、Chroma、Milvus、Pinecone等。
""")
    
    # 1. 加载文档
    print("📄 加载文档...")
    documents = DocumentLoader.load_directory("docs")
    print(f"  加载了 {len(documents)} 个文档")
    
    # 2. 分割文档
    print("✂️ 分割文档...")
    splitter = TextSplitter(chunk_size=100, chunk_overlap=20)
    chunks = splitter.split(documents)
    print(f"  分割成 {len(chunks)} 个块")
    
    # 3. 创建向量存储
    print("🔢 创建向量索引...")
    embedding = OpenAIEmbeddings()
    vectorstore = VectorStore(embedding)
    vectorstore.create(chunks, save_path="vectorstore")
    
    # 4. 测试检索
    print("\n🔍 测试检索...")
    results = vectorstore.search("什么是RAG?", k=2)
    for i, doc in enumerate(results):
        print(f"  结果{i+1}: {doc.page_content[:100]}...")
    
    # 5. 构建RAG链
    print("\n🤖 构建RAG问答链...")
    llm = ChatOpenAI(model="gpt-4", temperature=0)
    vectorstore.load("vectorstore")
    rag = RAGChain(llm, vectorstore)
    
    # 6. 执行问答
    questions = [
        "什么是RAG?",
        "人工智能和机器学习有什么关系?",
        "常用的向量数据库有哪些?"
    ]
    
    print("\n💬 RAG问答测试:")
    for q in questions:
        print(f"\n问题: {q}")
        answer = rag.invoke(q)
        print(f"回答: {answer}")

main()
代码说明

📦 核心组件

  • DocumentLoader - 文档加载
  • TextSplitter - 文本分割
  • VectorStore - 向量存储
  • RAGChain - 问答链

🔄 工作流程

  • 加载文档
  • 分割成chunks
  • 向量化存储
  • 检索+生成

⚡ 优化策略

  • 调整chunk_size
  • 使用MMR检索
  • 混合检索
  • 重排序