RAG全流程拆解|从文档加载到增强生成,手把手搭建企业级知识问答

2 阅读10分钟

导读:RAG(检索增强生成)听起来高大上,但拆开来看就是六个步骤的流水线——数据加载、文档分块、Embedding向量化、向量存储、相似度检索、增强生成。这篇把每个环节的原理、踩坑点和代码全摊开讲,帮你从"能跑"到"好用"。


一、先搞清楚:RAG到底在解决什么问题?

大模型有两个硬伤:幻觉(一本正经胡说八道)和知识过时(训练数据截止到去年)。RAG的思路很简单——不让模型自己瞎编,而是先去知识库里"查资料",找到相关内容后再让模型基于这些内容回答。

打个比方:大模型像一个没带课本的考生,RAG就是让他开卷考试——先翻书找答案,再组织语言作答。答案有据可查,幻觉大幅降低。

整个流程长这样:

用户提问 → 问题向量化 → 向量数据库检索 → 取回相关文档片段 → 拼成Prompt → 大模型生成回答

但在这之前,还有一条离线构建链路

原始文档 → 数据加载 → 文档分块 → Embedding向量化 → 存入向量数据库

两条链路合在一起,就是完整的RAG流程。下面逐个拆解。


二、Step 1:数据加载——别小看这一步

2.1 做什么

把各种格式的文档(PDF、Word、TXT、HTML、Markdown、Excel)统一转成纯文本,方便后续处理。

2.2 常用工具

工具特点适用场景
LangChain DocumentLoaders支持50+格式,生态最全通用场景
LlamaIndex SimpleDirectoryReader自动识别格式,一行代码快速原型
Unstructured.ioPDF/HTML解析能力强,保留结构复杂文档
PyPDF2/pdfplumber纯Python,轻量简单PDF
python-docxWord文档专用.docx文件

2.3 踩坑实录

⚠️ PDF扫描件是大坑:很多公司的制度文件是扫描PDF,直接提取出来全是乱码。必须先做OCR(推荐PaddleOCR或Tesseract),再做版面分析,不然文本顺序全乱。

⚠️ 表格别直接丢:我之前做财务RAG,把Excel直接转纯文本,数字全挤在一起,模型根本看不懂。后来转成Markdown表格再分块,效果直接翻倍。

# LangChain 加载文档示例
from langchain.document_loaders import PyPDFLoader, TextLoader, UnstructuredExcelLoader

# PDF
loader = PyPDFLoader("knowledge_base.pdf")
docs = loader.load()

# TXT
loader = TextLoader("faq.txt", encoding="utf-8")
docs = loader.load()

# 每个doc有 page_content(文本)和 metadata(页码/来源等)
print(f"加载了 {len(docs)} 个文档片段")
print(f"第一段内容: {docs[0].page_content[:200]}")
print(f"元数据: {docs[0].metadata}")

三、Step 2:文档分块——RAG效果的天花板

分块是整个RAG流程里最容易被忽视但影响最大的一环。分得好,检索精准;分得烂,模型拿到碎片化内容直接开始编。

3.1 为什么不能不分块?

直接把整篇文档塞给模型?不行,原因有两个:

  1. Token限制:一篇文档可能几万字,远超模型的上下文窗口
  2. 检索精度:整篇文档的向量是所有内容的"平均值",查出来的结果往往不精准

3.2 六种分块策略对比

策略原理优点缺点推荐度
固定长度分块按字数/Token数切,设重叠最简单,速度快可能从句子中间切断⭐⭐ 原型用
递归字符分割按段落→换行→句号→逗号依次切保持语义完整,LangChain默认不识别文档结构⭐⭐⭐⭐ 通用首选
文档结构感知按标题/章节/代码块切保留文档结构,表格不拆散需要额外解析器⭐⭐⭐⭐⭐ 结构化文档
语义分块Embedding检测语义边界,变化大时切边界最精准慢,需要额外Embedding计算⭐⭐⭐⭐ 叙述性文档
父子层级分块小块检索,返回大块上下文兼顾精度和上下文存储翻倍⭐⭐⭐⭐⭐ 生产推荐
Agent分块用LLM判断分块边界质量最高10-50倍成本,巨慢⭐⭐⭐ 高价值文档

3.3 代码实战

from langchain.text_splitter import RecursiveCharacterTextSplitter

# 递归字符分割(推荐默认方案)
splitter = RecursiveCharacterTextSplitter(
    chunk_size=512,          # 每块最大字符数
    chunk_overlap=64,        # 块之间重叠字符数,防止语义断裂
    separators=["\n\n", "\n", "。", "!", "?", ".", " ", ""]
)

chunks = splitter.split_documents(docs)
print(f"分块数量: {len(chunks)}")
print(f"第一块长度: {len(chunks[0].page_content)} 字符")

💡 分块大小怎么选? 没有银弹,但有几个经验值:

  • 128-256 Token:适合FAQ、短问答,检索精度高但上下文少
  • 512 Token:通用推荐起点,大多数场景够用
  • 1024+ Token:适合长文档、需要丰富上下文的场景

⚠️ 踩坑提醒:chunk_overlap千万别设成0!我之前省这个参数,结果一个完整的API说明被切成两半,前半块在检索结果里但后半块不在,模型只能"半猜半编"。设64-128的重叠能大幅缓解这个问题。

3.4 生产推荐:父子层级分块

这是目前业界公认最实用的生产方案:

from langchain.text_splitter import RecursiveCharacterTextSplitter

# 父块(大块,用于给模型提供上下文)
parent_splitter = RecursiveCharacterTextSplitter(chunk_size=1024, chunk_overlap=64)
# 子块(小块,用于精准检索)
child_splitter = RecursiveCharacterTextSplitter(chunk_size=256, chunk_overlap=32)

parent_chunks = parent_splitter.split_documents(docs)
child_chunks = child_splitter.split_documents(docs)

# 子块存入向量数据库用于检索
# 检索到子块后,返回对应的父块给模型

四、Step 3:Embedding向量化——把文本变成数字

4.1 做什么

把每个文本块通过Embedding模型转成一个高维向量(比如768维),语义相近的文本在向量空间里距离也近。

4.2 模型选型

模型维度中文支持特点
BAAI/bge-large-zh-v1.51024✅ 原生中文中文场景首选,MTEB榜单靠前
BAAI/bge-m31024✅ 多语言支持Dense+Sparse+Multi-vector
text-embedding-3-small1536OpenAI,API调用方便
m3e-base768✅ 原生中文轻量,本地部署快

💡 选型建议:中文场景直接上BGE系列,开源免费效果还好。需要多语言或和OpenAI生态打通就用text-embedding-3。

from sentence_transformers import SentenceTransformer

# 加载模型(首次运行会自动下载)
model = SentenceTransformer('BAAI/bge-large-zh-v1.5')

# 批量编码
texts = [chunk.page_content for chunk in chunks]
embeddings = model.encode(texts, show_progress_bar=True, batch_size=32)

print(f"向量形状: {embeddings.shape}")  # (N, 1024)

⚠️ 踩坑提醒:查询向量和文档向量必须用同一个模型生成!** 我之前用BGE存的文档,查询时手滑用了OpenAI的Embedding,结果相似度全乱套,检索出来的东西驴唇不对马嘴。


五、Step 4:向量存储——给向量安个家

5.1 做什么

把文本块和对应的向量一起存入向量数据库,建立索引,方便后续快速检索。

5.2 存储方案选择

方案类型适用场景
FAISS算法库本地原型、算法实验
LanceDB嵌入式数据库快速Demo、中小规模
Milvus分布式数据库生产环境、大规模数据
Chroma轻量数据库本地开发、Jupyter Notebook
Elasticsearch搜索引擎需要混合搜索(关键词+向量)

5.3 代码实战(以LanceDB为例)

import lancedb
import numpy as np

# 连接本地数据库
db = lancedb.connect("./rag_vector_db")

# 准备数据:向量 + 元数据
data = []
for i, (chunk, embedding) in enumerate(zip(chunks, embeddings)):
    data.append({
        "id": i,
        "vector": embedding.tolist(),
        "text": chunk.page_content,
        "source": chunk.metadata.get("source", "unknown"),
        "page": chunk.metadata.get("page", 0)
    })

# 创建表并建索引
table = db.create_table("knowledge_base", data=data, mode="overwrite")
table.create_index(metric="cosine", num_partitions=16)
print(f"存储完成,共 {len(table)} 条记录")

💡 元数据很重要:存向量时一定要带上来源、页码、章节等元数据。检索时可以过滤(比如只搜某个文档),生成时可以标注引用来源,用户一看就知道答案出自哪里。


六、Step 5:检索——找到最相关的知识片段

6.1 做什么

用户提问 → 问题向量化 → 在向量数据库中搜索最相似的Top-K个文本块。

6.2 检索策略

策略说明适用场景
纯向量检索只用Embedding相似度通用场景
混合检索向量 + 关键词(BM25)需要精确匹配(人名/编号)
重排序(Rerank)先粗筛Top-50,再用Cross-Encoder精排生产环境推荐

6.3 代码实战

# 1. 用户问题向量化
query = "如何配置Nginx反向代理?"
query_vector = model.encode([query])[0].tolist()

# 2. 向量检索
results = table.search(query_vector) \
    .limit(5) \
    .to_pandas()

# 3. 查看检索结果
for _, row in results.iterrows():
    print(f"[相似度: {1 - row['_distance']:.4f}] {row['text'][:100]}...")
    print(f"  来源: {row['source']}{row['page']}页\n")

6.4 高级:混合检索 + Rerank

# 混合检索:向量 + 关键词过滤
results = table.search(query_vector) \
    .where("source = 'nginx_docs'") \
    .limit(20) \
    .to_pandas()

# Rerank:用Cross-Encoder精排
from sentence_transformers import CrossEncoder
reranker = CrossEncoder('BAAI/bge-reranker-base')

pairs = [(query, row['text']) for _, row in results.iterrows()]
scores = reranker.predict(pairs)

# 按重排序分数排序,取Top-5
ranked_indices = np.argsort(scores)[::-1][:5]
final_results = [results.iloc[i] for i in ranked_indices]

⚠️ 踩坑提醒:Top-K不是越大越好!我之前设Top-20,结果塞进Prompt的内容太多,模型反而被无关信息干扰开始胡说。一般Top-3到Top-5最合适,配合Rerank效果更好。


七、Step 6:增强生成——让模型基于检索内容回答

7.1 做什么

把检索到的文本块和用户问题拼成一个Prompt,喂给大模型生成最终回答。

7.2 Prompt模板设计

prompt_template = """你是一个专业的知识问答助手。请根据以下检索到的参考资料回答用户问题。
要求:
1. 只基于提供的参考资料回答,不要编造信息
2. 如果参考资料不足以回答问题,请明确说明
3. 回答时标注信息来源

【参考资料】
{context}

【用户问题】
{question}

【回答】
"""

7.3 完整生成代码

from openai import OpenAI

# 构建上下文
context_parts = []
for _, row in final_results.iterrows():
    context_parts.append(f"[来源: {row['source']}] {row['text']}")

context = "\n\n".join(context_parts)

# 填充Prompt
prompt = prompt_template.format(context=context, question=query)

# 调用大模型
client = OpenAI(api_key="your-api-key", base_url="your-base-url")
response = client.chat.completions.create(
    model="qwen-plus",  # 或 gpt-4o、deepseek-chat 等
    messages=[
        {"role": "system", "content": "你是一个专业的知识问答助手。"},
        {"role": "user", "content": prompt}
    ],
    temperature=0.1,  # 低温度,减少幻觉
    max_tokens=1024
)

answer = response.choices[0].message.content
print(answer)

💡 Temperature设多少? RAG场景建议0-0.3。我们是让模型基于已有内容回答,不需要"创意"。Temperature太高容易偏离检索内容开始自由发挥。我一般设0.1,既不太死板又不会乱编。


八、完整链路串联

把上面六步串起来,就是一个完整的RAG系统:

def rag_query(question, model, table, reranker=None):
    """完整RAG查询链路"""
    # 1. 问题向量化
    query_vector = model.encode([question])[0].tolist()

    # 2. 向量检索
    results = table.search(query_vector).limit(10).to_pandas()

    # 3. Rerank(可选)
    if reranker:
        pairs = [(question, row['text']) for _, row in results.iterrows()]
        scores = reranker.predict(pairs)
        ranked = np.argsort(scores)[::-1][:5]
        results = [results.iloc[i] for i in ranked]

    # 4. 构建Prompt
    context = "\n\n".join(
        f"[{row['source']}] {row['text']}" for _, row in results.iterrows()
    )
    prompt = prompt_template.format(context=context, question=question)

    # 5. 大模型生成
    response = client.chat.completions.create(
        model="qwen-plus",
        messages=[
            {"role": "system", "content": "你是专业的知识问答助手。"},
            {"role": "user", "content": prompt}
        ],
        temperature=0.1
    )

    return response.choices[0].message.content

# 使用
answer = rag_query("如何配置Nginx反向代理?", model, table, reranker)
print(answer)

九、RAG优化清单

从"能跑"到"好用",通常需要优化这些点:

  • 数据清洗:去重、去噪、统一术语("LLM"和"大语言模型"标准化)
  • 分块调优:从512 Token开始,根据检索效果调整大小和重叠
  • 元数据标注:来源、时间、分类、权限,过滤和溯源全靠它
  • 混合检索:向量+关键词,解决精确匹配问题(人名/编号/错误码)
  • Rerank精排:粗筛Top-50→Cross-Encoder精排→Top-5,召回率+精度双提升
  • 查询改写:用户问题口语化/模糊,先用LLM改写成标准查询再检索
  • 父子分块:小块检索精准,大块生成有上下文
  • 缓存策略:相同/相似问题直接返回缓存结果,省Token省延迟
  • 评估体系:用RAGAS或自建评测集,定期测检索准确率和回答质量

最后说一句大实话:RAG效果不好,80%的问题出在数据质量和分块策略上,而不是模型不够强。与其花时间换更贵的大模型,不如把文档清洗干净、分块调好,性价比高得多