# RAG 实战:如何用 LangChain 构建本地知识库问答系统
如果你用过 ChatGPT 这类大语言模型,大概会遇到这些问题:
– 模型的知识截止日期太早,最新的专业内容它根本不知道
– 每次都得在 Prompt 里塞一大段背景信息,超长度是家常便饭
– 模型有时候会一本正经地胡说八道,给你一个看起来对但实际上错得离谱的答案
这些问题都能用 RAG(Retrieval Augmented Generation,检索增强生成)来解决。RAG 的思路其实很朴素:别让模型凭空编答案,先从知识库里找到相关的内容,再让模型根据这些内容生成回答。
这篇文章手把手教你用 Python + LangChain + Chroma 在本地搭一个知识库问答系统。整个过程不需要任何外部 API,所有数据都存在你自己电脑上。
—
## 背景介绍
事情是这样的。我之前需要处理大量技术文档——公司的技术方案、团队分享、开源项目的手册林林总总加起来有好几百份。
最开始我把所有文档都塞进 Prompt,结果马上撞墙了:
– 单个 Prompt 有长度限制,根本装不下
– 每次都要在超长的上下文里让模型找重点,响应慢得让人想砸键盘
– 有些内容模型根本记不住,或者给记错了
后来我了解到 RAG 这个方案,发现它简直就是为这种情况量身定做的。RAG 的工作流程分三步走:
1. **文档处理**:把长文本切成小块,转换成向量
2. **向量检索**:用户提问时,把问题也转换成向量,在向量数据库里搜相似的内容
3. **答案生成**:把检索到的内容当作上下文,让模型基于这些来回答
这就像你看书时遇到不懂的地方,先去索引里翻相关章节,然后结合这些章节来理解。模型也是这么干的。
—
## 环境准备
先装必要的依赖库。在终端里执行:
“`bash
pip install langchain langchain-community chromadb tiktoken unstructured \
langchain-openai faiss-cpu pypdf
“`
如果你用 OpenAI 的 API(得自己准备 Key),可以把 `langchain-openai` 换成你用的模型供应商。如果想完全离线跑,我推荐用 Ollama 支持的本地模型。
建项目目录和文件结构:
“`bash
mkdir -p rag_demo/{documents,vectors}
cd rag_demo
“`
我们在目录下建一个 `main.py`,再加一个 `documents` 文件夹扔原始文档。
—
## 文档处理:把文本变成向量
核心是把文档转成向量,存进向量数据库。这步通常跑一次就行,之后每次查询都是对已建好的向量库操作。
“`python
from langchain_community.document_loaders import PyPDFLoader, TextLoader
from langchain.text_splitter import RecursiveCharacterTextSplitter
from langchain_community.vectorstores import Chroma
from langchain_openai import OpenAIEmbeddings
import os
# 配置
DOCS_DIR = “./documents”
VECTOR_STORE_DIR = “./vectors”
def load_documents():
“””加载目录下的所有文档”””
documents = []
# 支持 PDF 和 TXT
for file in os.listdir(DOCS_DIR):
filepath = os.path.join(DOCS_DIR, file)
if file.endswith(“.pdf”):
loader = PyPDFLoader(filepath)
documents.extend(loader.load())
elif file.endswith(“.txt”):
loader = TextLoader(filepath, encoding=”utf-8″)
documents.extend(loader.load())
return documents
def split_documents(documents, chunk_size=500, chunk_overlap=50):
“””把长文档切成小块”””
text_splitter = RecursiveCharacterTextSplitter(
chunk_size=chunk_size,
chunk_overlap=chunk_overlap,
length_function=len,
)
return text_splitter.split_documents(documents)
def create_vector_store(documents):
“””创建向量数据库”””
# 用 OpenAI 的 embedding 模型
embeddings = OpenAIEmbeddings()
# 或者用本地模型(需要提前启动 Ollama)
# from langchain_community.embeddings import OllamaEmbeddings
# embeddings = OllamaEmbeddings(model=”nomic-embed-text”)
vector_store = Chroma.from_documents(
documents=documents,
embedding=embeddings,
persist_directory=VECTOR_STORE_DIR
)
vector_store.persist()
return vector_store
def init_knowledge_base():
“””初始化知识库”””
print(“正在加载文档…”)
documents = load_documents()
print(f”共加载 {len(documents)} 个文档”)
print(“正在切分文档…”)
split_docs = split_documents(documents)
print(f”切分后得到 {len(split_docs)} 个文本块”)
print(“正在创建向量数据库…”)
vector_store = create_vector_store(split_docs)
print(“知识库创建完成!”)
return vector_store
if __name__ == “__main__”:
init_knowledge_base()
“`
这段代码干了三件事:
1. **加载文档**:支持 PDF 和 TXT 两种格式,想加其他格式的自己扩展
2. **切分文本**:把长文档切成 500 字符左右的小块,相邻块之间重叠 50 字符,保证检索时上下文不断
3. **生成向量**:调用 embedding 模型,把每个文本块转成向量,存进 Chroma 数据库
运行:
“`bash
python main.py
“`
输出差不多是这样:
“`
正在加载文档…
共加载 3 个文档
正在切分文档…
切分后得到 45 个文本块
正在创建向量数据库…
知识库创建完成!
“`
耗时取决于文档数量和大小,从几秒到几分钟不等。向量库会保存在 `./vectors` 目录下,下次直接加载就行,不用重建。
—
## 问答:检索 + 生成
知识库建好就能用来回答问题了。
“`python
from langchain_openai import ChatOpenAI
from langchain import PromptTemplate
from langchain.chains import RetrievalQA
def load_vector_store():
“””加载已有的向量数据库”””
embeddings = OpenAIEmbeddings()
return Chroma(
persist_directory=VECTOR_STORE_DIR,
embedding=embeddings
)
def create_qa_chain(vector_store):
“””创建问答链”””
llm = ChatOpenAI(
model=”gpt-3.5-turbo”,
temperature=0
)
# 自定义 Prompt 模板
template = “””你是一个专业的技术文档问答助手。请根据以下参考内容来回答用户的问题。
参考内容:
{context}
用户问题:{question}
请根据参考内容回答。如果参考内容中没有相关信息,请如实说明你没有找到相关内容,不要编造答案。
“””
prompt = PromptTemplate(
template=template,
input_variables=[“context”, “question”]
)
chain = RetrievalQA.from_chain_type(
llm=llm,
chain_type=”stuff”,
retriever=vector_store.as_retriever(
search_kwargs={“k”: 3}
),
chain_type_kwargs={“prompt”: prompt},
return_source_documents=True
)
return chain
def chat(vector_store):
“””交互式问答”””
chain = create_qa_chain(vector_store)
print(“\n===== 知识库问答系统 =====”)
print(“输入问题进行咨询,输入 ‘quit’ 退出\n”)
while True:
question = input(“问题: “).strip()
if question.lower() == “quit”:
print(“再见!”)
break
if not question:
continue
result = chain.invoke({“query”: question})
print(f”\n回答: {result[‘result’]}”)
print(f”\n参考来源: “, end=””)
for i, doc in enumerate(result[“source_documents”]):
source = doc.metadata.get(“source”, “未知”)
print(f”{i+1}. {source}”)
print()
if __name__ == “__main__”:
vector_store = load_vector_store()
chat(vector_store)
“`
运行:
“`bash
python chat.py
“`
然后就能问问题了。比如我之前扔了几篇 LangChain 相关的文档进去,问它”LangChain 支持哪些文档格式”,系统会从向量库里检索相关内容,然后把检索结果和用户问题一起发给模型,生成最终回答。
输出差不多这样:
“`
问题: LangChain 支持哪些文档格式
回答: 根据参考文档,LangChain 支持多种文档格式的加载,包括:
1. PDF 文件 – 使用 PyPDFLoader
2. 文本文件 – 使用 TextLoader
3. HTML 文件 – 使用 UnstructuredHTMLLoader
4. Markdown 文件 – 使用 MarkdownLoader
5. Word 文档 – 使用 DocxLoader
此外,LangChain 还支持从 URL、Notion、Discord 等平台加载内容。
参考来源:
1. langchain_docs.pdf
2. loaders_tutorial.pdf
“`
模型给出的答案直接来自文档内容,还标明了出处。如果你问一个文档里根本没有的问题,模型会如实告诉你没找到相关内容。
—
## 运行结果
系统跑起来之后,我测了几个典型场景:
**场景一:找具体问题**
– 输入:”如何在 LangChain 中使用自定义 Prompt?”
– 输出会检索到包含 Prompt 相关内容的文档,给出具体代码
**场景二:跨文档检索**
– 输入:”这个项目的技术架构是什么?”
– 就算相关描述分散在多个文档里,向量检索也能把它们都找出来
**场景三:不存在的内容**
– 输入:”这篇文档的作者是谁?”
– 检索不到时,模型会直接说没找到
响应速度取决于两点:向量检索通常几毫秒就搞定;模型 API 调用时间看你用什么模型,GPT-3.5-turbo 一般 2-3 秒。
—
## 总结
这趟实践下来,RAG 方案确实能打:
**优点**:
1. 文档库可以随时更新,解决模型知识过时的问题
2. 不用每次都传完整文档,Token 消耗省了一大半
3. 答案有据可查,能通过源码验证模型是不是在编
4. 本地部署,数据安全,敏感文档不用上传到外面
**得注意的地方**:
1. 文本切分的大小和重叠度会影响检索效果,具体数值要根据文档特点调
2. 向量检索的 Top-K 参数直接影响召回率和准确率
3. embedding 模型的选择决定了检索质量,OpenAI 效果最好,本地模型还在追赶
这个系统还能继续优化:加个 Web 界面让非技术人员也能用;定时增量更新知识库,不用每次重建;添加引用来源高亮,方便用户定位原文。
感兴趣的话可以继续深挖:怎么评估 RAG 系统的检索质量、怎么处理结构化数据(表格、数据库)、怎么结合 Agent 实现更复杂的多步推理。这些都挺有搞头的。