手把手教你从零搭建一个RAG知识库问答系统
为什么你需要一个RAG系统?
先问一个问题:你调用大模型API时,有没有遇到过这些尴尬时刻?
问它你公司内部的操作手册,它一本正经地胡说八道
让它分析你刚收到的客户邮件,它说“我的知识截止到2024年”
想让它帮你从一堆文档里找答案,你得手动复制粘贴好几轮
根本原因很简单:大模型没有你的私有数据。
RAG(Retrieval-Augmented Generation,检索增强生成)就是解决这个问题的标准方案。它的核心思路极其朴素:
在让AI回答之前,先从你的知识库里检索相关内容,然后把这些内容“塞”进提示词里,让AI基于这些资料来回答。
这样一来,AI就从一个“什么都懂一点但什么都不精”的博学家,变成了你专属的“读过你所有文档的实习生”。
整体架构:一张图看懂RAG
在动手之前,我们先看清全貌。一个完整的RAG系统只有两大阶段:
阶段一:数据索引(离线准备,做一次就行)
你的文档(PDF/Word/网页) ↓ 文档加载器(读取) ↓ 文本分割器(切成小段) ↓ 嵌入模型(转为向量) ↓ 向量数据库(存储索引)
阶段二:问答检索(在线执行,每次提问都走一遍)
用户提问 ↓ 问题向量化(用同一个嵌入模型) ↓ 向量相似度检索(在数据库里找最相关的Top-K段落) ↓ 构建提示词(检索到的内容 + 用户问题) ↓ 大模型生成回答 ↓ 返回给用户
这套架构没什么高深的理论,但组合起来就能解决大模型的“失忆症”问题。下面我们一步一步把它变成可运行的代码。
准备工作:安装依赖
我选择的技术栈是 Python + LangChain + ChromaDB + OpenAI风格的API,这是目前最主流、最轻量的组合。
pip install langchain langchain-community chromadb pypdf sentence-transformers
说明:如果你用的是国内大模型(文心、通义、智谱等),只需替换嵌入模型和LLM的调用方式即可,架构完全一样。本文示例用开源的
sentence-transformers做嵌入,避免依赖特定云服务。
Step 1:加载文档(以PDF为例)
from langchain_community.document_loaders import PyPDFLoader# 加载PDF文件loader = PyPDFLoader("你的文档.pdf")documents = loader.load()print(f"共加载了 {len(documents)} 页内容")print(documents[0].page_content[:200]) # 预览第一页的前200字PyPDFLoader会把PDF每一页作为一个独立的Document对象,包含文本内容和元数据(页码等)。
如果报错:pypdf 库没装全,执行 pip install pypdf 即可。
Step 2:文本分割(核心步骤)
为什么要把文档切成小段?因为大模型的上下文窗口有限(即使是128K的模型,塞太多不相关内容也会稀释注意力),而且向量检索的精度也要求文本块足够聚焦。
from langchain.text_splitter import RecursiveCharacterTextSplitter# 创建一个文本分割器text_splitter = RecursiveCharacterTextSplitter(
chunk_size=500, # 每块最大500个字符
chunk_overlap=50, # 块与块之间重叠50个字符(防止切断关键信息)
separators=["\n\n", "\n", "。", "!", "?", ",", " ", ""])# 执行分割chunks = text_splitter.split_documents(documents)print(f"文档被切成了 {len(chunks)} 个文本块")关键参数解释:
chunk_size=500:不是越大越好。一般500-1000字符适合中文场景,太长检索精度下降,太短丢失上下文。chunk_overlap=50:重叠部分能保证一个完整的知识点不会被拦腰切断。
如果你处理的文档很规整(比如技术手册),可以试试 MarkdownHeaderTextSplitter,按标题层级切分,效果更佳。
Step 3:向量化并存入数据库
这一步把文字变成“语义坐标”,让计算机能计算“苹果”和“水果”比“苹果”和“汽车”更接近。
from langchain_community.embeddings import HuggingFaceEmbeddingsfrom langchain_community.vectorstores import Chroma# 加载开源的嵌入模型(中文优化版)embeddings = HuggingFaceEmbeddings(
model_name="shibing624/text2vec-base-chinese")# 创建向量数据库(自动完成向量化和存储)vectorstore = Chroma.from_documents(
documents=chunks,
embedding=embeddings,
persist_directory="./chroma_db" # 持久化目录)print("向量数据库创建完成!")运行完这步,你的本地目录下会出现一个 ./chroma_db 文件夹,里面就是索引好的向量数据。下次运行时,你可以直接加载这个目录,无需重新索引。
如果你用的是OpenAI的Embeddings API,可以替换为:
from langchain_openai import OpenAIEmbeddings embeddings = OpenAIEmbeddings(api_key="你的key")
Step 4:实现检索函数
def search_docs(query, k=3):
"""检索与问题最相关的K个文本块"""
results = vectorstore.similarity_search(query, k=k)
return results# 测试一下test_query = "这个产品的主要功能是什么?"relevant_chunks = search_docs(test_query)for i, chunk in enumerate(relevant_chunks):
print(f"\n--- 相关段落 {i+1} ---")
print(chunk.page_content[:150] + "...")这个函数返回的是最相关的文本块列表,按相似度从高到低排序。
Step 5:组合RAG问答(最后一步)
把检索到的内容和用户的提问组合成一个完整的提示词,送给大模型。
from langchain_openai import ChatOpenAI# 初始化大模型(这里用OpenAI格式,可替换为国内模型)llm = ChatOpenAI(
model="gpt-3.5-turbo",
temperature=0.3,
api_key="你的key", # 或用环境变量
base_url="https://api.openai.com/v1" # 国内模型请替换对应地址)def rag_ask(question):
# 1. 检索相关文档
docs = search_docs(question, k=3)
# 2. 拼接上下文
context = "\n\n".join([doc.page_content for doc in docs])
# 3. 构建提示词
prompt = f"""你是一个专业的文档问答助手。请根据以下参考资料回答问题。如果参考资料中没有相关信息,请直接说"资料中没有提到相关内容",不要编造。
参考资料:{context}问题:{question}回答:"""
# 4. 调用大模型
response = llm.invoke(prompt)
return response.content# 测试answer = rag_ask("这个产品的退款政策是什么?")print(answer)完整代码汇总(拿来即用)
把上面所有代码片段拼在一起,就是一个完整的RAG系统。为了方便你直接运行,我整理了一个整合版本(只有50行左右):
# 安装依赖:pip install langchain langchain-community chromadb pypdf sentence-transformersfrom langchain_community.document_loaders import PyPDFLoaderfrom langchain.text_splitter import RecursiveCharacterTextSplitterfrom langchain_community.embeddings import HuggingFaceEmbeddingsfrom langchain_community.vectorstores import Chromafrom langchain_openai import ChatOpenAI# ---------- 配置 ----------PDF_PATH = "你的文档.pdf"EMBEDDING_MODEL = "shibing624/text2vec-base-chinese"OPENAI_API_KEY = "你的key" # 或从环境变量读取# ---------- 1. 加载 ----------loader = PyPDFLoader(PDF_PATH)docs = loader.load()# ---------- 2. 分割 ----------splitter = RecursiveCharacterTextSplitter(chunk_size=500, chunk_overlap=50)chunks = splitter.split_documents(docs)# ---------- 3. 索引 ----------embeddings = HuggingFaceEmbeddings(model_name=EMBEDDING_MODEL)vectorstore = Chroma.from_documents(chunks, embeddings, persist_directory="./db")vectorstore.persist()# ---------- 4. 检索 ----------def retrieve(query, k=3):
return vectorstore.similarity_search(query, k=k)# ---------- 5. 问答 ----------llm = ChatOpenAI(model="gpt-3.5-turbo", api_key=OPENAI_API_KEY)def ask(question):
docs = retrieve(question)
context = "\n\n".join([d.page_content for d in docs])
prompt = f"根据资料回答:\n{context}\n\n问题:{question}"
return llm.invoke(prompt).content# ---------- 6. 运行 ----------if __name__ == "__main__":
while True:
q = input("\n请输入问题(输入q退出):")
if q.lower() == 'q':
break
print("回答:", ask(q))进阶优化(效果翻倍的几个小技巧)
如果你发现回答质量不够理想,按以下优先级逐一优化:
| 优化点 | 做法 | 预期效果 |
|---|---|---|
| ① 调整分块大小 | 根据文档类型尝试 chunk_size=300 或 800 | 检索精度提升 |
| ② 增加检索数量 | k=5 给模型更多上下文 | 召回率提升 |
| ③ 优化提示词 | 明确要求"如果找不到就直说,不要编造" | 减少幻觉 |
| ④ 换嵌入模型 | 用 BAAI/bge-large-zh-v1.5(目前中文SOTA) | 语义检索更准 |
| ⑤ 增加重排序 | 检索后用 CohereRerank 再排序一次 | 相关段落更靠前 |
其中最立竿见影的是④和⑤,但①-③不需要额外成本,建议先调优。
常见问题排查
Q:加载PDF时报错“No module named 'pypdf'”?
pip install pypdf
Q:向量数据库已经存在,如何增量添加新文档?
vectorstore = Chroma(persist_directory="./db", embedding_function=embeddings)vectorstore.add_documents(new_chunks)
Q:用国内大模型怎么替换?
以智谱为例:
from langchain_community.chat_models import ChatZhipuAI llm = ChatZhipuAI(model="glm-4", api_key="你的key")
其他模型替换方式类似,只需修改 ChatOpenAI 为对应SDK即可。
到这里,你已经亲手搭建了一个完整的RAG知识库问答系统。别小看这50行代码,它背后的架构和当下最火的AI产品(比如各个大厂的文档助手、企业知识库)在核心原理上完全一致。
接下来你可以做什么?
把你的工作文档、学习笔记、个人博客丢进去,做一个专属知识库
加上一个Web界面(Streamlit或Gradio),变成可分享的产品
接入飞书或微信机器人,让团队都能用
动手跑起来吧,遇到任何具体报错,欢迎留言讨论。下一篇文章,我会写如何用这个RAG系统做更复杂的Agent应用,敬请期待。
📌 完整代码已上传,建议你对着文章手敲一遍,理解会比直接复制深刻得多。动手过程中有任何卡点,欢迎在评论区交流。