aria / blog

AI 批改实验报告:最难的不是评分,是 Word

高校合作项目里做实验报告批改智能体:DOCX 解析、OMML 公式提取、向量查重、Dify 评分工作流、SSE 流式输出。真正花功夫的不是调模型,是让 Word 里的东西忠实地到达模型眼前——包括一版会被拍平的公式坑出冤枉扣分的实测。

这个项目的目标说出来很轻巧:学生上传 Word 实验报告,AI 自动批改出分。合作方是浙大,场景是真实课堂,实验报告一批几十份,老师批不过来,希望机器先过一遍。

设想里的链路是:解析 Word、对齐模板章节、查重、评分、流式吐结果。评分和查重的部分,模型能力是够的。把工期吃掉的是第一步:把一个 .docx 变成模型能看懂的东西。这篇文章记这条路上的坑,从最经典的一个说起。

公式在 .text 里根本不存在

python-docx 是最顺手的起点,直到发现一个事实:paragraph.text 里没有公式。

Word 的公式存在 OOXML 的 m:oMath 节点里,python-docx 的文本提取只认 w:t。一份流体力学实验报告,正文里的伯努利方程、雷诺数表达式,在 .text 的输出里全部消失。章节文字都在,公式位置直接空洞。实验报告的核心就是公式,这已经不算细节缺失了。

所以公式提取得自己做:直接操作底层 XML,把 m:oMath 节点找出来,取 m:t 文本。到这里只解决了”有没有”,下一个坑紧跟而来。

拍平的公式,害模型扣了冤枉分

公式提出来之后,第一版做法很朴素:把节点里的文本顺序拼起来。分数 \frac{a}{b} 拼出来就是 ab,上下标的关系全部丢失。

测试的时候栽了个跟头。有一份真实的报告,学生公式写对了,模型却给了”公式表达混乱”的扣分。排查到喂给模型的文本才发现:一个结构清晰的分式,在拍平之后变成了无意义的一串符号。模型没批错,它看到的东西确实是乱的。

这件事之后我记下一条:模型误判,先查喂给它的东西。

同一个分式的两种喂法

修法是结构感知的渲染,递归处理 OMML 节点,分式渲染成 (分子)/(分母),上下标带明确标记:

def _render_omml_node(el) -> str:
    # 分式:m:num / m:den 分别递归,保留结构
    n = _render_omml_children(el.find(_OMML_NS + "num"))
    d = _render_omml_children(el.find(_OMML_NS + "den"))
    if n or d:
        return f"({n})/({d})"
    e = _render_omml_children(el.find(_OMML_NS + "e"))
    ...

另外正文和公式要按文档顺序穿插拼接。OOXML 里段落的孩子节点既有 w:r(文字)也有 m:oMath(行内公式),先取完文字再取公式,会得到正文空洞、公式堆在段尾的畸形文本。顺序本身就是信息。

Symbol 字体的老古董

第三个坑更冷门。老版 Word 的公式编辑器(EQ 域那一代)把希腊字母和运算符存在 Symbol 字体的私有区码位上,直接取文本得到的是一串看不见的方块,校准系数 ξ 直接失踪。

解法是一张 Symbol 字体映射表,把私有区码位翻译回可读字符。这种东西没有任何文档会提醒你,只有真实文档扑上来才知道。

章节切开,和模板对齐

前面都是”取到内容”,还差”取对结构”。批改的前提是学生报告的”实验原理”对齐模板的”实验原理”,章节切分靠 Word 的编号定义(numbering.xml 里的多级编号)加启发式:编号层级变了、编号重启了、样式变了,都可能是章节边界。切出来的结果对齐模板章节,评分工作流才能按章节比对。

到这一步,一份 Word 才算真正”解析完成”。后面轮到模型上场。

查重:向量负责”像不像”,LLM 负责”哪里像”

查重分两级。

第一级是向量:每个章节正文过 embedding(Qwen3),存进 Milvus,检索相似章节。这里有个教训:reranker 重排匹配结果时,用章节正文,别用章节名。章节名毫无区分度,“实验原理""实验步骤”人人一样,按名重排约等于随机。代码里给无正文的匹配留了退回章节名的兜底,但那只是保底,日志会记警告。

第二级是 LLM 判定:向量检索说”这两节相似度 0.87”,但 0.87 意味着抄了、还是做了同一个实验的必然相似?这一步把两边的完整正文交给 LLM,让它判断是不是实质雷同,并输出具体匹配的文本片段。查重结论要能指着说”这两段一致”,老师才敢信。

评分与流式

评分主体是 Dify 工作流:查重分析一个流,内容评分一个流。内容评分的输出是结构化的扣分点列表,扣了什么、依据哪条标准、对应报告哪个位置,前端再渲染成批注。这一步的要点是让工作流输出可校验的结构,不能让它自由发挥写散文。

过程用 SSE 推给前端:解析中、查重中、评分中、每个章节的扣分点出来了……学生上传之后盯着页面,批改进度一段一段往外走,比对着 loading 转几十秒好受得多。

写在最后

这条链路做完,批改一份报告的全流程自动化跑通,两个智能体进了真实课堂。回头看工时的分布,模型调用相关的代码不到两成,八成在 DOCX 解析、章节对齐、坐标回标这些”脏活”上。项目交付之后很长一段时间,解析服务没有再为公式的事返过工。

← 返回列表