为什么需要专门的 LLM 测试框架

传统软件测试有断言、有 mock、有覆盖率,但到了 LLM 应用这里,这套体系全失效了。LLM 的输出是自然语言,非确定性、非结构化;同一个 prompt 在不同模型、不同温度参数下能给出完全不同的回答。你没法写 assert response == "Hello",也没法用 diff 工具比较两个对话。

Promptfoo 正是在这个断层里冒出来的工具——GitHub 23K+ Stars,已被 OpenAI 收购但仍保持 MIT 开源。它的核心理念很简单:把 LLM 测试变成声明式配置,通过预定义的评测指标(正确性、相关性、安全性等)对模型输出做自动评分,再嵌入 CI/CD 管道让每次 prompt 变更都可追溯。

本文从源码架构和配置设计两个层面,拆解 promptfoo 到底怎么工作的,以及如何把它接入你的测试流水线。

声明式测试配置:YAML 即测试用例

Promptfoo 最核心的设计决策是用 YAML 描述测试场景。不需要写 Python 或 TypeScript 代码,一个 promptfooconfig.yaml 就定义了全部测试逻辑。

# promptfooconfig.yaml
prompts:
  - "Translate this to French: {{input}}"
  - "Act as a French translator: {{input}}"
  - |
    System: You are a professional translator.
    User: {{input}}

providers:
  - id: openai:gpt-4o
    config:
      temperature: 0.3
  - id: anthropic:claude-sonnet-4
    config:
      temperature: 0.5
  - id: openai:gpt-4o-mini

tests:
  - vars:
      input: "Hello, how are you?"
    assert:
      - type: contains-any
        value: ["Bonjour", "Salut"]
      - type: llm-rubric
        value: "The response should be a polite French greeting"
      - type: cost
        threshold: 0.01
  - vars:
      input: "Where is the Eiffel Tower?"
    assert:
      - type: llm-rubric
        value: "Response should mention Paris and be factual"
      - type: latency
        threshold: 3000

三段 prompts × 三个 providers × 两个测试用例 = 9 个组合,promptfoo 自动遍历所有排列并评分。这比手写脚本调 API 然后肉眼检查结果要系统得多。

内置断言类型:从字符串匹配到 LLM 裁判

Promptfoo 提供了 30+ 种断言类型,按能力分层:

断言类型 作用 适用场景
contains / contains-any 关键词匹配 基础验证,如必须包含 "error"
javascript 自定义 JS 表达式 复杂逻辑断言
llm-rubric 用 LLM 评 LLM 语义正确性、语气、风格
cost token 成本阈值 控制推理成本
latency 响应延迟 性能回归检测
model-graded-closedqa 闭卷问答评分 事实性验证
factuality 事实一致性 防幻觉检测

其中最有价值的是 llm-rubric——用一个 LLM(默认 GPT-4o)作为裁判来评估另一个 LLM 的输出质量。其原理是自动构造一个评测 prompt,让裁判模型给出 1-5 分的评分和理由。

# 用 LLM 裁判评估回答质量
assert:
  - type: llm-rubric
    value: |
      Score the response on a scale of 1-5:
      - 5: Perfect, comprehensive, and well-structured
      - 3: Adequate but missing details
      - 1: Incorrect or irrelevant
      Provide a brief justification.
    threshold: 4  # 低于 4 分即失败

源码层面的评估引擎调度

这是 promptfoo 评估流程的核心代码路径。从 promptfoo eval 命令到最终报告输出,经历以下调度链:

CLI (eval.ts)
  → EvalConfigLoader (load config + resolve providers)
    → PromptRunner (iterate prompt × provider matrix)
      → TestRunner (execute tests + collect outputs)
        → AssertionEngine (run all assert types)
          → LLMRubricEvaluator (call judge model)
          → CostTracker (calculate token usage)
          → LatencyTimer (measure response time)
    → ResultAggregator (merge + rank)
      → ReportGenerator (HTML / JSON / CSV)

关键设计亮点在 AssertionEngine——它用责任链模式(Chain of Responsibility)串联不同类型的断言器,每个断言器只关心自己的匹配逻辑:

// 简化的断言引擎调度逻辑
interface Assertion {
  type: string;
  value: string | number;
  threshold?: number;
}

class AssertionEngine {
  private handlers: Map<string, AssertionHandler>;

  async evaluate(output: string, assertions: Assertion[], vars: Record<string, string>) {
    const results = [];
    for (const assert of assertions) {
      const handler = this.handlers.get(assert.type);
      if (!handler) throw new Error(`Unknown assertion type: ${assert.type}`);

      const start = performance.now();
      const pass = await handler.evaluate(output, assert, vars);
      results.push({
        type: assert.type,
        pass,
        latency: performance.now() - start,
      });
    }
    return results;
  }
}

每个断言类型的 handler 独立实现,互不干扰。llm-rubric handler 内部会构造一条评测 prompt 发给裁判模型,解析返回的评分 JSON:

class LLMRubricHandler implements AssertionHandler {
  async evaluate(output: string, assert: Assertion, vars: Record<string, string>) {
    const rubricPrompt = `
      You are evaluating an AI assistant's response.
      Rubric: ${assert.value}
      Response to evaluate: """${output}"""

      Respond with JSON: { "score": <number 1-5>, "reason": "<brief justification>" }
    `;

    const judgeResponse = await this.judgeModel.call(rubricPrompt);
    const { score } = JSON.parse(judgeResponse);
    return score >= (assert.threshold ?? 4);
  }
}

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/checkout@v4
      - uses: actions/setup-node@v4
        with:
          node-version: '22'

      - name: Install promptfoo
        run: npm install -g promptfoo

      - name: Run evaluations
        run: promptfoo eval
        env:
          OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }}
          ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }}

      - name: Compare with baseline
        run: promptfoo auto-update --threshold 0.9

      - name: Comment PR with results
        uses: promptfoo/actions@v1
        if: github.event_name == 'pull_request'
        with:
          promptfoo_results: 'promptfoo-output.json'

这里 auto-update 做的事情是:将本次评估结果与上一次基线对比,如果有指标下降超过阈值(10%),则标记为失败。防止 PR 合入后 prompt 质量反而变差。

在 Node.js 测试套件中调用 API

除了 CLI 模式,promptfoo 也提供编程接口:

import promptfoo from 'promptfoo';

const results = await promptfoo.evaluate({
  prompts: ['Summarize: {{text}} in one sentence'],
  providers: ['openai:gpt-4o'],
  tests: [
    {
      vars: { text: 'AI safety is important because...' },
      assert: [
        { type: 'llm-rubric', value: 'The summary should be under 20 words and capture the main point', threshold: 4 },
        { type: 'latency', threshold: 2000 },
      ],
    },
  ],
});

console.log(results.stats);
// { passRate: 0.85, totalTokens: 1523, avgLatencyMs: 1240 }

适合在 Jest / Vitest / Mocha 测试中混入 AI 评测。

红队测试(Red Teaming):自动发现 prompt 注入漏洞

Promptfoo 的 red team 模块是另一个亮点。它不是简单用一个 prompt 列表去测,而是基于模型的攻击向量生成——让攻击者模型自动生成对抗性 prompt:

promptfoo redteam init
promptfoo redteam run

这会生成数百条测试用例,覆盖: - Prompt 注入:尝试绕过系统指令 - 越狱攻击:DAN (Do Anything Now) 类 prompt - 角色扮演越权:冒充管理员 - 信息泄露:诱导输出 API key、数据库密码 - 拒绝服务:超长输入导致 OOM - 偏见与有害内容:生成歧视性、暴力内容

配置粒度很细:

# redteam.config.yaml
redteam:
  plugins:
    - id: prompt-injection
      config:
        strategies: ["direct", "indirect", "encoded"]
    - id: jailbreak
      config:
        severity: "high"
    - id: pii
      config:
        detection-types: ["email", "phone", "ssn", "api-key"]
    - id: harmful
      categories: ["hate-speech", "self-harm", "violence"]

  target:
    id: openai:gpt-4o
    config:
      systemPrompt: "You are a helpful customer support agent..."

输出是 HTML 报告,会列出每个攻击向量、目标响应和风险等级。这个功能在生产环境上线前做一次,能省很多安全审计的沟通成本。

RAG 评估:不只是测 LLM

RAG 应用的评测比纯 LLM 调用更复杂——牵涉检索质量和生成质量两个维度。Promptfoo 用 rag 类型的测试覆盖这个场景:

prompts:
  - "Answer based on the context: {{context}}\n\nQuestion: {{input}}"

providers:
  - id: openai:gpt-4o

tests:
  - vars:
      input: "What is the refund policy?"
      context: file://docs/refund-policy.md
    assert:
      - type: rag
        config:
          # 检索质量
          retrieval:
            - type: precision
              threshold: 0.8  # 检索结果相关度
            - type: recall
              threshold: 0.7  # 是否召回全部关键文档
          # 生成质量
          generation:
            - type: faithfulness
              threshold: 0.9  # 回答忠实于上下文
            - type: answer-relevance
              threshold: 0.8  # 回答与问题的相关性

这里 faithfulness 评测特别有意思——它不检查回答是否"正确"(因为没有 ground truth),而是检查回答中每个陈述是否都能在提供的 context 中找到依据。本质上是在做引用溯源检查,对抗 RAG 最常见的幻觉问题。

实际踩坑记录

过去半年在项目里深度使用了 promptfoo,几个值得注意的点:

1. LLM 裁判的一致性偏差

llm-rubric 时,裁判模型偶尔会"偏袒"同个家族的模型——GPT-4 给 GPT-4o 打分偏高,Claude 给 Claude 打分偏高。解法:裁判模型用第三方(如 DeepSeek V3),或对同家族做归一化校准。

2. 测试成本控制

全量跑几十个模型 × 几百条测试用例,token 消耗不小。建议分策略: - 每次 PR 跑最小集(1-2 个模型 × 核心断言) - 每天定时跑全量集 - 用 cost 断言设 token 上限,超出即失败

3. 随机性带来的假阴性

温度 > 0 时,同样的测试跑两次可能得到不同结果。建议: - 评测环境设 temperature: 0 - 对有随机性的测试跑 3 次取多数票 - 通过 --repeat 3 参数控制

4. 上下文窗口限制

RAG 评测中 context 很长时容易超 token 限制。promptfoo 默认不会自动截断,需要手动在 provider config 里设 max_tokensmax_payload_size

providers:
  - id: openai:gpt-4o
    config:
      max_tokens: 4096
      max_payload_size: 32000

对比其他 LLM 测试工具

工具 定位 配置方式 CI/CD 红队测试 开源
Promptfoo 全栈 LLM eval YAML MIT
LangSmith LangChain 生态 Python SDK 部分
DeepEval Python 原生 Pytest Apache 2.0
RAGAS RAG 专项 Python Apache 2.0
Guardrails 输出校验 XML/RAIL MIT

Promptfoo 的差异化优势是 YAML 声明式 + 红队测试 + CI/CD 原生集成,且语言无关——不绑定 Python 生态,Node.js、Go、Rust 项目都能用。

进阶方向

  • 自定义断言插件:通过 promptfoo plugins 接口写自定义评估器
  • 缓存策略:对确定性测试启用响应缓存,减少重复 API 调用
  • 漂移检测:持续跟踪同一 prompt 在模型更新后的输出变化
  • 多模态评测:promptfoo v1.x 已开始支持图像输入评测

Promptfoo 的核心价值不是替你做测试,而是把 LLM 测试从手工对话变成可编程、可复现、可量化的工程流程。对于正在把 LLM 推向生产的团队,这应该是工具箱里的第一件工具。

Logo

欢迎加入DeepSeek 技术社区。在这里,你可以找到志同道合的朋友,共同探索AI技术的奥秘。

更多推荐