首页
/ Ponytail 实例剖析:用 Intl.NumberFormat 替代 numeral 完成货币与千分位格式化

Ponytail 实例剖析:用 Intl.NumberFormat 替代 numeral 完成货币与千分位格式化

2026-09-04 16:20:32作者:范靓好Udolf

本文基于 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.NumberFormat is 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, source benchmarks/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.jsbenchmarks/correctness.js 两个断言构成,在 benchmarks/promptfooconfig.yamldefaultTest 中注册: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.yamltests 列表配置的是五个任务(邮箱校验、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" 一节收录了同一份阶梯。对照本例:

  1. 需要存在吗? 需要,任务明确要求格式化,不能跳。
  2. 本代码库里已有吗? 示例是独立小任务,无现成 helper 可复用(这一级在项目内任务上经常命中,见下文基准佐证)。
  3. 标准库能做吗? 命中。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-fnsIntl.DateTimeFormatplural/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_moneytasks.py)检查两件事:小额正确性(1050 × 2999 × 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 的行为,仓库给出的路径是:

  1. 直接采纳结论:JS 项目中把 numeral/accounting 替换为 Intl.NumberFormat,上文的三段代码即可原样复制运行;换语言/地区只改第一个参数。
  2. 安装 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 极端派。
  3. 保留 ponytail: 注释约定:即使不装技能,"在刻意简化的地方留一行说明上限与升级路径"的约定本身就值得沿用——示例首行注释正是这个约定的示范。
  4. 验证: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 全覆盖,省掉 numeralaccounting 的依赖与维护成本,还顺带获得 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 中把 numeralIntl.NumberFormat 直接对位的对照表。对日常开发者的可迁移结论只有一条:在为一类格式化需求装库之前,先问一句平台是不是已经带了。

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

项目优选

收起
kernelkernel
deepin linux kernel
C
33
18
ops-transformerops-transformer
本项目是CANN提供的transformer类大模型算子库,实现网络在NPU上加速计算。
C++
1.12 K
2.72 K
kernelkernel
openEuler内核是openEuler操作系统的核心,既是系统性能与稳定性的基石,也是连接处理器、设备与服务的桥梁。
C
528
588
ops-nnops-nn
本项目是CANN提供的神经网络类计算算子库,实现网络在NPU上加速计算。
C++
906
1.82 K
pytorchpytorch
作为 Ascend for PyTorch 社区的核心组件,TorchNPU 是昇腾专为 PyTorch 打造的深度学习适配插件,使 PyTorch 框架能够直接调用昇腾 NPU,为开发者提供昇腾 AI 处理器的超强算力。
Python
854
1.34 K
docsdocs
暂无描述
Markdown
891
5.78 K
jiuwenswarmjiuwenswarm
JiuwenSwarm 是一款基于openJiuwen开发的智能AI Agent,它能够将大语言模型的强大能力,通过你日常使用的各类通讯应用,直接延伸至你的指尖。
Python
3.53 K
1.01 K
ops-mathops-math
本项目是CANN提供的数学类基础计算算子库,实现网络在NPU上加速计算。
C++
1.34 K
1.45 K
cann-learning-hubcann-learning-hub
CANN 学习中心仓,支持在线互动运行、边学边练,提供教程、示例与优化方案,一站式助力昇腾开发者快速上手。
Jupyter Notebook
987
504
AscendNPU-IRAscendNPU-IR
AscendNPU-IR是基于MLIR(Multi-Level Intermediate Representation)构建的,面向昇腾亲和算子编译时使用的中间表示,提供昇腾完备表达能力,通过编译优化提升昇腾AI处理器计算效率,支持通过生态框架使能昇腾AI处理器与深度调优
C++
540
384