Ponytail 邮件校验案例解析:同一模型同一提示词下 75 行对 3 行,Promptfoo 基准背后的完整方法
本文围绕 examples/email-validation.md 展开:它记录了同一个真实任务(“写一个校验邮箱地址的 Python 函数”)在两种设置下、由同一个模型(Claude Haiku 4.5,temperature 1)逐字(verbatim)产出的对比结果——无技能(no-skill)组 75 行代码,ponytail 组 3 行代码。读完本文,你能理解这组对比样本的完整出处、每一侧输出的内容构成、ponytail 规则集(SKILL.md)如何驱动输出,以及如何用 promptfoo 在本地复现该基准。
1. 样本来源:基准运行中的逐字模型输出
examples/email-validation.md 开头就声明了样本的采集方式,这一点在 examples/README.md 中被再次强调:
- 这些示例不是人工编写的(“These are not hand-written”),而是基准运行中的原文输出;
- 同一个模型、同一个任务,分别在“无技能”(
## Without Ponytail)和“带 ponytail”(## With Ponytail)两种设置下作答,供并排对比; - 模型为 Claude Haiku 4.5,temperature 1,原始来源为
benchmarks/output.json(基准运行产物); - 复现命令:
npx promptfoo@latest eval -c benchmarks/promptfooconfig.yaml。
该任务的原始提示词在基准配置中定义(见 benchmarks/promptfooconfig.yaml):
- vars: { task: "Write me a Python function that validates email addresses." }
它是五个日常任务之一(email validator、JS debounce、CSV sum、React countdown、FastAPI rate-limit),全部来自同一份配置文件。
2. 基线组(无技能):75 行的邮件校验代码
无技能组收到的是裸任务——没有系统提示词,只有任务本身。模型的回答结构是“给方案 + 再给更稳的方案 + 推荐第三方库 + 对比表”,这正是典型的“over-build”形态:75 行代码覆盖三个版本。
2.1 简单正则版
import re
def validate_email(email: str) -> bool:
"""
Validates an email address using a regular expression.
Args:
email (str): The email address to validate
Returns:
bool: True if valid, False otherwise
"""
# Regular expression pattern for email validation
pattern = r'^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$'
return re.match(pattern, email) is not None
# Test cases
if __name__ == "__main__":
test_emails = [
"user@example.com", # Valid
"john.doe@company.co.uk", # Valid
"test+tag@domain.org", # Valid
"invalid.email@", # Invalid
"missing@domain", # Invalid
"@nodomain.com", # Invalid
"spaces in@email.com", # Invalid
"user@domain..com", # Invalid (double dot)
]
for email in test_emails:
result = validate_email(email)
print(f"{email:<30} -> {result}")
一个函数加一段 docstring、一条正则、一个内嵌测试循环——模型还主动替调用方准备了 8 个测试用例。
2.2 更健壮版(带详细反馈)
同一份回答里,模型又给了一个“advanced”版本,返回 (is_valid, message) 元组,追加了 RFC 层面的检查:
import re
def validate_email_advanced(email: str) -> tuple[bool, str]:
"""
Advanced email validation with detailed feedback.
Args:
email (str): The email address to validate
Returns:
tuple: (is_valid, message)
"""
# Basic checks
if not email or not isinstance(email, str):
return False, "Email must be a non-empty string"
email = email.strip()
if len(email) > 254:
return False, "Email is too long (max 254 characters)"
# Regular expression pattern
pattern = r'^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$'
if not re.match(pattern, email):
return False, "Email format is invalid"
# Additional checks
local_part, domain = email.rsplit('@', 1)
if len(local_part) > 64:
return False, "Local part is too long (max 64 characters)"
if local_part.startswith('.') or local_part.endswith('.'):
return False, "Local part cannot start or end with a dot"
if '..' in email:
return False, "Email cannot contain consecutive dots"
return True, "Email is valid"
# Test the advanced version
if __name__ == "__main__":
test_emails = [
"user@example.com",
"invalid..email@domain.com",
".invalid@domain.com",
"user@domain",
]
for email in test_emails:
is_valid, message = validate_email_advanced(email)
print(f"{email:<35} -> {is_valid:5} ({message})")
254 字符总长、64 字符本地部分、点号边界、连续点号——这些是真实的 RFC 约束,但它们是用户没有要求的“规格完整性”。
2.3 第三方库版
from email_validator import validate_email, EmailNotValidError
def validate_email_production(email: str) -> bool:
"""
Validates email using the email_validator library.
More reliable for production use.
Install with: pip install email-validator
"""
try:
# Validate and normalize the email
valid = validate_email(email)
return True
except EmailNotValidError:
return False
# Usage
if __name__ == "__main__":
print(validate_email_production("user@example.com")) # True
print(validate_email_production("invalid@")) # False
2.4 模型自己的对比与建议
原文档最后附了模型给出的三方案对比表和结论:
| 方法 | 优点 | 缺点 |
|---|---|---|
| 正则(简单) | 快、无依赖 | 校验有限 |
| 正则(进阶) | 更彻底 | 仍不完全符合 RFC |
| email-validator 库 | 符合 RFC、可靠 | 外部依赖 |
模型建议:生产环境用 email-validator(正确处理 RFC 5321/5322),快速脚本用简单正则。注意:这套“三版本 + 对比表 + 推荐”的输出正是 ponytail 想消除的东西——用户只问了一个函数,却收到三个实现和一段设计论述。
3. Ponytail 组:3 行代码与“刻意跳过”的声明
同一模型、同一提示词,在 ponytail 规则集下,输出是:
import re
def is_valid_email(email: str) -> bool:
return bool(re.match(r'^[^@]+@[^@]+\.[^@]+$', email))
外加一句刻意跳过的说明(原文):
Skipped: RFC 5322 parser, DNS MX lookup, confirmation email. Add when you actually need to reject
user+tag@sub.domain.co.ukor catch typos, until then, this catches 99% of "oops I fat-fingered it" cases.
这个输出形态不是偶然的,它由规则集 skills/ponytail/SKILL.md 直接约束:
- 决策阶梯(The ladder)(skills/ponytail/SKILL.md#L32-L48):先问“这东西需要存在吗(YAGNI)”→ 代码库里已有吗 → 标准库能做吗 → 平台原生能力能覆盖吗 → 已装依赖能解决吗 → 能一行搞定吗 → 最后才是“能工作的最少代码”。邮箱校验落在第 3 级(标准库
re)与第 6 级(一行)之间,因此停止在 3 行。 - 输出纪律(skills/ponytail/SKILL.md#L66-L75):代码先行,随后最多三行说明“跳过了什么、何时再加”,固定模式为
[code] → skipped: [X], add when [Y]。示例中的那句 "Skipped: … Add when …" 就是这个模式的逐字体现——RFC 5322 解析、DNS MX 查询、确认邮件都被点名为“被跳过项”,并给出了触发追加的具体条件。 - 强度分级:默认
full档执行完整阶梯;ultra档会更激进地挑战需求本身,lite档则只指出更懒的替代方案。
规则集还有一条重要边界(skills/ponytail/SKILL.md#L90-L112):信任边界上的输入校验、防数据丢失的错误处理、安全措施不可被简化掉;“懒”针对的是过度构建,不是理解问题本身。
结论行(原文):75 → 3 lines of code, same model, same prompt.
4. 基准如何搭建:配置与两个 Arm(源码级)
这一节解释“no-skill arm vs ponytail arm”在工程上如何实现。
4.1 入口配置 benchmarks/promptfooconfig.yaml
description: "Ponytail vs caveman vs no-skill: same model, same tasks. Measures code LOC (deterministic) and tokens/cost (API telemetry)."
providers:
- id: anthropic:messages:claude-haiku-4-5-20251001
config: { max_tokens: 8192, temperature: 1 }
# …另有 claude-sonnet-4-6 与 claude-opus-4-8,同样 max_tokens 8192 / 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
defaultTest:
assert:
- type: javascript
value: file://loc.js
metric: code_loc
- type: javascript
value: file://correctness.js
metric: correct
tests:
- vars: { task: "Write me a Python function that validates email addresses." }
# …debounce、CSV sum、React countdown、FastAPI rate-limit
要点:
- 三个 provider 全部
temperature: 1, max_tokens: 8192,保证“同模型同参数”;样本中使用的 Haiku 4.5 即其一; - 三个 arm 以
prompts形式注入,每个 arm 是一个 JS 模块,把vars.task包装成不同的消息序列; - 每个测试单元格都挂两个断言:
loc.js记录代码行数(度量,恒通过),correctness.js做正确性把关(gate,答错即失败)。
4.2 Arm 实现:baseline 与 ponytail 的差异只有一个系统提示
基线组(benchmarks/arms/baseline.js)只有两行,任务裸发:
// Baseline arm: no skill, just the task.
module.exports = ({ vars }) => [{ role: 'user', content: vars.task }];
ponytail 组(benchmarks/arms/ponytail.js)则把仓库自带的 SKILL.md 整文件作为系统提示词读入:
// 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');
module.exports = ({ vars }) => [
{ role: 'system', content: system },
{ role: 'user', content: vars.task },
];
注意“Single source of truth”的注释:基准中使用的规则与用户插件安装后生效的规则是同一份文件 skills/ponytail/SKILL.md,避免了“基准里贴的是旧规则”这类偏差。因此第 3 节对 SKILL.md 的解读,同时就是对 ponytail arm 行为机制的解读。
4.3 LOC 如何计算:benchmarks/loc.js
行数不是简单地数 \n,而是去除注释后的非空行:
- 优先从 fenced 代码块(```) 中提取;若无围栏代码,把整个回复视为一个块(对应 correctness.js 中“模型常裸答代码”的兜底策略);
- 先剥掉
/* ... */块注释(注释里写了原因:早期只过滤*对齐的 JSDoc,普通块注释会被误计为代码); - 再过滤空行、
//、#、*开头的行。
这正是 examples 目录标题 “Without (LOC) / With (LOC)”(75 / 3)的口径:75 与 3 都是按此规则计出的代码行数,不含散文。
5. 正确性门槛:为什么“更短”没有被判成“更烂”
benchmarks/README.md 对两个指标的定位一句话:“A broken one-liner that scores great on LOC will fail on correctness.” 具体到邮箱任务,benchmarks/correctness.js#L74-L130 的 email 检查器做了三件事:
- 从回复中提取 Python 代码块(找不到围栏时,含
def的裸文本块也认); - 在生成的函数名候选(
validate_email、is_valid_email、email_validator、is_valid、validate)中定位校验函数,兜底逻辑是找任意单参可调用对象——所以基线组的validate_email与 ponytail 组的is_valid_email都能被同一个 harness 抓住; - 追加断言后真正执行(spawn python3/python):
if not fn("user@example.com"):
failures.append("rejected valid: user@example.com")
if not fn("a@b.co"):
failures.append("rejected valid: a@b.co")
if fn("no-at-sign"):
failures.append("accepted invalid: no-at-sign")
if fn(""):
failures.append("accepted invalid: empty string")
if fn("@missing-local.com"):
failures.append("accepted invalid: @missing-local.com")
可以验证 ponytail 的 3 行正则在语义上确实覆盖这些断言:^[^@]+@[^@]+\.[^@]+$ 要求本地部分非空(拒 @missing-local.com 与空串)、域部分含至少一个点(拒 no-at-sign),同时放行 user@example.com 与 a@b.co。也就是说“更短”在这个 harness 下是被执行验证过的,而非仅靠行数好看。
执行细节上,harness 写入临时 .py 文件后调用系统 Python 运行,超时默认 30 秒(可用 PONYTAIL_CORRECTNESS_TIMEOUT_MS 覆盖,见 benchmarks/correctness.js#L14-L17)。README 同时提醒:五个任务中 email、debounce、CSV 是真执行,React countdown 与 FastAPI rate-limit 只做关键词/结构检查。
6. 如何复现这个样本
按 benchmarks/README.md 的说明,复现邮箱这一个单元格(或完整五任务矩阵)的路径是:
前置条件:Anthropic API key(环境变量或 .env 文件,见 benchmarks/promptfooconfig.yaml#L7)、Node.js ≥ 22.22.0(promptfoo 引擎约束)、Python 3 与 pandas(correctness 检查器会 spawn Python 执行生成代码)。
# 在 benchmarks/ 目录下(README 的写法,.env 位于仓库根目录)
npx promptfoo@latest eval -c promptfooconfig.yaml --env-file ../.env --repeat 10
npx promptfoo@latest view
两个容易踩的点(均出自 README 原话):
--env-file ../.env是必需的,因为 promptfoo 只从当前目录(benchmarks/)读.env,而文件实际放在仓库根目录;- 单跑一次只能得到一次采样;官方数字是 每格 10 次取中位数,成本数字另行以 30 次重跑复核。
本地模型路线(无 API key)也可跑:python benchmarks/benchmark-local.py --model llama3.2 --repeat 3(经 Ollama)。README 同时给出诚实提示:该规则集在强指令遵循的 Claude 级模型上表现好,迁移到小型本地模型时“多步决策阶梯”不能被可靠遵循,结果会变差。
7. 如何解读“75 → 3”这组数字
结合仓库内已有的结果数据,这组对比应该这样读:
- 口径:
code_loc只数代码行,不含散文。基线组那 75 行里包含 8 个自写测试用例、docstring 和第二个函数的完整脚手架;ponytail 组的 3 行不含任何自测——SKILL.md 明确说“trivial one-liners need no test, YAGNI applies to tests too”(skills/ponytail/SKILL.md#L107-L112),所以两边在“是否自带测试”上并非同口径竞争,这是阅读时的一个注意点。 - 横向位置:examples/README.md 的总表中,email validation 的 75 → 3 是五个任务里 LOC 压缩最悬殊的一个(debounce 116→10、CSV sum 20→3、React countdown 267→9、rate limit 128→10)。
- 全局基准:benchmarks/README.md#L38-L44 的 10 次中位数表显示,五个任务合计 Haiku 上 baseline 518 行 vs ponytail 39 行,Sonnet 693 → 44,Opus 256 → 51;caveman(另一个散文压缩技能)落在中间。
- 诚实性边界:README 的 2026-06-18 更新明确指出,这类数字是单轮(single-shot)对比裸模型的口径,裸模型“多选项 + 评论”的回答把散文也算进去了,因此会高估优势;更可信的口径是 agentic 基准(真实 Claude Code 会话跑真实公开仓库),ponytail 在“过度构建陷阱”型任务上减 60–94%,在已经极简的代码上打平,且保持 100% 安全(见 benchmarks/README.md#L64-L71)。邮箱样例属于典型的“过度构建陷阱”任务——用户要一个函数,裸模型交付了三套实现。
8. 小结
examples/email-validation.md 这个样本的价值不在于“3 行正则能校验邮箱”本身,而在于它展示了一条可复现的验证链:同一份 skills/ponytail/SKILL.md 作为系统提示注入(benchmarks/arms/ponytail.js)→ 模型输出被 benchmarks/loc.js 以去注释非空行口径度量 → 被 benchmarks/correctness.js 真实执行 5 个断言把关 → 最终得到“同模型、同提示词、75 行对 3 行、且更短的一侧功能不降”的结论。想深入其他任务样本(debounce、CSV sum、React countdown、rate limit)或成本/延迟数据,可从 examples/README.md 与 benchmarks/README.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 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