RAG 系统开发学习笔记:从零到 Web 应用
本笔记记录了我从零开始构建一个完整 RAG(检索增强生成)系统的全过程,包括环境搭建、文档处理、向量检索、LLM 生成、跨语言处理、Web 界面等所有环节。既是学习复盘,也是后续查阅的手册。
📋 目录
一、项目概述
1.1 项目目标
构建一个 3D 打印故障排查助手,实现:
- 用户用中文提问
- 系统从英文 3D 打印排障文档中检索相关内容
- 大模型基于检索结果用中文回答
- 提供 Web 界面,支持多轮对话
1.2 最终成果
用户提问(中文)
↓
【1】问题向量化(bge-m3 多语言模型)
↓
【2】Chroma 向量库检索 Top-K 相关块
↓
【3】拼接 Prompt(资料 + 问题)
↓
【4】LLM 生成中文答案
↓
【5】Web 界面展示 + 参考资料溯源1.3 技术栈
| 组件 | 选型 | 说明 |
|---|---|---|
| 开发语言 | Python 3.13+ | |
| 框架 | LangChain | LLM 应用编排 |
| Embedding | BAAI/bge-m3 | 多语言,中英跨语言检索 |
| 向量库 | Chroma | 轻量、本地持久化 |
| LLM | 本地部署Qwen3:8b模型 | OpenAI 协议兼容 |
| Web 界面 | Streamlit | 纯 Python 快速搭建 |
| 文档格式 | txt / md / pdf | 通过 LangChain Loader |
二、核心概念速查
2.1 RAG 是什么
RAG = 检索(Retrieval)+ 生成(Generation)
让大模型先"查资料",再"回答问题"。相比纯 LLM 的优势:
- ✅ 减少幻觉(基于真实资料回答)
- ✅ 可引入私有/最新知识
- ✅ 答案可溯源
2.2 关键概念
| 概念 | 一句话解释 |
|---|---|
| Embedding | 把文字变成一串数字(向量),语义相近的文字向量也相近 |
| 向量数据库 | 存储向量并支持"找最相似"的数据库 |
| Chunk | 把长文档切成的小块,便于检索和喂给 LLM |
| chunk_size | 每块的目标字符数,中文 300~800,英文 500~1000 |
| chunk_overlap | 相邻块的重叠字符数,通常为 chunk_size 的 10%~20% |
| Top-K | 检索时返回最相似的 K 个块,通常 K=3~8 |
| Rerank | 对召回结果二次精排,提升精度 |
| temperature | LLM 输出随机性,RAG 场景用 0 |
| 跨语言 RAG | 问题与知识库不同语言时,用多语言 embedding 模型 |
2.3 温度(temperature)取值建议
| 场景 | temperature |
|---|---|
| RAG 问答、事实查询 | 0 ~ 0.2 |
| 代码生成 | 0 ~ 0.2 |
| 翻译、总结 | 0.3 ~ 0.5 |
| 通用聊天 | 0.7 |
| 创意写作 | 0.8 ~ 1.5 |
三、环境搭建
3.1 创建虚拟环境
cd C:\MyProject\3DRAG
python -m venv rag_env
rag_env\Scripts\activate激活成功后终端会显示 (rag_env)。
3.2 安装依赖
# 升级 pip
python -m pip install --upgrade pip
# 设置国内镜像源(加速下载)
pip config set global.index-url https://pypi.tuna.tsinghua.edu.cn/simple
# 核心依赖
pip install langchain langchain-community langchain-openai
pip install langchain-text-splitters
pip install langchain-huggingface langchain-chroma
pip install chromadb
pip install sentence-transformers
pip install pypdf
pip install streamlit
pip install python-dotenv3.3 各依赖作用
| 包 | 作用 |
|---|---|
langchain | LLM 应用核心框架 |
langchain-community | 第三方集成(Loader、VectorStore) |
langchain-openai | OpenAI 协议对接(兼容 DeepSeek、智谱等) |
langchain-text-splitters | 文本切分(新版独立包) |
langchain-huggingface | HuggingFace embedding 集成 |
langchain-chroma | Chroma 向量库集成 |
chromadb | 向量数据库本体 |
sentence-transformers | 本地 embedding 模型 |
pypdf | PDF 解析 |
streamlit | Web 界面框架 |
python-dotenv | 环境变量管理 |
3.4 HuggingFace 镜像配置(重要)
国内访问 huggingface.co 会失败,必须配置镜像:
import os
os.environ["HF_ENDPOINT"] = "https://hf-mirror.com"
# ↑ 必须放在所有 HuggingFace 相关 import 之前永久配置(PowerShell):
[Environment]::SetEnvironmentVariable("HF_ENDPOINT", "https://hf-mirror.com", "User")四、完整开发流程
4.1 第一步:准备知识库文档
project/
├── docs/
│ ├── 3D Printing Troubleshooting Guide.md
│ └── ...
└── ...文档质量决定一切:垃圾进,垃圾出。优先使用干净、结构清晰的文档。
4.2 第二步:加载文档
from langchain_community.document_loaders import TextLoader, PyPDFLoader
documents = []
for file in os.listdir("docs"):
path = os.path.join("docs", file)
if file.lower().endswith((".txt", ".md", ".markdown")):
loader = TextLoader(path, encoding="utf-8")
elif file.lower().endswith(".pdf"):
loader = PyPDFLoader(path)
else:
continue
documents.extend(loader.load())支持的格式与 Loader 对照:
| 格式 | Loader |
|---|---|
| .txt / .md | TextLoader |
PyPDFLoader | |
| .docx | Docx2txtLoader |
| .csv | CSVLoader |
| 网页 URL | WebBaseLoader |
4.3 第三步:切分文档
from langchain_text_splitters import RecursiveCharacterTextSplitter
# 中文文档
splitter = RecursiveCharacterTextSplitter(
chunk_size=500,
chunk_overlap=50,
separators=["\n\n", "\n", "。", "!", "?", ";", ",", " ", ""]
)
# 英文文档
splitter = RecursiveCharacterTextSplitter(
chunk_size=800,
chunk_overlap=100,
separators=["\n\n", "\n", ". ", "! ", "? ", "; ", ", ", " ", ""]
)
chunks = splitter.split_documents(documents)切分三原则:
- chunk_size:中文 300~800,英文 500~1000
- chunk_overlap:chunk_size 的 10%~20%
- separators:按语义优先级排列(段落 → 换行 → 句子 → 词)
4.4 第四步:向量化 + 存储
from langchain_huggingface import HuggingFaceEmbeddings
from langchain_chroma import Chroma
embedding_model = HuggingFaceEmbeddings(
model_name="BAAI/bge-m3", # 多语言模型
model_kwargs={"device": "cpu"},
encode_kwargs={"normalize_embeddings": True}
)
vectorstore = Chroma.from_documents(
documents=chunks,
embedding=embedding_model,
persist_directory="./chroma_db"
)Embedding 模型选择:
| 模型 | 大小 | 语言 | 场景 |
|---|---|---|---|
| BAAI/bge-small-zh-v1.5 | 100 MB | 中文 | 纯中文,速度优先 |
| BAAI/bge-m3 | 2.2 GB | 多语言 | 跨语言检索 ✅ |
| paraphrase-multilingual-MiniLM-L12-v2 | 470 MB | 多语言 | 轻量多语言 |
4.5 第五步:检索 + 生成
from langchain_openai import ChatOpenAI
from langchain_core.prompts import ChatPromptTemplate
from langchain_core.runnables import RunnablePassthrough
from langchain_core.output_parsers import StrOutputParser
# 1. LLM
llm = ChatOpenAI(
model="模型",
api_key="your-key",
base_url="URL",
temperature=0,
max_tokens=800,
timeout=30
)
# 2. Prompt
prompt = ChatPromptTemplate.from_template("""
你是一个 3D 打印故障排查专家,精通 FDM 和树脂两种工艺。
请根据下面的参考资料回答用户问题。
要求:
1. 优先使用参考资料中的内容回答
2. 注意区分 FDM 和树脂打印,不要把树脂内容当 FDM 用
3. 如果资料中没有直接答案,说明"资料中未直接提及,以下为推测"
4. 回答必须使用中文,关键英文术语保留
5. 末尾列出参考的资料编号
【参考资料】
{context}
【用户问题】
{question}
【回答】
""")
# 3. 检索器
retriever = vectorstore.as_retriever(search_kwargs={"k": 8})
# 4. 格式化函数(关键:带编号)
def format_docs(docs):
return "\n\n".join(
f"[资料{i+1}] {doc.page_content}"
for i, doc in enumerate(docs)
)
# 5. 组装链
rag_chain = (
{"context": retriever | format_docs, "question": RunnablePassthrough()}
| prompt
| llm
| StrOutputParser()
)
# 6. 调用
answer = rag_chain.invoke("大底面翘边怎么办?")4.6 第六步:Web 界面(Streamlit)
import streamlit as st
st.set_page_config(page_title="3D 打印助手", page_icon="🖨️")
st.title("🖨️ 3D 打印故障排查助手")
@st.cache_resource # 关键:只加载一次模型
def load_rag():
# 加载 embedding、向量库、LLM、rag_chain
...
return retriever, rag_chain
with st.spinner("加载中..."):
retriever, rag_chain = load_rag()
# 聊天历史
if "messages" not in st.session_state:
st.session_state.messages = []
# 显示历史
for msg in st.session_state.messages:
with st.chat_message(msg["role"]):
st.markdown(msg["content"])
# 输入
question = st.chat_input("请输入你的问题...")
if question:
st.session_state.messages.append({"role": "user", "content": question})
with st.chat_message("user"):
st.markdown(question)
with st.chat_message("assistant"):
with st.spinner("思考中..."):
docs = retriever.invoke(question)
answer = rag_chain.invoke(question)
st.markdown(answer)
with st.expander("📚 查看参考资料原文"):
for i, doc in enumerate(docs):
st.markdown(f"**[资料{i+1}]**")
st.text(doc.page_content[:500])
st.session_state.messages.append({
"role": "assistant",
"content": answer,
"sources": docs
})启动:
streamlit run app.py五、关键代码详解
5.1 RecursiveCharacterTextSplitter 的切分逻辑
它按 separators 列表从上到下尝试:
1. 先按 "\n\n"(段落)切
2. 如果某段还超 chunk_size,按 "\n"(换行)切
3. 还超,按 "。"(句子)切
4. 继续按 "!?;," 切
5. 最后按空格、单字切好处:优先保持语义完整,不会把一句话从中间砍断。
5.2 为什么 chunk_overlap 很重要
例子:
原文:...退货需要在7天内申请。超过7天,
将无法办理退货手续...
不重叠:
块1:...退货需要在7天内申请。超过7天,
块2:将无法办理退货手续...
→ 用户问"超过7天能退货吗",可能只召回块2,缺上下文
有重叠:
块1:...退货需要在7天内申请。超过7天,
块2:退货需要在7天内申请。超过7天,将无法办理退货手续...
→ 上下文完整5.3 format_docs 为什么要加编号
不加编号:LLM 收到一堆纯文本,不知道哪块是哪块,无法引用。
加编号:
[资料1] # 45. 打印件边缘翘起(翘曲)
在模型的底部,打印件向上弯曲...
[资料2] 原因
这些3D打印问题是打印床没有正确调平的典型迹象...LLM 就能在回答末尾写 📚 参考:[资料1]、[资料3]。
5.4 @st.cache_resource 的必要性
Streamlit 每次交互都重新运行整个脚本。如果没有缓存:
- 每次提问都重新加载 bge-m3(10~15 秒)
- 每次提问都重新连接向量库
@st.cache_resource 让函数只运行一次,结果被缓存复用。
这是 Streamlit + RAG 最重要的技巧。
5.5 st.session_state 的作用
Streamlit 每次交互都重新运行脚本,普通变量会丢失。session_state 是跨交互持久的存储,用来保存聊天历史。
六、踩坑记录与解决方案
坑 1:LangChain 模块路径变化
报错:
ModuleNotFoundError: No module named 'langchain.text_splitter'原因:LangChain 把文本切分拆成独立包。
解决:
# 旧(已失效)
from langchain.text_splitter import RecursiveCharacterTextSplitter
# 新
from langchain_text_splitters import RecursiveCharacterTextSplitter坑 2:langchain-community 弃用警告
DeprecationWarning: `langchain-community` is being sunset...原因:LangChain 正在把社区集成拆成独立包。
解决:装新包,用新导入路径。
pip install langchain-huggingface langchain-chroma# 旧
from langchain_community.embeddings import HuggingFaceEmbeddings
from langchain_community.vectorstores import Chroma
# 新
from langchain_huggingface import HuggingFaceEmbeddings
from langchain_chroma import Chroma坑 3:HuggingFace 下载失败
报错:
连接尝试失败 ... https://huggingface.co/...原因:国内无法访问 HuggingFace。
解决:配置镜像,必须在 import 之前设置。
import os
os.environ["HF_ENDPOINT"] = "https://hf-mirror.com"
# ↑ 必须放最前面
from langchain_huggingface import HuggingFaceEmbeddings坑 4:UnicodeEncodeError: 'ascii' codec can't encode...
原因:api_key 或 base_url 里含中文/全角字符。
解决:
- 检查
ChatOpenAI的api_key、base_url、model三个参数 - 必须纯 ASCII(英文/数字/符号)
- 重新从服务商后台复制 Key
坑 5:min() iterable argument is empty
原因:docs/ 目录为空或没有可加载的文件。
解决:
- 用
dir docs检查文件是否存在 - 确认扩展名是
.txt/.md/.pdf - 检查文件编码是否为 UTF-8
坑 6:Windows 隐藏扩展名导致 测试.txt.txt
解决:资源管理器 → 查看 → 勾选"文件扩展名"。
坑 7:跨语言检索不匹配
现象:中文提问,英文文档,检索不到。
原因:用了纯中文 embedding 模型(如 bge-small-zh)。
解决:换多语言模型 BAAI/bge-m3。
坑 8:不同打印工艺内容互相污染
现象:问 PETG(FDM),召回了树脂打印内容。
解决:在 Prompt 里加"工艺区分"要求:
如果参考资料里的内容明显属于其他打印工艺(如树脂打印 SLA/DLP),
而用户问的是 FDM 打印,不要强行引用,应说明工艺差异。坑 9:每次运行都重新加载模型
原因:脚本式运行,没有缓存。
解决:
- Streamlit:用
@st.cache_resource - 命令行:做成常驻循环,只加载一次
七、项目结构
7.1 当前结构
C:\MyProject\3DRAG\
├── rag_env\ # 虚拟环境
├── docs\ # 知识库文档
│ ├── 3D Printing Troubleshooting Guide.md
│ └── ...
├── chroma_db\ # 向量库(持久化)
├── build_vectorstore.py # 建库脚本
├── rag_chat.py # 命令行版 RAG
├── check_db.py # 检索验证脚本
├── app.py # Web 界面
├── requirements.txt # 依赖清单
└── README.md # 项目说明7.2 推荐整理后结构
C:\MyProject\3DRAG\
├── docs\ # 知识库文档
├── chroma_db\ # 向量库(可重新生成)
├── src\ # 源码
│ ├── build_vectorstore.py
│ ├── rag_chat.py
│ ├── check_db.py
│ └── app.py
├── .env # API Key(不进 Git)
├── .gitignore # 忽略 rag_env、chroma_db、.env
├── requirements.txt
└── README.md7.3 生成依赖清单
pip freeze > requirements.txt以后别人拿到项目:
pip install -r requirements.txt7.4 .gitignore 示例
rag_env/
chroma_db/
.env
__pycache__/
*.pyc
.streamlit/八、性能优化
8.1 影响 RAG 速度的因素
| 环节 | 耗时 | 优化方向 |
|---|---|---|
| 加载 embedding 模型 | 5~15 秒 | 缓存(只加载一次) |
| 问题向量化 | <1 秒 | 换小模型 |
| 向量检索 | <1 秒 | 加索引(数据量大时) |
| LLM 生成 | 5~30 秒 | 换快模型 / 限制输出长度 |
| Rerank | 1~2 秒 | 按需开启 |
8.2 LLM 速度对比
| 模型 | 速度 | 质量 |
|---|---|---|
| glm-4-flash | ⚡⚡⚡ 极快 | 好(免费) |
| deepseek-chat | ⚡⚡ 快 | 好 |
| qwen-turbo | ⚡⚡ 快 | 中 |
| gpt-4o-mini | ⚡⚡ 快 | 好 |
| deepseek-reasoner | 🐢 极慢 | 极好 |
注意:RAG 场景不要用推理模型(R1、o1 等),它们会思考很久。
8.3 关键优化清单
- [x] 用
@st.cache_resource缓存模型 - [x] 用交互式脚本避免重复加载
- [x] 设置
max_tokens=800限制输出 - [x] 设置
timeout=30防止卡死 - [x] 选速度快的 LLM(如 glm-4-flash)
- [ ] 用更小的 embedding 模型
- [ ] 加 GPU 加速(有显卡的话)
九、进阶方向
9.1 检索质量优化
| 方向 | 做法 | 收益 |
|---|---|---|
| Rerank | 召回 20 → 精排 5 | 精度 ↑10~20% |
| 混合检索 | 向量 + BM25 | 对术语/型号更准 |
| 查询改写 | LLM 改写问题为关键词 | 跨语言场景效果显著 |
| HyDE | LLM 先生成假答案再检索 | 语义匹配更准 |
| Parent-Child | 小块检索,大块喂 LLM | 兼顾精度和上下文 |
9.2 工程化
| 方向 | 说明 |
|---|---|
| 增量更新 | 只添加新文档,不重建整个库 |
| 对话记忆 | 支持多轮追问 |
| 流式输出 | 边生成边显示,体验更好 |
| API 封装 | FastAPI 提供 HTTP 接口 |
| 部署 | Streamlit Cloud / 云服务器 |
| 用户认证 | 多用户隔离 |
| 日志监控 | 记录每次问答 |
9.3 评估体系(RAGAS)
用 RAGAS 框架自动评估:
- 检索召回率:相关资料是否被召回
- 忠实度:答案是否基于资料(无幻觉)
- 相关性:答案是否切题
- 上下文精度:召回内容是否相关
9.4 学习路径
第 1 周:Python 基础 + RAG 概念
第 2 周:跑通最小 demo
第 3 周:换真实文档,理解每一步
第 4 周:优化检索(Rerank、混合检索)
第 5 周:工程化(API + 前端 + 多轮)
第 6 周:评估与调优十、学习心得
10.1 从 0 到 1 的关键节点
| 阶段 | 关键收获 |
|---|---|
| 环境搭建 | 虚拟环境、国内镜像、依赖管理 |
| 文档加载 | 多格式 Loader、编码问题 |
| 切分 | chunk_size/overlap 的权衡 |
| 向量化 | Embedding 模型选择、多语言支持 |
| 存储 | Chroma 持久化、向量维度一致性 |
| 检索 | Top-K、跨语言检索 |
| 生成 | Prompt 设计、防幻觉、引用溯源 |
| Web | Streamlit、缓存、session_state |
10.2 三条最重要的经验
1. 模型选择比 Prompt 优化更重要
- 跨语言场景,选对 embedding 模型(bge-m3)比调 Prompt 提升大
- LLM 速度差异巨大,选快模型体验天差地别
2. Prompt 要具体,不要"聪明"
- 明确要求"区分 FDM 和树脂",LLM 才能照做
- 明确要求"引用编号",LLM 才会标注来源
- 泛泛的"根据资料回答"效果不如具体指令
3. 缓存是一切体验问题的基础
- Streamlit 的
@st.cache_resource让加载时间从每次 15 秒变成只一次 - 命令行改交互式,让提问从 30 秒变成 5 秒
10.3 新手最容易踩的坑
- 环境问题:没激活虚拟环境、镜像没配
- 模块路径变化:LangChain 拆包,导入路径老变
- 编码问题:中文乱码、API Key 混入中文
- 模型选择:纯中文模型做英文文档
- 不缓存:每次重跑都重新加载模型
- Prompt 太泛:没具体要求,LLM 就随意发挥
10.4 下一步计划
- [ ] 补充 PETG 相关资料
- [ ] 加 Rerank 提升检索精度
- [ ] 加对话记忆支持追问
- [ ] 做增量更新
- [ ] 部署到云端
- [ ] 建评估集,用 RAGAS 量化效果
📚 参考资源
- LangChain 官方文档:https://python.langchain.com
- Chroma 文档:https://docs.trychroma.com
- BGE 模型:https://huggingface.co/BAAI
- Streamlit 文档:https://docs.streamlit.io
- RAGAS 评估:https://docs.ragas.io
- HuggingFace 镜像:https://hf-mirror.com
🏷️ 版本记录
| 日期 | 版本 | 说明 |
|---|---|---|
| 2026-09 | v1.0 | 完成最小 RAG、Web 界面、跨语言支持 |
总结一句话:RAG 的本质 = 检索(找资料)+ 生成(写答案)。
从零跑通一遍,就已经入门了。剩下的都是在主线上做优化。
本笔记持续更新,记录 RAG 学习的每一步。