
LLM Zoomcamp 2025 模块一实战用 minsearch 与 Elasticsearch 为课程 FAQ 搭建 RAG 问答系统【免费下载链接】llm-zoomcampLLM Zoomcamp - a free online course about real-life applications of LLMs. In 10 weeks you will learn how to build an AI system that answers questions about your knowledge base. Register here 项目地址: https://gitcode.com/GitHub_Trending/ll/llm-zoomcamp本篇基于 LLM Zoomcamp 2025 年课程仓库中 模块一Introduction 的官方文档编写完整复现该模块的教学路径把 DataTalks Club 三个 Zoomcamp 课程的 FAQ 文档解析为结构化数据先用轻量库 minsearch 实现关键词检索并接入 OpenAI 生成答案再将检索层替换为 Elasticsearch最终得到一个可回答“我还能加入课程吗”“怎么运行 Kafka”等问题的 RAGRetrieval-Augmented Generation问答系统。读完本篇你可以独立完成 FAQ 文档解析、索引构建、检索调参boost/filter/multi_match、Prompt 组装与 token 成本估算这一整套 RAG 工程流程。1. 模块目标与 RAG 总体架构模块一的官方目标只有一句话学习 LLM 与 RAG 是什么并实现一个简单的 RAG 流水线用于回答 Zoomcamp 课程 FAQ 文档中的问题。整个模块围绕两件事展开索引三个 Zoomcamp 课程的 FAQ 文档Data Engineering、Machine Learning、MLOps 三个 Google Docs 文档其文档 ID 在 parse-faq.ipynb 中映射为data-engineering-zoomcamp、machine-learning-zoomcamp、mlops-zoomcamp三个课程名构建一个 QA 系统根据用户问题从 FAQ 库中检索相关片段再交给 LLM 基于这些片段作答。从模块的课时编排1.1 介绍 LLM 与 RAG → 1.2 环境准备 → 1.3 检索 → 1.4 OpenAI 生成 → 1.5 代码整理 → 1.6 Elasticsearch可以看出该模块刻意把 RAG 拆成“检索”与“生成”两个可独立替换的组件检索层先用手写搜索库 minsearch 打通流程再换成 Elasticsearch 体会真实搜索引擎生成层则始终是一个“把 prompt 发出去、拿回答案”的薄封装。这个设计也是后续整个课程向量检索、评估、监控等模块的基础骨架。2. 数据基础FAQ 文档如何变成 documents.jsonRAG 的第一环是数据。parse-faq.ipynb 展示了从 Google Docs 到 JSON 的完整解析过程核心逻辑是通过 Google Docs 的导出接口把文档下载为 docx 格式https://docs.google.com/document/d/{file_id}/export?formatdocx用io.BytesIO在内存中构造字节流用python-docx读取段落并按样式名区分语义层级heading 1视为章节标题sectionheading 2视为问题标题question其余普通段落按换行拼接为该问题的答案文本text每遇到新的heading 2就把上一段累积的答案文本落盘为一条记录最终得到{text, section, question}三元组列表再按课程组织写入documents.json。清洗细节上clean_line函数会先strip()再剥离 BOM 字符\uFEFF这是处理 Google Docs 导出文本时常见的脏数据。解析产物 documents.json 的顶层结构是“课程 → 文档列表”的嵌套数组[ { course: data-engineering-zoomcamp, documents: [ { text: The purpose of this document is to capture frequently asked technical questions..., section: General course-related questions, question: Course - When will the course start? } ] } ]在 rag-intro.ipynb 中第一步就是把嵌套结构“拍平”给每条 FAQ 打上课程标签供后续按课程过滤with open(documents.json, rt) as f_in: docs_raw json.load(f_in) documents [] for course_dict in docs_raw: for doc in course_dict[documents]: doc[course] course_dict[course] documents.append(doc)拍平后的记录形如{text: ..., section: General course-related questions, question: Course - When will the course start?, course: data-engineering-zoomcamp}。从作业解答 notebook 的索引进度条可以确认三个课程 FAQ 合计948 条问答记录这个量级也正是本模块选择关键词检索而非向量检索的合理性所在——向量检索是 2025 年课程模块二的主题。同目录下还有一份更小的documents-llm.json是同一 FAQ 集合的精简数据文件。3. 环境准备课时 1.2模块一要求安装的最小依赖集如下注意其中notebook与elasticsearch是锁定了版本的官方如此指定是为了保证 notebook 与 ES 客户端行为一致pip install tqdm notebook7.1.2 openai elasticsearch8.13.0 pandas scikit-learn ipywidgets各依赖的用途从 notebook 实际 import 关系看tqdmipywidgets批量向 ES 写入 948 条文档时显示进度条openai调用 OpenAI Chat Completions API 生成答案elasticsearch连接本地 Elasticsearch 做索引与查询pandas、scikit-learn模块内用于结果统计与展示。文档同时给出替代方案如果没有 pip 环境可以安装 Anaconda 或 Miniconda 后再装上述依赖。检索库 minsearch 需要单独安装见下节。4. 检索用 minsearch 索引 FAQ 并做带过滤、带 boost 的搜索课时 1.3pip install minsearchminsearch 是本课程配套的“自己造搜索轮子”工作产出的搜索库。在 rag-intro.ipynb 中索引的构建只需要声明两类字段import minsearch index minsearch.Index( text_fields[question, text, section], keyword_fields[course] ) index.fit(documents)text_fields中的三个字段会被分词、建倒排索引参与相关度打分keyword_fields中的course字段整体视为一个不可分词的关键词专门用来做精确过滤——相当于 SQL 里的WHERE course data-engineering-zoomcampnotebook 中作者直接用这行注释类比。搜索封装成了带 boost 的函数question字段命中权重 3 倍section只有 0.5 倍体现“问题标题比章节标题更能代表用户意图”的调参思路def search(query): boost {question: 3.0, section: 0.5} results index.search( queryquery, filter_dict{course: data-engineering-zoomcamp}, boost_dictboost, num_results5 ) return resultsfilter_dict只缩小候选集、不参与打分boost_dict则改变各字段命中的相对得分——这两个概念与后文 Elasticsearch 的filter与question^3写法一一对应是模块内刻意保持的“概念平移”。5. 生成调用 OpenAI 并构造 Prompt课时 1.4生成环节分三步初始化客户端、构造 prompt、取回答案。先做一个不带任何上下文的对照组直接把用户问题发给 gpt-4o。对问题 the course has already started, can I still enroll?模型返回的是一段泛泛而谈的“通用建议”去课程平台查政策、联系讲师、确认截止日期等——这正是 RAG 要解决的问题模型并不掌握这门课程的具体规定。from openai import OpenAI client OpenAI() response client.chat.completions.create( modelgpt-4o, messages[{role: user, content: q}] ) response.choices[0].message.content然后把检索结果拼进上下文。注意build_prompt中上下文条目的格式每条检索结果展开为section / question / answer三行条目之间用空行分隔def build_prompt(query, search_results): prompt_template Youre a course teaching assistant. Answer the QUESTION based on the CONTEXT from the FAQ database. Use only the facts from the CONTEXT when answering the QUESTION. QUESTION: {question} CONTEXT: {context} .strip() context for doc in search_results: context context fsection: {doc[section]}\nquestion: {doc[question]}\nanswer: {doc[text]}\n\n prompt prompt_template.format(questionquery, contextcontext).strip() return promptPrompt 模板有两个关键设计一是角色设定“Youre a course teaching assistant”二是事实边界约束“Use only the facts from the CONTEXT”用于抑制模型脱离 FAQ 的自由发挥。生成函数本身是一层薄封装def llm(prompt): response client.chat.completions.create( modelgpt-4o, messages[{role: user, content: prompt}] ) return response.choices[0].message.content5.1 不依赖云服务本地 LLM 与 OpenAI API 替代方案模块文档明确说明如果不想使用托管服务可以本地运行 LLM参见 2024 年课程的开源模块 cohorts/2024/02-open-source/README.md其 “2.7 Ollama - Running LLMs on a CPU” 一节。由于 Ollama 对外暴露 OpenAI 兼容的 API要把 1.4 节的示例改到本地跑只需改几行代码指向 Ollama 的 base_url 与模型名。仓库的 awesome-llms.md 汇总了大量 OpenAI API 替代服务其核心规律是多数服务都暴露 OpenAI 兼容 API通常只需修改base_url、API key 和模型名并按“持续免费额度 / 注册赠送额度 / 纯付费”三类整理本地推理则列出了 Ollama、LM Studio、vLLM 等工具。6. 代码整理把散落的步骤收敛成 rag() 函数课时 1.5前两节写出的代码是“平铺”的搜、拼 prompt、调 LLM 各自独立。课时 1.5 的主题就是去重与模块化最终收敛为三个函数加一个组合函数query how do I run kafka? def rag(query): search_results search(query) prompt build_prompt(query, search_results) answer llm(prompt) return answer整理后的端到端效果均为 notebook 中的真实输出rag(how do I run kafka?)返回分 Java 与 Python 两条路径的运行说明包含java -cp build/libs/...与python -m venv env等具体命令rag(the course has already started, can I still enroll?)返回 “Yes, you can still enroll in the course even after it has started. You are eligible to submit homework assignments, but please be mindful of the deadlines for the final projects...”与 FAQ 原文一致——对照 5.1 节无上下文时的泛泛回答可以直观看到“检索注入上下文”带来的答案差异。rag()就是该模块产出的最小完整 RAG 流水线检索可换 ES→ 提示构造 → 生成。7. 换用 ElasticsearchDocker 启动、索引配置与查询课时 1.6模块的后半段把 minsearch 换成生产级搜索引擎。注意区分两个版本号官方文档给出的演示镜像是8.4.3而模块一作业见第 9 节要求跑8.17.6二者 API 对本模块用到的功能均兼容。7.1 Docker 启动 Elasticsearchdocker run -it \ --rm \ --name elasticsearch \ -m 4GB \ -p 9200:9200 \ -p 9300:9300 \ -e discovery.typesingle-node \ -e xpack.security.enabledfalse \ docker.elastic.co/elasticsearch/elasticsearch:8.4.3参数含义--rm退出即删容器-m 4GB限制内存ES 默认要吃满一半物理内存学习场景应显式限制9200是 HTTP 端口、9300是节点间通信端口discovery.typesingle-node声明单节点模式xpack.security.enabledfalse关闭鉴权方便本地直连。文档同时给出备用命令当出现 “error pulling image configuration” 拉取失败时直接从 Docker Hub 拉取elasticsearch:8.4.3去掉-m 4GB与 Elastic 官方仓库前缀docker run -it \ --rm \ --name elasticsearch \ -p 9200:9200 \ -p 9300:9300 \ -e discovery.typesingle-node \ -e xpack.security.enabledfalse \ elasticsearch:8.4.37.2 建索引settings 与 mappingsfrom elasticsearch import Elasticsearch es_client Elasticsearch(http://localhost:9200) index_settings { settings: { number_of_shards: 1, number_of_replicas: 0 }, mappings: { properties: { text: {type: text}, section: {type: text}, question: {type: text}, course: {type: keyword} } } } index_name course-questions es_client.indices.create(indexindex_name, bodyindex_settings)number_of_shards: 1/number_of_replicas: 0学习场景的单分片无副本配置降低内存占用text/section/question三个字段声明为text类型即会被标准分析器分词并建倒排索引course声明为keyword即不分词、整体存储专供term精确过滤——这与 minsearch 中keyword_fields[course]的角色完全对应。批量写入使用es_client.index()配合 tqdm 显示进度948 条约 30 秒写入完毕from tqdm.auto import tqdm for doc in tqdm(documents): es_client.index(indexindex_name, documentdoc)7.3 查询bool multi_match(best_fields) term 过滤def elastic_search(query): search_query { size: 5, query: { bool: { must: { multi_match: { query: query, fields: [question^3, text, section], type: best_fields } }, filter: { term: { course: data-engineering-zoomcamp } } } } } response es_client.search(indexindex_name, bodysearch_query) result_docs [] for hit in response[hits][hits]: result_docs.append(hit[_source]) return result_docs这段查询与 minsearch 版本逐项对应fields: [question^3, text, section]中的^3就是字段 boost等价于boost_dict{question: 3.0}bool.filter里的term查询等价于filter_dictsize: 5等价于num_results5。elastic_search的返回值刻意保持为纯_source文档列表因此rag(query)函数只需把search替换为elastic_searchprompt 构造与 LLM 调用完全不用改——这正是 1.5 节“检索层可替换”设计的兑现。7.4 multi_match 的几种类型文档特别指出此处使用type: best_fields并把multi_match的完整类型语义整理在 elastic-search.md 中五种类型及适用场景如下该文件均给出 JSON 示例统一以 How do I run docker on Windows? 为例类型行为适用场景best_fields各字段分别打分取最高分只要“至少一个字段匹配得好”即可本模块 FAQ 检索选它most_fields各字段得分累加命中字段越多越相关的场景示例中对question^4做 boostcross_fields把所有字段当作一个大字段打分同一文本的不同表述如同义词跨字段互补phrase按完整短语精确匹配精确短语检索phrase_prefix短语前缀匹配自动补全 / typeahead8. 模块一作业从集群信息到 token 成本核算课时 1.7作业全文见 homework.md官方解答在 homework_solution.ipynb。这里按题序梳理要点与官方答案作业注明“若结果不完全一致选最接近的选项”Q1 跑集群并取版本信息。启动 ES 8.17.6 后执行curl localhost:9200查看version.build_hash。官方解答中该值为dbcbbbd0bc4924cfeb28929dc05d82d662c527b7Lucene 9.12.0。Q2 索引数据。用与视频相同的方式把course设为 keyword、其余字段设为 text向 ES 写入数据的客户端方法是es_client.index(...)选项中insert/put/add均为干扰项。Q3 搜索与打分。查询 How do execute a command on a Kubernetes pod?只查question与text两个字段、questionboost 为 4、type为best_fields看_scoresearch_query { size: 5, query: { bool: { must: { multi_match: { query: query, fields: [question^4, text], type: best_fields } }, } } } search_results es_client.search(indexindex_name, bodysearch_query) search_results[hits][hits][0][_score] # - 44.50556top1 得分为 44.50556对应选项 44.50。Q4 加过滤条件。改问 How do copy a file to a Docker container?filter限定course为machine-learning-zoomcamp返回 3 条。官方解答中三条命中的_score依次约为 73.39、66.69、59.81第 3 条问题是 How do I copy files from a different folder into docker containers working directory?。Q5 组装 prompt。用Q: {question} / A: {text}模板渲染每条命中条目间以两个换行\n\n连接再套入与 5.1 节相同的教学助手 prompt 模板context_template Q: {question} A: {text} .strip() prompt_template Youre a course teaching assistant. Answer the QUESTION based on the CONTEXT from the FAQ database. Use only the facts from the CONTEXT when answering the QUESTION. QUESTION: {question} CONTEXT: {context} .strip() context_pieces [] for hit in search_results[hits][hits]: doc hit[_source] context_pieces.append(context_template.format(**doc)) context \n\n.join(context_pieces) prompt prompt_template.format(questionquery, contextcontext) len(prompt) # - 1446prompt 长度为 1446 字符。Q6 计算 token 数。OpenAI 按 token 计费官方 Python 包内部使用tiktoken做分词import tiktoken encoding tiktoken.encoding_for_model(gpt-4o) len(encoding.encode(prompt)) # - 320 encoding.decode_single_token_bytes(63842) # 单个 token 还原为字节如 b.该 prompt 为 320 tokens约 1446 字符可直观感受英文 FAQ 语料下字符/token ≈ 4.5 的比例。Bonus不计分生成答案与成本核算。把 prompt 发给 OpenAI 看实际回答文档提示可用 Ollama 替代见模块二。成本核算题给定单次请求平均发送 150 tokens、收回 250 tokens跑 1000 次按作业文档中 gpt-4o 的定价输入 $0.005/1K tokens、输出 $0.015/1K tokens计算输入 150,000 tokens 计 $0.75输出 250,000 tokens 计 $3.751000 次请求合计约$4.50定价以作业文档给出的时点为准会随官方价格表变化。9. 延伸阅读与模块内文件索引模块文档还指向几类延伸资料FAQ 解析代码讲解视频README 提到有一段视频专门演示 FAQ 解析代码解析逻辑即本第 2 节所述可直接对照 parse-faq.ipynb 阅读开源 LLM 选学对本地跑模型感兴趣的2024 课程提供了 Ollama 相关视频命令行细节见 cohorts/2024/02-open-source/README.mdOpenAI API 替代服务清单见 awesome-llms.md 的 OpenAI API Alternatives 一节模块内 open-ai-alternatives.md 已迁移指向该文件。本模块在仓库中的文件地图如下全部位于 cohorts/2025/01-intro/ 目录文件作用README.md模块导航六个课时 作业入口本文主体parse-faq.ipynbGoogle Docs FAQ → docx →documents.json的解析流水线documents.json948 条三课程 FAQ 记录模块一主数据集documents-llm.json同集合的较小数据文件rag-intro.ipynbminsearch 索引 OpenAI 生成 ES 检索的完整 notebookelastic-search.mdmulti_match五种类型的语义与 JSON 示例homework.md/homework_solution.ipynbQ1–Q6 作业与官方解答含真实打分与 token 数小结模块一用不到 100 行核心代码搭出了 RAG 的最小闭环——rag(query) elastic_search(query) build_prompt(query, results) llm(prompt)并通过 minsearch → Elasticsearch 的替换演示了检索层的可插拔性。掌握其中的字段类型选择text vs keyword、bool/multi_match/term查询结构与 boost 调参后即可无缝进入 2025 课程的后续模块向量检索、评估等课程目录见 cohorts/2025/README.md。【免费下载链接】llm-zoomcampLLM Zoomcamp - a free online course about real-life applications of LLMs. In 10 weeks you will learn how to build an AI system that answers questions about your knowledge base. Register here 项目地址: https://gitcode.com/GitHub_Trending/ll/llm-zoomcamp创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考