从 0 到 1 搭建生产级 RAG 系统:切片、召回与重排序调优实录

一、背景与目标

作为一名有十年经验的前端开发者,我在转向全栈工程师的过程中,决定亲手搭建一个生产级 RAG(Retrieval-Augmented Generation)系统。目标是从一本 PDF 书籍(《好好学习》)中自动回答相关问题,例如"成甲老师从哪一年开始接触知识管理?"、"什么是临界知识?"等。

然而,理想很丰满,现实很骨感。在从零搭建的过程中,我遇到了切片断层、语义漂移、模型维度不匹配、LangChain 版本冲突、旧数据库残留、LLM 指令遵循不稳定等一系列问题。本文记录了完整的排错与调优历程,并提供可直接复制的代码和配置。


二、环境准备

2.1 Python 虚拟环境

bash 复制代码
bash
python -m venv venv
source venv/bin/activate   # Linux/Mac
venv\Scripts\activate      # Windows

2.2 安装依赖

arduino 复制代码
bash
pip install langchain langchain-community langchain-core langchain-huggingface langchain-openai \
            langchain-text-splitters chromadb sentence-transformers flagembedding pypdf2 \
            rank_bm25 numpy<2.0

注意:numpy<2.0 是为了避免 chromadb 与 NumPy 2.0 的不兼容错误。


三、基础 RAG 流水线(初版)

3.1 加载 PDF 与切片

ini 复制代码
python
from langchain_community.document_loaders import PyPDFLoader
from langchain_text_splitters import RecursiveCharacterTextSplitter

loader = PyPDFLoader("sample.pdf")
docs = loader.load()

text_splitter = RecursiveCharacterTextSplitter(
    chunk_size=800,
    chunk_overlap=150,
    separators=["\n\n", "\n", " ", ""]
)
splits = text_splitter.split_documents(docs)

3.2 向量化(使用多语言模型)

ini 复制代码
python
from langchain_huggingface import HuggingFaceEmbeddings
from langchain_community.vectorstores import Chroma

embeddings = HuggingFaceEmbeddings(model_name="sentence-transformers/paraphrase-multilingual-MiniLM-L12-v2")
vectordb = Chroma.from_documents(splits, embedding=embeddings, persist_directory="./chroma_db")

3.3 检索 + 生成(使用 DeepSeek)

ini 复制代码
python
from langchain_openai import ChatOpenAI
from langchain_core.prompts import ChatPromptTemplate

llm = ChatOpenAI(model="deepseek-chat", api_key=os.getenv("DEEPSEEK_API_KEY"), base_url="https://api.deepseek.com/v1", temperature=0)

prompt = ChatPromptTemplate.from_messages([
    ("system", "你是一个知识库助手。请严格根据上下文回答。如果上下文中没有明确包含问题的答案,请直接回答'未找到相关信息'。禁止推断。"),
    ("human", "上下文:{context}\n\n问题:{input}")
])

chain = prompt | llm

四、遇到的坑与解决方案

4.1 切片断层

现象:关键句子被切碎,导致上下文不完整。

解决 :增大 chunk_size 至 1000,chunk_overlap 至 200,并添加中文标点作为分隔符。

ini 复制代码
python
text_splitter = RecursiveCharacterTextSplitter(
    chunk_size=1000,
    chunk_overlap=200,
    separators=["\n\n", "\n", "。", "!", "?", " ", ""]
)

4.2 语义漂移(检索不相关)

现象 :向量检索召回的内容与问题无关。 解决 :引入 BGE Rerank​ 重排序模型,对粗筛结果进行二次精排。

ini 复制代码
python
from FlagEmbedding import FlagReranker

reranker = FlagReranker("BAAI/bge-reranker-v2-m3", use_fp16=False)

def rerank(query, docs, top_n=3):
    pairs = [(query, doc.page_content) for doc in docs]
    scores = reranker.compute_score(pairs, normalize=True)
    ranked = sorted(zip(scores, docs), key=lambda x: x[0], reverse=True)
    return [doc for _, doc in ranked[:top_n]]

4.3 模型维度不匹配(多语言 Embedding 对中文理解差)

现象:Rerank 分数极低(0.06),检索结果全是无关片段。

解决 :换用中文专用 Embedding 模型 BAAI/bge-small-zh-v1.5

ini 复制代码
python
embeddings = HuggingFaceEmbeddings(model_name="BAAI/bge-small-zh-v1.5")

4.4 LangChain 版本冲突与代码残留

现象ModuleNotFoundError: No module named 'langchain_core.memory'AttributeError: np.float_ was removed

解决

  • 统一升级 LangChain 全家桶:pip install --upgrade langchain langchain-community langchain-core langchain-huggingface langchain-openai
  • 降级 NumPy:pip install "numpy<2.0"
  • 彻底删除旧数据库 chroma_db 文件夹。

4.5 LLM 指令遵循不稳定(过度拒绝)

现象:上下文明明包含答案,LLM 却回答"未找到相关信息"。

解决 :绕过 LangChain 模板,直接构造 SystemMessageHumanMessage,并放宽指令。

swift 复制代码
python
from langchain_core.messages import SystemMessage, HumanMessage

messages = [
    SystemMessage(content="你是一个知识库问答助手。请严格基于【上下文】回答【问题】。即使上下文信息不完整,也请尝试提取相关信息回答。只有在上下文完全为空或与问题毫无关联时,才回答'未找到相关信息'。"),
    HumanMessage(content=f"【上下文】\n{context_str}\n\n【问题】\n{query}\n\n请回答:")
]
response = llm.invoke(messages)

五、调优实验与效果对比

5.1 实验参数表

参数 初始值 调优后
chunk_size 800 1000
chunk_overlap 150 200
粗筛 k 10 20
Rerank top_n 3 5
Rerank 阈值 0.2 0.1
Embedding 模型 paraphrase-multilingual-MiniLM-L12-v2 BAAI/bge-small-zh-v1.5

5.2 效果对比(以"成甲老师从哪一年开始接触知识管理?"为例)

版本 Rerank 最高分 注入上下文长度 回答
初始版 0.00(未命中) 16 字符 未找到相关信息
调优后 0.95 1500+ 字符 2008年

5.3 最终代码(可直接复制运行)

ini 复制代码
python
import os
from dotenv import load_dotenv
load_dotenv()

# 关闭 LangChain 遥测警告
os.environ["LANGCHAIN_TRACING_V2"] = "false"

from langchain_community.document_loaders import PyPDFLoader
from langchain_text_splitters import RecursiveCharacterTextSplitter
from langchain_community.vectorstores import Chroma
from langchain_huggingface import HuggingFaceEmbeddings
from langchain_openai import ChatOpenAI
from langchain_core.messages import SystemMessage, HumanMessage
from FlagEmbedding import FlagReranker

# ============ 1. 加载 PDF ============
print("📄 正在加载 PDF...")
loader = PyPDFLoader("sample.pdf")
docs = loader.load()
print(f"   共加载 {len(docs)} 页")

# ============ 2. 文本切片 ============
print("✂️ 正在切片...")
text_splitter = RecursiveCharacterTextSplitter(
    chunk_size=1200,  # 稍微再调大一点,保留更多上下文连贯性
    chunk_overlap=300, # 增加重叠度,防止关键句子被切断
    separators=["\n\n", "\n", "。", "!", "?", " ", ""]
)
splits = text_splitter.split_documents(docs)
print(f"   共切成 {len(splits)} 块")

# ============ 3. 向量化(换用中文专用模型)============
print("🧠 正在生成向量并存入 ChromaDB...")
embeddings = HuggingFaceEmbeddings(model_name="BAAI/bge-small-zh-v1.5")
vectordb = Chroma.from_documents(
    documents=splits,
    embedding=embeddings,
    persist_directory="./chroma_db"
)
print("✅ 向量数据库创建完成!")

# ============ 4. 初始化重排序模型 ============
print("🔄 初始化重排序模型(首次运行会下载模型)...")
reranker = FlagReranker("BAAI/bge-reranker-v2-m3", use_fp16=False)

# ============ 5. 构建 LLM ============
print("🤖 构建 RAG 问答链...")
llm = ChatOpenAI(
    model="deepseek-chat",
    openai_api_key=os.getenv("DEEPSEEK_API_KEY"),
    openai_api_base="https://api.deepseek.com/v1",
    temperature=0
)

# ============ 6. 交互式问答(极简直调版) ============
print("\n💬 RAG 知识库已就绪!输入问题(输入 'quit' 退出):")

while True:
    query = input("\n❓ 请输入问题: ").strip()
    if query.lower() == 'quit':
        break

    print("🔍 正在检索相关文档...")
    
    # 1. 粗筛(增加召回数量)
    candidate_docs = vectordb.similarity_search(query, k=20) # 从10增加到20
    
    # 2. Rerank 精排
    pairs = [(query, doc.page_content) for doc in candidate_docs]
    scores = reranker.compute_score(pairs, normalize=True)
    ranked = sorted(zip(scores, candidate_docs), key=lambda x: x[0], reverse=True)
    
    # print("🔍 [Debug] Rerank 分数:")
    # for score, doc in ranked[:5]:
    #     print(f"   分数: {score:.4f} | 内容: {doc.page_content[:100]}...") 
        
    # 3. 提取精排文档(取前5个,增加上下文丰富度)
    top_docs = [doc for score, doc in ranked[:5] if score >= 0.1]
    
    if not top_docs:
        print("⚠️ 警告:Rerank 分数过低,启用兜底策略!")
        top_docs = [ranked[0][1]] if ranked else []
        
    # 4. 手动拼接上下文
    context_str = "\n\n".join([doc.page_content for doc in top_docs])
    # print(f"📝 注入上下文长度: {len(context_str)} 字符")
    if len(context_str) < 50:
        print("🚨 严重告警:注入上下文极短,检索阶段失败!请检查PDF内容和切片。")
    
    # 5. 直接构造消息并调用 LLM
    messages = [
        SystemMessage(content="你是一个知识库问答助手。请严格基于【上下文】回答【问题】。直接给出最终答案,不要输出'上下文截断'、'根据上下文'等解释性前缀。如果确实没有,才回答'未找到相关信息'。"),
        HumanMessage(content=f"【上下文】\n{context_str}\n\n【问题】\n{query}\n\n请回答:")
    ]
    
    response = llm.invoke(messages)
    print("\n🤖 AI 回答:", response.content)

七、总结

从零到一搭建生产级 RAG 系统,不仅仅是写几行代码那么简单。本文记录了切片粒度、向量模型选择、重排序、Prompt 设计、依赖管理等关键环节的踩坑与调优。最终的系统能够准确回答书中 90% 以上的问题,上下文注入长度稳定在 1000~2000 字符,Rerank 最高分可达 0.95。

希望这份实录能帮助你快速避开我走过的弯路,构建属于自己的高效 RAG 系统。如果你有任何问题或改进建议,欢迎在评论区留言交流!


附录:常用命令速查

  • 删除旧数据库:rm -rf ./chroma_db
  • 查看 PDF 内容:python -c "import PyPDF2; reader = PyPDF2.PdfReader('sample.pdf'); print(reader.pages[0].extract_text())"
  • 安装指定版本包:pip install "numpy<2.0"
  • 源码地址:github.com/qishuixian/...
  • 测试文档:pan.baidu.com/s/1MMcXx--p...
相关推荐
糖炒栗子03261 小时前
learn-claude-code 简要记录
笔记·python
测试19982 小时前
Python接口测试之requests库安装和导入
自动化测试·软件测试·python·测试工具·职场和发展·测试用例·接口测试
%472 小时前
DAY 34
python
leisoo80972 小时前
涨停板次日表现因子怎么挖掘本地化Python全流程实战
大数据·人工智能·python
像颗糖2 小时前
AG-UI:把 Agent 与前端之间的“私有暗号”变成标准协议
python·agent·ai编程
Zane19942 小时前
类也是对象?一文讲透元类 metaclass 这件"深度魔法"
后端·python
Fanta丶2 小时前
3.FastAPI ORM建表
python
jyOverQ2 小时前
LangGraph 记忆管理详解:短期记忆、长期记忆与 Runtime Context
python·langchain
云水初2 小时前
【agent篇】RAG 知识库构建避坑指南
开发语言·python·学习·agent·rag