首页 / AI 实战教程 / RAG 系统实战:从零搭建检索增强生成应用 18 次阅读
RAG 系统实战:从零搭建检索增强生成应用
AI 实战教程 ⚡ 中级 ⏱ 约 45 分钟

RAG 系统实战:从零搭建检索增强生成应用

大模型会幻觉、知识有截止日期——这是 LLM 落地最头疼的两件事。 RAG(Retrieval-Augmented Generation,检索增强生成)用一套「先检索、再生成」的架构完美解决了这两个问题。 本教程手把手带你用 Python + LangChain + FAISS 从零构建一套完整的 RAG 系统,每一步都有可直接运行的代码。

📅 2026年2月28日 🛠 Python · LangChain · FAISS · OpenAI 📦 完整代码随文附上

一、RAG 是什么,为什么你需要它

RAG 系统架构概览

在没有 RAG 的世界里,你问 ChatGPT「我们公司的报销政策是什么?」它会随机编造一个答案——因为它根本不知道你公司的内部文件。 更糟糕的是,它编造的时候往往信心十足,让人难以分辨真假。

RAG 的核心思想用一句话概括:先检索相关文档,再把文档内容交给 LLM 生成答案。 这样 LLM 就不需要「记住」所有知识,它只需要在给定上下文中找到答案——这正是语言模型最擅长的事情。

RAG vs 微调:怎么选?

对比维度 RAG 微调(Fine-tuning)
知识更新成本 低,更新文档即可 高,需重新训练
答案可追溯 ✅ 可标注来源 ❌ 难以溯源
适合场景 企业知识库、文档问答 特定风格、专业术语
开发门槛 中等,工程化即可 高,需要 GPU 资源
幻觉风险 较低(有依据) 仍然存在
💡
本教程技术栈:Python 3.10+、LangChain 0.3、FAISS-CPU、OpenAI API(或本地 Ollama)。 所有依赖均可通过 pip 安装,无需 GPU。

完整系统架构

RAG 全流程数据流向

📄 原始文档
✂️ 文本分块
🔢 向量嵌入
🗄️ FAISS 索引
↑ 离线建索引阶段
❓ 用户提问
🔍 语义检索
📋 构造 Prompt
🤖 LLM 生成
↑ 在线查询阶段

二、环境搭建与项目结构

项目目录结构与依赖安装

我们将构建一个结构清晰、职责分明的项目。每个文件专注一件事,方便你后续扩展。

项目结构预览

rag-from-scratch/
├── data/                    # 知识库文档(.txt / .pdf / .md)
│   └── company_policy.txt
├── app/
│   ├── __init__.py
│   ├── document_loader.py   # 文档加载 + 分块
│   ├── vector_store.py      # 向量索引(FAISS)
│   ├── retriever.py         # 检索逻辑
│   └── rag_chain.py         # RAG 主链路
├── main.py                  # 入口:交互式问答
└── requirements.txt
1

安装依赖

创建虚拟环境后安装以下包。faiss-cpu 是纯 CPU 版本的向量相似度搜索库,无需 GPU。

# 创建并激活虚拟环境
python -m venv .venv
source .venv/bin/activate  # Windows: .venv\Scripts\activate

# 安装核心依赖
pip install langchain langchain-openai langchain-community \
            faiss-cpu pymupdf tiktoken openai python-dotenv

如果你想完全本地运行(不依赖 OpenAI),可以改用 Ollama:

# 本地 LLM 方案
pip install langchain-ollama
# 然后本地启动:ollama run qwen2.5:7b
LangChain 从 0.3 版本开始将各集成拆分为独立包。如果遇到 ImportError,检查你是否安装了对应的 langchain-xxx 子包。
2

配置 API Key

在项目根目录创建 .env 文件,放置你的 OpenAI API Key:

# .env
OPENAI_API_KEY=sk-xxxxxxxxxxxxxxxxxxxxxxxx
# 如果用国内中转服务
# OPENAI_BASE_URL=https://your-proxy.com/v1
⚠️
永远不要把 .env 文件提交到 Git。确保 .gitignore 中已包含 .env

三、文档加载与智能分块

文档分块策略与语义切分

分块(Chunking)是 RAG 系统中最容易被忽视、但影响最大的环节。 块太大会超出 LLM 上下文窗口,块太小会丢失上下文。一个好的分块策略需要在文档语义边界处切割

3

实现文档加载器

创建 app/document_loader.py,支持 TXT 和 PDF 两种格式:

# app/document_loader.py
from pathlib import Path
from langchain_community.document_loaders import TextLoader, PyMuPDFLoader
from langchain.text_splitter import RecursiveCharacterTextSplitter
from langchain_core.documents import Document
from typing import List


def load_documents(data_dir: str = "data") -> List[Document]:
    """加载 data/ 目录下的所有文档"""
    docs = []
    data_path = Path(data_dir)

    for file_path in data_path.rglob("*"):
        if file_path.suffix == ".txt":
            loader = TextLoader(str(file_path), encoding="utf-8")
            docs.extend(loader.load())
        elif file_path.suffix == ".pdf":
            loader = PyMuPDFLoader(str(file_path))
            docs.extend(loader.load())

    print(f"✅ 共加载 {len(docs)} 个文档片段")
    return docs


def split_documents(docs: List[Document]) -> List[Document]:
    """将文档分割为适合检索的小块"""
    splitter = RecursiveCharacterTextSplitter(
        chunk_size=800,        # 每块最多 800 字符
        chunk_overlap=100,     # 相邻块重叠 100 字符(保留上下文)
        separators=["\n\n", "\n", "。", "!", "?", ".", " ", ""],
    )
    chunks = splitter.split_documents(docs)
    print(f"✅ 分割为 {len(chunks)} 个文本块")
    return chunks
📌
chunk_overlap 为什么重要? 当一个关键句子恰好落在两块的边界时,重叠区域确保它至少完整地出现在某一块中,避免信息丢失。

四、构建向量索引(FAISS)

向量索引是 RAG 的「大脑」。它把每个文本块转换为一个高维数字向量, 语义相似的文本在向量空间中距离更近。检索时,我们把用户问题也转换成向量, 然后找最近的 K 个块。

4

创建向量存储

创建 app/vector_store.py,封装 FAISS 索引的创建和持久化:

# app/vector_store.py
from langchain_openai import OpenAIEmbeddings
from langchain_community.vectorstores import FAISS
from langchain_core.documents import Document
from typing import List
import os


def get_embeddings():
    """返回 Embedding 模型实例"""
    return OpenAIEmbeddings(
        model="text-embedding-3-small",  # 比 ada-002 更便宜且更好
        openai_api_key=os.environ.get("OPENAI_API_KEY"),
    )


def build_vector_store(chunks: List[Document], index_path: str = "faiss_index"):
    """从文本块构建 FAISS 向量索引并持久化到磁盘"""
    embeddings = get_embeddings()

    print("⏳ 正在生成向量嵌入,请稍候...")
    vector_store = FAISS.from_documents(chunks, embeddings)
    vector_store.save_local(index_path)

    print(f"✅ 向量索引已保存至 {index_path}/")
    return vector_store


def load_vector_store(index_path: str = "faiss_index"):
    """从磁盘加载已有的 FAISS 索引"""
    embeddings = get_embeddings()
    vector_store = FAISS.load_local(
        index_path,
        embeddings,
        allow_dangerous_deserialization=True,  # 本地文件信任即可
    )
    print(f"✅ 已从 {index_path}/ 加载向量索引")
    return vector_store
💰
节省费用小技巧:索引建好后保存到磁盘,下次启动直接 load_vector_store() 读取,不必重复调用 Embedding API。 对于 1000 个文本块,text-embedding-3-small 的费用不到 0.01 美元。

五、组装 RAG 链路,连接 LLM

RAG 链路组装与 Prompt 设计

这一步是整个系统的核心:将检索器和生成器用一个 Prompt 模板串联起来。 好的 Prompt 设计能显著降低幻觉——我们明确告诉模型「只能用给定的上下文回答,没有就说不知道」。

5

实现 RAG 主链路

创建 app/rag_chain.py,使用 LangChain Expression Language(LCEL)组合各组件:

# app/rag_chain.py
from langchain_openai import ChatOpenAI
from langchain_core.prompts import ChatPromptTemplate
from langchain_core.output_parsers import StrOutputParser
from langchain_core.runnables import RunnablePassthrough
from langchain_community.vectorstores import FAISS
import os


# 系统 Prompt:严格限制模型只使用检索到的上下文
RAG_PROMPT = ChatPromptTemplate.from_messages([
    ("system", """你是一个专业的知识库问答助手。请严格遵守以下规则:

1. 只根据下方提供的【参考文档】来回答问题
2. 如果参考文档中没有相关信息,直接说"根据现有文档,我无法回答这个问题"
3. 引用信息时注明来源(如"根据第X部分...")
4. 回答要简洁准确,不要过度解读文档内容

【参考文档】
{context}"""),
    ("human", "{question}"),
])


def format_docs(docs):
    """将检索到的文档列表格式化为字符串"""
    formatted = []
    for i, doc in enumerate(docs, 1):
        source = doc.metadata.get("source", "未知来源")
        formatted.append(f"[文档{i}] 来源:{source}\n{doc.page_content}")
    return "\n\n---\n\n".join(formatted)


def create_rag_chain(vector_store: FAISS, k: int = 4):
    """
    创建 RAG 链路
    k: 检索最相似的 k 个文本块
    """
    # 1. 配置检索器
    retriever = vector_store.as_retriever(
        search_type="mmr",          # MMR 算法:兼顾相关性和多样性
        search_kwargs={"k": k, "fetch_k": 20},
    )

    # 2. 配置语言模型
    llm = ChatOpenAI(
        model="gpt-4o-mini",        # 性价比最高的选择
        temperature=0,              # 设为 0:减少随机性,提升一致性
        openai_api_key=os.environ.get("OPENAI_API_KEY"),
    )

    # 3. 用 LCEL 组装链路
    rag_chain = (
        {
            "context": retriever | format_docs,   # 检索 → 格式化
            "question": RunnablePassthrough(),    # 用户问题直接传递
        }
        | RAG_PROMPT
        | llm
        | StrOutputParser()
    )

    return rag_chain, retriever
🔍
为什么用 MMR(最大边界相关性)? 普通的 TopK 检索可能返回 K 个语义几乎相同的块,浪费上下文窗口。 MMR 在保证相关性的同时,确保每个返回的块都带来新的信息。
6

创建主程序入口

创建 main.py,整合所有模块:

# main.py
import os
from pathlib import Path
from dotenv import load_dotenv
from app.document_loader import load_documents, split_documents
from app.vector_store import build_vector_store, load_vector_store
from app.rag_chain import create_rag_chain

load_dotenv()

INDEX_PATH = "faiss_index"


def setup():
    """首次运行:构建向量索引"""
    print("🚀 首次运行,开始构建知识库...")
    docs = load_documents("data")
    chunks = split_documents(docs)
    vector_store = build_vector_store(chunks, INDEX_PATH)
    return vector_store


def main():
    # 判断是否已有索引
    if Path(INDEX_PATH).exists():
        vector_store = load_vector_store(INDEX_PATH)
    else:
        vector_store = setup()

    rag_chain, retriever = create_rag_chain(vector_store, k=4)

    print("\n✅ 知识库就绪!输入 'quit' 退出\n")
    print("=" * 50)

    while True:
        question = input("\n❓ 你的问题:").strip()
        if question.lower() in ("quit", "exit", "q"):
            break
        if not question:
            continue

        print("\n🔍 正在检索相关文档...")
        # 也可以单独查看检索到了哪些文档
        retrieved_docs = retriever.invoke(question)
        print(f"   找到 {len(retrieved_docs)} 个相关文档块")

        print("\n🤖 正在生成答案...\n")
        answer = rag_chain.invoke(question)
        print(f"📝 答案:\n{answer}")
        print("\n" + "=" * 50)


if __name__ == "__main__":
    main()
🎯
测试数据准备:data/ 目录放一个 company_policy.txt, 内容是任意公司政策文本(哪怕几百字都行),然后运行 python main.py 测试效果。

六、进阶优化:提升检索质量

基础 RAG 跑通之后,下面这些优化手段能显著提升答案质量:

优化 1:混合检索(向量 + 关键词)

纯向量检索对精确词汇(如产品型号、人名、代码片段)效果较差。引入 BM25 关键词检索做补充:

pip install langchain-community rank_bm25
# 混合检索示例
from langchain_community.retrievers import BM25Retriever
from langchain.retrievers import EnsembleRetriever

# 关键词检索器(BM25)
bm25_retriever = BM25Retriever.from_documents(chunks)
bm25_retriever.k = 4

# 向量检索器
faiss_retriever = vector_store.as_retriever(search_kwargs={"k": 4})

# 混合:60% 向量 + 40% 关键词
ensemble_retriever = EnsembleRetriever(
    retrievers=[faiss_retriever, bm25_retriever],
    weights=[0.6, 0.4],
)

优化 2:查询重写(Query Rewriting)

用户问题往往口语化,不适合直接做向量检索。先用 LLM 改写成更适合检索的形式:

from langchain_core.prompts import PromptTemplate

rewrite_prompt = PromptTemplate.from_template("""
将以下用户问题改写为一个适合搜索文档的查询语句,保持核心意图不变,使用关键词更清晰的表达:

原始问题:{question}
改写后的查询:
""")

rewrite_chain = rewrite_prompt | llm | StrOutputParser()

# 使用时
rewritten = rewrite_chain.invoke({"question": "上个月我申请的那个事怎么样了"})
# → "员工申请审批状态查询"

优化 3:语义分块(Semantic Chunking)

比固定长度分块更智能——在语义边界处切割,而不是在字数到了就硬切:

from langchain_experimental.text_splitter import SemanticChunker

# SemanticChunker 会在相邻句子语义差异较大时切割
semantic_splitter = SemanticChunker(
    embeddings=get_embeddings(),
    breakpoint_threshold_type="percentile",  # 按相似度百分位切
    breakpoint_threshold_amount=95,
)
semantic_chunks = semantic_splitter.split_documents(docs)
⚠️
SemanticChunker 每次切割都会调用 Embedding API,成本比固定长度分块高约 10 倍。 建议只在离线建索引阶段使用,切好后保存结果,不要重复调用。

七、完整运行演示

假设 data/company_policy.txt 内容包含公司报销政策,以下是完整的运行效果:

$ python main.py

✅ 已从 faiss_index/ 加载向量索引
✅ 知识库就绪!输入 'quit' 退出

==================================================

❓ 你的问题:出差报销的上限是多少?

🔍 正在检索相关文档...
   找到 4 个相关文档块

🤖 正在生成答案...

📝 答案:
根据公司差旅报销政策(第3条),出差报销上限如下:

- **国内出差**:住宿费用不超过 500 元/天,餐饮补贴 100 元/天
- **跨省出差**:可申请高铁二等座全额报销,飞机需提前 3 天申请
- **海外出差**:需经部门负责人和 HR 双重审批,上限按当地标准执行

如有特殊情况超出限额,需填写《超标报销申请表》并附主管签字。

==================================================
注意答案中出现了「根据公司差旅报销政策(第3条)」这样的引用——这正是 RAG 的优势:答案有据可查,不是凭空捏造。

八、常见问题与排查

问题 原因 解决方案
检索结果不相关 chunk_size 太大或分块策略不佳 减小 chunk_size(尝试 400~600),启用语义分块;检查原始文档质量
模型仍然幻觉 检索到的块确实不包含答案 加强 Prompt 约束;考虑增大 k 值;检查文档是否覆盖了该问题领域
响应很慢 Embedding API 调用延迟 使用本地 Embedding 模型(如 mxbai-embed-large via Ollama)
中文检索效果差 英文 Embedding 模型对中文支持弱 改用多语言模型:text-embedding-3-large 或 BGE-M3
FAISS 加载报错 allow_dangerous_deserialization 未设置 load_local() 调用中加上 allow_dangerous_deserialization=True
文档更新后答案还是旧的 向量索引没有重建 删除 faiss_index/ 目录,重新运行 main.py 触发重建

九、总结与下一步

恭喜你!你已经从零构建了一套完整的 RAG 系统,涵盖了:

  • 文档加载与智能分块:RecursiveCharacterTextSplitter + chunk_overlap
  • 向量索引:OpenAI Embedding + FAISS 本地持久化
  • 语义检索:MMR 算法保证相关性和多样性
  • 答案生成:约束型 Prompt + GPT-4o-mini
  • 进阶优化:混合检索、查询重写、语义分块

推荐的后续学习方向

  1. Agentic RAG:让 LLM 自主决定是否需要检索、检索什么,构建能主动思考的 AI Agent
  2. GraphRAG:在知识图谱上做检索,更适合处理实体关系复杂的场景
  3. 多模态 RAG:检索对象扩展到图片、表格、代码,构建真正的多模态知识库
  4. 评估框架:接入 RAGAS 或 TruLens,用 Precision / Recall / Faithfulness 指标量化 RAG 质量
🚀
本教程完整代码已整理好每一步,可直接复制运行。 掌握这套 RAG 基础后,你就拥有了构建企业级 AI 应用的核心武器——它正是 ChatPDF、NotebookLM 以及大量企业知识库产品的技术内核。
选择栏目
今日简报 播客电台 AI 实战教程 关于我
栏目
全球AI日报国内AI日报全球金融日报国内金融日报全球大新闻日报国内大新闻日报Claude Code 玩法日报OpenClaw 动态日报GitHub 热门项目日报
我的收藏
播客版
0:00
--:--