Ponytail 实例剖析:用 Intl.NumberFormat 替代 numeral 完成货币与千分位格式化
本文基于 ponytail 仓库中 examples/number-formatting.md 这一真实基准产出展开:面对"把数字格式化为货币并带千分位分隔符"这一日常任务,未加载 ponytail 技能的模型会 npm install numeral,而加载后的模型直接给出内置的 Intl.NumberFormat 方案。读完后你能掌握三件事:如何用 Intl.NumberFormat 覆盖货币、百分比、紧凑记数三类格式化场景,ponytail 的"决策阶梯"是如何一步步把解法推向标准库的,以及这套"少装一个依赖"的判断在仓库基准体系中如何被验证和复现。
原始任务与两种回答
示例文档的完整设定只有一个任务:
Task: "Format numbers as currency and with thousand separators."(把数字格式化为货币并带千分位分隔符。)
同一模型(Claude Haiku 4.5)、同一任务,分无技能基线与 ponytail 两臂各答一次,这就是示例文档的对比结构。
无 Ponytail:安装一个格式库
基线臂的回答是先装依赖,再用库完成格式化:
npm install numeral
# or: npm install accounting
import numeral from "numeral";
numeral(1234567.89).format("$1,234.00"); // "$1,234,567.89"
numeral(0.745).format("0.0%"); // "74.5%"
numeral(1500).format("0.0a"); // "1.5k"
三条 format() 调用分别演示了货币格式、百分比缩写格式(0.0%)和紧凑缩写(0.0a 把 1500 压成 1.5k)。功能上都对,但代价是一个新的 npm 依赖。
有 Ponytail:内置的 Intl.NumberFormat
ponytail 臂的回答不带任何依赖,代码首行就是一条自我说明式的注释(这是 ponytail 的惯用标记,下文会讲):
// ponytail: Intl.NumberFormat does this, locale-aware
new Intl.NumberFormat("en-US", { style: "currency", currency: "USD" })
.format(1234567.89);
// → "$1,234,567.89"
new Intl.NumberFormat("en-US", { style: "percent" })
.format(0.745);
// → "74.5%"
new Intl.NumberFormat("en-US", { notation: "compact" })
.format(1500);
// → "1.5K"
示例文档给出的结论只有一行,但它就是这个例子的核心论点:
1 dependency → 0 dependencies.
Intl.NumberFormatis built into every JS runtime, handles every locale correctly, and gets currency symbols, decimal separators, and grouping right for any market without a lookup table. A library that hardcodes formats will always be wrong for someone.
翻译过来:1 个依赖 → 0 个依赖。Intl.NumberFormat 内置于每个 JS 运行时,按 locale 正确处理货币符号、小数分隔符和分组符;而硬编码格式表的库,总会在某个市场出错。
这组回答的出处:它是基准产出,不是手写的
理解这个示例的前提是知道它不是作者手写的示范,而是 ponytail 基准跑出来的逐字模型输出。examples/README.md 说明了出处与复现方式:
Real model output, verbatim from benchmark runs, the same task answered by the same model with no skill (
## Without Ponytail) and with ponytail (## With Ponytail), so you can compare side by side. Model: Claude Haiku 4.5, temperature 1, sourcebenchmarks/output.json.
即:模型为 Claude Haiku 4.5、temperature 1,结果直接取自 benchmarks/output.json;"These are not hand-written"。复现命令也写在同一文档里:
npx promptfoo@latest eval -c benchmarks/promptfooconfig.yaml
主 README.md 把整个 examples/ 目录称为 "More survivors"(活下来的幸存者示例),与开头的 <input type="date"> 案例并列,作为"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 system = fs.readFileSync(path.join(__dirname, '..', '..', 'skills', 'ponytail', 'SKILL.md'), 'utf8');也就是说,示例中
// ponytail: ...这种注释风格、"先问该不该写"的思考方式,都直接来自这份 SKILL.md,而不是模型自带的能力。 -
评分侧由 benchmarks/loc.js 和 benchmarks/correctness.js 两个断言构成,在 benchmarks/promptfooconfig.yaml 的
defaultTest中注册:loc.js统计围栏代码块里的非空非注释行(code_loc指标,只测量不设门槛),correctness.js是正确性闸门——一个"更短但跑不通"的答案会在闸门上失败。这就是为什么示例能同时展示"更短"和"仍然正确":两套指标各管一半。 -
运行环境方面,benchmarks/README.md 要求 Node.js ≥ 22.22.0(promptfoo 引擎约束)和环境中的
ANTHROPIC_API_KEY,本地复现的完整命令是npx promptfoo@latest eval -c promptfooconfig.yaml --env-file ../.env --repeat 10(--env-file ../.env是因为 promptfoo 从benchmarks/目录读.env,而文件在仓库根目录)。
需要说明的一个细节:当前 benchmarks/promptfooconfig.yaml 的 tests 列表配置的是五个任务(邮箱校验、debounce、CSV 求和、React 倒计时、FastAPI 限流),examples/README.md 的对照表也只列了这五个;number-formatting 这一篇同样标注来源为 benchmarks/output.json,属于同体系基准产出的示例文档。
为什么会停在 Intl.NumberFormat:阶梯的第三、第四级
这个示例最值得展开的不是代码本身,而是"为什么是它"。ponytail 的核心机制是一条写死在 skills/ponytail/SKILL.md 里的"决策阶梯"(The ladder),要求模型在动笔前从第一级开始检查,停在第一个成立的那一级:
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
主 README.md 的 "How it works" 一节收录了同一份阶梯。对照本例:
- 需要存在吗? 需要,任务明确要求格式化,不能跳。
- 本代码库里已有吗? 示例是独立小任务,无现成 helper 可复用(这一级在项目内任务上经常命中,见下文基准佐证)。
- 标准库能做吗? 命中。
Intl是 ECMAScript 标准的一部分,Intl.NumberFormat属于"标准库/平台内置"层,阶梯在此停住——后面的"一行代码"和"最小实现"级根本不需要爬。
SKILL.md 同时规定了阶梯的使用纪律:阶梯在理解问题之后运行,而不是代替理解;两个级别都成立时取更高的那个。它还对"故意留下的简化"有强制标记规则:用 ponytail: 前缀的注释写明上限和升级路径。示例里那句 // ponytail: Intl.NumberFormat does this, locale-aware 正是这条规则的产物——它不是装饰性注释,而是向后续维护者声明"这里刻意选了内置方案,理由写在后面"。
仓库里还有一份专门的佐证文档 docs/platform-native.md,标题直译为"平台原生解法",开篇即 ponytail 的第一问题:"does the platform already do this?"。在其 "JavaScript / Browser APIs" 一节的对照表中,本例的映射关系被逐字列出:
| You think you need | What the platform has |
|---|---|
numeral / accounting |
new Intl.NumberFormat("en-US", { style: "currency", currency: "USD" }) |
同一张表里还并列了 date-fns → Intl.DateTimeFormat、plural/i18n 复数规则 → Intl.PluralRules("en-US").select(count) 等条目,说明"货币格式化"只是 ponytail 反复处理的"装库冲动"类型之一。该文档结尾的 "The Pattern" 一节把这个模式抽象成五行,可以视为示例结论的通用化:
Platform team spends years solving the problem.
Package author wraps it.
You install the wrapper.
The wrapper goes unmaintained.
You debug the wrapper.
结论是 "Skip the wrapper. The platform ships with your app for free.",并留了一个例外出口:当原生方案确实不够(老浏览器兼容、边缘用例、规模化后的易用性)时,库才挣得它的位置——"Install it then, not before."
三个 Intl.NumberFormat 调用逐项拆解
把示例中三条内置写法拆开看,可以确认它们与 numeral 的三个用例一一对应,且各自覆盖了什么参数:
1. 货币格式
new Intl.NumberFormat("en-US", { style: "currency", currency: "USD" }).format(1234567.89);
// → "$1,234,567.89"
style: "currency"声明货币样式,currency: "USD"给出 ISO 4217 货币代码;- 千分位分隔符(
,)与两位小数(.89)由 locale 数据自动生成,不需要 numeral 那种"$1,234.00"手工格式串; - 与 numeral 的差异体现在 locale 能力上:把
"en-US"换成其他 locale,符号、分组、小数位都会跟着变,无需查表或换库。
2. 百分比格式
new Intl.NumberFormat("en-US", { style: "percent" }).format(0.745);
// → "74.5%"
style: "percent" 内部完成"乘 100 + 加百分号",输入保持比例形式(0.745),避免手工换算出错。numeral 侧的 0.0% 同样输出 74.5%,两边等价。
3. 紧凑记数(compact notation)
new Intl.NumberFormat("en-US", { notation: "compact" }).format(1500);
// → "1.5K"
notation: "compact" 把 1500 压成 1.5K。注意输出与 numeral 的 1.5k 只有大小写之差(en-US 下是 K),这是两个方案在该场景下唯一的可见差异。若需要更细控制,Intl.NumberFormat 还支持 compactDisplay: "short" | "long"、maximumFractionDigits 等选项,但本例用默认值已满足任务。
示例文档最后那句 "A library that hardcodes formats will always be wrong for someone"(硬编码格式表的库总会在某人那里出错)指向的正是这个维度:numeral 的格式串是人写的字符串,Intl.NumberFormat 的格式来自随运行时/操作系统更新的 CLDR 语言数据——后者的错误面小得多,且维护成本不在你这边。
仓库内的第二份证据:连基准题都在考"别手搓格式"
这个"格式要交给已有的正确实现"的原则不止停留在示例里,它还以测试题的形式出现在仓库的 agentic 基准中。benchmarks/agentic/tasks.py 里的 #217b "reuse-money" 任务直接围绕货币格式:
-
题目背景设定项目里有一个
money.py,其中format_money是全项目统一的货币格式——"a leading $ and a thousands separator, e.g. 1050 -> '$10.50', 123456 -> '$1,234.56'",实现只有两行:def format_money(cents): """Project-wide currency format: a leading $ and a thousands separator, e.g. 1050 -> '$10.50', 123456 -> '$1,234.56'. Use this everywhere money is shown.""" return f"${cents / 100:,.2f}" -
任务要求新写的
line_item(name, cents, qty)按"项目里展示金额的方式"展示小计。评分函数score_reuse_money(tasks.py)检查两件事:小额正确性(1050 × 2与999 × 1的普通用例),以及"复用性"——传入能产生四位数总额的参数(61728 × 2分 =$1,234.56),若输出里没有出现$1,234.56,就判定为 "re-implemented formatting (no grouping)",即手搓了一个丢掉千分位逗号的新格式。reused = ("$1,234.56" in fn("Pallet", 61728, 2)) # 61728*2 = 123456 cents -> $1,234.56 return _ok(correct, reused, "reused format_money" if reused else "re-implemented formatting (no grouping)")
这与 examples/number-formatting.md 是同一枚硬币的两面:示例文档讲的是"没有现成方案时,去标准库取"(阶梯第 3 级);#217b 讲的是"有现成方案时,复用项目里的"(阶梯第 2 级,且 SKILL.md 称重造几米之外已有的 helper "is the most common slop")。两个方向合起来,就是 ponytail 对待"格式化"这类任务的全部策略:先查库内,再查标准库/平台,最后才轮到第三方包,而第三方包只有在原生方案被证实不够时才登场。
如何在自己项目里应用这套做法
不需要安装任何东西就能借用这个示例的结论,但如果你想完整继承 ponytail 的行为,仓库给出的路径是:
- 直接采纳结论:JS 项目中把
numeral/accounting替换为Intl.NumberFormat,上文的三段代码即可原样复制运行;换语言/地区只改第一个参数。 - 安装 ponytail 技能(若使用 Claude Code / Codex 等支持技能的主机):按 README.md 的安装节操作,例如 Claude Code 下执行
/plugin marketplace add DietrichGebert/ponytail后/plugin install ponytail@ponytail(两条需分别发送)。安装后默认强度为full,可用/ponytail lite|full|ultra或环境变量PONYTAIL_DEFAULT_MODE调整,用 "stop ponytail" 或 "normal mode" 关闭。强度含义见 skills/ponytail/SKILL.md 的 Intensity 表:lite只提示更懒的替代方案,full(默认)强制执行阶梯,ultra是 YAGNI 极端派。 - 保留
ponytail:注释约定:即使不装技能,"在刻意简化的地方留一行说明上限与升级路径"的约定本身就值得沿用——示例首行注释正是这个约定的示范。 - 验证:ponytail 的边界规则明确"Never simplify away: input validation at trust boundaries, error handling that prevents data loss, security measures"(skills/ponytail/SKILL.md "When NOT to be lazy" 一节)。就数字格式化而言,这意味着:可以省掉依赖,但上游传来的非数字、NaN、负数等边界处理不能因为"更懒"而被删掉。
小结
examples/number-formatting.md 用最小的一个任务演示了 ponytail 的主张:货币、百分比、紧凑记数三种格式化,Intl.NumberFormat 一个内置 API 全覆盖,省掉 numeral 或 accounting 的依赖与维护成本,还顺带获得 locale 正确性。它不是一段手写教程,而是基准跑出来的真实模型输出(Claude Haiku 4.5,temperature 1,来源 benchmarks/output.json,可用 npx promptfoo@latest eval -c benchmarks/promptfooconfig.yaml 复现);支撑它做出这个选择的,是 skills/ponytail/SKILL.md 中"标准库先于依赖"的决策阶梯,以及 docs/platform-native.md 中把 numeral 与 Intl.NumberFormat 直接对位的对照表。对日常开发者的可迁移结论只有一条:在为一类格式化需求装库之前,先问一句平台是不是已经带了。
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