背景
你肯定遇到过这种情况:电脑里存了几百篇笔记,想让 AI 基于这些内容回答问题。普通的 ChatGPT 再强,也看不到你本地的东西——它的训练数据总有截止日期,而且不会读你的文件。
这就是 RAG 要解决的问题。RAG 全称 Retrieval Augmented Generation(检索增强生成),核心思路是:先从你的文档里找到相关内容,再让大模型根据这些内容回答。这样答案有据可查,不会瞎编;也能用到最新的、私有的信息。
这篇文章教你用 Python、LangChain 和 Ollama 在本地搭建一个完整的 RAG 系统。全部开源,不用任何付费 API。
问题
假设你的笔记文件夹长这样:
notes/
├── 2024-01-15-关于Go语言的思考.md
├── 2024-03-20-Python异步编程心得.md
├── 2024-06-10-分布式系统设计原则.md
└── 2024-11-05-如何优化数据库查询.md
现在你想问:”我之前有没有记录过缓存的设计模式?”
常规做法是把所有笔记复制粘贴到对话框。先不说麻烦,文件太多根本塞不进去。
RAG 就是来处理这个问题的:自动理解问题,找到最相关的文档块,让大模型基于这些内容生成答案。
详细步骤
准备工作
首先,我们需要安装必要的 Python 包。创建一个新的 Python 环境(建议使用 Python 3.10 或更高版本),然后执行以下命令:
pip install langchain langchain-community langchain-text-splitters chromadb ollama pypdf sentence-transformers
这些包分别是:
langchain:RAG 框架langchain-text-splitters:分割文档chromadb:向量数据库,存文档嵌入ollama:本地跑大模型sentence-transformers:生成文本向量
步骤一:启动本地大模型
还没装 Ollama?从官网下载安装。装好后在终端运行:
ollama serve
ollama pull llama3
这会下载 Llama 3(约 4.7GB)。如果网络不好,可以选 mistral 或 phi3 这类更小的模型。
步骤二:准备文档加载器
创建一个 loader.py,实现文档加载。LangChain 支持 Markdown、PDF、TXT、Word 等多种格式:
from langchain_community.document_loaders import DirectoryLoader, TextLoader
from langchain.text_splitter import RecursiveCharacterTextSplitter
from langchain.schema import Document
import os
def load_documents(folder_path: str):
"""加载指定文件夹下的所有文档"""
# 支持多种格式的加载器
loaders = {
'.md': TextLoader,
'.txt': TextLoader,
'.py': TextLoader,
}
all_documents = []
for root, dirs, files in os.walk(folder_path):
for file in files:
file_path = os.path.join(root, file)
ext = os.path.splitext(file)[1].lower()
if ext in loaders:
try:
loader = loaders[ext](file_path, encoding='utf-8')
docs = loader.load()
all_documents.extend(docs)
print(f"加载成功: {file_path}")
except Exception as e:
print(f"加载失败 {file_path}: {e}")
return all_documents
def split_documents(documents: list[Document], chunk_size: int = 500, chunk_overlap: int = 50):
"""将文档分割成小块"""
text_splitter = RecursiveCharacterTextSplitter(
chunk_size=chunk_size,
chunk_overlap=chunk_overlap,
length_function=len,
separators=["\n\n", "\n", "。", " ", ""]
)
return text_splitter.split_documents(documents)
步骤三:创建向量存储
文档需要转换成向量才能进行相似度搜索。我们使用 ChromaDB 作为向量数据库:
from langchain_community.vectorstores import Chroma
from langchain_community.embeddings import HuggingFaceEmbeddings
def create_vector_store(documents, persist_directory: str = "./chroma_db"):
"""创建向量存储"""
# 使用轻量级嵌入模型
embeddings = HuggingFaceEmbeddings(
model_name="sentence-transformers/all-MiniLM-L6-v2",
model_kwargs={'device': 'cpu'}
)
# 创建 Chroma 向量数据库
vector_store = Chroma.from_documents(
documents=documents,
embedding=embeddings,
persist_directory=persist_directory
)
vector_store.persist()
return vector_store
这里选择的 all-MiniLM-L6-v2 是一个轻量级的嵌入模型,在 CPU 上也能快速运行。如果你有 GPU,可以换成 BAAI/bge-large-zh-v1.5 等中文效果更好的模型。
步骤四:构建 RAG 链
现在是最关键的部分——构建完整的 RAG 问答链:
from langchain_community.chat_models import ChatOllama
from langchain.chains import RetrievalQA
from langchain.prompts import PromptTemplate
def build_rag_chain(vector_store):
"""构建 RAG 问答链"""
# 初始化本地大模型
llm = ChatOllama(
model="llama3",
temperature=0.7
)
# 提示词模板
prompt_template = """你是一个技术笔记助手。根据上下文回答问题。
上下文:
{context}
问题:{question}
如果上下文没有相关信息,直接说没有。"""
prompt = PromptTemplate(
template=prompt_template,
input_variables=["context", "question"]
)
# 创建问答链
qa_chain = RetrievalQA.from_chain_type(
llm=llm,
chain_type="stuff",
retriever=vector_store.as_retriever(
search_kwargs={"k": 3} # 找出最相关的 3 个文档块
),
chain_type_kwargs={"prompt": prompt},
return_source_documents=True
)
return qa_chain
步骤五:运行完整流程
把所有模块组合在一起,形成完整的主程序:
import os
def main():
# 配置路径
NOTES_FOLDER = "./notes"
VECTOR_DB_PATH = "./chroma_db"
print("=" * 50)
print("本地 RAG 问答系统")
print("=" * 50)
# 第一步:加载文档
print("\n[1/3] 正在加载文档...")
documents = load_documents(NOTES_FOLDER)
print(f"共加载 {len(documents)} 个文档")
# 第二步:分割文档
print("\n[2/3] 正在分割文档...")
chunks = split_documents(documents)
print(f"分割成 {len(chunks)} 个文本块")
# 第三步:创建向量存储
print("\n[3/3] 正在构建向量索引...")
if os.path.exists(VECTOR_DB_PATH):
# 加载已有的向量数据库
from langchain_community.vectorstores import Chroma
from langchain_community.embeddings import HuggingFaceEmbeddings
embeddings = HuggingFaceEmbeddings(
model_name="sentence-transformers/all-MiniLM-L6-v2"
)
vector_store = Chroma(
persist_directory=VECTOR_DB_PATH,
embedding_function=embeddings
)
else:
vector_store = create_vector_store(chunks, VECTOR_DB_PATH)
print("向量索引构建完成!")
# 构建问答链
qa_chain = build_rag_chain(vector_store)
# 交互式问答
print("\n" + "=" * 50)
print("系统准备就绪!请输入您的问题(输入 'quit' 退出)")
print("=" * 50 + "\n")
while True:
question = input("问题: ").strip()
if question.lower() == 'quit':
break
if not question:
continue
result = qa_chain({"query": question})
print("\n" + "-" * 50)
print("回答:")
print(result["result"])
print("-" * 50)
# 显示参考的文档来源
if result.get("source_documents"):
print("\n参考来源:")
for i, doc in enumerate(result["source_documents"], 1):
print(f" {i}. {doc.metadata.get('source', '未知来源')}")
print()
if __name__ == "__main__":
main()
运行结果
运行上述代码后,系统会先加载和索引文档。首次运行需要一些时间来完成向量化的过程。索引完成后,你可以开始提问:
问题: 我之前有没有记录过关于缓存的设计模式?
------------------------------------------------------------
回答:
根据你之前的技术笔记,你确实记录过缓存相关的设计模式。在
2024-06-10 的「分布式系统设计原则」文档中,你提到了以下几种缓存策略:
1. Cache-Aside(旁路缓存):应用程序首先检查缓存,如果不存在则从数据库加载并写入缓存。
2. Write-Through:数据同时写入缓存和数据库,保证强一致性。
3. Write-Behind:异步写入数据库,性能高但可能存在数据丢失风险。
4. Redis 集群的主从复制配置,用于高可用场景。
这些模式在分布式系统中非常实用,建议根据具体业务场景选择合适的策略。
------------------------------------------------------------
参考来源:
1. ./notes/2024-06-10-分布式系统设计原则.md
可以看到,系统准确地找到了相关笔记,并基于内容生成了准确的回答。回答不仅指出了具体的设计模式,还引用了原始文档的位置。
总结
这篇文章带你走完了搭建本地 RAG 系统的全过程。总结一下这个方案的几个优点:
完全本地化——数据都存在自己电脑上,不用上传到任何第三方服务。
开源免费——不依赖商业 API,一台普通电脑就能跑。
灵活扩展——可以换不同的嵌入模型、大模型,也能支持更多文档类型。
后续优化方向:
- 换成 BGE 等中文效果更好的嵌入模型
- 添加 PDF、Word 格式支持
- 实现增量索引,只更新变化了的文件
- 做个 Web 界面,方便其他人用
RAG 最大的价值在于:让大模型真正变成懂你私有知识的助手。