LLM Zoomcamp 2026 第一模块实战:以课程笔记为知识库,从零构建 Agentic 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 2026 期第一模块(Agentic RAG)的课后作业 cohorts/2026/01-agentic-rag/homework.md 展开,完整还原作业要求的数据准备、关键词检索、RAG 管线、分块(chunking)与函数调用 Agent 全流程。读者按本文操作,可以独立完成"把课程笔记当成知识库"的 RAG 系统,并把它升级为会自行决定"何时搜索、搜索什么"的智能体,同时掌握输入 token 用量统计与滑动窗口分块这两种关键实战技能。
作业背景:知识库换成课程笔记本身
课程模块本身(cohorts/2026/01-agentic-rag/README.md)讲的是:先用关键词搜索构建一个最小可用的 RAG 管线,再引入函数调用,让 LLM 自己决定何时搜索、搜索什么,从而把固定管线变成 Agent。本作业与模块走的是同一条路径,唯一的区别是知识库:不再是课程 FAQ,而是课程笔记本身。
课程仓库按"期数 + 模块"组织,2026 期一共有七个模块:
01-agentic-rag(Agentic RAG)02-vector-search(向量检索)03-orchestration(编排)04-evaluation(评估)05-monitoring(监控)06-best-practices(最佳实践)07-project-example(项目示例)
每个模块目录下存放着一组带编号的 Markdown 课程页,例如cohorts/2026/01-agentic-rag/01-intro.md、02-environment.md直到16-other-frameworks.md。这些页面就是学员上课时实际阅读的内容,本次作业直接把它们抓下来作为 RAG 的知识库。
注意:作业原文中给出的目录示意是
cohorts/2026/01-agentic-rag/lessons/01-intro.md这样的形态(对应课程仓库的某个历史提交);在当前仓库中,课程页直接存放在各模块目录下。作业通过路径过滤器只保留含/lessons/的 Markdown 文件,意图是"只抓课程页、不抓顶层 README 等无关文件",这个过滤思路在任何目录结构下都成立。
环境准备
作业要求按照模块课程的 02-environment.md 准备环境,核心是:安装uv、初始化项目、添加依赖(requests、minsearch、openai、jupyter、python-dotenv),并把 OpenAI API Key 写入.env文件(同时把.env加入.gitignore)。
在此基础上,本作业多需要一个额外的库:gitsource,它负责从 GitHub 仓库下载文件:
uv add gitsourceLLM 方面,作业推荐使用 OpenAI 的gpt-5.4-mini,但也可以换用任意模型与提供商——只需相应调整 client 与 usage 字段的读取方式。模块课程的 code/pyproject.toml 给出了完整的依赖清单,其中minsearch>=0.0.11、openai>=2.37.0、toyaikit>=0.0.11都是本作业会直接用到的库。
数据准备:用 gitsource 拉取课程页
为了保证所有人使用完全一致的数据,作业固定使用课程仓库的某个提交(8c1834d),并通过gitsource提供的GithubRepositoryDataReader拉取:
from gitsource import GithubRepositoryDataReader reader = GithubRepositoryDataReader( repo_owner="DataTalksClub", repo_name="llm-zoomcamp", commit_id="8c1834d", allowed_extensions={"md"}, filename_filter=lambda path: "/lessons/" in path, ) files = reader.read()各参数的含义与作用:
| 参数 | 值 | 作用 |
|---|---|---|
repo_owner | "DataTalksClub" | GitHub 仓库所属组织 |
repo_name | "llm-zoomcamp" | 要下载的仓库名 |
commit_id | "8c1834d" | 固定提交,确保数据一致 |
allowed_extensions | {"md"} | 只检查 Markdown 文件 |
filename_filter | lambda path: "/lessons/" in path | 只保留课程页,剔除顶层 README 等文件 |
GithubRepositoryDataReader会下载整个仓库并遍历其中所有文件,但由于限定了allowed_extensions={"md"},实际只检查 Markdown;再叠加filename_filter,最终只留下各模块课程页。
files中的每个文件都有parse()方法,返回一个包含filename与content的字典,汇总成文档列表:
documents = [] for file in files: doc = file.parse() documents.append(doc)Q1 验证点:对documents计数,即可得到课程页总数。这也是作业的第一道题,选项为 24 / 72 / 240 / 720。
Q2 索引与检索:minsearch 的 text / keyword 字段
检索是 RAG 的地基。模块课程 05-search.md 详细讲解了 minsearch 的用法:任何搜索引擎本质都是score = sim(query, document)——对每条文档计算相似度、排序、取 Top N。minsearch 是一个纯内存的轻量关键词搜索引擎,无需 Docker,概念与 Elasticsearch / Lucene 一脉相承(text 字段、keyword 字段、boost、filter 都来自 Elasticsearch 的术语)。
关键词字段与文本字段的分工是理解本作业的关键:
- text 字段:被分词(split、lowercase、去停用词)后参与相关性排序,是你"搜什么"的字段;
- keyword 字段:做精确匹配,等价于 SQL 的
WHERE course = '...',用来把搜索空间限制到某个子集,不受排序与 boost 影响。
作业要求用content作为 text 字段、filename作为 keyword 字段来建索引:
from minsearch import Index index = Index( text_fields=["content"], keyword_fields=["filename"], ) index.fit(documents)fit的命名来自 scikit-learn 的惯例——对数据"拟合"一个索引。
随后用下面的查询进行检索(这也是作业的第二道题):
How does the agentic loop keep calling the model until it stops?
search_results = index.search( query, num_results=5, boost_dict={"content": 1.0}, # 只有 content 一个 text 字段时 boost 可省略 )Q2 验证点:查看第一条结果的filename字段,即可从四个候选答案中选出正确项。候选答案涉及cohorts/2026/01-agentic-rag/lessons/03-rag.md、14-agentic-loop.md、04-evaluation/lessons/13-llm-as-judge.md、06-best-practices/lessons/02-hybrid-search.md——检索目标"agentic loop"对应的正是模块课程里的 14-agentic-loop.md 一课。
关于 boost 与 filter:多字段场景下可以通过boost_dict调整字段权重(例如 FAQ 场景里{"question": 3.0, "section": 0.5},问句命中比章节名命中更有信息量);单字段场景下通常不需要 boost。keyword 字段配合filter_dict使用,例如 FAQ 场景按course过滤,保证答案只来自本课程。
Q3 RAG:改造 RAGBase 并读取 token 用量
有了索引,就可以构建 RAG 助手。模块课程准备了两个可复用文件:
- code/ingest.py——数据加载与索引构建
- code/rag_helper.py——RAG 逻辑(search、prompt、LLM)
rag_helper.py中的RAGBase类把index和llm_client作为构造参数注入(而不是模块级全局变量),因此可以传入任意带search方法的索引对象和任意 OpenAI 兼容 client,也便于子类化覆写局部逻辑。核心方法如下:
search(query, num_results=5):委托给index.search,FAQ 场景下使用boost_dict={"question": 3.0, "section": 0.5}与filter_dict={"course": self.course};build_context(search_results):把检索结果格式化成上下文文本;build_prompt(query, search_results):套用PROMPT_TEMPLATE拼出QUESTION: ...+CONTEXT: ...的 prompt;llm(prompt):把developer角色的INSTRUCTIONS与用户 prompt 一起发给llm_client.responses.create(model=self.model, input=input_messages),只返回response.output_text;rag(query):串联 search → build_prompt → llm,返回答案字符串。
问题在于:RAGBase是为 FAQ 的section/question/answer三字段结构写的,而本作业的文档只有filename和content。作业给出了两种解决方案:
- 自己实现 RAG 流程(完全绕开 FAQ 特定逻辑);
- 沿用
RAGBase,只改与 FAQ 结构相关的两处:search(改为查我们的content/filename索引)与build_context(改为直接输出doc['content']而非拼section/Q:/A:)。
推荐方案 2,改动最小。改造后的search与build_context大致如下:
class LessonRAG(RAGBase): def search(self, query, num_results=5): return self.index.search(query, num_results=num_results) def build_context(self, search_results): return "\n\n".join(doc['content'] for doc in search_results).strip()Q3 的核心难点在 token 统计:作业要求回答"给模型发送了多少输入(prompt)token"。选输入 token 而非价格,是因为价格随模型与提供商不同而变,但 prompt 的大小对所有人是一致的。大多数 LLM API 都会在响应对象上暴露 token 用量(如response.usage.input_tokens或prompt_tokens)。
为此需要修改rag_helper.py:
- 把
llm从"只返回文本"改为返回整个响应对象; - 把
rag改为同时返回答案与 usage(可以返回一个 tuple,或定义一个小的 dataclass)。
改造后的示意:
def llm(self, prompt): input_messages = [ {'role': 'developer', 'content': self.instructions}, {'role': 'user', 'content': prompt} ] return self.llm_client.responses.create( model=self.model, input=input_messages ) # 返回整个 response,而不是 response.output_text def rag(self, query): search_results = self.search(query) prompt = self.build_prompt(query, search_results) response = self.llm(prompt) return response.output_text, response.usage.input_tokensQ3 验证点:用改造后的 RAG 回答 "How does the agentic loop keep calling the model until it stops?",打印 usage 中的输入 token 数,与 700 / 7000 / 70000 / 700000 四个选项对比选最接近的。
Q4 分块:chunk_documents 滑动窗口
课程页很长,有的长达数千字符。长文档会让检索变得不精确:页面深处的一次匹配会拖进整个页面作为上下文。常见解法是分块(chunking):把每个页面切成更小的、互相重叠的片段,再对片段建索引。
gitsource提供了现成的chunk_documents辅助函数,底层是滑动窗口算法:
from gitsource import chunk_documents chunks = chunk_documents(documents, size=2000, step=1000)窗口机制(以size=2000、step=1000为例):
- 每个 chunk 是页面文本中一个长度为
size(2000)字符的窗口; - 窗口每次向前滑动
step(1000)字符; - 因为
step < size,相邻 chunk 重叠size - step(1000)字符,跨越边界的段落会在某个 chunk 中保持完整; - 每个 chunk 保留原始字段(如
filename),并新增start(该 chunk 在页面中的起始偏移量)与content(chunk 文本)。
Q4 验证点:统计chunks的数量,与 70 / 295 / 1100 / 4500 四个选项对比。
分块的价值可以在 07-project-example/07-chunking.md 模块课程中找到佐证:长文档切块后,检索粒度从"整页"细化为"页内片段",召回的是真正相关的那一段文本。
Q5 分块后的 RAG:验证 token 下降
分块使每次请求更小,因为发送给 LLM 的上下文变小了。作业要求量化这个收益:
- 对 Q4 的
chunks建索引(同样content为 text 字段、filename为 keyword 字段); - 把 RAG 指向 chunk 索引;
- 用与 Q3 完全相同的方式读取输入 token,回答同一个查询;
- 对比 Q3 与 Q5 的输入 token 数。
Q5 验证点:判断分块版本少用了多少输入 token,选项为"大致相同 / 少 3× / 少 10× / 少 30×"。由于分块后上下文从"整页"缩小为"单个 chunk",输入 token 通常会出现数量级级别的下降。
Q6 转成 Agent:search 工具 + agentic loop
到目前为止,搜索只执行一次,用的还是用户的原话查询。现在把它 agentic:给 LLM 一个search工具,让它自己决定何时搜索、搜索什么。
作业建议使用模块课程 15-frameworks.md 中介绍的教学用小型 Agent 库toyaikit(也可以使用 OpenAI Agents SDK、PydanticAI、LangChain,或自己手写循环):
uv add toyaikit第一步:定义带类型提示与 docstring 的 search 函数
大多数 Agent 框架会读取函数的类型提示(type hint)与 docstring 来自动生成工具 schema(JSON Schema),所以不用手写 schema:
def search(query: str) -> list[dict]: """ Search the course lessons for entries matching the given query. """ return chunk_index.search( query, num_results=5, )这里使用 Q4 的 chunk 索引。函数名、类型提示、docstring 三者共同决定工具对外暴露的形态。
第二步:用 toyaikit 组装 Agent
from toyaikit.llm import OpenAIClient from toyaikit.tools import Tools from toyaikit.chat import IPythonChatInterface from toyaikit.chat.runners import OpenAIResponsesRunner, DisplayingRunnerCallback agent_tools = Tools() agent_tools.add_tool(search) # 不传 schema,让框架从 type hint + docstring 生成 chat_interface = IPythonChatInterface() callback = DisplayingRunnerCallback(chat_interface) runner = OpenAIResponsesRunner( tools=agent_tools, developer_prompt=instructions, chat_interface=chat_interface, llm_client=OpenAIClient(model="gpt-5.4-mini"), )作业给出的 Agent 指令(刻意引导它多搜索几次):
You're a course teaching assistant. Answer the student's question using the search tool. Make multiple searches with different keywords before answering.
然后提问:
How does the agentic loop work, and how is it different from plain RAG?
result = runner.loop(prompt=question, callback=callback)第三步:理解 agentic loop 的内部机制
Agent 背后就是模块课程 14-agentic-loop.md 手写的那个while True循环:模型返回响应 → 若含函数调用则执行并回填结果 → 再次调用模型 → 直到模型返回不再含工具调用的最终答案。Agent 三要素是instructions(developer消息)、tools(search)与memory(不断追加的消息历史)。toyaikit 的 runner 正是封装了这个循环;它的LoopResult还提供all_messages(完整对话历史)、token 数与cost(由 token 用量计算),方便调试多轮 Agent。
Q6 验证点:Agent 会自行决定搜索次数,因此答案会随运行略有波动——统计它调用search的次数,与 0 / 4 / 10 / 20 对比选最接近的(作业用gpt-5.4-mini实测;换模型或提供商,次数可能不同)。
作业要点小结
| 步骤 | 核心知识点 | 验证方式 |
|---|---|---|
| 数据准备 | GithubRepositoryDataReader+filename_filter | 统计课程页数量 |
| Q2 检索 | minsearch text / keyword 字段 | 检查首条结果filename |
| Q3 RAG | 改造RAGBase的search/build_context | 读取 usage 输入 token |
| Q4 分块 | chunk_documents滑动窗口 | 统计 chunk 数量 |
| Q5 分块 RAG | 上下文变小 → token 下降 | 对比 Q3 与 Q5 输入 token |
| Q6 Agent | 函数调用 + agentic loop | 统计search调用次数 |
所有代码与数据都可以在当前仓库中找到依据:环境与依赖见 cohorts/2026/01-agentic-rag/02-environment.md 与 code/pyproject.toml,RAG 辅助类见 code/rag_helper.py,Agent 循环原理见 14-agentic-loop.md,toyaikit 用法见 15-frameworks.md。作业还鼓励"learning in public"(公开分享学习过程),并提供了 LinkedIn / Twitter 的示例文案模板;完成后可在课程平台提交结果,若答案与选项不完全一致,选择最接近的即可。
【免费下载链接】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),仅供参考