- 教程
- 人工智能
- 大模型
- RAG
【免费下载链接】all-in-rag
🔍大模型应用开发实战一:RAG 技术全栈指南,在线阅读地址:https://datawhalechina.github.io/all-in-rag/
本篇以 all-in-rag 仓库「尝尝咸淡 RAG 系统」知识库中的真实菜谱 洋葱炒猪肉的做法 为贯穿全文的案例,完整讲解一道结构化菜谱从"原料数据"到"可问答知识"的加工链路:元数据增强、Markdown 结构分块、父子文本块检索去重、以及 LLM 分步指导生成。读完你将掌握 RAG 系统中"小块检索、大块生成"策略在一份真实文档上的落地细节,并可直接对照源码复现整个问答流程。
一、数据源本体:洋葱炒猪肉这道菜到底写了什么
先回到一切数据的起点——仓库中的原始文档 洋葱炒猪肉的做法。它位于data/C8/cook/dishes/meat_dish/目录下,属于「荤菜」分类的菜谱之一,全文结构高度规整,是典型的可用于 RAG 构建的结构化 Markdown 数据。其完整内容如下:
洋葱炒猪肉的做法
咸中带甜,简单上手,一不小心可能让人多吃一碗饭。一般只需 15 分钟即可完成。
预估烹饪难度:★★★
文档正文由「必备原料和工具」「计算」「操作」「附加内容」四个二级标题章节构成,每个章节职责单一、篇幅精炼,这为后续按标题分块提供了天然的切分依据。
1.1 必备原料和工具
- 洋葱
- 猪肉片
- 蕃茄酱
- 麻油
1.2 计算(每份用量配比)
食材
| 食材 | 用量 |
|---|---|
| 洋葱 | 一颗(是主角,喜欢吃洋葱可以多半颗~一颗) |
| 猪肉 | 250g |
| 蒜头 | 3 瓣 |
调味料
| 调味料 | 用量 |
|---|---|
| 食用油 | 15ml |
| 黑胡椒 | 1.25g |
| 酱油 | 30ml |
| 糖 | 15g |
| 麻油 | 5ml |
| 番茄酱 | 15ml |
| 料酒 | 15ml |
备注:配方中的小匙、大匙属于体积计量单位,若家中没有标准量勺,可参照常见"茶匙(约 5ml)、大匙(约 15ml)"的换算标准来精准确定用料。
1.3 操作步骤
- 洋葱切片,猪肉切片(或切丝),蒜头拍碎,并将上述调味料混合备用。
- 炒锅内倒入 1 大匙食用油,等待约 10 秒让油温升高后倒入猪肉。
- 炒至猪肉变色后,下蒜头炒香,盛起备用。
- 原锅下洋葱翻炒 3~4 分钟,加入调味料炒匀。
- 下刚盛起备用的猪肉,翻炒至猪肉熟透。
- 猪肉熟后再翻炒 1~2 分钟即可起锅。
1.4 附加内容
- 猪肉可选猪肩肉片或切好的肉丝,按个人喜好决定。
这段原文是后续所有 RAG 环节的事实基础:检索必须能命中它,生成必须忠于它。接下来我们看这份菜谱是如何被「尝尝咸淡 RAG 系统」加工成可检索、可回答的知识单元的。
二、数据准备:一份菜谱如何被加载并打上元数据标签
在 docs/chapter8/02_data_preparation.md 中明确了数据准备模块的设计目标:把原始 Markdown 菜谱文件转化为带丰富元数据的父文档(完整菜谱)与子文档(按标题分割的小块),为"小块检索、大块生成"打基础。对应实现位于 code/C8/rag_modules/data_preparation.py。
2.1 加载:递归发现与父文档 ID
load_documents()通过Path(self.data_path).rglob("*.md")递归扫描数据目录下的所有 Markdown 文件,逐文件读取内容,并为每个父文档分配一个确定性的唯一 ID——基于文件相对数据根目录的路径做 MD5 哈希(而非随机 UUID),这样同一文件在不同构建之间拥有稳定 ID,便于索引复用与父子关系重建:
parent_id = hashlib.md5(relative_path.encode("utf-8")).hexdigest()洋葱炒猪肉这份文档被读取后,即成为一个doc_type="parent"的父文档,source字段指向其文件路径。
2.2 元数据增强:从路径与内容中自动提取
加载完成后,_enhance_metadata()会对每个父文档做三项关键增强,以洋葱炒猪肉为例:
| 元数据字段 | 提取方式 | 洋葱炒猪肉的实际取值 |
|---|---|---|
dish_name | 取文件名 stem | 洋葱炒猪肉 |
category | 在路径中匹配分类目录名 | 荤菜(命中meat_dish) |
difficulty | 用正则★+匹配内容中的连续星号,按数量映射 | 中等(原文为 ★★★) |
分类映射与难度映射在源码中集中定义,便于检索模块复用(见 data_preparation.py 的CATEGORY_MAPPING与DIFFICULTY_LABELS):
CATEGORY_MAPPING = { 'meat_dish': '荤菜', 'vegetable_dish': '素菜', 'soup': '汤品', 'dessert': '甜品', 'breakfast': '早餐', 'staple': '主食', 'aquatic': '水产', 'condiment': '调料', 'drink': '饮品' } difficulty_map = {5: '非常困难', 4: '困难', 3: '中等', 2: '简单', 1: '非常简单'}这份元数据的价值在于:用户提问"推荐一道荤菜""有没有中等难度的下饭菜"时,系统可以从查询中提取过滤条件(main.py中的_extract_filters_from_query()),直接做category/difficulty的元数据过滤检索,而不是把过滤负担全部压给向量相似度。
三、结构分块:洋葱炒猪肉被切成哪五个子块
结构规整的菜谱与第 2 章学过的 Markdown 结构分块思路天然契合(参见 docs/chapter2/05_text_chunking.md)。DataPreparationModule._markdown_header_split()使用 LangChain 的MarkdownHeaderTextSplitter,按三级标题层级切分:
headers_to_split_on = [ ("#", "主标题"), # 菜品名称 ("##", "二级标题"), # 必备原料、计算、操作等 ("###", "三级标题") # 简易版本、复杂版本等 ] markdown_splitter = MarkdownHeaderTextSplitter( headers_to_split_on=headers_to_split_on, strip_headers=False # 保留标题,便于理解上下文 )以洋葱炒猪肉为例,分块结果恰好对应文档的五个语义单元:
洋葱炒猪肉的做法.md(父文档,parent_id=xxx) ├── 子块1:# 洋葱炒猪肉的做法 + 简介 + 预估烹饪难度 ★★★ ├── 子块2:## 必备原料和工具 + 原料清单 ├── 子块3:## 计算 + 食材/调味料用量配比表 ├── 子块4:## 操作 + 六步制作步骤 └── 子块5:## 附加内容 + 猪肉选材建议每个子块都会继承父文档的全部元数据,并额外标注doc_type="child"、chunk_id(随机 UUID)、chunk_index(在父文档中的位置)、parent_id,同时在parent_child_map中登记child_id -> parent_id的映射关系,为后续"由子块回溯父文档"做准备。
分块的效果是检索粒度变细:用户问"洋葱炒猪肉需要什么调味料",向量检索可以精确命中"计算"子块,而不是在整个文档中大海捞针;而strip_headers=False保留了标题,使每个子块都自带语义上下文。
四、小块检索、大块生成:检索去重与父子文档回溯
4.1 为什么要"小块检索,大块生成"
如果直接把整份菜谱作为一个检索单元,用户提问"洋葱炒猪肉要放多少酱油"时,这个具体问题只占全文极小比例,向量检索很可能排不到前面;反之若只检索小块,命中的"计算"子块又缺少"操作"步骤的上下文。因此系统采用混合策略:检索阶段用小子块精确匹配,生成阶段回溯完整父文档保证上下文完整,这正是 docs/chapter8/01_env_architecture.md 中强调的设计核心。
4.2 混合检索与 RRF 重排
code/C8/rag_modules/retrieval_optimization.py 中的RetrievalOptimizationModule同时启动两个检索器:
- 向量检索:基于 BGE 嵌入模型(默认
BAAI/bge-small-zh-v1.5)+ FAISS 向量库做语义相似度召回; - BM25 检索:基于
BM25Retriever做关键词精确召回。
两者结果通过_rrf_rerank()按 Reciprocal Rank Fusion 公式1 / (k + rank + 1)(k=60)融合排序,兼顾语义与词面匹配。例如"洋葱炒猪肉"这类菜名查询,BM25 对菜名关键词高度敏感,而"咸中带甜的下饭菜"这类语义查询则依赖向量召回,RRF 将两者的排名优势合并。
4.3 智能去重:多个子块合并为一份完整菜谱
由于洋葱炒猪肉被切成 5 个子块,用户提问时可能同时命中"计算"与"操作"两个子块。此时DataPreparationModule.get_parent_documents()会:
- 统计每个
parent_id被命中的子块数量作为相关性指标; - 按命中次数降序排列父文档;
- 输出去重后的完整父文档列表,避免把同一道菜重复传给 LLM。
源码实现见 data_preparation.py,其日志输出形如从 2 个子块中找到 1 个去重父文档: 洋葱炒猪肉(2块),直观展示了"多块命中、一份输出"的效果。
五、生成阶段:菜谱如何变成分步骤烹饪指导
检索回溯得到完整父文档后,由 code/C8/rag_modules/generation_integration.py 的GenerationIntegrationModule负责生成回答。main.py中的ask_question()会先通过query_router()把问题路由为三类:
list:推荐/列表类,如"推荐几道荤菜",直接输出菜名列表(generate_list_answer);detail:做法细节类,如"洋葱炒猪肉怎么做",走分步指导模式(generate_step_by_step_answer);general:一般知识类,走基础回答模式(generate_basic_answer)。
以 detail 路由为例,提示词要求模型围绕"菜品介绍 / 所需食材 / 制作步骤 / 制作技巧"灵活组织输出,且明确约束:优先使用原文中的实用技巧,不强行填充无关内容。这意味着 LLM 的回答会严格锚定上文那份真实菜谱——洋葱、250g 猪肉、3 瓣蒜头、调味料配比与六步操作都会原样呈现,而不是模型自由发挥。_build_context()会把父文档连同dish_name、category、difficulty等元数据拼装进上下文(默认最大 2000 字符),保证模型"看到"的是带标签的完整菜谱。
六、端到端运行:配置、依赖与一次真实问答
6.1 关键配置项
系统配置集中在 code/C8/config.py 的RAGConfig数据类中,核心参数如下:
| 配置项 | 默认值 | 作用 |
|---|---|---|
data_path | ../../data/C8/cook | 菜谱数据目录,即洋葱炒猪肉所在的知识库根目录 |
index_save_path | ./vector_index | FAISS 索引持久化路径,二次启动秒级加载 |
embedding_model | BAAI/bge-small-zh-v1.5 | 向量嵌入模型 |
llm_model | kimi-k2-0711-preview | 生成阶段的大模型 |
top_k | 3 | 检索返回子块数量 |
temperature | 0.1 | 生成温度,低值保证回答更贴近原文 |
max_tokens | 2048 | 单次生成最大 token 数 |
6.2 环境与运行
按 docs/chapter8/01_env_architecture.md 的指引,在code/C8目录安装依赖并配置 Kimi API Key(通过环境变量MOONSHOT_API_KEY,源码在main.py初始化时会校验其是否存在):
conda create -n cook-rag-1 python=3.12.7 conda activate cook-rag-1 cd code/C8 pip install -r requirements.txt # 配置 MOONSHOT_API_KEY 后运行 python main.py依赖清单见 code/C8/requirements.txt,包括langchain、langchain-huggingface、langchain-text-splitters、faiss-cpu、rank_bm25、sentence-transformers等。首次运行会经历"加载文档 → 元数据增强 → 结构分块 → 构建 FAISS 索引 → 保存索引"的完整链路,并打印知识库统计(文档总数、文本块数、菜品分类、难度分布);索引已存在时则直接加载缓存,实现秒级启动。
6.3 用这份菜谱验证一次完整问答
进入交互式问答后,提问"洋葱炒猪肉怎么做"会依次触发:
- 查询路由判定为
detail; - 混合检索命中洋葱炒猪肉的"计算""操作"等子块;
get_parent_documents去重回溯出完整父文档;- 分步指导模式生成包含食材清单、六步操作与制作技巧的回答。
如果追问"洋葱炒猪肉要放多少糖",_extract_filters_from_query虽无法提取分类/难度过滤条件,但精确的子块检索仍会命中"计算"章节,直接给出"糖 15g"的用量答案——这正是结构分块 + 小块检索带来的实战收益。
七、小结
以「洋葱炒猪肉」这一份真实菜谱为线索,我们完整走通了 all-in-rag「尝尝咸淡 RAG 系统」从数据到答案的整条链路:结构化 Markdown 文档经过元数据增强(菜名、荤菜分类、中等难度)与 Markdown 三级标题分块(1 份文档 → 5 个子块),在混合检索(向量 + BM25 + RRF)阶段以小粒度精确命中用户意图,再经父子文档映射去重回溯完整菜谱,最后由 LLM 在 detail 路由下生成忠于原文的分步指导。这份菜谱既是厨房里一道 15 分钟的快手菜,也是理解 RAG 数据加工流水线一份难得的"麻雀虽小、五脏俱全"的样本数据。若想深入阅读模块实现,推荐对照 data_preparation.py、retrieval_optimization.py 与 generation_integration.py 三份源码逐行研读。
- 教程
- 人工智能
- 大模型
- RAG
【免费下载链接】all-in-rag
🔍大模型应用开发实战一:RAG 技术全栈指南,在线阅读地址:https://datawhalechina.github.io/all-in-rag/
相关推荐
Hindsight 中 AI Agent 的短期记忆与长期记忆:两层架构、retain/recall 实现与落地评估方法
Hindsight 中 AI Agent 的短期记忆与长期记忆:两层架构、retain/recall 实现与落地评估方法 理解"短期记忆 vs 长期记忆"这一主
教程人工智能大模型RAGall-in-rag 数据工程实战:白灼虾菜谱从 Markdown 到 RAG 知识库的完整解读
all in rag 数据工程实战:白灼虾菜谱从 Markdown 到 RAG 知识库的完整解读 在 Datawhale 的 all in rag 仓库中, c
教程人工智能大模型RAGall-in-rag 实战:冬瓜酿肉菜谱文档的 RAG 全流程解析与制作指南
all in rag 实战:冬瓜酿肉菜谱文档的 RAG 全流程解析与制作指南 在 Datawhale all in rag 项目的第 8 章中,一个以「家常菜食
教程人工智能大模型RAG
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考