用 Promptfoo 系统化评测 Claude 分类提示词:claude-cookbooks 中从配置、运行到结果分析的完整实践
本篇指南围绕 claude-cookbooks 仓库中 capabilities/classification 教程的评测环节展开:如何使用 Promptfoo 对三种保险工单分类提示词(简单分类、RAG 增强、RAG+思维链)在 5 组不同 temperature 下的表现做自动化 A/B 评测。读完后,你将掌握 Promptfoo 配置文件(promptfooconfig.yaml)中 prompts、providers、tests、transform、output 五大配置段的完整写法,学会用命令行运行评测,并能用 pandas 读取结果文件做准确率归因分析。
背景:为什么需要 Promptfoo 做分类评测
capabilities/classification/guide.ipynb 演示了用 Claude 将保险客服工单分到 10 个业务类别(Billing Inquiries、Claims Assistance、Billing Disputes 等)的完整过程:从约 10% 的随机基线,到 ~70% 的简单提示词,再到 94% 的 RAG 增强版和 97% 的 RAG+思维链版本。Notebook 适合快速迭代,但它难以支撑生产级的评测需求——更大的测试集、多提示词变体并行对比、不同模型/温度参数的系统性比较、以及提示词改动后的回归检测。
本仓库的解法是把评测从 Notebook 中拆出来,交给开源 LLM 评测工具 Promptfoo 编排:同一份 68 条测试工单,同时跑 3 个提示词函数 × 5 个 temperature(0.0/0.2/0.4/0.6/0.8)共 15 组配置,输出自动化的 PASS/FAIL 判定,结果落盘为 CSV 供 Notebook 汇总分析。本指南的主体就是 评测说明文档 所描述的这套流程,并结合仓库中的配置文件与源码逐一展开。
前置条件
Promptfoo 是 Node.js 生态工具,运行前需要确认环境:
- 系统已安装 Node.js 与 npm(可参考 npm 官方安装指南,官方文档不在此列出链接);
- Promptfoo 可以通过 npm 安装后使用,也可以直接用 npx 免安装运行。本指南采用 npx 方式,无需本地全局安装。
一个重要的省事细节:由于 evaluation 目录下已经提供了初始化好的 promptfooconfig.yaml,不需要再执行 npx promptfoo@latest init 交互式初始化,直接进入配置阅读与运行阶段即可。
Python 侧的依赖则来自 RAG 检索组件。evaluation 目录下的提示词函数会复用 vectordb.py 中的 VectorDB 类,运行评测前需要安装 voyageai、numpy 等包;由于向量库文件 vector_db.pkl 已在仓库中预构建(由 68 条训练工单用 VoyageAI 的 voyage-2 模型编码而成),无需重新计算 embedding,也因此在只读评测场景下 VoyageAI 密钥的消耗仅来自查询侧 embedding(代码中还有查询缓存进一步降低调用次数)。
promptfooconfig.yaml:评测编排的五个配置段
整个评测由 promptfooconfig.yaml 一个文件编排,官方文档将其核心能力概括为四类:提示词导入、多 LLM 供应商接入、内置断言测试、以及结果输出。本仓库的配置覆盖了其中所有关键环节,逐段拆解如下。
1. Prompts:以 Python 函数导入三个提示词
# prompts defined in the prompts.py file
prompts:
- prompts.py:simple_classify
- prompts.py:rag_classify
- prompts.py:rag_chain_of_thought_classify
Promptfoo 支持以多种格式导入提示词,这里选择的是 prompt function(Python 函数)形式:prompts.py:函数名。三个函数定义在 prompts.py 中,与 guide.ipynb 中的分类函数一一对应,但有一个关键差异——它们不调用 Claude API,而是只返回提示词字符串:
def simple_classify(context: dict):
X = context["vars"]["text"]
prompt = (
textwrap.dedent("""
You will classify a customer support ticket into one of the following categories:
<categories>
{{categories}}
</categories>
Here is the customer support ticket:
<ticket>
{{ticket}}
</ticket>
Respond with just the label of the category between category tags.
""")
.replace("{{categories}}", categories)
.replace("{{ticket}}", X)
)
return prompt
API 调用的编排、结果存储全部交由 Promptfoo 处理。选择 Python 函数而非静态模板字符串,还有一个实际收益:模块顶层会直接实例化并加载向量库(见 prompts.py#L1-L7),使得 RAG 类提示词函数可以在生成提示词时同步完成检索:
from vectordb import VectorDB
vectordb = VectorDB()
# Load the vector database
vectordb.load_db()
simple_classify:类别定义 + 工单文本,要求模型把标签写在<category>标签内;rag_classify:在检索到语义最相似的 5 条训练样例后,以<example><query>...</query><label>...</label></example>的 XML 结构注入 few-shot 示例;rag_chain_of_thought_classify:在 RAG 基础上追加思维链指令,要求模型先在<scratchpad>标签内逐步推理,再在<category>标签内给出最终类别。
三个函数的类别定义(10 个 <category><label>...<content>... 块)与 Notebook 中完全一致,保证了评测口径的统一。
2. Providers:同一模型 × 5 档温度参数
providers:
- id: anthropic:messages:claude-haiku-4-5
label: "Haiku: T-0.0"
config:
max_tokens: 4096
temperature: 0
- id: anthropic:messages:claude-haiku-4-5
label: "Haiku: T-0.2"
config:
max_tokens: 4096
temperature: 0.2
# ... T-0.4 / T-0.6 / T-0.8 共 5 个 provider
Promptfoo 允许接入不同平台的多款 LLM 做交叉对比,本例的用途更聚焦:固定使用 claude-haiku-4-5(与 Notebook 中 MODEL = "claude-haiku-4-5" 一致),通过重复声明 provider 并以 label 区分,构成一个温度扫描实验矩阵。每个 provider 统一设置 max_tokens: 4096,temperature 依次取 0、0.2、0.4、0.6、0.8。这直接回答了一个分类任务中很实际的问题:Notebook 中固定的 temperature=0.0 是否真的是最优选择?评测矩阵会自动给出答案。
3. Tests:CSV 逐行断言 + 全局断言的两层设计
tests: dataset.csv
测试数据来自 dataset.csv,即 guide.ipynb 使用的同一份 68 条测试工单(与 data/test.tsv 同源)。Promptfoo 支持大量内置确定性断言,本例采用两层断言结构,原因是"评测条件随每一行变化 vs. 对所有测试用例一致":
- 逐行断言(定义在 CSV 中):
dataset.csv的列结构为text,label,__expected,其中__expected列写明该条工单的真值标签,例如:
text,label,__expected
I'm confused about a charge on my recent auto insurance bill...,Billing Inquiries,contains:Billing Inquiries
即"该条预测结果必须包含真值类别"。这是精确判分依据。
- 全局断言(定义在配置文件的
defaultTest中):对所有 15 组配置都成立的最低条件——输出必须命中 10 个合法类别之一,否则视为格式/输出失败:
assert:
- type: icontains-any
value:
- 'Billing Inquiries'
- 'Policy Administration'
- 'Claims Assistance'
- 'Coverage Explanations'
- 'Quotes and Proposals'
- 'Account Management'
- 'Billing Disputes'
- 'Claims Disputes'
- 'Policy Comparisons'
- 'General Inquiries'
icontains-any 是不区分大小写的"包含任一值"断言。这样,"输出跑题/空输出"和"输出正确类别但判错真值"两类错误可以被区分开。
4. Transform:从自由文本中抽取待测标签
defaultTest:
options:
transform: file://transform.py
由于模型输出是一段自然语言(思维链版本还包含大段推理文本),断言不能直接作用于原始响应,需要先用一个 Python 变换函数把待测标签抽出来。transform.py 全文如下:
def get_transform(output, context):
try:
return output.split("<category>")[1].split("</category>")[0].strip()
except Exception as e:
print(f"Error in get_transform: {e}")
return output
函数约定:Promptfoo 会以 get_transform(output, context) 签名调用它。这里取第一个 <category> 与 </category> 之间的文本并去空白;解析失败时打印错误并原样返回输出(此时全局断言大概率判 FAIL,错误因此可观测)。这一点与 Notebook 中 CoT 版本的解析逻辑(response.content[0].text.split("<category>")[1].strip())是同一思路,只是抽离成了可复用、可被评测框架直接调用的纯函数。
值得注意的是一个实现差异:Notebook 中的 simple_classify/rag_classify 通过 assistant prefill("<category>")加 stop_sequences=["</category>"] 强制模型只输出标签;而 promptfoo 版本的提示词函数无法在返回的字符串中表达 prefill,因此改为依赖提示词中的格式指令("Respond with just the label of the category between category tags")+ transform 抽取。从 promptfooconfig.yaml 的 provider 配置中也可以确认没有设置 prefill 相关参数,两条实现路径在输出约束机制上是等价的近似。
5. Output:结果落盘路径
outputPath: ../data/results.csv
outputPath 相对于评测运行目录(即 evaluation/)解析,实际写入 results.csv。Promptfoo 支持多种输出格式,也支持用其 Web UI 查看,但这里选择 CSV,是因为 guide.ipynb 中的结果分析单元直接用 pandas 读取该文件(见下文)。
运行评测
按 评测说明 的步骤执行:
- 打开终端,进入评测目录(仓库内的
capabilities/classification/evaluation); - 设置两个必需的环境变量——
ANTHROPIC_API_KEY用于调用 Claude,VOYAGE_API_KEY用于 RAG 提示词函数中的查询 embedding:
export ANTHROPIC_API_KEY=YOUR_API_KEY
export VOYAGE_API_KEY=YOUR_API_KEY
- 执行评测:
npx promptfoo@latest eval
- 需要提高请求并发度时(默认并发为 4),用
-j参数调整:
npx promptfoo@latest eval -j 25
评测完成后,终端会按 dataset.csv 的每行打印各配置的判定结果。此时仓库中已提交的 results.csv 即是一次完整运行的产物:每个单元格是一段 JSON,包含 provider(如 "Haiku: T-0.0")、id、metrics(含 score、testPassCount、testFailCount、totalLatencyMs、tokenUsage、cost)以及 [PASS]/[FAIL] 文本,可以直接回到 guide.ipynb 做汇总分析。
结果分析:回到 guide.ipynb 读取结果
Notebook 末尾的分析单元(guide.ipynb 第 33 个代码单元)展示了如何消费这份 CSV:
import json
import pandas as pd
promptfoo_results = pd.read_csv("./data/results.csv")
examples_columns = promptfoo_results.columns[2:]
number_of_providers = 5
number_of_prompts = 3
prompts = ["Simple", "RAG", "RAG w/ CoT"]
columns = ["label", "text"] + [
json.loads(examples_columns[prompt * number_of_providers + provider])["provider"]
+ " Prompt: "
+ str(prompts[prompt])
for prompt in range(number_of_prompts)
for provider in range(number_of_providers)
]
promptfoo_results.columns = columns
result = (
promptfoo_results.iloc[:, 2:].astype(str).apply(lambda x: x.str.count("PASS")).sum()
/ len(promptfoo_results)
* 100
).sort_values(ascending=False)
print(result)
思路是:15 个结果列按"3 提示词 × 5 provider"的顺序排列,先解析每列 JSON 中的 provider 字段把列名重命名为 Haiku: T-x.x Prompt: xxx,再统计每列 PASS 出现次数除以总行数得到准确率,降序输出。
基于仓库中已提交的 results.csv 与 Notebook 的结论记录,关键发现可以交叉验证:
| 提示词 | temperature | testPassCount(n=68) | 准确率 | 单轮成本 |
|---|---|---|---|---|
| Simple | 0.0 | 48/68 | ~70.6% | $0.0136 |
| RAG | 0.0 | 64/68 | ~94.1% | $0.0183 |
| RAG w/ CoT | 0.0 | 65/68 | ~95.6% | $0.0391 |
Notebook 记录的规律与 results.csv 中的 metrics 完全吻合:
- CoT 对温度几乎不敏感:RAG w/ CoT 在 T=0.0/0.2/0.8 下 testPassCount 均为 65/68(约 95.59%),思维链推理过程本身稳定了输出,采样随机性影响很小;
- RAG 整体稳健但 T=0.0 最优:RAG 各温度下通过率在 89%~94% 区间,T=0.0 达到 94.12%(64/68);
- Simple 提示词温度无关:各温度下稳定在 70% 上下(48~49/68),说明没有 RAG/CoT 时模型基本只靠类别定义工作;
- 生产建议:
temperature=0.0+ RAG w/ CoT 组合一致性与准确率最佳。
metrics 中的 tokenUsage 还提供了成本归因视角:CoT 版本每次运行 completion tokens 约 1.6 万(T=0.0 时为 16557),而 Simple/RAG 仅约 850,接近 20 倍,这正是 CoT 总成本($0.039)明显高于 Simple($0.014)的原因——在准确率收益(约 25 个百分点)与推理成本之间做取舍时,这类数据是决策依据。
源码层面的实现印证
为了让评测结果可信、可复现,有几处仓库源码细节值得核对:
向量检索参数。vectordb.py 中的 search(query, k=5, similarity_threshold=0.85) 使用 numpy 点积计算相似度并只保留高于阈值的样例:
similarities = np.dot(self.embeddings, query_embedding)
top_indices = np.argsort(similarities)[::-1]
top_examples = []
for idx in top_indices:
if similarities[idx] >= similarity_threshold:
example = {
"metadata": self.metadata[idx],
"similarity": similarities[idx],
}
top_examples.append(example)
if len(top_examples) >= k:
break
注意一个值得留意的版本差异:evaluation/vectordb.py 的相似度阈值默认是 0.85,而 guide.ipynb 内联的 VectorDB 实现为 0.75(见 guide.ipynb 中 VectorDB 代码单元)。从源码结构看,这会导致评测环境下检索到的 few-shot 示例比 Notebook 交互运行时更少、更严格,比较两组结果时应将此纳入考量。此外评测版还内建了 query_cache 查询缓存(vectordb.py#L45-L51),对重复出现的测试文本只调用一次 embedding API,配合预构建的 vector_db.pkl(db_path 相对 evaluation/ 目录指向 ../data/vector_db.pkl,即分类教程共享的 data 目录),使评测对 VoyageAI 的依赖降至最低。
向量库内容来源。vector_db.pkl 中的训练样例来自 train.tsv(68 条带标签工单),与 test.tsv 同源同构;evaluation/dataset.csv 则与测试集对齐(68 行数据 + 表头)。三个数据文件(train.tsv / test.tsv / dataset.csv)保证了"Notebook 内联评测"与"Promptfoo 批量评测"在同一数据面上可比。
断言与输出的对应关系。results.csv 每格 JSON 中的 assertPassCount(如 Simple T-0.0 为 116 = 2 条断言 × 58 次通过)表明每条用例同时跑了 CSV 的 __expected 断言与 defaultTest 的 icontains-any 断言,两层断言都在生效。
要点回顾
- 一份 promptfooconfig.yaml 即可编排"3 提示词 × 5 温度"的 15 组对照实验,核心配置段为 prompts(Python 提示词函数)、providers(同模型多温度)、tests(CSV 逐行断言 + 全局断言)、defaultTest.transform(Python 抽取函数)、outputPath(CSV 落盘);
- 提示词函数只负责"生成提示词",API 调用与结果存储交给 Promptfoo;RAG 能力通过复用 vectordb.py 的
VectorDB类在提示词生成阶段完成,前提是有预构建的向量库和VOYAGE_API_KEY; - 运行只需
export两个密钥后执行npx promptfoo@latest eval(-j 25可提升并发,默认 4),产物为带 PASS/FAIL 与完整 metrics 的 CSV; - 结果用 pandas 统计每列 PASS 占比即可得到各配置准确率,结合
testPassCount、tokenUsage、cost字段可做温度敏感性与成本归因分析; - 复现时留意评测版
VectorDB的similarity_threshold=0.85与 Notebook 版 0.75 的差异,以及 CoT 方案约 20 倍的输出 token 成本。
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 StartedRust0623
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