作为助教批作业,题目都是开放性题目,没有标准答案,但也不是那么无迹可寻。既然要评分总得有标准。我发现LLM在阅卷方面确实有前途的,老师人工阅卷是“有状态”的,你很难说ta看到的第一份试卷和第二份试卷之间是否存在时间上的不平等,试卷的答案本身对老师的影响也积攒到下一份试卷。但是LLM是“无状态”的,在公平这一点上就把人类判官远远抛在后面了。
我的工作流是这样设计的:
Step1 格式清洗
1)28个学生交上来28个pdf/docx,通过python统一提取成txt格式。
# 统一提取文本
import PyPDF2
from docx import Document
def extract_text(file_path):
if file_path.endswith('.pdf'):
with open(file_path, 'rb') as f:
reader = PyPDF2.PdfReader(f)
text = '\n'.join([page.extract_text() for page in reader.pages])
elif file_path.endswith('.docx'):
doc = Document(file_path)
text = '\n'.join([para.text for para in doc.paragraphs])
return text
for student_file in student_files:
text = extract_text(student_file)
save_as_txt(text, output_path)
2)题目拆分。每个文档(即试卷)包括三道题目的回答,作业题是4选3,我们需要把每个txt一分为三,同时明确归类到Q1~Q4. 这一步就需要LLM介入了,因为需要根据语义来判断到底是在回答哪一道题。有的同学会把题目原样抄一遍再回答,有的是直接回答。有的还不会明确标出哪一道题,而是直接用自己的序号来标注。所以需要结合LLM来分类、清晰。
prompt = f"""
请分析以下学生作业文本,识别学生回答了哪3道题目,并将答案拆分,删除原始题目,完整保留学生回答的每一个字符,包括标点和参考文献。
可选题目列表:
Q1: RNA-seq局限性分析
Q2: XBP1s非常规剪接机制
Q3: 转座子转录本检测
Q4: IGV测序密度分析
学生作业原文:
{student_text}
请输出JSON格式:
{{
"Q1_RNA-seq局限性": "第一题的回答内容...",
"Q2_XBP1s非常规剪接": "第二题的回答内容...",
"Q3_转座子转录本": "第三题的回答内容..."
}}3) 将“每个题目的每个回答”解析为标准化json,这一步其实逻辑上也和第二步一样,不过拆分出来更容易debug,因为让LLM直接按照给定的json输出的错误率还是有点大,需要很多次重试。干脆拆分成单独的步骤了。
经历了以上三步,就将28个pdf/docx文档转换成了28个格式统一的JSON文件(每个文件包含一个学生的3道题的回答)。
{
"student_name": "张三",
"student_id": "2025311001",
"questions": {
"Q1_RNA-seq局限性": {
"question_title": "题目1_RNA-seq局限性",
"answer": "学生对第一题的完整回答内容..."
},
"Q2_XBP1s非常规剪接": {
"question_title": "题目2_XBP1s非常规剪接",
"answer": "学生对第二题的完整回答内容..."
},
"Q3_转座子转录本": {
"question_title": "题目3_转座子转录本",
"answer": "学生对第三题的完整回答内容..."
}
}
}Step2 制定评分标准
对于一道题目来说,综合学生所有回答(即——将28个学生JSON按题目重组为4个题目JSON,对应Q1~Q4,每个文件包含所有回答该题的学生),结合原始题目要求,明确得分点、扣分点,在JSON中加入rubric字段。
Phase 2输出示例 (按题目重组后的JSON结构,包含所有学生回答+评分标准)
{
"question": "基于二代测序技术的RNA-seq在带来丰富的转录组数据的同时...",
"rubric": {
"total": 100,
"q1": {
"points": 30,
"dimensions": [
{
"name": "覆盖广度",
"weight": 0.4,
"desc": "是否覆盖了建库、测序、比对等不同环节的丢失信息。"
},
{
"name": "机制深度",
"weight": 0.6,
"desc": "是否准确解释了信息丢失的原因。"
}
],
"core_points": [
"低丰度/稀有转录本丢失 (原因:PCR偏好、测序深度不足)",
"特定类型RNA丢失 (原因:PolyA富集策略导致lncRNA/circRNA/组蛋白mRNA丢失)",
"结构/异构体信息丢失 (原因:短读长难以跨越剪接位点/无法复原全长)"
],
"bonus_points": [
"提及RNA修饰信息丢失 (如m6A在常规流程中消失)",
"提及等位基因特异性表达 (ASE) 掩盖"
],
"deductions": [
{"reason": "仅列举名词无解释", "points": -5},
{"reason": "将假阳性信号(Q2内容)错写在Q1", "points": -3}
],
"anchors": {
"excellent": "涵盖3个以上核心点,且对每个点有'原因-后果'的完整描述",
"average": "列出了2-3个点,但描述较为笼统",
"poor": "只提到1个点,或主要在描述测序错误而非信息掩盖"
}
},
"q2": {
"points": 30,
"dimensions": [...],
"core_points": [...],
"...": "第2小题的评分标准,结构同q1"
},
"q3": {
"points": 40,
"dimensions": [...],
"core_points": [...],
"...": "第3小题的评分标准,结构同q1"
}
},
"students": [
{
"student_name": "张三",
"student_id": "2025311001",
"answer": "学生完整回答内容..."
},
{
"student_name": "李四",
"student_id": "2025311002",
"answer": "学生完整回答内容..."
}
]
}Step3 LLM大批量阅卷
到这一步就可以开始阅卷了,对于每一次API call,LLM会收到三部分信息:“原始题目”、“评分标准”、“学生回答”。而且是一个学生的一个题目的回答,完全排除了被其他同学的作业或者被同一份试卷中其他题目干扰的可能性,唯一打分标准就是我们写好的rubric.
def build_grading_prompt(question, rubric, student):
"""
为单次评分构建完整的prompt
输入:
- question: 题目原文
- rubric: 评分标准 (包含dimensions, core_points, deductions等)
- student: 学生信息 (姓名、学号、答案)
输出:
- 格式化的prompt字符串
"""
# 1. 格式化评分标准为可读文本
rubric_text = format_rubric(rubric)
# 2. 组装完整prompt
prompt = f"""你是一位严谨的评分助手。请根据以下评分标准,对学生的答案进行评分。
## 题目
{question}
## 评分标准
{rubric_text}
## 学生答案
学生姓名: {student['student_name']}
学号: {student['student_id']}
答案内容:
{student['answer']}
## 评分要求
1. 严格按照评分标准:每个得分点必须有原文依据
2. 引用学生原话:得分点和扣分点都需要引用学生原文作为证据(关键词即可,不超过20字)
3. 分项给分:对每个子题单独评分
## 输出格式要求
请以严格的JSON格式输出评分结果...
"""
return prompt为了方便人工复核,也需要把输出格式给LLM定死,除了最终答案,也需要给出每小题得分,哪一点加了几分,哪一点扣除了几分。然后还需要一道题的每个小问的comments和整体overall_comments。
单次评分结果的JSON结构
{
"success": true,
"student_id": "2025311001",
"student_name": "张三",
"grading_result": {
"scores": {
"q1": {
"max_score": 30,
"score": 28,
"core_points_hit": [
"低丰度/稀有转录本丢失",
"特定类型RNA丢失",
"结构/异构体信息丢失"
],
"bonus_points_hit": [
"提及RNA修饰信息丢失"
],
"deductions": [
"部分解释不够深入: -2分"
],
"evidence": [
"低丰度转录本:建库中的PCR...",
"非编码RNA的完整序列...",
"转录本结构信息:短读长..."
],
"comments": "回答涵盖了主要知识点,对原因有解释,但部分描述可以更深入。"
},
"q2": {
"max_score": 30,
"score": 27,
"core_points_hit": ["嵌合体/假融合基因", "定量虚高"],
"bonus_points_hit": [],
"deductions": ["-3分:未提及比对错误"],
"evidence": ["嵌合体假象:建库中...", "PCR重复偏差:..."],
"comments": "区分了序列假象和定量偏差,但遗漏了比对环节的分析。"
},
"q3": {
"max_score": 40,
"score": 35,
"core_points_hit": ["针对性明确", "实验手段合理", "提及验证"],
"bonus_points_hit": ["方案细节详实"],
"deductions": ["-5分:缺少完整的分析策略"],
"evidence": ["针对低丰度转录本...", "使用UMI技术...", "qPCR验证"],
"comments": "场景明确,湿实验方案合理,但缺乏系统的生信分析流程。"
}
},
"total_score": 90,
"max_total_score": 100,
"grade_level": "优秀",
"overall_comments": "该生对RNA-seq技术有较好理解,能够准确识别主要问题并提出针对性方案,但部分分析深度可以进一步加强。"
},
"metadata": {
"question_name": "题目1_RNA-seq局限性",
"round": 1,
"timestamp": "2025-12-30T04:34:32",
"model": "glm-4.7"
}
}当然,LLM输出有随机性,偶尔还会犯傻,所以,对于每个回答,我重复了三次独立阅卷,判断结果是否差异过大,差异过大的再人工复核。修正不合理评分后,三次取平均值,作为最终评分。然后三道题的分数总和就是总卷面分。

为了不至于犯太离谱的错误,这次实际上我全部作业也手动看了一遍,按照AI给的分数从高到低过了一遍,几乎是没有错误。虽然用AI写作业被诟病已久,但这次实践让我觉得AI在阅卷方面大有可为,将“公平”这件事情做的比人类好得多。不过我也意识到它的问题,一些我明显感觉是AI写的回答特点就是又全面又精准,会得分很高,人类回答当然答得好的也能得高分,但我总觉得也会引入一些系统性不公正。比如学生可能只写他认为在实践中最重要的一点,而AI阅卷会认为不够全面。这需要我们在制定rubric时就提前预料这种偏差的存在,并在AI阅卷后人工修正。
知识性总结放在:https://fanyiming.life/?p=14317
我还得暗自感谢GLM让我白嫖它的coding plan,这次用的glm-4.7-thinking整个workflow下来包括杂七杂八的重试少说调用了几千次api,要正常按token计费也得大几十块钱了。coding plan本来是局限于编程Agent使用,不过我让claude code测试了几轮发现,只要在请求体前面加几句话伪造成cline就可以走coding plan的流量了,希望官方晚点修复这个漏洞,我现在几乎所有api call都开始白嫖glm了xs。
# API Headers
API_HEADERS = {
"User-Agent": "Cline-VSCode-Extension",
"HTTP-Referer": "https://cline.bot",
"X-Title": "Cline",
"X-Cline-Version": "3.42.0",
"Content-Type": "application/json",
}