最近这段时间,我一直在想一件事:我们已经很习惯把分析流程写成代码了,但教学流程其实还没有被真正写成代码。

数据读取、预处理、建模、可视化、报告生成,这些环节在 R 里都已经很自然。我们会写函数,会写 package,会把一整套分析步骤做成可复用工作流。可一旦到了“教别人学会这件事”,很多时候流程又退回到了文档、聊天、截图、会议演示和临场发挥。老师脑子里明明有一套非常清楚的教学路径:先看什么,再做什么,哪里容易错,错了该怎么提醒,什么时候给 hint,什么时候应该让学生自己再想一轮;但这套东西,过去通常没有一个很好的承载形式。

我最近刚做了一个新的 R 包,叫 edify。它想做的事情,说大不大,说小也不小:把 lesson、判题规则、提示层级、补救路径和 AI 教练放进同一个 runtime 里,让“教学过程”本身也能被 author、被运行、被复盘。

我现在越来越觉得,这件事真正值得兴奋的地方,不是“又做了一个 AI 教育工具”,而是它试图把一个长期依赖经验和即时互动的过程,收进程序化工作流里。过去我们常说 reproducible analysis;我现在很想试试,能不能也做出 reproducible teaching


这件事为什么会让我兴奋

如果只看表面,edify 好像只是把 Markdown lesson 包了一层 runtime:能写 task、能判题、能给 hint、还能加一点 AI 反馈。这听起来不算特别新鲜。

但如果往下想一层,它其实在补一块很空的接口。

过去我们写教程,通常有几种办法。第一种是写一篇文章或者 notebook,读者顺着看、顺着敲。第二种是做一个 Shiny app 或网页,把一些交互放进去。第三种是直接把学习过程交给聊天式 AI,问一句答一句。它们都各自有价值,但也各自有一个共同问题:教学规则没有被清楚地结构化下来。

文章很适合解释概念,但不太擅长表达“如果学生在第 2 步错在这里,我应该怎么继续引导”。网页可以做交互,但 authoring 成本高。纯聊天式 AI 看起来最灵活,但最容易把“老师原本想怎么教”冲淡,最后变成一个很会说话、但不一定按你预期路径带学生走的系统。

edify 想做的,恰好是把这一层补出来。

它不是先问“模型会不会教”,而是先问:老师能不能先把 lesson 结构写清楚? 题目是什么、标准答案是什么、比较模式是什么、可以给几层 hint、如果学生连续答错,应该怎么补救、如果答对了,下一题是什么。把这些先写下来之后,AI 再接进来。于是 AI 在这里的角色,不是取代 lesson,而是站在 lesson 上说话。

这个顺序我很喜欢。因为它保留了教学意图的主导权。


edify 到底是什么

如果要用一句话概括,我会说:

edify 是一个 lesson-first 的交互学习 runtime。

它的核心不是“生成内容”,而是“运行一节课”。

在这个 package 里,一节课可以写成一个 Markdown 文件。文件前面是 YAML frontmatter,写课程标题、目标、runtime 配置、retries 等;后面是一道道 task。每个 task 可以带:

  • edify-check:这道题怎么判
  • edify-hint:可以给几层提示
  • edify-rubric:对/错/部分正确时,老师想怎么评价
  • edify-remediation:答错后下一步怎么补

也就是说,lesson 不再只是“内容”,而开始变成一个可执行教学对象

这套设计我现在越来越认同。因为很多时候,我们缺的不是再多一段教学文本,而是一个能把“教学过程”稳稳托住的最小 runtime。


我最在意的,不是 AI,而是 lesson-first

虽然 edify 现在已经能接 AI 反馈,但我真正想守住的,其实不是“AI 感”,而是 lesson-first

这一点非常重要。

如果没有 lesson-first,系统就很容易滑向另一种状态:所有交互都交给模型,学生问什么,模型就答什么,哪里卡住了,模型就自由发挥。这当然会很灵活,但也会很快失去教学的一致性。今天一个学生问到这里,模型沿着 A 路线讲;明天另一个学生提了个类似问题,模型可能沿着 B 路线讲。两个回答都不一定错,但它们不再是同一节课。

我更想要的不是这种“无限自由”,而是有边界的互动

老师先写 lesson,先定义 task,先把 hint、rubric、remediation 这些结构定下来;然后 runtime 来执行;最后 AI 只在需要的时候补一层解释、澄清、追问和鼓励。这样整个系统的骨架还是课程作者提供的,而不是模型临场 improvisation。

我觉得这是 edify 和很多“直接接个聊天模型做教学”的思路最不一样的地方。它不是先有 AI,再看怎么塞一点教学逻辑;它是先有教学逻辑,再看 AI 应该站在哪里发力。


这次做出来的 MVP,已经能跑到哪一步

目前这个版本,其实已经不是一个纯概念验证了,而是一个可以真正试用的最小闭环

现在它已经能做这些事:

  • 用 Markdown 写 lesson
  • 校验 lesson 结构
  • 预览课程和教师报告
  • 启动交互式 console 学习循环
  • 支持 expression、object、output 三类判题
  • 支持多层 hint
  • 支持 retries
  • 支持 task 自动推进
  • 支持 session summary
  • 支持 rubric 和 remediation
  • 支持可选的 AI 解释反馈

也就是说,它现在已经不只是“能解析 lesson 文件”,而是真能把一节课跑起来。

下面这个例子基本能说明现在的形态:

library(edify)
 
path <- tempfile(fileext = ".md")
new_lesson(path, template = "two_tasks")
 
learn_lesson(path, student_id = "demo-user", ai_feedback = TRUE)

进入之后,你不是只会看到一堆对象 print 出来,而是会进入一个真正的交互 loop。你可以像学生一样边探索、边试、边问。

比如你可以这样做:

head(mtcars)
/ask 我应该先关注哪些列
/hint
/submit subset(mtcars, mpg > 25)

这里面我现在最满意的一点,是交互语义终于开始像一个学习环境,而不是一个命令行壳子。

普通输入默认是探索代码,会在 session 的独立 workspace 里执行;/submit ... 才是正式交答案;/ask ... 则是直接跟教练互动。这样一来,“探索”和“作答”终于分开了。

这个区分其实非常关键。因为真正的学习过程,本来就不只是提交答案。学生一定会先看数据、先试表达式、先犯错、先问问题。一个把所有输入都当作 submission 的系统,交互一定会很别扭。把这层拆开之后,我才第一次觉得它开始像一个真的 tutor runtime。


我觉得最有意思的,是“错误也进入了教学流程”

最近在调这套交互的时候,有一个点让我印象特别深。

一开始如果用户在 /submit 里写错表达式,比如列名写错、函数名不对,R 会直接把错误抛出来。技术上这当然正常,但从教学体验上看,其实很糟。因为 lesson 被错误打断了,学生只看到了报错,没有看到下一步。

我把出错行为改了:提交错误不会打断 lesson,而是被捕获成一次失败 submission。

现在如果学生写:

/submit filter(mtcars, mpg > 25) |> filter(cly == 4)

系统不会直接退出,而是会返回一个结构化结果:

  • 这次 submission 没通过
  • 错误信息是什么
  • Next steps 应该怎么走
  • 如果开启了 AI,再补一段教练式解释

这个变化意味着错误不再只是系统状态,而是正式进入教学流程。

这其实是我做 edify 时越来越确信的一件事:教学 runtime 不应该只在“答对时”工作,它更应该在“答错时”工作。

很多时候,学生真正需要的并不是“你对了,下一题”,而是“你错在什么地方、下一步应该怎么动、这里是拼写问题、函数问题、数据结构问题,还是根本还没搞清楚题目要求”。如果 runtime 能把这些都接住,AI 才真正有地方发挥价值。


AI 在这里最适合扮演什么角色

我现在越来越不想把 AI 描述成“判题器”。

至少在这个阶段,我更愿意让它扮演三个角色:

第一,解释器
规则判题告诉你过没过,AI 负责把“为什么”说成人能吸收的话。

第二,教练
不是直接给最终答案,而是在学生卡住的时候,顺着 rubric、remediation、错误信息和当前 task,把下一步说清楚。

第三,错误接应层
学生输入了探索代码,或者 /submit 里写错了表达式,这时候 AI 最适合做的,不是“替你做完”,而是帮你判断这更像拼写问题、数据结构问题,还是函数上下文问题,并给一个下一步动作建议。

这个边界我现在还挺满意。因为它保留了一个很重要的分工:

  • 规则负责判定
  • runtime 负责状态
  • AI 负责解释

这样 AI 是强的,但不是失控的。


我为什么会觉得这件事值得继续做下去

说到底,edify 最让我兴奋的,不是它今天已经有多少功能,而是它让我第一次比较清楚地看到了一条路径:

我们也许真的可以把“教学”这件事,往 package 和 runtime 的方向推进。

过去我们已经把很多科研活动都程序化了:分析流程、可视化流程、报告流程、模型部署流程。教学一直比较特殊,因为它有大量人和人之间的即时互动、纠错、追问和引导。但现在有了 LLM 之后,这部分互动第一次有机会被纳入一个相对稳定的 runtime 里。前提是,我们不要一上来就把一切都交给模型,而是先把课程结构写清楚。

我觉得 edify 的意义,如果以后真的能走下去,不会只是“又一个教育包”,而更可能是:它尝试给 lesson、teaching flow、interactive remediation 这些长期很难结构化的对象,找到一个 R 侧的接口。

这件事对我来说很有吸引力。因为一旦接口稳定下来,后面很多东西都会自然长出来:

  • 更丰富的 lesson 模板
  • 更自然的教学 authoring 语法
  • 更好的 tutor-style 错误恢复
  • Shiny 或网页前端
  • session 历史与 learner progression
  • 更稳的 AI coaching policy

这些都还没完全展开,但路径已经比一开始清楚得多了。


现在这版还远远不是终点

当然,edify 现在还只是一个 MVP。

它现在更偏:

  • console-first
  • lesson-first
  • rule-based checking first
  • teacher-authored flow first

它还没有重点做:

  • 图形化 learner 界面
  • 更复杂的多轮 AI session 管理
  • learner 历史持久化
  • 更成熟的课程包生态

但我反而觉得,停在这个阶段是健康的。因为到目前为止,它已经足够让我和别人真正去试,去感受哪些交互是自然的,哪些地方还只是“技术上可行但教学上不顺”。

而这类系统最怕的,其实不是功能不够多,而是没有真实使用反馈就一路加复杂度。


如果我要用一句话来概括 edify

如果让我现在用一句话概括它,我大概会这么说:

edify 想做的,不是让 AI 替老师上课,而是把老师脑子里的教学路径先写成 runtime,再让 AI 在这条路径上接住学生。

我觉得这件事值得继续做。

因为一旦 lesson 可以被 author,teaching flow 可以被 run,错误可以被接住,hint 可以被调度,AI 可以被约束在正确的位置上,教学这件事就第一次不再只是内容分发,而开始有点像一个真正可编程的系统。

这正是我现在最感兴趣的地方。

这个包的源码已经在GitHub上, https://github.com/yulab-smu/edify