一份从零开始的完整指南,涵盖安装、接入、断言到进阶追踪的全流程
为什么要评测智能体?
AI 智能体(Agent)与传统 LLM 调用有着本质区别:它不是一个“输入-输出”的简单过程,而是经历“规划→调用工具→观察结果→再规划”的多次循环。这种复杂性使得传统的手工测试难以覆盖全面——两个智能体可能给出相同的最终答案,但一个调用了 3 次工具,另一个却调用了 30 次,光看最终文本完全看不出这种差异。
这就是我们需要自动化评测工具的原因。
Promptfoo 正是这样一款工具:它能帮我们批量运行测试用例,自动打分,甚至能深入智能体内部,追踪每一次工具调用的轨迹。
第一步:安装 Promptfoo
Promptfoo 是一个命令行工具,要求 Node.js ^20.20.0 或 >=22.22.0。三种安装方式选一种即可:
全局安装(推荐,以后直接用 promptfoo 命令):
npm install -g promptfoo
临时运行(不想全局安装时):
npx promptfoo@latest
Homebrew 安装(Mac/Linux 用户):
brew install promptfoo
安装完成后验证一下:
promptfoo –version
能看到类似 0.97.0 的版本号,就说明装好了。
第二步:理解评测的运行原理
Promptfoo 的评测流程可以用一个循环概括:配置 → 评测 → 查看。
首先,你需要创建一个 promptfooconfig.yaml 配置文件,在里面定义三个核心要素:
- prompts:要测试的提示词模板
- providers:被测试的对象(模型或你的智能体)
- tests:测试用例和断言规则
然后,用一条命令启动评测:
promptfoo eval
如果你想在修改配置后自动重新评测,可以加上 –watch 参数:
promptfoo eval –watch
评测完成后,打开网页看结果:
promptfoo view
这个命令会启动本地 Web 服务器,并在浏览器中自动打开可视化报告页面。
记住这个"config → eval → view"循环,它会贯穿你测试任何智能体的全过程。
第三步:把你的智能体接进来(核心)
Promptfoo 本身不直接认识 LangGraph、CrewAI、Google ADK 这些框架的 API,所以你需要写一个"翻译层"文件,把 Promptfoo 的调用请求转发给智能体,再把智能体的结果转发回来。这个翻译层在 Promptfoo 中称为 Python Provider。
3.1 最简单的例子:理解三个参数和一个返回值
官方文档给出的最简示例:
# echo_provider.py
def call_api(prompt, options, context):
"""Simple provider that echoes the prompt with a prefix."""
config = options.get('config', {})
prefix = config.get('prefix', 'Tell me about: ')
return {
"output": f"{prefix}{prompt}"
}
-
def call_api(prompt, options, context): —— Promptfoo 只认这一个函数名,签名固定。
- prompt:当前测试用例渲染后的最终输入文本。
- options:字典,options['config'] 对应 YAML 里 provider 下的 config 字段,用来传自定义参数(模型名、超参数、密钥路径等)。
- context:字典,context['vars'] 是当前测试用例定义的所有变量。
-
config = options.get('config', {}):取出配置字典,没配置就用空字典兜底,避免 KeyError。
-
prefix = config.get('prefix', 'Tell me about: '):取 prefix 配置项,YAML 没写就用默认值。
-
return {"output": f"{prefix}{prompt}"}:最关键的一行——不管智能体内部逻辑多复杂,最终必须返回一个带 output key 的字典。Promptfoo 后续所有断言都基于这个 output 字段。
配套的 YAML 配置:
# promptfooconfig.yaml
providers:
– id: 'file://echo_provider.py'
prompts:
– 'Tell me a joke'
– 'What is 2+2?'
-
providers: – id: 'file://echo_provider.py':file:// 前缀告诉 Promptfoo 这不是内置模型 ID,而是本地 Python 文件,加载里面的 call_api。
-
prompts: 列表里的每一条字符串都会作为 prompt 参数传进 call_api,跑一次测试。
性能提示:Promptfoo 对 Python Provider 采用常驻进程的方式执行。脚本只在 worker 启动时加载一次,之后每次调用都复用这个进程。所以哪怕你的智能体 import 很重(比如加载模型权重),也不会拖慢每次调用的速度。
3.2 真实案例:CrewAI 多智能体系统接入全过程
CrewAI 是一个多智能体协作框架,我们用一个完整的招聘智能体案例来演示真实项目怎么接。
第一步:安装依赖
pip install crewai
npm install -g promptfoo
验证都装好了:
python3 -c "import crewai ; print('✅ CrewAI ready')"
promptfoo –version
第二步:定义 CrewAI 智能体
这部分是 CrewAI 自己的 API,跟 Promptfoo 无关:
# agent.py
import os
import asyncio
from typing import Dict, Any
from crewai import Agent, Task, Crew
OPENAI_API_KEY = os.environ.get("OPENAI_API_KEY")
def get_recruitment_crew(model: str = "openai:gpt-4o") –> Crew:
agent = Agent(
role="Senior Recruiter specializing in technical roles",
goal="Find the best candidates for a given set of job requirements and return the results in a valid JSON format.",
backstory="你是一个经验丰富的招聘专家,擅长从大量简历中筛选出最匹配的候选人。",
verbose=False,
model=model,
api_key=OPENAI_API_KEY
)
task = Task(
description="根据以下招聘需求,找出最合适的候选人:{requirements}",
expected_output="一个包含 candidates 数组和 summary 字符串的 JSON 对象",
agent=agent
)
crew = Crew(
agents=[agent],
tasks=[task],
verbose=False
)
return crew
async def run_recruitment_agent(prompt: str, model: str = "openai:gpt-4o") –> Dict[str, Any]:
try:
crew = get_recruitment_crew(model=model)
result = await crew.kickoff_async(inputs={"requirements": prompt})
return {"output": result, "raw_output": str(result)}
except Exception as e:
return {"error": str(e), "raw_output": ""}
第三步:写 Promptfoo 适配层
真正对接 Promptfoo 的是下面这个函数:
# agent.py (接续上面的代码)
def call_api(prompt: str, options: Dict[str, Any], context: Dict[str, Any]) –> Dict[str, Any]:
try:
# 从配置中获取模型名
config = options.get("config", {})
model = config.get("model", "openai:gpt-4o")
# CrewAI 内部是异步的,但 Promptfoo 要求同步返回,所以用 asyncio.run 包装
result = asyncio.run(run_recruitment_agent(prompt, model=model))
if "error" in result:
return {"error": result["error"], "raw": result.get("raw_output", "")}
return {"output": result["output"]}
except Exception as e:
# 兜底捕获,避免整个评测进程崩溃
return {"error": f"An error occurred in call_api: {str(e)}"}
-
config = options.get("config", {}) / model = config.get("model", "openai:gpt-4o"):跟 echo 例子一样,从 YAML 传进来的配置里取模型名,没配就用默认值。这样你可以在 YAML 中为不同测试场景指定不同模型。
-
result = asyncio.run(run_recruitment_agent(prompt, model=model)):CrewAI 内部的执行是异步的(async def),但 Promptfoo 要求 call_api 同步返回,所以用 asyncio.run(…) 把异步调用包成同步。这一步内部会走完 CrewAI 的完整协作流程。
-
if "error" in result: return {"error": …}:如果内部执行出错(比如 LLM 返回格式不对),把错误信息带出来,方便你在 Promptfoo 报告里看到失败原因,而不是让整个评测崩溃。
-
except Exception as e: return {"error": …}:兜底捕获,任何意外异常都转换成 Promptfoo 能理解的 {"error": …} 格式。
第四步:编写 YAML 配置文件
# promptfooconfig.yaml
providers:
– id: 'file://agent.py'
config:
model: 'openai:gpt-4o'
prompts:
– '我们正在招聘一名高级 Python 工程师,要求有 5 年以上经验,熟悉 Django 和微服务架构。'
tests:
– description: '招聘需求包含技术栈要求'
assert:
– type: is–json
value:
schema:
type: object
required: ['candidates', 'summary']
properties:
candidates:
type: array
items:
type: object
required: ['name', 'experience_years', 'skills']
properties:
name:
type: string
experience_years:
type: number
skills:
type: array
items:
type: string
summary:
type: string
第五步:运行评测
export OPENAI_API_KEY="sk-xxx-your-api-key-here"
promptfoo eval
promptfoo view
这一步 Promptfoo 会:用配置调用 CrewAI Provider → 输入招聘需求 → 收集结构化输出 → 用断言检查候选人列表和摘要是否存在 → 生成 pass/fail 报告。网页里能看到测试用例表、每条的 pass/fail、通过率、延迟等统计信息。
3.3 其他框架同理
只要你能用 Python 写出一个 call_api(prompt, options, context) 函数并返回带 output 的字典,任何框架都能接入。官方提供了完整的对照表和可运行示例,覆盖 LangGraph、LangChain、CrewAI、Python 版 OpenAI Agents SDK、PydanticAI、Google ADK、Strands Agents 等主流框架。
第四步:写断言(Assertion),让 Promptfoo 自动打分
跑完 promptfoo eval 后,Promptfoo 拿到 Provider 返回的 output 字段,然后按 tests[].assert 里配置的规则逐条判断对错。
断言分两大类:
确定性断言(不需要模型参与,直接匹配)
| contains | 检查输出里是否包含某个字符串 |
| not-contains | 检查输出里是否不包含某个字符串 |
| equals | 完全匹配 |
| matches | 正则表达式匹配 |
| is-json | 检查输出是否是合法 JSON,还能配合 JSON Schema 校验结构 |
示例:CrewAI 招聘案例中,用 is-json 配合 Schema 校验输出必须包含 candidates 数组和 summary 字符串:
assert:
– type: is–json
value:
schema:
type: object
required: ['candidates', 'summary']
properties:
candidates:
type: array
items:
type: object
required: ['name', 'experience_years']
properties:
name:
type: string
experience_years:
type: number
summary:
type: string
模型评分断言(让另一个 LLM 当裁判)
| llm-rubric | 用自然语言写评分标准,裁判模型给出 pass/score/reason |
示例:
assert:
– type: llm–rubric
value: '回答是否礼貌、专业,且没有事实错误?'
写好断言后,跑完 promptfoo eval,打开 promptfoo view,网页里会显示每条测试用例的输入、Agent 的输出、pass/fail 状态,以及整体通过率、延迟、断言数量统计。
重要:这一整套打分完全不依赖 OpenTelemetry——只要 call_api 能正常返回 output,分数就能算出来。
第五步(进阶):把智能体内部的工具调用变成可断言的证据
如果只看最终答案还不够,想验证"智能体内部到底调用了哪些工具、参数对不对、顺序对不对",就要用到 OpenTelemetry 追踪。
5.1 为什么需要它
智能体不像普通 LLM 一次输出就完事,它会经历"决策→调用工具→观察结果→再决策"的循环。两个智能体给出同样的最终答案,但一个调了 3 次工具、另一个调了 30 次,光看最终文本完全看不出这种差异。
trajectory:* 断言就是为解决这个问题而设计的——它们不只看最终输出,而是分析智能体执行的完整轨迹。
5.2 怎么开启追踪
开启追踪需要两步:配置和代码埋点。
第一步:在 YAML 中启用追踪
在 promptfooconfig.yaml 顶层加一段配置,让 Promptfoo 启动本地 OTLP 接收器:
tracing:
enabled: true
otlp:
http:
enabled: true
tracing.enabled: true 表示"要发送 OTLP 遥测数据",tracing.otlp.http.enabled: true 表示"启动内置的接收服务器"。
开启后,Promptfoo 会通过一个叫 traceparent 的字段(W3C 标准的追踪上下文格式),把当前测试用例的追踪上下文传给 Provider。
第二步:在 Provider 代码中接入 OpenTelemetry SDK
不同框架接入方式不同:
-
内置 Provider(如 openai:agents:*):直接在 YAML 里配置一行 tracing: true 就行,不用写代码,SDK 内部的工具调用、模型调用、交接事件都会自动转成 span 导出。
-
自定义 Python Provider(CrewAI、Google ADK、LangGraph 等):需要你在 call_api 里手动接入 Python 的 OpenTelemetry SDK,解析 Promptfoo 传来的 traceparent,起一个 span,再把框架自身产生的 span 作为子 span 导出。
Python 版 OpenAI Agents SDK 的官方示例演示了完整流程:Promptfoo 注入追踪上下文 → 示例代码解析并配置一个自定义的 TracingProcessor → 这个处理器把 SDK 内部产生的 span 转换成 OTLP JSON 格式 → Promptfoo 接收后就能在网页的 Trace Timeline 里看到。
如果跳过这一步导出,Promptfoo 完全看不到 SDK 内部的工具调用和交接过程,trajectory:* 断言就没有数据可用。
第三步:查看追踪结果
开启追踪后跑一遍评测、打开网页,在任意一条测试结果上点击放大镜图标,滚动到"Trace Timeline"区域,就能看到 Agent 内部执行的完整时间线。
5.3 针对轨迹写断言
一旦追踪数据能进来,就可以在 assert 列表里加上以下几类断言:
| trajectory:tool-used | Agent 是否调用了指定工具 | value 可以是字符串、字符串数组,或带 pattern/min/max 的对象 |
| trajectory:tool-args-match | 调用工具时传的参数是否符合预期 | 支持用 {{ order_id }} 这种模板变量动态匹配参数值 |
| trajectory:tool-sequence | 工具调用的先后顺序 | 默认 mode: in_order(中间可以有其他步骤),也可以设 mode: exact |
| trajectory:goal-success | 让裁判模型基于完整轨迹判断任务是否真正达成 | 能识别"嘴上说做了但实际没调用工具"的情况 |
| trajectory:step-count | 统计轨迹里某类步骤的数量 | 比如限制 Agent 最多执行 3 次命令 |
| skill-used | Agent 是否路由到了正确的"技能" | 目前对 Claude Agent SDK、OpenAI Codex SDK 等生效 |
| trace-span-count | 产生了多少个 span | 用于验证追踪链路本身是否健康 |
示例:验证 Agent 是否按正确顺序调用了"搜索"和"计算"工具,且搜索次数不超过 5 次:
assert:
– type: trajectory:tool–sequence
value:
tools: ['search', 'calculate']
mode: 'in_order'
– type: trajectory:step–count
value:
step_type: 'tool'
tool_name: 'search'
max: 5
速查表:从安装到进阶追踪的完整流程
| 1. 安装 | npm install -g promptfoo | 需要 Node.js ^20.20.0 或 >=22.22.0 |
| 2. 初始化 | promptfoo init | 生成示例配置文件,快速上手 |
| 3. 接入自己的 Agent | 写 call_api(prompt, options, context) 返回 {"output": …} | 任意 Python Agent 框架统一接口 |
| 4. 配置测试用例 | 在 YAML 中定义 tests 和 assert | 从 contains/is-json 等确定性断言开始 |
| 5. 运行评测 | promptfoo eval | 加上 –watch 可实现自动重跑 |
| 6. 查看报告 | promptfoo view | 浏览器中查看详细结果 |
| 7. 进阶:轨迹评估 | 开启 tracing + 接入 OTel SDK | 用 trajectory:* 断言校验中间步骤 |




