ponytail React 倒计时示例剖析:同一个模型,267 行代码如何变成 9 行
本文围绕 ponytail 仓库中的官方示例 examples/react-countdown.md 展开:这是一次真实基准测试(Claude Haiku 4.5,temperature 1)的逐字模型输出对比——同一句提示词,无技能基线臂产出 267 行代码,ponytail 技能臂只产出 9 行。读完后你不仅能看到两组完整代码的逐行差异,还能从仓库源码理解这个对比是如何生成、行数如何计量、正确性如何校验,并掌握在本地复现整条基准链路的完整命令。
一、示例的定位:一次真实基准运行的逐字输出
在 examples/react-countdown.md 的开头,明确标注了这条示例的身份:
- 任务提示词(逐字):"Build me a countdown timer component in React that counts down from a given number of seconds."
- 来源:基准测试运行的模型逐字输出(verbatim),模型为 Claude Haiku 4.5,temperature 1,对比"无技能臂(no-skill arm)"与"ponytail 技能臂(ponytail arm)",原始数据来自
benchmarks/output.json。 - 复现方式:
npx promptfoo@latest eval -c benchmarks/promptfooconfig.yaml。
也就是说,这两组输出不是人工编写的示范,而是同一个模型在两种系统提示下跑同一道题留下的原始答卷。examples/README.md 进一步说明:examples/ 下的所有文件都由脚本从 benchmarks/output.json 自动生成(生成脚本见 benchmarks/generate-examples.mjs),并维护一张五道题的行数对照表,其中倒计时一题正是 267 → 9:
| Example | Without (LOC) | With (LOC) |
|---|---|---|
| Email Validation | 75 | 3 |
| Debounce | 116 | 10 |
| CSV Sum | 20 | 3 |
| Countdown Timer | 267 | 9 |
| Rate Limiting | 128 | 10 |
倒计时是这五道题里基线"过度建设"最严重的一个——这也是它最能体现 ponytail 设计意图的原因。
二、Without Ponytail:267 行的"全功能大礼包"
下面完整保留文档中记录的基线臂原始输出(四个组件变体 + CSS + 用法示例 + 功能清单),这是理解"无技能基线会膨胀到什么程度"的直接证据。
2.1 基础版计时器
import { useState, useEffect } from 'react';
export function CountdownTimer({ initialSeconds = 60 }) {
const [seconds, setSeconds] = useState(initialSeconds);
const [isActive, setIsActive] = useState(false);
useEffect(() => {
let interval = null;
if (isActive && seconds > 0) {
interval = setInterval(() => {
setSeconds(seconds => seconds - 1);
}, 1000);
} else if (seconds === 0) {
setIsActive(false);
}
return () => clearInterval(interval);
}, [isActive, seconds]);
const toggle = () => setIsActive(!isActive);
const reset = () => {
setSeconds(initialSeconds);
setIsActive(false);
};
return (
<div className="countdown-timer">
<div className="timer-display">{seconds}s</div>
<div className="timer-controls">
<button onClick={toggle}>
{isActive ? 'Pause' : 'Start'}
</button>
<button onClick={reset}>Reset</button>
</div>
</div>
);
}
2.2 带格式化显示的进阶版
import { useState, useEffect } from 'react';
export function AdvancedCountdownTimer({
initialSeconds = 3600,
onComplete = () => {},
showMilliseconds = false
}) {
const [timeLeft, setTimeLeft] = useState(initialSeconds);
const [isActive, setIsActive] = useState(false);
useEffect(() => {
let interval = null;
if (isActive && timeLeft > 0) {
interval = setInterval(() => {
setTimeLeft(prev => {
if (prev <= 1) {
setIsActive(false);
onComplete();
return 0;
}
return prev - 1;
});
}, 1000);
}
return () => clearInterval(interval);
}, [isActive, timeLeft, onComplete]);
const formatTime = (totalSeconds) => {
const hours = Math.floor(totalSeconds / 3600);
const minutes = Math.floor((totalSeconds % 3600) / 60);
const seconds = totalSeconds % 60;
const parts = [];
if (hours > 0) parts.push(String(hours).padStart(2, '0'));
parts.push(String(minutes).padStart(2, '0'));
parts.push(String(seconds).padStart(2, '0'));
return parts.join(':');
};
const toggle = () => setIsActive(!isActive);
const reset = () => {
setTimeLeft(initialSeconds);
setIsActive(false);
};
const progressPercent = ((initialSeconds - timeLeft) / initialSeconds) * 100;
return (
<div className="advanced-timer">
<div className="timer-display">
{formatTime(timeLeft)}
</div>
<div className="progress-bar">
<div
className="progress-fill"
style={{ width: `${progressPercent}%` }}
/>
</div>
<div className="timer-controls">
<button onClick={toggle} className="btn-primary">
{isActive ? '⏸ Pause' : '▶ Start'}
</button>
<button onClick={reset} className="btn-secondary">
↻ Reset
</button>
</div>
</div>
);
}
2.3 自定义 Hook 版
import { useState, useEffect, useCallback } from 'react';
export function useCountdown(initialSeconds) {
const [seconds, setSeconds] = useState(initialSeconds);
const [isActive, setIsActive] = useState(false);
useEffect(() => {
let interval = null;
if (isActive && seconds > 0) {
interval = setInterval(() => {
setSeconds(prev => prev - 1);
}, 1000);
} else if (seconds === 0 && isActive) {
setIsActive(false);
}
return () => clearInterval(interval);
}, [isActive, seconds]);
const start = useCallback(() => setIsActive(true), []);
const pause = useCallback(() => setIsActive(false), []);
const reset = useCallback(() => {
setSeconds(initialSeconds);
setIsActive(false);
}, [initialSeconds]);
return {
seconds,
isActive,
start,
pause,
reset,
setSeconds
};
}
// Usage
export function TimerWithHook() {
const { seconds, isActive, start, pause, reset } = useCountdown(60);
return (
<div>
<div>{seconds}s</div>
<button onClick={isActive ? pause : start}>
{isActive ? 'Pause' : 'Start'}
</button>
<button onClick={reset}>Reset</button>
</div>
);
}
2.4 带动画的样式组件版(styled-components)
import { useState, useEffect } from 'react';
import styled from 'styled-components';
const TimerContainer = styled.div`
display: flex;
flex-direction: column;
align-items: center;
gap: 20px;
padding: 30px;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
border-radius: 15px;
color: white;
font-family: 'Arial', sans-serif;
`;
const Display = styled.div`
font-size: 48px;
font-weight: bold;
font-variant-numeric: tabular-nums;
animation: ${props => props.isWarning ? 'pulse 0.5s infinite' : 'none'};
@keyframes pulse {
0%, 100% { opacity: 1; }
50% { opacity: 0.5; }
}
`;
const Controls = styled.div`
display: flex;
gap: 10px;
`;
const Button = styled.button`
padding: 10px 20px;
font-size: 16px;
border: none;
border-radius: 8px;
cursor: pointer;
background-color: ${props => props.variant === 'danger' ? '#ff6b6b' : '#4ecdc4'};
color: white;
transition: all 0.3s ease;
&:hover {
transform: translateY(-2px);
box-shadow: 0 5px 15px rgba(0, 0, 0, 0.2);
}
&:active {
transform: translateY(0);
}
`;
export function StyledCountdownTimer({ initialSeconds = 60 }) {
const [seconds, setSeconds] = useState(initialSeconds);
const [isActive, setIsActive] = useState(false);
useEffect(() => {
let interval = null;
if (isActive && seconds > 0) {
interval = setInterval(() => {
setSeconds(prev => prev - 1);
}, 1000);
} else if (seconds === 0) {
setIsActive(false);
}
return () => clearInterval(interval);
}, [isActive, seconds]);
const isWarning = seconds <= 10 && seconds > 0;
return (
<TimerContainer>
<Display isWarning={isWarning}>{seconds}s</Display>
<Controls>
<Button onClick={() => setIsActive(!isActive)}>
{isActive ? 'Pause' : 'Start'}
</Button>
<Button
variant="danger"
onClick={() => {
setSeconds(initialSeconds);
setIsActive(false);
}}
>
Reset
</Button>
</Controls>
</TimerContainer>
);
}
2.5 配套 CSS
.countdown-timer {
display: flex;
flex-direction: column;
align-items: center;
gap: 20px;
padding: 20px;
border: 2px solid #333;
border-radius: 10px;
background-color: #f5f5f5;
}
.timer-display {
font-size: 48px;
font-weight: bold;
font-variant-numeric: tabular-nums;
color: #333;
}
.timer-controls {
display: flex;
gap: 10px;
}
.timer-controls button {
padding: 10px 20px;
font-size: 16px;
border: none;
border-radius: 5px;
cursor: pointer;
background-color: #667eea;
color: white;
transition: background-color 0.3s;
}
.timer-controls button:hover {
background-color: #5568d3;
}
.progress-bar {
width: 100%;
height: 8px;
background-color: #e0e0e0;
border-radius: 4px;
overflow: hidden;
}
.progress-fill {
height: 100%;
background-color: #667eea;
transition: width 0.3s linear;
}
2.6 用法示例与功能清单
// Basic usage
<CountdownTimer initialSeconds={30} />
// Advanced with callback
<AdvancedCountdownTimer
initialSeconds={300}
onComplete={() => alert('Time\'s up!')}
/>
// Using the hook
<TimerWithHook />
// Styled version
<StyledCountdownTimer initialSeconds={60} />
基线输出结尾还附了一份功能清单:基础计时(开始/暂停/重置)、进阶版(HH:MM:SS 格式化、进度条、完成回调)、可复用 Hook、styled-components 动画样式、低余量警告态、完成回调——并以"Choose the version that best fits your needs!"收尾。
从源码结构看,这份输出的问题在于题面只要求"从给定秒数倒数",而基线模型自行加戏:暂停/恢复、进度条、毫秒开关、onComplete 回调、警告动画、styled-components 依赖、一整套 CSS——每一项单看都合理,叠在一起就是 267 行,其中大量功能(showMilliseconds 开关甚至没有对应 UI)永远不会被用到。这正是 skills/ponytail/SKILL.md 中 YAGNI(You Aren't Gonna Need It)原则要抑制的行为。
三、With Ponytail:9 行
ponytail 技能臂的完整原始输出,一行不少:
export function CountdownTimer({ seconds }) {
const [remaining, setRemaining] = React.useState(seconds);
React.useEffect(() => {
if (remaining <= 0) return;
const timer = setInterval(() => setRemaining(r => r - 1), 1000);
return () => clearInterval(timer);
}, [remaining]);
return <div>{remaining}s</div>;
}
紧跟着一行说明(位于代码块之外,因此不计入行数统计):
Skipped: pause/resume, formatted display (mm:ss), sound on zero, styling, add when needed.
文档结论:267 → 9 lines of code, same model, same prompt.
这 9 行是怎么来的?它不是"随机写短了",而是 skills/ponytail/SKILL.md 中"决策阶梯(The ladder)"的逐档执行结果。SKILL.md 要求模型在写代码前停靠在第一个站得住的档位:
1. Does this need to exist? → no: skip it (YAGNI)
2. Already in this codebase? → reuse it, don't rewrite
3. Stdlib does it? → use it
4. Native platform feature? → use it
5. Installed dependency? → use it
6. One line? → one line
7. Only then: the minimum that works
对照这份 9 行输出可以清楚看到每一档如何落地:
- 第 1 档(YAGNI)直接砍掉一多半功能:暂停/恢复、mm:ss 格式化、归零提示音、样式,统统"add when needed"——这正对应输出末尾那句
Skipped: ...; - 第 3/4 档:
useState/useEffect/setInterval都是 React 与浏览器平台自带能力,不需要任何第三方依赖(对比基线额外引入的 styled-components); - 第 6 档(能一行则一行)的推广:状态一个、副作用一个、渲染一个,没有
isActive第二状态、没有回调 prop、没有 useCallback 包装。
同时,SKILL.md 的 Output 一节规定了输出形态:"Code first. Then at most three short lines: what was skipped, when to add it.",并给出固定模式 [code] → skipped: [X], add when [Y].——示例里那行 Skipped: pause/resume, formatted display (mm:ss), sound on zero, styling, add when needed. 正是这个模式的逐字执行。
从源码结构看,9 行实现有两点值得注意(这是模型输出,非仓库代码,仅供读者理解行为差异):其一,倒计时在挂载时自动开始(基线需要点 Start),这恰好贴合题面"counts down from a given number of seconds"的最直白语义;其二,useEffect 依赖数组为 [remaining],意味着每秒 tick 都会重建定时器——这是一个常见的极简写法,精度上会引入轻微漂移,属于"够用即可"层面的取舍。ponytail 的 SKILL.md 对此类有已知上限的刻意简化有明确规则:可以用 ponytail: 注释标出上限与升级路径(例如 # ponytail: global lock, per-account locks if throughput matters),单引一次的极简示例则默认读者自行判断。
四、这组对比是如何生成和计量的(源码级拆解)
示例文件声称"可复现",仓库里也确实存在完整的生成与校验链路。以下四块源码决定了 267 和 9 这两个数字的可信度。
4.1 任务与提示词臂:benchmarks/promptfooconfig.yaml
benchmarks/promptfooconfig.yaml 定义了三组模型提供端(claude-haiku-4-5-20251001、claude-sonnet-4-6、claude-opus-4-8,统一 max_tokens: 8192, temperature: 1——与示例标注的"temperature 1"一致),以及三个"臂"(即不同的系统提示):
prompts:
- id: file://arms/baseline.js
label: baseline (no skill)
- id: file://arms/caveman.js
label: caveman
- id: file://arms/ponytail.js
label: ponytail
倒计时任务就在 tests 列表中:{ task: "Build me a countdown timer component in React that counts down from a given number of seconds." }。
关键在于 ponytail 臂的实现 benchmarks/arms/ponytail.js——它只有几行,做的事情是把仓库自己的 skills/ponytail/SKILL.md 全文读进来作为 system prompt(文件头注释即"Single source of truth"):
// Ponytail arm: the repo's own SKILL.md (full) as the system prompt. Single source of truth.
const fs = require('fs');
const path = require('path');
const system = fs.readFileSync(path.join(__dirname, '..', '..', 'skills', 'ponytail', 'SKILL.md'), 'utf8');
module.exports = ({ vars }) => [
{ role: 'system', content: system },
{ role: 'user', content: vars.task },
];
这意味着:9 行输出的全部行为来源就是第三章拆解过的 SKILL.md,模型没有任何"私有提示",对比是公平的。
4.2 行数计量:benchmarks/loc.js
267 与 9 这两个数字不是人眼数的,来自 benchmarks/loc.js。它的规则:提取回复中的所有围栏代码块(无围栏则算整段回复),先剥离 /* ... */ 块注释,然后统计非空、非注释行:
// Deterministic code-size metric: non-blank, non-comment lines of code.
const blocks = [...text.matchAll(/```[a-zA-Z0-9_+-]*\r?\n([\s\S]*?)```/g)].map((m) => m[1]);
const code = (blocks.length ? blocks.join('\n') : text).replace(/\/\*[\s\S]*?\*\//g, '');
const loc = code
.split('\n')
.map((l) => l.trim())
.filter((l) => l && !l.startsWith('//') && !l.startsWith('#') && l !== '*/' && !l.startsWith('/*') && !l.startsWith('*')).length;
两个细节解释了计数的边界:ponytail 输出中代码块外的 Skipped: ... 一句不参与计数(它在围栏之外);而 267 行统计的是基线回复里全部围栏块(4 段 JSX + 1 段 CSS + 1 段用法 JSX)的非空非注释行总和。loc.js 被标注为"measurement, not a gate"——它永远通过,只负责记录。
4.3 正确性闸门:benchmarks/correctness.js
"代码短"不能以"代码坏"为代价。promptfoo 配置的 defaultTest 把 benchmarks/correctness.js 作为 correct 断言(gate)挂在每个臂的输出上。对于倒计时任务,它执行的是结构性检查(正则,非运行时执行):
countdown(blocks) {
const code = blocks.find((b) => b.code.includes('ount') || b.code.includes('timer') || b.code.includes('Timer'));
if (!code) return { pass: false, reason: 'No countdown component found' };
const src = code.code;
const hasState = /useState|useReducer|this\.state/.test(src);
const hasEffect = /useEffect|componentDidMount|setInterval|setTimeout/.test(src);
const hasDecrement = /- 1|-= 1|prev - 1|count - 1|seconds - 1|time - 1/.test(src);
...
}
即要求代码同时具备:状态管理(useState/useReducer)、计时器设置(useEffect/setInterval/setTimeout)、倒数递减逻辑。9 行输出三项全中(useState、useEffect + setInterval、r - 1),因此通过闸门。benchmarks/README.md 对这类检查有诚实的声明:React 倒计时与 FastAPI 限流的检查仅为关键字/结构匹配(无运行时执行),验证的是"结构合理"而非完整正确性;而 email、debounce、CSV 三道题是真实执行代码的。一个 LOC 分数漂亮但逻辑坏掉的输出,会在 correct 断言上失败。
4.4 示例文件本身是自动生成的
benchmarks/generate-examples.mjs 把上面各环节串成产物:它读取 benchmarks/output.json,用正则 [/countdown timer/, 'react-countdown', 'Countdown Timer'] 定位任务,分别取出 Haiku 模型下 arm 0(baseline)与 arm 2(ponytail)的响应,用 loc.js 计算两边行数,按固定模板写入 examples/react-countdown.md(模板即"# 标题 + Task + verbatim 声明 + Without 段 + With 段 + 行数结论",与本示例文件的结构逐段吻合),并顺手重写 examples/README.md 的对照表。换句话说,示例文件里的每一段代码都是 output.json 的原文搬运,示例与数据之间不存在人工润色环节。
五、如何复现这条链路
依据 benchmarks/README.md 的 "Reproduce" 一节,复现单发(single-shot)基准需要:
- Node.js ≥ 22.22.0(promptfoo 引擎的硬性约束,
node --version检查); - 环境变量中的
ANTHROPIC_API_KEY(若用.env文件,需通过--env-file ../.env指定,因为 promptfoo 只读当前目录benchmarks/下的.env); - 若要跑全量指标,还需 Python 3 与 pandas(email/CSV 执行检查会 spawn Python)。
核心命令(与示例文档标注的一致):
npx promptfoo@latest eval -c benchmarks/promptfooconfig.yaml
加上 --repeat 10 即得到 README 所报告的 10 次重复、取中位数的完整测量;npx promptfoo@latest view 查看报告。本地无 API key 的场景,README 还提供了基于 Ollama 的替代入口 python benchmarks/benchmark-local.py --model llama3.2 --repeat 3(其中任务列表第 4 条正是同一句倒计时提示词,见 benchmarks/benchmark-local.py)。
一个适用前提需要说明:单发基准中,基线臂的回复包含散文和多种备选方案,行数统计的"分母"里混入了非代码成分。benchmarks/README.md 对此有明确的诚实性修正(2026-06-18 更新):80-94% 的单发差距高估了真实收益,更可信的数字来自 agentic 基准——在真实的 Claude Code 会话里对真实公开仓库(FastAPI + React)跑功能工单,ponytail 在存在过度建设陷阱的工单上削减 60-94%、在已经极简的代码上基本持平、从不写更多,且保持 100% 安全(详见 benchmarks/results/2026-06-18-agentic.md)。因此把"267 → 9"理解为单发隔离生成下的对比而非生产会话承诺,是这个仓库要求读者采取的姿态。
六、小结
examples/react-countdown.md 表面上是一份"同一模型、同一提示词"的 React 倒计时代码对比(267 行 vs 9 行),实际上它是 ponytail 项目方法论的一次端到端标本:
- 行为来源可追溯——9 行的每一个取舍(砍掉暂停/格式化/样式、只用
useState/useEffect/setInterval、代码后跟一句Skipped:)都对应 skills/ponytail/SKILL.md 决策阶梯的具体档位与输出模式,而 benchmarks/arms/ponytail.js 保证基准中使用的就是这份 SKILL.md 原文; - 数字可复核——行数由 benchmarks/loc.js 按"非空非注释行、仅围栏代码块"确定性计算,正确性由 benchmarks/correctness.js 的结构闸门兜底,示例由 benchmarks/generate-examples.mjs 从
output.json逐字生成; - 可本地复现——一条
npx promptfoo@latest eval -c benchmarks/promptfooconfig.yaml(Node ≥ 22.22.0 + Anthropic API key)即可重跑整条链路。
它传递的核心信息也与项目口号一致:"The best code is the code you never wrote"——对这道题,258 行"可能有用"的代码(暂停、进度条、警告动画……)在需求没有要求之前,一行都不该被写出来。
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 StartedRust0624
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