提示词契约体检
提示词是自然语言,但它的依赖全是实体:路径、脚本、命令、模型名、平台字段、权限声明。
这些东西没有编译期检查,所以它们不报错,只会以三种方式在几小时后暴露:
> ① 静默空跑 —— 第 1 步要读的文件在目标机上不存在,Agent 找不到就跳过,
> 任务"跑完了",产物还是昨天的。发现时已空转两天。
> ② 平台驳回 —— 描述里写了「本包不含 base64」,代码里 import base64。
> 平台按字面匹配,五次驳回,理由逐字相同。
> ③ 判据自相矛盾 —— 同一份报告里 confirmed 22.9 GB 大于 ceiling 15.8 GB,
> 结论却是 Reachable。没人算过两个数之间的关系。
这个技能把这三类都变成可数的 finding:每条带规则号、文件:行号、原话证据、为什么算问题、一行修法。
产物是机器可读的 findings.json,不是一段建议。同一输入跑两次,字节相同。
一句话的框架:代码有编译期,提示词没有。 上面三类都是编译器本来会拦住的东西 ——
名字解析不出来(D1)、源码在真正的解析器下读不出来(D3)、声明被实现打脸(D4)。
所以「指令的编译器」这个说法是对的,但有一处要当场说清:它找不出两段话在语义上
打架(一节写「三句话内回答」,另一节写「列 100 条」)。那需要语义,R603 属于这
一类,标的是 [manual]。DESIGN.md 第零节有完整的映射表和另外三条边界。
产物
| 产物 | 位置 | 解决什么 |
|---|---|---|
| 六维契约 | references/CONTRACT.md |
D1–D6 的定义、判据、检出方式、已知误报 |
| 规则表 | references/RULES.md |
逐条:维度 / 严重度 / 检测 / 修复 / 反例 |
| 平台硬约束 | references/PLATFORMS.md |
四家平台的解析器差异与字段规范,标明哪些是平台强制 |
| 事故库 | references/INCIDENTS.md |
11 条真实失效,带原话与根因,规则号是事后回填的 |
| 测量端契约 | references/COMPANION.md |
配套 MCP 服务器的七个工具、问题→入口映射、没装 MCP 时的等价命令 |
| 体检脚本 | scripts/audit.py |
目标 → findings.json / report.md / 退出码 |
何时使用
- 要发布一个 Skill,不确定
description会不会被目标平台的解析器读成空 - 要交一个定时任务给提示词驱动,怕它「跑完了但什么都没做」
- 平台驳回了一次但理由只有一句模板话,要自己先找出会被字面匹配命中的地方
- 从别人手里接手一份提示词,要 5 分钟内知道它依赖的哪些东西已经不存在
- 改了提示词的一句话,不确定有没有把某个隐式契约(字段数量 / 段落 / 权限)弄坏
- 要把提示词检查挂进 CI,需要一个返回退出码、结果稳定的命令
典型提问
体检这个 SKILL.md,看看发到 SkillHub 会不会被解析成空描述。这个 GEN-PROMPT.md 引用的文件,在这台机器上还都存在吗?Agensi 驳回了,说声明与实现不一致,把所有「声明 vs 代码」的冲突找出来。我的 SKILL.md 说有几个脚本,实际包里有几个?多出来的是谁?把这个体检挂进 CI,FAIL 阻断、WARN 只记日志。
快速开始
# 1) 体检一个 skill 包(退出码 0=PASS / 1=WARN / 2=FAIL / 4=目标不可读)
python scripts/audit.py path/to/skill-dir
# 2) 按目标平台的字段规范体检(额外跑 description 长度/段落/name 一致性)
python scripts/audit.py path/to/skill-dir --platform skillhub
# 3) 落盘:机器可读 + 人读
python scripts/audit.py path/to/skill-dir --platform agensi --json out/f.json --md out/r.md
# 4) 确定性门禁:内部跑两遍比对字节,不一致直接 FAIL
python scripts/audit.py path/to/skill-dir --double-run
# 5) 回归基线:改规则后必须跑,召回下降即阻断
python scripts/baseline.py
# 6) 变异测试:把 R501 故意改坏两次,断言两个夹具各自被打红
python tests/_mutate.py
# 7) 真的去测 D5:拿用户实际打出来的提问喂它
python scripts/audit.py path/to/skill-dir --triggers 我的提问.txt
triggers.txt 是唯一一个工具没法从包里得到的东西。 一行一条真实提问,空行与
# 注释忽略。给了它,D5 就变成规则 R501:凡与 description 一个字都不重的提问,
逐条报出来。不给它,报告里印的是 D5 触发覆盖:未测量 —— 印出来,不是省略掉,
因为一块会被读成通过的空白比一条红更危险。
本包自带一份(36 条,取的是契约失败之后人嘴里的话,语料哈希与筛选正则写在文件头),
它是格式示范,不是对你买家的断言:一个作者、一台机器、一种语言,不是市场样本。
把你的前 20 条真实提问替换进去,这一维才算真在测你的买家。
两种运行方式
① 独立跑(默认,零依赖) —— 只需要 python scripts/audit.py,Python 3.8+ 即可,
退出码直接当 CI 门禁。发给别人、进流水线、在没有 Agent 宿主的环境里跑,都用这一种。
② 接测量端(可选) —— 配一个 MCP 服务器,把同一份引擎的七个工具挂到 Agent 手里:
体检、平台硬数字、事故检索、规则可实现性核对、基线门禁、规则草案、权限自述。
它多给的只有一件事:每个数字都能当场核对。PASS / FAIL 不会因此改变一个字。
契约(含七个工具的输入输出、问题→入口映射、没装时的等价命令)见 references/COMPANION.md。
报告纪律(三条,缺一条结论就不可信)
- 数字来自工具或命令输出,不来自记忆。是估计就写「约」,别让它看起来像测过的。
- 每条结论标来源:
[机器]=有输出支撑,[人判]=规则表标[人工],[未验]=没证据。
三者不许混写成同一段肯定句。 - 报不了就报「未验 + 怎么验」,不报结论。平台没公开的规则、没基线时的 D5 覆盖、
需要出网的 URL 可达性,都属此类——编一个比空着危害大得多。
六维判据
| 维 | 名字 | 一句话判据 | 典型失效 |
|---|---|---|---|
| D1 | 依赖可满足 | 引用的路径 / 脚本 / 命令在目标环境存在 | 指向已下线的目录,任务静默空跑 |
| D2 | 字段合规 | 结构化字段满足目标平台的硬数字与段落格式 | quickPrompts 少一个、description 被截断 |
| D3 | 解析器兼容 | 目标消费方的解析器真能读出 name / description | 写了折叠块,朴素解析器读成空 |
| D4 | 声明-实现一致 | 说了「只读 / 不发信 / 不含 X」就真成立 | 声明不含 X,代码里有 import X |
| D5 | 触发覆盖 | description 能被真实用户问法命中 |
写得太抽象,Agent 永远选不中 |
| D6 | 确定性 | 同输入两次跑,产物字节相同 | 产物混进时间戳 / 随机 id / 绝对路径 |
D1 最高频,D4 是平台驳回的直接原因,D6 是「可审计」的底座。
六条不变量
- 「文件存在」不等于「引用成立」。
references/x.md存在但里面是空壳,链路照样断。
D1 只证第一层;第二层报「内容存疑,需人读」,不混进 FAIL 计数。 - 声明按字面查,不按语义查。平台是字面匹配的:你写「本包不含 base64」它也命中。
所以先反向 grep,再讨论措辞——顺序反了会得到「自认为合规、提交即驳回」的包。 - 解析器兼容要用「目标解析器的等价物」实测,不能用「我用 YAML 库读出来了」。
朴素逐行解析器读不到的写法,yaml.safe_load全能读。 - 长度上限要标注是平台强制还是自定规范。混在一起就会出现
「按规范判 FAIL、但线上那条 327 字早就过审了」这种自相矛盾的建议。 - 产物不许带时间戳。
generated_at一进 JSON,CI 里每次 diff 都是脏的,门禁必然被关掉。
要可追溯就写输入 hash——它同时表达「哪一份」和「有没有变」。 - 只读是能被验证的,不是被声明的。跑前跑后对目标做一次 mtime + hash 快照,
两次相同才算只读。写在文档里的「只读」只是承诺。
边界
- 不改目标文件。修复以 finding 的
fix字段给出,落不落盘由人决定。
自动改提示词比自动改 DNS 更容易造成「看起来修好了」。 - 不判「提示词写得好不好」。文风、人设、说服力没有客观判据,硬判就是编。
这个技能只判实体、字段、解析器、声明、确定性。 - 不读语义。两节互相矛盾、人设和任务打架、要求是修辞写出来的 —— 这些都不报。
只查形状与字面;需要语义的规则一律标[manual],不做假检出。 - 不联网校验外部端点。URL 是否 200 需要出网,默认不跑,避免把审计变成端口扫描。
- 不替代真实执行。「能不能一次跑通」的最终判据是跑一次;这里只负责把能静态
确定的 90% 挑出来。 - D5 只在给了真实提问时才被测。 那就是规则 R501:有
triggers.txt(或
--triggers)时,凡与description一个字都不重的提问逐条报出。没有基线,报告
印的是「D5 触发覆盖:未测量」—— 那块空白不是通过。反过来,只要重上一个词就不报,
所以一次干净的运行只意味着「没有词汇信号」,不意味着「覆盖已验证」。 - R501 不猜。 它分不出「这个问题该由别的入口承接」和「描述用词不同」,两者
长得一模一样,所以它是 WARN,所以修法留了第二个出口:把决定记进SKILL.md。 - R501 只读
description一处。 平台可能还匹配triggers字段,那是更宽的
面;只读一处会让规则更容易报(更吵,不会更静),这个方向是安全的那一侧。 - 不猜平台没公开的规则。驳回模板话术常与真实规则不同,遇到就报
「无法判定 + 建议的验证动作」,不报结论。