all-in-rag 食谱知识库实战:以「椒盐玉米」为例解析结构化菜谱文档的加载、分块与检索生成全流程
【免费下载链接】all-in-rag🔍大模型应用开发实战一:RAG 技术全栈指南,在线阅读地址:https://datawhalechina.github.io/all-in-rag/项目地址: https://gitcode.com/datawhalechina/all-in-rag
本文以 all-in-rag 开源项目中「尝尝咸淡」RAG 食谱问答系统(code/C8)为背景,选用其知识库中的 椒盐玉米 菜谱文档作为贯穿全文的实例,完整讲解一份结构化 Markdown 菜谱是如何被数据准备模块加载、元数据增强、按标题层级分块,再进入 FAISS 向量索引与 BM25 关键词检索,最终由大模型生成分步烹饪指导的。读完本文,你将掌握"小块检索、大块生成"父子文本块架构在真实数据上的落地方式,并能对照仓库源码独立理解任意一份菜谱文档在 RAG 流水线中的处理细节。
一、菜谱文档:RAG 系统中的结构化数据样本
在 code/C8 项目中,知识库数据存放于 data/C8/cook 目录,按荤菜、素菜、汤品、甜品等分类组织成子目录。椒盐玉米.md位于素菜分类vegetable_dish下,是极具代表性的结构化菜谱样本,其内容由四个标准章节构成:
- 一级标题 + 难度评级:
# 椒盐玉米的做法与预估烹饪难度:★★★; - 必备原料和工具:玉米粒、椒盐、芝麻粒、油、淀粉、两个塑料簸箕、若干吸油纸;
- 计算:给出按"标准分量"换算的用料配比(玉米粒 350g、淀粉 40-70g、椒盐粉 10g、芝麻粒 10g);
- 操作:从解冻、吸油纸吸水、裹淀粉到煎制出锅的完整步骤序列;
- 附加内容:关于玉米粒购买与解冻的经验性提示。
这种"标题层级清晰、章节边界明确"的文档结构,正是第 2 章所讲 Markdown 结构分块 的理想输入。由于每个章节篇幅较短(几百字以内),不会出现单块超出模型上下文窗口的问题,可以直接按标题分块而不必与递归字符分割器组合使用。椒盐玉米这份文档的全部核心要素——原料清单、用量配比、操作步骤、附加技巧——在后续的数据准备与生成阶段都会被完整继承。
提示:本文引用的菜谱文档与运行代码均位于仓库 code/C8 对应的食谱问答项目中,读者可按 环境配置与项目架构 一节创建 Python 3.12.7 虚拟环境并安装 requirements.txt 中的依赖后运行 main.py 复现全流程。
二、数据准备:菜谱文档如何进入知识库
2.1 递归加载 Markdown 文件并生成父文档
数据准备模块的入口是 data_preparation.py 中的DataPreparationModule.load_documents()。它使用Path.rglob("*.md")递归扫描data_path下的全部菜谱文件,逐份读取原始 Markdown 内容,并为每个文件生成一个"父文档"(doc_type 标记为parent)。其核心代码如下:
for md_file in data_path_obj.rglob("*.md"): with open(md_file, 'r', encoding='utf-8') as f: content = f.read() parent_id = hashlib.md5(relative_path.encode("utf-8")).hexdigest() doc = Document( page_content=content, metadata={"source": str(md_file), "parent_id": parent_id, "doc_type": "parent"} )以椒盐玉米为例,加载后其source指向data/C8/cook/dishes/vegetable_dish/椒盐玉米/椒盐玉米.md,page_content是完整的菜谱正文,parent_id则由相对路径的 MD5 哈希唯一确定——注意这与文档中早期的uuid.uuid4()方案不同,当前仓库实现(data_preparation.py)采用确定性哈希,保证同一路径每次加载生成的父文档 ID 稳定,便于索引缓存复用。
2.2 元数据增强:从路径与内容自动抽取分类、菜名、难度
加载完成后,_enhance_metadata()会为每个父文档补充三层关键元数据:
- 菜品分类:从文件路径片段匹配
CATEGORY_MAPPING(vegetable_dish→ 素菜,另有荤菜、汤品、甜品、早餐、主食、水产、调料、饮品等映射); - 菜品名称:直接取文件名(
dish_name = "椒盐玉米"); - 难度等级:用正则
re.search(r'★+', content)统计连续星号数量,映射到五档难度——5 星"非常困难"、4 星"困难"、3 星"中等"、2 星"简单"、1 星"非常简单"。
因此椒盐玉米文档会被自动标注为:category=素菜、dish_name=椒盐玉米、difficulty=中等。这些元数据正是后续 main.py 中_extract_filters_from_query()实现元数据过滤检索的依据——当用户问"推荐个简单的素菜"时,系统能据此精确限定category与difficulty。
2.3 Markdown 标题分块:一份菜谱拆成五个子块
chunk_documents()调用_markdown_header_split(),通过 LangChain 的MarkdownHeaderTextSplitter按#、##、###三级标题分割:
headers_to_split_on = [ ("#", "主标题"), # 菜品名称 ("##", "二级标题"), # 必备原料、计算、操作等 ("###", "三级标题") # 简易版本、复杂版本等 ] markdown_splitter = MarkdownHeaderTextSplitter( headers_to_split_on=headers_to_split_on, strip_headers=False # 保留标题,便于理解上下文 )strip_headers=False意味着分块后仍保留标题行,使每个子块自带语义上下文。对椒盐玉米文档,分割结果对应 数据准备模块实现 中描述的父子映射:
父文档(椒盐玉米.md) ├── 子块1:# 椒盐玉米的做法 + 简介 + 难度评级 ├── 子块2:## 必备原料和工具 + 食材清单 ├── 子块3:## 计算 + 用量配比 ├── 子块4:## 操作 + 详细制作步骤 └── 子块5:## 附加内容每个子块都会继承父文档元数据,并额外打上chunk_id、parent_id、doc_type=child、chunk_index、chunk_size等标记,同时把child_id → parent_id关系写入parent_child_map,为"小块检索、大块生成"奠定基础。
2.4 智能去重:多个子块归并为一个完整菜谱
当用户询问"椒盐玉米怎么做"时,检索阶段可能同时命中子块 2、3、4。get_parent_documents()会统计每个父文档被子块命中的次数作为相关性分数,按分数降序去重输出父文档,避免 LLM 收到重复内容。从源码结构看(data_preparation.py 对应方法),命中子块越多的菜谱在最终结果中排序越靠前。
三、索引构建与检索:玉米粒"配方"如何被精确召回
3.1 向量索引与索引缓存
IndexConstructionModule(位于 index_construction.py)使用配置指定的嵌入模型BAAI/bge-small-zh-v1.5将子块向量化并写入 FAISS 向量库,索引持久化到index_save_path(默认./vector_index)。main.py 启动时优先尝试加载已保存索引:命中缓存则秒级启动,未命中则走"加载文档 → 分块 → 建索引 → 保存"全流程。
3.2 混合检索与 RRF 重排
RetrievalOptimizationModule 同时维护两路检索器:
- 向量检索器:
vectorstore.as_retriever(search_type="similarity", search_kwargs={"k": 5}),捕捉语义相似度; - BM25 检索器:
BM25Retriever.from_documents(chunks, k=5),捕捉关键词精确匹配。
hybrid_search()将两路结果通过RRF(Reciprocal Rank Fusion)融合:对每个文档,按1 / (k + rank + 1)(仓库实现中 RRF 常数 k=60,见 retrieval_optimization.py)累加两个榜单的排名分数,按总分降序取top_k(配置默认 3)。这种设计让"椒盐玉米"这类既包含语义(怎么做)又包含术语(淀粉、簸箕、椒盐)的查询,无论走语义还是关键词通道都能被稳定召回。
metadata_filtered_search()则先在混合检索结果上放大 3 倍候选,再按category/difficulty等过滤条件筛选,最终返回top_k条,供"推荐个简单的素菜"这类带约束的查询使用。
四、生成:让大模型基于菜谱产出分步烹饪指导
检索到椒盐玉米的相关子块后,main.py 依次完成查询路由、查询重写与生成:
- 查询路由:
query_router()将问题分类为list(推荐列表)、detail(详细做法)、general(一般问题); - 查询重写:
detail与general类查询交由query_rewrite()判断是否需要补充烹饪术语后重写,list类保持原样; - 上下文组装:
_build_context()将去重后的父文档连同dish_name、category、difficulty元数据拼装为上下文字符串,并限制在max_length=2000字符内(见 generation_integration.py); - 分步回答:
generate_step_by_step_answer()通过 generation_integration.py 中的提示词模板,引导 Moonshot 模型输出"菜品介绍 / 所需食材 / 制作步骤 / 制作技巧"四段式回答,同时要求不强行填充无关内容。
当用户问"椒盐玉米怎么做"时,系统实际召回的是"操作"子块(精确匹配),但生成时送入 LLM 的是完整父文档——椒盐玉米的原料(玉米粒 350g、淀粉 40-70g、椒盐粉 10g、芝麻粒 10g)、工具(簸箕、吸油纸)与"中火先煎 30s 不翻炒、再轻微翻炒 3 分钟"的关键火候全部在场,回答自然完整可用。这正是"小块检索保证精度、大块生成保证完整性"的直观体现,详细设计动机可参考 环境配置与项目架构 中关于父子文本块的论述。
五、配置项速查:让椒盐玉米这类文档跑起来的关键参数
config.py 中的RAGConfig数据类集中管理全系统配置,下表是与菜谱文档处理直接相关的核心参数:
| 配置项 | 默认值 | 作用与影响 |
|---|---|---|
data_path | ../../data/C8/cook | 知识库根目录,rglob("*.md")递归扫描的起点 |
embedding_model | BAAI/bge-small-zh-v1.5 | 子块向量化所用嵌入模型 |
llm_model | kimi-k2-0711-preview | 生成回答所用大模型(需配置MOONSHOT_API_KEY环境变量) |
top_k | 3 | 混合检索最终返回的子块数量 |
temperature | 0.1 | 生成温度,值越低回答越稳定、越忠实于菜谱原文 |
max_tokens | 2048 | 单次生成的最大 token 数,足够覆盖一份完整菜谱的分步指导 |
index_save_path | ./vector_index | FAISS 索引持久化路径,存在时跳过重建实现秒级启动 |
修改data_path指向包含任何结构化 Markdown 菜谱的目录(如继续在素菜目录下新增菜谱文件),系统会自动完成加载、元数据增强与分块,无需改动代码。
六、从一份菜谱到一套知识库:实践要点小结
通过椒盐玉米这一实例,可以总结出在 all-in-rag 食谱 RAG 项目中处理结构化文档的三条经验:
- 让数据结构说话:菜谱文档统一的标题层级(
#/##/###)天然适配 Markdown 结构分块,配合strip_headers=False保留语义上下文,是小块精确检索的前提; - 元数据是检索的杠杆:分类、菜名、难度这些从路径与内容自动抽取的字段,支撑了"推荐简单素菜"这类带约束查询的元数据过滤,极大提升召回精准度;
- 检索与生成的粒度可以不同:小粒度子块负责命中,大粒度父文档负责供给 LLM 完整上下文,配合 RRF 混合重排与智能去重,最终生成既全面又不重复的烹饪回答。
如需深入源码细节,可继续阅读 数据准备模块实现(父子文本块与元数据增强)、索引与检索(FAISS/BM25 混合检索)与 生成系统(提示词与路由设计),并对照 椒盐玉米.md 原始文档逐行验证整条流水线的处理结果。
【免费下载链接】all-in-rag🔍大模型应用开发实战一:RAG 技术全栈指南,在线阅读地址:https://datawhalechina.github.io/all-in-rag/项目地址: https://gitcode.com/datawhalechina/all-in-rag
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考