SKILL 24 / DATA · DEEP-DIVE

Plan-Validate-Execute 模式

先交一份能被机器骂回来的计划 · 官方 pdf / docx / xlsx 里的真实校验脚本 · 四个验证器四种退出码契约

2026-09-25 · BigCat

上期讲 reference 该拆到哪一层。这期讲那个 scripts/ 目录里到底该放什么——答案是验证器。官方 pdf / docx / xlsx / pptx 四个 skill 的校验脚本逐个打开看,包括它们互相矛盾的退出码。

前置 → Skill 23 按域拆 Reference 的 Skill 结构(三层落位里 scripts/ 那一格)

// WHY THIS MATTERS

批量改 50 个表单域、给一份合同加红线、往财务模型里铺一排公式——这类活的共同点是:模型一边想一边改,错了你还不知道错在哪一步。官方给这类活起了个名字,叫 plan-validate-execute:先让模型把打算做的事写成一份结构化文件,用脚本把这份文件骂一遍,过了才允许动原件。Skill authoring best practices 里有一整节叫 Create verifiable intermediate outputs 专门讲它。本期不停在文档上——把 anthropics/skills 里真实存在的八九个校验脚本一个个打开,看它们怎么写报错、什么时候退 1、什么时候明明报错却退 0。

// 01

它替你干掉什么:批量操作里「一边想一边改」

论断:这套模式买的不是正确性,是可回滚的思考过程——计划是一份 JSON,改它零成本;原件被改坏了,成本不一定是零。

官方原文给的适用范围

best-practices 那一节把场景写死了四项:Batch operations, destructive changes, complex validation rules, high-stakes operations. 举的例子也很具体——按一张表格去更新 PDF 里 50 个表单域,没有校验的话模型可能「引用不存在的域、造出互相冲突的值、漏掉必填项、或者把更新打错位置」。流程官方写成五步:analyze → create plan file → validate plan → execute → verify。

安装命令(两个 plugin,四个 skill 全在第一个里)

# pdf / docx / xlsx / pptx 四件套 —— 本期主角全在这里
/plugin marketplace add anthropics/skills
/plugin install document-skills@anthropic-agent-skills

# skill-creator / mcp-builder 等(03 节会用到)
/plugin install example-skills@anthropic-agent-skills

不装 plugin 也行——这四个 skill 目录可以直接拷进 ~/.claude/skills/ 或 .claude/skills/,scripts/ 里的 Python 文件本来就是独立可跑的。什么时候别装:你根本不碰 Office / PDF 文件,那这四个装了纯交 metadata 税;但 scripts/ 里的写法值得读,本期就是干这个。

第一条立刻能跑的用法:填一份可填写 PDF

skills/pdf/forms.md 开头第一句就是 CRITICAL: You MUST complete these steps in order. Do not skip ahead to writing code. 四条命令,在 skills/pdf/ 目录下跑:

# ① 探:有没有可填写域(注意:文档里这行漏了 .py,见 04 节)
python scripts/check_fillable_fields.py form.pdf

# ② 析:把真实域抽成 JSON —— field_id / page / rect / type
python scripts/extract_form_field_info.py form.pdf field_info.json

# ③ 计划:你自己写 field_values.json,field_id 必须与上一步一致
#    [{"field_id":"last_name","description":"...","page":1,"value":"Simpson"}]

# ④ 执行:脚本先校验 ID 与值域,不过就 exit 1,不写输出
python scripts/fill_fillable_fields.py form.pdf field_values.json out.pdf

第 ④ 步是整套模式的缩影:fill_fillable_fields.py 在写任何字节之前先对着 field_info.json 把你的计划核一遍,报错长这样——ERROR: Invalid value "X" for radio group field "citizenship". Valid values are: [...]。它把合法取值全列出来,正是官方那条 implementation tip 的实现:报错要 verbose 到模型能照着改。

出处 · Anthropic · Skill authoring best practices 的 Create verifiable intermediate outputs / Workflows and feedback loops 两节(2026-09 访问)· 脚本行为读自 skills/pdf/scripts 的 main 分支源码,2026-09
// 02

机制:让「打算做什么」变成一个能被集合比对的对象

论断:这套模式能成立只有一个前提——计划里的每个键,都能和一份真实存在的清单做集合比对。field_id 能,「把标题改得更专业些」不能。

官方目录里其实有两种变体,别混着用

A. 校验计划(plan-first)—— pdf extract_form_field_info.py → 你写 field_values.json → fill_fillable_fields.py 先核 ID/值域,不过则 exit 1,原件没动过 extract_form_structure.py → 你写 fields.json(含 bounding box) → check_bounding_boxes.py 独立校验 → fill_pdf_form_with_annotations.py → convert_pdf_to_images.py 看图复核 B. 校验产物(execute-then-validate)—— docx / pptx / xlsx 改 word/document.xml → zip → office/validate.py(XSD + 关系 + 内容类型) openpyxl 写公式 → recalc.py(LibreOffice 全量重算) pptxgenjs writeFile → office/validate.py(图表 + 幻灯片 XML)

差别是「错误在哪一刻被发现」。A 类的代价是你得先有一份可枚举的真实清单(PDF 的域表、表格的行边界);没有这份清单,计划里的键就没法比对,模式退化成一场仪式。B 类不需要事先枚举,代价是原件已经被改过了——docx 的做法是用 --original 兜底:python scripts/office/validate.py out.docx --original doc.docx 会把 XSD 检查基线对到原件上,原件自带的错就不算你的,还有 --auto-repair 修常见问题。

为什么中间产物必须是 JSON,不能是散文

一个容易写反的地方:check_bounding_boxes.py 的相交检查同时用到 entry_bounding_box 和 label_bounding_box。只写 entry 不写 label,它就查不出「文字压到标签上」这类错——而这恰好是最常犯的一类。计划的 schema 少一个字段,验证器就瞎一只眼,而且不会告诉你它瞎了。
// 03

真实用法与边界:五条链路,逐个给命令

论断:验证器的位置不是随便放的。归一化要在写计划之前,清理要在结构定稿之后,看图复核永远在最后——顺序错了,每一步都在校验一个已经不成立的东西。

① 非填充式 PDF:计划里连坐标都要先被校验

python scripts/extract_form_structure.py in.pdf form_structure.json  # 标签/横线/复选框坐标
# 你写 fields.json;pdf_width/pdf_height 表示 PDF 坐标,image_* 表示像素坐标
python scripts/check_bounding_boxes.py fields.json                    # 先校验,再填
python scripts/fill_pdf_form_with_annotations.py in.pdf fields.json out.pdf
python scripts/convert_pdf_to_images.py out.pdf verify/              # 最后看图

同目录还有一个 create_validation_image.py,把 entry 框画红、label 框画蓝叠在页面图上——forms.md 里没提它,但它是「让模型用眼睛验计划」最直接的一个工具。什么时候别用这条链路:PDF 本来就有可填写域(先跑 check_fillable_fields.py 判一下),手算坐标纯属自找麻烦。

② docx 红线:归一化必须在写计划之前

unzip -q doc.docx -d unpacked/ && find unpacked -type l -delete
python scripts/merge_runs.py unpacked/     # 先合并碎片 run,否则文字在 XML 里不是连续串
# 编辑 unpacked/word/document.xml —— 不要重排、不要 pretty-print
(cd unpacked && zip -Xr ../out.docx .)
python scripts/office/validate.py out.docx --original doc.docx --author "Reviewer"

merge_runs.py 替你干掉的那件事,是 Word 把一句话切成一堆 <w:r> 导致「你看得见的短语在 XML 里并不存在」。它是 plan 的前置条件:不先归一化,计划里写的定位字符串根本匹配不上。--author 则是一个很妙的 plan 校验——它报出「你改了文字却没包 <w:ins>/<w:del>」的地方,官方原话说这种错 is easy to do by accident and invisible in the accepted view。出清洁版用 accept_changes.py,但它和 pandoc --track-changes=accept 都有同一个已知缺陷(删掉段落标记时留下空段落),官方自己写在 SKILL.md 里。

③ pptx:清理必须在结构定稿之后

skills/pptx/scripts 里 clean.py 的说明只有一句硬约束:Run after <p:sldIdLst> is final——它删的是「已经没人引用」的幻灯片和媒体,结构还在变就跑,删的可能是你下一步要用的。thumbnail.py 是选版式阶段的「看图做计划」,但第二个参数必须传:默认前缀 thumbnails 会让同目录第二份 deck 静默覆盖第一份的网格图。

④ xlsx:验证器是强制的,而且它只证明「能算」

recalc.py 是本期唯一被写成 mandatory 的验证器(openpyxl 写公式不带缓存值,不重算的话 pandas 读回来全是 None)。但 SKILL.md 自己按了刹车:A green recalc proves your formulas evaluate, not that they are right. 差一行的区间会产出一个干净无错、数字全错的文件。它给的对策是行为上的:先写 2–3 个公式核对结果,再铺整张网格。

⑤ 把这套用在 skill 自己身上

skill-creator 的 SKILL.md 给了两条命令:python -m scripts.run_loop --eval-set <trigger-eval.json> --skill-path <skill> 和 python -m scripts.package_skill <path/to/skill-folder>;scripts/ 下还有 quick_validate.py / run_eval.py / aggregate_benchmark.py。它强调的顺序是 GENERATE THE EVAL VIEWER BEFORE evaluating inputs yourself——先把判据固化成可跑的东西,再动手判。什么时候别上:只有一个 SKILL.md、你自己一个人用,写 eval 的成本大于收益。

⑥ 模型会跳过验证器——这时候才该上 hooks

以上全部依赖模型肯按顺序跑。真要钉死,就把它挪出模型的判断范围:Claude Code hooks 的 PreToolUse 在工具调用前触发、exit code 2 直接拦掉(官方原话:On events that can block, exit 2 blocks whether or not you print JSON),拦截理由可以用 permissionDecision: "deny" + permissionDecisionReason 精确回传给模型。PostToolUse 拦不住(工具已经跑了),exit 2 只是把 stderr 作为警告递给模型。什么时候别上 hooks:一次性任务、或者校验本身需要判断力——hooks 是确定性闸门,不是审稿人(详见 Skill 27)。

三条判据,用来在上面这几条链路之间取舍

  1. 有没有一个不依赖模型判断的 oracle?XSD、域表、公式错误码都是。没有 oracle 就别搭这套——你只是让模型自己批自己的作业,还多烧一轮 token。
  2. 计划的键能不能和真实清单做集合比对?能(field_id、单元格地址、slideN.xml)→ 走 A 类,先校验计划。只能「看起来对」(文案、版式美感)→ 走 B 类或直接看图,别硬造 JSON。
  3. 失败可逆吗?不可逆或批量 → A 类,官方那四项适用范围就是这条。单文件、可重跑、五分钟能重来 → B 类够了,docx / xlsx 全系都是 B 类。
// 04

代价与替代品:四个官方验证器,四种退出码契约

论断:这是本期最该带走的一条——你不能假设「验证器退 0 就是过了」。同一个仓库里的四个脚本,对这件事的答案互不相同,而且都没写在显眼处。

逐个读源码得到的退出码表

脚本 发现问题时 退出码 ────────────────────────────────────────────────────────────────────── fill_fillable_fields.py print ERROR + 不写输出 1 ✅ fail-closed office/validate.py 报告失败项 1 ✅ fail-closed recalc.py JSON status: "errors_found" 0 ⚠️ 静默 check_bounding_boxes.py print "FAILURE: ..." 0 ⚠️ 静默 recalc.py 的最后一行是:sys.exit(1 if "error" in result else 0) → 只有「压根没算成」(JSON 里有 error 键)才退 1;算成了但一堆 #REF! 退 0。 check_bounding_boxes.py 全程没有 sys.exit(1):框全相交也退 0。

后果很具体:你在 CI 或 hook 里写 python scripts/recalc.py out.xlsx && echo OK,一份满是 #NAME? 的工作簿会一路绿灯过去。xlsx 的 SKILL.md 明确警告过这件事——errors_found exits 0, so never treat a clean exit as a clean workbook——但 check_bounding_boxes.py 的同类行为没有任何文档提醒,是读源码才看到的。把这两个接进自动化时,必须解析它们的 stdout,不能只看 $?。

两处「照抄文档就会失败」的地方

① forms.md 第一条命令漏了扩展名。原文写 python scripts/check_fillable_fields <file.pdf>,仓库里的真实文件名是 check_fillable_fields.py(同目录其余七个脚本都带 .py)。照抄即 No such file or directory。
② best-practices 里的示例脚本名,一个都不存在。那节 PDF 工作流示范用的是 analyze_form.py / validate_fields.py / fill_form.py / verify_output.py,真实 skills/pdf/scripts/ 里是 extract_form_field_info.py / check_bounding_boxes.py / fill_fillable_fields.py / convert_pdf_to_images.py。文档教的是模式,不是命令——命令永远从 ls 的输出里抄。

三笔账

不上这套,你还能怎么办

  1. B 类:直接做完再校验。docx / xlsx / pptx 三个官方 skill 全是这么干的——它们连 plan 文件都没有。可重跑的单文件操作,这就够了。
  2. 把闸门挪到 hooks。PreToolUse exit 2,不给模型跳过的机会(Skill 27)。
  3. 用工具自带的 dry-run。能让上游工具替你做 --check 的,就别自己造中间格式——少一份要维护的 schema。
  4. 让另一个实例复核。上下文隔离的第二双眼睛,代价是协调税(Skill 26)。
  5. 就看图。convert_pdf_to_images.py / thumbnail.py / soffice.py --convert-to pdf + pdftoppm 这条路线,官方四件套全都保留了——因为有些东西只有眼睛能验。
最后一条诚实的代价:这套模式提高的是「发现错误的时间点」,不是「错误的数量」。一份通过全部校验的 fields.json 照样能把客户的名字填进出生日期栏——因为没有任何脚本知道那一栏该填什么。校验器守的是格式,不是意图;意图仍然只能你自己看一眼。

// 一页选购卡

先读这三份:① skills/pdf/forms.md——全仓库唯一一份把 plan-validate-execute 从头到尾写完的文件,两条链路(可填写域 / 坐标标注)都在里面。② fill_fillable_fields.py——98 行,把「校验后再写」和「报错要列出合法取值」两件事写成了最短范例。③ best-practices 的 Create verifiable intermediate outputs 一节——模式的定名处与四条适用场景。

按需再看:check_bounding_boxes.py(独立 plan 校验器的写法,注意它退 0)· xlsx/scripts/recalc.py(产物校验 + JSON 契约 + 退出码陷阱)· docx/scripts/office/validate.py(--original 基线、--auto-repair、--author 红线检查)· merge_runs.py(写计划之前的归一化)· pptx/scripts(clean.py 的时序约束、thumbnail.py 的前缀坑)· skill-creator(run_loop.py / package_skill.py,把这套用在 skill 自己身上)· Claude Code hooks(PreToolUse exit 2 硬门)。

明确别做:给没有 oracle 的任务硬造 plan 文件 · 把验证器的退出码当真而不读 stdout · 从文档里抄脚本名(两处已证实与仓库不符)· 在 <p:sldIdLst> 定稿前跑 clean.py · 在 merge_runs.py 之前写 docx 的定位计划 · 把「recalc 绿了」当成「数字对了」。

// 术语表

plan-validate-execute
官方给这套模式的名字:模型先把打算做的改动写成结构化文件,脚本校验这份文件,过了才执行,最后再验产物。适用于批量、破坏性、高风险、规则复杂的操作。
verifiable intermediate output(可校验中间产物)
那份 JSON 计划本身。「可校验」的硬标准是:它的每个键都能和一份真实存在的清单做集合比对。
oracle(判定依据)
不依赖模型判断就能说「对/错」的东西——XSD、表单域表、公式错误码。没有 oracle,这套模式就只是仪式。
fail-closed / fail-open
发现问题时拒绝继续(退非 0、不写输出)叫 fail-closed;照样退 0 让调用方自己去读 stdout 叫 fail-open。官方四个验证器两种都有。
execute-then-validate
没有 plan 文件、改完再校验产物的变体。docx / xlsx / pptx 全系都是这一类,靠 --original 或重算兜底。
归一化(normalization)
写计划之前把原件整理成可定位的形态。merge_runs.py 合并碎片 run 就是典型:不做这一步,计划里的定位字符串在 XML 里根本不存在。
PreToolUse hook
Claude Code 在工具调用前触发的钩子,exit 2 直接拦掉该次调用。把「必须先跑验证器」从模型的自觉变成硬约束的唯一办法。

// 延伸阅读