首页
/ Ponytail 邮件校验案例解析:同一模型同一提示词下 75 行对 3 行,Promptfoo 基准背后的完整方法

Ponytail 邮件校验案例解析:同一模型同一提示词下 75 行对 3 行,Promptfoo 基准背后的完整方法

2026-09-04 09:41:11作者:廉皓灿Ida

本文围绕 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.uk or 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-L130email 检查器做了三件事:

  1. 从回复中提取 Python 代码块(找不到围栏时,含 def 的裸文本块也认);
  2. 在生成的函数名候选(validate_emailis_valid_emailemail_validatoris_validvalidate)中定位校验函数,兜底逻辑是找任意单参可调用对象——所以基线组的 validate_email 与 ponytail 组的 is_valid_email 都能被同一个 harness 抓住;
  3. 追加断言后真正执行(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.coma@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 3pandas(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.mdbenchmarks/README.md 继续追踪。

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

项目优选

收起
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
590
ops-nnops-nn
本项目是CANN提供的神经网络类计算算子库,实现网络在NPU上加速计算。
C++
904
1.82 K
pytorchpytorch
作为 Ascend for PyTorch 社区的核心组件,TorchNPU 是昇腾专为 PyTorch 打造的深度学习适配插件,使 PyTorch 框架能够直接调用昇腾 NPU,为开发者提供昇腾 AI 处理器的超强算力。
Python
854
1.34 K
docsdocs
暂无描述
Markdown
889
5.78 K
jiuwenswarmjiuwenswarm
JiuwenSwarm 是一款基于openJiuwen开发的智能AI Agent,它能够将大语言模型的强大能力,通过你日常使用的各类通讯应用,直接延伸至你的指尖。
Python
3.52 K
1.01 K
ops-mathops-math
本项目是CANN提供的数学类基础计算算子库,实现网络在NPU上加速计算。
C++
1.33 K
1.45 K
cann-learning-hubcann-learning-hub
CANN 学习中心仓,支持在线互动运行、边学边练,提供教程、示例与优化方案,一站式助力昇腾开发者快速上手。
Jupyter Notebook
983
503
AscendNPU-IRAscendNPU-IR
AscendNPU-IR是基于MLIR(Multi-Level Intermediate Representation)构建的,面向昇腾亲和算子编译时使用的中间表示,提供昇腾完备表达能力,通过编译优化提升昇腾AI处理器计算效率,支持通过生态框架使能昇腾AI处理器与深度调优
C++
540
384