Ponytail 的 Group By 示例深读:用 Object.groupBy 替代 lodash 依赖,以及背后的决策阶梯
本文以 Ponytail 仓库中的 examples/group-by.md 为例,完整拆解"按 key 分组对象数组"这一日常任务在有无 Ponytail 技能下的两种典型输出,并结合 skills/ponytail/SKILL.md 的决策阶梯与 docs/platform-native.md 的平台内建对照表,讲清楚为什么 Agent 会跳过 lodash 选择 Object.groupBy,以及在不同运行时环境下如何正确回退。读完后你可以掌握:该示例对应的真实可复现来源、Object.groupBy 的运行时可用性判断方法,以及在浏览器/Node 不支持时选 reduce 而非依赖库的取舍逻辑。
任务定义:"Group this array of objects by a key"
examples/group-by.md 给出的任务只有一句话:
Task: "Group this array of objects by a key."
这是一个极高频的场景:拿到 orders 这类对象数组,需要按某个字段(如 status)把元素聚合成 { pending: [...], shipped: [...], delivered: [...] } 这样的分组结果。仓库用同一模型、同一任务分别生成了"无技能"(Without Ponytail)与"带 Ponytail"(With Ponytail)两版输出,以便直接对比。
没有 Ponytail 时的典型输出
第一种路径是"装个依赖":
npm install lodash
import { groupBy } from "lodash";
const byStatus = groupBy(orders, "status");
// → { pending: [...], shipped: [...], delivered: [...] }
第二种路径是手写 reduce:
const byStatus = orders.reduce((acc, order) => {
(acc[order.status] ??= []).push(order);
return acc;
}, {});
两条路径各有代价:前者为了一个功能引入一整包工具库(即使现代打包器会 tree-shaking,依赖面、版本升级、许可证都要多背一份);后者虽然零依赖,但多行累加逻辑、??= 空值兜底,读起来不如语言内建直观。
带 Ponytail 时的输出
// ponytail: Object.groupBy does this
const byStatus = Object.groupBy(orders, order => order.status);
// → { pending: [...], shipped: [...], delivered: [...] }
一行内建 API,外加一条 ponytail: 注释说明"为什么选它"。原文档的总结是:1 dependency (or a reduce) → 1 built-in——一个依赖(或一段 reduce)变成了一个内建。这正是 Ponytail 的核心口号 "The best code is the code you never wrote" 在依赖管理上的体现:少装一个 lodash,就少一个需要维护、审计和升级的包。
// ponytail: 这种注释不是随手写的风格,而是 skills/ponytail/SKILL.md 中成文的规则:对"刻意简化、砍掉了真实边角"的方案,要用 ponytail: 注释标出上限与升级路径。配套地,README 中的 /ponytail-debt 命令会把这些被推迟的 ponytail: 捷径收进一张"债务台账",避免"稍后处理"变成"永远不处理"(见 README.md 的命令表)。
为什么跳过 lodash:七级决策阶梯
Ponytail 的"懒"不是随机偷懒,而是一套写死在技能里的决策顺序。skills/ponytail/SKILL.md 定义了 "The ladder",Agent 在写码前先爬梯子,停在第一个成立的台阶:
1. Does this need to exist? → 不需要就不写(YAGNI)
2. Already in this codebase? → 复用代码库里已有的实现
3. Stdlib does it? → 用标准库
4. Native platform feature? → 用平台内建
5. Installed dependency? → 用已安装的依赖,绝不为此新增依赖
6. One line? → 一行解决
7. Only then: the minimum that works
把这个例子放进梯子走一遍,轨迹非常清晰:
- 台阶 1–2 排除后,任务在台阶 3(标准库)/ 台阶 4(平台内建)就落地了——
Object.groupBy是语言运行时自带的,直接命中,根本走不到"要不要装一个新依赖"这一步; - 台阶 5 的规则是"用已安装依赖,绝不为几行代码能搞定的事新增依赖",这正是
npm install lodash方案被否掉的原因; - 台阶 6 的"一行"恰好与台阶 3/4 的答案重合,
Object.groupBy本身就是单行方案。
仓库里还有一份"平台内建速查表"直接支撑了这个判断:docs/platform-native.md 的 "JavaScript / Browser APIs" 一节里,lodash.groupby 一行明确写着它的平台替代就是 Object.groupBy(arr, fn)。该文档的立场(The Pattern 一节)是:平台团队花多年解决的问题,包作者包一层,你装了这个 wrapper,wrapper 失维护,你再去调试 wrapper——跳过 wrapper,平台功能随应用免费附带。
另外注意技能中的一条配套规则:两个标准库方案体积相同时,"选在边角情况下正确的那个"(skills/ponytail/SKILL.md)。懒是少写代码,不是挑更弱的算法——这条规则解释了为什么 Ponytail 选内建 API 而不是"能用就行"的更 hacky 写法。
运行时可用性与回退决策
Object.groupBy 不是"永远可用",它的选型依赖目标运行时。原文档给出的发布节点是:
| 运行时 | 支持起点 |
|---|---|
| Chrome | 117 |
| Firefox | 119 |
| Safari | 17.4 |
| Node.js | 21 |
原文档还给出了两个进阶决策:
- 想要
Map而不是普通对象:用Map.groupBy(orders, o => o.status)。从 JS 标准语义看,两者差别在于键的类型处理——Object.groupBy的结果键会被字符串化(非字符串键也会落成字符串属性名),而Map.groupBy保留回调返回值原样作键,对"分组键本身是对象/数字且需要保留类型"的场景更合适; - 目标环境不支持时(如需要兼容 IE11 或旧版 Node):回退到前文的
reduce一行写法——原文档特别强调,此时的正确选择是reduce,而不是 lodash。依赖库没有因为"运行时太旧"而变得更值得装,零依赖的手写版才是与旧运行时匹配的方案。
一个容易混淆的点值得澄清:本仓库基准测试的 README(benchmarks/README.md)要求 Node.js ≥ 22.22.0,那是 promptfoo 引擎自身的版本约束;而 Object.groupBy 的"Node.js 21"指该 API 在 Node 主线可用的起点。两者是不同语境,不要混用。
示例来源:一次可复现的真实基准运行
这组对比不是人手写的。examples/README.md 说明:examples/ 下都是基准运行的真实模型输出(verbatim),同一个模型(Claude Haiku 4.5,temperature 1,来源 benchmarks/output.json)对同一任务分别以"无技能"与"带 ponytail"两种配置作答。示例目录包含 group-by、debounce、csv-sum、deep-clone、modal-dialog 等十余个例子,超出基准标准五任务的表内清单。
整个对照链路在仓库里可以逐环核对:
- 对照实验配置:benchmarks/promptfooconfig.yaml 定义了三条对照臂(baseline 无技能、caveman、ponytail)与标准任务集;
- Ponytail 臂的注入方式:benchmarks/arms/ponytail.js 直接把仓库的 skills/ponytail/SKILL.md 全文读入作为 system prompt——也就是说,示例中 Agent 表现出的"先查内建、再谈依赖"行为,正是这份技能文本约束的结果;
- 质量门:benchmarks/README.md 的 Metrics 表说明,
loc.js负责度量代码行数(总是通过,只记录),而correctness.js是门槛——"一个在 LOC 上得分很高但实际坏掉的一行代码,会在 correctness 上失败"。这保证了"一行内建"的省不是牺牲正确性换来的。
复现命令(见 benchmarks/README.md):
npx promptfoo@latest eval -c benchmarks/promptfooconfig.yaml
落到自己项目里的用法
如果你想在真实编码流程中让 Agent 表现出同样的选择习惯(遇到 groupBy 先查内建、拒绝装 lodash),仓库提供两类接入方式(详见 README.md 的 Install 一节):
- 插件安装(以 Claude Code 为例,分两条独立 prompt 发送):
/plugin marketplace add DietrichGebert/ponytail
/plugin install ponytail@ponytail
Codex、GitHub Copilot CLI、OpenCode、Gemini CLI 等 20 个宿主都有对应的安装命令;卸载则用 /plugin remove ponytail 等宿主命令,并可用 node scripts/uninstall.js 清理插件目录外的状态文件。
- 指令文件模式(Cursor、Cline、Copilot Chat 等无插件的宿主):把 AGENTS.md 或对应规则文件(
.cursor/rules/、.clinerules/等)复制进项目即可,零配置生效。
日常配合的斜杠命令中,与本示例直接相关的有两个:/ponytail-review 会对当前 diff 做过度工程审查并回传"可删清单"(比如 diff 里新装了 lodash 却只用了 groupBy,就会被它揪出来);/ponytail-debt 则汇总代码里的 ponytail: 注释形成推迟事项台账。强度用 /ponytail lite|full|ultra|off 调节,默认 full,也可用环境变量 PONYTAIL_DEFAULT_MODE 或 ~/.config/ponytail/config.json 预设。
小结
examples/group-by.md 用最小篇幅演示了 Ponytail 的完整决策闭环:任务 → 爬梯子 → 停在"平台内建"台阶 → 一行 Object.groupBy + 一条 ponytail: 注释 → 附运行时可用性清单与旧环境的 reduce 回退。它同时是一个可验证的样本——背后有完整的 promptfoo 对照配置、correctness 门槛与真实模型输出存档支撑。对读者而言,其价值不仅是"哦原来有 Object.groupBy",更在于一套可迁移的判断框架:下一次 Agent 要 npm install 某个工具库之前,先问自己那七个台阶的问题。
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 StartedRust0622
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