物流行业智能问答 RAG 系统实现全记录
文章目录
1. 项目背景
1.1 什么是 RAG?
RAG(Retrieval-Augmented Generation,检索增强生成)是一种让大语言模型(LLM)能够回答私有知识的技术方案。它的核心思想是:先检索,再回答。
❌ 直接问 LLM(无 RAG):
用户:"我的货物 ABC123456 在哪里?"
LLM:"抱歉,我无法获取实时物流信息。" ← 没有你的内部数据
✅ RAG 方式:
用户:"我的货物 ABC123456 在哪里?"
系统:① 从知识库检索相关文档 → ② 塞进 Prompt → ③ LLM 基于文档回答
LLM:"货物 ABC123456 当前位于上海分拨中心。" ← 基于真实数据
1.2 项目目标
构建一个物流行业智能问答系统,用户可以用自然语言提问物流相关问题(货物追踪、仓储信息、运输状态等),系统从私有物流文档中检索相关信息,并由大模型生成准确回答。
1.3 项目目录结构
Logistics_Consult_RAG_Platform/
├── data/
│ └── 物流信息.pdf ← 私有知识库(物流数据)
├── doc/
│ └── 物流行业智能问答RAG系统.pdf ← 参考文档
├── faiss/
│ └── wuliu/
│ ├── index.faiss ← 向量索引文件
│ └── index.pkl ← 文档元数据文件
├── .env ← DeepSeek API Key(不提交到 Git)
├── bulid_vector_db.py ← 离线脚本:PDF → 向量知识库
├── test_retrieval.py ← 测试脚本:验证检索效果
├── rag_qa.py ← 后端:RAG 问答核心逻辑
├── app.py ← 前端:Streamlit Web 聊天界面
└── pyproject.toml ← 项目配置
2. 技术选型
2.1 整体技术栈
| 组件 | 选型 | 说明 |
|---|---|---|
| LLM(大语言模型) | DeepSeek(云端 API) | 性价比高,中文能力顶级,兼容 OpenAI SDK |
| Embedding(向量化模型) | BAAI/bge-small-zh-v1.5 | 开源中文 Embedding 模型,CPU 可运行,512 维向量 |
| 向量数据库 | FAISS(Facebook AI Similarity Search) | 本地存储,毫秒级检索,无需外部服务 |
| 文档加载 | PyMuPDFLoader | 基于 PyMuPDF 库,支持中文 PDF 解析 |
| 文本分割 | RecursiveCharacterTextSplitter | 递归式分割,优先按语义边界切分 |
| RAG 框架 | LangChain | 编排整个 RAG 流程 |
| 前端 | Streamlit | 纯 Python 写 Web 界面,无需 HTML/CSS/JS |
| 环境管理 | Python 3.12 + venv | 虚拟环境隔离依赖 |
2.2 为什么选这些组件?
为什么用 DeepSeek 而不是本地模型?
- 本地部署大模型需要 GPU 资源(至少 8GB 显存),开发和运维成本高
- DeepSeek API 按 token 计费,价格低廉(约 ¥1/百万 token),且无需维护基础设施
- 兼容 OpenAI SDK,一行代码切换,迁移成本几乎为零
为什么用 BGE 本地 Embedding 而不是云端 API?
- Embedding 模型轻量(BGE-small 约 100MB),CPU 即可推理,无需 GPU
- 本地运行无网络延迟,无 API 费用
- 数据不出本地,满足隐私需求
为什么用 FAISS 而不是 Elasticsearch / Milvus?
- 数据量小时(几百到几千条),FAISS 最简单——无需安装数据库服务,直接读写文件
- 向量检索性能优秀,支持多种索引类型
- 与 LangChain 深度集成,两行代码完成存储和查询
为什么用 Streamlit 而不是 FastAPI + Vue?
- 本项目是内部工具 / Demo,不需要复杂的前端工程化
- Streamlit 用纯 Python 写界面,函数调用即前后端通信,省去 API 设计、路由、JSON 序列化
- 从零到上线只需一个文件,非常适合原型验证
3. 环境搭建
3.1 安装依赖
# 创建虚拟环境
python -m venv .venv
# 激活虚拟环境(Windows)
.venv\Scripts\activate
# 安装依赖包
pip install langchain langchain-community pymupdf faiss-cpu \
sentence-transformers openai streamlit python-dotenv
3.2 各包职责
| 包名 | 职责 |
|---|---|
langchain |
RAG 核心框架,提供 Chain、PromptTemplate 等抽象 |
langchain-community |
社区集成,包含 FAISS、PyMuPDFLoader、HuggingFaceEmbeddings 等 |
pymupdf |
PDF 解析引擎,提取文本 |
faiss-cpu |
向量存储和相似度搜索 |
sentence-transformers |
运行 BGE 等 Embedding 模型 |
openai |
调用 DeepSeek API(兼容 OpenAI SDK) |
streamlit |
Web 聊天界面 |
python-dotenv |
从 .env 文件加载环境变量 |
3.3 配置 DeepSeek API Key
- 前往 platform.deepseek.com 注册并获取 API Key
- 在项目根目录创建
.env文件:
DEEPSEEK_API_KEY=sk-xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
⚠️
.env应加入.gitignore,避免 API Key 泄露到版本控制。
4. Step 1:文档处理与向量化
文件:bulid_vector_db.py
这一步是整个 RAG 系统的基础——将 PDF 文档转化为可被机器检索的向量知识库。
4.1 整体流程
PDF 文件 → 加载文本 → 文本分割 → 向量化 → 存入 FAISS
4.2 完整代码
from langchain_community.document_loaders import PyMuPDFLoader
from langchain_community.embeddings import HuggingFaceEmbeddings
from langchain_text_splitters import RecursiveCharacterTextSplitter
from langchain_community.vectorstores import FAISS
# 1. 加载 PDF
loader = PyMuPDFLoader("data/物流信息.pdf")
docs = loader.load()
print(f"加载到的文档数量: {len(docs)}")
# 2. 文本分割
text_splitter = RecursiveCharacterTextSplitter(
chunk_size=200,
chunk_overlap=20,
separators=["\n\n", "\n", "。", "!", "?", ";", ",", " ", ""]
)
split_docs = text_splitter.split_documents(docs)
print(f"切分后的文档块数量: {len(split_docs)}")
# 3. 向量化
embedding = HuggingFaceEmbeddings(
model_name="BAAI/bge-small-zh-v1.5",
model_kwargs={"device": "cpu"},
encode_kwargs={"normalize_embeddings": True}
)
# 4. 存入 FAISS
db = FAISS.from_documents(split_docs, embedding)
db.save_local("faiss/wuliu")
4.3 关键概念详解
4.3.1 PyMuPDFLoader:PDF 文本提取
loader = PyMuPDFLoader("data/物流信息.pdf")
docs = loader.load()
# docs 的结构:
# [
# Document(
# page_content="物流公司:速达物流\n货物追踪:\n货物编号:ABC123456\n...",
# metadata={"source": "data/物流信息.pdf", "page": 0}
# )
# ]
load()返回一个list[Document],每个Document代表一页 PDFpage_content是该页的纯文本内容metadata包含来源文件路径和页码
4.3.2 RecursiveCharacterTextSplitter:递归式文本分割
这是 RAG 质量的关键环节。分割太粗,检索不精准;分割太细,上下文丢失。
RecursiveCharacterTextSplitter(
chunk_size=200, # 每个文本块的最大字符数
chunk_overlap=20, # 相邻块之间的重叠字符数
separators=[ # 分割符优先级(从高到低)
"\n\n", # 1. 先按段落切
"\n", # 2. 再按换行切
"。", # 3. 按句号切
"!", # 4. 按感叹号切
"?", # 5. 按问号切
";", # 6. 按分号切
",", # 7. 按逗号切
" ", # 8. 按空格切
"" # 9. 最后才按字符硬切
]
)
参数理解:
| 参数 | 含义 | 调优建议 |
|---|---|---|
chunk_size |
每个文本块的最大字符数 | 太小丢失上下文,太大检索不精准。一般 300~800 |
chunk_overlap |
相邻块重叠的字符数 | 防止关键信息刚好落在边界上。通常为 chunk_size 的 10%~20% |
separators |
分割符优先级列表 | 中文文档务必加入 。!?;, 等中文标点 |
分割示意:
原文:"第一段内容。第二段内容。第三段内容。"
chunk_size=200, chunk_overlap=20
chunk_1: "第一段内容。第二段..." ← 0~200 字符
chunk_2: "第一段...第二段内容。第三段..." ← 180~380 字符(与 chunk_1 重叠 20 字符)
chunk_3: "...第三段内容。" ← 360~末尾
本项目数据量小的情况:
本项目 物流信息.pdf 只有 208 个字符,设置 chunk_size=200 后只切出 2 个块。对于这种极小数据量,chunk_size 设大一点(比如 500)甚至只保留一个整块也是可行的。但在实际生产环境中,文档会更多更大,保留合理的分割参数即可。
4.3.3 HuggingFaceEmbeddings:文本向量化
embedding = HuggingFaceEmbeddings(
model_name="BAAI/bge-small-zh-v1.5",
model_kwargs={"device": "cpu"},
encode_kwargs={"normalize_embeddings": True}
)
| 参数 | 含义 |
|---|---|
model_name |
HuggingFace 上的模型名称。BGE-small-zh 是 BAAI 出品的中文 Embedding 模型,轻量(约 100MB),输出 512 维向量 |
model_kwargs={"device": "cpu"} |
传给 SentenceTransformer 的初始化参数。"cpu" 表示 CPU 推理 |
encode_kwargs={"normalize_embeddings": True} |
传给 model.encode() 的参数。归一化后向量长度为 1,计算相似度时直接做内积即可 |
什么是 Embedding(向量化)?
文本无法直接被计算机比较相似度,需要先转成数字向量。语义相近的文本,向量在空间中距离也相近:
"货物在哪里" → [0.1, 0.8, 0.3, ...]
"物流信息" → [0.1, 0.7, 0.4, ...] ← 与上面接近(都是物流相关)
"今天天气" → [0.9, 0.1, 0.7, ...] ← 与上面很远(完全不同的话题)
为什么需要归一化?
未归一化向量: [3, 4] → 向量长度 = 5
归一化后向量: [0.6, 0.8] → 向量长度 = 1
归一化后,两个向量的相似度 = 内积 = cos(夹角),计算更快更准。
4.3.4 FAISS:向量存储与检索
db = FAISS.from_documents(split_docs, embedding)
db.save_local("faiss/wuliu")
内部发生了什么?
for each doc in split_docs:
vector = embedding.embed_query(doc.page_content) # 文本 → 512维向量
faiss_index.add(vector) # 向量存入索引
metadata_store[id] = doc # 原文存入 pkl
生成的文件:
| 文件 | 内容 | 大小 |
|---|---|---|
index.faiss |
FAISS 向量索引,存储所有 chunk 的向量数据 | 4.1 KB |
index.pkl |
Python pickle 文件,存储原文和元数据 | 1.4 KB |
5. Step 2:检索验证
文件:test_retrieval.py
在接入 LLM 之前,先验证检索效果——确保能搜到正确的文档,否则后续问答一定是错的。
5.1 完整代码
from langchain_community.embeddings import HuggingFaceEmbeddings
from langchain_community.vectorstores import FAISS
# 加载向量库
embedding = HuggingFaceEmbeddings(
model_name="BAAI/bge-small-zh-v1.5",
model_kwargs={"device": "cpu"},
encode_kwargs={"normalize_embeddings": True}
)
db = FAISS.load_local("faiss/wuliu", embedding, allow_dangerous_deserialization=True)
# 测试检索
question = "货物ABC123456在哪里?"
docs = db.similarity_search(question, k=2)
for i, doc in enumerate(docs):
print(f"--- 相关文档 {i+1} ---")
print(doc.page_content)
print()
5.2 关键概念详解
5.2.1 FAISS.load_local:加载向量库
db = FAISS.load_local("faiss/wuliu", embedding, allow_dangerous_deserialization=True)
| 参数 | 含义 |
|---|---|
"faiss/wuliu" |
之前 save_local 的目录路径 |
embedding |
必须和创建时使用同一个模型。否则向量空间不匹配,检索结果错乱 |
allow_dangerous_deserialization=True |
安全确认。index.pkl 是 pickle 文件,设为 True 表示你信任这个文件来源 |
5.2.2 similarity_search:语义搜索
docs = db.similarity_search("货物ABC123456在哪里?", k=2)
| 参数 | 含义 |
|---|---|
"货物ABC123456在哪里?" |
查询文本。FAISS 内部会先用 embedding 模型把它转成向量 |
k=2 |
返回最相似的 top-k 个文档 |
内部流程:
1. query_vector = embedding.embed_query("货物ABC123456在哪里?")
2. distances, indices = faiss_index.search(query_vector, k=2)
3. 根据 indices 从 index.pkl 取出对应的 Document 原文
4. 返回 list[Document]
5.3 检索结果
--- 相关文档 1 ---
物流公司:速达物流 公司总部:北京市 业务范围:国际快递、仓储管理
货物追踪:
货物编号:ABC123456
发货日期:2023-01-15
当前位置:上海分拨中心
预计到达日期:2023-01-20
运输方式:
运输公司:快运通
运输方式:陆运
出发地:广州
目的地:重庆
预计运输时间:3天
仓储信息:
仓库名称:东方仓储中心
仓库位置:深圳市
存储货物类型:电子产品
存储条件:常温仓储
--- 相关文档 2 ---
存储条件:常温仓储
当前库存量:1000件
结果分析:
- 文档 1 包含了查询目标的核心信息(货物编号 ABC123456、当前位置上海分拨中心),检索命中 ✅
- 文档 2 是文档尾部的碎片,独立价值不高。这是因为文档只有 208 字符却被切成 2 块,尾部碎片缺乏完整语义。对于小文档,可以适当增大 chunk_size 或直接按 1 个整块处理
6. Step 3:RAG 问答接入 DeepSeek
文件:rag_qa.py
将检索模块与 LLM 串联,实现真正的"检索增强生成"。
6.1 整体流程
用户提问 → FAISS 检索相关文档 → 拼接 Prompt → DeepSeek 生成回答
6.2 完整代码
import os
from dotenv import load_dotenv
from langchain_community.embeddings import HuggingFaceEmbeddings
from langchain_community.vectorstores import FAISS
from openai import OpenAI
# 1. 加载环境变量
load_dotenv()
# 2. 加载向量库
embedding = HuggingFaceEmbeddings(
model_name="BAAI/bge-small-zh-v1.5",
model_kwargs={"device": "cpu"},
encode_kwargs={"normalize_embeddings": True}
)
db = FAISS.load_local("faiss/wuliu", embedding, allow_dangerous_deserialization=True)
# 3. 初始化 DeepSeek
client = OpenAI(
api_key=os.getenv("DEEPSEEK_API_KEY"),
base_url="https://api.deepseek.com",
)
# 4. Prompt 模板
PROMPT = """你是一个物流行业智能助手。请根据以下参考资料回答用户问题。
参考资料
{context}
用户问题
{question}
## 回答要求
1. 如果参考资料中有答案,直接引用
2. 如果参考资料信息不足,如实说明
3. 回答简洁准确,不要编造"""
# 5. RAG 问答函数
def rag_qa(question):
# 5.1 检索:用向量库检索最相关的文档
docs = db.similarity_search(question, k=2)
context = "\n".join([doc.page_content for doc in docs])
# 5.2 拼接 prompt:把检索结果和问题填入模板
full_prompt = PROMPT.format(context=context, question=question)
# 5.3 调用 LLM 生成回答
response = client.chat.completions.create(
model="deepseek-chat",
messages=[{"role": "user", "content": full_prompt}]
)
return response.choices[0].message.content
# 6. 测试入口
if __name__ == "__main__":
questions = [
"货物ABC123456在哪里?",
"仓库里存了什么?",
"从广州到重庆的运输需要几天?"
]
for q in questions:
print(f"问:{q}")
print(f"答:{rag_qa(q)}")
print("-" * 50)
6.3 关键概念详解
6.3.1 DeepSeek API 初始化
client = OpenAI(
api_key=os.getenv("DEEPSEEK_API_KEY"),
base_url="https://api.deepseek.com",
)
DeepSeek 兼容 OpenAI 的 SDK 接口格式,所以直接使用 openai 库,只需修改 base_url 和 api_key。这种兼容性意味着:
- 如果将来想切换到其他兼容 OpenAI 的模型(如通义千问、GLM),只需改 URL 和 Key
- 代码逻辑完全不变,零迁移成本
6.3.2 Prompt 模板设计
PROMPT = """你是一个物流行业智能助手。请根据以下参考资料回答用户问题。
参考资料
{context}
用户问题
{question}
## 回答要求
1. 如果参考资料中有答案,直接引用
2. 如果参考资料信息不足,如实说明
3. 回答简洁准确,不要编造"""
Prompt 是 RAG 系统中最重要的"软代码"。三个关键设计原则:
| 原则 | 体现 |
|---|---|
| 角色设定 | “你是一个物流行业智能助手” —— 限定 LLM 行为范围 |
| 资料注入 | {context} —— 检索结果填在这里,是 LLM 回答的知识来源 |
| 约束条件 | “如实说明、不要编造” —— 防止 LLM 幻觉(Hallucination) |
6.3.3 rag_qa 函数:RAG 核心逻辑
def rag_qa(question):
# 检索
docs = db.similarity_search(question, k=2)
context = "\n".join([doc.page_content for doc in docs])
# 拼接
full_prompt = PROMPT.format(context=context, question=question)
# 生成
response = client.chat.completions.create(
model="deepseek-chat",
messages=[{"role": "user", "content": full_prompt}]
)
return response.choices[0].message.content
完整数据流:
question = "货物ABC123456在哪里?"
Step 1: 检索
→ db.similarity_search("货物ABC123456在哪里?", k=2)
→ 返回 2 个相关 Document
→ context = "物流公司:速达物流...当前位置:上海分拨中心...\n存储条件:常温仓储..."
Step 2: 拼接
→ PROMPT.format(context=context, question=question)
→ 最终 prompt 发给 DeepSeek
Step 3: 生成
→ DeepSeek 返回:"货物ABC123456当前位于上海分拨中心。"
6.4 测试结果
问:货物ABC123456在哪里?
答:货物ABC123456当前位于上海分拨中心。
问:仓库里存了什么?
答:仓库存储的货物类型是电子产品。
问:从广州到重庆的运输需要几天?
答:从广州到重庆的陆运预计运输时间为3天。
三个问题全部正确回答,RAG 流程跑通 ✅
7. Step 4:Streamlit Web 界面
文件:app.py
7.1 Streamlit 的核心理念
Streamlit 与传统 Web 开发有本质区别:
| 对比维度 | 传统 Web(Flask + Vue) | Streamlit |
|---|---|---|
| 前端代码 | HTML + CSS + JavaScript | 纯 Python |
| 前后端通信 | HTTP 请求 → JSON 响应 | 函数调用 rag_qa(prompt) |
| 状态管理 | Vuex / Redux / Pinia | st.session_state |
| 事件处理 | onClick / onChange | Python if 语句 |
| 部署 | Nginx + uWSGI + 前端构建 | streamlit run 一条命令 |
Streamlit 的核心执行模型:每次用户交互,整个脚本从头到尾重新执行。 这个模型很像 Jupyter Notebook 的 “Run All”,每次操作都触发一次完整的重新渲染。
7.2 完整代码
import streamlit as st
from rag_qa import rag_qa
# 页面配置
st.set_page_config(page_title="物流智能问答", layout="wide")
st.title("🚚 物流行业智能问答系统")
# 初始化聊天历史
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"])
# 接收用户输入
if prompt := st.chat_input("请输入你的物流问题..."):
# 显示用户消息
st.session_state.messages.append({"role": "user", "content": prompt})
with st.chat_message("user"):
st.markdown(prompt)
# 调用 RAG 生成回答
with st.chat_message("assistant"):
with st.spinner("思考中..."):
answer = rag_qa(prompt)
st.markdown(answer)
st.session_state.messages.append({"role": "assistant", "content": answer})
7.3 逐行解析
7.3.1 页面配置
st.set_page_config(page_title="物流智能问答", layout="wide")
st.title("🚚 物流行业智能问答系统")
st.set_page_config():设置浏览器标签页标题和页面布局st.title():页面顶部大标题,相当于 HTML<h1>
7.3.2 前后端连接——只需一行 import
from rag_qa import rag_qa
这就是前后端的全部连接! 没有 API 路由,没有 JSON 序列化,没有 HTTP 请求。rag_qa 是一个普通的 Python 函数,输入问题字符串,输出答案字符串。Streamlit 中函数调用即前后端通信。
7.3.3 st.session_state:跨重执行的记忆
if "messages" not in st.session_state:
st.session_state.messages = []
这是 Streamlit 中最重要的概念。由于每次交互脚本都会重新执行,普通变量会被重置:
# ❌ 错误做法
messages = [] # 每次交互都重置为空列表!
# ✅ 正确做法
if "messages" not in st.session_state:
st.session_state.messages = [] # 只在第一次初始化
st.session_state 是一个在多次脚本执行之间持久化的字典。你可以把它理解为 session 级别的缓存。
7.3.4 渲染聊天历史
for msg in st.session_state.messages:
with st.chat_message(msg["role"]):
st.markdown(msg["content"])
st.chat_message("user"):渲染用户头像气泡st.chat_message("assistant"):渲染机器人头像气泡st.markdown():在气泡内渲染 Markdown 内容with:Python 上下文管理器,表示"在这个气泡里放内容"
7.3.5 聊天输入与事件驱动
if prompt := st.chat_input("请输入你的物流问题..."):
:= 是 Python 3.8 引入的海象运算符(walrus operator),等价于:
prompt = st.chat_input("请输入你的物流问题...")
if prompt: # 用户输入了内容(非空字符串)
# 执行后续逻辑
Streamlit 的事件驱动模型:不是通过回调函数,而是通过 if 判断。 st.chat_input() 在用户未输入时返回空字符串,if 条件不成立,后续代码跳过。用户输入后,prompt 有值,触发后续逻辑。
7.3.6 调用后端并显示回答
with st.chat_message("assistant"):
with st.spinner("思考中..."):
answer = rag_qa(prompt)
st.markdown(answer)
| 组件 | 作用 |
|---|---|
st.chat_message("assistant") |
创建机器人聊天气泡 |
st.spinner("思考中...") |
显示转圈加载动画,提升用户体验 |
rag_qa(prompt) |
调用后端 RAG 函数,等待检索 + LLM 生成 |
st.markdown(answer) |
在气泡里显示回答 |
7.4 完整执行流程
用户打开页面 http://localhost:8501
│
│ app.py 从头执行
│ 第 11 行:初始化 messages = []
│ 第 15 行:渲染历史消息(此时为空,无输出)
│ 第 20 行:显示 st.chat_input() 输入框,等待用户输入
│
└─── 用户输入 "货物ABC123456在哪里?" 并按回车
│
│ app.py 从头执行!(核心理念)
│ 第 11 行:messages 已存在,跳过初始化
│ 第 15 行:渲染之前的聊天历史
│ 第 20 行:prompt = "货物ABC123456在哪里?",进入 if
│ 第 22 行:用户消息存入 messages
│ 第 23 行:渲染用户气泡
│ 第 27 行:渲染机器人气泡 + 转圈动画
│ 第 29 行:rag_qa(prompt) 执行
│ → 检索 FAISS 相关文档
│ → 拼接 Prompt
│ → 调用 DeepSeek API
│ → 返回答案
│ 第 30 行:显示答案
│ 第 31 行:答案存入 messages
│
└─── 等待下一次输入...
7.5 运行方式
streamlit run app.py
浏览器访问 http://localhost:8501 即可使用。
8. 系统全景图
8.1 运行时架构
┌─────────────────────────────────────────────────────────┐
│ app.py (Streamlit) │
│ Web 聊天界面 │
│ │ │
│ rag_qa(question) │
│ │ │
│ ┌───────────────┼───────────────┐ │
│ │ │ │ │
│ ┌────▼────┐ ┌─────▼─────┐ ┌────▼────┐ │
│ │ FAISS │ │ BGE 模型 │ │DeepSeek │ │
│ │ 向量检索 │◄───│ 向量化 │ │ API │ │
│ │ (本地) │ │ (本地CPU) │ │ (云端) │ │
│ └────┬────┘ └──────────┘ └────┬────┘ │
│ │ │ │
│ │ ① 用户问题向量化 │ │
│ │ ② 在 FAISS 中找相似文档 │ │
│ │ ③ 取回原文 │ │
│ │ │ │
│ └───────► 原文 + 问题 ──────────┘ │
│ ④ 拼接 Prompt │
│ ⑤ 调用 DeepSeek 生成回答 │
│ ⑥ 返回给用户 │
└─────────────────────────────────────────────────────────┘
8.2 一次问答的完整数据流
用户输入:"货物ABC123456在哪里?"
Step 1 — 检索(本地)
BGE 模型将问题向量化 → [0.02, -0.43, 0.89, ..., 0.33](512 维)
FAISS 在向量空间中搜索最接近的向量
返回匹配的原文 Document
Step 2 — 拼接 Prompt
context = 检索到的原文
question = 用户问题
填入 PROMPT 模板,得到完整的 prompt 字符串
Step 3 — 生成(云端)
将 prompt 发送到 DeepSeek API
DeepSeek 基于参考资料生成答案
返回:"货物ABC123456当前位于上海分拨中心。"
Step 4 — 渲染(浏览器)
Streamlit 在聊天界面显示回答
回答存入 st.session_state.messages 用于后续显示
8.3 三组件分工
| 组件 | 运行位置 | 做什么 | 不做什么 |
|---|---|---|---|
| BGE 模型 | 本地 CPU | 把文本变成数字向量 | 不理解文本含义,只做"翻译" |
| FAISS | 本地磁盘 | 存储向量 + 快速找相似 | 不生成回答,只做"配对" |
| DeepSeek | 云端 API | 理解问题,基于资料生成回答 | 不知道你的私有数据 |
一句话总结:FAISS 是"索引",BGE 是"翻译",DeepSeek 是"大脑"。
9. 踩坑记录
9.1 拼写错误导致模型下载失败
错误信息:
RepositoryNotFoundError: 401 Client Error.
Repository Not Found for url: .../BAAI/bge-samll-zh-v1.5/...
原因: 模型名中 small 拼成了 samll。
解决: BAAI/bge-small-zh-v1.5(注意是 small 不是 samll)。
教训: HuggingFace 模型名区分大小写,务必精确复制模型页面上的名称。
9.2 IDE 自动补全混入无关 import
错误信息:
DeprecationWarning: lib2to3 package is deprecated
from lib2to3.fixes.fix_input import context
原因: IDE 自动补全错误地将 fix_input 识别为 context 的来源,混入了 from lib2to3.fixes.fix_input import context。lib2to3 是 Python 2 到 3 的迁移工具,与项目毫无关系。
解决: 删除该行。IDE 的自动补全不一定正确,每次 import 都要看清来源。
教训: 不要在代码中保留你不理解的 import。
9.3 从另一个文件 import 导致代码被执行
错误现象: 运行 rag_qa.py 时,终端输出了 test_retrieval.py 的检索结果。
原因: rag_qa.py 中写了 from test_retrieval import question。Python 的 import 会执行被导入文件的全部顶层代码。
解决: 删除该 import。如果确实需要共享变量,应该提取到公共模块中。
教训: import 不只导入符号,还会执行整个文件。不要在一个脚本中 import 另一个脚本的顶层变量,应该把公共逻辑提取到独立的工具模块。
9.4 FAISS 传参变量混淆
错误现象: FAISS.from_documents(docs, embedding) 报错。
原因: 传入了原始的 docs(未切分的文档),应该传入 split_docs(切分后的文档块)。
解决: FAISS.from_documents(split_docs, embedding)。
教训: 注意变量名的语义——docs 是加载后的原始文档,split_docs 是切分后的文档块。给变量起有意义的名字可以避免这类错误。
9.5 保存路径拼写错误
错误现象: 向量库保存后找不到。
原因: db.save_local("faiss/wuli") 少了最后一个字母 u,应该是 "faiss/wuliu"。
解决: 修正路径拼写。
教训: 文件名和路径名建议直接复制粘贴,避免手打拼错。
10. 总结与展望
10.1 核心收获
-
RAG 的本质是"先查资料,再回答"——将 LLM 不知道的私有知识先检索出来,注入 Prompt,让 LLM 基于真实资料生成回答。
-
Embedding 模型和 LLM 是分工的——Embedding 负责"翻译"(文本→向量),LLM 负责"理解+生成"(向量→回答)。两者各司其职,不能互相替代。
-
Streamlit 的执行模型是"全量重跑"——每次交互整个脚本重新执行,
st.session_state是唯一的跨执行记忆。理解这个模型是写好 Streamlit 的关键。 -
Prompt 设计是 RAG 系统中最被低估的环节——好的 Prompt 能显著提升回答质量,约束条件(“不要编造”)能有效抑制 LLM 幻觉。
10.2 后续优化方向
| 方向 | 说明 |
|---|---|
| 多轮对话 | 目前每次提问独立,加入 chat_history 实现上下文追问 |
| 代码重构 | 提取公共模块,消除 rag_qa.py 和 app.py 的重复代码 |
| 性能优化 | 用 @st.cache_resource 缓存模型加载,避免每次交互重新加载 |
| 更多数据源 | 支持更多 PDF 文件、CSV、数据库等数据源 |
| 云端 Embedding | 将 BGE 本地模型替换为 DeepSeek Embedding API,彻底消除本地模型依赖 |
| 检索优化 | 引入 MMR(最大边际相关性)检索、重排序(Rerank)等策略提升检索质量 |
| 生产部署 | 配置 Nginx 反向代理、添加认证、Docker 化 |
10.3 技术栈总结
RAG 框架: LangChain
向量数据库: FAISS
Embedding: BAAI/bge-small-zh-v1.5(本地 CPU)
LLM: DeepSeek API(云端)
前端: Streamlit
PDF 解析: PyMuPDF
语言: Python 3.12
本文记录了从零到一搭建物流行业智能问答 RAG 系统的完整过程,包括环境搭建、文档向量化、检索验证、LLM 问答、Web 界面等所有环节。希望能帮助到同样在探索 RAG 的开发者。
更多推荐



所有评论(0)