Ponytail 的深克隆方案:用 structuredClone 内置 API 替代 lodash.cloneDeep
当你对 AI 编程助手说"深克隆这个对象"时,它最常见的两个回答是:npm install lodash 然后用 cloneDeep,或者 JSON.parse(JSON.stringify(obj)) 这个经典但脆弱的 hack。Ponytail——一个让 AI Agent 以"办公室里最懒的资深工程师"思维方式写代码的技能包——对这个任务给出的答案是运行时内置的 structuredClone():一行代码、零依赖。本文以仓库中 examples/deep-clone.md 这个真实基准对比为核心,讲清"无 Ponytail"与"有 Ponytail"两种输出各自的问题与价值,并结合仓库中的 平台原生存根清单、技能定义 与基准复现方式,说明这套"先查平台、再查标准库、最后才写代码"的决策流程是如何落到具体代码上的。
任务背景:深克隆是典型的"过度依赖"陷阱
examples/ 目录下的每篇文档都不是人工编写的示例,而是基准测试中同一模型、同一提示词在"无技能"与"带 Ponytail"两种模式下的逐字真实输出(模型为 Claude Haiku 4.5,temperature 1,来源 benchmarks/output.json),见 examples/README.md。读者可以自行复现这些对比:
npx promptfoo@latest eval -c benchmarks/promptfooconfig.yaml
深克隆恰好是"一个函数就能解决却动辄拉来整个依赖包"的典型场景。下面完整继承原文档的三组代码,逐一分析。
Without Ponytail:两个默认选项的代价
选项一:安装 lodash
无技能模式下,模型的直接回答是给整个 lodash 库安装依赖:
npm install lodash
import { cloneDeep } from "lodash";
const copy = cloneDeep(original);
问题不在 cloneDeep 本身——它实现得没错——而在于为了一个函数引入了整个依赖:包体积、供应链安全面、版本升级维护,全部为了一个 cloneDeep。Ponytail 的原话是:"Pull lodash in when you need the rest of it, not for one function."(等你需要 lodash 的其他功能时再引入,而不是为了一个函数。)
选项二:JSON 往返 hack
另一个"零依赖"的经典做法:
// fragile: loses Date, undefined, Map, Set, circular refs, functions
const copy = JSON.parse(JSON.stringify(original));
原文档的注释点明了它的脆弱性:JSON.parse/stringify 会静默丢弃 Date、undefined、Map、Set、循环引用、函数——而且不报错,只是悄悄丢数据。静默丢数据比抛错更危险:new Date(2026, 0, 1) 经 JSON 往返后会变成一个字符串,Map 会变成空对象 {},循环引用则直接栈溢出。这类 bug 往往在很久之后才在下游代码中暴露。
With Ponytail:一个内置 API 收编全部场景
带 Ponytail 时,模型的输出收敛为一行:
// ponytail: structuredClone does this
const copy = structuredClone(original);
原文档的结论句是:"1 dependency (or a fragile hack) → 1 built-in." structuredClone 是 Web 平台标准的深克隆 API,它正确处理 Date、Map、Set、ArrayBuffer、RegExp、循环引用等所有 JSON.parse/stringify 静默丢弃的类型。可用性方面,原文档给出的前提是:2022 年起所有主流浏览器、Node.js v17 起均内置该 API——如果你的目标运行时仍在此之下(例如要兼容 IE11 或很老的 Node),才需要考虑回退方案,而那属于"原生方案确实不够用"的例外情形,仓库中 平台原生存根清单 对这类例外的态度也是明确的:
When the native solution is genuinely insufficient (old browser support, edge cases it doesn't handle, ergonomics that matter at scale), the library earns its place. Install it then, not before.
也就是说:lodash 在"原生确实不够用"时才挣得它的位置,而不是之前。
它命中了 Ponytail "决策梯子"的哪一级?
深克隆这个例子不只是"代码变短了",它演示了 Ponytail 技能定义中的核心机制。skills/ponytail/SKILL.md 规定 Agent 在写代码前必须爬一遍七级梯子,停在第一级能站住的地方:
1. 这玩意儿需要存在吗? → 不需要:跳过(YAGNI)
2. 本代码库里已经有了? → 复用,别重写
3. 标准库能做吗? → 用标准库
4. 平台原生功能覆盖吗? → 用原生功能
5. 已安装的依赖能解决吗? → 用它,别为几行代码新装依赖
6. 能一行写完吗? → 一行
7. 最后才是:能跑的最小代码
对"深克隆这个对象"这个任务:第 1 级通过(确实需要一份独立副本)、第 2 级跳过(代码库里没有现成工具)、第 4 级直接站住——平台原生的 structuredClone 就是答案,根本没机会走到"装依赖"或"手写递归"那一步。仓库中 docs/platform-native.md 的 JavaScript/Browser APIs 对照表把这条决策固化成了可查的速查表:
| You think you need | What the platform has |
|---|---|
query-string / qs |
new URLSearchParams(location.search) |
lodash.clonedeep |
structuredClone(obj) |
lodash.groupby |
Object.groupBy(arr, fn) |
uuid (v4) |
crypto.randomUUID() |
这张表横跨 HTML 元素、CSS 能力、浏览器 API、Swift/SwiftUI、Node 标准库、Python 标准库和数据库多个层面(完整清单见 docs/platform-native.md),深克隆只是其中一行,但它与同目录下的 examples/deep-clone.md、examples/group-by.md 等示例共同构成一套模式:你以为是需求的东西,往往是平台早就内置的能力。
值得注意的是梯子运行在"理解问题之后",而不是代替理解。SKILL.md 明确写着:先读任务触及的代码、追完真实流程,再爬梯子;且"懒"从不用于省略信任边界处的输入校验、防数据丢失的错误处理、安全与无障碍——深克隆场景中对应的就是:该 API 对无法克隆的类型(如 DOM 节点、含函数的对象)会直接抛 DataCloneError 而不是静默截断,行为边界清晰可测。
与其他示例横向对照:短到多少才算"对"?
深克隆的 1 行输出放在整个 examples 目录里并不是孤例。examples/README.md 给出了无/有 Ponytail 的行数对比,同为"装依赖 vs 用内置"模式还有:
| Example | Without (LOC) | With (LOC) |
|---|---|---|
| Email Validation | 75 | 3 |
| Debounce | 116 | 10 |
| CSV Sum | 20 | 3 |
| Countdown Timer | 267 | 9 |
| Rate Limiting | 128 | 10 |
其中 react-countdown.md 展示了无技能模式下的失控形态(267 行:四个变体、styled-components、CSS 动画、"Features" 清单),而有技能模式只交付任务要求的最小实现并附带一句 "Skipped: pause/resume, formatted display, … add when needed"。深克隆则是同类压缩的极致版:不是 267→9,而是"1 个依赖 → 1 个内置"。
如何验证:从仓库复现这条对比
上面的对比全部可验证,不需要相信任何人的转述:
- 查看原始对比文档:examples/deep-clone.md;
- 复现基准:在仓库根目录运行
npx promptfoo@latest eval -c benchmarks/promptfooconfig.yaml(配置见 benchmarks/promptfooconfig.yaml),示例输出即来自该基准的benchmarks/output.json; - 查看决策梯度的完整规则文本:skills/ponytail/SKILL.md;
- 查看"库 → 平台内置"的完整速查表:docs/platform-native.md。
小结:深克隆问题的可迁移结论
- 默认答案排序:遇到"深克隆"先想到
structuredClone,前提是浏览器(2022+)或 Node.js v17+;更老的运行时才评估JSONhack(知道它丢什么)或 lodash; - 依赖引入的门槛:为单个函数安装整个库是过线行为,"等需要这个库的其他功能时再装";
- 静默失败优于显式失败的反面:
JSON.parse(stringify)的危险在于不报错地丢数据,选内置 API 时优先选行为边界明确的; - 方法论层面:这一例是 Ponytail 七级梯子的标准走法——先确认任务真实存在,再优先复用代码库、标准库、平台原生能力,最后才动笔写代码。梯子不是"少写代码"的借口,"懒于方案、不懒于读代码"是 SKILL.md 的原文要求。
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