简介:面向高中语文教师、教育研究者和LLM应用开发者的高中作文自动批改工具源码包。项目将大语言模型引入作文批改场景,通过深层语义分析识别语法错误、逻辑矛盾与表达不当,并依据高中作文评分标准在结构、逻辑、文采等维度自动评分,同时给出建设性修改意见,能有效减轻人工批改负担。压缩包共18个文件,以Python源码、编译缓存pyc、工程配置、依赖清单与说明文档为主,整体仅21KB,适合快速阅读和二次开发。已有94人学习下载。main.py作为程序入口,llm_func.py封装模型交互与提示词逻辑,utils.py和tasks.py提供工具函数与批改任务调度,test_article.py用于验证效果,README与requirements.txt说明运行环境和依赖,便于复现流程、替换或微调语言模型,并进一步扩展个性化反馈与学情数据分析能力。
1. 用LLM批改高中作文:这不是给作文打分,是给老师的深夜留一条退路
深夜十一点,语文老师刚改完当天的五十四篇周记,圆珠笔油还没干。批改一篇作文,熟练工也要七八分钟,越改到后面越不敢下分数——手抖了,评语糊了。基于LLM的高中语文作文自动批改应用,就是把“先看立意、再看结构、再抠语言、最后给发展等级”这套阅卷人打法,搬进一个zip包:解压、装依赖、启动,把作文贴进去,五分钟内拿回一份带分项得分、扣分依据、升格建议的批阅单。它解决的不是“作文能不能被机器看懂”的哲学问题,而是“老师今晚能不能睡上一觉”的现实问题。这篇笔记写给想自己搭一套的老师、教育产品工程师,以及受够了规则评分“只查错别字不评好坏”的开发者。
2. 落地前先看清全貌:模型怎么选,批改流水线怎么排
2.1 为什么不建议直接用通用问答提示词批作文
很多人拿到LLM第一反应是写一句“请批改下面这篇作文,给出评价和分数”,把作文粘贴在提示词后面。这样确实能出结果,但结果基本不可用。通用问答模型默认“顺着用户说”,你敢贴作文,它就敢夸“结构完整、语言优美、是一篇佳作”,然后给你一个 90 分——这个分数没法给家长交代,因为评语里没有任何一处对应到作文的具体句子。
批改和生成是两种任务。生成要多样性,批改要一致性和可解释性。一个稳定的批改应用,提示词里必须有评分量表(rubric)、扣分规则、输出格式约束,还得告诉模型“不要改稿、不要夸、只评”。规则式批改引擎(关键词匹配、字数统计、句式识别)能抓病句和错字,但判断不了“论证是否深入、材料是否贴切、立意是否新颖”——这些只能靠语义理解。LLM的价值正好在这:它能把多维评分标准装进上下文,给出接近人阅卷分项评分的判断。
2.2 推荐流水线架构:三阶段串联与每阶段的模型选型
我建议把整个应用拆成三个独立阶段,而不是让一次大模型调用干完所有事。
- 预处理阶段:清洗文本、去空行、分段、识别标题和结尾,统计字数。这部分用正则和字符串处理就够了,不占大模型资源。
- 核心评阅阶段:把作文正文 + 评分量表 + 评分要求一起发给LLM,要求逐项打分并给出扣分依据。这是唯一必须用大模型的地方。
- 结果规整阶段:模型输出JSON,做schema校验,字段缺失就自动触发一次“补充评阅”调用,而不是直接让程序崩溃。
模型选型上,线上API优先考虑Qwen、GLM这类中文语料扎实的商用模型;离线部署优先考虑7B~14B量级的开源模型。批改不是“单点能力最强就好”,而是“判断稳定”最重要。你不需要一个文采飞扬的模型,你需要一个每次打分都差不多的模型——这一点在第4章展开。
2.3 用Gradio还是FastAPI:zip应用的两个入口形态
zip交付时,入口形态决定用户怎么启动这个应用。两种我都试过,给你一张对比表。
| 入口形态 | 适用场景 | 启动方式 | 界面复杂度 | 注意点 |
|---|---|---|---|---|
| Gradio | 老师本机试用、教研组小范围跑 | python app.py打开浏览器 | 极低,自带页面 | 并发弱,不适合同时十几个老师用 |
| FastAPI + 前端页面 | 学校机房/服务器部署 | uvicorn main:app启动 | 较高,需要配静态页 | 接口清晰,好对接教务系统 |
新手验收阶段我强烈建议先用Gradio跑通,批量实测三五篇再换FastAPI。Gradio拖慢了批改速度是正常的,它把渲染和推理挤在同一个进程里,别在这阶段怀疑模型能力。
3. 把“好作文”翻译成让LLM听话的判分维度:内容、结构、语言、发展等级
3.1 高考作文评分的四个维度如何映射为大模型指令
高考作文评分标准可以分成内容、结构、语言、发展等级四个维度。直接把这些中文术语丢给LLM不够,模型对“内容”的理解可能是一段抽象赞美。你要把它们翻译成可判别的评分行为。
我在项目里是这样映射的:
- 内容:论点是否集中、论据是否贴切、论证是否有层次。落到prompt里就是“找出中心论点;检查每段材料是否支撑中心论点;判断论证是单纯罗列还是层层推进”。
- 结构:段落推进、过渡衔接、首尾呼应。落到prompt里就是“为每段写一句话概括;检查段间过渡句是否存在;判断结尾是否只重复开头”。
- 语言:句式变化、用词准确度、语病密度。落到prompt里就是“标出高频词;指出句式单一的位置;列出疑似病句,并说明为什么”。
- 发展等级:见解深刻、材料丰富、表达有创意。落到prompt里就是“是否提出超出常规套路的观点;是否使用跨越学科的材料;是否有比喻或意象上的新意”。
映射完成后的指令,模型才知道自己该做什么动作,而不是凭语感打一个总分。你可以先把这份映射存成JSON配置,后面调参只改配置,不改代码。
3.2 分项量表prompt骨架与温度系数设置
以下是我常用的评分指令骨架,你看完可以直接改成自己的。
# prompt_builder.py import json rubric = { "内容": "论点是否集中、论据是否贴切、论证是否有层次", "结构": "段落推进、过渡衔接、首尾呼应", "语言": "句式变化、用词准确度、语病密度", "发展等级": "见解深度、材料丰富度、表达创新性" } system_prompt = ( "你是高考语文阅卷组组长。请根据评分量表,对输入作文逐项评分。" "不要重写作文,不要整体赞美。\n" "评分量表:\n" + json.dumps(rubric, ensure_ascii=False) + "\n" "输出格式:\n" "{\"内容\": {\"得分\": 0, \"扣分依据\": \"\"}, " "\"结构\": {\"得分\": 0, \"扣分依据\": \"\"}, " "\"语言\": {\"得分\": 0, \"扣分依据\": \"\"}, " "\"发展等级\": {\"得分\": 0, \"扣分依据\": \"\"}}" ) def build_prompt(essay: str) -> str: return f"请批改以下作文:\n{essay}"这里的逻辑是:把评分量表放到system层,让模型在“阅卷组长”角色下工作。输出格式限定为JSON,且要求给出扣分依据——这既方便后端解析,又逼着模型做“有依据的判断”。temperature系数我固定设在0.1,top_p设0.7,这两个参数保证每次打分不会因为采样波动差出分数档。如果你用的是第三方API,还要把随机种子固定下来,否则同样的作文每次调用结果都可能不一样。
3.3 分段长文处理的窗口策略:拼接、切片与要点保留
高中作文一般800到1000字,主流模型的上下文窗口装下没问题。但“装得下”不等于“处理得好”,作文越长,答辩越容易开小差。超过1200字的作文,我会先切段再评。
一种常见做法是对半切片:第一遍评上半部分(开头、论点提出、前两个分论点),第二遍评下半部分(后两个分论点、结尾),最后让模型输出整合结果。这比一次性把全文丢进去更能抓住论证结构。还有一种做法是“要点保留”:先用一次调用提取作文的论点、论据、过渡句等骨架信息,再做第二遍评分,把骨架信息附在原文后面作为注意力锚点。两种方法都是为了一个目的:防止模型读到最后忘了开头。
窗口策略要配合输出格式约束。分段评阅时,每个分段都按同样的JSON结构输出,最后在应用层做加权汇总,而不是让模型自己“合并记忆”。
4. 让每次打分一视同仁,也让它能装进zip:一致性设计、自校验与目录交付
4.1 评分漂移的根源:位置偏差、措辞敏感与样例锚定
大模型评分漂移是个真实存在的“玄学”,它不是模型坏了,而是你的指令不够稳定。三个最常见的偏差源:位置偏差、措辞敏感、样例锚定。
位置偏差指模型更容易重点关注开头和结尾,中间段落的优缺点容易被忽略。措辞敏感指prompt里一个字的调整(比如“请批改”和“请审阅”)都可能让分数波动。样例锚定则更隐蔽:如果你在prompt里给了“这是一篇48分范文”,模型会把这篇作文向48分靠拢;给60分范文,它就可能往高档走。
应对办法是:不把范文写进评分提示词,而是把评分的判别标准写成“正例描述/反例描述”。我的做法是构造“评分锚点表”,对应每个分数档位写清楚该档作文长什么样。给模型看“描述性锚点”而不是“完整范文”,能大幅减少锚定效应。
4.2 RAG参考作文库与回归校验:让大模型当自己的改卷组长
要让每次打分“一视同仁”,我还建议加一层参考作文库(RAG知识库)。做法是整理三五十篇历年考场高分作文和典型低分作文,按“内容”“结构”“语言”“发展等级”四个维度做分档标注,向量化后存到本地库里。批改时,先按作文内容检索最相近的3到5个参考案例,把它们的分档标注作为上下文一起送进prompt。这相当于让模型先看看“这类作文往年怎么给分”,再动笔打分。
这一层同时兼顾了“LLM as Judge”的思路:让大模型在参考样例的辅助下做裁判,而不是拍脑袋评分。额外还有一层自校验:评分结果出来后,用独立的一次调用让模型复核“你的扣分依据是否匹配所给分数档位”,如果模型自己都说“过低”或“过高”,就把这次调用标记为低置信度,并转入人工复核队列。
回归校验是整个应用质量的守门员。我固定三篇不同风格的文章作为基线,每次改prompt或换模型版本都先跑一遍这三篇,对比前后分数差。这个思路和“基于LLM的单元测试”类似——大模型应用也得有回归测试,否则一个提示词改动,可能让全班成绩整体往上浮。
4.3 zip目录布局与两条部署路径(含模型挂载)
zip包里的目录布局直接决定用户能不能顺利跑起来。这是我现在用的目录结构:
llm_essay_grader/ ├── app.py # Gradio 入口 ├── main.py # FastAPI 入口 ├── requirements.txt ├── graders/ │ ├── prompt_builder.py # 评分指令构建 │ ├── rubric.json # 评分量表配置 │ └── validator.py # JSON 输出校验 ├── eval/ │ ├── regression_cases/ # 回归基线作文 │ └── anchors/ # 分档锚点描述 ├── storage/ │ ├── ref_vectors/ # 参考作文向量库 │ └── review_history/ # 批改记录存档 └── README.md # 启动说明这里需要注意两件事。第一,模型权重不要和代码打在同一层压缩包里。如果用户要离线部署,我会在README里写清楚“把量化GGUF模型放到models/目录”,图上标注的一个7B量化模型动辄4到5个GB,代码zip只有几十KB;合并打成一个zip,解压出问题用户分不清是代码错还是模型错。第二,requirements.txt务必锁版本。Gradio和FastAPI的大版本更新经常破坏兼容性,不锁版本,半年后用户解压zip装依赖容易直接报错。
不给模型文件的zip,本质上交付的是“应用外壳 + 使用说明”。它的落地路径有两类:一是纯API模式,用户自带大模型API Key,改一个环境变量就能用;二是离线模式,我把量产级别的GGUF模型也放进去,整个包会非常大,但好处是作文数据不出机房,学校更愿意用。
| 部署路径 | 模型来源 | 隐私强度 | 跑通难度 | 适用对象 |
|---|---|---|---|---|
| API模式 | 用户自备Key | 中(作文上云) | 低 | 教研组试用、个人老师 |
| 离线模式 | 包内置GGUF | 高(数据不出本机) | 中 | 学校机房、有合规要求 |
5. 自动批改应用的5个避坑记录:从漏判“发展等级”到幻觉泛滥
5.1 现象一:特征分给得出,发展等级永远打不满
上线第一周,我最常收到的反馈是“内容、结构、语言分得都挺准,发展等级怎么永远只有一半分”。不是那批学生真的缺乏创意,是提示词出了问题——我在评分量表里把“发展等级”排在最后一个,模型给前面三个维度用完注意力后,“发展等级”成了它的注意力盲区。
原因:模型对“列表末尾项”的关注度天然低于开头项,且“发展等级”本身缺乏可操作的判别动作。
解决:把发展等级拆成“创意点识别、素材丰富度、逻辑纵深”三个子问题,单独定义一个函数调用,并配正反例锚点,而不是让模型统一评分。
# grader.py 中新增的一个独立评阅调用 def grade_development(essay: str) -> dict: prompt = ( "只评发展等级,忽略其他维度。\n" "检查三个子项:\n" "1. 是否提出超出常规套路的观点\n" "2. 素材是否跨学科、非陈词滥调\n" "3. 论证是否有递进深度\n" "对每个子项给出是/否及示例原文。" ) # 调用模型,返回结构化评分 return llm_call(prompt, essay)5.2 现象二:同一篇作文换台机器,分数差了4分
教研组里有人用Mac,有人用Windows,同一篇作文分数能差出一个档位。我一开始以为是模型版本不一致,后来查出来是量化模型文件精度不同、推理时的随机种子没固定。
原因:离线部署的GGUF模型量化等级不一致,加上采样参数不固定,导致结果漂移。
解决:在README里规定统一用q4_k_m量化版本,并在代码里固定seed、temperature、top_p。更稳妥的做法是在系统启动时校验模型文件的SHA256,不一致就报错提示,而不是让用户“凑合跑”。
5.3 现象三:长作文批到一半,输出变成“观点总结”
超过1400字的作文,输出经常在三分之一处从“评分”变成“内容摘要”,然后就没了。这不是模型真的“累了”,是上下文过长导致模型把任务理解偏移了。
原因:模型的有效注意力窗口小于标称输入窗口,长文本末尾语义权重过高,模型把整段输入当成“总结任务”。
解决:切成两段评分,然后合并。合并时应用层负责加权平均,不让模型自己记忆前文。实测这样做之后,长作文的评分稳定性明显回升。
5.4 现象四:参考范文一多,LLM开始照着范文“借分”
加入RAG参考库后出现了一个新问题:系统对某些作文明显“手松”,给的评语像在描述参考范文,而不是眼前这篇作文。
原因:检索回来的参考案例是完整范文,模型读了范文全文后,会把其中高分特征“套”到当前作文上,形成幻觉。
解决:向量库里存的不再是范文全文,而是“分档锚点描述”——即“该档作文典型特征”的抽象文本。检索时只给模型看这些描述,不给范文本体,避免模型抄袭范文打分。
5.5 现象五:离线部署的模型幻觉严重,线上API又怕隐私
试用阶段大家都不敢把学生作文传第三方API,怕数据合规上有问题;而本地跑7B模型,又发现它经常编造作文里不存在的句子作为“扣分依据”。
原因:小参数量模型的指令遵循能力和上下文约束力都弱于大参数模型。
解决:我现在的折中方案是本地7B模型做初筛和分项打分,对低置信度样本(比如模型自己觉得“把握不准”的)才走云端API复核。成本不高,隐私影响面也小。更省事的做法是:离线部署也改用14B量化模型,牺牲一部分性能换判断质量。
6. 进阶:用历史作文微调批改口径,以及三分钟回归验证
如果你打算把应用用在期中、期末这种正式阅卷场景,建议你总有一天要走到微调这一步。不是训练模型学会写作,而是训练它默认你所在学校的评分口径——不同学校的语文组,对“深刻”的理解和宽容度并不一致。
常见的做法是整理300到500篇带教研组集体定档分的老作文,拆成训练集和验证集,用LoRA的方式在不改动基础模型全部参数的前提下做低秩微调。我在项目里只微调过一版,效果最明显的变化不是分数更准,而是评语的“口径”更贴近本校老师习惯——原来模型只会说“论证不够深入”,微调后能说出“本文对‘快与慢’只讨论了个人层面,没有上升到时代背景”,这就是阅卷组长的口吻。这个效果,靠改prompt很难做到。
微调之外更通用的进阶能力是回归验证。改任何一条提示词、换任何一版模型、动任何向量库之前,先跑三篇固定作文,对比评分是否仍在预设范围内。我的回归基线文件名叫regression_cases/,内容覆盖三类:一篇48分议论文、一篇40分偏题记叙文、一篇55分有创意的散文。每次改完跑一遍,看分数档位和评语基调解体性有没有变化。
| 回归样例 | 期望得分区间 | 重点关注 |
|---|---|---|
| 48分议论文 | 46~50 | 论点集中,论据偏常规 |
| 40分偏题记叙文 | 38~42 | 语言不错,内容跑偏 |
| 55分创意散文 | 52~57 | 结构松散但表达有亮点 |
这三分钟的时间千万别省。我吃过一次亏:改了一条“请重视语言表达的多样性”的prompt,主观上觉得加强了语言维度,结果回归测试三篇作文的“内容”得分全部掉档。原因是新指令让模型把注意力优先放在了句式上,挤占了内容和论点的判断权重。从那以后我养成了习惯:任何改动先跑回归,不跑就不上生产。
如果你要把这个应用真正推给自己的同事或客户,我最后一条建议是:先拿10篇旧作文跑通全流程,打印出评分单给语文教研组长看,让他挑毛病。你不需要让机器替代老师的判断,你只需要让机器替老师把“每篇作文都改完”这件事做完。希望帮到你。
本文还有配套的精品资源,点击获取