wth2jhl.online

Back

Ponytail:把「懒惰的资深工程师」写成一个 skill#

仓库 DietrichGebert/ponytail ↗,MIT 协议,主打 Claude Code、Codex、Cursor、Copilot 这些编码 agent。它的自我描述很直白:The best code is the code you never written(最好的代码是你压根没写的那段)。

我是带着一个挺具体的疑问去翻它的:一个 skill,不也就是一段塞进 system prompt 的文字吗?凭什么能让同一个模型少写一半代码?看完之后我的结论是——它真正的活儿不在提示词写得多好,而在于把规则塞进上下文的时机和方式。

先给结论,免得你读到后面忘了:

  • 它的规则正文只有一页纸,朴素得几乎不像 AI 产品
  • 真正的工程量在 hook 层(约 1300 行 JS):怎么让这段文字在恰当的时机、以恰当的格式,出现在恰当的宿主里
  • 效果不是玄学,是 39 任务 × 5 次 × 3 组 = 585 次会话跑出来的
  • 最值得抄的不是它的规则,是它敢把”砍代码”和”补测试”绑在一起这个决定

0. 先看它长什么样#

Ponytail 5 基准测试结果总览

上面这张是它自己的基准结果图,数字等下细说。我想先说图里那句标语:代码行数 -53%,但高风险逻辑带测试的比例反而从 68% 涨到 98%。

按常理,“让 AI 少写代码”和”让 AI 多写测试”是互相打架的两件事。少写代码就该少写测试,多写测试就该多写代码。它凭什么两头都要?

答案藏在它规则正文的第一条和倒数第二条里,我先摆出来:

Be lazy about the solution, never about the change itself —— 对方案要懒,对改动本身绝不能偷懒。

翻译过来就是:砍的是你凭空发明的架构,不是任务本身要求的完整性。 砍掉一个自己造的抽象层是加分项,砍掉一个该跟着改的调用方是负分项。它把这两件事分开表述,是整个设计的支点。

1. 那段规则正文:一页纸,七级阶梯#

skills/ponytail/SKILL.md 一共 3.5KB,去掉 frontmatter 正文不到一页纸。核心是一张有序清单:

Ponytail 的决策阶梯

这个阶梯的顺序不是随便排的。它的隐含逻辑是成本递增:从第 1 级到第 7 级,你付出的”自己动手写代码”成本越来越高,但代码量越来越少。

  • 第 1 级「这东西需要存在吗」——答”不需要”直接跳过,这是唯一允许”什么都不做”的出口
  • 第 2-5 级都是”找现成的”:仓库里已有的 → 标准库/平台自带 → 已装的依赖 → 一行能搞定
  • 第 6 级兜底才轮到”写最小能跑的代码”

有意思的是第 3、4 级的顺序:标准库优先于平台原生,并且明确写了一句「除非项目有自己的,那项目自带的组件赢过原生控件」(A house component beats a native widget)。

这条挺反直觉的。按理说”用平台自带的最省事”是常识,但它反过来——因为仓库里已有的组件意味着一致性。你在这个项目里新引一个原生控件,风格就破了,还得处理主题、样式、a11y。这条规则背后是真实的工程权衡,不是口号。

最后那句收尾,是它区别于普通风格提示词的地方:

Never cut:信任边界上的校验、防止数据丢失的错误处理、安全、无障碍、真实硬件需要的校准、用户明确要的东西。

砍代码的规则,必须配一份”不许砍”的清单。 没有后半句,前面的”最小改动”就变成了”能少写就少写”的偷工减料指令。这是我认为它设计里最值得学的一点——约束和许可必须成对出现。

档位:不是非黑即白#

它给了四档,不同时刻该用不同的:

档位行为
lite按要求做,顺带用一行点出”还有更小的方案”,让用户自己选
full上面那套完整规则,默认档
ultra在动手前先质疑需求本身,凡是不被需求证成的部分都先怼回去
off关掉

有个细节挺精妙:规则正文里只有一个表和一组示例是分档的,其余规则所有档位共享。所以它做过滤的方式不是维护 N 份提示词,而是读同一份正文,按当前档位把不相关的行筛掉:

二十来行,单份真源。这里有个容易踩的坑它处理了:第二段匹配要求行尾必须有引号,注释里写明了原因——不然一条恰好以档位名开头的普通规则(比如 - Full: ...)会被误判成示例,在其他档位里被整条丢掉。

2. 真正的工程量:hook 怎么把规则塞进去#

到这里都还算平常。让我改变看法的是接下来的部分。

一段 skill 正文,模型不一定会照做。Claude Code 只有在你显式调用 skill 时才加载它,而 Ponytail 要求的是”整个会话期间持续生效”。要做到这点,就得在会话开始那一刻就把规则推进上下文——这就是 hook 的活儿。

Ponytail 的 hook 注入链路

整条链路没有任何一次网络请求、没有一个模型调用,全是本地文本拼接,外加一次限时 3 秒的 grep。这点让我印象很好——它把”行为塑造”的成本压到了接近于零。

几个我觉得写得漂亮的地方:

① per-project 的 flag 文件

多个仓库同时开着 agent 会话时,模式状态不能互相覆盖。它用 sha256(项目路径) 做 key 分文件存:

const projectStatePath = projectDir
  ? path.join(stateDir, 'ponytail-modes',
    createHash('sha256').update(path.normalize(projectDir)).digest('hex'))
  : null;
js

但它同时也写一份共用的 .ponytail-active。为什么?因为 statusline 脚本要读。代码里那句 ponytail: 注释坦率地承认了这个妥协:同一个仓库里的多个会话仍然共享档位,最后写入的赢。

这种”我知道这里不完美,但我选择这样并写下来”的注释风格,比假装完美更值得信任。 整个仓库里这类注释有几十处。

② 五个宿主的输出方言

同一个 writeHookOutput(),五种完全不同的格式:

if (isCopilot) {
  // Copilot 只在 SessionStart 读 additionalContext,别的事件一律输出 {}
  writeOut(JSON.stringify(
    event === 'SessionStart' && context ? { additionalContext: context } : {}));
  return;
}
js

而且判定宿主的顺序很讲究——isCursor 排在最后,注释解释得很清楚:Cursor 的 CURSOR_VERSION 只在它自己构造的 hook 进程环境里赋值,不会泄漏到跑在 Cursor 终端里的 Claude Code 会话。如果先判 Cursor,就会把普通 Claude Code 会话误认成 Cursor。

这里有个 Codex 的坑,注释里记着:systemMessage 在 Codex 里会被渲染成黄色的 warning: 提示,读起来像每次会话都在报警。所以它只用 additionalContext,并在注释里留了一句:如果 openai/codex#16933 落地并隐藏了 additionalContext,记得把模式信号加回来。

③ 上下文压缩不能重置档位

这个 bug 我觉得特别典型。上下文一压缩,SessionStart 会在同一个会话里重跑一遍。如果不处理,用户上午设的 /ponytail lite 会被一次自动压缩悄悄拍回 full,而且没有任何提示。

它的解法是从 stdin 的 source 字段看这次是不是压缩触发的:

source = JSON.parse(input.replace(/^\uFEFF/, '')).source;
activate(source === 'compact' ? (readMode() || 'off') : getDefaultMode());
js

这类”自动化流程会重入你的状态机”的坑,只有真正在用的人才踩得到。 它有 1000+ 个 issue,大部分应该都是这类实战细节。

顺带一提,整个 activate 函数的失败策略高度一致:所有异常都静默吞掉,永远不让 hook 拖垮会话启动。注释写着 “Silent fail — flag is best-effort, don’t block the hook”。输入侧则设了 1 秒兜底定时器,即使 stdin 永远不来也会退出。

④ ponytail-map.js:把”先复用”变成零成本

规则里第 2 级是”仓库里已经有了吗?复用”。但这需要 agent 去搜索,而搜索很贵。于是它顺手扫一遍仓库,把顶层函数、类、导出列出来塞进上下文:

const BUDGET = Number(process.env.PONYTAIL_MAP_CHARS || 2000);
const SRC = /\.(py|js|jsx|mjs|cjs|ts|tsx|go|rs|rb|java|kt|php|swift|cs)$/;
const SHARED = /(util|helper|lib|common|shared|service|component|hook|core|model|schema|api)/i;
js

每种语言一条正则(不是 parser,注释里写明了),一个目录输出一行,预算 2000 字符,文件名匹配 utils|components|services 这类的目录优先排在前面——预算不够时先保住最可能被复用的那些。

它的注释里也留了升级路径:regex, not a parser. Misses nested or dynamic definitions; a tree-sitter index is the upgrade if the map ever needs to be exact.

这个设计我觉得非常聪明。它没有试图让模型更聪明,而是把”聪明”的前置条件(知道仓库里有什么)预先算好塞进去。 这是 prompt 工程从”写辞藻”转向”做工程”的标志性转变。

3. 另一半:审查和审计技能#

除了主规则,它还有 5 个配套 skill,设计上有个我认为很聪明的区分。

skill作用是否改代码
ponytail懒惰模式本体会
ponytail-review审查一个改动不改
ponytail-audit审查整个仓库不改
ponytail-debt收集所有 shortcut: 注释成账本不改
ponytail-gain展示基准测试成绩不改
ponytail-help速查卡不改

只有主 skill 会写代码,其余全部只读。 这不是小事——一个”让你少写代码”的工具,如果自己审查时却会顺手改代码,那它就成了自动重构器,用户不敢开。

审查报告的结构:三档 + 四段#

review 和 audit 的输出格式几乎一样,都是把发现分成三档:

  • Must fix:bug、安全、数据丢失、在预期负载下会崩
  • Should fix:没测试的高风险代码、真实的慢、重复代码、干杂活的函数
  • Nice to have:小的提速、更短的写法

编号跨档连续,这样用户可以说”修 2 和 5”。每条发现固定四段:

2. **Orders land on the wrong day** (`billing/close_day.py:L40-52`)
   - **What this is:** At midnight this job closes the day and bills all orders of that day.
   - **Problem:** It takes "today" from the server clock, which runs in UTC...
   - **Fix:** Compute the day once in the shop's time zone: `datetime.now(...)`. One line.
   - **If we skip it:** Late orders show the wrong date, accounting fixes them by hand.
plaintext

第三段尤其关键:修复方案必须是最小的那个,并且允许”删代码”作为修复方式。 报告末尾还要求给出 Lean: -<N> lines possible,把”能删多少”当成一等公民指标。

它甚至规定了审查者自身的纪律:

Every finding needs a concrete case: “this input or situation leads to this wrong result”. No case, no finding.

没有具体触发场景的猜测,不算发现。这一条把”审查”和”抒发个人偏好”切开了。

ponytail-review 的输出示例

注意这张图里的核心发现:问题在一个没被本次改动碰到的文件里。src/routes/feed.js 不在 diff 里,但因为别处改了字段名,它现在每次请求都崩。审查只看 diff 是抓不到这个的,所以 review skill 里明确写了:Read the diff, then the code it touches: callers of every changed function…

shortcut: 注释:给偷懒留个账本#

这是我觉得整套设计里最巧的一点。

规则里说:对方案要懒,但不许偷偷摸摸地偷懒。 怎么落实?就是每个走捷径的地方,必须留一条注释说明这个捷径的天花板在哪、什么时候该升级:

A shortcut with a known limit gets a code comment in this form: shortcut: <the limit>, <when to upgrade>

而 ponytail-debt 技能就是去把所有这类注释捞出来做成账本:

git grep --no-index --exclude-standard -nE '(#|//|/[*]) ?(shortcut|ponytail):'
plaintext

它还会给只写了天花板、没写升级触发条件的注释打上 no-trigger 标签,因为那些是会悄悄烂掉的。

// ponytail: VS Code Copilot never sets COPILOT_PLUGIN_DATA — it only injects
// CLAUDE_PLUGIN_ROOT... Without this fallback isCopilot was false, so ponytail
// assumed native Claude Code and emitted the statusline nudge...
js

上面这条就是它自己仓库里的 ponytail: 注释。它把自己的每一个技术债都记在案,而且注明了为什么还不改。这才是”懒惰”和”懈怠”的分界线:前者知道自己欠了什么,后者连账都不记。

4. 那组数字是怎么来的#

到这里我已经有点被说服了,但代码写得漂亮不代表效果真的好。所以我重点看了它的基准测试方法论——这部分意外地严谨。

代码量、成本、时间的对比

测试条件:

  • 585 次会话:39 个任务 × 5 次 × 3 组(无 skill / Ponytail v4.13 / Ponytail 5)
  • 模型 Opus 5.5,headless Claude Code 2.1.288
  • 12 个任务来自真实的 full-stack-fastapi-template ↗ 仓库
  • 18 个任务带隐藏检查:agent 不知道自己会被怎么测,但代码写完后会被真正跑一遍
  • 三组逐任务交替,让时段和模型漂移平均打到三组身上

几个我觉得值得学的做法:

① 统计口径是几何均值 + bootstrap 区间,不是平均数

它报的是”每个任务取 5 次的中位数,再算跨任务比值的几何均值,附 95% bootstrap 置信区间”。所以 -53% 后面跟着 [-63..-41]。这比裸报一个数字诚实得多——你能看出这个效果有多少是噪声。

② 主动披露对自己不利的数字

它自己报了一条:

Injected bugs caught, where a test exists: no skill 77%, Ponytail 5 69%

也就是说,一旦测试存在,无 skill 那组写的测试质量反而更高。它把这个对自己不利的数字直接放进主表里,还补了一句:No skill writes the most thorough single tests, but often writes none.

这就是我前面说的那句”少写代码”和”多写测试”能同时成立的真正解释:它提高的是”该写测试时会不会写”的概率,不是单个测试的质量。 68% → 98% 是覆盖率提升,而 77% → 69% 说明单点质量是下降的。

如果不看这两个数,很容易被那张 98% 的图误导。

③ 承认测不出来的东西

它专门解释了为什么有 21 个任务不计入质量结果:

Their “pass” only means the code was delivered or compiles, so I do not count them as a quality result.

还有个细节:完整性评审模型因为只看 diff 而把一个漏了 col import 的产出判成不完整,而那个 import 其实在文件第 5 行。它复查后手动改判成完整。这种”我知道评审工具有偏差,我复查了”的坦白,比伪造一个完美数字可信。

④ 明确说明”这个数字不是你这个仓库的”

ponytail-gain 技能里有一段我觉得是全仓库最清醒的话:

These are benchmark averages, not this repo. NEVER print a per-repo savings number (“you saved X lines here”): the unbuilt version was never written, so there is no real baseline to subtract from.

意思是:因为 agent 没有真的写出那份”更长的版本”,你根本无从比较,所以不许假装能算出你省了多少。真正的 per-repo 数据只有一处来源——/ponytail-debt 那个数得出来的账本。

一个 skill 主动禁止自己输出用户最想看的那种数字,这种克制比任何 benchmark 数字都加分。

测试覆盖的提升

5. 那个 335 行变 10 行的例子#

我印象最深的是这张 before/after 图。

日期选择器的 before/after

任务是”给前端加个日期选择器”。

无 skill 的版本,335 行:装一个日期选择器库(react-datepicker),然后写一个自己的 Calendar 组件、自己处理 weekStartsOn、自己加 isDateSelectable 判断、自己写 onNavigate、自己做焦点管理……它甚至在 useRef 里存了”当前显示的月份”,因为原生库不给你。

Ponytail 5 的版本,10 行:

import * as React from "react"
import { Input } from "@/components/ui/input"

// ponytail: native date input (value is "YYYY-MM-DD"); swap for react-day-picker + popover
//   if a custom calendar UI is needed.
function DatePicker(props: Omit<React.ComponentProps<typeof Input>, "type">) {
  return <Input type="date" data-slot="date-picker" {...props} />   // 浏览器自带的日期选择器
}

export { DatePicker }
tsx

两句话讲清楚它干了什么:这个仓库已经有个 Input 组件,而每个浏览器都自带日期选择器。 把俩拼一起就完事了。

然后它老老实实留了 shortcut: 注释,写明天花板在哪(值格式是 YYYY-MM-DD)、什么时候该升级(真要自定义日历 UI 的时候换成 react-day-picker + popover)。

仓库审计示例

这张审计图我特意留着,因为它展示了”输出格式”的实际效果——注意报告正文是英文的。这不是偷懒,是刻意的:skill 里写死了 Very simple English: short sentences, everyday words. Explain a technical term the first time you use it. The reader may never have seen this code.

理由也很实在:审查报告的读者往往是不熟悉这段代码的运维或上级,用他们的母语写反而更容易误解术语。中文语境下这个选择未必适用,但”审查报告的目标读者决定语言”这个思路本身是通用的。

6. 我觉得它没做好的地方#

光说好话没意思。这项目有几个地方我持保留态度:

① 分母是它自己选的

-53% 这个数字,对照的是”同一个 agent 不带任何 skill”。但你装一个 skill,本来就是想要行为改变——问题不是”该不该变”,而是”这个变化是不是你想要的”。它证明了”装 Ponytail 比不装省”,没证明”装 Ponytail 比装别的东西省”。

同理,tmpl-fe-datepicker 那个 335 → 10 的例子,单看很震撼,但 12 个 repo 任务里,-53% 有相当比例来自这种”agent 本来就过度设计”的场景。如果你的团队本来就只写小脚本,这个收益会大幅缩水。

② 规则对”有历史包袱的仓库”不太友好

整套规则的核心是”复用仓库里已有的东西”。但如果这个仓库本身的历史代码就是一堆过度设计呢?”照着周围的写法来”这条(Keep the structure the codebase already has)会让它去模仿坏味道。

③ 只能作用于编码任务

这一点它自己写得很清楚:Use on any coding task。写文档、做调研、设计讨论的时候,这套”少写”的规则反而可能有害。所以 off 档是必要的,但这也意味着你得自己记住什么时候关掉它。

④ 子 agent 的匹配是可选的,而且失败开放

PONYTAIL_SUBAGENT_MATCHER 不设的话,规则会注入给每一个子 agent,包括那些专门做探索、读文件的。想缩小范围得自己配正则。默认值偏激进。

7. 如果你想抄这个思路#

我把这项目里我认为可以直接搬走的东西列一下:

1. 约束和许可成对出现

想约束 agent”少做”,必须同时写清楚”不许少做的是什么”。只写前半句,规则立刻会退化成偷工减料指令。

2. 单份真源 + 按需筛行

别为不同档位维护多份提示词。一份正文,运行时按标记筛掉不适用的行。它那个”示例行必须有引号才算是示例”的细节,说明这个坑是真的有人踩过。

3. 状态要 per-project,而且要有兜底

多仓库并行是常态。sha256(项目路径) 做 key 这种做法可以直接抄。

4. 自动化流程会重入你的状态机

上下文压缩、自动重试,这些都会重跑 hook。凡是通过读文件恢复状态的,都要考虑”这次是重入还是新会话”。

5. 给技术债一个可 grep 的标记

shortcut: <天花板>, <升级条件> 这个约定,跟任何代码规范体系都不冲突。加一条 grep 就能出一份账本,还能自动标记出”只写了一半”的那些。这个我觉得跟 Ponytail 本身完全无关,可以独立用在任何项目上。

6. 审查报告只读

想做一个”帮你改进代码”的工具,先想清楚审查阶段会不会写代码。写了,用户就不敢开。

7. 数字要带区间,并且主动报对自己不利的那个

-53% [-63..-41],以及那句 77% vs 69%。这不是谦虚,这是让读者能自己判断的最低成本。

8. 最后#

回到最开始那个疑问:一段 skill 提示词凭什么能改变行为?

我的答案是:改变行为的成本不在”说得多好”,而在”多容易真的被读到、真的被遵守”。

Ponytail 的规则正文,如果只写在文档里,agent 大概率不会照做。它做的是把这一页纸变成了一条工程链路——在会话开始时、在每个子 agent 开始时、在上下文压缩后,反复把它送到模型面前;同时给它配了自动审查、技术债账本、以及一个不允许它乱报数字的自检机制。

规则本身朴素得甚至有点老派:先看要不要做,再看有没有现成的,最后才动手写;动手了就留测试;偷懒了就在注释里记账。

这大概就是资深工程师和新手工程师的区别,不是知识量,是知道什么时候不用写。

它做的事就是把这件事写下来,然后以最小的力气,反复提醒 agent。


项目地址:github.com/DietrichGebert/ponytail ↗。文中基准数据来自仓库内 benchmarks/results/2026-10-07-agentic.md(Opus 5.5,39 任务 × 5 次 × 3 组 = 585 次会话),配图为其官方中文版素材。

Ponytail:让 AI 少写代码的一个 skill,凭什么省掉一半行数
https://wth2jhl.online/blog/ponytail-lazy-senior-dev-skill/
Author 小吴同学
Published at October 23, 2026