首页
/ ponytail React 倒计时示例剖析:同一个模型,267 行代码如何变成 9 行

ponytail React 倒计时示例剖析:同一个模型,267 行代码如何变成 9 行

2026-09-05 22:53:02作者:蔡怀权

本文围绕 ponytail 仓库中的官方示例 examples/react-countdown.md 展开:这是一次真实基准测试(Claude Haiku 4.5,temperature 1)的逐字模型输出对比——同一句提示词,无技能基线臂产出 267 行代码,ponytail 技能臂只产出 9 行。读完后你不仅能看到两组完整代码的逐行差异,还能从仓库源码理解这个对比是如何生成、行数如何计量、正确性如何校验,并掌握在本地复现整条基准链路的完整命令。

ponytail 基准测试中三个模型下各臂的代码行数中位数

一、示例的定位:一次真实基准运行的逐字输出

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-20251001claude-sonnet-4-6claude-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 配置的 defaultTestbenchmarks/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 行输出三项全中(useStateuseEffect + setIntervalr - 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 项目方法论的一次端到端标本:

  1. 行为来源可追溯——9 行的每一个取舍(砍掉暂停/格式化/样式、只用 useState/useEffect/setInterval、代码后跟一句 Skipped:)都对应 skills/ponytail/SKILL.md 决策阶梯的具体档位与输出模式,而 benchmarks/arms/ponytail.js 保证基准中使用的就是这份 SKILL.md 原文;
  2. 数字可复核——行数由 benchmarks/loc.js 按"非空非注释行、仅围栏代码块"确定性计算,正确性由 benchmarks/correctness.js 的结构闸门兜底,示例由 benchmarks/generate-examples.mjsoutput.json 逐字生成;
  3. 可本地复现——一条 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 行"可能有用"的代码(暂停、进度条、警告动画……)在需求没有要求之前,一行都不该被写出来。

登录后查看全文
热门项目推荐
相关项目推荐