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

  1. 前往 platform.deepseek.com 注册并获取 API Key
  2. 在项目根目录创建 .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 代表一页 PDF
  • page_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_urlapi_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 contextlib2to3 是 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 核心收获

  1. RAG 的本质是"先查资料,再回答"——将 LLM 不知道的私有知识先检索出来,注入 Prompt,让 LLM 基于真实资料生成回答。

  2. Embedding 模型和 LLM 是分工的——Embedding 负责"翻译"(文本→向量),LLM 负责"理解+生成"(向量→回答)。两者各司其职,不能互相替代。

  3. Streamlit 的执行模型是"全量重跑"——每次交互整个脚本重新执行,st.session_state 是唯一的跨执行记忆。理解这个模型是写好 Streamlit 的关键。

  4. Prompt 设计是 RAG 系统中最被低估的环节——好的 Prompt 能显著提升回答质量,约束条件(“不要编造”)能有效抑制 LLM 幻觉。

10.2 后续优化方向

方向 说明
多轮对话 目前每次提问独立,加入 chat_history 实现上下文追问
代码重构 提取公共模块,消除 rag_qa.pyapp.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 的开发者。

Logo

欢迎加入DeepSeek 技术社区。在这里,你可以找到志同道合的朋友,共同探索AI技术的奥秘。

更多推荐