RAG 项目完整学习笔记与总结

一、项目整体理解

这是一个生产级的多层 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 微调)

核心流程

  1. 检查训练好的模型路径,存在则加载,不存在则用基座模型初始化

  2. 从基座模型加载分词器

  3. 训练过程:

    • 加载数据文件,区分特征值和标签值

    • 8:2 原则划分训练集和测试集

    • 数据预处理:分词器将文本 ID 化,标签数字化(0/1)

    • Dataset 封装(继承 torch.utils.data.Dataset

    • TrainingArguments 配置训练参数

    • Trainer 训练 + 保存模型

  4. 评估:打印分类报告和混淆矩阵

  5. 预测:输入 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 检索模块

核心流程

  1. 从 Redis 获取缓存(answer:{query}),命中则直接返回

  2. 未命中则进行 BM25 打分:

    • query 分词

    • bm25.get_scores() 计算原始分数

    • _softmax() 归一化到 [0,1]

    • argmax() 取最高分索引

  3. 阈值判断(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 封装,覆盖了一个完整问答系统的所有环节。

核心收获

  1. RAG 不是单一技术,是系统工程:需要综合考虑文档处理、向量检索、模型选型、系统架构、用户体验等多个维度

  2. 分层设计很重要:三级检索降级(缓存 → BM25 → RAG)在保证效果的同时控制了成本

  3. 父子文档是精妙设计:子块精准检索 + 父块完整上下文,兼顾精度和完整性

  4. 模块化让系统可扩展:每个组件职责单一,可独立替换和升级

  5. 用户体验不容忽视:流式输出、多会话支持、历史记忆,这些细节决定了系统的可用性


文档整理完成,记录了今天所有的核心理解和技术要点。

相关推荐
Pkmer2 小时前
Harness Engineering: 人类掌舵,智能体执行
llm·agent
前端双越老师2 小时前
OpenClaw 实战记录:前端 VS 全栈 招聘岗位分析
前端·agent·全栈
EdisonZhou2 小时前
MAF快速入门(23)通过C#类定义Skills
llm·agent·.net core
Flying pigs~~3 小时前
企业级模块化RAG项目(mysql➕redis➕milvus➕模型微调➕bm25➕fastapi➕ollama➕Prompt➕多策略选择)
人工智能·redis·mysql·docker·prompt·milvus·rag
rising start3 小时前
RAG入门与在Dify中的简单实践
embedding·dify·rag
weitingfu3 小时前
大语言模型架构演进:从BERT到GPT再到Mamba的正确打开方式
人工智能·ai·语言模型·架构·bert·agent·ai编程
HIT_Weston3 小时前
47、【Agent】【OpenCode】本地代理增强版分析(JSON解析)
人工智能·json·agent·opencode
七牛云行业应用18 小时前
Hermes Agent总报错?别砸电脑,这10个天坑教你5分钟填平
agent
王同学的AI学习日记18 小时前
替你筛完70个Skills!手把手教你调教Hermes Agent!
agent