一张语文周清卷,怎样从截图走到可修改的 Word
一道题可能有两个空,一次修改要同时出现在学生卷和答案卷里。记录 class 如何把截图里的语文练习,做成能修改、能保存、能打印的 Word 和 PDF。
9 月做 class 的时候,我碰到一个很具体的请求:围绕语文课上的主题,出一份能发给学生的周清练习。老师手边已经有练习截图、教材资料和想考的篇目。最初听起来像是把文字交给模型,等它吐出一张试卷;真正做下去,问题却集中在纸上的细节:哪个字是给定字,哪个字需要写,哪个字上面要标拼音;一题可能有两个空,答案版还得和学生版落在同一位置。
8 月我写过 class 从 Excel 走到班主任工作台的第一版。那时关注的是名册、请假、成绩等日常记录如何留在同一处。这次的起点则是一份不完整、可能只有截图的练习材料。老师要从它出发,最后拿走填空版和答案版的 Word、PDF。页面能显示一张漂亮的卷子,离这件事完成还很远。
原题里的空位,不能靠猜
语文练习里常见的题型,给程序出的难题比外观复杂。比如「贮蓄」可以是一道题:保留一个字的拼音提示,让学生写另一个字,同时再给加点字注音。它在题目统计里只算一个词语,在作答统计里却有两个空。「水波( )( )」看起来像一个连续的括号区域,实际上是两个独立答案。还有「咄咄逼人」,如果两个「咄」都加点,就要分别对应两个 duō,不能只留下一个拼音。读者看纸张时一眼能分辨的事,截图识别和生成模型未必能稳定分辨。
我最早担心的是另一种更隐蔽的错误:学生版看起来留了空,答案版却来自另一次生成。只要两次模型调用在选词、顺序或标点上有一点偏差,两份纸就无法逐题对应。老师校对一次,也无法保证修改会同时出现在两版里。因此我只保存一份带完整答案的题目。填空版隐藏该让学生填写的部分,答案版在同一位置把内容显出来;Word、PDF 和页面预览都从这份题目往下走。
原题重排和自由出题还不能混为一谈。自由出题时,老师给的是主题、目标词语数量和想练的古诗,程序要协助挑选。原题重排时,老师交来的其实已经是一张练习,只是载体是图片或文档。它的顺序、给定字、拼音提示乃至括号位置都有教学意图。
把原题识别出来,再「优化」成模型偏爱的样子,反而可能改掉老师要保留的内容。所以我给原题重排单独设了处理方式,要求逐项带回提示和挖空位置。文字模糊、同音字无法判定时,就留下待确认项。这使识别更费事,却让老师知道系统究竟看懂了多少。
这一步并不只是加一个 answer 字段。每道题要保存完整的词语或主旨句;每个空另有自己的编号、位置、答案、书写宽度和分值。给定字、拼音提示和加点位置也要单独记住。纸面上的括号与下划线是最后画出来的,不能拿它们当题目本身。否则改一个字,提示就可能错位;调整行距时,也可能意外改变题意。
字符位置还有一个容易漏掉的坑。JavaScript 字符串的普通下标和老师看到的「第几个字」不是一回事。实现里先把文本做 Unicode NFC 归一化,再用 Array.from() 按 code point 拆开,用同一种计数方式记录空位、提示和加点。这里没有什么神奇算法,只是先统一「位置」的定义,再让编辑器、检查程序和导出都遵守它。这个约定让「应和」只给「和」注音这类细节能被保存,而不必在排版时重新猜一遍。
我让模型做候选题,让程序守住结构
class 原有 Agent 已经能处理其他文档任务,所以我没有另造一个孤立的聊天出卷器。9 月 22 日新增的「主题出卷」既在左侧菜单里有独立页面,也作为 Agent 的一项技能接入。老师直接打开页面可以选择范围、资料、题量和版式;在 Agent 中明确提出出卷需求时,也会调用同一套创建服务,返回实际保存的编辑入口。聊天里出现一段试题文字,不等于系统已经创建了可以修改和导出的试卷。
模型在这个流程中的工作,是按指定范围提供候选考点:完整词语或主旨句、逐字拼音、要挖空的位置、可供核对的来源说明。它返回结构化数据,不负责写 HTML、排页面或画成品卷。程序再为题目和空位建立编号,检查题型、重复项、位置、数量与总分。没有通过检查的题,会留下原因并单独重试;已经得到的题和老师改过的内容继续保留。生成一次没有把所有题补齐,老师也不必从头来过。
如果一次请求只返回了部分候选题,页面会指出缺的是哪几道。某个词语的拼音与汉字数对不上,或古诗主旨题变成了诗句默写,也会被拦下来。这样的提示没有一张完整试卷好看,却能让老师知道该修哪道题。
更重要的是,出错不意味着重来。那次线上生成保留了前面已有的正确题,再补余下的题。老师已经改过的内容也继续留着。对一份需要反复校对的练习来说,能从上次停下的地方继续,比追求一次生成得完美更实用。
这里还要区分两种常被混在一起的「依据」。围绕主题自由选题,系统可以推荐词语,但要标清楚「推荐、待核对」。如果要求按某份教材或练习资料出题,就得先拿到老师确认过的材料。只有教材名称,不能推断某词一定在书里,更不能编页码或词典核验结果。
截图、PDF、DOCX 或粘贴文字都可以送进来,读出的文字和原题位置仍要由老师看一遍。原题可能把提示、加点和空位放在上下两行。OCR 能认出字,不代表已经分清哪个拼音属于哪个空。
这一点也是我对「AI 帮老师出卷」的取舍。模型能节省整理材料和起草候选题的时间,却不能替老师判断一个字的读音是否符合这节课的语境、主旨填空是否符合课堂讲法。系统没有接入权威词典,也不能自动证明所有学科事实。每道生成题起初都要老师核对;确认来源、拼音和主旨后,才可以导出。让老师在操作中逐题确认,比在页面底部写一句「请人工检查」有用。
修改过的题,下一次生成不能冲掉
出卷不是按一下按钮就结束。老师看完第一版,可能把某个字换掉、修改拼音、调宽一个书写空,或者保留原题的提示方式。系统如果在下一次补题时把这些劳动覆盖,编辑器就失去意义。因此题目区分了生成内容、手工修改、锁定与核对状态。批量生成会绕开已经修改或锁定的题;如果老师主动选择替换某一道,界面和服务都要求明确表达这个动作。替换失败时,原题留在原处。
保存还得考虑多窗口。试卷存在数据库里,有版本号和修改记录。一个窗口改完后,另一个旧窗口再保存,系统会提示版本冲突,同时留住尚未保存的修改。老师可以另存一份,也可以选择放弃本机修改,载入已保存的版本。
创建试卷时也给请求编号,网络重试不会凭空多出一张相同的卷子。这些看不见的处理,服务的是一个朴素需求:改过的题要在,下次打开还要在。
版本保护在出卷场景里格外重要,因为一次误覆盖可能要到打印后才发现。老师也许在一个窗口修了拼音,在另一个窗口调了分值;服务端只认最新版本,前端则留着本机草稿,让老师决定如何处理。导出时同样会核对保存的版本号,防止屏幕上刚改过的试卷和下载下来的文件不是同一修订。这样的检查没有增加一条试题,却决定了交付文件能不能让人放心。
统计同样不能只显示一个「题数」。一份卷子可以有 36 个生字词、4 道古诗主旨题,共 40 道题;但其中混合题和多个书写空加起来,可能有 45 个作答空。按题计分与按空计分也不是同一种规则。系统把词语题数、古诗题数、空数、各部分分值和待核对数量分别计算,并在导出前核对目标总分。这样「贮蓄」不会为了凑空数被误拆成两道词语题,老师看到的 100 分也有清楚的构成。
纸上的位置,比预览里的好看更难
试卷最后要被打印,所以我没有把浏览器预览当作终点。页面、Word 和 PDF 共用一套布局计算:每个文字块、拼音提示、括号、书写线都有位置和尺寸;切换到答案版时,改变的是空位显示的内容,不是重新排一张卷子。长标题、过宽词语、放不下的题会提前给出问题,而不是导出一个被裁掉半行的文件。老师也可以明确选择允许分页。
Word 的价值是老师拿到后还能继续改,PDF 的价值是打印时有更稳定的版面。两者实现方式不同:Word 生成可编辑的文字和固定位置的段落,PDF 从同一布局生成打印页面,并检查字体是否真的加载。也因为实现不同,不能只打开其中一个文件就说导出成功。空白版和答案版的括号、提示及行距尤其要逐一核对;学生版一旦从题干或提示里露出答案,答案版对齐再漂亮也没有用。
版面上最容易被忽略的是上方提示与下方书写线的关系。一个拼音提示可能比下面的汉字宽;直接按字宽横排,相邻提示会挤在一起。随意挪动括号,学生又看不出提示指向哪个字。
因此排版时先算每个小单元需要多宽,再安排提示和正文。空白版与答案版保留同一组文字框,只替换该显示答案的文字。Word 和 PDF 虽然用不同方式生成,题目位置却只维护一套规则。
9 月 23 日的线上验收,给了这套设计一次具体考验。我们从老师给的练习截图提取内容,再逐题校正字形、声调和上下关系,通过线上模型补齐题目。一次补生成其余 8 题花了 84.7 秒;最终保存的修订版有 36 个词语、4 道古诗、45 个作答空,按题计分是词语 72 分、古诗 28 分,总分 100 分。它不是一个硬编码在产品里的演示题库:识别、生成、编辑、保存和导出都走了线上流程。
验收时盯的不是「40 道题都在」这么粗的指标。我回头看「贮蓄」是否仍是一题两空,「应和」是否只给指定字注音,「咄咄逼人」两个加点字是否都有拼音;还把原题的两个空恢复为两个独立作答位置,调整了两道古诗主旨的答案和书写宽度,并检查不该有的句号有没有留在纸上。做这些细改后再导出,才能知道编辑器保存的究竟是不是最终打印的数据。
9 月 23 日收尾时,出卷相关的 31 项回归检查和 13 个工作区类型检查通过;最后一轮排版修复集中处理了原题格线与书写线的对应。这些检查能确认「一题两空」在数据里没有被拆坏。
纸面的问题还得看实际文件。打开 Word,再逐页看 PDF,才能发现字体、断行或打印区域的偏差。代码检查和文件检查都过了,才算那份样卷在当时可用。
从同一份已保存的试卷修订中,线上导出了填空版和答案版的 PDF、Word,共四个文件。两份 PDF 都是 A4 一页,可选中文字;字体检查显示嵌入和 Unicode 映射正常。两份 Word 能编辑,空白版与答案版的文字框位置一致,实际用 LibreOffice 打开和渲染后仍为一页。四份文件的全部页面都检查了裁切、重叠和错位。对于一张需要印发的卷子,这些具体检查比「下载按钮可点」更接近交付。
我尤其在意 Word:老师拿到文件后,还可能为自己班上的进度调一道题、改一句提示。可编辑文字让这一步有余地,但前提是文件本身能正常打开,调整前的版面也与 PDF 对得上。否则「提供 Word」只是多了一个下载选项,实际工作还是得回到截图和复制粘贴。这次至少把可编辑和版面一致放在了同一轮检查里。
过程并非一路顺畅。连续导出第二份 PDF 时,线上曾返回 HTTP 500;稍后单独重试成功,四份文件最终都拿到了。一次重试成功只能说明当时交付完成,不能证明间歇性错误已经消失。导出失败时,页面还需要给老师清楚的提示和重试机会。
页面发布到 v0.1.51 并完成线上核对;同一轮的桌面安装包上传失败。所以这次能确认的是网页流程和四份导出文件,不能把桌面版本也写成已经发布。
做完这张卷子后
这次工作给我留下的不是一个「一键出卷」按钮,而是一套更具体的顺序:先确认材料和题意,再让模型整理候选答案,接着把空位、提示和分数变成可检查的数据,让老师修改和确认,最后从同一份修订生成学生版与答案版。任何一步省掉,后面看似成功的导出都可能只是一张无法放心印发的纸。
class 最初让我关心的是老师的资料能否留在一个工作台里。这张周清卷把问题往前推了一步:资料进入系统后,老师的判断、修改与交付文件能否连成一条不中断的线。那天最让我踏实的验收结果,不是模型一次答对了多少,而是改完一道具体的题以后,保存、预览、Word 和 PDF 里看到的仍是同一道题。
以后再做类似功能,我会更早拿一份真实材料走完整个过程。看它在哪里读错、在哪里需要老师停下来判断、改过后又能否顺利拿去打印,比只盯着生成按钮的速度更能说明产品是否帮上了忙。
