RAG 数据生成模块
AI 摘要
data_preparation.py 为 RAG 系统实现父子文档检索:按 Markdown 标题将食谱分块,子块精准匹配,父块提供完整上下文;自动提取分类、难度等元数据,按匹配数排序去重返回父文档,兼顾检索精度与生成质量。
📚 模块概述
这是一个用于 RAG(检索增强生成)系统的数据准备模块,专门处理食谱数据。它实现了父子文档(Parent-Child Document)的检索策略,是一种高级的 RAG 优化技术。
🎯 核心设计思想
父子文档检索模式:
- 子文档(chunks):小块文本,用于语义检索(查询匹配更精准)
- 父文档(documents):完整食谱,用于生成回答(上下文更完整)
这种设计解决了 RAG 中的矛盾:小块检索精准但上下文不足,大块上下文完整但检索不精准。
🏗️ 类结构分析
1. 静态配置(类属性)
CATEGORY_MAPPING = {
'meat_dish': '荤菜',
'vegetable_dish': '素菜',
'soup': '汤品',
# ...
}
CATEGORY_LABELS = ['荤菜', '素菜', '汤品', ...]
DIFFICULTY_LABELS = ['非常简单', '简单', '中等', '困难', '非常困难']- 统一维护分类和难度标签
- 支持多语言路径到中文分类的映射
- 可被其他模块复用(如查询构造模块)
2. 实例属性
self.documents: List[Document] # 父文档列表
self.chunks: List[Document] # 子文档列表
self.parent_child_map: Dict[str, str] # 子块ID -> 父文档ID映射🔧 核心方法详解
① load_documents() - 文档加载
功能:从文件系统加载 Markdown 格式的食谱文件
关键实现:
# 1. 生成确定性父文档ID(基于相对路径的MD5)
relative_path = Path(md_file).resolve().relative_to(data_root).as_posix()
parent_id = hashlib.md5(relative_path.encode("utf-8")).hexdigest()
# 2. 创建Document对象,标记为父文档
doc = Document(
page_content=content,
metadata={
"source": str(md_file),
"parent_id": parent_id,
"doc_type": "parent" # 关键标记
}
)设计亮点:
- 使用文件路径生成确定性ID(避免重复加载时ID变化)
- 保留原始 Markdown 格式(而非纯文本)
- 自动增强元数据
② _enhance_metadata() - 元数据增强
功能:从文件路径和内容中提取结构化信息
提取逻辑:
分类提取(从路径):
# 路径示例: data/C8/cook/meat_dish/红烧肉.md for key, value in self.CATEGORY_MAPPING.items(): if key in path_parts: # 检查路径中是否包含 'meat_dish' doc.metadata['category'] = value # 设置为 '荤菜'难度提取(从内容):
if '★★★★★' in content: doc.metadata['difficulty'] = '非常困难' elif '★★★★' in content: doc.metadata['difficulty'] = '困难' # ...菜品名称:
doc.metadata['dish_name'] = file_path.stem # 文件名(不含扩展名)
③ chunk_documents() - 文档分块
功能:将父文档分割成子块,建立父子映射关系
分块策略:
# 使用 Markdown 标题层级进行结构化分割
headers_to_split_on = [
("#", "主标题"), # 菜品名称
("##", "二级标题"), # 必备原料、计算、操作等
("###", "三级标题") # 简易版本、复杂版本等
]父子关系建立:
for i, chunk in enumerate(md_chunks):
child_id = str(uuid.uuid4()) # 生成子块唯一ID
chunk.metadata.update({
"chunk_id": child_id,
"parent_id": parent_id, # 关联父文档
"doc_type": "child", # 标记为子文档
"chunk_index": i # 在父文档中的顺序
})
self.parent_child_map[child_id] = parent_id # 建立映射优势:
- 保留 Markdown 语义结构(按标题分割,不是按字符数)
- 每个子块都知道自己属于哪个父文档
- 子块保留完整标题信息(
strip_headers=False)
④ get_parent_documents() - 父文档检索
功能:根据检索到的子块,智能去重地获取对应父文档
核心算法:
# 1. 统计每个父文档的相关性(被匹配的子块数量)
parent_relevance = {}
for chunk in child_chunks:
parent_id = chunk.metadata.get("parent_id")
parent_relevance[parent_id] = parent_relevance.get(parent_id, 0) + 1
# 2. 按相关性排序(匹配次数多的排前面)
sorted_parent_ids = sorted(parent_relevance.keys(),
key=lambda x: parent_relevance[x],
reverse=True)
# 3. 去重并返回父文档
parent_docs = [parent_docs_map[pid] for pid in sorted_parent_ids]应用场景:
假设检索到 5 个子块:
- 红烧肉的"必备原料"块
- 红烧肉的"操作步骤"块
- 糖醋排骨的"必备原料"块
- 红烧肉的"计算用量"块
- 糖醋排骨的"操作步骤"块
结果:返回 2 个父文档:
- 红烧肉(相关性 3)
- 糖醋排骨(相关性 2)
⑤ 其他辅助方法
分类过滤:
def filter_documents_by_category(self, category: str):
return [doc for doc in self.documents
if doc.metadata.get('category') == category]统计信息:
def get_statistics(self):
return {
'total_documents': len(self.documents),
'total_chunks': len(self.chunks),
'categories': {...},
'difficulties': {...},
'avg_chunk_size': ...
}🔄 工作流程
1. 加载数据
load_documents()
└─> 读取 .md 文件
└─> 生成 parent_id
└─> 增强元数据(分类、难度、菜名)
2. 文档分块
chunk_documents()
└─> Markdown 标题分割
└─> 为每个子块生成 chunk_id
└─> 建立 parent_child_map
3. 检索使用
子块 → 向量检索(精准匹配)
└─> get_parent_documents()
└─> 根据 parent_id 查找父文档
└─> 智能去重和排序
└─> 返回完整食谱💡 设计亮点
- 确定性ID生成:基于文件路径的 MD5,避免重复加载时ID变化
- 结构化分块:按 Markdown 标题语义分割,而非简单字符数
- 相关性排序:父文档按匹配子块数排序,优先返回最相关的
- 元数据丰富:自动提取分类、难度、菜名等结构化信息
- 可扩展设计:类方法提供配置标签,便于其他模块复用
📊 数据流示例
输入文件:data/C8/cook/meat_dish/红烧肉.md
# 红烧肉
## 必备原料
五花肉 500g...
## 操作步骤
1. 切块...加载后父文档:
{
"page_content": "# 红烧肉\n## 必备原料...",
"metadata": {
"parent_id": "a3f2c1...",
"doc_type": "parent",
"category": "荤菜",
"difficulty": "简单",
"dish_name": "红烧肉"
}
}分块后子文档(3个):
[
{
"page_content": "# 红烧肉",
"metadata": {"chunk_id": "uuid1", "parent_id": "a3f2c1...", "doc_type": "child", "chunk_index": 0}
},
{
"page_content": "## 必备原料\n五花肉 500g...",
"metadata": {"chunk_id": "uuid2", "parent_id": "a3f2c1...", "doc_type": "child", "chunk_index": 1}
},
{
"page_content": "## 操作步骤\n1. 切块...",
"metadata": {"chunk_id": "uuid3", "parent_id": "a3f2c1...", "doc_type": "child", "chunk_index": 2}
}
]🎓 总结
这个模块实现了 父子文档检索模式,是高级 RAG 技术的典型应用:
- 检索阶段:用小块(子文档)提高匹配精度
- 生成阶段:用大块(父文档)提供完整上下文
通过结构化分块、智能去重和相关性排序,在检索精度和上下文完整性之间达到最佳平衡。