一、项目整体理解
这是一个生产级的多层 RAG 问答系统,核心特点:
-
多级检索降级:Redis 缓存 → BM25 关键词检索 → Milvus 向量检索,层层递进
-
多轮对话支持:MySQL 存储会话历史,自动保留最近5轮
-
流式输出:最终答案逐字返回,用户体验好
-
模块化设计:文档加载、文本切分、向量存储、检索策略各司其职
二、配置与基础模块
Config 类设计
配置类的核心是多层 fallback 机制:
python
self.MYSQL_HOST = self.config.get('mysql', 'host', fallback='localhost')
-
配置文件缺失时使用默认值,不会因为配置缺失而崩溃
-
使用
eval处理列表配置(如valid_sources),但需注意安全风险 -
路径自动推导:无论从哪里导入,都能找到
config.ini
关键配置参数
| 配置项 | 值 | 说明 |
|---|---|---|
| MySQL port | 33060 | 非默认端口,注意 |
| parent_chunk_size | 1200 | 父块较大,保留完整上下文 |
| child_chunk_size | 300 | 子块小,检索精准 |
| candidate_m | 2 | 最终只取2个文档给 LLM |
Logger 设计
python
if not logger.handlers: # 避免重复添加
防止多次导入时添加多个 handler,避免日志重复打印。
三、文档处理模块
父子文档切分策略
这是整个项目的核心设计之一:
python
"""
原始文档 (3000字)
│
▼ parent_splitter (chunk_size=1200)
┌─────────────────────────────────────────────────────────────┐
│ Parent Chunk 0 (1200字) parent_id: doc_0_parent_0 │
│ │ │
│ ▼ child_splitter (chunk_size=300) │
│ ┌──────────────┐ ┌──────────────┐ ┌──────────────┐ ┌────────────┐ │
│ │ Child 0_0 │ │ Child 0_1 │ │ Child 0_2 │ │ Child 0_3 │ │
│ │ parent_id │ │ parent_id │ │ parent_id │ │ parent_id │ │
│ │ parent_ │ │ parent_ │ │ parent_ │ │ parent_ │ │
│ │ content │ │ content │ │ content │ │ content │ │
│ └──────────────┘ └──────────────┘ └──────────────┘ └────────────┘ │
└─────────────────────────────────────────────────────────────────────┘
"""
设计思想:
-
子块入库:用于检索,粒度小,匹配精准
-
父块输出:作为 LLM 上下文,内容完整,语义连贯
-
子块中直接存储
parent_content,避免向量库二次查库
中文优化切分器
python
self._separators = [
"\n\n", # 段落分隔
"\n", # 换行
"。|!|?", # 中文句尾标点(正则支持多匹配)
"\.\s|\!\s|\?\s", # 英文句尾+空格
";|;\s", # 分号
",|,\s" # 逗号
]
相比 LangChain 原生切分器,这个版本对中文更友好,能够按语义边界进行切分。
文档加载器总结
| 加载器 | 文字 | 表格 | 图片OCR | 特殊处理 |
|---|---|---|---|---|
| WordLoader | ✅ | ✅ | ✅ | 内嵌图片OCR |
| PDFLoader | ✅ | ❌ | ✅ | 旋转校正、阈值过滤 |
| PPTLoader | ✅ | ✅ | ✅ | 组合递归、形状排序 |
| 图片加载器 | ❌ | ❌ | ✅ | 纯OCR |
PDF 图片过滤逻辑:只提取占据页面 60% 以上面积的大图,小图直接丢弃。这是一个性能优先的设计决策。
四、向量存储模块(Milvus)
Schema 设计
python
schema.add_field("id", VARCHAR, is_primary=True) # MD5 哈希作为主键
schema.add_field("text", VARCHAR) # 子块文本内容
schema.add_field("dense_vector", FLOAT_VECTOR, dim=1024) # 稠密向量
schema.add_field("sparse_vector", SPARSE_FLOAT_VECTOR) # 稀疏向量
schema.add_field("parent_id", VARCHAR) # 父块 ID(溯源)
schema.add_field("parent_content", VARCHAR) # 父块内容(避免二次查询)
schema.add_field("source", VARCHAR) # 学科过滤字段
混合检索原理
稠密向量:由 BGE-M3 模型生成,捕获语义相似性
稀疏向量:也是由 BGE-M3 生成,是学习出来的关键词权重分布
python
# 稀疏向量示例(概念示意)
sparse_vector = {
token_id_1: 0.85, # "人工智能" 的权重
token_id_2: 0.62, # "课程" 的权重
token_id_3: 0.31, # "介绍" 的权重
}
两阶段排序
| 阶段 | 方法 | 目的 |
|---|---|---|
| 第一阶段 | WeightedRanker 加权合并 | 快速筛选,从百万级降到百级 |
| 第二阶段 | BGE-Reranker 重排序 | 精排,从百级降到最终 K 个 |
为什么需要两步?
-
加权合并:速度快(毫秒级),适合初筛
-
Reranker:精度高(秒级),适合精排
索引类型选择
| 索引类型 | 适用场景 | 配置 |
|---|---|---|
| IVF_FLAT | 稠密向量,追求精度 | nlist=128 |
| SPARSE_INVERTED_INDEX | 稀疏向量专用 | drop_ratio_build=0.2 |
加权排序权重
python
ranker = WeightedRanker(1.0, 0.7) # 稠密:稀疏 = 1.0:0.7
教育问答中,语义理解 比关键词匹配更重要,所以稠密向量权重更高。
多路召回的本质
用户 Query → 稠密向量检索 + 稀疏向量检索 → 加权合并 → Rerank 精排 → 最终结果
你的理解:"多路召回指的是 bge-m3 的稠密向量和稀疏向量分别做近似值得分,然后使用 rerank 来做统一排序"
修正 :rerank 是在加权合并之后做的,不是替代加权合并。加权合并是快速初筛,rerank 是精排。
五、查询分类器(BERT 微调)
核心流程
-
检查训练好的模型路径,存在则加载,不存在则用基座模型初始化
-
从基座模型加载分词器
-
训练过程:
-
加载数据文件,区分特征值和标签值
-
8:2 原则划分训练集和测试集
-
数据预处理:分词器将文本 ID 化,标签数字化(0/1)
-
Dataset 封装(继承
torch.utils.data.Dataset) -
TrainingArguments 配置训练参数
-
Trainer 训练 + 保存模型
-
-
评估:打印分类报告和混淆矩阵
-
预测:输入 query → 分词 → 模型 → logits → argmax → 返回类别
标签映射
python
self.label_map = {"通用知识": 0, "专业咨询": 1}
训练参数说明
| 参数 | 值 | 说明 |
|---|---|---|
| num_train_epochs | 3 | 训练轮次 |
| per_device_batch_size | 8 | 批次大小 |
| warmup_steps | 20 | 学习率预热步数 |
| weight_decay | 0.01 | 权重衰减 |
| fp16 | False | 禁用混合精度(CPU 训练) |
六、策略选择器(LLM 路由)
四种检索策略
| 策略 | 适用场景 | 示例 |
|---|---|---|
| 直接检索 | 意图明确,问具体信息 | "AI 学科学费是多少?" |
| 子查询检索 | 涉及多个实体/方面 | "比较 Milvus 和 Zilliz Cloud 的优缺点" |
| HyDE | 抽象,直接检索效果差 | "人工智能在教育领域的应用" |
| 回溯问题检索 | 复杂,需简化 | "100亿条数据存 Milvus 可以吗?" |
设计思路
用 LLM 做策略选择,而不是硬编码规则:
-
将 4 种策略 + 示例拼成 Prompt
-
让 LLM 判断当前 query 属于哪种策略
-
返回策略名称字符串
优点 :灵活,新增策略只需改 Prompt
缺点:不确定性,增加一次 LLM 调用
七、BM25 检索模块
核心流程
-
从 Redis 获取缓存(
answer:{query}),命中则直接返回 -
未命中则进行 BM25 打分:
-
query 分词
-
bm25.get_scores()计算原始分数 -
_softmax()归一化到 [0,1] -
argmax()取最高分索引
-
-
阈值判断(threshold=0.85):
-
best_score >= 0.85:MySQL 查答案 → 缓存到 Redis → 返回 -
best_score < 0.85:返回(None, True)→ 降级到 RAG
-
Softmax 归一化
python
def _softmax(self, scores):
exp_scores = np.exp(scores - np.max(scores)) # 减去最大值防止溢出
return exp_scores / exp_scores.sum()
为什么需要?
-
BM25 原始分数范围很大(几到几百)
-
归一化到 [0,1] 后,可以用统一的阈值(0.85)
返回值含义
| 返回值 | 含义 |
|---|---|
answer, False |
找到答案,不需要 RAG |
None, True |
未找到,需要降级到 RAG |
None, False |
无效查询 |
八、RAG 主流程
完整调用链路
python
用户 Query
│
▼
1. 获取历史对话(MySQL,session_id 隔离,最近5轮)
│
▼
2. BERT 分类器判断
├── "通用知识" → 直接 LLM 回答
└── "专业咨询" → 进入 RAG 流程
│
▼
3. 策略选择器(LLM 路由)→ 选择检索策略
│
├── 直接检索:原始 query
├── 回溯问题检索:LLM 简化问题
├── 子查询检索:LLM 拆分成多个子查询
└── HyDE:LLM 生成假设答案
│
▼
4. Milvus 混合检索 + 重排序 → 返回父块
│
▼
5. 构建 Prompt(context + history + query)
│
▼
6. LLM 流式生成最终答案
│
▼
7. 更新 MySQL 对话历史(保留最近5轮,删除旧记录)
流式输出设计
python
def generate_answer(self, query, source_filter=None, history=None):
# ... 检索逻辑 ...
# 最终答案使用流式输出
for token in self.llm(prompt_input):
yield token
返回格式 :(content, is_complete)
-
is_complete=False:流式输出中 -
is_complete=True:输出完成
九、会话管理
会话表结构
python
CREATE TABLE conversations (
id INT AUTO_INCREMENT PRIMARY KEY,
session_id VARCHAR(36) NOT NULL,
question TEXT NOT NULL,
answer TEXT NOT NULL,
timestamp DATETIME NOT NULL,
INDEX idx_session_id (session_id)
);
历史获取逻辑
python
# SQL 查询:最新5条(最新在前)
SELECT question, answer FROM conversations
WHERE session_id = %s
ORDER BY timestamp DESC LIMIT 5
# 反转:恢复时间正序
history = history[::-1]
# 格式化:供 LLM 理解
history_text = "\n".join([f"Q:{h['question']}\nA:{h['answer']}" for h in history])
历史更新与清理
python
-- 插入新记录
INSERT INTO conversations (session_id, question, answer, timestamp)
VALUES (%s, %s, %s, NOW())
-- 删除超出5轮的旧记录(嵌套子查询绕过 MySQL 限制)
DELETE FROM conversations
WHERE session_id = %s AND id NOT IN (
SELECT id FROM (
SELECT id FROM conversations
WHERE session_id = %s
ORDER BY timestamp DESC LIMIT 5
) AS sub
)
十、集成问答系统
初始化组件
python
self.logger = logger
self.config = Config()
self.mysql_client = MySQLClient()
self.redis_client = RedisClient()
self.bm25_search = BM25Search(self.redis_client, self.mysql_client)
self.vector_store = VectorStore()
self.rag_system = RAGSystem(self.vector_store, self.call_dashscope)
self.init_conversation_table() # 创建 conversations 表
三级检索降级
| 优先级 | 方法 | 触发条件 | 速度 |
|---|---|---|---|
| 1 | Redis 缓存 | answer:{query} 存在 |
最快 |
| 2 | BM25 + MySQL | 相似度 ≥ 0.85 | 快 |
| 3 | RAG (Milvus) | BM25 未命中 | 慢但全面 |
流式返回格式
python
# 命中 BM25
yield answer, True
# RAG 流式
for token in rag_system.generate_answer(...):
yield token, False
yield "", True
# 未找到
yield "未找到答案", True
十一、架构设计亮点总结
1. 模块化设计
-
每个组件职责单一,可独立替换和升级
-
工厂模式 + 策略模式处理多种文档格式
2. 检索策略
-
稠密向量(语义)+ 稀疏向量(关键词),互补优势
-
两阶段排序:加权合并快速初筛 + Cross-Encoder 精排
-
LLM 路由智能选择检索策略
3. 工程实现
-
父子文档策略:子块精准检索 + 父块完整上下文
-
多格式文档加载:PDF/Word/PPT/图片,统一接口
-
智能 OCR:只对大图进行 OCR,避免性能浪费
-
流式输出:逐字返回,用户体验好
-
自动历史清理:只保留最近5轮,控制上下文长度
-
两级缓存:Redis 缓存答案和 BM25 索引
4. 用户体验
-
多会话支持:每个 session_id 独立历史
-
学科过滤:可按学科限定检索范围
-
历史追溯:所有问答都有记录
十二、数据流总结
python
data/*.pdf/.docx/.pptx
│
▼ 文档加载器(OCRPDFLoader/OCRDOCLoader/OCRPPTLoader)
Document 对象
│
▼ document_processor.py(父子切分)
Child Chunks(子块入库,父块内容存 metadata)
│
▼ vector_store.py(BGE-M3 嵌入 + Milvus 存储)
Milvus 向量库
│
▼ new_main.py(用户查询入口)
│
├── Redis 缓存命中 → 直接返回
│
├── BM25 命中(≥0.85)→ MySQL 查答案 → 缓存 → 返回
│
└── BM25 未命中 → RAG 流程
│
├── QueryClassifier(BERT)→ 分类
├── StrategySelector(LLM)→ 选策略
├── VectorStore(Milvus)→ 混合检索 + 重排序
└── call_llm_stream(Ollama)→ 流式输出
十三、核心设计决策记录
为什么要用父子文档?
-
子块检索:粒度小,匹配精准
-
父块输出:内容完整,语义连贯
-
子块中直接存储
parent_content:避免向量库二次查库
为什么要用混合检索?
-
稠密向量:理解语义,"汽车"和"轿车"在语义空间中是接近的
-
稀疏向量:匹配关键词,BGE-M3 学习出来的权重分布
-
两者互补,覆盖更多检索场景
为什么要用两阶段排序?
-
加权合并:速度快(毫秒级),适合从百万级降到百级
-
Reranker:精度高(秒级),适合从百级降到最终 K 个
-
不能在百万级文档上直接跑 Reranker,太慢
为什么要用 BERT 分类器?
-
快速识别通用问题(如"今天天气怎么样")
-
避免不必要的 RAG 调用,节省成本和时间
-
轻量级,CPU 即可运行
为什么要用 LLM 做策略选择?
-
灵活:新增策略只需改 Prompt
-
智能:利用 LLM 的语义理解能力
-
低成本:只需一次小模型调用
为什么要做三级检索降级?
| 级别 | 方法 | 成本 | 速度 |
|---|---|---|---|
| 1 | Redis 缓存 | 极低 | 最快 |
| 2 | BM25 + MySQL | 低 | 快 |
| 3 | Milvus + RAG | 高 | 慢 |
常见问题直接命中缓存或 BM25,复杂问题才走 RAG,平衡成本和效果。
十四、运行与部署
启动命令
python
# 命令行模式
python new_main.py
# API 服务模式
python app.py
# WebSocket + HTTP 服务
python app.py # 端口 8001
环境要求
-
Python 3.10+
-
MySQL 8.0+
-
Redis 7.0+
-
Milvus 2.4+
-
Ollama(本地 LLM)
依赖安装
python
pip install -r requirements.txt
配置文件(config.ini)
需配置:MySQL、Redis、Milvus、LLM API、检索参数、应用参数
十五、总结
这个 RAG 项目从文档加载到向量检索,从 BM25 降级到多轮对话,从流式输出到 API 封装,覆盖了一个完整问答系统的所有环节。
核心收获:
-
RAG 不是单一技术,是系统工程:需要综合考虑文档处理、向量检索、模型选型、系统架构、用户体验等多个维度
-
分层设计很重要:三级检索降级(缓存 → BM25 → RAG)在保证效果的同时控制了成本
-
父子文档是精妙设计:子块精准检索 + 父块完整上下文,兼顾精度和完整性
-
模块化让系统可扩展:每个组件职责单一,可独立替换和升级
-
用户体验不容忽视:流式输出、多会话支持、历史记忆,这些细节决定了系统的可用性
文档整理完成,记录了今天所有的核心理解和技术要点。