把大学物理实验报告做成 Agent Skill

文中姓名、学号、座位号等个人信息均以占位符代替;实验报告的模板与格式要求,请以学校当期发布为准。工作流仅作学习使用,鼓励所有同学在遵守学校规定的前提下,探索适合自己的自动化工具。严肃反对任何学术不端行为。

项目地址:physics-lab-report-skill(MIT;不含任何学校的模板文件,模板需自备)

1.问题提出

大学物理实验这门课有件事很磨人:每做一次实验就要交一份格式完全固定的报告。而且不是交一次就完事——上课前交预习报告,做完实验把原始数据填进记录表,最后合成 PDF 提交。

拆开看,每次的重复劳动是这么几块:

  • 个人信息:封面 5 项(课程名称/姓名/学号/专业班级/开课学期),正文信息行 6 项(实验日期/姓名/座位号/实验时段/专业/指导老师)。每次都要重敲。
  • 正文:实验目的、实验仪器、实验原理、实验操作步骤。结构固定,内容随实验变。
  • 原理图:老师明确要求配图。
  • 原始数据:实验结束后照抄仪器读数,还要保证两游标读数差、偏向角这些关系自洽。
  • 格式:模板里用红字写着“正文宋体,小四号,1.5 倍行距”,Word 里每次都得再确认一遍没跑偏。
  • 命名:交上去的文件必须叫 <座位号>-<姓名>-<实验名称>.pdf。

手工做一次,从整理内容到排版导出,一个小时起步。一学期十来次实验,就是十几个小时花在排版上。

所以我给它写了一个 Agent Skill。

2.先把“规范”从模板里挖出来

写 Skill 的第一个坑,是急着写 prompt,而不是先把规范读明白。

学校发的 实验报告模板2026秋.docx 里有一堆红色文字。第一次看会以为那是“待填内容”,其实它们是排版说明——老师把要求直接写在模板里了。全篇一共 5 处:

红字内容 实际含义
8.12 用双光栅测量微弱振动位移 示例实验名,不是让你做这个
(红色为式样或说明,自行删除) 红字全部要删掉
(正文宋体,小四号,1.5倍行距) 正文格式要求
(位置不够,可自行加页) ×2 内容超页可以加页

也就是说,格式规范不在任何一份单独的文档里写着,它藏在模板的红字批注里。如果不先把模板拆开读一遍,Skill 里根本不知道该用什么字号。

于是第一步不是写代码,是做一份干净模板:删掉全部红字,只留空白表单结构。这份干净模板成为后面所有生成的基线。

模板的“实验报告撰写要求”页还写着完整报告由“预习 + 数据记录 + 课后处理”三部分组成——这条后来成了整个设计里最反复的一处,第 8 节细说。

3.工作流:两种模式,一份 .profile

报告的生命周期天然分成两段,Skill 也就设计成两种模式:

预习模式(上课前,我给实验名称)

  1. 读 .profile 取个人信息,缺字段必须先问,不许猜
  2. 确认实验名称——这一步不能省,后面细说
  3. 画原理图
  4. 生成报告 docx:信息行 + 目的/仪器/原理/操作步骤 + 插图
  5. 导出 PDF 到 提交/

数据模式(实验后,我把仪器读数发过去)

  1. 转录数据,逐格核对
  2. 自洽性校核:分光计两游标读数差应约等于 180°;偏向角 δ 应约等于两游标各自算出的 |α₁−α₂| 的平均值。对不上就报出来,不擅自改数据
  3. 填实验日期 + 原始数据记录表
  4. 重新导出 PDF,同名覆盖

个人信息单独抽成一份 ~/物理实验报告/.profile,纯文本 键: 值:

1
2
3
4
5
6
姓名: <姓名>
学号: <学号>
座位号: <座位号>
专业班级: <班级>
指导老师: <老师>
实验时段: <时段>

它是唯一信息源。Skill 每次重新读,字段缺失就停下来问,绝不在报告里硬编码——不然换学期、换老师就要满仓库改。

几个约定也定死在这一层:

  • 只保留最终稿,预习稿和终稿同名,直接覆盖,不做版本管理;
  • 实验日期在实验后才填,预习阶段该栏留空;
  • 提交文件名固定 <座位号>-<姓名>-<实验名称>.pdf(连字符分隔)。

4.原理图必须自己画

老师要求配图,第一反应是去网上找。我放弃了这条路,理由有三条:

  1. 版权不明——教学 PPT 里的图多半有出处,公开博客上挂着不合适;
  2. 分辨率不可控——截图放大就糊,打印出来更糟;
  3. 符号系统对不上——网图里的角度标记、变量名往往和我的正文公式不一致。

最重要的是第三条。自己画能保证图里的几何量和正文公式严格自洽。最小偏向角那张图就是典型:正文写 n = sin[(A + δmin)/2] / sin(A/2),那张图里就不能随便画个角度了事。我是先按 A = 60°、n = 1.6 反解出 r = 30°、i = 53.13°、δmin = 46.26°,再把这些数代进去画线,脚本运行时还会把这三个值打印出来核对。

于是我把绘图封装成一个 figstyle.py,基于 matplotlib,提供 line / dashed / arrow / mid_arrow / poly / circle / arc / angle / angle_dim / right_angle / text / leader 这些图元。几个设计上的取舍:

  • 坐标一律 y 轴向上,角度用度、逆时针为正——和数学课本一致,写公式时不用做心算转换;
  • mid_arrow 把箭头放在线段中段,而不是顶点。光路图里有入射点、出射点这种多线交汇的顶点,箭头堆在那里会糊成一团;
  • leader(目标点, 文字位置, 文字) 自动加引线和小圆点,结构图的标注不用手算偏移;
  • 参考文档里放两个可直接运行的完整范例(结构类图、光路类图),下次照着改就行。

5.工程上最值得记的几件事

5.1 docx 不是普通文档格式

我第一版是直接改 OOXML 的,踩了一串坑:

  • 中文字体必须同时设 w:eastAsia。只设 ascii/hAnsi,中文会回落成默认字体,你以为设了宋体其实没设;
  • 下标必须用 vertAlign,不能用 Unicode 下标字符 ₁₂₀。宋体缺这些字形,Word 里会显示成方框。表格里的 α₁、δ₀ 全中招;
  • rPr 子元素的顺序受 CT_RPr schema 约束:rFonts → b → bCs → sz → szCs → vertAlign → u。顺序错了严格的校验器直接报错;
  • 表格边框 python-docx 没有 API,只能塞 XML。

结论很简单:别手搓 OOXML,用 python-docx 把格式封装起来。style_run() 和 cell_para() 两个函数吃掉了上面所有坑,Skill 层只写“这段是加粗标题、这段是正文”,不碰底层。

5.2 定位锚点不能用正文内容

这是我犯的一个设计错误。第一版插图时,我是按正文句子定位的——“找到以『分光计由望远镜、平行光管』开头的段落,把图插在它后面”。

这在当次实验里能跑通,但换个实验就完全失效,因为正文是每次生成的,根本不能当锚点。

正确做法是只用模板自带的固定标签定位:实验日期:、实验时段:、实验目的:、原始数据记录表……这些标签在任何一次实验里都在同一个位置。正文表也靠 cell(0,0) 文本以“实验目的”开头来识别。

一句话:锚点必须来自不变量,不能来自变量。

5.3 别和坏掉的依赖较劲

系统 Python 里的 matplotlib 是坏的——报 numpy.core.multiarray failed to import,典型的 numpy 2.x 与旧 matplotlib 二进制不兼容。我一开始用 Pillow 手绘绕过去了,能画但很别扭:弧线、箭头、字体都要自己算。

后来干脆建了专用环境:

1
2
conda create -n physlab -y -c conda-forge \
python=3.12 python-docx pillow matplotlib numpy

一次解决,之后画图直接用 matplotlib,代码短了一半。遇到环境问题,“绕过去”往往比“修好它”更贵。

5.4 排版是玄学,必须目视

有两个坑,代码层面完全正确,渲染出来就是不对:

一是 LibreOffice 不认表格单元格里的“与下段同页”。 python-docx 的 keep_with_next 写进去了,Word 里有效,但 LibreOffice 转 PDF 时照样把插图和题注拆到两页。解决办法只能是导出后把 PDF 转成图片目视核对,发现拆页就把图宽缩 0.5cm 重来。

二是记录表那个 21cm 的大格。 模板里原始数据记录表最后一行是“至少 21cm”,留白给手写。但数据是打进去的,这一行加上前面的表头就超出一页,结果表头留在上一页、数据跑到下一页。改成 15cm 自适应,并在记录表标题上加 page_break_before,整张表才稳定落在独立一页。

这两件事教会我一件事:排版正确性没法靠单元测试保证,只能看。

5.5 matplotlib 的两个小坑

顺手记两个绘图细节:

  • 画布长宽比要等于数据范围的长宽比,即 h/w = Δy/Δx。否则 aspect='equal' 会在图里留出大片空白边,插进文档后图显得特别小。我那两张光路图一开始四周全是白边,就是这个原因;
  • dot() 的半径别用 points。我最初用 marker='o', ms=r*260,ms 是 points 而 r 是数据坐标,换个坐标范围点的大小就失控——第一版结构图上所有引线端点全变成了大黑疙瘩。改成 matplotlib.patches.Circle、半径用数据坐标才对。

6.实测暴露的两个真 bug

这部分我想单独说,因为它推翻了我对“写完了”的判断。

Skill 的脚本写完之后,我做了一次从零重建:删掉中间产物,用干净模板 + spec.json + data.json 把整份报告重新跑一遍。结果暴露出两个静态检查发现不了的问题:

  1. build 阶段漏填原始数据记录表的“实验名称”和“姓名”。 之前那份能对,是因为我在更早的手工流程里填过,脚本本身根本没写这段逻辑;
  2. 大格行高导致记录表拆页(就是 5.4 里那个)。

如果我只看“当前这份报告是对的”就收工,这两个 bug 会一直潜伏到下一次实验才炸。

结论:Skill 写没写完,不看代码,看能不能从零跑通。

7.目录纪律:提交区只能放最终产物

项目结构是这样的:

1
2
3
4
5
6
~/物理实验报告/
├── .profile 个人信息(唯一来源)
├── README.md 目录说明 + 纪律
├── templates/ 干净模板 + 原始模板
├── .build/<名称>/ 所有中间产物(docx/图/脚本/json)
└── 提交/<座位号>-<姓名>-<名称>.pdf 最终交付

期间出了个小插曲:同一台机器上另一个 agent 不知道这套约定,把 .docx 和一张调试用的 .png 丢进了 提交/。

这类问题的根因不是“文件放错了”,而是约定只存在于我脑子里。所以补了三处:

  • SKILL.md 里写明“提交区纪律”,并说明非实验类作业(比如绪论作业)走同一套约定;
  • 在 ~/物理实验报告/README.md 写清目录树、三条纪律、命令速查,任何 agent 进来先看这个;
  • 检查脚本里加一条:ls 提交/ 出现非 PDF 就报警。

顺便还发现那个 agent 的生成脚本第 27 行写着 OUT = os.path.join(ROOT, "提交", "xxx.docx")——它把 docx 直接写进提交区。这才是文件跑错地方的真正源头。改掉输出路径,问题从根上没了。

8.老师改了要求之后

上线没几天,老师在群里发了新的要求。对照下来有三处冲突:

项 原实现 新要求
报告首页 封面页 + 学生实验守则 + 撰写要求 实验报告不要这两页,只有绪论课作业才交
完整报告组成 原理步骤 → 原始数据记录表 原理步骤 → 数据处理 → 分析讨论 → 附件:原始数据记录表
绪论课作业 内容顺序排列 老师指定的大方框形式

最尴尬的是“数据处理”那一栏。项目刚开始时我判断课后处理不属于这份交付物,把它从模板里删掉了,还在文档里写死“以后都不用”。结果老师给的“完整实验报告组成”里,数据处理和分析讨论恰恰是第 2、3 部分。

这件事的教训不是“当初不该删”,而是:我把一个会变的决定硬编码进了模板和文档。

改法里有三处值得记:

  • 命名规范只存在于一个函数里(submission_name())。改一个格式串就够了,不用满仓库找散落的字符串;
  • “去封面”做成开关,而不是把模板删掉:strip_front_matter() 以正文标题那段为界,把之前的内容整段删掉;需要封面的(绪论课作业)传 front_matter=True。一份模板同时服务两种交付物;
  • 锚点用结构而非内容:strip_front_matter() 找的是“文本恰好是『实验』的那一段”,所以模板以后加栏目、删栏目都不影响它。

顺带还揪出两个潜伏的 bug:绪论作业里图片高度上限拿英寸去比厘米,max_h=8.3 实际被当成 8.3 英寸(约 21cm),限制形同虚设;以及删除某段文字时用了等值判断,而那段文字在运行时其实是重复三遍的,判断一直落空、标题始终没被删掉。两个都是“看起来在正常工作”的那种错。

9.结语

这个 Skill 最终的形态是:

1
2
3
4
5
6
7
8
9
physics-lab-report/
├── SKILL.md 工作流 + 硬规则 + 已知坑
├── scripts/
│ ├── labkit.py docx 操作库(格式、锚点、表格、导出、自检)
│ ├── lab.py 命令行入口(profile/build/data/export/check)
│ └── figstyle.py 原理图绘制库
└── reference/
├── format.md 模板结构、锚点位置、字段对照
└── figures.md 绘图指南 + 两个可运行范例

用起来就是五条命令:

1
2
3
4
5
lab.py profile                             # 看个人信息缺什么
lab.py build .build/<名称>/spec.json # 预习模式:生成报告
lab.py data .build/<名称>/data.json # 数据模式:填日期与原始数据
lab.py export "<实验名称>" # 导出 PDF 到 提交/
lab.py check "<实验名称>" --figs 3 # 交付前自检

回头看,真正有价值的不只是“省下十几个小时”,而是几条可以搬到别的项目上的经验:

  • 规范常常藏在文档里而不是文档外——先读模板,再写 prompt;
  • 锚点要来自不变量——用固定标签定位,别用会变的内容;
  • 能自己生成的图就别去网上找——可控、自洽、没版权问题;
  • Skill 的验收标准是从零跑通,不是“这次结果是好的”;
  • 约定要写下来,否则下一个 agent(或者三个月后的自己)一定会踩;
  • 会变的需求别硬编码——命名规则收进一个函数,可选项做成开关,规范改了才不用推倒重来。

项目已开源:physics-lab-report-skill · MIT License

署名

  • 实际作者:DeepSeekV41Flash(本文由 AI 生成)
  • 参与编辑:Harekasa(人工审核校对)
  • 日期:2026-09-18