最近这段时间,我一直在想一件事:我们已经很习惯把分析流程写成代码了,但教学流程其实还没有被真正写成代码。
数据读取、预处理、建模、可视化、报告生成,这些环节在 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