Ponytail 鲁棒性审计:让模型写最懒的代码,会不会在边缘用例上写错?
本文以 Ponytail 仓库中 2026-06-16 的鲁棒性审计报告为骨架,完整还原这次"敌意测试"的设计与全部结论:Ponytail 技能(一种强制模型产出最简最短解法的 SKILL.md 规则集)是否会诱导弱模型在 n=0、世纪闰年、减法罗马数字等经典边缘用例上产出错误代码。读完你可以掌握一套自校验基准(self-verified benchmark)的完整构造方法——如何用"已知正确"与"已知偷懒且错误"的参照实现先验证测量仪器本身,再评估模型输出——并理解"parse ≠ validate"这类跨供应商的隐性失效模式为何无法靠修改提示词修复。
背景:从 issue #65 到一次敌意审计
Ponytail 的核心主张是"最好的代码是你从未写下的代码":通过 skills/ponytail/SKILL.md 把模型约束成"房间里最懒的资深工程师",优先复用、优先标准库、优先原生特性,直到找到第一个能用的最短解。这一主张自然会引来质疑:把模型往最短解上推,会不会让弱模型在边缘用例上产出错误的代码?
这正是 issue #65 提出的问题。同一天的另一份报告 benchmarks/results/2026-06-16-correctness-gate-fix.md 已经证明:此前社区报告的正确率暴跌其实是正确性闸门(correctness gate)自身的两个 bug(读不了无代码围栏的输出、debounce 任务测错了交付物)造成的假象。修好闸门之后,真正悬而未决的问题就落在了本次审计头上——用一个故意敌意的测试集、在足够高的样本量下,直接回答"降级"是否存在。
审计方法:先验证仪器,再打分
审计的两个对比臂(arm)是:
baseline:不加载任何技能,裸模型作答;ponytail:把完整的 SKILL.md 作为 system prompt 注入。
单轮(single-shot)生成、默认参数,模型为 gpt-4.1-mini 与 gpt-5.4-mini 两个弱模型,每格 n=20。
整套方法的核心防御是自校验(self-verification)。测试集共 16 个任务,每个任务都内置两份参照实现:good(已知正确)和 bad(已知偷懒且错误)。任何模型输出被打分之前,先运行 node robustness-audit.js --selftest 验证这 16 对参照:good 必须全部通过断言、bad 必须全部失败,16/16 全部成立才允许开始消耗 API 额度。从 benchmarks/robustness-audit.js 的源码可以直接看到这套机制:
- 每个任务是一个结构体:
{ name, prompt, names, arity, cases, good, bad },其中cases是[参数, 期望值]断言数组,names是模型可能使用的多个候选函数名(如 email 任务接受validate_email/is_valid_email/validate等别名); checkPy()会把生成的代码拼进一个 Python 测试夹具(harness)里,在系统临时目录写成脚本、用python3执行,任何一条断言不匹配(MISMATCH)、抛异常(EXC)或找不到函数(NOFN)都判失败,且带 10 秒超时;pyBlock()负责从模型回复的 fenced 代码块中抽取 Python 代码(优先取语言标记含py的块,否则取第一个块)。
// benchmarks/robustness-audit.js --selftest 分支:16 项仪器全绿才允许跑真测试
for (const t of TASKS) {
const g = checkPy(t.good, t), b = checkPy(t.bad, t);
const pass = g === true && b === false; // good 必须过、bad 必须挂
...
}
(对应源码 benchmarks/robustness-audit.js#L168-L178。)
另外两个工程细节值得注意:运行是串行的,目的是避免 API 配额 429 错误把分母缩掉(失败请求被单独计数为 err,从分母中扣除,见 benchmarks/robustness-audit.js#L186-L193);API key 从仓库根目录的 .env 读取,模板见 .env.example。
12 个经典边缘用例陷阱:两臂全平
16 个任务中的前 12 个是算法题,每格 n=20。结果:两个弱模型上,全部 12 题 baseline 20/20 == ponytail 20/20。这些题目的共同设计思想是"偷懒版实现能过常见用例、必挂边缘用例",原始报告给出的陷阱表如下:
| 任务 | 懒实现会漏掉的陷阱 |
|---|---|
| is_prime | n = 0、1、负数 |
| factorial / fibonacci | n = 0 |
| binary_search | 空列表、target 在最后一个下标(off-by-one) |
| is_leap_year / days_in_month | 1900 不是闰年、2000 是闰年(世纪规则) |
| int_to_roman | 减法形式(4=IV、9=IX、40=XL) |
| flatten | 嵌套深度超过一层 |
| clamp | 值本身已在区间内 |
| chunk | 末尾的余数 |
以闰年题为例,robustness-audit.js 中的 bad 参照就是最典型的"懒错":return y % 4 == 0——常见年份全对,但断言里埋了 [1900, false] 和 [2400, true] 两条世纪规则检查(见 benchmarks/robustness-audit.js#L61-L65)。罗马数字题的 bad 参照则故意省掉 (900,'CM')、(400,'CD') 等减法档位,专打 int_to_roman(4) 这类输入。
首次运行中唯一低于 20 的格子是 gpt-5.4-mini 的 flatten,19/20——一次没有复现的随机失误:加试到 n=50 时拿到 50/50。(clamp 显示 19/19 则是 1 次 API 报错,不是答错。)
结论的第一半就此落定:在经典的"最短解诱导错误"怀疑场景里,Ponytail 没有比裸模型多错一题,保持基线平权(baseline parity)。
验证器任务:email 滑点是跨供应商、且与模型大小无关
16 个任务的后 4 个是验证器:email、url、creditcard、ipv4。真正暴露差异的是 email,其机理是一个 "parse ≠ validate"陷阱:在"优先标准库"(stdlib-first)的压力下,OpenAI 模型有时会伸手去拿 email.utils.parseaddr——那是一个解析器,不是校验器,它会接受 @missing-local.com 这种没有 local part 的畸形输入——而不是写一条显式检查。robustness-audit.js 里的 bad 参照精确复刻了这个反模式(见 benchmarks/robustness-audit.js#L98-L102)。
滑点的分裂维度是供应商(provider),不是模型大小。
OpenAI 系(email,baseline vs ponytail,n=50–100):
| 模型 | baseline | ponytail |
|---|---|---|
| gpt-4.1-mini | 100% | 98% |
| gpt-4.1 | 100% | 79% |
| gpt-5.4-mini | ~100% | ~92% |
| gpt-5.4 | 100% | 98% |
| gpt-5.5 | 98% | 94% |
Claude 系(email,baseline vs ponytail,n=40):
| 模型 | baseline | ponytail |
|---|---|---|
| claude-haiku-4-5 | 35/40 | 40/40 |
| claude-sonnet-4-6 | 0/40 * | 40/40 |
| claude-opus-4-8 | 39/40 | 40/40 |
每一个 OpenAI 模型无论大小都会滑(gpt-4.1 完整版最差,79%),而 Ponytail 的目标平台 Claude 全系在 ponytail 下都是 100%。
* Sonnet baseline 的 0/40 是一个返回类型伪影(artifact),不是逻辑失败,不能读作"Sonnet 不会校验邮箱"。不受约束的 Sonnet 把校验器过度工程化成返回 dict({is_valid, message})而非 bool;测试按 bool 调用该函数,非空 dict 恒为真值,于是它"接受"所有地址、得 0 分。若按 dict 感知(读 is_valid 字段)来判读,其逻辑约 75% 正确(9/12)。诚实的结论很窄:ponytail 写出了任务隐含的朴素正确 bool,而不受约束的模型把接口过度构建,绊倒了天真的 if validate(x) 调用方。这与同日 gate 修复报告中观察到的 Sonnet 失分是同一现象。
其余三个验证器在两个供应商下都稳定在约 100%:它们的"懒的标准库选择"(ipaddress、Luhn 校验、scheme 检查)本身就是严格的,唯独 email 最显眼的标准库工具是个解析器。
那个"没有发生的修复":8 次 SKILL.md 编辑全部无效
SKILL.md 本身已经有两条直接针对这个滑点的规则:"Never simplify away: input validation at trust boundaries..."(绝不简化掉信任边界的输入校验)以及"Two stdlib options, same size? Take the one that's correct on edge cases."(两个标准库选项一样长?选那个在边缘用例上正确的)。团队仍然认真尝试过用编辑技能文本来把 OpenAI 侧的通过率推到 100%——8 次不同编辑,覆盖反向施压措辞(counter-pressure wording)、强制检查条款、显式检查优于委托、few-shot 示例、各种组合与三处放置位置。结果:
- 每一次编辑的得分都 ≤ 当前技能,其中几个差得多(有一次崩到 78%);
- 每一次都让中位 LOC 膨胀;
- 最有希望的一版做了 n=100 的决定性 A/B:
OLD skill: 96/100 (96.0%)
NEW skill: 95/100 (95.0%) -> within noise, no reliable effect
反向指令会起反作用:往技能文本上堆校验规则,只会让小模型过度思考,产出更多坏校验器而不是更少。伸手去拿 parseaddr 的这个反射住在 OpenAI 模型的训练里,没有任何技能措辞能可靠覆盖它——所以什么都没发版。报告的原话点题:往技能里加一段不起作用的文字,正是 Ponytail 这个存在本身要阻止的 cargo-cult(货舱崇拜)行为。
结论:LOC 减半,Claude 上零正确性代价
"Ponytail 会让模型性能降级"这一说法不被数据支持。跨 12 个边缘用例陷阱,ponytail 与 baseline 完全平权;在验证器上它对每一个 Claude 模型都是 100%——那是它的目标平台。唯一的瑕疵是 OpenAI 模型上的 email 校验器滑点(一个跨供应商的 parseaddr 反射,每个尺寸都存在),已如实记录在此、且无法靠技能文本修复。而 LOC 收益(大约一半的代码量)在 Claude 上没有付出任何正确性代价。
复现指南
以下命令继承自原文档的 Reproduce 一节,可在本仓库中直接运行(.env 从 .env.example 复制而来,位于仓库根目录;--selftest 不需要任何 API key):
cd benchmarks
node robustness-audit.js --selftest # 验证全部 16 项仪器(无需 API)
node robustness-audit.js # 16 任务审计,gpt-5.4-mini,n=20
AUDIT_MODEL=gpt-4.1-mini node robustness-audit.js
# email 跨供应商测试(即那个滑点)
ME_MODELS="gpt-4.1,gpt-5.4,gpt-5.5" ME_N=50 node model-email.js # OpenAI (OPENAI_API_KEY)
node claude-email.js # Claude (ANTHROPIC_API_KEY)
OPENAI_API_KEY / ANTHROPIC_API_KEY 均从 ../.env 读取。相关脚本与可调参数:
| 脚本 | 用途 | 关键环境变量(默认值) |
|---|---|---|
| benchmarks/robustness-audit.js | 16 任务、两臂(baseline/ponytail)审计 | AUDIT_MODEL(默认 gpt-5.4-mini)、AUDIT_N(默认 20)、--selftest |
| benchmarks/model-email.js | email 任务在多 OpenAI 模型上高 n 复测 | ME_MODELS(默认 gpt-4.1-mini,gpt-5.4-mini)、ME_N(默认 100) |
| benchmarks/claude-email.js | email 任务在 Claude 三档模型上复测 | CE_MODELS(默认 haiku/sonnet/opus 三档)、CE_N(默认 40) |
这三个脚本共享同一套 checkPy / pyBlock / TASKS 导出(model-email.js 与 claude-email.js 都是 require('./robustness-audit.js') 后取 email 任务),因此任何对断言集的修改都会同时作用于所有测量,仪器的自校验性质不会被脚本分叉破坏。审计结果所在的 benchmarks/results/ 目录还收录了其他各次专项报告(成本复核、agentic 安全等),而更宏观的三臂基准(LOC/成本/延迟)与复现流程见 benchmarks/README.md。
atomcodeClaude Code 的开源替代方案。连接任意大模型,编辑代码,运行命令,自动验证 — 全自动执行。用 Rust 构建,极致性能。 | An open-source alternative to Claude Code. Connect any LLM, edit code, run commands, and verify changes — autonomously. Built in Rust for speed. Get StartedRust0623
Hy4-previewHy4 preview 是由腾讯混元团队研发的新一代混合专家(MoE)旗舰模型。模型总参数量 770B,每个 token 激活 49B,主干共包含78层,第一层采用标准 FFN,其余 77 层均为 MoE 结构,每层包含 256 个路由专家与 1 个共享专家,每个 token 激活 top-8 路由专家及共享专家。主干之外原生内置 1 层 MTP(总参数量 10B,激活 0.7B)以支持投机解码。Python00
GLM-5.3GLM-5.3 与 GLM-5.2 使用相同的基座模型——所有提升均来自后训练。与 GLM-5.2 相比,它在复杂编程和长程任务上的表现显著提升。Jinja00
GLM-5.3-FlashGLM-5.3-Flash (320B-A18B),是GLM-5系列的首个原生多模态模型。320B总参数,能力超过GLM-5.2Jinja00
Spark-X2.5-4BSpark-X2.5-4B 旨在让强大的 AI 更实用、更高效、更易获得。在广泛日常任务中表现强劲,涵盖对话、写作、翻译、推理、编码、工具调用以及智能体工作流,并在同等规模的开源模型中取得领先成绩。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00
Spark-X2.5-1.7BSpark-X2.5-1.7B 旨在让强大的 AI 更加实用、高效且易于获取。这些模型在广泛的日常任务中表现出色,涵盖对话、写作、翻译、推理、编程、工具调用和智能体工作流,并在同等规模的开源模型中取得领先结果。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00