10分钟搭建第一个RAG问答系统

📅 2026/7/4 19:46:55 👁️ 阅读次数 📝 编程学习
10分钟搭建第一个RAG问答系统

10分钟搭建第一个RAG问答系统

前两篇我们把概念和技术栈聊透了。今天不废话,直接撸代码——10分钟,从零跑通一个RAG问答系统

你会得到一个能上传PDF、然后对着它问问题的AI助手。麻雀虽小,五脏俱全。

大家好,我是黒漂技术佬。


一、先明确:我们这10分钟要干什么?

完整流程:

上传 PDF 文档 → 解析文本 → 分块 → Embedding → 存入 Chroma ↓ 用户提问 → Embedding → Chroma检索 → 拼接Prompt → LLM回答

技术选型(最小可行版):

组件选型理由
文档解析PyMuPDF(fitz)轻量,处理中文PDF效果好
分块LangChain TextSplitter不用重复造轮子
EmbeddingSiliconFlow API(BGE-large)免部署,注册即用,免费额度够玩
向量库Chroma零配置,Python 一行代码起
LLMDeepSeek API性价比最高,中文一流

为什么用 API 而不是本地部署?先跑通,再优化。本地部署 Embedding 模型要下好几个G的模型文件,调试起来一小时起步。先用 API 把坑踩一遍,后面再考虑私有化。


二、环境准备

Step 1:安装依赖

pipinstalllangchain langchain-community langchain-chroma\pymupdf chromadb openai python-dotenv

这里解释一下:

  • langchain:RAG 框架,帮你把各种组件串起来
  • langchain-chroma:LangChain 的 Chroma 集成
  • pymupdf:PDF 解析利器
  • chromadb:向量数据库(轻量版)
  • openai:虽然我们用 DeepSeek,但 DeepSeek 兼容 OpenAI 的接口规范,所以可以用 openai 库调用

Step 2:申请 API Key

DeepSeek:去 platform.deepseek.com 注册,点「API Keys」创建一个。新用户一般送 10 元额度,够你玩几天的。

SiliconFlow(Embedding 用):去 siliconflow.cn 注册,同样创建 API Key。他们的 BGE-large-zh 模型调用免费额度相当大方。

Step 3:配置环境变量

在项目根目录创建.env文件:

DEEPSEEK_API_KEY=sk-xxxxxxxxxxxxxxxxDEEPSEEK_BASE_URL=https://api.deepseek.com/v1SILICONFLOW_API_KEY=sk-xxxxxxxxxxxxxxxxSILICONFLOW_BASE_URL=https://api.siliconflow.cn/v1

三、核心代码(60行,逐行注释)

新建rag_mini.py,贴入以下代码:

importosfromdotenvimportload_dotenvfromlangchain_text_splittersimportRecursiveCharacterTextSplitterfromlangchain_community.document_loadersimportPyMuPDFLoaderfromlangchain_chromaimportChromafromlangchain_openaiimportOpenAIEmbeddings,ChatOpenAIfromlangchain_core.promptsimportChatPromptTemplatefromlangchain_core.output_parsersimportStrOutputParserfromlangchain_core.runnablesimportRunnablePassthrough load_dotenv()# ========== 1. 加载并解析 PDF ==========defload_pdf(file_path:str):"""加载 PDF 文件,返回 LangChain Document 对象列表"""loader=PyMuPDFLoader(file_path)documents=loader.load()print(f"loaded{len(documents)}pages from{file_path}")returndocuments# ========== 2. 文本分块 ==========defsplit_documents(documents):"""把长文档切成小块,每块约 500 字,相邻块重叠 100 字"""text_splitter=RecursiveCharacterTextSplitter(chunk_size=500,# 每块最多 500 个字符chunk_overlap=100,# 相邻块重叠 100 个字符separators=["\n\n","\n","。",".","!","?"," ",""]# 分隔符优先级:先按段落切,再按句子切)chunks=text_splitter.split_documents(documents)print(f"split into{len(chunks)}chunks")returnchunks# ========== 3. 向量化并存入 Chroma ==========defbuild_vectorstore(chunks):"""把文本块 Embedding 后存入 Chroma 向量数据库"""# SiliconFlow 兼容 OpenAI 接口格式embeddings=OpenAIEmbeddings(model="BAAI/bge-large-zh-v1.5",# 中文最好的开源Embedding之一base_url=os.getenv("SILICONFLOW_BASE_URL"),api_key=os.getenv("SILICONFLOW_API_KEY"),)vectorstore=Chroma.from_documents(documents=chunks,embedding=embeddings,persist_directory="./chroma_db"# 向量数据持久化到本地# 这样下次启动不用重新 Embedding)print(f"vectorstore built with{vectorstore._collection.count()}vectors")returnvectorstore# ========== 4. 构建完整的 RAG 链 ==========defbuild_rag_chain(vectorstore):"""组装 RAG 流水线:检索 → 拼 Prompt → LLM 生成"""# LLM:用 DeepSeek(兼容 OpenAI 接口)llm=ChatOpenAI(model="deepseek-chat",# DeepSeek-V3base_url=os.getenv("DEEPSEEK_BASE_URL"),api_key=os.getenv("DEEPSEEK_API_KEY"),temperature=0.3,# 低温度 = 回答更确定性,不易编造)# Prompt 模板:告诉 LLM 怎么回答prompt=ChatPromptTemplate.from_template(""" 你是一个专业的企业知识库助手。请根据下面的文档内容,回答用户的问题。 规则: 1. 只能基于提供的文档内容回答,不要使用你自己的知识 2. 如果文档中没有相关信息,请直接说"文档中没有找到相关信息" 3. 回答要简洁、准确,用中文 【文档内容】 {context} 【用户问题】 {question} 【回答】 """)# 构建链:检索器 → 格式化 → LLM → 解析输出retriever=vectorstore.as_retriever(search_kwargs={"k":5}# 每次检索返回最相关的 5 个文本块)defformat_docs(docs):"""把检索到的文档拼接成一段文本"""return"\n\n".join(doc.page_contentfordocindocs)chain=({"context":retriever|format_docs,"question":RunnablePassthrough()}|prompt|llm|StrOutputParser())returnchain# ========== 5. 主流程 ==========defmain():pdf_path="test.pdf"# ← 改成你自己的 PDF 路径# 检查是否有已保存的向量库(避免重复 Embedding)ifos.path.exists("./chroma_db"):print("found existing vectorstore, loading...")embeddings=OpenAIEmbeddings(model="BAAI/bge-large-zh-v1.5",base_url=os.getenv("SILICONFLOW_BASE_URL"),api_key=os.getenv("SILICONFLOW_API_KEY"),)vectorstore=Chroma(persist_directory="./chroma_db",embedding_function=embeddings)else:print("building new vectorstore...")docs=load_pdf(pdf_path)chunks=split_documents(docs)vectorstore=build_vectorstore(chunks)# 构建 RAG 链chain=build_rag_chain(vectorstore)# 交互式问答print("\n===== RAG 问答系统就绪,输入 quit 退出 =====\n")whileTrue:question=input("你:")ifquestion.lower()=="quit":break# 流式输出(打字机效果)print("AI:",end="",flush=True)forchunkinchain.stream(question):print(chunk,end="",flush=True)print("\n")if__name__=="__main__":main()

四、跑起来

python rag_mini.py

第一次运行会下载 Chroma 的依赖,然后依次执行:解析PDF → 分块 → Embedding → 入库。之后在同目录下生成chroma_db/文件夹,下次启动直接加载,秒进问答状态

找一份你自己的 PDF 扔进去试试。比如公司的《员工手册》、产品的《技术白皮书》、或者你在网上随便下载的某份技术文档。

问几个问题感受一下:

你:这份文档的核心内容是什么? AI:(基于文档内容回答……) 你:第三章提到了哪些安全要求? AI:(从第三章的文本块中检索并回答……) 你:作者最喜欢的编程语言是什么? AI:文档中没有找到相关信息

注意最后一个问题——如果 PDF 里确实没写这个,AI 应该老实说"不知道",而不是编一个 Python 出来。这就是 Prompt 模板里那个规则的作用。


五、这60行代码里藏着哪些关键细节?

1.RecursiveCharacterTextSplitter的分隔符很重要

separators=["\n\n","\n","。",".","!","?"," ",""]

这个列表的顺序决定了切分优先级:先找双换行(段落边界),找不到再找单换行、句号、感叹号……最后才在空格甚至任意字符处切。这样能最大限度保证每个 chunk 是一个完整的语义单元,而不是在句子中间一刀砍断。

2.chunk_overlap=100是安全网

相邻 chunk 有 100 字的重叠。假设原文是「……因此,系统设计遵循了以下原则:第一,高内聚低耦合……」——"以下原则"刚好在 chunk A 末尾,"第一……“在 chunk B 开头。如果没有 overlap,检索时可能只命中 chunk A 或只命中 chunk B,导致 LLM 看不到"原则的具体内容”,回答质量就差一截。

3.temperature=0.3抑制幻觉

Temperature 控制 LLM 输出的"随机性"。知识库问答场景,我们希望 AI 忠实于文档,而不是自由发挥。所以把 temperature 调到 0.3(范围 0~2,越低越确定性)。但这个值也不能是 0,太低会让回答像机器人复读机。

4. Chroma 的 persist_directory 让你不重新花钱

Embedding API 虽然便宜,但每调一次都是钱。把向量库持久化到本地后,只要 PDF 不变,重启多少次都不需要重新调 Embedding API——这就是persist_directory的价值。


六、跑通之后,你会发现的第一个问题

95% 的人第一次跑通 RAG Demo 后,都会说同一句话:“这东西有时候准,有时候不准啊。”

没错。Demo 和「好用」之间,还隔着好几座大山:

  • 文档里有表格,解析后变成一坨无意义的数字
  • 用户问法太口语化,向量检索完全跑偏
  • 一个 PDF 上百页,但用户的问题高度浓缩,Top-5 的检索结果全是废话

这些就是我们后面几篇要逐一解决的。今天这篇的目的只是让你10 分钟内把整个链路跑通,让你对大框架有个感性认识。知道哪里会出问题,才知道后面该往哪优化。


💬 你跑通了吗?用的是什么文档?有什么奇怪的问题?评论区发出来,一起看看怎么调!