☰
从零搭建RAG私有知识库:LangChain+FAISS+Ollama实战指南
2026/10/20 19:48:14 网站建设 项目流程

1. 项目概述:RAG私有知识库到底解决什么问题

RAG(检索增强生成)这个缩写,最近一年多几乎成了大模型落地场景里曝光率最高的词。但很多初次接触的朋友,看到的资料要么是纯理论讲原理,要么是上来就扔一堆代码,中间的“为什么这么做”“资源不够怎么办”反而没人讲清楚。这篇就把整个过程拆开揉碎,从一个完全空白的本地环境开始,带你把一套私有知识库真正跑起来,最终的效果是:你把自己手头的文档丢进去,然后像聊天一样问它“我们公司考勤制度里关于迟到是怎么规定的”,它能从文档里找到答案并组织成一段通顺的话回复你。

这套方案的核心价值在于数据私有化。你不需要把任何内部资料上传到公网服务,所有环节——文档存储、向量检索、问题生成——都跑在本地机器上。很多团队第一次想尝试大模型应用,卡住的原因往往不是技术本身,而是“数据能不能出内网”这一关,RAG私有化方案直接从源头规避了这个合规问题。对于只有一台普通电脑的初学者,或者只有几台服务器的小团队,这条路是完全走得通的。

另外要说明的是,这篇文章面向的是零基础读者,但我会尽量把每一步的“为什么这么做”讲清楚,而不是给一份只能照着敲的脚本。如果你已经跑通过类似项目,可以直接跳到第4章看实操细节,里面有不少我用过之后觉得值得记录的经验。

2. 方案选型:为什么是这四件套

2.1 技术组件全景图

先把整套系统的骨架画出来:需要四个核心组件协同工作。文档加载与处理层负责把PDF、Word、TXT等格式的原始文件读出来并拆分成适合检索的文本块;嵌入模型负责把文本块转换成一串高维向量,让计算机能“理解”语义相似度;向量数据库负责存储这些向量并支持快速检索;大语言模型则负责阅读检索到的相关内容,组织成自然语言答案。

我选的组合是:Python 3.10 + LangChain框架 + 轻量级嵌入模型(模型名以bge开头的小尺寸版本)+ FAISS向量库,模型推理端用一个名为Ollama的本地模型管理工具。这套组合的特别之处在于:它没有一个组件是重型的,全部加起来在普通笔记本电脑上都能流畅运行,不需要GPU服务器,也不需要额外申请任何云服务。

2.2 组件选型的思考过程

这套选型是经过对比之后的折中方案。先看LangChain:它最被诟病的地方是抽象层次多、升级频繁,但它的文档加载器覆盖了市面上绝大多数常见格式,对新手来说避免了到处找解析库的麻烦。如果你已经是熟练工,完全可以不用框架自己写流程,但对于从零起步的人,LangChain能把开发工作量压缩到一个小时内。

再看FAISS和向量数据库的取舍。市面上有Chroma、Milvus、Weaviate等专业向量数据库,功能更丰富,支持数据持久化、元数据过滤、分布式部署等。但对一个起步阶段的私有知识库来说,FAISS的优势在于轻量和极快的检索速度,它本质上是一个高效的相似度搜索库,一个文件就搞定了索引的保存和加载。后续文档量增长到十万级以上再迁移到正式向量数据库也不迟。

嵌入模型选择了国产的开源版本bge-small-zh,它在中文语义匹配上的表现比同尺寸的通用模型要稳,而且被LangChain直接支持调用。这里有个原则:嵌入模型的语言能力必须和你的文档语言匹配,英文文档用英文优化的模型效果才好,中文场景强行用英文模型,检索召回率会明显下降。

2.3 模型推理端的本地化方案

大语言模型这块选择Ollama来运行开源模型,是因为它把模型下载、依赖管理、API服务三个环节都封装好了,不需要手动配置Python环境和CUDA。下载一个几GB的模型文件,执行一条命令就能起来一个兼容OpenAI格式的本地接口服务,这对新手非常友好。

Ollama支持的模型中,我推荐从Qwen系列或者Llama 3系列的中小型版本起步。7B到8B参数量的模型经过量化后,在有16GB内存的机器上可以流畅运行,生成速度和连贯性都在可接受范围内。当然纯CPU运行会慢一些,但测试问答场景完全够用。后面第5章会给出具体的效果实测记录。

3. 环境准备:先把地基打好

3.1 安装Python与创建独立环境

整个过程从安装Python开始。建议用3.10版本,太新的版本有些底层依赖可能还没适配,太老的版本则不支持LangChain的新特性。安装时务必勾选“Add Python to PATH”,否则后面命令行里敲python会提示找不到命令。

接下来手动创建虚拟环境——这一步很多教程会跳过去直接装依赖,但强烈建议不要省。虚拟环境的作用是给这个项目隔离出一块独立的依赖空间,不会和系统里其他Python项目互相污染,后续想删掉整个项目直接删文件夹就行,不会给系统留垃圾。

创建方法在命令行里执行:

python -m venv rag_env

然后激活环境。Windows系统激活命令是:

rag_env\Scripts\activate

macOS或Linux系统是:

source rag_env/bin/activate

看到命令行前面出现了(rag_env)前缀,就说明环境已经切过来了。之后安装的所有包都会装进这个隔离空间里。

3.2 安装项目依赖

依赖安装用pip一行命令搞定。需要说明的是,LangChain最近做了模块拆分,核心包和社区贡献的包需要分开装,文档加载器、嵌入模型这些都在社区包里。我用的是下面这组经过实测的版本组合,直接复制到命令行执行即可:

pip install langchain-core==0.3.29 langchain==0.3.13 langchain-community==0.3.13 langchain-text-splitters==0.3.5 faiss-cpu==1.9.0 sentence-transformers==3.4.1 ollama==0.4.3 streamlit==1.41.1 pypdf==5.1.0

这里有几个容易踩坑的细节。faiss-cpu在Windows上不需要额外装Visual Studio编译工具,直接用预编译包就行;pypdf是用来解析PDF文档的,如果只处理TXT或Word可以暂时不装,但建议装上备用;streamlit是为了最后做一个可视化问答界面。

安装过程中如果出现某个包找不到对应版本的情况,大概率是Python版本不匹配,检查一下是否用的3.10。还有一点,安装完成后不要立刻关掉命令行,后面还要验证安装结果。

3.3 验证安装与准备测试文档

装完后执行两条验证命令:

import langchain, faiss, sentence_transformers, ollama print("依赖检查通过")

能打印出结果就说明环境没问题。如果某个import报错,多半是该包没装成功,重新执行一次pip install对应的包即可。

接下来需要准备一份测试文档。不建议一上来就用长篇的PDF图书,也不建议用图片版的扫描件(没有文字层,处理起来很麻烦),最好是纯文本的TXT、Markdown或者内容清晰的PDF。我这次用的是某个模拟项目的一份产品FAQ文档,大概四十多页,内容涵盖功能介绍、故障排查、使用技巧。这类问答型文档对RAG系统非常友好,因为问题和答案的表述方式与检索逻辑天然契合。

3.4 Ollama模型部署

最后安装Ollama推理引擎并下载模型。去官网下载对应操作系统的安装包,装完打开服务,然后执行:

ollama pull qwen2.5:7b

这个命令会从模型仓库拉取一个约4.7GB的量化模型文件,国内网络环境下可能需要等待一段时间,建议使用稳定的网络连接。拉取完成后测试一下接口是否正常:

ollama run qwen2.5:7b

输入任意问题,能正常回复对话就说明本地大模型已经就绪。还没完,需要额外拉一个嵌入模型——注意,RAG系统的检索模块需要专用嵌入模型,和大语言模型是两回事,这个在后面会详细解释:

ollama pull bge-m3

这个模型负责把文本变成向量,文件体积比语言模型小很多,下载很快。

4. 分步构建RAG检索问答系统

4.1 核心流程梳理

整个系统的运行逻辑可以用一句话概括:先对文档做索引,再对问题做检索,最后把检索到的内容交给大模型整理成答案。这里的关键是理解为什么需要“切分”这一步。

如果你把一篇几万字的文档原封不动地丢给模型,让它直接回答,它会面临两个问题:一是模型上下文窗口有限,塞不下全部内容;二是即便塞得下,模型也难以从海量信息里精准定位某一句话,回答容易模棱两可甚至张冠李戴。RAG的办法是把文档切成若干块——“块”的大小有讲究——然后对每个块做向量化存储,用户提问时先把问题转成向量,去库里找语义最接近的几个块,再把这些块作为参考资料交给模型精读,让它基于这些材料作答。

类比来说,这就像开卷考试,先把整本教材切成小卡片并整理好索引,考试时根据题目先翻出对应的几张卡片,再照着卡片内容组织答案,准确率自然比闭卷默写高得多。

4.2 第一步:加载与清洗文档

创建一个Python脚本文件,命名为build_knowledge_base.py,下面是第一步代码:

from langchain_community.document_loaders import PyPDFLoader # 加载PDF文件 loader = PyPDFLoader("data/product_faq.pdf") documents = loader.load() print(f"成功加载 {len(documents)} 页文档")

不要以为加载就完事了,这里有个非常重要的清洗步骤。PDF解析出来的文本通常混有页眉页脚、页码、无意义换行符,这些噪声如果不处理,会让切出来的文本块质量很差,进而直接影响检索效果。我先用一段简单脚本做清洗:

def clean_text(text): # 去掉常见的页眉页脚和多余空白 lines = [line.strip() for line in text.split('\n') if line.strip()] cleaned = '\n'.join(lines) return cleaned documents = [doc.page_content for doc in documents] # 得到干净的文本列表

实操中你会发现,有些PDF的排版会让两栏文本错乱拼接,或者代码块里的空格被压缩掉,这些问题必须在加载阶段发现并处理,越早处理代价越小。如果文档格式五花八门,建议统一转成TXT或Markdown再喂给系统,兼容性最好。

4.3 第二步:文本切分的参数选择

文本切分是整个RAG链路中对最终效果影响最大的环节,没有之一。切分的块太大,检索时可能把不相关的内容混进来;块太小,又可能切断语义完整的句子。LangChain提供了多种切分器,最常用的是按字符数切分的RecursiveCharacterTextSplitter,它优先尝试按段落分隔符切,其次按句子,再按字符,层层递进,尽量保持语义完整。

我的配置如下:

from langchain_text_splitters import RecursiveCharacterTextSplitter text_splitter = RecursiveCharacterTextSplitter( chunk_size=300, chunk_overlap=50, separators=["\n\n", "\n", "。", "!", "?", ";", ",", " ", ""] ) chunks = text_splitter.split_text("\n\n".join(documents)) print(f"切分为 {len(chunks)} 个文本块")

两个关键参数:chunk_size是每个块的目标字符数,chunk_overlap是相邻块之间重叠的字符数。重叠的目的是保证跨越切分边界的语义片段在两侧都各保留一份,避免“苹果公司的总部位于……”这句话正好被拦腰截断,导致两半都语义残缺。

为什么选300这个数值?这是针对中文场景的经验值。中文信息密度高,300字大约能表达一个完整的知识要点,同时又能被模型的上下文窗口轻松容纳。如果文档是技术规范类的,可以适当调大到500;如果是社交媒体类碎片文本,200就行。这个参数没有绝对标准,后面讲评测时会给出调试方法。

4.4 第三步:向量化并构建索引

现在进入重头戏。文本块准备好了,但要让计算机能“检索”,必须把它们变成向量。这里用到的是bge-m3嵌入模型,它会把一段文本映射成一个高维空间中的坐标点,语义相近的文本在坐标空间里距离也近。

from langchain_community.embeddings import HuggingFaceEmbeddings from langchain_community.vectorstores import FAISS from langchain_ollama import OllamaEmbeddings # 使用本地Ollama提供的bge-m3模型生成向量 embeddings = OllamaEmbeddings(model="bge-m3") # 为每个文本块生成向量并创建FAISS索引 vectorstore = FAISS.from_texts( texts=chunks, embedding=embeddings ) # 保存索引到本地文件 vectorstore.save_local("faiss_index") print("向量索引构建完成,已保存到 faiss_index 目录")

这一步会消耗一些内存和CPU时间。我实测下来,一万个文本块的向量化大约需要三到五分钟,做完后能看到faiss_index文件夹里的索引文件。向量化完成前不用担心磁盘占用,几十MB而已。

有人会问,为什么不用前面提到的sentence-transformers库直接加载模型,而要走Ollama?两种方式都能工作,但走Ollama可以复用已经拉取好的模型,省去重复下载,而且接口统一,管理起来更清爽。

4.5 第四步:加载索引实现检索问答

建立索引是一次性的工作,但用户的提问是多次的。所以需要一个单独的问答脚本,每次启动时加载已有的FAISS索引,然后接收用户问题、检索相关文本块、调用大模型生成答案。这里先把检索逻辑写清楚:

from langchain_community.vectorstores import FAISS from langchain_ollama import OllamaEmbeddings from langchain_ollama import OllamaLLM # 加载已有的索引 embeddings = OllamaEmbeddings(model="bge-m3") vectorstore = FAISS.load_local("faiss_index", embeddings, allow_dangerous_deserialization=True) # 用户问题 query = "产品无法连接Wi-Fi时应该怎么办?" # 检索最相关的4个文本块 docs = vectorstore.similarity_search_with_score(query, k=4) for i, (doc, score) in enumerate(docs): print(f"候选片段 {i+1}(相似度:{score:.4f})") print(doc.page_content) print("---")

这里similarity_search_with_score会返回检索结果和相似度分数,分数越小代表距离越近,可以借此观察检索质量。建议先单独跑一下这一步,看看检索到的文本块与问题是否真正相关。如果结果明显跑偏,先别急着调大模型,排查切分参数或嵌入模型,这一步对了整个系统才对。

4.6 第五步:用LangChain串联完整问答流程

最后一步把检索和生成拼接起来,形成完整的RAG问答链路:

from langchain.chains import RetrievalQA from langchain_ollama import OllamaLLM # 设置本地大语言模型 llm = OllamaLLM(model="qwen2.5:7b") # 创建检索增强问答链 qa_chain = RetrievalQA.from_chain_type( llm=llm, retriever=vectorstore.as_retriever(search_kwargs={"k": 4}), return_source_documents=True ) # 执行问答 question = "产品无法连接Wi-Fi时应该怎么办?" result = qa_chain.invoke({"query": question}) print("回答:") print(result["result"])

参数k控制了检索返回的文本块数量,我一般设置在3到5之间。太少了信息可能不全,太多了噪声会让模型抓不住重点,4是一个均衡值。运行这条脚本后,如果一切正常,你应该能看到模型基于检索到的片段生成的答案,底下还会跟着来源片段,方便核对答案是否出自原文。

4.7 编写可复用脚本的注意事项

如果只是跑通一次demo,把上面代码粘到脚本里顺序执行就够了。但后续如果要添加新文档、重新构建索引、持续问答,代码结构最好是函数化的。我一般会拆成三个文件:ingest.py负责加载文档和构建索引,query.py负责加载索引和问答,utils.py放公共的文本清洗函数。这样新增文档时只需增量执行ingest.py,索引文件会更新,问答端不用动。

还有一个小细节:FAISS索引文件的加载接口出于安全考虑要求传入allow_dangerous_deserialization=True,这是新版的安全机制,防止加载来路不明的序列化数据。如果是从自己机器上生成的索引,可以放心打开;如果是从网上下载的别人分享的索引文件,建议重新验证一下内容再使用。

5. 可视化界面:用Streamlit做一个聊天机器人

命令行跑通之后,体验上总觉得差点意思。为了让自己用起来舒服,也为了让非技术背景的同事能直接上手,我基于Streamlit写了一个简单的网页问答界面。它创建了浏览器中的对话入口,输入问题、点击按钮就能得到答案,不需要接触任何代码。

import streamlit as st from langchain_community.vectorstores import FAISS from langchain_ollama import OllamaEmbeddings, OllamaLLM from langchain.chains import RetrievalQA st.set_page_config(page_title="本地知识库问答", page_icon="") st.title("本地知识库问答") # 缓存加载,避免每次操作重复加载模型 @st.cache_resource def load_chain(): embeddings = OllamaEmbeddings(model="bge-m3") vectorstore = FAISS.load_local("faiss_index", embeddings, allow_dangerous_deserialization=True) llm = OllamaLLM(model="qwen2.5:7b") return RetrievalQA.from_chain_type( llm=llm, retriever=vectorstore.as_retriever(search_kwargs={"k": 4}), return_source_documents=True ) qa_chain = load_chain() # 聊天界面 if "messages" not in st.session_state: st.session_state.messages = [] for message in st.session_state.messages: st.chat_message(message["role"]).write(message["content"]) if prompt := st.chat_input("请输入你的问题"): st.chat_message("user").write(prompt) st.session_state.messages.append({"role": "user", "content": prompt}) with st.spinner("正在检索资料并生成回答..."): response = qa_chain.invoke({"query": prompt}) answer = response["result"] st.chat_message("assistant").write(answer) st.session_state.messages.append({"role": "assistant", "content": answer})

启动方式是在命令行执行streamlit run app.py,浏览器会自动打开一个本地页面。实测中这个页面在局域网内也可以访问,同一网络的同事可以直接在手机上体验,简单来说就是一个小型团队知识库问答应用。

这里最实用的设计是用了@st.cache_resource装饰器。如果没有它,页面每次交互都会重新加载嵌入模型和向量索引,等待时间动辄十几秒,体验会很糟糕。加上缓存后,模型只加载一次,后续问答秒级响应。

6. 效果优化与实测:怎么把“模糊的实现”变成“好用的系统”

6.1 第一次实测的预期与现实

完成上述搭建后,我拿那份模拟项目的产品FAQ做了一轮完整测试,记录了几个典型问题的实际表现,作为衡量系统基准效果的参考。

第一个问题:“产品支持哪些连接方式?”系统回答准确列出了蓝牙、Wi-Fi、USB三种方式,并说明了各自的适用场景,回答内容和原文基本一致。

第二个问题:“电池续航时间多久?”答案给出的续航数据和文中的“正常使用约8小时、重度使用约5小时”完全吻合,而且正确区分了两种使用场景。

第三个问题:“设备进水了怎么办?”这次的回答开始暴露出问题——模型给出的建议里有“立即开机检查”这种危险操作,而原文明确写的是“切勿开机,先擦干静置24小时”。为什么会出现这种错误?核心原因是检索环节没有找到足够精准的文本片段,模型只能凭自己训练时习得的常识作答。这恰好说明RAG系统质量的关键在检准率,而不在生成模型本身。

6.2 针对检索效果的调优方法

针对上面的问题,我按优先级从低到高逐步做了四项调整,每一项都实实在在地提升了准确率。

第一步是调整文本切分参数。把chunk_size从300改成200,chunk_overlap从50改成30,意图是让每个文本块包含更聚焦的语义单元,检索时命中的候选片段更精准。修改后重新构建索引,该问题候选片段相关性有所提升。

第二步是扩充相似度检索数量。把k值从4提高到8,先让系统多召回几个候选片段,再用后续步骤做精排,防止正确答案被截断在结果窗口之外。但这同时带来了副作用:模型看到的噪声增多了,有时候反而被干扰。

第三步是引入重排序模型。这是效果提升最明显的一步:先用向量检索召回10个候选,再用一个专门的中文重排序模型逐一对候选与问题的相关性打分,只取分数最高的3个交给大模型。重排序模型理解语义的能力比纯向量距离更细腻,能有效过滤掉“字面相关但语义跑偏”的文本。这也是对第二步副作用的修正方案。

第四步是提示词优化。在检索QA链中自定义了一个提示模板:

from langchain.prompts import PromptTemplate custom_prompt = PromptTemplate( template=( "你是一个严谨的知识库问答助手。请根据以下资料片段回答问题。" "如果资料中没有相关信息,请直接回答“资料库中未找到相关信息”," "不要编造。\n\n" "资料片段:\n{context}\n\n" "问题:{question}\n\n" "回答:" ), input_variables=["context", "question"] ) qa_chain = RetrievalQA.from_chain_type( llm=llm, retriever=vectorstore.as_retriever(search_kwargs={"k": 4}), chain_type_kwargs={"prompt": custom_prompt} )

这段提示词的核心作用是把模型“不懂装懂”的空间压缩到最小。模型基于资料回答问题时,即使没找到完整信息,也倾向于靠常识补全,这在严谨的知识管理场景里是致命的,而明确告诉它“没找到就直说”能堵住大部分编造来源。

经过四步调整后,前面提到的“设备进水”问题得到了正确回答。需要提醒的是,每调整一个参数都应该在固定的测试问题上重新跑一遍,记录回答正确与否,而不是凭感觉判断。建议准备一份涵盖不同难度的问题集,至少二十条,当作回归测试用例,每次改动后逐条验证。

6.3 在普通电脑上的资源占用与性能表现

很多人会担心本地部署跑不动模型。我实际测试了这台模拟用的电脑配置:CPU是普通桌面级处理器,内存16GB,没有独立显卡。在这种配置下,模型回答一个问题的生成时间大约在五到十秒之间,这个速度在对话场景下可以接受。

内存占用方面需要注意:同时加载嵌入模型和大语言模型,峰值占用约8GB,再加上系统本身的占用,16GB内存刚好够用,但浏览器标签开多了可能会感觉到卡顿。如果机器只有8GB内存,建议换用更小的语言模型版本,比如Qwen系列更轻量的量化档位,生成速度会变快,回答质量略有下降但日常问答影响不大。如果追求更好体验,有一块显存8GB以上的独立显卡,模型可以完全放进显存,生成时间能缩短到两秒以内。

6.4 继续扩展的三个方向

这套骨架跑通后,可以根据实际需求做不少扩展。第一是增加文档类型支持:目前只实现了PDF和TXT,接上Word文档加载器和HTML加载器后就能处理常见办公文档。第二是增加增量更新机制:目前文档一多就要全量重建索引,对于频繁更新的动态文档库效率偏低,一种常用方案是定期重建,另一重方案是把新文本块直接追加到现有索引。第三是增加多轮对话记忆:目前每次提问都是独立的,不记得之前的对话,配合历史记录组件后体验会更自然。

有意思的是,这三个方向都不需要推翻现有架构,都是在LangChain既有组件上做配置调整。这也是当初选择这套技术栈的回报:生态足够丰富,后续成长空间留得很足。

实测下来,整个项目从零开始到跑通,大约需要三到四个小时,大部分时间花在工具安装和模型下载上,真正写代码的时间不到四十分钟。建议第一次动手时保持耐心,如果某个环节失败了,优先检查环境配置和网络状态,这两个原因占了初学者遇到问题的八成以上。等系统真正跑起来,你会对RAG这个技术在知识管理场景里的价值有非常直观的体会——它并不神秘,但确实能成为解决实际问题的趁手工具。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询