
带过四十人以上项目制课程的人大概都经历过同一个噩梦学生代码收上来了却不知道从哪一份开始看。有人少提交一个文件有人把依赖写死在绝对路径里有人压根没跑起来有人跑起来了但是输出格式跟题目要求差三个空格。你往群里喊一声“重交”第二天又有五六个人来问“老师我的环境哪里不对”。我在这套EPGF教学环境里做验收与评分自动化最直接的感受就是这件事不该靠助教肝也不该靠学生自觉它应该是一个确定性的流程。这篇内容写给三类人正在给多个班级或大量学生上机课程做作业验收的助教被“环境不一致”折磨到想写脚本但不知道从哪下手的讲师以及想把作业评判标准从“感觉像对了”变成“有据可查”的课程管理者。EPGF在这里承担两件事验收——确认学生交付的环境和代码真的能跑起来评分——根据既定的规则自动产出可解释的分数。你不一定完整部署这套框架但理解它的设计逻辑能直接改善你自己的教学流程。1. 为什么教学环境的“验收”必须抢在评分之前自动化1.1 手动验收的两个致命问题先说痛点。传统作业批改流程里助教拿到的是一个压缩包但压缩包背后是一个完整的、不可见的环境学生用了什么版本的Python、装没装某个包、代码是在macOS上写的还是Windows上写的、路径是不是写死了。你把压缩包解压到自己机器上第一步不是看代码而是先把环境试出来。一次两次行几十份上百份就完全失控。我见过最高纪录是一个助教一天只批完六份作业剩下的时间全部花在装依赖、调版本、问学生“你这个库在哪装的”。手动验收的第二个致命问题是标准不统一。同一个助教早上面下午面都可能手抖更别说一组助教各批各的。有人说“能跑就行”有人说“格式不对扣五分”还有人说“结果差不多就给过”。这不是评分标准的问题了是验收环节就失效了。你后来发现学生A被扣了十分学生B同样的错误拿满分再想去纠正就要面对一连串“为什么”的追问。所以验收必须自动化而且要抢在评分之前。你只有先让机器回答“这份代码在可控环境里能不能按预期跑完”评分才有讨论的必要。1.2 EPGF 是怎么界定“验收”的EPGF对验收的定义不是“代码不报错”而是三件事同时成立环境可达学生声明的依赖和运行时版本在一个干净的容器里能被正确安装和调用。交付完整作业目录里该有的文件都有没有缺失没有被改名。行为符合预期跑完之后输出内容能通过预先定义的检查点而不是只看有没有输出。这三件事对应三个层次的检查缺一不可。只有环境可达可能代码一跑就崩只有交付完整可能文件都在但内容完全不对只有行为符合可能环境里装了别的东西、代码根本不可复现。EPGF把这三层拆成可独立执行的步骤任何一层失败都会直接阻断后续的评分动作。注意验收和评分是两个环节。验收回答“这个作业有没有资格被评分”评分回答“这份作业值多少分”。在验收没跑通之前就评分等于允许学生在不可控的运气因素上拿分。明白这个区分之后再看EPGF的做法就比较顺理成章了。它会把每次提交的作业放进一个临时环境自动执行环境准备、依赖解析、样例运行、结果比对最后生成一份验收报告。这个报告不是简单告诉你过没过而是告诉你卡在哪一步、原因是权限、依赖、格式还是逻辑。助教只需要看报告就能决定要不要让这个学生重交或者是不是已经可以进入评分节点。2. EPGF 验收自动化的骨架目录规范与配置声明2.1 一个最小可用的 assignment 结构任何自动化都依赖约定。EPGF对作业目录的约定非常朴素基本原则是“让学生少想一步让机器少猜一次”。每个作业在课程仓库里对应一个独立目录结构大概是assignments/ └── assignment-02/ ├── problem.md ├── config.yaml ├── samples/ │ ├── sample1_input.txt │ └── sample1_expected.txt └── tests/ ├── test_basic.py └── test_edge.pyproblem.md是题目说明学生能看到但验收系统不依赖它config.yaml是验收和评分规则的唯一事实来源samples是公开样例学生能自己跑tests是隐藏的判分测试学生看不到。这个设计我们后面解释先说好处助教不再需要给每个学生单独发说明学生提交到固定入口所有东西对齐同一套配置。学生的提交目录也有约定。EPGF要求每个学生一个文件夹文件夹名对应学号或用户名里面就是他们交付的所有内容。验收系统拿到这个目录之后先检查存在的文件是否与config.yaml里声明的required_files一致再决定执行后续流程。这一步看起来简单但非常实用因为“该交的没交齐”在所有缺勤原因里占比最高的。2.2 验收配置的核心字段与判定逻辑config.yaml是这套自动化里最重要的文件它的作用不是写死规则而是把规则变成可见的声明。以一个小作业为例assignment: assignment-02 lang: python3.10 runtime: python3 required_files: - main.py prepare_cmd: pip install -r requirements.txt timeout_sec: 60 checks: - name: Program runs without error cmd: python3 main.py samples/sample1_input.txt expect: exit_code: 0 - name: Produce expected output on sample1 cmd: python3 main.py samples/sample1_input.txt expect: stdout_contains: total42字段的含义都比较直白。lang和runtime声明环境规格required_files做交付完整性检查prepare_cmd是在容器里准备依赖的命令checks是一个个验证点每个检查都可以声明执行的命令和期望结果比如退出码、标准输出、文件生成情况。执行器按顺序读取配置先生成容器再执行prepare_cmd然后逐个执行检查项碰到失败项就记录并停止避免后面的检查全被环境问题带崩。这里我最想提醒的是timeout_sec。很多第一次配置的人会忽略它结果学生代码里有一个死循环执行器挂半小时整个队列都被卡住。EPGF的默认值是60秒但实际配置时要根据作业类型自己定CPU密集型任务给长一点I/O型给短一点宁可写成宽松值也不要让单个任务阻塞整个批次的验收队列。2.3 验收结果为什么要四态而不是“对/错”我见过不少团队把验收做成“红绿判断”跑了就是绿没跑就是红。这是最省事但也是最容易误伤的做法。EPGF把验收结果分成四态PASS、FAIL、WARN、UNKNOWN。这四态各有用途。PASS代表全部检查通过可以直接进入评分流程FAIL代表至少有一个硬性检查不通过比如必备文件缺失、主程序无法启动WARN代表通过了硬性检查但存在可疑情况比如某个边缘用例没生成预期文件、某个输出带了额外日志但主结果正确UNKNOWN代表执行过程本身出了问题比如超时、容器创建失败、依赖安装卡住。为什么要区分后两种状态因为处理动作完全不同。FAIL通常让学生重交或者直接判零分WARN往往可以放行进评分但要在后期随机复核时重点看UNKNOWN则最像“裁判自己出了故障”此时不能判定学生要重新执行或人工排查。四态的价值在于它把“学生的问题”和“流程的问题”分开处理避免把所有异常都归罪于代码。我在这上面吃过亏。有一批作业全部显示FAIL整个班炸了锅结果排查下来是容器镜像更新后某个系统包版本冲突跟学生代码没有任何关系。从那以后EPGF配置里我坚持把环境准备阶段单独作为一项检查任何非代码因素都先记录为UNKNOWN而不是直接归为FAIL。3. 从验收跨到评分EPGF 自动评分流水线怎么搭3.1 测试用例与评分项的映射关系验收过了接下来才是真正给分。很多人以为自动化评分就是“跑一下测试、统计通过率”但教学场景没那么简单。你通常需要分题型、分知识点给分一个测试挂了未必说明学生完全不会可能只是某个边界条件没处理好。EPGF的做法是把评分项和测试用例独立映射。举个实际例子某个作业要求学生实现一个统计函数接口是analyze(data) - dict要求返回总量、均值、最大值三个字段。评分配置可能是这样的scoring: - item: basic_correctness weight: 60 tests: - test_basic.py::test_normal_input - test_basic.py::test_negative_values - item: edge_cases weight: 25 tests: - test_edge.py::test_empty_list - test_edge.py::test_single_element - item: code_style weight: 15 tests: - test_style.py::test_no_global_vars - test_style.py::test_function_docstring每个评分项有自己的权重和关联的测试用例。执行时EPGF会逐项统计小分再乘权重求和。这跟“所有测试等权平均”的最大区别在于它能体现你的教学侧重点。如果你这周重点讲边界条件就把edge_cases权重调高如果重点是代码风格就加大对应项权重。而且因为映射关系是显式的学生问“为什么这道题扣了分”你可以直接指出是哪一项权重扣了哪条而不是给一个笼统的“测试没过”。3.2 动态扣分、权重复核和成绩表生成除了固定比例评分流水线还要考虑动态扣分。EPGF支持“扣分触发器”penalty trigger当检测到特定情况时从总分里扣掉固定分数而不是在权重中体现。举例来说验收阶段的WARN如果被放行到评分阶段可以配置一个penalty比如“未提交README扣5分”。这比把代码功能判错更公平因为它扣的是“交付规范性”不影响学生对功能本身的理解判断。成绩表生成是最后一公里。EPGF会把评分结果、验收结果、测试明细合并成一张成绩表按班级和学号排列每个学生一行。成绩表的每条记录保留三个层级的信息总分、分项得分、测试级日志。总分方便录入系统分项得分方便答疑测试级日志方便复核。这里说一个容易被忽视的配置grade_export_encoding。中文成绩如果导出时编码不对Excel打开全是乱码。我们踩过一次之后统一设置输出为UTF-8带BOMExcel直接识别再用grade.py --version脚本转成CSV或者直接对接课程平台的成绩接口。3.3 一套案例从样例代码到最终分数再给一个完整的小案例。假设一个Python入门作业题目是“统计一段文本中单词出现的次数输出前十个高频词”。学生的实现逻辑可能正确但输出格式多了括号或引号。逐条手工看会很累但评分流水线可以自动判定核心功能和高阶要求的达成情况。配置里basic_correctness权重60测试输入是固定文本、期望输出是“word: count”的严格匹配performance权重20测试用一篇长文本要求执行时间小于3秒code_quality权重20检查是否有函数定义、是否有if __name__入口。所有测试跑完后流水线先汇总各评分项的得分再执行一次“总分配置里的惩罚项”比如文件里包含了绝对路径就扣5分注释里出现拷贝粘贴痕迹扣3分。最后把结果落成表格。这个案例最值得学习的不是测试怎么写而是评分结构要分维度。学生不会因为一个输出空格问题丢掉整道题的分但你也不该因为他功能全对就忽略交付质量问题。多层映射就是为了让这两种情况各自落地不混在一起。4. 助教规模化实操批量处理、异常分类和人工兜底4.1 批量拉取结果与异常分类的日常姿势配置好验收和评分之后助教的日常就变了从“逐个跑学生代码”变成“批量看报告”。EPGF的命令行接口支持一次拉取整个班级的验收与评分结果epgf report --course cs101 --assignment assignment-02 --batch batch-01这条命令会生成一个汇总目录包含三样东西一个总览表格谁过了、谁没过、谁待定、一个按学生分组的详情目录每个人的日志、输出、测试报告以及一个异常摘要所有非PASS状态的结果汇总。助教的工作不是从零开始检查代码而是从异常摘要里做分类。批量异常分类是我的日常。我习惯在拿到摘要后按顺序先看状态FAIL的先看卡在哪个环节是缺文件、装依赖失败、还是输出不符合预期UNKNOWN的先看执行日志确认是不是环境问题WARN的先看警告原因记下来集中在复评环节处理。没有特殊情况的PASS我基本不会打开看除非到了随机复核阶段。4.2 随机复核与公平性兜底机制说到自动化有一个最常见的质疑机器评的分数万一误判怎么办我的回答是机器误判的概率远低于人手误判但你依然需要兜底机制。EPGF里我比较推荐“随机复核”加“分层复核”的组合。随机复核最简单每次作业里抽10%到20%的学生由助教人工重跑验证机器判定是否合理。这相当于给整个流水线做一个抽检抽检覆盖率不需要多高但能有效防止“整个评分规则写错但无人发现”的灾难性情况。我见过一次配置里把expect字段写反导致所有反向结果自动判过直到随机复核才暴露。分层复核更精细对WARN状态的所有结果、对成绩分布极端的样本、对分数波动超过往次作业阈值的学生进行二次检查。EPGF虽然没有内置十分智能的异常检测模型但它会把每个人的历次成绩存进一个本地数据库助教可以用简单的SQL或者脚本找出“上次60分、这次突然100分”之类的可疑样本。提示评分自动化之后人工复核不能省。但人工复核的对象不再是全部作业而是机器主动标记出来的少数样本。这才能实现“规模化”而不仅仅是“自动化”。4.3 规模化以后助教时间都花在哪了我经常被问一个问题用这套东西是不是助教就没事干了我的体验恰恰相反助教工作占比变了活反而更有意义了。自动化之前助教60%的时间在跑环境、调依赖、跟学生对线剩下40%才在看代码质量。自动化之后跑环境和判分交给机器助教的时间重新分配为30%处理异常摘要和人工复核30%深入分析学生典型错误并产出教学反馈20%优化作业题目和测试用例剩下20%和学生答疑。同样是40个学生的课程助教投入总时长下降了一半但给学生的反馈质量和个性程度反而提升了。这背后还有一个隐性收益学生的体验更稳定了。以前学生交完作业等一周都不知道结果现在验收结果几小时内就能自动反馈给他们评分结果一键导出。学生能更快决定要不要重交或者问问题助教也不需要反复处理“老师我交了吗”这种查询。规模化不是把人换成机器而是把机器的确定性注入流程让人的精力集中在判断和反馈上。5. EPGF 验收与评分自动化里的常见坑和排查路径5.1 坑一容器权限导致样例依赖装不完这是我在EPGF里遇到最多的坑而且非常隐蔽。容器启动之后prepare_cmd执行的是pip install -r requirements.txt但很多学生的requirements.txt里包含只允许当前用户安装的包或者设计上就写成需要wheel和系统依赖。在默认的沙箱环境里如果执行用户是一个非root的受限用户安装会在某个包上失败整个验收状态就会变成FAIL。后来我在配置里增加了prepare_as_root: false和install_user: student之类的字段但核心经验不是这个配置项而是排查思路一旦看到大量任务卡在同一个依赖上且报错都是权限相关不要急着怀疑学生先看容器镜像和安装用户。这个教训让我养成了一个习惯每次更新作业配置之前先跑一遍“干净容器演练”把所有步骤在理想环境里过一遍再考虑权限和系统差异。5.2 坑二浮动误差与随机数据让评分结果抖动第二个高频坑是评分结果不稳定。特别是涉及数值计算、统计分析的作业学生代码本身没问题但测试期望值和实际输出因为浮点误差差了0.000001。第一次遇到时全班一半人挂了同一道题但随便挑一个学生的代码手动跑又完全正常。问题出在测试用例的断言方式。EPGF默认支持精确匹配但你可以用正则或者模糊匹配来定义期望。数值类作业我们统一改用范围断言期望值不是42而是[41.9, 42.1]。同时我们会要求测试数据固定随机种子不靠运气跑结果。这个坑的关键教训是测试用例也要像代码一样被审查不然自动化评分会放大人为误差。5.3 坑三并发执行时端口与资源竞争作业里如果一个项目启动了后端服务开一个端口监听如果多个任务同时在一个宿主机上执行端口就互相冲突学生代码本身没问题但验收会报“端口被占用”。EPGF的设计是为每个任务分配独立容器但容器仍然共享宿主机的网络资源。我踩过一次之后统一在验收配置里声明network: none或者针对需要网络的作业设置随机端口映射。更通用的一些做法是所有本地服务型作业优先让代码读取环境变量指定的端口号避免硬编码。注意这个坑不是EPGF特有的任何做批量执行的教学环境都会遇到只是越早把它当成默认规则后面越省事。5.4 一条可复用的排查链路如果你在EPGF里遇到了未知问题我的建议是顺着“日志→容器→配置→数据”这条链路排查而不是直接向学生要代码或让他们换环境。先看执行日志。EPGF会把每个检查点的标准输出、标准错误和退出码都保留下来这些日志通常能直接告诉你这个作业是缺文件还是运行超时还是输出比对失败。日志不解决问题时手动进入容器复现用EPGF的调试模式单独执行这一份作业加--interactive参数进入容器后手动跑一遍学生代码基本能确认是不是代码逻辑问题。再往上就是检查配置本身expect字段是否写反、required_files是否错误地排除了某些合法文件、权重字段是不是被脚本解析出错。最后才追究数据问题输入样例是不是包含空行、文件名是不是用了全角字符、编码是不是不一致。这套链路我推荐直接写在助教手册里。新助教上手EPGF时很容易一看到FAIL就直接私聊学生重交但跟着这个路径走80%的异常在半小时内能定位到真实原因剩下的才需要升级处理。我自己也从“忙得脚不沾地”变成“每天花半小时看一下异常摘要就行”这大概就是我坚持这套做法的理由。EPGF这套教学环境的验收与评分自动化帮我处理了整整两个学期的助教与课程管理工作最有价值的收获是标准一旦变成配置执行就不再需要消耗情绪。机器判定得不准的地方你看日志能找到原因机器判定得准的地方学生也挑不出毛病。如果你也想在自己的教学环境里尝试建议从小处开始先固定一个最小作业的目录结构配上三到五个检查项跑通一次验收再逐步扩展评分维度。后面你再面对几十份作业时你会感谢当时写下第一份config.yaml那个下午的自己。