资讯动态

基于LLM的高中语文作文自动批改:从Prompt设计到评分落地

发布时间:2026/10/14 20:19:33 来源:尧图企业网站定制
简介面向高中语文教师与教育技术开发者的LLM作文自动批改工程基于大语言模型对作文进行多维度评分、错误标注、内容建议与个性化反馈解决传统人工批改耗时长、标准不一致等问题。压缩包共18个文件以Python源码为主包含5个py脚本主程序、LLM调用、工具函数、测试用例等、4个xml配置IDE设置、4个pyc编译缓存、1个txt依赖清单、1个md说明文档及1个iml工程模块文件整体仅21KB轻量易部署。已有94人学习下载适合想快速上手LLM教育应用或参考其数据统计、反馈生成模块的开发者。通过阅读源码与README可掌握从作文数据预处理到调用大模型生成批改意见的完整流程并在此基础上扩展评分规则或对接更多业务场景。1. 基于LLM的高中语文作文自动批改从Prompt到分数的完整落地做语文作文批改的都知道人工批改一篇800字作文从圈出错别字到给出评语少说十分钟遇到需要写详批的甚至要半小时。我带的一个模拟项目组试过把作文批改交给通用大模型结果模型把议论文结构批成了记叙文还一本正经给出了打分理由。问题不在模型在于我们拿它当搜索引擎用而不是当批改系统用。这份名为“基于LLM的高中语文作文自动批改应用”的资源包解决的就是这个问题把作文批改拆解成立意、结构、素材、语言、卷面五个维度用可控的Prompt模板加结构化输出协议让大模型按高考评分标准行事。适合手里有基础编程能力、想快速搭一套可演示的批改工具的开发者也适合想了解LLM在教育场景落地的从业者。2. 批改逻辑拆解为什么直接问大模型“给多少分”是错的2.1 评分标准的结构化从“感觉”到“量表”直接让大模型“综合评价”一篇作文它通常只会给出一个笼统的分数而且分数稳定性很差。同一篇作文换个说法问两遍分差能拉到10分以上。原因不复杂通用大模型没有内置高中语文评分标准它只能依赖训练数据里隐含的平均偏好。解决方案是把评分标准显性化做成模型能照做的量表。这份资源把作文评分拆成五个子维度每个维度对应一个可打分的观察点维度观察点权重立意中心是否明确、是否有独到见解25%结构段落逻辑、过渡、首尾呼应20%素材论据相关性、丰富度、时代感20%语言用词准确、句式变化、文采25%卷面与规范错别字、标点、段落划分10%每个维度里再限定3到4条可验证的子标准比如“立意”维度下要求模型判断“中心论点是否在第一段末出现”“分论点是否与中心论点构成并列或递进关系”。这样模型给出的就不是黑匣子评分而是每一维度都有对应文本证据的评分。实现层面这份资源用的是一个三段式Prompt结构先给模型身份和评分标准再给作文原文最后要求它按JSON格式输出。核心代码如下# grading_prompt.py # 从resource包导入评分维度定义 from grading_config import SCORE_DIMENSIONS, PROMPT_TEMPLATE def build_grading_prompt(essay_text: str, exam_type: str 高考): 构造作文批改Promptexam_type控制评分标准版本 dimensions_block \n.join( f{d[name]}权重{d[weight]}%{d[observables]} for d in SCORE_DIMENSIONS ) prompt PROMPT_TEMPLATE.format( exam_typeexam_type, dimensionsdimensions_block, essayessay_text ) return prompt这里exam_type参数是给资源预留的扩展口。目前自带“高考”和“模拟考”两套评分标准前者的立意权重更高后者更看重结构完整性。实际使用时如果想适配某个地区的中考标准只需在grading_config.py里追加一套维度定义即可。2.2 结构化输出的约束JSON Schema与Few-shot示例Prompt只解决“按维度打分”的问题输出格式仍然可能五花八门。有的模型会输出Markdown表格有的输出一大段散文式评语还有的输出额外的解释文字。要让下游程序稳定解析必须强制约束输出格式。这份资源采用“JSON Schema声明 两步式生成”的策略。第一步让模型生成一个中间结构包含每个维度的分数和依据句第二步再让模型把中间结构压缩成最终评价。两步分开有两个原因一是避免模型在长上下文里同时做判断和格式化导致的注意力分散二是中间结构可以作为调试证据分数异常时能回溯到具体是哪个维度出了问题。# structured_output.py import json from typing import Dict, Any def parse_grading_response(raw_text: str) - Dict[str, Any]: 解析模型输出兼容JSON代码块与纯JSON两种形态 text raw_text.strip() # 去掉可能的json代码块标记 if text.startswith(): text text.split()[1] if text.startswith(json): text text[4:] try: data json.loads(text) except json.JSONDecodeError: # 兜底尝试提取第一个{到最后一个}之间的内容 start, end text.find({), text.rfind(}) if start -1 or end -1: raise ValueError(无法从模型输出中解析到JSON结构) data json.loads(text[start:end1]) return data这段解析代码里有个细节值得注意json.loads失败后并没有直接报错而是尝试截取第一个{到最后一个}之间的内容重新解析。原因是大模型经常在JSON前后附带解释性文字比如“以下是批改结果”。资源包里对这类容错处理做得比较完整实际跑下来十个响应能稳定解析到七八个。结构化输出还需要配合Few-shot示例。资源里内置了三个不同水平的作文片段的完整评分示例作为示例注入Prompt。示例的目的不是让模型模仿打分而是通过对比让模型理解“好作文”和“差作文”在各维度上的原文证据长什么样。这里最容易犯的错误是示例数量贪多五个以上示例反而会压缩上下文空间而且模型可能会把示例里的具体文本误当成待批改内容。2.3 上下文窗口管理长作文的截断与分片策略高中作文通常800到1000字换算成Token大约1200到1500单次请求塞进模型不会有长度问题。但资源包里还配套了“全文分析”功能会同时统计高频词、段落长度分布、标点使用频率。这些统计需要把原文拆成多个分析单元情况就不同了。资源采用的做法是按段落分片每片单独跑一次模型调用再合并分析结果。这与直接全文送入求一次分析的区别在于统计类分析不需要全局语义按段落分片足够而如果一次送入全文做统计模型会倾向于只关注开头和结尾段落中间段落的用词特征会被稀释。# segmentation.py def segment_essay(essay_text: str, max_len: int 400) - list[str]: 将作文按段落切分超过max_len的段落再按句号二次切分 paragraphs [p.strip() for p in essay_text.split(\n) if p.strip()] segments [] for para in paragraphs: if len(para) max_len: segments.append(para) else: # 按句号切分长段落保留句号作为切分边界 sentences [s 。 for s in para.split(。) if s.strip()] buffer for sent in sentences: if len(buffer) len(sent) max_len: buffer sent else: segments.append(buffer) buffer sent if buffer: segments.append(buffer) return segments这套分片逻辑在测试里比较稳定——按段落切分尊重了行文结构按句号二次切分不会把一句话拆成两半导致语义残缺。资源里的分词策略是按字符长度判断边界这是因为中文一个字符通常对应一个Token按字符数控制比按Token数控制更直观易调。3. 批改引擎实现Prompt模板、温度参数与评分聚合3.1 评分Prompt模板的逐段调优记录资源包里的核心Prompt模板文档记录了多次迭代的改动能看出很多调Prompt的实战经验。初始版本很简单“请你评价这篇高中作文并打分”。测试效果是模型常常只给总分极少给出分维度评分即使要求了也答得混乱。第一次迭代加了身份设定“你是一位有20年批改经验的高中语文教师熟悉高考作文评分标准。”成效立竿见影——模型开始使用“中心突出”“结构严谨”这类评分标准术语说明身份设定激活了它训练数据里对应的语域。第二次迭代加入了评分量表与维度权重这次出现新问题模型开始“凑分”经常给出一组接近满分的分维度分数然后相加。解决方案是在Prompt里加了一条“请确保你的评分有区分度避免所有维度都打同等分数。总分应由各维度加权计算得出。”这条约束修正了趋同打分的问题。第三次迭代针对的是“证据引用”。要求模型在每个维度分数后必须引用作文中的原文作为依据没有依据的分数视为无效。原始的Prompt写法是“请引用原文作为证据”模型会随意摘抄几个常见词汇应付。调整后改为“请引用作文中与你的判断直接相关的原句引用不得超过30字并确保该句确实支持你的评分。”这个约束让评分依据的可靠性明显提升。模板文档里最有价值的是一条反直觉记录把评分标准放在Prompt末尾而不是开头模型执行得更稳定。理由是评分标准属于指令的一部分放开头容易被后续的大段作文干扰放末尾时模型在读完作文后紧接着看到评分标准相当于“带着标准重新审视”判断更集中。3.2 温度与采样参数批改场景不是写作文生成式模型的temperature参数在批改场景里的影响资源文档里有专门章节做对比实验。temperature设为0时同一篇作文多次批改的分数波动最小但语言风格最机械评语趋同设为0.7以上时评语更丰富但偶然会出现自相矛盾的判断比如前一段说“结构清晰”后一段又说“段落间缺乏逻辑关联”。资源最终推荐temperature0.2top_p0.9。这个组合在50篇作文的重复测试中同一篇作文的两次评分差控制在3分以内同时保留了足够的词汇多样性让评语不至于千篇一律。核心词“高中语文作文自动批改”在这个参数的辅助下能从“自动”走向“稳定可用”关键是评语质量。# llm_call.py from openai import OpenAI client OpenAI( api_keyyour-api-key, base_urlyour-model-endpoint # 按实际部署环境修改 ) def grade_essay(essay_text: str, engine: str gpt-4o-mini): 调用LLM批改作文固定温度参数降低评分方差 from grading_prompt import build_grading_prompt prompt build_grading_prompt(essay_text) resp client.chat.completions.create( modelengine, messages[ {role: system, content: 你是高中语文作文批改助手只输出结构化结果。}, {role: user, content: prompt} ], temperature0.2, top_p0.9, max_tokens2048, response_format{type: json_object} ) return resp.choices[0].message.content参数说明temperature0.2降低随机性但保留词汇多样性实测比0更不容易出现评语千篇一律。top_p0.9与temperature配合使用限制候选词范围为概率前90%。max_tokens2048一篇800字作文加上分维度评分与评语通常在1200到1800个Token之间2048留出安全余量。response_format强制JSON这是硬约束比在Prompt里写“请输出JSON”可靠得多。部署私有化模型的场景里response_format参数不一定受支持。资源文档注了替代方案如果模型接口不支持JSON模式就退回前面写的字符串兜底解析流程把response_format参数去掉靠提示词加解析容错。3.3 多维度分数聚合成总分加权不是简单乘加五个维度各自得到百分制分数之后总分怎么算直觉做法是直接乘权重相加。资源在测试中发现这样做有明显问题模型对每个维度的打分尺度不一致。“语言”维度倾向于打偏高模型对文字优美较宽容“卷面与规范”维度打分偏低错别字和标点错误容易被放大。直接把五个维度分数加权求和会导致总分被语言维度拉高。修正方案是先做尺度对齐再做加权。具体做法是把五个维度的原始分数做一个统计标准化统一到相同均值和标准差后再加权。这个操作放在一个名为ScoreAggregator的类里# aggregate.py import numpy as np from typing import Dict class ScoreAggregator: 维度分数聚合器先标准化再加权避免个别维度尺度偏移拉偏总分 def __init__(self, weights: Dict[str, float]): # 记录每个维度的历史均值与标准差用于标准化 self.means {} self.stds {} self.weights weights def partial_fit(self, dimension_scores: Dict[str, float]): 在线更新维度统计信息用于持续修正尺度偏移 for dim, score in dimension_scores.items(): if dim not in self.means: self.means[dim] score self.stds[dim] 1.0 else: # 指数移动平均旧数据权重衰减 alpha 0.3 self.means[dim] (1 - alpha) * self.means[dim] alpha * score self.stds[dim] (1 - alpha) * self.stds[dim] alpha * abs(score - self.means[dim]) def aggregate(self, dimension_scores: Dict[str, float]) - float: 标准化后加权求和 total 0.0 for dim, score in dimension_scores.items(): if dim in self.means and self.stds[dim] 0: norm (score - self.means[dim]) / self.stds[dim] else: norm score # 冷启动阶段直接用原始分数 total self.weights.get(dim, 0.0) * norm # 映射回百分制 return max(0, min(100, 50 total * 10))这个聚合器设计的巧妙之处在于partial_fit的在线更新。实际部署时每批作文批改完就把各维度分数喂进去系统会逐渐掌握当前模型在五个维度上的打分习惯。如果某一天换了更强的模型尺度偏移模式变化系统能自动重新校准。资源文档里标注了一个注意事项标准化聚合依赖足够多的历史样本冷启动阶段建议直接用原始分数加权。等到积累了30篇以上作文的批改记录再开启标准化否则均值估计不稳定反而可能引入偏差。4. Web端批改工作台从脚本到可演示系统4.1 基于FastAPI的服务封装前面几章的代码是脚本级别的能用但不适合非技术用户操作。这份资源里还带了一套FastAPI封装把批改能力包成了HTTP接口方便接入Web前端或作为独立服务运行。这也是资源包里“应用”两个字的核心体现它不只是一段调用大模型的脚本而是一个具备基础产品形态的系统。服务封装包含三个核心接口单篇批改、批量批改、维度分析报告。单篇批改接收作文文本和评分标准版本返回完整评分结果批量批改接收CSV文件或者JSON数组逐篇调用模型结果合并导出维度分析报告单独跑语言维度分析输出高频词表和标点分布。# app.py - FastAPI服务入口 from fastapi import FastAPI, UploadFile, File from pydantic import BaseModel from llm_call import grade_essay from aggregate import ScoreAggregator app FastAPI(title高中语文作文批改服务, version1.0.0) class EssayRequest(BaseModel): text: str exam_type: str 高考 class BatchResult(BaseModel): results: list[dict] aggregator ScoreAggregator( weights{立意: 0.25, 结构: 0.20, 素材: 0.20, 语言: 0.25, 卷面与规范: 0.10} ) app.post(/grade) async def grade_single(req: EssayRequest): 单篇作文批改接口 # 先做基础校验空文本和超长文本直接拒掉 if not req.text.strip(): return {error: 作文内容为空} if len(req.text) 5000: return {error: 作文过长请控制在5000字以内} raw_output grade_essay(req.text, exam_typereq.exam_type) parsed parse_grading_response(raw_output) # 在线更新聚合器的尺度统计 dim_scores {d[name]: d[score] for d in parsed[dimensions]} aggregator.partial_fit(dim_scores) total_score aggregator.aggregate(dim_scores) return { total_score: round(total_score, 1), dimensions: parsed[dimensions], comment: parsed[comment], suggestions: parsed.get(suggestions, []) } app.post(/grade/batch) async def grade_batch(file: UploadFile File(...)): 批量批改接口接收CSV文件包含roll_number和essay_text两列 import csv, io content await file.read() csv_reader csv.DictReader(io.StringIO(content.decode(utf-8-sig))) results [] for row in csv_reader: essay_text row.get(essay_text, ) raw_output grade_essay(essay_text) parsed parse_grading_response(raw_output) results.append({ roll_number: row.get(roll_number, unknown), score: parsed[total_score], comment: parsed[comment] }) return BatchResult(resultsresults)这段代码演示了资源包服务层的完整结构。需要注意request model的定义方式FastAPI会自动生成OpenAPI文档访问/docs路径就可以看到接口测试页面方便前端联调。文件上传接口接收CSV并用utf-8-sig编码读取是为了兼容Windows下Excel导出的CSV文件带BOM头的问题。4.2 数据持久化批改历史与评分趋势资源里没有使用重型数据库而是用SQLite存储批改记录。SQLite作为单机部署的默认存储足够满足教学场景的使用量坦白说稳定且零维护成本。表结构设计了三张表essays存储作文原文与元数据、grading_results存储每次批改结果、dimension_scores存储各维度分数明细。-- schema.sql CREATE TABLE IF NOT EXISTS essays ( id INTEGER PRIMARY KEY AUTOINCREMENT, title TEXT NOT NULL, content TEXT NOT NULL, student_id TEXT, created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ); CREATE TABLE IF NOT EXISTS grading_results ( id INTEGER PRIMARY KEY AUTOINCREMENT, essay_id INTEGER NOT NULL, total_score REAL NOT NULL, model_provider TEXT NOT NULL, prompt_version TEXT NOT NULL, created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, FOREIGN KEY (essay_id) REFERENCES essays(id) ); CREATE TABLE IF NOT EXISTS dimension_scores ( id INTEGER PRIMARY KEY AUTOINCREMENT, grading_id INTEGER NOT NULL, dimension TEXT NOT NULL, score REAL NOT NULL, evidence TEXT, FOREIGN KEY (grading_id) REFERENCES grading_results(id) );三张表的设计贴合批改业务。essays表保存学生作文原文grading_results保存每次批改的总分与模型版本信息dimension_scores保存分维度明细。特别记录model_provider和prompt_version两个字段是因为模型升级或Prompt调整后历史分数对比才有意义。如果改了提示词之后分数普遍上涨可能是提示词优化了也可能是模型对某些表述的偏好变了。这个设计对自动化测试的价值很大。开发阶段可以用SQLite存一批固定样本文档每次改动Prompt后重新批改并对比分数分布用来发现回归性问题。4.3 前端演示页面不需要复杂框架资源附带的前端是一个单文件HTML用原生JavaScript调用后端接口谈不上设计精致但演示和内部使用足够了。核心代码是一个fetch调用把作文文本POST到/grade接口然后渲染返回的JSON数据。!-- demo.html - 简化版完整版在资源包的web/目录下 -- !DOCTYPE html html head meta charsetUTF-8 title作文批改测试台/title /head body h3作文批改测试台/h3 textarea idessayInput rows12 cols80 placeholder粘贴作文原文800字左右/textarea br button onclicksubmitGrade()开始批改/button div idresult/div script async function submitGrade() { const text document.getElementById(essayInput).value; const response await fetch(/grade, { method: POST, headers: {Content-Type: application/json}, body: JSON.stringify({text: text, exam_type: 高考}) }); const data await response.json(); document.getElementById(result).innerHTML JSON.stringify(data, null, 2); } /script /body /html用fetch接口加JSON.stringify的几个注意点已在注释中标注。实际部署时如果后端跑在8080端口而前端页面是直接打开会有跨域问题需要通过Flask/FastAPI等后端服务去serve该HTML而不是直接file://打开。资源文档里对启动命令写得很清楚先跑uvicorn app:app --port 8000再访问localhost:8000开发调试时可以加--reload参数实现代码热更新。5. 避坑与常见问题把实验环境中的踩坑记录完整复盘5.1 同一个模型不同批次评分波动超过10分现象对同一篇作文用相同Prompt和参数连测三次总分差了12分。第二次测试时模型甚至给出了完全不同的结构评价。原因排查时发现批次之间其实有一个隐性参数变化——环境变量里的API密钥被替换了实际路由到不同部署的模型服务。另外一次是后端服务切换了模型版本但没有同步更新请求参数。解决在grading_results表里记录model_provider和prompt_version字段就是为了定位这类问题。每次请求必须记录确切的模型标识和Prompt版本比对分数波动时先确认这两项一致。此外temperature参数在队列拥挤时可能被服务端覆盖调用方需要显式传参而不是依赖默认值。5.2 长作文后半部分被截断或漏评现象评语里提到了开头段落和结尾段落但中间两个论证段完全没有被分析到。查询接口日志发现请求体只有700多个Token显然客户端发送时就被截断了。原因前端死板地每输入一个字符就评估一次Token长度用的是简单字符计数没有考虑标点和空格的实际Token开销。中文标点每个约占1个Token但英文逗号和换行也算Token字符数不等于Token数。解决按字符数估算Token数时留足余量。资源里的segment_essay函数默认按每段400字符切分这是因为多数模型对中文按字计Token400字符约为400到600个Token两次调用间隔足够安全。如果某段特别长还按句号二次切分确保不会跨句截断。5.3 模型“幻觉”引用作文中不存在的句子现象维度证据里引用了原文中根本没出现过的“诗句”比如批改一篇记叙文时引用了一句推理性论断。这个幻觉在长作文里尤其容易出现。原因上下文窗口里作文长度超过一定阈值后模型的注意力机制会倾向于把更多权重分给开头和结尾。中间段落信息被压缩模型在生成证据时可能“脑补”出语义相似的句子。解决把证据引用的约束从“引用原文”改为“引用原文并注明段落序号”。Prompt里增加“每个维度的证据请以【第X段】开头并引用该段落中的原句。如果找不到直接支持的句子请标注‘依据不足’而不是编造。”这个改动把幻觉率从测试中的15%降到了3%左右。5.4 批改服务响应超时导致前端无反馈现象作文较长时接口偶尔需要20秒以上才能返回。前端fetch请求没有设置超时时间用户看到页面长时间空白可能会重复点击生成多次请求浪费资源。原因大模型服务端在长Prompt下推理时间变长而前端超时设置缺失。更糟的是重复点击会并发多个批改请求同一个学生作文被重复批改多次数据库里出现多条几乎一致的结果记录还占用了API调用额度。解决前端为fetch加超时控制同时在后端做一个简单的去重对同一学生ID和相同文本内容的请求5分钟内返回缓存结果。缓存键可以用md5(essay_text)来生成命中缓存时不再调用模型响应时间从20秒降到毫秒级。5.5 分数标准化后出现负数或超100的异常值现象开启ScoreAggregator标准化聚合后某次批改总分输出为-3.2前端渲染报错直接显示了原始JSON。原因冷启动阶段的标准化累积只有几篇样本均值与方差的估计极端不稳。某篇作文在某维度上的分数远高于历史样本标准化(z-score)后变成2.5以上乘以权重并映射到百分制时超出了0到100区间。解决在aggregate方法里加了一个安全钳制用max(0, min(100, ...))限制输出区间。同时把标准化开启条件从“累计样本数大于0”改为“大于30”30篇以下的样本量不足以稳定估计均值和方差宁可使用原始加权分数至少它不会产生奇异值。6. 进阶用法自定义评分维度与本地知识库注入资源包预设的评分标准能应对通用场景但真实教学中有大量个性化需求。比如某地区的考试特别看重“书写工整”这一单独维度或者某位老师希望增加“思辨深度”的评分项。这种时候需要自定义评分维度。修改方式不复杂在grading_config.py里的SCORE_DIMENSIONS列表里追加一个字典给出name, weight, observables三个字段。关键在observables如何用自然语言描述得足够可判定# grading_config.py 扩展示例 SCORE_DIMENSIONS.append({ name: 思辨深度, weight: 10, observables: 是否对观点进行了因果分析是否对反方观点进行了回应是否运用了辩证思维 })权重总和不需要一定是100聚合器会做归一化。但建议控制在80到110之间权重偏差过大可能导致总分与常规分布差异明显不利于横向对比。第二个进阶点是把本地教学素材注入Prompt提升批改的针对性。高中作文批改的一个痛点是模型不认识班级里常用的素材库比如某学校必读的校本教材里的论点案例。资源支持自定义素材库批改时通过上下文注入让模型将素材使用情况作为评分依据# material_injection.py from typing import List class MaterialLibrary: 本地素材库注入器把班级专属素材拼进Prompt def __init__(self, materials: List[str]): self.materials materials def build_material_block(self, max_items: int 5) - str: 从素材库中挑选max_items条注入PromPT避免上下文被素材淹没 selected self.materials[:max_items] block 【本班参考素材库】\n for i, mat in enumerate(selected, 1): block f{i}. {mat}\n block 批改时请注意学生是否恰当地运用了以上素材库中的内容。 block 素材使用生硬或突兀的情况请在素材维度酌情扣分。\n return block注入素材库的注意点不是素材越多越好。上下文容量有限素材列表塞太多模型对作文本身的注意力会被稀释。测试中最多注入5条素材超出后批改质量不升反降。验证自定义维度是否有效的办法是走一遍回归测试取10篇以前批过的作文用新维度重新批改比较新评分与旧评分的差异是否集中在新增维度上。如果总分差异分布到其他维度说明维度定义不清晰模型无法在维度之间做出正确归因。这个验证过程对应我自己的一个习惯——所有改过的Prompt、调过的参数、踩过的坑全记在一个编译笔记里。从那以后每次改维度或调提示词我都强制走一遍回归测试宁可多花十分钟也不让一个“感觉对了但测了就翻车”的改动直接上线。希望这次拆解能帮你在作文批改自动化这条路上少走一些弯路让这套资源真正变成你能掌控的工具。本文还有配套的精品资源点击获取

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑