LLM Zoomcamp 2026 第一模块实战:以课程笔记为知识库,从零构建 Agentic RAG

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 gitsource

LLM 方面,作业推荐使用 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_filterlambda 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。作业给出了两种解决方案:

  1. 自己实现 RAG 流程(完全绕开 FAQ 特定逻辑);
  2. 沿用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_tokens

Q3 验证点:用改造后的 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 的上下文变小了。作业要求量化这个收益:

  1. 对 Q4 的chunks建索引(同样content为 text 字段、filename为 keyword 字段);
  2. 把 RAG 指向 chunk 索引;
  3. 用与 Q3 完全相同的方式读取输入 token,回答同一个查询;
  4. 对比 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),仅供参考