0

RAG本地知识库搭建实战:从文档导入到智能问答全流程

2026.05.30 | youres | 5次围观

为什么你需要自己的RAG知识库

大模型很聪明,但它不认识你公司的内部文档、不记得你项目的历史决策、更不知道你上周开会讨论了什么。每次让AI回答业务问题,要么凭空编造,要么给你一个"据我所知截至训练数据截止日期"的免责声明。RAG(检索增强生成)就是为了解决这个问题——让大模型先查你的资料,再回答你的问题

我花了三周时间为团队搭建了一套本地RAG知识库,从最开始的Naive RAG到最终的混合检索方案,踩了无数坑。这篇文章把整个搭建过程、关键决策和踩坑记录完整分享出来,帮你少走弯路。

一、RAG核心原理:三步走而不是黑魔法

RAG的运作流程其实很直白:

  • 检索(Retrieval):把用户的问题变成查询,从知识库中找到最相关的文档片段
  • 增强(Augmented):把检索到的内容作为上下文,拼接到提示词中
  • 生成(Generation):大模型基于增强后的上下文生成回答

听起来简单,但每一步都有大量工程细节。大多数教程只讲"跑通一个demo",实际生产中你会发现:检索不准、上下文太长被截断、回答引用了错误段落——这些才是真正需要解决的问题。

二、环境准备:选型比努力重要

2.1 大模型选择

本地部署优先选Ollama,一条命令拉取模型:

ollama pull qwen2.5:7b
ollama pull nomic-embed-text

qwen2.5:7b中文能力强,7B参数量在16G内存的机器上流畅运行。nomic-embed-text是嵌入模型,负责把文本转成向量。如果你的机器配置够,qwen2.5:14b效果会更好,但7B已经是性价比甜点。

不想本地跑模型的话,豆包API和DeepSeek API都是国内低延迟的选择,调用成本也不高。

2.2 向量数据库选型

数据库优点缺点适合场景
Chroma零配置,Python原生大规模性能差原型验证、小规模
Milvus高性能,分布式部署复杂企业级、百万级文档
QdrantRust写的,快且轻生态稍弱中等规模生产
FAISSMeta出品,纯计算无持久化、无服务嵌入式、研究用途

我的建议:先用Chroma跑通流程,确认检索效果满意后再切Qdrant。别一上来就搞Milvus集群,你会发现90%的时间在运维数据库而不是优化RAG效果。

2.3 开发框架

LangChain是主流选择,但LlamaIndex在RAG场景下更专注。我的实践:用LangChain做整体编排,LlamaIndex的检索引擎做核心检索模块。两者不是互斥的。

pip install langchain langchain-community llama-index chromadb pypdf

三、核心实现:从文档到智能问答

3.1 文档加载与分块

分块是RAG效果的第一道坎。块太大,检索不精准;块太小,上下文不完整。我的经验值:中文文档500-800字一块,重叠100字

from langchain.text_splitter import RecursiveCharacterTextSplitter

splitter = RecursiveCharacterTextSplitter(
    chunk_size=600,
    chunk_overlap=100,
    separators=["\n\n", "\n", "。", "!", "?", ";", ",", " ", ""],
)

from langchain_community.document_loaders import PyPDFLoader, TextLoader, Docx2txtLoader

def load_documents(folder_path):
    docs = []
    for file in Path(folder_path).glob("**/*"):
        if file.suffix == ".pdf":
            loader = PyPDFLoader(str(file))
        elif file.suffix == ".txt" or file.suffix == ".md":
            loader = TextLoader(str(file), encoding="utf-8")
        elif file.suffix == ".docx":
            loader = Docx2txtLoader(str(file))
        else:
            continue
        loaded = loader.load()
        # 给每个文档加上来源元数据
        for doc in loaded:
            doc.metadata["source"] = file.name
        docs.extend(loaded)
    return docs

docs = load_documents("./knowledge_base")
chunks = splitter.split_documents(docs)
print(f"加载 {len(docs)} 个文档,切分为 {len(chunks)} 个块")

一个容易忽略的细节:分块时保留元数据。当检索返回多个片段时,来源信息能帮你判断哪个更可信,也让回答能引用出处。

3.2 向量化与存储

from langchain_community.embeddings import OllamaEmbeddings
from langchain_community.vectorstores import Chroma

embeddings = OllamaEmbeddings(model="nomic-embed-text")

vectorstore = Chroma.from_documents(
    documents=chunks,
    embedding=embeddings,
    persist_directory="./chroma_db",
    collection_metadata={"hnsw:space": "cosine"},
)

# 保存到磁盘,下次直接加载
vectorstore.persist()

嵌入模型的选择直接影响检索质量。nomic-embed-text在英文上表现优异,但中文场景我更推荐用bge-large-zh或m3e-base,这两个在C-MTEB榜单上中文表现更好:

from langchain_community.embeddings import HuggingFaceEmbeddings

embeddings = HuggingFaceEmbeddings(
    model_name="BAAI/bge-large-zh-v1.5",
    model_kwargs={"device": "cuda"},  # 有GPU就用上
    encode_kwargs={"normalize_embeddings": True},
)

3.3 问答链搭建

from langchain_community.llms import Ollama
from langchain.chains import RetrievalQA

llm = Ollama(model="qwen2.5:7b", temperature=0.3)

qa_chain = RetrievalQA.from_chain_type(
    llm=llm,
    chain_type="stuff",  # 把检索结果全部塞进prompt
    retriever=vectorstore.as_retriever(
        search_type="mmr",  # 最大边际相关性,去重又多样
        search_kwargs={"k": 4, "fetch_k": 10},
    ),
    return_source_documents=True,
)

result = qa_chain.invoke("我们的项目部署流程是什么?")
print(result["result"])
print("\n来源:", [doc.metadata["source"] for doc in result["source_documents"]])

search_type用mmr而非similarity,这是一个关键优化:mmr会在保证相关性的同时尽量选择内容不重复的文档片段,避免4个检索结果说的都是同一件事。

四、效果提升:让回答从"能用"到"好用"

4.1 混合检索:BM25 + 向量检索

纯向量检索有一个致命弱点:专有名词、产品代号、缩写词的检索经常翻车。比如搜索"ROI审批流程",向量检索可能返回一堆讲投资回报率的文档,而不是你们内部那个叫ROI的审批系统。

BM25关键词检索恰好弥补这个短板。两者融合的效果远超单独使用:

from langchain.retrievers import BM25Retriever, EnsembleRetriever

# BM25检索器(关键词匹配)
bm25_retriever = BM25Retriever.from_documents(chunks, k=4)

# 向量检索器(语义匹配)
vector_retriever = vectorstore.as_retriever(search_kwargs={"k": 4})

# 混合检索:BM25和向量各占50%权重
ensemble_retriever = EnsembleRetriever(
    retrievers=[bm25_retriever, vector_retriever],
    weights=[0.5, 0.5],
)

实际效果:我测试了50个业务问题,纯向量检索准确率68%,混合检索提升到86%。提升主要来自专有名词和缩写词的查询场景。

4.2 查询改写:让用户的问题更好检索

用户问"怎么申请服务器",但文档里写的是"计算资源申请流程"。这个语义鸿沟光靠向量相似度有时跨不过去。用LLM先改写查询:

from langchain.prompts import ChatPromptTemplate

rewrite_prompt = ChatPromptTemplate.from_template(
    "你是一个查询优化专家。将用户的问题改写为更适合检索的查询,"
    "补充可能的同义词和相关术语。\n\n用户问题:{question}\n\n优化后的查询:"
)

# 先改写再检索
original_query = "怎么申请服务器"
rewritten = (rewrite_prompt | llm).invoke({"question": original_query})
print(f"原查询:{original_query}")
print(f"改写后:{rewritten}")

4.3 自定义提示词模板

默认的stuff提示词太泛了。针对知识库场景定制模板,效果差异明显:

from langchain.chains import RetrievalQA
from langchain.prompts import PromptTemplate

custom_prompt = PromptTemplate(
    template="""你是公司内部知识库助手。请严格根据以下参考资料回答问题。
如果参考资料中没有相关信息,请明确说"根据现有知识库无法回答",不要编造内容。

参考资料:
{context}

问题:{question}

回答格式:
1. 直接回答问题
2. 标注信息来源(哪个文档)
3. 如果有相关流程,列出步骤""",
    input_variables=["context", "question"],
)

qa_chain = RetrievalQA.from_chain_type(
    llm=llm,
    chain_type="stuff",
    retriever=ensemble_retriever,
    return_source_documents=True,
    chain_type_kwargs={"prompt": custom_prompt},
)

关键点:"不要编造内容"这个指令必须显式写入。大模型天生喜欢"帮忙",你不说它就会根据模糊记忆补充内容。

五、生产部署的三个坑

5.1 文档更新同步

知识库不是建好就完事的。文档会更新、新增、删除。我的做法是维护一个文档指纹索引:

import hashlib

def get_file_hash(filepath):
    with open(filepath, "rb") as f:
        return hashlib.md5(f.read()).hexdigest()

def sync_knowledge_base(folder_path, vectorstore, hash_db):
    current_files = set(Path(folder_path).glob("**/*"))
    current_hashes = {f: get_file_hash(f) for f in current_files if f.suffix in [".pdf", ".txt", ".md", ".docx"]}

    # 找出新增和修改的文件
    to_add = [f for f, h in current_hashes.items() if hash_db.get(str(f)) != h]
    # 找出删除的文件(向量库中按source元数据删除)
    to_remove = [f for f in hash_db if Path(f) not in current_files]

    # 增量更新向量库
    if to_add:
        new_docs = load_documents(folder_path)  # 只加载变更文件
        new_chunks = splitter.split_documents(new_docs)
        vectorstore.add_documents(new_chunks)

    # 更新指纹库
    for f in to_add:
        hash_db[str(f)] = current_hashes[f]
    for f in to_remove:
        del hash_db[str(f)]

5.2 检索结果缓存

同一个问题被多人反复问很常见。加一层缓存,命中率高的查询直接返回,省掉检索和生成开销:

import hashlib, json, time

class QueryCache:
    def __init__(self, ttl=3600):
        self.cache = {}
        self.ttl = ttl

    def get(self, query):
        key = hashlib.sha256(query.encode()).hexdigest()
        if key in self.cache:
            result, ts = self.cache[key]
            if time.time() - ts < self.ttl:
                return result
            del self.cache[key]
        return None

    def set(self, query, result):
        key = hashlib.sha256(query.encode()).hexdigest()
        self.cache[key] = (result, time.time())

5.3 多用户并发

Ollama默认单请求处理,多人同时提问会排队。解决方案:

  • 设置 OLLAMA_NUM_PARALLEL=4 环境变量,允许并行处理
  • 或用vLLM替代Ollama,原生支持连续批处理
  • 如果是API调用(豆包/DeepSeek),天然支持并发,按量付费即可

六、效果评估:别靠感觉

搭建完了怎么判断效果好不好?我用了三个指标:

指标含义及格线优秀线
检索召回率相关文档是否被检索到70%85%
回答准确率回答是否事实正确75%90%
拒答率知识库没答案时是否拒绝回答60%80%

评估方法:准备50个标准问答对(人工标注),用脚本批量跑,对比AI回答和标准答案。这个工作量不小,但一次评估能发现大量问题——我的知识库第一轮评估准确率只有62%,调整分块策略和混合检索权重后提升到87%。

总结

搭建RAG知识库的核心不是"用什么模型"或"用什么数据库",而是检索策略和提示词工程。向量检索解决了语义匹配,BM25解决了精确匹配,两者融合是当前性价比最高的方案。分块大小、重叠窗口、查询改写这些看似不起眼的参数,对最终效果的影响远大于换一个大模型。

如果你刚开始做RAG,我的建议路径:先用Chroma + Ollama跑通基础流程 → 加BM25混合检索 → 定制提示词模板 → 搭建评估体系 → 再考虑向量数据库和模型的升级。先把流程跑通,再逐步优化,别一上来就想做完美方案。更多AI自动化实战经验,可参考MCP Server开发实战OpenClaw本地部署指南

版权声明

本文仅代表个人观点。
本文系AI辅助作者原创,未经许可,转载请保留原文链接。

发表评论