☰
RAG 创建实战:从零搭一个能回答问题的知识库
2026/9/30 13:08:13 网站建设 项目流程

1. 引言

今天不聊概念,直接上手。我带你从零搭一个 RAG(Retrieval-Augmented Generation,检索增强生成)系统,让大模型能基于你自己的文档回答问题。整个过程我会按真实操作的口吻来写,每一步都告诉你我做了什么、踩了什么坑、怎么排查。
先交代一下我的环境:Python 3.10,Windows 11,用 OpenAI 兼容接口(本地也可以用 Ollama 跑开源模型)。代码我尽量写得能直接复制运行。
本文的最终成果:一个能直接复制运行、可复用的 RAG 脚本——你只要把docs/里的资料换成自己的,就能让大模型基于你的知识库回答问题。适用读者:有 Python 基础、想快速搭建 RAG 的开发者。阅读完你将获得什么:从原理到踩坑的完整实战路径,以及一套开箱即用的代码,照着敲就能跑通。

2. 原理与大纲

先花两分钟把原理讲清楚,后面动手才不会懵。RAG 说白了就一句话:先检索,再生成。大模型本身记不住你的私有资料,那就先把资料切成块、转成向量存起来;用户提问时,先从向量库里捞出最相关的几块,拼进 prompt 一起喂给模型,让它「看着资料回答」。
整个流程拆开就是四步:

  • 加载:把文档读进来
  • 切块:把长文档切成小块
  • 向量化存储:把块转成向量存进向量库
  • 检索生成:提问时检索相关块,拼给模型生成答案

下面这张图把整个 RAG 流程串起来,后面每一步都对应图里的一个环节:

用户提问

加载文档

切分文档

向量化存储

向量库 ChromaDB

检索相关块

拼进 Prompt

大模型生成答案

返回回答

下面按这个顺序一步步来。先看大纲:

    1. 引言
    1. 原理与大纲
    1. 准备工作:装依赖
    1. 准备文档数据
    1. 切分文档
    1. 生成向量并存入向量库
    1. 构建检索链
    1. 完整跑通:一个可复用的脚本
    1. 实现方式对比
    1. 踩坑记录
    1. 总结

2. 准备工作:装依赖

打开终端,先建个虚拟环境,免得把系统 Python 搞乱:

python-mvenv rag_env rag_env\Scripts\activate# Windows# source rag_env/bin/activate # Linux/Mac

然后装依赖。我用的核心库是 LangChain 0.3 系列、ChromaDB 做向量库、以及 OpenAI 的 SDK:

pipinstalllangchain langchain-community langchain-openai chromadb openai python-dotenv

装完验证一下版本,防止版本不兼容:

python-c"import langchain; print(langchain.__version__)"

我这边输出的是0.3.x。如果你装到 0.2 或更早,建议升级到 0.3,因为后面有些 API 写法不一样。

3. 准备文档数据

RAG 的第一步是「喂数据」。我准备了几篇 Markdown 格式的技术笔记,放在docs/目录下。你也可以用 PDF、TXT、网页,后面我会说怎么换加载器。

先写一个加载文档的脚本。这里我用 LangChain 的目录加载器,一次性把整个文件夹读进来:

fromlangchain_community.document_loadersimportDirectoryLoader,TextLoader loader=DirectoryLoader("docs",glob="**/*.md",loader_cls=TextLoader,loader_kwargs={"encoding":"utf-8"},)docs=loader.load()print(f"加载了{len(docs)}个文档")

这里有个坑:Windows 下如果文档是 GBK 编码,TextLoader默认用 UTF-8 读会报UnicodeDecodeError。我一开始就踩了,后来在loader_kwargs里显式指定encoding="utf-8"才解决。如果你的文档是别的编码,改成对应的就行。

4. 切分文档

加载进来的文档可能很长,直接塞给向量库效果很差,所以要先切块。切块的大小和重叠度直接影响检索质量,我一般这样配:

fromlangchain_text_splittersimportRecursiveCharacterTextSplitter splitter=RecursiveCharacterTextSplitter(chunk_size=500,chunk_overlap=50,separators=["\n\n","\n","。","!","?"," ",""],)chunks=splitter.split_documents(docs)print(f"切分成{len(chunks)}个块")

chunk_size=500表示每块约 500 字符,chunk_overlap=50让相邻块有 50 字符重叠,避免把一句话从中间切断导致语义丢失。separators里我加了中文标点,这样中文文档切得更自然。

5. 生成向量并存入向量库

接下来把每个块转成向量,存进 ChromaDB。这一步是 RAG 的核心——把文本变成机器能算相似度的数字。

我用 OpenAI 的 embedding 模型:

fromlangchain_openaiimportOpenAIEmbeddingsfromlangchain_community.vectorstoresimportChroma embeddings=OpenAIEmbeddings(model="text-embedding-3-small")vectorstore=Chroma.from_documents(documents=chunks,embedding=embeddings,persist_directory="./chroma_db",)print("向量库创建完成,已持久化到 ./chroma_db")

跑完你会看到./chroma_db目录下生成了索引文件。persist_directory指定持久化路径,这样下次启动不用重新算向量。

如果你没有 OpenAI 的 key,也可以用本地模型。我试过用 Ollama 跑nomic-embed-text,效果也还行:

fromlangchain_community.embeddingsimportOllamaEmbeddings embeddings=OllamaEmbeddings(model="nomic-embed-text")

6. 构建检索链

向量库建好了,现在把它接进大模型。这里我用 LangChain 的RetrievalQA链,它会把「检索 + 生成」串起来:

fromlangchain_openaiimportChatOpenAIfromlangchain.chainsimportRetrievalQA llm=ChatOpenAI(model="gpt-4o-mini",temperature=0)qa_chain=RetrievalQA.from_chain_type(llm=llm,chain_type="stuff",retriever=vectorstore.as_retriever(search_kwargs={"k":4}),)answer=qa_chain.invoke("什么是 RAG?")print(answer["result"])

k=4表示每次检索取最相似的 4 个块喂给模型。chain_type="stuff"表示把所有检索结果直接拼进 prompt,适合块数不多的情况。

7. 完整跑通:一个可复用的脚本

上面几步拆开讲,实际用的时候我习惯合成一个脚本,方便反复跑。下面是我最终用的版本:

importosfromdotenvimportload_dotenvfromlangchain_community.document_loadersimportDirectoryLoader,TextLoaderfromlangchain_text_splittersimportRecursiveCharacterTextSplitterfromlangchain_openaiimportOpenAIEmbeddings,ChatOpenAIfromlangchain_community.vectorstoresimportChromafromlangchain.chainsimportRetrievalQA load_dotenv()# 1. 加载loader=DirectoryLoader("docs",glob="**/*.md",loader_cls=TextLoader,loader_kwargs={"encoding":"utf-8"})docs=loader.load()# 2. 切分splitter=RecursiveCharacterTextSplitter(chunk_size=500,chunk_overlap=50,separators=["\n\n","\n","。","!","?"," ",""])chunks=splitter.split_documents(docs)# 3. 向量化 + 存储embeddings=OpenAIEmbeddings(model="text-embedding-3-small")vectorstore=Chroma.from_documents(documents=chunks,embedding=embeddings,persist_directory="./chroma_db")# 4. 检索 + 生成llm=ChatOpenAI(model="gpt-4o-mini",temperature=0)qa_chain=RetrievalQA.from_chain_type(llm=llm,chain_type="stuff",retriever=vectorstore.as_retriever(search_kwargs={"k":4}),return_source_documents=True,)whileTrue:query=input("请输入问题(输入 exit 退出):")ifquery.lower()=="exit":breakresult=qa_chain.invoke(query)print("\n回答:",result["result"],"\n")# 打印来源文档,方便追溯答案出处print("--- 来源文档 ---")fori,docinenumerate(result["source_documents"],1):source=doc.metadata.get("source","未知来源")snippet=doc.page_content[:100]print(f"[{i}] 来源:{source}")print(f" 片段:{snippet}...")print()

.env文件里放你的 API key:

OPENAI_API_KEY=sk-xxxx

9. 实现方式对比

上面用的是 LangChain + ChromaDB 这一套,也是目前最主流的组合。但 RAG 的实现路径不止一条,我把我试过的几种列出来,优缺点都摆一摆,你按自己的场景挑。
方式一:LangChain + ChromaDB(本文用的)

  • 优点:生态成熟、文档多、上手快,切块、向量化、检索一条龙都封装好了
  • 缺点:依赖较重,LangChain 版本升级 API 容易变;ChromaDB 单机够用,数据量大或要并发时吃力

方式二:LlamaIndex + FAISS

  • 优点:对「索引」这件事做得更细,支持多种索引结构,检索精度高
  • 缺点:概念比 LangChain 多,学习曲线陡一点;FAISS 是内存索引,重启要重新加载

方式三:纯手写(embedding + 向量检索 + prompt 拼接)

  • 优点:最轻量,没有框架包袱,每一步都看得懂、可控
  • 缺点:要自己处理切块、持久化、检索逻辑,代码量上去了,维护成本高

方式四:RAGFlow / Dify 这类平台

  • 优点:可视化配置,拖拽就能搭,适合快速验证和给非技术同事用
  • 缺点:定制性差,想改底层逻辑就受限;数据量大时性能不一定好

怎么选?我的建议是:想快速跑通、验证想法,用方式一;对检索精度要求高、愿意折腾,试方式二;想彻底搞懂原理,强烈建议手写一遍方式三;团队里非技术人多,再考虑方式四。
怎么选?我的建议是:想快速跑通、验证想法,用方式一;对检索精度要求高、愿意折腾,试方式二;想彻底搞懂原理,强烈建议手写一遍方式三;团队里非技术人多,再考虑方式四。

四种方式放在一起对比,优缺点和适用场景一目了然:

方式优点缺点适用场景
方式一:LangChain + ChromaDB生态成熟、文档多、上手快,切块、向量化、检索一条龙都封装好了依赖较重,LangChain 版本升级 API 容易变;ChromaDB 单机够用,数据量大或要并发时吃力快速跑通、验证想法,个人或小团队起步
方式二:LlamaIndex + FAISS对「索引」这件事做得更细,支持多种索引结构,检索精度高概念比 LangChain 多,学习曲线陡一点;FAISS 是内存索引,重启要重新加载对检索精度要求高、愿意折腾的进阶场景
方式三:纯手写最轻量,没有框架包袱,每一步都看得懂、可控要自己处理切块、持久化、检索逻辑,代码量上去了,维护成本高想彻底搞懂原理、学习 RAG 内部机制
方式四:RAGFlow / Dify 平台可视化配置,拖拽就能搭,适合快速验证和给非技术同事用定制性差,想改底层逻辑就受限;数据量大时性能不一定好团队里非技术人多、需要快速交付原型

四种方式的定位差异,用一张图看得更清楚:

方式四:RAGFlow / Dify

可视化、拖拽即用

定制性差

方式三:纯手写

最轻量、可控

代码量大、维护成本高

方式二:LlamaIndex + FAISS

索引精细、精度高

概念多、内存索引

方式一:LangChain + ChromaDB

生态成熟、上手快

依赖较重、单机够用

按场景选择

8. 踩坑记录

这一节把我实际遇到的问题列出来,你遇到类似情况可以直接对照:

坑 1:编码问题

  • 现象:加载文档报UnicodeDecodeError
  • 解决:loader_kwargs={"encoding": "utf-8"},或改成文档实际编码

坑 2:检索结果答非所问

  • 现象:模型回答跟问题对不上
  • 解决:调大k值,或减小chunk_size让块更聚焦;也可以检查文档切分是否把关键信息切断了

坑 3:向量库重复写入

  • 现象:每次跑脚本都往同一个./chroma_db追加,导致检索结果重复
  • 解决:重建前先删掉旧目录,或者用Chroma(persist_directory=..., embedding=...)加载已有库而不是重新from_documents

坑 4:LangChain 版本 API 变化

  • 现象:RetrievalQA或Chroma导入报错
  • 解决:确认langchain是 0.3.x,langchain-community和langchain-openai都装了

11. 总结

到这里,一个能用的 RAG 系统就跑通了。核心流程就四步:加载文档 → 切块 → 向量化存储 → 检索生成。你只要把docs/里的内容换成自己的资料,就能让大模型基于你的知识库回答问题。
最后用一张图回顾整个核心流程:

加载文档

切块

向量化存储

检索生成

回答

说到底,RAG 的本质就一件事:给大模型配一个「外挂记忆」。模型记不住你的私有资料,那就把资料变成可检索的向量,提问时先捞出来再让它回答。不管用 LangChain、LlamaIndex 还是手写,绕来绕去都是「检索 + 生成」这两个动作。理解了这一点,换什么框架都只是换工具,核心思路不变。

下一步你可以试试:换 PDF 加载器、接入本地模型、或者把检索结果和引用来源一起返回。有问题欢迎在评论区交流。

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

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

立即咨询