RAG 系统开发学习笔记:从零到 Web 应用

本笔记记录了我从零开始构建一个完整 RAG(检索增强生成)系统的全过程,包括环境搭建、文档处理、向量检索、LLM 生成、跨语言处理、Web 界面等所有环节。既是学习复盘,也是后续查阅的手册。

📋 目录

  1. 项目概述
  2. 核心概念速查
  3. 环境搭建
  4. 完整开发流程
  5. 关键代码详解
  6. 踩坑记录与解决方案
  7. 项目结构
  8. 性能优化
  9. 进阶方向
  10. 学习心得

一、项目概述

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+
框架LangChainLLM 应用编排
EmbeddingBAAI/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对召回结果二次精排,提升精度
temperatureLLM 输出随机性,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-dotenv

3.3 各依赖作用

包作用
langchainLLM 应用核心框架
langchain-community第三方集成(Loader、VectorStore)
langchain-openaiOpenAI 协议对接(兼容 DeepSeek、智谱等)
langchain-text-splitters文本切分(新版独立包)
langchain-huggingfaceHuggingFace embedding 集成
langchain-chromaChroma 向量库集成
chromadb向量数据库本体
sentence-transformers本地 embedding 模型
pypdfPDF 解析
streamlitWeb 界面框架
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 / .mdTextLoader
.pdfPyPDFLoader
.docxDocx2txtLoader
.csvCSVLoader
网页 URLWebBaseLoader

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)

切分三原则:

  1. chunk_size:中文 300~800,英文 500~1000
  2. chunk_overlap:chunk_size 的 10%~20%
  3. 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.5100 MB中文纯中文,速度优先
BAAI/bge-m32.2 GB多语言跨语言检索 ✅
paraphrase-multilingual-MiniLM-L12-v2470 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.md

7.3 生成依赖清单

pip freeze > requirements.txt

以后别人拿到项目:

pip install -r requirements.txt

7.4 .gitignore 示例

rag_env/
chroma_db/
.env
__pycache__/
*.pyc
.streamlit/

八、性能优化

8.1 影响 RAG 速度的因素

环节耗时优化方向
加载 embedding 模型5~15 秒缓存(只加载一次)
问题向量化<1 秒换小模型
向量检索<1 秒加索引(数据量大时)
LLM 生成5~30 秒换快模型 / 限制输出长度
Rerank1~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 改写问题为关键词跨语言场景效果显著
HyDELLM 先生成假答案再检索语义匹配更准
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 设计、防幻觉、引用溯源
WebStreamlit、缓存、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 新手最容易踩的坑

  1. 环境问题:没激活虚拟环境、镜像没配
  2. 模块路径变化:LangChain 拆包,导入路径老变
  3. 编码问题:中文乱码、API Key 混入中文
  4. 模型选择:纯中文模型做英文文档
  5. 不缓存:每次重跑都重新加载模型
  6. Prompt 太泛:没具体要求,LLM 就随意发挥

10.4 下一步计划

  • [ ] 补充 PETG 相关资料
  • [ ] 加 Rerank 提升检索精度
  • [ ] 加对话记忆支持追问
  • [ ] 做增量更新
  • [ ] 部署到云端
  • [ ] 建评估集,用 RAGAS 量化效果

📚 参考资源


🏷️ 版本记录

日期版本说明
2026-09v1.0完成最小 RAG、Web 界面、跨语言支持

总结一句话:RAG 的本质 = 检索(找资料)+ 生成(写答案)。
从零跑通一遍,就已经入门了。剩下的都是在主线上做优化。

本笔记持续更新,记录 RAG 学习的每一步。

最后修改:2026 年 09 月 20 日
如果觉得我的文章对你有用,请随意赞赏