食用方法:导入文末的工作流 JSON 文件后,按以下步骤操作即可浏览!我的运行环境:win11 + chrome
对于每一位英语老师来说,批改学生作文是一项既重要又耗时的工作。从辨认字迹、理解内容,到对照评分标准、给出修改建议,整个过程充满了重复性劳动。如果能有一位不知疲倦、标准统一的“助教”来分担这项任务,老师们就能将更多精力投入到更具创造性的教学设计中。
今天,我将向您展示如何利用低代码自动化平台 n8n 和强大的生成式AI模型(如 Google Gemini),从零开始搭建一个全自动的英语书面表达批量评分与学情分析工作流。本教程将引述n8n官方文档的设计理念,详细拆解每一个配置步骤,让您即便不是程序员,也能亲手打造一个属于自己的AI助教。
一、 实现原理与所需工具
这个工作流的核心思想是 “OCR + LLM” 的两阶段处理模式:
-
第一阶段:单篇批阅
- OCR (Optical Character Recognition, 光学字符识别): 利用AI的视觉能力,识别学生手写作文图片,将其转换为可编辑的电子文本。
- LLM (Large Language Model, 大语言模型): 扮演“评分老师”的角色,根据预设的评分标准(Prompt)和作文要求,对OCR识别出的文本进行打分、分析,并以结构化的JSON格式输出评分报告。
-
第二阶段:学情分析
- 在完成对全班所有作文的批阅后,系统会自动汇总每份报告中的“扣分点”,再次调用LLM。
- 这一次,LLM将扮演“教研组长”的角色,分析全班学生的共性问题,并生成一份宏观的教学建议报告,帮助老师精准定位教学重点和难点。
所需工具:
- n8n: 一个开源的、可视化的工作流自动化工具,是整个流程的“指挥中心”。官方网站
- Docker: 用于快速、便捷地安装和运行n8n。
- Google Gemini API Key: 我们需要一个AI模型的API密钥来执行OCR和评分任务。您可以从Google AI Studio免费获取。
- Deep seek API Key: 最后调用DeepSeek生成班级总结。
二、 环境准备:用Docker一键安装n8n
第一步:安装 Docker Desktop for Windows
Docker 需要 Windows 的虚拟化功能来运行。现代的 Docker Desktop 会自动通过 WSL 2 (Windows Subsystem for Linux 2) 来实现这一点,这是目前在 Windows 上运行 Docker 的最佳方式。
-
检查系统要求:
- 确保您使用的是 Windows 10 64位(家庭版或专业版,版本 21H2 或更高) 或 Windows 11 64位。
- 确保您的电脑在 BIOS/UEFI 中开启了虚拟化技术。大多数现代电脑默认是开启的。
-
下载 Docker Desktop:
- 打开浏览器,访问 Docker 官方下载页面:https://www.docker/products/docker-desktop/
- 点击 “Download for Windows” 按钮,下载安装程序
Docker Desktop Installer.exe
。
-
运行安装程序:
-
双击下载的
.exe
文件。-
小技巧:docker默认安装在C盘,你可以导航到安装目录下,使用下面的命令将其安装在指定目录(下面的示例目录为
E:\DockerDesktop
)start /w "" "Docker Desktop Installer.exe" install --installation-dir=E:\DockerDesktop
安装到指定目录后,启动因为用户对目录的访问权限问题会出错,只需对
.vhdx
授予当前用户访问的权限即可。
-
-
在安装界面,请务必勾选 “Use WSL 2 instead of Hyper-V (recommended)”。这个选项通常是默认选中的。
-
点击 “Ok” 或 “Install”,然后等待安装过程完成。这个过程可能会下载一些额外的组件,请保持网络连接。
-
-
重启电脑:
- 安装完成后,Docker Desktop可能 会提示您重启电脑。
第二步:验证 Docker 安装是否成功
重启电脑后,Docker Desktop 应该会自动启动(如果没启动,可以从开始菜单手动打开它)。桌面上可能会出现一个教程窗口,您可以先关掉它。
现在,我们需要验证 Docker 是否已经准备就绪。
-
打开终端:
- 点击“开始”菜单,输入
PowerShell
并打开 Windows PowerShell。(使用命令提示符
/CMD
也可以)。
- 点击“开始”菜单,输入
-
检查 Docker 版本:
- 在 PowerShell 窗口中,输入以下命令并按回车:
docker --version
- 如果您看到类似
Docker version 25.0.3, build 4debf41
的输出,说明 Docker 客户端已经安装成功。
- 在 PowerShell 窗口中,输入以下命令并按回车:
第三步:准备本地文件夹(用于数据持久化)
我们的目标命令中包含两个 -v
参数,它们用于将您电脑上的文件夹“链接”到容器内部。在运行命令之前,我们需要确保这些文件夹真实存在。
-
打开文件资源管理器,导航到 E盘。
-
创建第一个文件夹(用于存放n8n的工作流和配置数据):
- 在 E 盘根目录下,创建一个名为
n8n_data
的文件夹。 - 进入
n8n_data
文件夹,在里面再创建一个名为.n8n
的文件夹。 - 最终的完整路径应该是:
E:\n8n_data\.n8n
- 在 E 盘根目录下,创建一个名为
-
创建第二个文件夹(用于存放作文图片和评分报告):
- 回到 E 盘根目录下,创建一个名为
lianxi
的文件夹。 - 进入
lianxi
文件夹,在里面再创建一个名为images
的文件夹。 - 最终的完整路径应该是:
E:\lianxi\images
- 回到 E 盘根目录下,创建一个名为
说明:
E:\n8n_data\.n8n
这个文件夹是给 n8n “自己” 用的,它会把你的所有设置和工作流保存在这里。E:\lianxi\images
这个文件夹是给 “你” 和 n8n “交互” 用的。你会把作文图片放进去,n8n 会从这里读取,然后把生成的评分报告也写回到这里。
第四步:执行最终的 n8n 启动命令
现在,万事俱备,我们可以运行那条最终的命令了。
-
回到您之前打开的 PowerShell 窗口。
-
复制并粘贴以下完整命令,然后按回车:
docker run -d --name my-n8n-container -p 5678:5678 -v E:\n8n_data\.n8n:/home/node/.n8n -v E:\lianxi\images:/home/node/files n8nio/n8n:latest
-
分析执行过程:
- 首次运行,Docker 会因为本地没有
n8nio/n8n:latest
镜像而开始自动下载。您会看到几行下载进度条。请耐心等待,镜像文件有几百MB。 - 下载完成后,命令会立即执行。因为我们使用了
-d
(后台模式),它不会显示 n8n 的运行日志,而是会直接输出一长串由字母和数字组成的 容器ID。 - 看到这个长长的ID,就代表 n8n 容器已经成功在后台启动了!
- 首次运行,Docker 会因为本地没有
第五步:访问并开始使用 n8n
-
检查容器状态 (可选但推荐):
- 在 PowerShell 中输入以下命令,查看正在运行的容器列表:
docker ps
- 您应该能看到一个名为
my-n8n-container
的容器,并且它的STATUS
显示为Up ...
。
- 在 PowerShell 中输入以下命令,查看正在运行的容器列表:
-
打开 n8n Web 界面:
- 打开您的 Chrome 或 Edge 浏览器。
- 在地址栏输入:
http://localhost:5678
并访问。 - 稍等片刻,您应该就能看到 n8n 的首次设置界面了,它会请您设置一个所有者账户。
恭喜您!您已经成功在 Windows 上部署了一个生产级的、数据持久化的 n8n 服务。 现在,您可以开始在 Web 界面上创建或导入您的自动批阅工作流了。您放在 E:\lianxi\images
文件夹里的任何图片,在工作流中都可以通过 /home/node/files
路径被访问到。
三、 工作流节点深度解析
接下为,逐一拆解这个工作流中的每一个节点。根据n8n的数据结构文档,每个节点都会接收来自上游的数据项(Items),处理后,再将结果传递给下游。
Part 1: 个人作文批阅循环
这个部分是工作流的主体,它会对每一张作文图片执行一次完整的批阅流程。
1. 节点: On form submission
(表单触发)
- 节点类型:
n8n-nodes-base.formTrigger
- 作用: 作为工作流的起点。它会生成一个独特的Webhook URL,构成一个简单的在线表单。老师在执行任务前,先通过此表单提交本次批阅的通用信息。
- 配置详解:
Form Title
: “输入作文要求” - 表单的标题。Form Description
: “输入作文要求和上传作文” - 对表单的描述。Form Fields
: 这里定义了两个输入框。fieldLabel
:composition_requires
- 用于输入本次作文的具体题目和要求。fieldLabel
:General_scoring_requirements
- 用于输入总的评分原则或注意事项。Required Field
: 均已勾选,确保老师不会忘记填写。
2. 节点: Read/Write Files from Disk
(读取作文图片)
-
节点类型:
n8n-nodes-base.readWriteFile
-
作用: 读取我们映射到Docker容器中的所有学生作文图片。
-
配置详解:
- Operation: Read File(s) From Disk 从磁盘上读取文件。
File Selector
:/home/node/files/*.png
- 使用通配符*
匹配/home/node/files/
目录下的所有.png
文件。
-
注意事项:
- 路径关键点: 此处的路径是n8n 容器内部 的路径。它能够访问到您本地的文件,完全得益于Docker启动命令中的
-v
卷映射。这是新手最容易出错的地方。 - 确保您的作文图片是
.png
格式,或修改为对应的文件格式(如.jpg
)。
- 路径关键点: 此处的路径是n8n 容器内部 的路径。它能够访问到您本地的文件,完全得益于Docker启动命令中的
3. 节点: Loop Over Items
(循环处理)
- 节点类型:
n8n-nodes-base.splitInBatches
- 作用: 这是一个流程控制节点。上一步可能一次性读取了30张图片(30个Items),此节点会将这批数据拆分,让后续的节点对每一张图片(每一个Item)单独执行一次。
- 配置详解:
Batch Size
:1
- 这是默认设置,意为“一次处理一个Item”。
- 注意事项:
- 根据n8n的循环文档,这个节点是实现对多项数据逐一处理的基础。在它的后面连接的节点会自动形成一个循环,直到所有分批后的数据都处理完毕。
4. 节点: Convert Image to Base64 (Image)
(图片转码)
- 节点类型:
n8n-nodes-base.extractFromFile
- 作用: Gemini Vision API不直接接收图片文件,而是接收Base64编码的字符串。此节点负责完成这个转换。
- 配置详解:
Operation
:Binary to Property
- 选择将二进制数据转换并存为一个属性。Input Binary Field
:data
- 这是n8n中表示文件二进制数据的默认属性名。我们使用表达式data
来指定它。Destination Output Field
:base64EncodedImage
- 转换后的Base64字符串将被存放在名为base64EncodedImage
的新JSON字段中。
- 注意事项:
- 确保上游节点(
Read/Write Files
)确实输出了包含data
属性的二进制数据项。
- 确保上游节点(
5. 节点: HTTP Request (OCR from Image)
(调用模型进行OCR)
- 节点类型:
n8n-nodes-base.httpRequest
- 作用: 第一次调用Google Gemini API,执行OCR任务。
- 配置详解:
Method
:POST
URL
:https://generativelanguage.googleapis/v1beta/models/gemini-pro-vision:generateContent?key=...
- 这是Gemini Vision模型的API端点。Authentication
:None
Send Body
:选中Specify Body
:Using JSON
JSON Body
: 包含了发送给API的请求体。text
: “请只识别图片中…” - 这是给AI的指令(Prompt),告诉它识别什么,忽略什么。data
:{{ $json.base64EncodedImage }}
- 关键引用。这里通过{{...}}
表达式,动态地将上一步生成的Base64字符串嵌入到请求中。
- 说明:
- 使用n8n的Credential设置Gemini api key,没有HTTP Request 灵活!。在后续班级评估报告使用Deep seek 在Credential中创建中。
6. 节点: 准备post模型的json格式数据
(Code)
- 节点类型:
n8n-nodes-base.code
- 作用: 堪称工作流的“大脑”,通过JavaScript动态生成一个结构精密的指令(Prompt),用于指导AI进行评分。
- 配置详解 (代码逻辑):
const compositionRequires = $('On form submission')...
- 使用$('NodeName').first().json.fieldName
的语法,从“On form submission”节点获取老师输入的作文要求。这是n8n跨节点引用数据的强大功能。const ocrText = $input.first().json...
- 获取上一步OCR节点返回的识别文本。const promptText = \
…`` - 使用模板字符串,将角色设定、评分标准、作文题目、学生作文等所有信息,组合成一个完整的、逻辑严密的Prompt。- 输出格式约束: 在Prompt中,通过一个JSON示例,强制要求AI以固定的JSON格式返回评分结果。这是保证后续节点能稳定解析数据的核心。
- 模型参数: 在
generationConfig
中将temperature
设为0.0
,消除AI的随机性,保证评分的稳定性。(这个很关键哦!!!) return { requestBody: requestBody };
- 将构建好的整个API请求体作为输出,传递给下一个节点。
- 注意事项:
- 这个节点的代码质量直接决定了AI评分的质量。任何对评分标准、输出格式的调整,都应在此处修改。
- 开始在评分节点中直接准备
post
的json
数据,因为引用变量的缘故,json
数据难以通过,后来采用code
先准备post
给模型的数据,然后直接引用就ok了。
7. 节点: 调用模型评分
(HTTP Request)
-
作用: 第二次调用Gemini API,将上一节点精心准备的评分Prompt发送出去。
-
配置详解:
JSON Body
:={{ $json.requestBody}}
- 使用表达式,将上一个Code节点返回的整个requestBody
对象作为本次请求的内容。
8. 节点: 清除响应中markdown标记
(Code)
- 作用: AI返回的JSON数据有时会被Markdown的代码块(```json… ```)包裹。此节点负责清理这些格式,解析出纯净的JSON对象,并将其重新组织成一份人类易读的文本报告。
- 配置详解 (代码逻辑):
rawGeminiOutputText.match(/```json\\n([\\s\\S]*)\\n```/)
- 使用正则表达式提取JSON字符串。JSON.parse(...)
- 将JSON字符串转换为JavaScript对象。try...catch
- 容错处理。如果AI返回的不是有效的JSON,工作流不会崩溃,而是会输出错误信息,便于排查。const fullReport = \
…`` - 将解析后的JSON对象中的各个字段(如分数、优点、缺点)格式化成一份完整的、带标题和分隔线的报告文本。
- 注意事项:
- 这是保证最终产出文件美观、可读的关键一步。您可以根据自己的喜好调整报告的格式。
9. 节点: 保存为txt文件Convert to File
& 将评分结果写入文件
Read/Write Files from Disk
- 作用: 将上一步生成的报告文本,以独立文件的形式保存下来。
- 配置详解:
ConvertToFile
节点: 将文本字符串转换为n8n内部的二进制文件格式。ReadWriteFile
节点:Operation
:Write File to Disk
File Path and NameFile Path and Name
:/home/node/files/{{ $('Read/Write Files from Disk').item.json.fileName.split(".")[0]}}_评分结果.txt
这是一个动态文件名的绝佳示例。它引用了最初读取文件节点的原始文件名,去掉.png
后缀,然后拼接上_评分结果.txt
,为每份报告创建了唯一的、可关联的文件名。
- 注意事项:
- 确保Docker的卷映射正确,并且n8n有权限写入该目录。
Part 2: 班级学情汇总分析
当上述循环对所有图片都执行完毕后,工作流的第二条分支将自动启动。
10. 节点: 重新读取所有评分报告
& 解码报告文本
- 作用: 流程在这里稍作等待,直到所有个人报告(
.txt
文件)都生成完毕。然后,它会读取所有这些报告文件,并将内容解码为文本流,准备进行汇总。 - 注意事项:
- 这两个节点的执行时机很关键,它必须在循环之后。n8n的执行顺序逻辑确保了这一点。
11. 节点: 汇总班级扣分点
(Code)
- 作用: 对全班的错误进行归纳。
- 配置详解 (代码逻辑):
const allInputItems = $input.all();
- 使用$input.all()
获取上游节点传来的所有(全班的)报告数据。for (const item of allInputItems)
- 遍历每一份报告。fileContent.match(/【扣分点】\\n([\\s\\S]*?)\\n【得分依据】/)
- 再次使用正则表达式,但这次的目标是从每份报告中精确提取“扣分点”部分的内容。allStudentDeductionPoints.push(...)
- 将提取到的扣分点添加到一个数组中。summarizedDeductionsForDeepSeek = ...
- 将数组中所有学生的扣分点合并成一个长字符串,并用分隔符隔开。
- 注意事项:
- 这个节点的健壮性取决于前面报告生成的格式一致性。如果“【扣分点】”等关键字发生变化,此处的正则表达式也需要同步更新。
12. 节点: 准备班级总结请求
& 调用...生成班级总结
- 作用: 构建最终的“教研组长”Prompt,并调用AI生成全局性的教学建议。
- 配置详解:
准备班级总结请求
(Code):role: "system"
: 设定AI为“经验丰富的中学英语教师”。role: "user"
: 给出详细指令,要求AI基于汇总的扣分点,分析共性问题,并提出教学建议。将上一步汇总的summarizedDeductions
字符串填入其中。
调用...生成班级总结
(HTTP Request): 发送这个“终极”Prompt。
- 注意事项:
- 在
准备班级总结请求
节点中,模型被指定为deepseek-chat
,但在调用...生成班级总结
节点中,URL仍指向Gemini。这是一个命名与实际调用的不一致。在实际使用中,您应确保节点名称、请求体中的模型名称(如果需要)和请求的URL是匹配的。为了保持一致性,我们假设这里仍然使用Gemini。
- 在
13. 节点: 将内容转为二进制Convert to File
& 写入班级总结文件Read/Write Files from Disk
- 作用: 将AI生成的班级教学总结报告,保存为最终的
班级英语作文教学总结.txt
文件。 - 配置详解:
写入班级总结文件
节点的File Name
被硬编码为固定文件名,因为每次执行只生成一份总结报告。
- 注意事项:
- 至此,整个工作流执行完毕。您可以在映射的本地文件夹中找到所有学生的个人评分报告和一份总的班级教学总结。
四、 可能性与生产实践
这个工作流不仅仅是一个酷炫的演示,它揭示了AI赋能教育的巨大潜力,但在实际应用中也需要注意一些问题。
无限的可能性:
- 跨学科应用: 同样的逻辑可以轻松迁移到语文作文、历史问答题、甚至代码作业的初步批阅上。
- 个性化学习路径: 可以根据每个学生的“扣分点”报告,自动生成个性化的练习题或知识点复习建议。
- 数据可视化: 将每次批阅的得分、常见错误类型等数据存入数据库(如PostgreSQL),再通过BI工具(如Metabase)进行可视化,实现对学生、班级学情的长期追踪和深度洞察。
- 多模态反馈: 不仅生成文本报告,还可以调用TTS(Text-to-Speech)服务,为每个学生生成一段语音反馈,让反馈更加亲切。
生产实践的考量:
- Prompt是核心资产: 整个系统的效果高度依赖于Prompt的质量。需要不断迭代优化Prompt,使其更贴合教学场景,指令更清晰,输出格式更稳定。
- 人机协同(Human-in-the-Loop): AI目前是“助教”,而非“主考官”。对于最终成绩的评定,尤其是高风险的考试,AI的评分结果应作为教师的参考,由教师进行最终审核和确认。AI的价值在于处理80%的重复性工作,让教师聚焦于20%的需要人类智慧和情感关怀的环节。
- 成本与效率: API调用是收费的。在批量处理时,需要关注API的响应时间和费用。可以选择在夜间等空闲时间执行工作流,并选用性价比更高的模型(如工作流中使用的gemini-2.5-flash)。
- 错误处理与监控: 在生产环境中,需要为API调用失败、文件读写错误、数据格式异常等情况添加完善的错误处理逻辑(例如,失败后重试、发送邮件通知管理员等)。
结语
我们通过n8n和Gemini,并遵循n8n官方文档的最佳实践,成功地将一个繁琐的教学任务转变为一个高效、智能、且稳健的自动化流程。这不仅极大地解放了教师的生产力,更重要的是,它通过数据驱动的方式,为精准教学提供了前所未有的可能。
技术的浪潮奔涌而来,与其被动接受,不如主动拥抱。希望这份超详细的指南能够启发您,去探索更多AI与自动化技术在您工作领域中的应用,让技术真正成为我们能力的延伸。现在,就动手搭建属于你自己的AI助教吧!
参考作文
在第一个节点On form submission
中输入
- 作文要求
composition_requires
第二节书面表达(共1题,满分 15 分)
假如你是王丽,你校英语网站正在开展以“你坚持做的一件事”为主题的征文活动。请你根据以下提示和要求,用英语写一篇短文,向校刊投稿。
提示:1. What did you stick to doing?<
2. Why did you do it?
3.How did you feel?
要求:1.语义通顺,行文连贯;
2.词数 80左右,文章格式已给出,不计入总词数。
Dear Sir/ Madam,
Yours,
Wang Li
- 老师评卷时给出的“心"中的评分标准
General_scoring_requirements
:
依据作文要求的提示,要点一What did you stick to doing?和要点三.How did you feel?各二分,
要点二why did you do it给六分,字数字迹语法五分,字数多不扣分,如果语法有少量错误,酌情扣分,对于每个要点的给分尽量公平公正。开头Dear Sir/ Madam,和结尾部分Yours,
Wang Li如果作文没有给出不扣分。词数远低于要求,内容过于单薄,直接定为第一档次。
以上内容最后都将作为prompt传给了模型。所以对于General_scoring_requirements
yp描述的越准确,评分会更合理。
完整的工作流
请json文件中的key ,替换为你的API key
{
"name": "英语书面表达批量评分",
"nodes": [
{
"parameters": {
"formTitle": "输入作文要求",
"formDescription": "输入作文要求和上传作文",
"formFields": {
"values": [
{
"fieldLabel": "composition_requires",
"requiredField": true
},
{
"fieldLabel": "General_scoring_requirements",
"requiredField": true
}
]
},
"options": {}
},
"type": "n8n-nodes-base.formTrigger",
"typeVersion": 2.2,
"position": [
-3840,
-1200
],
"id": "4088f1eb-62d2-424e-a26b-9c25e6753aac",
"name": "On form submission",
"webhookId": "f988452b-1618-4ed5-8684-58b53823a939"
},
{
"parameters": {
"operation": "binaryToPropery",
"binaryPropertyName": "=data",
"destinationKey": "base64EncodedImage",
"options": {}
},
"type": "n8n-nodes-base.extractFromFile",
"typeVersion": 1,
"position": [
-3180,
-1200
],
"id": "49f09d3e-6ee9-4332-835f-1d251adcf312",
"name": "Convert Image to Base64 (Image)",
"alwaysOutputData": true
},
{
"parameters": {
"method": "POST",
"url": "https://generativelanguage.googleapis/v1beta/models/gemini-2.5-flash-preview-05-20:generateContent?key=请在这里替换为你自己的APIKEY",
"sendBody": true,
"specifyBody": "json",
"jsonBody": "={\n \"contents\": [\n {\n \"parts\": [\n {\"text\": \"请只识别图片中标题为“英语作文题”下方的英文文章内容。忽略“填空题”部分和所有数字/标记。输出纯英文手写文本。如果识别失败或内容为空,请回复'OCR_FAILED'。\"},\n {\n \"inline_data\": {\n \"mime_type\": \"image/png\",\n \"data\": \"{{ $json.base64EncodedImage }}\"\n }\n }\n ]\n }\n ],\n \"generationConfig\": {\n \"temperature\": 1,\n \"maxOutputTokens\": 65536\n }\n}",
"options": {}
},
"type": "n8n-nodes-base.httpRequest",
"typeVersion": 4.2,
"position": [
-2960,
-1200
],
"id": "4764e989-bff8-4ca7-b374-c31d97e0e34d",
"name": "HTTP Request (OCR from Image)"
},
{
"parameters": {
"method": "POST",
"url": "https://generativelanguage.googleapis/v1beta/models/gemini-2.5-flash-preview-05-20:generateContent?key=请在这里替换为你自己的APIKEY",
"sendHeaders": true,
"headerParameters": {
"parameters": [
{
"name": "Content-Type",
"value": "application/json"
}
]
},
"sendBody": true,
"specifyBody": "json",
"jsonBody": "={{ $json.requestBody}}",
"options": {}
},
"type": "n8n-nodes-base.httpRequest",
"typeVersion": 4.2,
"position": [
-2520,
-1200
],
"id": "78f2784b-e4b3-40b0-aab7-3e03810874f1",
"name": "调用模型评分"
},
{
"parameters": {
"jsCode": "// 获取作文题目和内容\nconst compositionRequires = $('On form submission').first().jsonposition_requires;\nconst ocrText = $input.first().json.candidates[0].content.parts[0].text;\nconst GeneralRequires =$('On form submission').first().json.General_scoring_requirements\n// 构建提示词\nconst promptText = `你是一位中学英语作文评分老师,请严格按照以下评分标准对中学生作文进行评分:\n\n书面表达评分标准:\n先根据整体印象定档,然后再根据以下档次打分(强调文章信息表达的完整性、语言的准确性、结构的逻辑性、卷面的整洁性、书写的规范性,允许表述合理前提下的多元性),做到给分有理、扣分有据!谨慎给满分,6分以下的二档、一档谨慎给分。\n\n第五档次(13-15分):包含全部要点,内容完整,连接词运用得恰当,语句十分流畅,没有语法错误,书写漂亮,词汇句型丰富。\n\n第四档次(10-12分):包含基本要点,遗漏少量要点;有少量拼写、语法错误;书写工整。\n\n第三档次(7-9分):包含部分要点;语法虽有较多错误,尚能达意;书写较工整。\n\n第二档次(4-6分):要点不明确;语法错误多,影响意思表达;尚有一至二个完整句子。\n\n第一档次(0~3分):白卷或文不对题;错误百出,不知所云。如果照抄了卷子或课本上的文章,一律无分。\n\n作文题目:\n${compositionRequires}\n\n学生作文:\n${ocrText}\n\n请严格按照以下格式输出评分结果,严格遵照总的评分原则:\n【总的评分原则】\n${GeneralRequires}\n\n【评分档次与分数】\n请先根据整体印象定档,明确写出属于哪一档次,并给出具体分数(0-15分)。\n\n【得分点】\n请详细列出本作文的优点(如内容完整、表达流畅、结构清晰、词汇丰富等),每一点单独列出。\n\n【扣分点】\n请详细列出本作文存在的问题或不足(如语法错误、要点遗漏、表达不清等),每一点单独列出。\n\n【得分依据】\n请结合评分标准,说明为什么给出这个分数,做到给分有理、扣分有据。\n\n【修改建议】\n针对作文中的错误或不足,给出具体的修改建议,逐条列出。\n--- 输出格式要求 ---\n请严格按照以下 JSON 格式输出评分结果,不要包含任何额外文字或解释:\n{\n \"grading_tier\": \"string\", // 例如: \"第五档次\", \"第四档次\" 等\n \"final_score\": integer, // 0-15之间的整数\n \"strengths\": [ // 详细列出本作文的优点,每一点作为数组中的一个字符串\n \"string\",\n \"string\"\n ],\n \"weaknesses\": [ // 详细列出本作文存在的问题或不足,每一点作为数组中的一个字符串\n \"string\",\n \"string\"\n ],\n \"scoring_justification\": \"string\", // 结合评分标准,详细说明给出这个分数的原因\n \"improvement_suggestions\": [ // 针对作文中的错误或不足,给出具体的修改建议,逐条列出\n \"string\",\n \"string\"\n ]\n}\n`;\n\n// 构建API请求的JSON\nconst requestBody = {\n contents: [\n {\n parts: [\n {\n text: promptText\n }\n ]\n }\n ],\n generationConfig: {\n temperature: 0.0, // 核心!设置为 0.0 以消除随机性,保证输出稳定性\n topP: 1.0, // 配合 temperature: 0.0,考虑所有概率累积到1的令牌\n topK: 1, // 配合 temperature: 0.0,只选择最有可能的那个词\n maxOutputTokens: 65536, // 根据预期输出长度设置,确保足够长以包含详细反馈\n // stopSequences: [] // 如果你的输出有明确的结束标记,可以添加,例如 [\"\\n---\"]\n },\n safetySettings: [ // 推荐添加,防止生成不安全内容\n {\n category: \"HARM_CATEGORY_HARASSMENT\",\n threshold: \"BLOCK_NONE\", // 评分场景可以适当放宽,但仍需注意\n },\n {\n category: \"HARM_CATEGORY_HATE_SPEECH\",\n threshold: \"BLOCK_NONE\",\n },\n {\n category: \"HARM_CATEGORY_SEXUALLY_EXPLICIT\",\n threshold: \"BLOCK_NONE\",\n },\n {\n category: \"HARM_CATEGORY_DANGEROUS_CONTENT\",\n threshold: \"BLOCK_NONE\",\n },\n ],\n};\n\n// 返回处理好的数据\nreturn {\n requestBody: requestBody\n};"
},
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [
-2740,
-1200
],
"id": "e9801b4b-6cc0-484d-9e86-5003a6262903",
"name": "准备post模型的json格式数据"
},
{
"parameters": {
"jsCode": "// 1. 获取 Gemini 模型的原始输出文本\n// 这是当前输入项中,Gemini 模型返回的原始文本内容,包含 '```json' 和 '```' 标记\nconst rawGeminiOutputText = $input.first().json.candidates[0].content.parts[0].text;\n\n// 2. 获取学生作文内容和作文题目\n// 注意:你这里使用了 $('NodeName') 方式引用其他节点的数据。\n// 请确保 'Extract OCR Result' 和 'On form submission' 节点在当前执行路径中是活动的且已成功运行,\n// 否则可能导致 'Referenced node doesn't exist' 错误。\nconst studentCompositionText = $('HTTP Request (OCR from Image)').first().json.candidates[0].content.parts[0].text|| \"未提供学生作文内容\";\nconst questionText = $('On form submission').first().jsonposition_requires || \"未提供作文题目\";\n\n\n// --- 新增或修改的辅助函数开始 ---\n\n// 清除Markdown格式的辅助函数\nfunction cleanMarkdown(text) {\n if (!text || typeof text !== 'string') {\n return text;\n }\n return text\n .replace(/\\*\\*(.*?)\\*\\*/g, '$1') // 移除粗体 **text**\n .replace(/\\*(.*?)\\*/g, '$1') // 移除斜体 *text*\n .replace(/`(.*?)`/g, '$1') // 移除行内代码 `text`\n .replace(/\\[(.*?)\\]\\(.*?\\)/g, '$1') // 移除链接 [text](url)\n .replace(/^\\s*[-*+]\\s+(.*?)$/gm, '$1') // 移除列表项前的 - * + 标记\n .replace(/^\\s*\\d+\\.\\s+(.*?)$/gm, '$1') // 移除列表项前的数字标记 (e.g., 1.)\n .replace(/\\n\\s*\\n/g, '\\n') // 将多个连续的空行压缩成一个\n .trim(); // 移除字符串首尾的空白\n}\n\n// 辅助函数,将数组格式化为带序号或星号的列表,并对每项内容进行Markdown清理\nfunction formatList(items, prefix = '- ') {\n if (!Array.isArray(items) || items.length === 0) {\n return \"无\";\n }\n // 关键修改:在这里调用 cleanMarkdown 函数处理每个列表项\n return items.map(item => `${prefix}${cleanMarkdown(item)}`).join('\\n');\n}\n\n// --- 新增或修改的辅助函数结束 ---\n\n\n// 3. 提取并解析 JSON 字符串\nlet gradingResult = null;\ntry {\n // 使用正则表达式匹配并提取 '```json' 和 '```' 之间的内容\n const jsonStringMatch = rawGeminiOutputText.match(/```json\\n([\\s\\S]*)\\n```/);\n\n if (jsonStringMatch && jsonStringMatch[1]) {\n // 成功匹配到 JSON 字符串,尝试解析\n gradingResult = JSON.parse(jsonStringMatch[1]);\n } else {\n // 如果没有匹配到 Markdown 代码块,尝试直接解析\n gradingResult = JSON.parse(rawGeminiOutputText);\n }\n} catch (error) {\n // 如果解析失败,记录错误并返回一个错误报告\n console.error(\"解析 Gemini 输出 JSON 失败:\", error.message);\n return {\n json: {\n error: \"无法解析 Gemini 评分结果,请检查模型输出格式。\",\n rawOutput: rawGeminiOutputText,\n errorMessage: error.message\n }\n };\n}\n\n// 4. 构建格式化的评分报告\n// 确保 gradingResult 不为 null 且包含预期的属性\nif (!gradingResult) {\n return {\n json: {\n error: \"Gemini 未返回有效的评分结果。\",\n rawOutput: rawGeminiOutputText\n }\n };\n}\n\nconst fullReport = `=== 英语作文评分报告 ===\n\n评分时间:${new Date().toLocaleString('zh-CN')}\n--------------------------------------------------\n\n作文题目:\n${cleanMarkdown(questionText)}\n--------------------------------------------------\n\n学生作文:\n${cleanMarkdown(studentCompositionText)}\n--------------------------------------------------\n\n=== 评分结果 ===\n\n【评分档次与分数】\n档次:${gradingResult.grading_tier || '未提供'}\n分数:${gradingResult.final_score !== undefined ? gradingResult.final_score : '未提供'}/15\n\n【得分点】\n${formatList(gradingResult.strengths)}\n\n【扣分点】\n${formatList(gradingResult.weaknesses)}\n\n【得分依据】\n${cleanMarkdown(gradingResult.scoring_justification || '未提供')}\n\n【修改建议】\n${formatList(gradingResult.improvement_suggestions)}\n\n=== 报告结束 ===`;\n\n// 返回处理后的文本数据(可以作为纯文本,也可以作为 JSON 属性)\nreturn {\n json: {\n reportText: fullReport,\n // 也可以将解析后的结构化数据也返回,方便后续节点使用\n gradingDetails: gradingResult\n }\n};"
},
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [
-2300,
-1200
],
"id": "d629106a-f8eb-4ed3-bbce-dec46408be49",
"name": "清除响应中markdown标记"
},
{
"parameters": {
"operation": "toText",
"sourceProperty": "reportText",
"options": {}
},
"type": "n8n-nodes-base.convertToFile",
"typeVersion": 1.1,
"position": [
-2080,
-1200
],
"id": "d3c4560c-9214-42a4-a3c7-afae51142481",
"name": "保存为txt文件"
},
{
"parameters": {
"operation": "write",
"fileName": "=/home/node/files/{{ $('Read/Write Files from Disk').item.json.fileName.split(\".\")[0]}}_评分结果.txt",
"dataPropertyName": "=data",
"options": {}
},
"type": "n8n-nodes-base.readWriteFile",
"typeVersion": 1,
"position": [
-1860,
-1200
],
"id": "86e24ee4-5f93-41e2-93f6-52c851df209a",
"name": "将评分结果写入文件"
},
{
"parameters": {
"fileSelector": "/home/node/files/*.png",
"options": {}
},
"type": "n8n-nodes-base.readWriteFile",
"typeVersion": 1,
"position": [
-3620,
-1200
],
"id": "2e1de929-7a40-4313-a599-e9e8cf60c307",
"name": "Read/Write Files from Disk"
},
{
"parameters": {
"options": {}
},
"type": "n8n-nodes-base.splitInBatches",
"typeVersion": 3,
"position": [
-3400,
-1200
],
"id": "b6a4327b-06ba-4dfd-9166-1d8aef84dfc4",
"name": "Loop Over Items"
},
{
"parameters": {},
"type": "n8n-nodes-base.noOp",
"name": "Replace Me",
"typeVersion": 1,
"position": [
-1640,
-1120
],
"id": "513b8c72-7a9a-4002-9729-64dbdcfed96f"
},
{
"parameters": {
"fileSelector": "/home/node/files/*.txt",
"options": {
"dataPropertyName": "data"
}
},
"type": "n8n-nodes-base.readWriteFile",
"typeVersion": 1,
"position": [
-3180,
-1400
],
"id": "efe5bafe-812d-46d7-ae47-3f434d42907b",
"name": "重新读取所有评分报告"
},
{
"parameters": {
"jsCode": "let allStudentDeductionPoints = [];\n\n// 当 Code 节点直接接收上游多个数据项时,\n// $input.all() 会返回所有输入数据项的数组。\nconst allInputItems = $input.all(); \n\nconsole.log(\"Received total input items for summarization:\", allInputItems.length); // 打印收到的项目总数\n\n// 确保 allInputItems 是一个数组,并遍历它\nif (Array.isArray(allInputItems)) {\n for (const item of allInputItems) { // item 现在是 n8nInputItem 类型\n console.log(\"Processing item with fileName:\", item.json.fileName || 'unknown file'); // 打印正在处理的文件名\n\n // 获取每个文件的文本内容\n // 假设“解码报告文本”节点将文本内容放在 'fileContent' 属性中\n const fileContent = item.json.fileContent; // 从 item.json 访问 fileContent\n\n if (typeof fileContent === 'string' && fileContent.trim() !== '') {\n // 在这里重新从完整的报告文本中提取扣分点\n let deductionPoints = '';\n const deductionMatch = fileContent.match(/【扣分点】\\n([\\s\\S]*?)\\n【得分依据】/);\n if (deductionMatch && deductionMatch[1]) {\n deductionPoints = deductionMatch[1].trim();\n }\n\n if (deductionPoints !== '') { // 检查扣分点是否成功提取且非空\n allStudentDeductionPoints.push(deductionPoints);\n console.log(\"Successfully extracted and added deduction points for an item.\");\n } else {\n console.warn(`No valid deduction points found in file: ${item.json.fileName || 'unknown file'}`);\n }\n } else {\n console.warn(`File has no valid 'fileContent' or it's empty: ${item.json.fileName || 'unknown file'}`);\n }\n }\n} else {\n // 这表示数据流异常,上游节点没有按预期输出数组\n console.error(`Critical Error: Input to \"汇总班级扣分点\" is not an array of items. Actual: ${JSON.stringify(allInputItems)}`);\n throw new Error(\"Input to 汇总班级扣分点 is not an array of items.\");\n}\n\n// 如果收集到的扣分点数组为空,提前返回一个提示\nif (allStudentDeductionPoints.length === 0) {\n console.warn(\"Collected deduction points array is empty after processing. Returning empty summary.\");\n return [{\n json: {\n summarizedDeductions: \"未能从所有学生数据中收集到有效扣分点,班级总结报告将为空。\"\n }\n }];\n}\n\n// 将所有学生的扣分点合并成一个长字符串,并添加清晰的分隔符和编号\nconst summarizedDeductionsForDeepSeek = allStudentDeductionPoints\n .map((points, index) => `学生 ${index + 1} 作文扣分点:\\n${points}`)\n .join('\\n\\n--- 学生扣分点分隔符 ---\\n\\n');\n\nconsole.log(\"Final summarized deductions:\\n\", summarizedDeductionsForDeepSeek);\n\n// 输出汇总后的扣分点字符串\nreturn [{\n json: {\n summarizedDeductions: summarizedDeductionsForDeepSeek\n }\n}];"
},
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [
-2740,
-1400
],
"id": "44e95a3b-6409-4e8d-ae33-d06ce5df8795",
"name": "汇总班级扣分点"
},
{
"parameters": {
"operation": "write",
"fileName": "/home/node/files/班级英语作文教学总结.txt",
"dataPropertyName": "=data",
"options": {
"append": false
}
},
"type": "n8n-nodes-base.readWriteFile",
"typeVersion": 1,
"position": [
-1860,
-1400
],
"id": "909ad80a-240a-4022-b5c5-77d893865c45",
"name": "写入班级总结文件"
},
{
"parameters": {
"operation": "text",
"destinationKey": "fileContent",
"options": {
"encoding": "utf8"
}
},
"type": "n8n-nodes-base.extractFromFile",
"typeVersion": 1,
"position": [
-2960,
-1400
],
"id": "a1a9d204-b5c9-4aef-803c-6219ec2d38e2",
"name": "解码报告文本"
},
{
"parameters": {
"jsCode": "// 获取上一个节点(汇总班级扣分点)输出的汇总扣分点字符串\nconst summarizedDeductions =$input.first().json.summarizedDeductions;\n\n// 构建 DeepSeek API 请求的 JSON 对象\nconst requestBody = {\n \"model\": \"deepseek-chat\", \n \"messages\": [\n {\"role\": \"system\", \"content\": \"你是一位经验丰富的中学英语教师,擅长从多个学生作文的扣分点中提取共性问题,并提供针对班级整体的、高效可行的教学策略和改进建议。你的目标是帮助老师提升全班学生的英语写作能力。\"},\n {\"role\": \"user\", \"content\": `以下是班级中多名学生的英语作文扣分点汇总。请你分析这些扣分点,识别出常见的语法、词汇、表达、结构等方面的问题,并基于这些共性问题,为教师提供一份针对班级整体的、具体的教学建议。建议应包含但不限于:\n1. 常见的错误类型及示例(可以从汇总的扣分点中提取)。\n2. 针对这些错误的教学方法或练习建议。\n3. 如何在日常教学中融入写作技能提升。\n\n请以清晰、分点的形式给出教学建议,可以直接用于教师备课或反馈给学生。\n\n学生扣分点汇总:\n${summarizedDeductions}` // <--- 检查这一行的末尾,确保有反引号 `\n } \n ],\n \"stream\": false,\n \"max_tokens\": 2048 \n};\n\n// 返回处理好的数据\nreturn [{\n json: {\n body: requestBody \n }\n}];"
},
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [
-2520,
-1400
],
"id": "3eaa6769-ef51-4f00-ad13-b863bb5c0110",
"name": "准备班级总结请求"
},
{
"parameters": {
"method": "POST",
"url": "https://generativelanguage.googleapis/v1beta/models/gemini-2.5-flash-preview-05-20:generateContent?key=请在这里替换为你自己的APIKEY",
"sendHeaders": true,
"headerParameters": {
"parameters": [
{
"name": "Content-Type",
"value": "application/json"
}
]
},
"sendBody": true,
"specifyBody": "json",
"jsonBody": "={{ $json.body }}",
"options": {}
},
"type": "n8n-nodes-base.httpRequest",
"typeVersion": 4.2,
"position": [
-2300,
-1400
],
"id": "a2ff72e6-206b-4011-9322-79eeeaa2bf18",
"name": "调用DeepSeek生成班级总结"
},
{
"parameters": {
"operation": "toText",
"sourceProperty": "choices[0].message.content",
"options": {
"encoding": "utf8"
}
},
"type": "n8n-nodes-base.convertToFile",
"typeVersion": 1.1,
"position": [
-2080,
-1400
],
"id": "f35d5858-a8df-44e1-b0e6-e1453b8c9ae7",
"name": "将内容转为二进制"
}
],
"pinData": {},
"connections": {
"On form submission": {
"main": [
[
{
"node": "Read/Write Files from Disk",
"type": "main",
"index": 0
}
]
]
},
"Convert Image to Base64 (Image)": {
"main": [
[
{
"node": "HTTP Request (OCR from Image)",
"type": "main",
"index": 0
}
]
]
},
"HTTP Request (OCR from Image)": {
"main": [
[
{
"node": "准备post模型的json格式数据",
"type": "main",
"index": 0
}
]
]
},
"准备post模型的json格式数据": {
"main": [
[
{
"node": "调用模型评分",
"type": "main",
"index": 0
}
]
]
},
"调用模型评分": {
"main": [
[
{
"node": "清除响应中markdown标记",
"type": "main",
"index": 0
}
]
]
},
"清除响应中markdown标记": {
"main": [
[
{
"node": "保存为txt文件",
"type": "main",
"index": 0
}
]
]
},
"保存为txt文件": {
"main": [
[
{
"node": "将评分结果写入文件",
"type": "main",
"index": 0
}
]
]
},
"Read/Write Files from Disk": {
"main": [
[
{
"node": "Loop Over Items",
"type": "main",
"index": 0
}
]
]
},
"Loop Over Items": {
"main": [
[
{
"node": "重新读取所有评分报告",
"type": "main",
"index": 0
}
],
[
{
"node": "Convert Image to Base64 (Image)",
"type": "main",
"index": 0
}
]
]
},
"Replace Me": {
"main": [
[
{
"node": "Loop Over Items",
"type": "main",
"index": 0
}
]
]
},
"将评分结果写入文件": {
"main": [
[
{
"node": "Replace Me",
"type": "main",
"index": 0
}
]
]
},
"重新读取所有评分报告": {
"main": [
[
{
"node": "解码报告文本",
"type": "main",
"index": 0
}
]
]
},
"汇总班级扣分点": {
"main": [
[
{
"node": "准备班级总结请求",
"type": "main",
"index": 0
}
]
]
},
"解码报告文本": {
"main": [
[
{
"node": "汇总班级扣分点",
"type": "main",
"index": 0
}
]
]
},
"准备班级总结请求": {
"main": [
[
{
"node": "调用DeepSeek生成班级总结",
"type": "main",
"index": 0
}
]
]
},
"调用DeepSeek生成班级总结": {
"main": [
[
{
"node": "将内容转为二进制",
"type": "main",
"index": 0
}
]
]
},
"将内容转为二进制": {
"main": [
[
{
"node": "写入班级总结文件",
"type": "main",
"index": 0
}
]
]
}
},
"active": true,
"settings": {
"executionOrder": "v1"
},
"versionId": "a372df73-faaa-47da-ad21-b660dc45cb29",
"meta": {
"instanceId": "d67491f40160579f36152b8024f8143de629631f779c740c844fb75920afadba"
},
"id": "3l0wDU7Aw8lKx1Tw",
"tags": []
}
模拟评分的结果:
=== 英语作文评分报告 ===
评分时间:2025/6/25 08:53:30
--------------------------------------------------
作文题目:
第二节书面表达(共1题,满分 15 分) 假如你是王丽,你校英语网站正在开展以“你坚持做的一件事”为主题的征文活动。请你根据以下提示和要求,用英语写一篇短文,向校刊投稿。 提示:1. What did you stick to doing?< 2. Why did you do it? 3.How did you feel? 要求:1.语义通顺,行文连贯; 2.词数 80左右,文章格式已给出,不计入总词数。 Dear Sir/ Madam, Yours, Wang Li
--------------------------------------------------
学生作文:
Dear Sir / Madam.
I stick to reading books that are useful for my
study in my spare time. Books can open my eyes and
make my brain become more active. I can improve my
writing skills just by reading excellent books, too. My teacher
recommended me a great book that had brilliant sentences and
words when I was seven. After that, I continued reading all
kinds of books in my free time till today. When I am
reading books, I feel relaxed and excited. That's why
I like reading so much. I can make me feel happier
and more confident by reading books, so I think I'll
stick to reading in the future.
Yours
Wang Li
--------------------------------------------------
=== 评分结果 ===
【评分档次与分数】
档次:第四档次
分数:12/15
【得分点】
- 包含全部要点,内容完整,对提示中的三个问题都进行了清晰的回答。
- 文章结构逻辑清晰,行文连贯,段落之间衔接自然。
- 连接词运用恰当,如“After that”, “When I am reading books”, “That's why”, “so I think”,增强了文章的流畅性。
- 词汇运用较为丰富和准确,如“useful”, “open my eyes”, “active”, “improve”, “excellent”, “brilliant”, “relaxed”, “excited”, “confident”等。
- 句型多样,表达流畅,语义通顺。
- 字数符合要求且略有超出,内容充实。
【扣分点】
- 存在少量语法错误,如“make my brain become more active”中“become”为冗余,可直接用“make my brain more active”。
- 存在代词使用错误,如“I can make me feel happier”应为“It can make me feel happier”或“Reading can make me feel happier”,主语和宾语指代不一致。
【得分依据】
根据整体印象,本文内容完整,包含所有提示要点,且对要点二(Why did you do it?)进行了充分的阐述,体现了较好的写作能力。文章结构逻辑清晰,连接词使用恰当,使得行文连贯流畅。词汇和句型运用较为丰富,表达清晰。虽然存在少量语法错误(如代词使用不当和冗余表达),但这些错误并未严重影响整体意思的表达,且符合第四档次“有少量拼写、语法错误”的描述。鉴于其内容完整性和整体流畅性,将其定为第四档次的高分,即12分。具体得分依据:要点一(What did you stick to doing?)2分,要点二(Why did you do it?)6分,要点三(How did you feel?)2分,共计10分。字数、字迹、语法部分共5分,因存在少量语法错误,酌情扣除3分,得2分。总分10+2=12分。
【修改建议】
- 将“make my brain become more active”修改为“make my brain more active”,使表达更简洁、地道。
- 将“I can make me feel happier”修改为“It can make me feel happier”或“Reading can make me feel happier”,以修正代词使用错误,确保主语和宾语指代一致。
- 在写作完成后,建议仔细检查代词的使用,确保指代清晰准确。
- 可以尝试使用更多高级连接词或更复杂的句式,进一步提升文章的连贯性和表达的深度。
=== 报告结束 ===
关于n8n的小故事:
n8n 的创始人Jan Oberhauser
将 “node”(因为它使用了 Node 视图,并且基于 Node.js)和 “automation”(自动化,这正是该项目旨在帮助实现的目标)组合成 nodemation
,并巧妙地将其缩写为 “n8n”,其中 “8” 代表 odematio”
中的 8 个字母。
发布者:admin,转转请注明出处:http://www.yc00.com/web/1754415885a5156754.html
评论列表(0条)