给 Prompt 或 Agent 建评测,很多人卡在第一步:要接哪个模型、key 从哪来。我一开始也这么想。后来发现 promptfoo 有个 echo provider,不需要 key,把输入原样返回,正好用来先把配置和断言这条链路跑通。先跑通再说。这篇记我装它、写断言、故意写错一条看它怎么报失败的完整过程。
npx 下载花了八分半
装的方式很多,README 里给了 brew、pip、npm 三种,我选了 npx,理由是不想往全局装东西。就这么简单。命令是 npx --yes promptfoo@latest --version,这一跑,下载花了大约八分半钟才吐出版本号 0.125.1。
中间 npm 还打了两条 deprecation 警告,一条是 node-domexception 说用平台原生的 DOMException,另一条是 @modelcontextprotocol/server-filesystem 说日期版本号不再发布、已经改回语义化版本 1.0.0。这两条是真实输出。我原样记下来了。这个下载时长是本机实测,不是估算,网络对 npm registry 的拉取就是这么慢。
版本号 0.125.1 值得记一下。promptfoo 现在挂在 OpenAI 名下,但仍然是 MIT 开源,README 第一段就写着这段关系。开源和归属是两件事。别因为收购就以为它闭源了。这是我读完 README 第一个想纠正的误解。npx 第一次下载会把包缓存到本地,第二次再跑就快了,这八分半是首次的代价,后面不是每次都等这么久。
echo provider 先把链路跑通
我写了一个最小的 promptfooconfig.yaml,providers 只写一个 echo,prompts 用 nunjucks 变量,tests 里放三条断言。echo 的机制很简单,它把渲染完的 prompt 原样当成输出返回,cost 永远是 0,tokenUsage 全 0,isRefusal 永远是 false。所以断言验的是输入经过变量替换之后长什么样,跟模型一点关系都没有。echo 还支持一个 delay 参数,单位毫秒,用来模拟慢响应,测你的 eval 在超时面前怎么表现。
prompts:
- '把这段文字原样返回:{{text}}'
providers:
- echo
tests:
- vars:
text: 'hello world'
assert:
- type: contains
value: 'hello world'
- vars:
text: 'goodbye world'
assert:
- type: contains
value: 'goodbye'
- vars:
text: 'foo bar'
assert:
- type: contains
value: 'THIS_SHOULD_FAIL_zzz'
然后跑下面这条命令:
npx --yes promptfoo@latest eval --no-cache
输出是一张矩阵表,每行一个测试用例。前两条标 [PASS],第三条标 [FAIL]。第三条是我故意写错的,value 里塞了一个不可能出现的字符串,就为了看失败长什么样。
最后的结果是 2 passed (66.67%),1 failed (33.33%),0 errors,Duration 0s,concurrency 4。这几个数都是本机终端直接跑出来的。concurrency 4 说的是同时跑 4 个用例,我 3 条一次就跑完了。
这里有个细节值得说。echo 的断言里用 type: contains 验的是子串包含,但官方还给了 similar、javascript、contains-json、llm-rubric 这些类型。除了 contains 和 javascript,别的都要再调一个 grading 模型或 embedding 模型。也就是说 echo 只能让你把纯字符串断言跑通,语义级的断言还是得回到真模型。这条边界是我读 provider 文档才看清的。
从 echo 换到真模型
跑通 echo 之后,换真模型只是一行配置的事,把 providers 从 echo 换成 openai:gpt-6-luna,再把 key 设成环境变量,README 里给的就是 export OPENAI_API_KEY=sk-... 这种形式。这一层我没真跑,因为不碰付费凭证,但配置结构已经从 echo 验证过了,换 provider 不会改断言这半边。
我的判断是:echo 这一步的价值,在于把评测该怎么组织和模型答得好不好这两件事拆开。前者用 echo 免费跑通,后者才值得花钱接真模型。很多人一上来就接真模型,结果配置写错了,白烧 token 才发现。那是顺序错了。先 echo 后真模型,这个顺序不该倒过来。
断言是回退的闸门
断言到底拦什么。一个 type: contains 验的是输出里必须出现某段话,一个 type: similar 验的是语义上接近某句话,一个 llm-rubric 验的是别自称 AI 这种定性要求。这些合起来,就是一道拦住回退的闸门。你改了 prompt,跑一遍 eval,断言还全绿,说明没改坏。有一条红了,说明改出问题了。
我建议的用法是:给每个关键场景至少写一条断言,把它接进 CI。promptfoo 官方支持 CI/CD 集成,跑完 exit code 非零就能挡 merge。断言这东西,写的时候觉得多余,回退发生的时候才知道它是最后一道网。这句话我是从自己改坏 prompt 的经历里体会的。那一次改了系统提示词,回退就发生在改动之后,当时要是有断言,当场就拦住了。
补一句具体的。如果你维护一个翻译 prompt,至少要有一条断言验输出里出现了目标语言的关键词,再加一条验它没把原文的专有名词吞掉:
tests:
- vars:
text: 'Hello world'
assert:
- type: contains
value: '你好'
- type: javascript
value: output.includes('world')
这两条断言写起来五分钟,但它能拦住绝大部分改坏。写断言别贪多,先覆盖会出人命的那几个场景。
给个分界。你在维护一个 prompt 或 agent、又怕改坏,那 promptfoo 这套断言值得上,而且先用 echo 跑通配置再上真模型。你只是偶尔调一次 prompt、没有回退的顾虑,那它的价值就只剩把输出排成一张矩阵方便肉眼看,可上可不上。真正值得抄的,不是它这个工具本身,是用断言把回退拦在 merge 之前这个习惯。