
1. 为什么 LLM 应用需要一套专门的评估框架传统软件测试那套东西搬到 LLM 应用上基本全废。你没法写assert response Hello因为同一个 prompt 在不同模型、不同温度下能给出完全不同的回答你也没法用 diff 工具比较两段对话因为自然语言没有逐字节相等这回事。更麻烦的是LLM 的输出是概率性的今天跑通不代表明天跑通模型厂商一次静默更新就可能让你的线上效果悄悄退化。Promptfoo 就是在这个断层里长出来的工具。它的核心思路很直接把 LLM 测试变成声明式配置用 YAML 描述测试场景通过预定义的评测指标正确性、相关性、安全性、成本、延迟等对模型输出自动评分再嵌进 CI/CD 管道让每次 prompt 变更都可追溯、可回滚。GitHub 上 23K Stars被 OpenAI 收购后仍保持 MIT 开源这个体量本身就说明工程团队对可编程评估的需求有多真实。这篇文章面向的是需要在研发流程里落地 LLM 评估与红队测试的工程团队。我会从 YAML 配置骨架讲到 CI/CD 流水线接入给出可直接复制的promptfooconfig.yaml和 GitHub Actions 片段并说明本地运行与流水线验证的具体动作。读完你应该能把评估从手动跑脚本看结果升级成每次 PR 自动跑、退化就拦截的持续集成环节。2. 前置准备模型接入与 API Key 配置Promptfoo 本身只是个调度器真正干活的是它背后调用的模型。所以第一步是把模型接入配好。这里有两种常见路径直接用官方 API Key或者通过兼容 OpenAI 协议的中转服务统一管理多模型。如果你团队里同时用 GPT、Claude、DeepSeek 等多个模型逐个申请 Key、逐个配环境变量会很乱。我一般会用一个兼容 OpenAI 协议的网关来统一出口比如 TaoToken 这类服务它提供标准的/v1/chat/completions接口Promptfoo 的openai:provider 只要改baseUrl就能指向它。先拿到 API Key访问 https://taotoken.net/api-keys 创建然后配置环境变量export OPENAI_API_KEYsk-你的key export OPENAI_BASE_URLhttps://taotoken.net/api/v1如果你用的是 Promptfoo 的openai:provider可以在 provider 配置里显式指定baseUrl这样就不用依赖全局环境变量多项目隔离更干净。模型对话调试可以直接在 https://taotoken.net/model-chat 里先手动验证一遍确认 Key 和模型名对得上再去写 YAML能省掉很多配置写错但报错看不懂的时间。注意Promptfoo 的 provider id 格式是openai:模型名走自定义 baseUrl 时模型名要和你网关支持的名称一致比如openai:gpt-4o、openai:claude-sonnet-4。名字写错不会在加载配置时报错而是在实际请求时返回 404排查起来比较绕。3. 可复制的 promptfooconfig.yaml 骨架Promptfoo 最核心的设计决策就是YAML 即测试用例。一个promptfooconfig.yaml定义了 prompts、providers、tests 三大部分Promptfoo 会自动遍历prompts × providers × tests的所有组合并评分。下面是一份可以直接跑的骨架# promptfooconfig.yaml description: 客服问答质量评估 prompts: - file://prompts/support.txt - 你是一个专业客服请回答{{question}} providers: - id: openai:gpt-4o config: temperature: 0 max_tokens: 1024 baseUrl: https://taotoken.net/api/v1 - id: openai:claude-sonnet-4 config: temperature: 0 baseUrl: https://taotoken.net/api/v1 defaultTest: options: provider: openai:gpt-4o assert: - type: latency threshold: 5000 tests: - vars: question: 退款政策是什么 assert: - type: contains-any value: [7天, 退款, 无理由] - type: llm-rubric value: 回答应准确说明退款时限和条件语气礼貌 threshold: 4 - type: cost threshold: 0.02 - vars: question: 你们的客服电话是多少 assert: - type: llm-rubric value: 回答应提供联系方式或引导用户到正确渠道不得编造号码 threshold: 4几个关键点值得展开。defaultTest里的options.provider指定了 LLM 裁判用哪个模型这个很重要——裁判模型和被测模型最好别是同一家族否则会有偏袒问题后面排障章节会细说。assert里的llm-rubric是最有价值的断言类型它用一个 LLM 当裁判给另一个 LLM 的输出打分threshold: 4表示低于 4 分即失败。cost和latency这两个断言容易被忽略但在生产环境里它们是硬约束。一个回答再准确如果单次成本 0.5 元、延迟 8 秒业务上也用不了。把它们写进断言等于给评估加上了工程底线。4. 断言类型与评估引擎的调度逻辑Promptfoo 内置 30 种断言类型按能力大致分三层。基础层是字符串匹配比如contains、contains-any、equals适合必须包含某个关键词这种硬性要求。中间层是javascript允许你写自定义 JS 表达式做复杂逻辑判断。上层是llm-rubric、factuality、model-graded-closedqa这类语义级评估用模型来判断模型。断言类型作用适用场景contains / contains-any关键词匹配基础验证如必须包含退款javascript自定义 JS 表达式复杂逻辑断言llm-rubric用 LLM 评 LLM语义正确性、语气、风格factuality事实一致性防幻觉检测costtoken 成本阈值控制推理成本latency响应延迟性能回归检测从源码调度角度看promptfoo eval命令的执行链路大致是CLI 加载配置 → EvalConfigLoader 解析 providers → PromptRunner 遍历 prompt × provider 矩阵 → TestRunner 执行测试 → AssertionEngine 跑所有断言 → LLMRubricEvaluator 调裁判模型 → CostTracker 和 LatencyTimer 收集指标 → ResultAggregator 汇总 → ReportGenerator 输出 HTML/JSON。关键设计在 AssertionEngine它用责任链模式串联不同类型的断言器每个 handler 只关心自己的匹配逻辑。llm-rubric的 handler 内部会构造一条评测 prompt 发给裁判模型解析返回的评分 JSON再和 threshold 比较。这种设计的好处是新增断言类型只需注册一个 handler不用改调度逻辑。5. 接入 CI/CD让 prompt 变更不再裸奔上线Promptfoo 最实用的场景就是嵌进 CI/CD。每次 PR 修改 prompt 或换模型时自动跑评估对比基线阻止退化。下面是 GitHub Actions 配置# .github/workflows/prompt-eval.yml name: Prompt Evaluation on: pull_request: paths: - prompts/** - promptfooconfig.yaml jobs: evaluate: runs-on: ubuntu-latest steps: - uses: actions/checkoutv4 - uses: actions/setup-nodev4 with: node-version: 22 - name: Install promptfoo run: npm install -g promptfoo - name: Run evaluations run: promptfoo eval --output promptfoo-output.json env: OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }} OPENAI_BASE_URL: ${{ secrets.OPENAI_BASE_URL }} - name: Compare with baseline run: promptfoo eval --baseline baseline.json --threshold 0.9 - name: Upload report uses: actions/upload-artifactv4 with: name: promptfoo-report path: promptfoo-output.json这里--baseline加--threshold 0.9的组合是关键把本次结果和上一次基线对比如果通过率下降超过 10%命令以非零退出码结束CI 直接标红PR 无法合并。这就把prompt 质量变成了和单元测试一样的硬门槛。如果你团队用 Coding Plan 做长期编码和 Agent 开发可以把评估流水线挂到同一个仓库里让 prompt 变更和代码变更走同一套 review 流程。具体接入方式参考 https://taotoken.net/coding-plan。6. 本地运行与流水线验证的具体动作配置写好后先在本地跑通再上 CI能省掉大量在流水线里盲调的时间。完整动作如下第一步安装并初始化npm install -g promptfoo promptfoo init第二步本地运行评估promptfoo eval运行结束后会输出一个汇总表显示每个 prompt × provider 组合的通过率、平均延迟、token 消耗。想看详细报告就加--outputpromptfoo eval --output report.html promptfoo viewpromptfoo view会启动一个本地 Web 界面把每个测试用例的输入、输出、断言结果、裁判评分理由都列出来。排查失败用例时这个界面比看终端输出高效得多。第三步验证 CI 行为。在本地模拟基线对比promptfoo eval --output baseline.json # 修改 prompt 后 promptfoo eval --baseline baseline.json --threshold 0.9如果修改导致通过率下降超过阈值命令会返回非零退出码。你可以在本地先确认这个行为符合预期再推到 CI。第四步接入红队测试。Promptfoo 的 red team 模块会基于攻击向量自动生成对抗性 promptpromptfoo redteam init promptfoo redteam run它会覆盖 prompt 注入、越狱、角色扮演越权、信息泄露、偏见与有害内容等类别输出 HTML 报告列出每个攻击向量、目标响应和风险等级。上线前跑一次能省掉很多安全审计的沟通成本。7. 本篇常见错误排查错误一Unknown assertion type通常是 YAML 缩进错了或者断言类型名拼写有误。Promptfoo 对 YAML 缩进敏感assert下的每一项必须对齐。建议用promptfoo validate先校验配置。错误二模型请求 404 或 401走自定义 baseUrl 时最常见的是模型名和网关支持的名字不一致或者baseUrl末尾多了/少了/v1。Promptfoo 的openai:provider 期望的 baseUrl 是https://taotoken.net/api/v1不要写成https://taotoken.net/api。401 则检查 API Key 是否配到了正确的环境变量。错误三LLM 裁判评分不稳定温度大于 0 时同样的测试跑两次可能得到不同结果出现假阴性。解法是把评测环境的temperature设为 0对随机性强的测试用--repeat 3跑三次取多数票。错误四裁判模型偏袒同家族模型GPT-4 给 GPT-4o 打分偏高Claude 给 Claude 打分偏高这是实测下来很明显的偏差。解法是裁判模型用第三方比如用 DeepSeek 或另一个家族的模型当裁判或者对同家族评分做归一化校准。错误五RAG 评测超 token 限制context 很长时容易超窗口。Promptfoo 默认不自动截断需要在 provider config 里显式设max_tokens和max_payload_sizeproviders: - id: openai:gpt-4o config: max_tokens: 4096 max_payload_size: 32000错误六CI 里跑全量太慢太贵几十个模型 × 几百条用例token 消耗不小。建议分策略每次 PR 只跑最小集1-2 个模型 × 核心断言每天定时跑全量集并用cost断言设 token 上限超出即失败。8. 把评估变成工程流程的下一步Promptfoo 的价值不在于替你做测试而在于把 LLM 测试从手工对话看感觉变成可编程、可复现、可量化的工程流程。YAML 声明式配置让它语言无关Node.js、Go、Rust 项目都能用红队测试模块让安全验证自动化CI/CD 原生集成让质量门槛可执行。如果你准备继续深入几个方向值得投入自定义断言插件通过 plugins 接口写团队专属的评估器响应缓存对确定性测试启用缓存减少重复 API 调用漂移检测持续跟踪同一 prompt 在模型更新后的输出变化。接入文档和 API 细节可以在 https://taotoken.net/doc 找到先把模型接入和 Key 管理理顺再往上叠评估流水线落地会顺很多。