1. 项目概述
1.1 项目简介
本项目是一个基于 RAG (Retrieval-Augmented Generation) 技术的智能问答助手应用。通过上传文档构建知识库,用户可以基于文档内容进行提问,系统会检索相关文档片段并结合大语言模型生成准确回答。
1.2 核心功能
| 功能模块 | 功能描述 |
|---|---|
| 文件上传 | 支持多种文档格式上传(PDF、TXT、DOCX、DOC、MD) |
| 文档分块 | 将文档切分为合理大小的片段,便于向量检索 |
| 向量存储 | 使用 Chroma 向量数据库存储文档嵌入向量 |
| 混合检索 | 结合向量检索和 BM25 关键词检索,提高召回率 |
| 重排精排 | 使用 CrossEncoder 对候选文档进行精排,提升相关性 |
| 智能问答 | 基于检索到的文档内容,通过大语言模型生成回答 |
1.3 项目结构
rag-rerank-retriever-app/
├── app.py # 应用入口
├── requirements.txt # 依赖列表
├── .env # 环境配置文件
├── config/ # 配置模块
│ ├── prompt_data.py # 提示词模板配置
│ ├── rerank_search.py # 混合检索与重排逻辑
│ ├── split_documents.py # 文档分块处理
│ └── vector_db_config.py # 向量数据库配置
└── view/ # 视图模块
├── index.py # 主页面逻辑
└── sidebar.py # 侧边栏组件
2. 技术架构
2.1 整体架构
┌─────────────────────────────────────────────────────────────────┐
│ 用户界面 (Streamlit) │
│ ┌─────────────┐ ┌──────────────────────────────────────────┐ │
│ │ Sidebar │ │ Main Chat Area │ │
│ │ 文件上传 │ │ 用户提问 → 流式回答展示 │ │
│ │ 参数配置 │ │ │ │
│ └─────────────┘ └──────────────────────────────────────────┘ │
└──────────────────────────────┬──────────────────────────────────┘
│
┌──────────────────────────────▼──────────────────────────────────┐
│ 业务逻辑层 (LangChain) │
│ ┌──────────────┐ ┌──────────────┐ ┌──────────────────────┐ │
│ │ DocSplitter │→│ VectorDBConfig│→│ Hybrid Search │ │
│ │ 文档分块处理 │ │ 向量数据库构建 │ │ 向量+BM25混合检索 │ │
│ └──────────────┘ └──────────────┘ └───────────┬────────────┘ │
│ │ │
│ ┌──────────▼────────────┐ │
│ │ Rerank Search │ │
│ │ CrossEncoder精排 │ │
│ └───────────┬────────────┘ │
│ │ │
│ ┌──────────▼────────────┐ │
│ │ Prompt + LLM │ │
│ │ 提示词组装+模型调用 │ │
│ └──────────────────────┘ │
└──────────────────────────────┬──────────────────────────────────┘
│
┌──────────────────────────────▼──────────────────────────────────┐
│ 数据存储层 │
│ ┌──────────────────────────────────────────────────────────┐ │
│ │ Chroma Vector DB │ │
│ │ 文档向量存储 / 相似度检索 / 文档检索 │ │
│ └──────────────────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────────────────┘
2.2 核心技术栈
| 技术分类 | 技术名称 | 版本要求 | 用途说明 |
|---|---|---|---|
| 前端框架 | Streamlit | >=1.37.0 | 快速构建交互式 Web 应用 |
| 大语言模型 | LangChain | >=1.3.0 | LLM 应用开发框架 |
| 向量数据库 | Chroma | >=0.3.21 | 轻量级向量存储与检索 |
| 嵌入模型 | HuggingFace Embeddings | - | 文本向量化(text2vec-base-chinese) |
| 重排模型 | SentenceTransformers | - | CrossEncoder 精排(bge-reranker-v2-m3) |
| 分词工具 | jieba3 | - | 中文分词,用于 BM25 检索 |
| 关键词检索 | rank_bm25 | - | BM25 算法实现 |
3. 模块详细说明
3.1 应用入口 - app.py
职责: 应用启动入口,初始化 Streamlit 应用。
# 核心逻辑
from view.index import get_streamlit_app
get_streamlit_app() # 调用主页面函数
问题: 调用 get_streamlit_app() 时未传递参数,但 index.py 中定义的函数签名为 get_streamlit_app(model, api_key),存在参数不匹配问题。
3.2 主页面 - view/index.py
职责: Streamlit 主页面逻辑,包含文档处理、聊天交互、检索与问答流程。
核心流程:
- 页面配置: 设置页面标题、图标和布局
- 侧边栏数据: 获取上传文件和配置参数
- 文档处理:
- 计算文件哈希值作为缓存键
- 使用
DocSplitter进行文档分块 - 使用
VectorDBConfig构建向量数据库
- 聊天界面: 维护消息历史,展示对话
- 检索与回答:
- 调用
hybrid_search获取候选文档 - 调用
rerank_search进行精排 - 组装 Prompt,调用 LLM 生成回答
- 调用
关键代码片段:
# 混合检索 + 重排流程
candidates = hybrid_search(question, vectordb, k=k_value * 2)
final_results = rerank_search(question, candidates, top_k=k_value)
contexts = "\n\n".join(final_results)
3.3 侧边栏 - view/sidebar.py
职责: 提供文件上传和参数配置界面。
配置参数:
| 参数名称 | 类型 | 默认值 | 范围 | 说明 |
|---|---|---|---|---|
| chunk_size | int | 300 | 100-1000 | 每个文档片段的最大字符数 |
| chunk_overlap | int | 50 | 0-200 | 相邻片段的重叠字符数 |
| k_value | int | 3 | 1-10 | 检索返回的文档数量 |
支持文件格式: txt、pdf、docx、doc、md、markdown
3.4 文档分块 - config/split_documents.py
职责: 将上传的文档切分为合适大小的文本片段。
类: DocSplitter
| 方法 | 参数 | 返回值 | 说明 |
|---|---|---|---|
__init__ | chunk_size, chunk_overlap | - | 初始化分块参数 |
split_documents | files, temp_dir | List[Document] | 处理文件并返回分块后的文档列表 |
_get_loader | file_name, file_path | Loader | 根据文件扩展名获取对应加载器 |
分块策略:
- 使用
RecursiveCharacterTextSplitter - 支持多种分隔符:
\n\n,\n,。,!,?,.,!,?,, `` - 确保语义完整性的同时控制片段大小
文件加载器映射:
| 文件格式 | 加载器 | 来源模块 |
|---|---|---|
| PyPDFLoader | langchain_classic.document_loaders | |
| txt | TextLoader | langchain_classic.document_loaders |
| docx | Docx2txtLoader | langchain_classic.document_loaders |
| doc | UnstructuredWordDocumentLoader | langchain_classic.document_loaders |
| md/markdown | UnstructuredMarkdownLoader | langchain_classic.document_loaders |
3.5 向量数据库 - config/vector_db_config.py
职责: 配置和管理向量数据库,存储文档嵌入向量。
类: VectorDBConfig
| 方法 | 参数 | 返回值 | 说明 |
|---|---|---|---|
__init__ | vector_db_type, vector_name | - | 初始化数据库类型和名称 |
get_vector_db_config | docs, embeddings | Chroma | 创建并返回向量数据库实例 |
实现细节:
- 使用 Chroma 作为向量数据库
- 每次构建时会删除旧的数据库目录,确保数据更新
- 嵌入模型使用
shibing624/text2vec-base-chinese(中文支持)
3.6 混合检索与重排 - config/rerank_search.py
职责: 实现混合检索和重排算法,提高检索准确性。
3.6.1 混合检索 - hybrid_search
算法流程:
- 向量检索: 使用 Chroma 的
similarity_search进行语义检索 - BM25 检索: 使用 jieba3 分词 + BM25Okapi 进行关键词检索
- 去重合并: 合并两种检索结果,去重后返回
关键代码:
def hybrid_search(question: str, vectordb: Chroma, k: int = 20) -> List[str]:
# 向量检索
vector_results = vectordb.similarity_search(question, k=k)
# BM25 检索
all_docs = vectordb.get(include=["documents"])["documents"]
tokenized_corpus = [seg.cut_text(doc) for doc in all_docs]
bm25 = BM25Okapi(tokenized_corpus)
bm25_scores = bm25.get_scores(tokenized_question)
# 去重合并
seen = {}
for doc in vector_docs + bm25_docs:
seen[doc] = None
return list(seen.keys())[:k]
3.6.2 重排精排 - rerank_search
算法流程:
- 构造输入: 将问题与每个候选文档配对
[(query, doc1), (query, doc2), ...] - 计算得分: 使用 CrossEncoder 模型计算相关性得分
- 排序截取: 按得分降序排序,截取 top_k 文档
重排模型: BAAI/bge-reranker-v2-m3(中英双语支持)
3.7 提示词配置 - config/prompt_data.py
职责: 定义问答提示词模板。
指令模板:
你是一个知识库问答助手。
请根据以下资料回答问题。
如果资料中没有相关信息,请直接回复"抱歉,我不知道"。
提示词模板:
{instructions}
资料:
{contexts}
问题:{question}
请基于以上资料回答:
3.8 自定义回调 - config/custom_callback.py
职责: 定义 Streamlit 回调处理器,用于实时展示 ReAct 推理步骤。(自行加入到代码中,展示推理过程)
from langchain_classic.callbacks.base import BaseCallbackHandler
import re
class CustomStreamlitCallbackHandler(BaseCallbackHandler):
"""
用于 st.status 的回调处理器,实时展示 ReAct 推理步骤。
"""
# 关键词模式(支持中英文,冒号后允许空格)
_PATTERNS = {
"thought": r"(?:Thought|思考)\s*[::]\s*(.*?)(?=\s*(?:Action|行动)\s*[::]|$)",
"action": r"(?:Action|行动)\s*[::]\s*(.*?)(?=\s*(?:Action Input|行动输入)\s*[::]|$)",
"action_input": r"(?:Action Input|行动输入)\s*[::]\s*(.*?)(?=\s*(?:Observation|观察)\s*[::]|$)",
"final_answer": r"(?:Final Answer|最终回答)\s*[::]\s*(.*?)$",
}
def __init__(self, write_func, show_tokens=False):
self.write_func = write_func
self.show_tokens = show_tokens
self._token_buffer = "" if show_tokens else None
def _emit(self, icon, title, content, code=True):
"""统一输出到 status,自动处理空内容"""
if not content:
content = "(无内容)"
else:
content = str(content)
if code:
markdown = f"{icon} **{title}**\n\n```text\n{content}\n```\n\n---"
else:
markdown = f"{icon} **{title}**\n\n{content}\n\n---"
self.write_func(markdown)
def _parse(self, text):
"""提取 Thought, Action, Action Input, Final Answer"""
result = {}
for key, pattern in self._PATTERNS.items():
match = re.search(pattern, text, re.DOTALL)
result[key] = match.group(1).strip() if match else None
return result
# -------------------- 回调事件 --------------------
def on_llm_start(self, serialized, prompts, **kwargs):
self.write_func("🔄 正在调用语言模型...")
def on_llm_end(self, response, **kwargs):
try:
if not response.generations:
return
text = response.generations[0][0].text
parsed = self._parse(text)
if parsed["thought"]:
self._emit("💭", "思考", parsed["thought"], code=False)
if parsed["action"]:
action_input = parsed["action_input"] or ""
self._emit(
"🔧",
f"执行工具: {parsed['action']}",
f"输入: {action_input}" if action_input else "",
code=False,
)
if parsed["final_answer"]:
self._emit("✅", "推理完成", "已生成最终回答,请查看上方聊天区域。", code=False)
# 显示 token 流(调试)
if self.show_tokens and self._token_buffer:
self._emit("📜", "完整 token 流(调试)", self._token_buffer)
self._token_buffer = ""
except Exception as e:
self.write_func(f"⚠️ 解析 LLM 输出出错: {e}")
def on_tool_start(self, serialized, input_str, **kwargs):
tool_name = serialized.get("name", "未知工具")
self.write_func(f"🛠️ 正在执行工具: {tool_name}")
def on_tool_end(self, output, **kwargs):
try:
if output is None:
return
output_str = str(output)
# 过滤解析错误
if any(kw in output_str for kw in ("Invalid", "incomplete", "Parsing error", "Could not parse")):
self.write_func("⚠️ 模型输出格式有误,正在重试...")
return
if len(output_str) > 800:
output_str = output_str[:800] + "\n... (截断)"
self._emit("📤", "观察结果", output_str)
except Exception as e:
self.write_func(f"⚠️ 处理工具输出出错: {e}")
def on_llm_new_token(self, token, **kwargs):
if self.show_tokens and token:
self._token_buffer += token
def on_error(self, error, **kwargs):
self.write_func(f"❌ 执行错误: {error}")
4. 核心数据流
4.1 文档上传与处理流程
用户上传文件 → Sidebar 获取文件 → DocSplitter 分块 → 嵌入模型向量化 → Chroma 存储
│ │ │ │ │
│ │ │ │ │
▼ ▼ ▼ ▼ ▼
多格式文件 文件列表 文档片段列表 向量列表 向量数据库
(pdf/txt/docx/md) (Streamlit) (RecursiveSplit) (text2vec) (ChromaDB)
4.2 问答检索流程
用户提问 → hybrid_search → rerank_search → 组装 Prompt → LLM 生成 → 流式输出
│ │ │ │ │ │
│ │ │ │ │ │
▼ ▼ ▼ ▼ ▼ ▼
文本问题 向量+BM25检索 CrossEncoder精排 上下文+问题 大语言模型 回答内容
(候选文档20个) (最终文档k个) (gpt-3.5)
4.3 完整时序图
用户 Streamlit DocSplitter Chroma
│ │ │ │
│ 上传文件 │ │ │
│─────────────────────────▶│ │ │
│ │──split_documents()───▶│ │
│ │ │──load() │
│ │ │──split() │
│ │◀────文档片段列表───────│ │
│ │ │ │
│ │────from_documents()──────────────────────▶│
│ │ │ │──存储向量
│ │◀────────Chroma实例───────────────────────│
│ │ │ │
│ │ │ │
│ 提问 │ │ │
│─────────────────────────▶│ │ │
│ │──hybrid_search()─────────────────────────▶│
│ │ │ │──similarity_search
│ │◀────────候选文档列表───────────────────────│
│ │ │ │
│ │──rerank_search()──────▶│ │
│ │ │──CrossEncoder精排 │
│ │◀────────精排结果───────│ │
│ │ │ │
│ │──LLM.generate()───────▶│ │
│ │ │ │
│◀──────────回答───────────│ │ │
5. 技术栈与依赖
5.1 依赖列表
| 依赖名称 | 版本要求 | 用途 |
|---|---|---|
| streamlit | >=1.37.0 | Web 应用框架 |
| langchain | >=1.3.0 | LLM 应用开发 |
| langchain-community | >=0.4.0 | LangChain 社区模块 |
| langchain-classic | >=1.0.8 | LangChain 经典模块 |
| langchain-chroma | >=1.1.0 | Chroma 集成 |
| chromadb | >=0.3.21 | 向量数据库 |
| langchain-openai | >=1.3.0 | OpenAI 集成 |
| python-dotenv | >=1.0.0 | 环境变量管理 |
| langchain-text-splitters | >=1.2.1 | 文本分块 |
| pypdf | >=6.14.0 | PDF 解析 |
| unstructured | >=0.23.1 | 非结构化文档处理 |
| rank_bm25 | - | BM25 算法 |
| jieba3 | - | 中文分词 |
| sentence_transformers | - | CrossEncoder 模型 |
| langchain-huggingface | - | HuggingFace 集成 |
注意:
requirements.txt中存在拼写错误 (langchian-classic应为langchain-classic),且缺少rank_bm25、jieba3、sentence_transformers、langchain-huggingface四个依赖。
5.2 模型配置
| 模型类型 | 模型名称 | 来源 | 用途 |
|---|---|---|---|
| 嵌入模型 | shibing624/text2vec-base-chinese | HuggingFace | 文本向量化 |
| 重排模型 | BAAI/bge-reranker-v2-m3 | HuggingFace | 文档精排 |
| LLM | gpt-3.5-turbo | OpenAI | 回答生成 |
| LLM (可选) | glm-4.5-air | 智谱 AI | 可选 |
6. 配置说明
6.1 环境变量配置 (.env)
# 大语言模型配置
AI_MODEL = "glm-4.5-air"
AI_API_KEY = "your_api_key_here"
# 可选配置(当前未使用)
# AI_MODEL = "qwen3.7-max-preview"
# AI_API_KEY = "your_qwen_api_key"
注意: .env 中配置的模型 (glm-4.5-air) 未被实际使用,代码中硬编码为 gpt-3.5-turbo。
6.2 运行环境配置
# 国内镜像配置(index.py 中)
os.environ["HF_ENDPOINT"] = "https://hf-mirror.com"
os.environ["HUGGINGFACE_HUB_URL"] = "https://hf-mirror.com"
7. 使用说明
7.1 环境准备
# 安装依赖(注意补充缺失的依赖)
pip install streamlit langchain langchain-community langchain-classic langchain-chroma chromadb langchain-openai python-dotenv langchain-text-splitters pypdf unstructured rank_bm25 jieba3 sentence_transformers langchain-huggingface
# 配置环境变量
# 编辑 .env 文件,设置 AI_MODEL 和 AI_API_KEY
7.2 运行应用
streamlit run app.py
测试:
测试文章:
2007年1月9日,美国旧金山MacWorld大会。当史蒂夫·乔布斯走上舞台说出“今天,苹果将重新发明手机”时,很少有人意识到,接下来的三十分钟将彻底改变世界。 人们最初以为苹果即将推出三种新设备——iPod、手机和互联网通信器。但当乔布斯展示那个融合了触控屏、iPod和互联网功能的设备时,全场沸腾。第一代iPhone的诞生,终结了诺基亚等传统手机厂商的霸权,开启了一场移动革命。此后,苹果发布会被称为“科技春晚”,在一年又一年的9月,令无数媒体人翘首以盼,让全球果粉熬夜围观。那不仅是一场发布会,更是一个新时代的起点。 乔布斯的演讲堪称经典。他先是巧妙地回顾了苹果的革命性产品——Mac改变了计算机,iPod改变了音乐。然后,他话锋一转,指向了当时市场上所谓的“智能”手机,指出它们既不智能,也难以使用。在吊足观众胃口后,他轻描淡写地说:“今天,我们要推出三款革命性产品:一部触控宽屏的iPod,一部革命性的手机,以及一个突破性的互联网通讯设备。” 他重复了三遍,台下观众在短暂的困惑后,爆发出雷鸣般的掌声——他们意识到,这不是三个独立的设备,而是一个设备。它就是iPhone。 那一刻的震撼,源于其颠覆性的设计。初代iPhone正面几乎只有一块3.5英寸的“巨大”屏幕和一个Home键。它抛弃了当时智能手机必备的物理键盘、手写笔,取而代之的是基于电容屏的多点触控技术。乔布斯在台上用手指放大照片、用惯性滚动联系人列表,这些在今天看来习以为常的操作,在当时无异于施展魔法。
问题一:第一代iPhone的诞生是什么时候,在哪发布的?
Rag问答助手的回答:
问题二:说一下初代iPhone正面设计的特征?
Rag问答助手的回答:
7.3 操作步骤
- 上传文档: 在侧边栏点击"上传文件",选择支持的文档格式
- 调整参数: 根据需要调整分块大小、重叠大小和检索数量
- 等待处理: 系统会自动处理文档并构建向量数据库
- 输入问题: 在聊天框中输入问题,系统会基于文档内容生成回答
7.4 注意事项
- 首次运行会下载嵌入模型和重排模型,可能需要较长时间
- 建议使用国内网络或配置 HF_ENDPOINT 加速模型下载
- 文档处理完成后会自动缓存,重新上传相同文件不会重复处理


1871



被折叠的 条评论
为什么被折叠?



