用 LangChain / LlamaIndex 搭建本地知识库 RAG 系统

Python 2026-05-07 356
目录
预计阅读时间:10 分钟

在大语言模型(LLM)席卷各行各业的今天,如何让 AI "读懂"你自己的私有文档、知识库,成为开发者最迫切的需求之一。RAG(Retrieval-Augmented Generation,检索增强生成) 正是解决这个问题的核心技术。

本文将手把手带你用 LlamaIndexLangChain 搭建一套完全本地运行的 RAG 系统,结合 Ollama 跑本地模型,无需花一分钱 API 费用。

什么是 RAG?

RAG 的核心思路很简单:

  1. 索引阶段:把你的文档切成小块,用 Embedding 模型转成向量,存入向量数据库
  2. 查询阶段:用户提问时,先把问题也转成向量,从数据库里找最相关的文档块
  3. 生成阶段:把检索到的文档块 + 用户问题一起塞给 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 相比有什么优缺点?"))  # 能理解上下文中的"它"

优化技巧

纯向量检索对关键词匹配不够精准,结合 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
暂无评论,来发表第一条评论吧

发表评论

登录 后发表评论

发现更多