把大学物理实验报告做成 Agent Skill
把大学物理实验报告做成 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 也就设计成两种模式:
预习模式(上课前,我给实验名称)
- 读
.profile取个人信息,缺字段必须先问,不许猜 - 确认实验名称——这一步不能省,后面细说
- 画原理图
- 生成报告 docx:信息行 + 目的/仪器/原理/操作步骤 + 插图
- 导出 PDF 到
提交/
数据模式(实验后,我把仪器读数发过去)
- 转录数据,逐格核对
- 自洽性校核:分光计两游标读数差应约等于 180°;偏向角 δ 应约等于两游标各自算出的 |α₁−α₂| 的平均值。对不上就报出来,不擅自改数据
- 填实验日期 + 原始数据记录表
- 重新导出 PDF,同名覆盖
个人信息单独抽成一份 ~/物理实验报告/.profile,纯文本 键: 值:
1 | 姓名: <姓名> |
它是唯一信息源。Skill 每次重新读,字段缺失就停下来问,绝不在报告里硬编码——不然换学期、换老师就要满仓库改。
几个约定也定死在这一层:
- 只保留最终稿,预习稿和终稿同名,直接覆盖,不做版本管理;
- 实验日期在实验后才填,预习阶段该栏留空;
- 提交文件名固定
<座位号>-<姓名>-<实验名称>.pdf(连字符分隔)。
4.原理图必须自己画
老师要求配图,第一反应是去网上找。我放弃了这条路,理由有三条:
- 版权不明——教学 PPT 里的图多半有出处,公开博客上挂着不合适;
- 分辨率不可控——截图放大就糊,打印出来更糟;
- 符号系统对不上——网图里的角度标记、变量名往往和我的正文公式不一致。
最重要的是第三条。自己画能保证图里的几何量和正文公式严格自洽。最小偏向角那张图就是典型:正文写 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_RPrschema 约束: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 | conda create -n physlab -y -c conda-forge \ |
一次解决,之后画图直接用 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 把整份报告重新跑一遍。结果暴露出两个静态检查发现不了的问题:
build阶段漏填原始数据记录表的“实验名称”和“姓名”。 之前那份能对,是因为我在更早的手工流程里填过,脚本本身根本没写这段逻辑;- 大格行高导致记录表拆页(就是 5.4 里那个)。
如果我只看“当前这份报告是对的”就收工,这两个 bug 会一直潜伏到下一次实验才炸。
结论:Skill 写没写完,不看代码,看能不能从零跑通。
7.目录纪律:提交区只能放最终产物
项目结构是这样的:
1 | ~/物理实验报告/ |
期间出了个小插曲:同一台机器上另一个 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 | physics-lab-report/ |
用起来就是五条命令:
1 | lab.py profile # 看个人信息缺什么 |
回头看,真正有价值的不只是“省下十几个小时”,而是几条可以搬到别的项目上的经验:
- 规范常常藏在文档里而不是文档外——先读模板,再写 prompt;
- 锚点要来自不变量——用固定标签定位,别用会变的内容;
- 能自己生成的图就别去网上找——可控、自洽、没版权问题;
- Skill 的验收标准是从零跑通,不是“这次结果是好的”;
- 约定要写下来,否则下一个 agent(或者三个月后的自己)一定会踩;
- 会变的需求别硬编码——命名规则收进一个函数,可选项做成开关,规范改了才不用推倒重来。
项目已开源:physics-lab-report-skill · MIT License
署名
- 实际作者:DeepSeekV41Flash(本文由 AI 生成)
- 参与编辑:Harekasa(人工审核校对)
- 日期:2026-09-18
