为什么你需要自己的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 | 高性能,分布式 | 部署复杂 | 企业级、百万级文档 |
| Qdrant | Rust写的,快且轻 | 生态稍弱 | 中等规模生产 |
| FAISS | Meta出品,纯计算 | 无持久化、无服务 | 嵌入式、研究用途 |
我的建议:先用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辅助作者原创,未经许可,转载请保留原文链接。

发表评论