RAG 入门实战:从 0 到 1 搭一个本地知识库问答系统
你是不是也有过这种经历:公司有一堆 PDF 文档、Word 制度、产品手册,每次想找点信息都得翻半天;想让 AI 帮自己答疑,但它一问三不知,还经常"一本正经地胡说八道"?
这篇教程就来解决这两个痛点。我会带你用最少的概念、最少的环境依赖,从零搭一个完全跑在自己电脑上的知识库问答系统。全程跟着做,不需要懂向量数据库原理,也不需要显卡。
一、RAG 是什么(先搞懂再动手)
RAG 是Retrieval-Augmented Generation的缩写,中文叫"检索增强生成"。
别被名字吓到,用一句话说:先去资料库里翻出相关的几页纸,再把这几页纸交给 AI,让它基于这些材料来回答问题。
用一个生活化的比喻:
想象你是一个刚入职的员工,被问到"我们公司的年假怎么算?“。你自己肯定答不上来。于是你先跑去档案室,翻出《员工手册》里关于年假的那一章,然后把这一章递给一个很会总结但记性不好的同事,说"你照着这页回答他”。这个同事读完后,用大白话把答案讲了出来。
在这个比喻里:档案室 = 你的文档库;翻出相关章节 = 检索(Retrieval);记性不好的同事 = 大语言模型(LLM);照着材料回答 = 生成(Generation)。
传统的大模型有个天生缺陷:它只知道自己训练时"背"过的内容,对你的私有资料一无所知,而且训练数据有截止日期。RAG 的核心思路就是——别让模型凭记忆瞎编,给它一份"开卷考试"的参考资料。这样既能回答私有知识,又能大幅减少幻觉(胡说八道)。
一句话记住 RAG:不开卷的 AI 在裸考,加了 RAG 的 AI 拿着你的资料在开卷答题。
二、整体架构:一条流水线
一个最基础的 RAG 系统,其实是把"建资料库"和"回答问题"分成了两个阶段。我们画一条流水线:
【知识库构建阶段(离线,做一次就行)】 文档 → 分块 → 向量化 → 存进向量库 .pdf/.txt 切小段 变成向量 (本地文件/数据库) 【问答阶段(在线,用户每次提问)】 用户提问 → 向量化问题 → 检索最相关的几块 → 拼成 Prompt → 大模型生成答案逐段解释:
- 文档(Document):你的原始资料,PDF、TXT、Word、网页都行。
- 分块(Chunking):把长文档切成一段一段的小块。为什么要切?因为大模型一次读不下整本手册,而且检索时"精准命中一小段"比"命中一大篇"更有用。
- 向量化(Embedding):用 embedding 模型把每一块文字变成一个长长的数字数组(向量)。语义相近的文字,向量在数学空间里也离得近。这一步是 RAG 的"魔法核心"。
- 向量库(Vector Store):把这些向量存起来,方便后面快速比对。本地最简单的做法就是存成本地文件。
- 检索(Retrieval):用户提问时,同样把问题向量化,然后在向量库里找"离问题最近的几块资料"。
- 生成(Generation):把检索到的资料 + 用户问题,拼成一个提示词(Prompt)丢给大模型,让它基于资料作答。
这六个环节里,前四步是"建库",只在你新增/修改文档时跑一次;后两步是"问答",每次提问都跑。这个区分很重要,后面写代码时会对应上。
三、环境准备
3.1 你需要什么
- Python 3.10 或以上(推荐 3.10/3.11,兼容性最好)。
- 一台普通的电脑,不需要独立显卡。我们用的 embedding 模型和生成模型都走"本地轻量"路线。
- 网络(第一次要下载模型,后面离线也能用)。
3.2 安装 Ollama(本地的模型运行器)
为了让整套系统完全本地、保护隐私、免 API 费用,我们用 Ollama 来跑模型。它相当于一个"本地模型管家",一行命令就能下载并运行开源模型。
- 去 ollama.com 下载安装,一路下一步即可。
- 打开终端,拉取两个模型(一个负责中文生成,一个负责中文向量化):
# 中文生成模型(7B 版本,4G 内存勉强,8G 更顺)ollama pull qwen2.5:7b# 中文 embedding 模型(bge-m3 对中文、中英混合都很强)ollama pull bge-m3如果你的电脑内存比较小(比如 8G 以下),可以把
qwen2.5:7b换成qwen2.5:3b,牺牲一点质量换流畅度。
3.3 安装 Python 依赖
建议先建一个虚拟环境,保持干净:
python-mvenv rag-env# Windows 激活:rag-env\Scripts\activate# macOS / Linux 激活:sourcerag-env/bin/activate pipinstallllama-index llama-index-llms-ollama llama-index-embeddings-ollamallama-index是今天的主角框架,它把"分块、向量化、检索、生成"封装成了非常简单的 API;另外两个是让它能调用本地 Ollama 模型的适配器。
四、代码实战:完整可运行的 RAG 流程
下面这段代码是端到端、能直接跑的最小可用版本。我会一边贴代码一边讲。
4.1 先造一份演示资料
教程要能"跟着做",得有素材。我们在项目目录建一个data文件夹,并写一份示例《员工手册》。你可以直接复制下面这段 Python 来生成:
importos os.makedirs("data",exist_ok=True)handbook=""" 公司员工手册(示例) 一、年假规定 正式员工入职满一年后可享受年假。工作年限1-10年的,每年5天;10-20年的,每年10天;20年以上的,每年15天。年假需提前3个工作日向直属主管申请,当年未休完的可顺延至次年3月底。 二、加班与调休 工作日加班可申请调休,调休需在加班发生起的2个月内使用。法定节假日加班按三倍工资结算,不可折抵调休。 三、报销流程 员工因公产生的差旅、餐饮费用,需在费用发生后30天内提交报销单,并附发票原件。单笔超过2000元的支出须事前邮件报备。财务在收到合规单据后10个工作日内打款。 四、转正与试用期 试用期一般为3个月,表现突出者可申请提前转正,但不得低于法定最短试用期。转正需由部门负责人提交评估表。 """withopen("data/员工手册.txt","w",encoding="utf-8")asf:f.write(handbook)print("演示文档已生成:data/员工手册.txt")4.2 主程序:搭知识库 + 问答
新建文件rag_demo.py,内容如下:
fromllama_index.coreimport(VectorStoreIndex,SimpleDirectoryReader,Settings,)fromllama_index.llms.ollamaimportOllamafromllama_index.embeddings.ollamaimportOllamaEmbedding# ---------- 1. 配置本地模型 ----------# 生成模型:负责"读资料、写答案"Settings.llm=Ollama(model="qwen2.5:7b",base_url="http://localhost:11434")# 向量化模型:负责把文字变成向量Settings.embed_model=OllamaEmbedding(model_name="bge-m3",base_url="http://localhost:11434")# 分块大小:每块大约多少字符(中文可按字符粗算)Settings.chunk_size=512Settings.chunk_overlap=64# 块与块之间留点重叠,避免一句话被切断# ---------- 2. 读取文档(构建阶段) ----------documents=SimpleDirectoryReader("data").load_data()print(f"已加载{len(documents)}个文档")# ---------- 3. 构建向量索引(分块 + 向量化 + 入库,一步到位) ----------index=VectorStoreIndex.from_documents(documents)print("知识库构建完成")# ---------- 4. 问答(在线阶段) ----------query_engine=index.as_query_engine(similarity_top_k=3)# 每次取最相关3块whileTrue:q=input("\n请输入你的问题(输入 exit 退出):")ifq.strip().lower()=="exit":breakresponse=query_engine.query(q)print("\n【答案】")print(response.response)4.3 跑起来看效果
确保 Ollama 已经在后台运行(任务栏有图标,或终端跑ollama serve)。然后:
python rag_demo.py试着问:
请输入你的问题:年假最多能休几天?你会看到模型基于《员工手册》回答:“工作年限20年以上的员工,每年可享受15天年假……” ——它确实读了你给的文件,而不是凭空编的。这就是 RAG 的魔力。
小提示:第一次提问会稍微慢一点,因为框架要懒加载索引。如果回答明显"答非所问"或"我无法从资料中找到",多半是下面"常见问题"里要讲的检索没调好,先别慌,往下看。
五、常见问题:三个最容易卡住新手的坑
5.1 分块大小(chunk_size)怎么选?
核心矛盾:块太大 → 一块里杂糅太多无关内容,模型容易跑偏;块太小 → 一句话被切成两半,语义不完整,检索命中了也拼不出完整答案。
经验值参考:
| 场景 | 建议 chunk_size | 说明 |
|---|---|---|
| 中文手册 / 制度类 | 300–600 字符 | 中文信息密度高,不用照搬英文的 512 token |
| 长论文 / 技术文档 | 800–1200 字符 | 段落完整更重要 |
| 超短问答对(FAQ) | 整条当作一块 | 一条 Q+A 不要拆 |
还有个常被忽略的参数chunk_overlap(重叠):让相邻块共享一小段文字,能防止"答案正好卡在两块边界"的尴尬。一般设为 chunk_size 的 10%–20%。
调试方法:直接改Settings.chunk_size,重新构建索引,看答案质量变化。别过度追求"最优值",80 分够用就行。
5.2 embedding 模型选哪个?
embedding 模型决定了"检索准不准",是中文 RAG 里最值得花心思的一环。对比几个常用选择:
- BAAI/bge 系列(推荐):
bge-m3、bge-large-zh对中文支持极好,HuggingFace 上下载量最高的中文向量模型之一。本教程用的bge-m3还能中英混搜。 - nomic-embed-text:Ollama 里开箱即用的轻量英文模型,中文一般,适合纯英文场景。
- OpenAI text-embedding-3-small:效果稳定但要 API key、要联网、要花钱,且数据出本地。
- 智源 / 百度千帆 等国内方案:中文友好,但多一层账号和额度配置。
给新手的建议:先用本地的bge-m3把流程跑通,确认架构没问题后,再考虑换更强的 embedding 来提精度。
5.3 检索效果怎么调?
如果模型"找不到答案"或"找错资料",按这个顺序排查:
- 调大
similarity_top_k:从 3 调到 5 或 8,让它多翻几块资料,宁可多给点上下文。 - 改分块:块太大就调小,块太小就调大 + 加 overlap(见 5.1)。
- 看检索到了啥:打印中间结果,确认召回的资料真的和问题相关:
retriever=index.as_retriever(similarity_top_k=5)nodes=retriever.retrieve("年假怎么算?")forninnodes:print(round(n.score,3),n.text[:80])# 看看分数和片段对不对得上- 换更强的 embedding:
bge-m3还嫌不准,可上bge-large-zh-v1.5(需要走 HuggingFace 本地加载,而不只是 Ollama)。 - 上 rerank(见第六节):在初步召回后,再让一个"精排模型"把最相关的排到最前面。
六、进阶提示:让你的 RAG 更聪明
当你把基础版跑顺了,下面三招能显著提升效果,而且都不复杂。
6.1 Hybrid Search(混合检索)
纯向量检索擅长"语义相似",但碰上专有名词、编号、精确匹配(比如"条款第 3.2 条"“订单号 A2026”)就容易翻车。混合检索 =向量检索 + 关键词检索(BM25)双管齐下,最后合并结果。
LlamaIndex 里可以这样开启:
fromllama_index.core.retrieversimportQueryFusionRetrieverfromllama_index.retrievers.bm25importBM25Retriever# 向量检索器vector_retriever=index.as_retriever(similarity_top_k=5)# 关键词检索器bm25_retriever=BM25Retriever.from_defaults(docstore=index.docstore,similarity_top_k=5)# 融合两者fusion=QueryFusionRetriever([vector_retriever,bm25_retriever],num_queries=1,# 不做 query 扩展时设为 1mode="reciprocal_rerank",# 用 RRF 算法融合排序)nodes=fusion.retrieve("报销单超过2000元要怎么办?")6.2 Rerank(重排序)
召回阶段为了快,用的是"粗排"(向量相似度);rerank 阶段再用一个更聪明但更慢的模型,对这 Top-N 个候选做"精排",把真正相关的顶上去。
fromllama_index.core.query_engineimportRetrieverQueryEnginefromllama_index.core.postprocessorimportSentenceTransformerRerank rerank=SentenceTransformerRerank(model="BAAI/bge-reranker-base",top_n=3)engine=RetrieverQueryEngine.from_args(retriever=vector_retriever,node_postprocessors=[rerank])rerank 是"花小钱办大事"的典型:召回放宽(多取点),精排收紧(只留最准的),答案质量肉眼可见地变稳。
6.3 Query Expansion(查询扩展)
用户的问题往往太短、太口语,比如只问"年假"。查询扩展就是让模型先把问题改写成几个更完整的问法,分别去检索,再把结果合并。相当于一个人不会只搜一个关键词,而是换了几种说法都搜一遍。
fromllama_index.core.indices.query.query_transformimport(DecomposeQueryTransform,)# 简单地让 LLM 生成 3 个相似问法,各自检索再融合# (可配合 QueryFusionRetriever 的 num_queries 参数使用)实际中,把 6.1 里的num_queries设成 3~4,框架就会自动用 LLM 生成多个变体查询并融合,这正是 query expansion 的落地方式。
一句话总结进阶三件套:混合检索解决"找不全",rerank 解决"排不准",query expansion 解决"问不清"。
七、作者经验总结
我自己从零搭 RAG 时踩过不少坑,最后沉淀成几条实在的建议,送给同样在入门的你:
- 先跑通,再优化。新手最容易犯的错误是一上来就纠结"哪个 embedding 最强、要不要上向量数据库"。别。先用
bge-m3+ Ollama + 本地文件,把整条流水线跑出第一个正确答案,成就感有了,后面的优化才有方向。 - 中文场景,分块和 embedding 是两个最影响体验的旋钮。英文教程里的 512 token 不能直接套到中文,按"字符数"重新估;embedding 优先选 bge 系列,别拿英文模型硬凑中文。
- 检索不好,别急着怪模型。90% 的"答非所问"其实发生在检索阶段——资料根本没被召回,模型再聪明也只能瞎编。养成"先看检索到了什么"的习惯,可以省下大量调 prompt 的时间。
- 本地方案值得坚持。用 Ollama 把生成和 embedding 都放在本地,不仅免费、隐私可控,更重要的是让你真正理解每一层在干嘛。等你需要更大规模、更强模型时,再迁移到云端也不迟。
- RAG 不是银弹。它解决的是"让模型基于你的资料回答",但资料的准确性、分块是否合理、问题是否清晰,依然决定上限。把它当成一个"会查资料的助手",而不是"全知全能的 oracle"。
RAG 听起来高大上,拆开看其实就是"切文档 → 存向量 → 搜相关 → 交给模型"四步。真正难的不是技术,而是愿意动手把第一个 demo 跑起来。希望这篇教程能帮你跨过那道门槛——打开终端,装好 Ollama,复制代码,按下回车,你就已经有了一个属于自己的本地知识库问答系统。
祝玩得开心,也玩得明白。
本文配套代码与示例文档均已给出,可直接复制运行。如需扩展,建议优先尝试第六节的三大进阶技巧。