目录
在大语言模型(LLM)席卷各行各业的今天,如何让 AI "读懂"你自己的私有文档、知识库,成为开发者最迫切的需求之一。RAG(Retrieval-Augmented Generation,检索增强生成) 正是解决这个问题的核心技术。
本文将手把手带你用 LlamaIndex 和 LangChain 搭建一套完全本地运行的 RAG 系统,结合 Ollama 跑本地模型,无需花一分钱 API 费用。
什么是 RAG?
RAG 的核心思路很简单:
- 索引阶段:把你的文档切成小块,用 Embedding 模型转成向量,存入向量数据库
- 查询阶段:用户提问时,先把问题也转成向量,从数据库里找最相关的文档块
- 生成阶段:把检索到的文档块 + 用户问题一起塞给 LLM,让它基于真实资料回答
用户问题 → Embedding → 向量检索 → 相关文档块
↓
LLM 生成答案 ← 提示词模板
相比直接问 LLM,RAG 的优势在于: - 不幻觉:答案有据可查,来自你的文档 - 可更新:文档更新后重建索引即可,无需重新训练模型 - 成本低:不需要微调,普通机器即可运行
技术栈选择:LangChain vs LlamaIndex
| 维度 | LlamaIndex | LangChain |
|---|---|---|
| 定位 | 专注数据索引与检索 | 通用 LLM 应用框架 |
| 上手难度 | 较低,开箱即用 | 较高,灵活但复杂 |
| RAG 场景 | ⭐⭐⭐⭐⭐ 首选 | ⭐⭐⭐⭐ 可用 |
| 生态 | 丰富的数据加载器 | 更多 Agent/Chain 组件 |
结论:做知识库 RAG 首选 LlamaIndex,需要复杂工作流再引入 LangChain。
本文两者都会介绍,你可以按需选用。
环境准备
安装依赖
# 基础依赖
pip install llama-index llama-index-llms-ollama llama-index-embeddings-ollama
pip install langchain langchain-community chromadb
pip install pypdf python-docx # 文档解析
安装 Ollama 并拉取模型
Ollama 可以让你在本地运行开源大模型,安装后执行:
# 拉取 LLM(推荐 qwen2.5 对中文支持好)
ollama pull qwen2.5:7b
# 拉取 Embedding 模型
ollama pull nomic-embed-text
显卡 8GB 显存可跑 7B 模型,没有 GPU 用 CPU 也能跑,只是慢一些。
方案一:用 LlamaIndex 搭建 RAG
LlamaIndex 在 RAG 场景下代码极其简洁,5 行核心代码就能跑起来。
准备文档
knowledge_base/
├── django_docs.pdf
├── project_readme.md
└── api_reference.txt
完整代码
from llama_index.core import VectorStoreIndex, SimpleDirectoryReader, Settings
from llama_index.llms.ollama import Ollama
from llama_index.embeddings.ollama import OllamaEmbedding
# 1. 配置本地模型
Settings.llm = Ollama(model="qwen2.5:7b", request_timeout=120.0)
Settings.embed_model = OllamaEmbedding(model_name="nomic-embed-text")
# 2. 加载文档
documents = SimpleDirectoryReader("./knowledge_base").load_data()
print(f"已加载 {len(documents)} 个文档块")
# 3. 构建索引(首次运行会做 Embedding,需要一点时间)
index = VectorStoreIndex.from_documents(documents)
# 4. 创建查询引擎
query_engine = index.as_query_engine(similarity_top_k=3)
# 5. 开始问答
response = query_engine.query("Django 的信号机制如何使用?")
print(response)
持久化索引
每次重建索引很耗时,生产环境应该把索引保存到磁盘:
from llama_index.core import StorageContext, load_index_from_storage
import os
PERSIST_DIR = "./storage"
if os.path.exists(PERSIST_DIR):
# 加载已有索引
storage_context = StorageContext.from_defaults(persist_dir=PERSIST_DIR)
index = load_index_from_storage(storage_context)
print("已从磁盘加载索引")
else:
# 首次创建并保存
documents = SimpleDirectoryReader("./knowledge_base").load_data()
index = VectorStoreIndex.from_documents(documents)
index.storage_context.persist(persist_dir=PERSIST_DIR)
print("索引已保存到磁盘")
自定义文本分块策略
默认分块大小 1024 tokens,可以根据文档类型调整:
from llama_index.core.node_parser import SentenceSplitter
Settings.text_splitter = SentenceSplitter(
chunk_size=512, # 每块 512 tokens
chunk_overlap=50, # 相邻块重叠 50 tokens,避免上下文断裂
)
方案二:用 LangChain + ChromaDB 搭建 RAG
LangChain 的方式更底层,但定制化程度更高。
完整代码
from langchain_community.document_loaders import DirectoryLoader, PyPDFLoader
from langchain.text_splitter import RecursiveCharacterTextSplitter
from langchain_community.embeddings import OllamaEmbeddings
from langchain_community.vectorstores import Chroma
from langchain_community.llms import Ollama
from langchain.chains import RetrievalQA
from langchain.prompts import PromptTemplate
# 1. 加载文档
loader = DirectoryLoader("./knowledge_base", glob="**/*.pdf", loader_cls=PyPDFLoader)
documents = loader.load()
# 2. 文本分块
splitter = RecursiveCharacterTextSplitter(
chunk_size=500,
chunk_overlap=50,
separators=["\n\n", "\n", "。", "!", "?", " "] # 中文友好分隔符
)
chunks = splitter.split_documents(documents)
print(f"共 {len(chunks)} 个文本块")
# 3. 向量化并存入 ChromaDB
embeddings = OllamaEmbeddings(model="nomic-embed-text")
vectorstore = Chroma.from_documents(
documents=chunks,
embedding=embeddings,
persist_directory="./chroma_db"
)
# 4. 自定义提示词模板(中文回答更准确)
prompt_template = '''你是一个专业的知识库助手。请根据以下上下文内容回答问题。
如果上下文中没有相关信息,请直接说"我在知识库中没有找到相关信息",不要编造答案。
上下文:
{context}
问题:{question}
回答:'''
PROMPT = PromptTemplate(
template=prompt_template,
input_variables=["context", "question"]
)
# 5. 构建检索问答链
llm = Ollama(model="qwen2.5:7b")
qa_chain = RetrievalQA.from_chain_type(
llm=llm,
chain_type="stuff",
retriever=vectorstore.as_retriever(search_kwargs={"k": 3}),
chain_type_kwargs={"prompt": PROMPT},
return_source_documents=True # 返回引用来源
)
# 6. 查询
result = qa_chain("项目的部署步骤是什么?")
print("回答:", result["result"])
print("\n来源文档:")
for doc in result["source_documents"]:
print(f" - {doc.metadata.get('source', '未知')} 第 {doc.metadata.get('page', '?')} 页")
进阶:构建带对话历史的 RAG
上面的例子每次问答是独立的,实际应用需要支持多轮对话:
from llama_index.core.memory import ChatMemoryBuffer
from llama_index.core.chat_engine import CondensePlusContextChatEngine
memory = ChatMemoryBuffer.from_defaults(token_limit=3900)
chat_engine = index.as_chat_engine(
chat_mode="condense_plus_context",
memory=memory,
verbose=True,
)
# 多轮对话
print(chat_engine.chat("Django 的 ORM 是什么?"))
print(chat_engine.chat("它和原生 SQL 相比有什么优缺点?")) # 能理解上下文中的"它"
优化技巧
1. 混合检索(Hybrid Search)
纯向量检索对关键词匹配不够精准,结合 BM25 效果更好:
from llama_index.retrievers.bm25 import BM25Retriever
from llama_index.core.retrievers import QueryFusionRetriever
vector_retriever = index.as_retriever(similarity_top_k=3)
bm25_retriever = BM25Retriever.from_defaults(index=index, similarity_top_k=3)
retriever = QueryFusionRetriever(
[vector_retriever, bm25_retriever],
similarity_top_k=3,
num_queries=1,
mode="reciprocal_rerank", # 融合排名算法
)
2. 重排序(Reranking)
检索到的文档块不一定按相关性排序,用 Reranker 二次排序:
from llama_index.postprocessor.flag_embedding_reranker import FlagEmbeddingReranker
reranker = FlagEmbeddingReranker(
top_n=3,
model="BAAI/bge-reranker-v2-m3", # 支持中文的 Reranker 模型
)
query_engine = index.as_query_engine(
similarity_top_k=10, # 先多取几个
node_postprocessors=[reranker], # 再用 Reranker 精选 top3
)
3. 文档预处理
- PDF 扫描件:用
pytesseract做 OCR 后再索引 - 表格数据:用
pandas转成自然语言描述再存入 - 代码文件:按函数/类分块,保留注释上下文
与 Django 集成
如果你想把 RAG 集成到 Django 项目中,推荐这个架构:
# models.py - 记录索引状态
class KnowledgeDocument(models.Model):
file = models.FileField(upload_to='knowledge/')
indexed_at = models.DateTimeField(null=True)
chunk_count = models.IntegerField(default=0)
# tasks.py - 异步索引(配合 Celery)
@shared_task
def index_document(doc_id):
doc = KnowledgeDocument.objects.get(id=doc_id)
documents = SimpleDirectoryReader(input_files=[doc.file.path]).load_data()
index = VectorStoreIndex.from_documents(documents)
index.storage_context.persist(persist_dir=f"./storage/{doc_id}")
doc.indexed_at = timezone.now()
doc.chunk_count = len(documents)
doc.save()
# views.py - 流式回答接口(SSE)
from django.http import StreamingHttpResponse
def ask_view(request):
question = request.GET.get('q')
def stream_response():
streaming_response = query_engine.query(question)
for token in streaming_response.response_gen:
yield f"data: {token}\n\n"
return StreamingHttpResponse(stream_response(), content_type='text/event-stream')
总结
| 场景 | 推荐方案 |
|---|---|
| 快速原型 | LlamaIndex 5 行代码版本 |
| 定制化检索 | LangChain + ChromaDB |
| 多轮对话 | LlamaIndex Chat Engine |
| 生产环境 | 混合检索 + Reranker + 持久化索引 |
| 与 Django 集成 | Celery 异步索引 + SSE 流式输出 |
RAG 的核心不在于框架选择,而在于文档质量和分块策略。一份结构清晰的文档,哪怕用最简单的方案也能得到很好的效果。
相关资源: - LlamaIndex 官方文档 - LangChain 官方文档 - Ollama 模型库
本文由 liangliangyy 原创,转载请注明出处。
评论
0