# 使用 Python + LangChain 构建本地 RAG 问答系统
## 背景介绍
大语言模型(LLM)现在很火,但直接用它有个很现实的问题:模型的知识都是训练数据喂出来的,没法及时更新,也没法让它理解你公司内部的那些保密文档。
这时候 RAG(检索增强生成)就派上用场了。简单说,就是先把你的文档转换成向量存起来,有人问问题的时候,系统去向量库里找最相关的内容,然后把找到的内容和问题一起喂给 LLM,让它根据这些材料来回答。
这样做的好处很明显:既能用上 LLM 的生成能力,又解决了知识过时和数据隐私的问题。
LangChain 是现在做 LLM 应用最常用的框架,封装了很多现成的工具,不用自己从零造轮子。本文就手把手教你怎么用 Python + LangChain 搭一个本地运行的 RAG 问答系统。
## 问题描述
实际工作中,需要让 AI 处理私有文档的场景太多了:
– 公司内部知识库
– 技术文档智能客服
– 论文摘要和问答
– 合同分析
传统做法是把整篇文档直接丢给 LLM,且不说 LLM 有上下文长度限制(一次只能处理那么多字),就算能处理,把一堆无关内容也塞进去,既浪费 token 又影响回答质量。
RAG 的做法是先把文档切成小块,转换成向量存到向量数据库里。问问题的时候,去向量库里找最匹配的文档块,只把这些相关内容发给 LLM。这样既不怕文档太长,又能让回答更准。
## 详细步骤
整个流程跑下来就这几步:
### 1. 环境准备
装好需要的 Python 包:
“`bash
pip install langchain langchain-community langchain-openai chromadb pypdf python-dotenv
“`
每个包的作用:
– langchain:核心框架
– langchain-community:各种集成好的组件
– langchain-openai:OpenAI 模型接口
– chromadb:向量数据库
– pypdf:读 PDF
– python-dotenv:管理环境变量
### 2. 加载和处理文档
用 LangChain 的文档加载器读取文件,然后切成小块。
### 3. 向量存储和检索
把文本块转成向量,存进 Chroma。查询时做相似度检索。
### 4. 搭问答链
把检索结果和问题组合,调用 LLM 生成回答。
## 完整代码示例
上代码,一个完整的 RAG 问答系统:
“`python
import os
from dotenv import load_dotenv
from langchain_community.document_loaders import PyPDFLoader
from langchain.text_splitter import RecursiveCharacterTextSplitter
from langchain_community.vectorstores import Chroma
from langchain_openai import OpenAIEmbeddings
from langchain.chains import RetrievalQA
from langchain_openai import OpenAI
# 加载环境变量
load_dotenv()
# 配置 OpenAI API Key
os.environ[“OPENAI_API_KEY”] = os.getenv(“OPENAI_API_KEY”, “your-api-key”)
class RAGQuestionAnsweringSystem:
“””本地 RAG 问答系统”””
def __init__(self, persist_directory=”./chroma_db”):
self.persist_directory = persist_directory
self.embeddings = OpenAIEmbeddings(model=”text-embedding-3-small”)
self.vectorstore = None
self.qa_chain = None
def load_pdf(self, pdf_path):
“””加载 PDF 文档”””
loader = PyPDFLoader(pdf_path)
documents = loader.load()
print(f”已加载 {len(documents)} 页文档”)
return documents
def split_documents(self, documents, chunk_size=1000, chunk_overlap=200):
“””分割文档为较小的片段”””
text_splitter = RecursiveCharacterTextSplitter(
chunk_size=chunk_size,
chunk_overlap=chunk_overlap,
length_function=len,
)
texts = text_splitter.split_documents(documents)
print(f”文档已分割为 {len(texts)} 个片段”)
return texts
def create_vectorstore(self, texts):
“””创建向量数据库”””
self.vectorstore = Chroma.from_documents(
documents=texts,
embedding=self.embeddings,
persist_directory=self.persist_directory
)
print(“向量数据库创建成功”)
return self.vectorstore
def load_vectorstore(self):
“””加载已存在的向量数据库”””
self.vectorstore = Chroma(
persist_directory=self.persist_directory,
embedding_function=self.embeddings
)
print(“向量数据库加载成功”)
return self.vectorstore
def setup_qa_chain(self, llm_model=”gpt-3.5-turbo”):
“””设置问答链”””
llm = OpenAI(model=llm_model, temperature=0)
self.qa_chain = RetrievalQA.from_chain_type(
llm=llm,
chain_type=”stuff”,
retriever=self.vectorstore.as_retriever(
search_type=”similarity”,
search_kwargs={“k”: 3}
),
return_source_documents=True
)
print(“问答链设置完成”)
return self.qa_chain
def ask(self, question):
“””提问并获取回答”””
if not self.qa_chain:
raise ValueError(“请先设置问答链”)
result = self.qa_chain({“query”: question})
print(f”\n问题: {question}”)
print(f”回答: {result[\”result\”]}”)
print(f”\n参考文档片段数: {len(result[\”source_documents\”])}”)
return result
def main():
# 初始化系统
rag_system = RAGQuestionAnsweringSystem(persist_directory=”./my_knowledge_base”)
# 示例:加载 PDF 文档
# pdf_path = “./documents/技术文档.pdf”
# documents = rag_system.load_pdf(pdf_path)
# texts = rag_system.split_documents(documents)
# rag_system.create_vectorstore(texts)
# 如果已有向量数据库,直接加载
rag_system.load_vectorstore()
rag_system.setup_qa_chain()
# 开始问答
questions = [
“这篇文章的主要内容是什么?”,
“作者的主要观点是什么?”,
“有哪些关键结论?”
]
for question in questions:
rag_system.ask(question)
print(“-” * 50)
if __name__ == “__main__”:
main()
“`
代码逻辑是这样的:先加载 PDF,用文本分割器把文档切块,用 OpenAI 的嵌入模型把文本转成向量存进 Chroma。问答的时候,从向量库找出最相关的 3 个文档块,连同问题一起发给 LLM 生成回答。
## 运行结果
跑一下,输出大概是这样:
“`
问题: 这篇文章的主要内容是什么?
回答: 根据文档内容,这篇文章主要讨论了人工智能在自然语言处理领域的发展历程…(具体回答取决于文档内容)
参考文档片段数: 3
————————————————–
问题: 作者的主要观点是什么?
回答: 作者认为大语言模型的出现标志着人工智能进入了一个新的发展阶段…
“`
系统从知识库找到了相关内容,给出了有针对性的回答。返回 3 个参考文档片段,说明 LLM 是基于这些材料生成的回答。
## 总结
本文带你用 Python + LangChain 搭了一个本地 RAG 问答系统。向量检索技术解决了 LLM 处理长文档和私有数据的老大难问题。
这个方案的好处:
– **没有长度限制**:文档再长也能处理,靠的是检索而不是全塞进去
– **数据留在本地**:不用发给第三方,不用担心泄露
– **回答更准**:只给 LLM 跟问题相关的内容
– **扩展方便**:想加新文档,改改代码跑一下就行
实际落地的时候,可以根据具体情况调优:改改文档怎么切、换个 embedding 模型、或者用更贵的 LLM。RAG 这个思路本身很通用,是做智能知识库的好起点。