AI
自动笔记系统:把课程学习变成可审核的内容流水线
最近我主要在建设 llmlearn。它表面上是一个课程笔记网站,实际上更像是一套面向课程学习的自动笔记系统:从公开课程资源开始,经过字幕整理、逐字稿翻译、Lecture Note 和 Blog 生成,再经过人工审核,最后发布为网站页面和 PDF。
The full English text is not available yet
This article is currently using its Chinese source. Add a matching file under content_en/writing/posts/ to enable the English version.
自动笔记系统:把课程学习变成可审核的内容流水线
最近我主要在建设 llm_learn。它表面上是一个课程笔记网站,实际上更像是一套面向课程学习的自动笔记系统:从公开课程资源开始,经过字幕整理、逐字稿翻译、Lecture Note 和 Blog 生成,再经过人工审核,最后发布为网站页面和 PDF。
这套系统解决的是把学习过程中大量重复、容易遗漏、难以追踪的工作组织成一条稳定流水线。模型负责生成候选内容,系统负责保存来源和状态,我来负责判断内容是否准确、是否值得公开。
一、把学习内容变成结构化数据
传统的课程笔记通常是一组互相独立的 Markdown 文件:一个文件放课程链接,一个文件放字幕,另一个文件放个人总结。文件多了以后,很难回答几个基本问题:这一讲的官方视频在哪里?中文逐字稿是否完成?Blog 是否经过检查?网站为什么展示了某一份内容?
llm_learn 的做法是先给课程和 Lecture 分配稳定的 ID。例如课程使用 cs336-2026,单讲使用 lecture-17。这些 ID 会同时出现在课程目录、文件夹、生成记录、审核记录和网站路由中。
以 CS336 为例,课程元数据集中在:
website/catalog-data/courses/cs336-2026.yaml
每一讲的内容则保存在:
llm/cs336-2026/notes/lecture-17/
├── sources.yaml
├── transcript.en.md
├── transcript.zh-CN.md
├── note.md
├── blog.md
└── review.yaml
这里有一个重要区别:文件存在,并不代表它已经公开。网站是否展示某个 Lecture Note、Blog 或逐字稿,由课程 YAML 里的 outputs 决定。这样,课程资料、候选稿和正式发布内容之间就有了明确边界。CS336 可以作为完整课程工作区公开,而 CS149 目前只发布课程资源和讲次导航,未审核的笔记不会因为文件存在而自动出现在网站上。
二、自动化流水线如何工作
整套流程可以概括为:
官方课程资源
↓
同步课程目录与发现资源
↓
准备字幕和英文逐字稿
↓
生成中文逐字稿、Lecture Note、Blog 候选稿
↓
规则校验与人工审核
↓
登记为公开输出
↓
生成课程网站和 PDF
这条流水线由 workflow/scripts/course.mjs 统一提供命令入口。
1. 同步课程和发现资源
npm run course -- sync cs336-2026
npm run course -- discover cs336-2026
sync 从官方课程页读取课程表,更新 Lecture 的顺序、标题、日期和资源入口,同时保留已经补充过的本地信息。discover 则进一步检查课程主页、视频页、代码仓库和讲义链接,把可能有价值的资源整理到审核清单中。
这一步的意义是先确认“我要学习什么”和“资料来自哪里”,而不是直接让模型面对一堆没有上下文的文本。
2. 准备字幕和逐字稿
npm run course -- prepare cs336-2026 lecture-17
准备阶段会读取视频元数据,并优先选择可用的官方或平台字幕。系统默认先使用字幕,不会因为缺少字幕就自动下载音频。生成的 sources.yaml 会记录视频地址、字幕语言、字幕类型、处理工具和当前状态。
英文逐字稿会保留自然段起始时间戳。字幕中的碎片会被合并成较完整的段落,但不会直接改写成总结文章。这样,后面的翻译和生成内容仍然可以回到原视频核对。
如果需要使用 KrillinAI 处理双语字幕,候选结果会写入当前 Lecture 的 references/krillinai/,包括英文和中文 SRT、Markdown 候选稿以及运行记录。候选文件不会直接覆盖正式逐字稿。
3. 生成候选内容
npm run course -- generate cs336-2026 lecture-17 --provider deepseek --run
生成器以英文逐字稿为主要输入,并在存在时参考本讲的讲义代码。它可以生成三类内容:
- 中文逐字稿:尽量保持原文段落和时间戳;
- Lecture Note:重新组织课程主线、核心概念、公式、误区和复习清单;
- Blog:面向读者重新组织叙事,解释这节课在整个课程中的位置。
这三类内容不是同一种文本。逐字稿追求可回听和可对照,Lecture Note 追求结构化复习,Blog 则允许重新安排论证顺序。系统把它们分开保存,避免为了生成一篇“好读的文章”而丢失原始材料。
三、流水线实际使用的技术
这套系统不是依赖某一个模型或某一个网站服务,而是由几层相对独立的工具组合起来。核心实现集中在 workflow/scripts/,通过 Node.js CLI 把资源处理、内容生成、校验和构建串起来。
| 环节 | 实际技术 | 主要作用 |
|---|---|---|
| 流程编排 | Node.js、ES Modules、npm scripts | 提供 course 命令,统一调用同步、转录、生成、校验和发布流程 |
| 元数据管理 | YAML、yaml npm 包 | 保存课程目录、Lecture 状态、来源、生成记录和公开输出 |
| 视频与字幕 | yt-dlp、Python video-subtitle-transcript 工具 | 读取视频元数据、选择字幕、提取带时间戳的英文逐字稿 |
| 双语转录 | KrillinAI CLI | 生成中英文 SRT 和 Markdown 候选逐字稿,并返回运行清单 |
| 内容生成 | Codex CLI、DeepSeek API | 基于逐字稿和讲义代码生成中文逐字稿、Lecture Note 和 Blog 候选稿 |
| 网站生成 | VitePress、Vue、markdown-it-mathjax3 | 将课程 YAML 和 Markdown 生成课程主页、Lecture 工作区和公式页面 |
| PDF 导出 | Node.js Markdown 转换器、XeLaTeX、TeX Live | 将 Lecture Note 转换为可打印的中文 PDF |
| 持续部署 | GitHub Actions、GitHub Pages | 安装依赖、运行构建,并发布 website/.vitepress/dist |
其中,Node.js 是整个工作流的控制层。package.json 中的 course、site:generate 和 build 脚本把多个步骤组合成可以重复执行的命令;workflow/scripts/course.mjs 负责课程级操作,generate-site.mjs 负责读取 Catalog 生成网站,export-latex.mjs 负责 PDF 输出。
YAML 在这里承担的不是普通配置文件角色,而是轻量级的内容数据库。课程元数据放在 website/catalog-data/courses/,每个 Lecture 还会有 sources.yaml、generation.yaml 或 review.yaml。系统不需要额外部署数据库,就可以通过版本控制保留课程目录、生成状态和审核过程的变化。
字幕处理也被拆成两层:yt-dlp 负责和公开视频资源交互,Python 工具负责字幕提取、清洗和段落合并;需要更完整的双语字幕时,再通过 Node.js adapter 调用 KrillinAI。这样做的好处是,字幕来源、转换工具和最终 Markdown 之间都有记录,不会把“模型生成的文章”误认为“视频原文”。
内容生成层目前支持两种路径:Codex CLI 和 DeepSeek。Codex 路径以只读方式读取仓库中的逐字稿和讲义代码,DeepSeek 路径通过 API 分块处理并保留时间戳标记。两条路径最终都要经过同一套候选稿检查和人工审核,不会因为更换模型就跳过质量门禁。
网站和 PDF 则是同一套源数据的两个输出端。VitePress 负责静态网站,Vue 组件负责课程卡片、Lecture 工作区和资源面板,markdown-it-mathjax3 负责公式渲染;XeLaTeX 负责中文排版和 PDF 编译。GitHub Actions 在 Linux 环境中安装 Node.js 和 XeLaTeX 工具链,运行完全相同的 npm run build,从而减少“本地能看、线上无法构建”的差异。
四、为什么自动生成之后还不能直接发布
自动化最适合处理重复劳动,但它不能自动证明内容正确。一个模型可能翻译得很流畅,却漏掉时间戳;可能写出结构完整的文章,却把讲义没有出现的数字、引用或结论补进去。因此,llm_learn 把审核设计成正式流程的一部分。
npm run course -- validate cs336-2026 lecture-17 --provider deepseek
npm run course -- promote cs336-2026 lecture-17 --provider deepseek --reviewed --overwrite
validate 会检查候选内容是否完整,并执行一些可以自动判断的质量门禁,例如:
- 中英文逐字稿的时间戳数量是否一致;
- 时间戳是否发生倒序;
- Lecture Note 和 Blog 是否错误地包含逐段时间戳;
- 是否出现相邻重复段落;
- 中文候选稿的长度是否异常;
- 文件头和正文结构是否符合约定。
这些检查只能发现“形式上可疑”的问题,不能替代阅读。人工审核仍然需要检查术语、完整性、事实、引用和模型幻觉。确认后,promote 才会把候选稿提升为正式内容,并写入 review.yaml。从这个角度看,review.yaml 不只是一个状态文件,也是内容发布的审计记录。
五、同一份学习内容如何变成网站和 PDF
当内容完成审核后,网站生成器会读取课程 YAML 和正式 Markdown,而不是直接扫描所有文件。运行:
npm run build
构建过程会先执行 LaTeX 导出,再由 workflow/scripts/generate-site.mjs 生成课程页面。生成器会根据 outputs 创建 Lecture 工作区、课程笔记页、Blog 页、双语逐字稿页和课程参考资料页,同时保留从生成页面回到源文件的关系。
这意味着课程内容可以从同一份源数据派生出多个界面:
正式 Markdown + Catalog 元数据
├── VitePress 课程网站
├── Lecture 工作区与 Blog 页面
└── Note 对应的 PDF 导出
网站生成文件位于 website/generated/ 和 website/.vitepress/generated/,它们是构建产物,不应该手动编辑。真正需要修改的是课程 YAML、源 Markdown、审核记录或生成器本身。
结语
我现在更愿意把 llm_learn 理解成一个“学习内容编译器”,而不是一个简单的 Markdown 文件夹:官方资源是输入,逐字稿和候选稿是中间产物,审核记录是质量门禁,课程网站和 PDF 是最终输出。
这套设计的核心不是追求完全自动化,而是让自动化有边界:重复工作交给脚本和模型,来源和过程被记录下来,最终发布仍然需要人的判断。对课程学习来说,这比单纯生成一篇看起来完整的文章更重要,因为真正需要积累的不是某一篇文章,而是一套可以继续使用、检查和扩展的知识生产流程。