LLM 应用没法用 assert "success" in resp.text 这种断言收口:输出是非确定性的,同一个 prompt 换一次采样就变,关键词断言要么天天误报要么形同虚设。promptfoo(GitHub 24k★,MIT,当前 v0.122)是目前把 LLM 测试工程化做得最完整的开源方案:一个 YAML 同时定义 prompts、providers、tests,断言按确定性→结构→语义→指标四层铺开,redteam 子命令直接生成对抗样本,输出 JUnit 报告、失败时非零退出码,原生接 CI。下面从一个客服问答 bot 的测试配置出发,拆断言分层、矩阵对比、红队扫描和 CI 接入的完整链路,配置全部可复制。
一、最小配置:一个 YAML 钉死"测什么、谁跑、怎么判"
promptfoo 的核心模型是 promptfooconfig.yaml 三段式:prompts(被测对象)、providers(执行推理的模型/接口)、tests(断言集)。
# promptfooconfig.yaml
prompts:
- "你是电商客服助手。用户问题:{{question}}\n先判断意图,再给出可操作的答复。"
providers:
- openai:gpt-4o-mini
tests:
- vars:
question: "你们的退款政策是什么?"
assert:
- type: contains
value: "退款"
- type: llm-rubric
value: "回答包含明确的退款条件和时限,语气专业,不编造政策"
跑 promptfoo eval,结果落成终端表格;promptfoo view 起本地 Web UI 逐条看。任一条断言失败,进程退出码非 0——这是后面接 CI 的地基。注意 {{question}} 是变量插值,测试数据与 prompt 模板解耦,200 条问题就是 200 个测试用例。
二、断言四层:从"碰运气"到"可量化"
promptfoo 的 assert 类型按判定成本与稳定性可以分四层,选型原则只有一条:能用确定性断言,就别请 LLM 当裁判。
| 层级 | 代表断言 | 判定方式 | 单次成本 | 稳定性 | 适用场景 |
|---|---|---|---|---|---|
| 确定性 | contains / equals / regex / is-json | 字符串与结构匹配 | 0 | 100% | 关键词、JSON 结构、枚举值 |
| 结构 | javascript / python | 自定义脚本跑输出 | 0 | 100% | 跨字段校验、复杂业务逻辑 |
| 语义 | similarity / llm-rubric | 向量相似度 / 裁判模型打分 | 中 | 中 | 语气、完整性、指令遵循 |
| 指标 | cost / latency | 采样统计 | 低 | 中 | 性能与成本回归门禁 |
组合用法的典型样例——python 断言做结构化校验,rubric 只留给语义判据:
tests:
- vars:
question: "订单一直显示处理中,怎么办?"
assert:
- type: python
value: |
import json
data = json.loads(output)
assert data["intent"] == "order_status", f"意图识别错误: {data['intent']}"
assert len(data["steps"]) >= 2, "排查步骤不足"
- type: llm-rubric
value: "给出可操作的排查步骤,且不索要密码等敏感信息"
threshold: 0.8
llm-rubric 不是魔法,本质是"另一个 LLM 当裁判"。两个关键坑:裁判模型不能与被测模型同源——让弱模型判自己等于考生自己改卷,生产配置里用 grader 单独指定强裁判;rubric 必须写可证伪的判据,"回答质量高"这种话裁判没法稳定打分,"包含明确的退款时限"才能。threshold: 0.8 也不是拍脑袋定的,先跑 50 条看分数分布再定阈值。
三、矩阵测试:prompt 版本和模型一起比
改 prompt 是迭代最频繁的操作,凭感觉改完上线,好坏全靠体感。prompts 段写多个变体、providers 写多个模型、测试集共享,跑完自动出交叉对比表:同一个问题,哪个 prompt 在哪个模型上过了哪些断言,一目了然。
prompts:
- file://prompts/base.txt
- file://prompts/with_fewshot.txt # 加了 3 条 few-shot 的版本
providers:
- openai:gpt-4o-mini
- ollama:qwen2.5:7b # 本地小模型做成本对照
tests: file://tests/orders.yaml # 测试集与配置分离,多场景复用
实测价值在回归可视化:promptfoo view 里直接对比 base 与 few-shot 两个版本在 200 条测试上的断言通过率。prompt 迭代从"感觉变好了"变成"通过率从 82% 涨到 91%,但意图识别子集掉了 3 个点"——后者才是可评审、可回滚的结论。
四、红队测试:redteam 一键扫注入与越狱
功能断言管"答得对不对",红队管"不该答的它答没答"。promptfoo redteam init 生成红队配置,plugins 按攻击面选,框架内置几十种插件:prompt-injection(提示词注入)、jailbreak(越狱)、harmful(有害内容)、pii(隐私泄露)、sql-injection、ssrf、shell-injection、system-prompt-override、excessive-agency(过度自主行动)等,对齐 OWASP LLM Top 10 与 MITRE ATLAS。
# redteam.yaml
prompts: [file://prompts/base.txt]
providers: [openai:gpt-4o-mini]
redteam:
plugins:
- prompt-injection
- jailbreak
- harmful
- pii
- sql-injection
numTests: 25
purpose: "电商客服助手,只回答售前售后问题,拒绝越权操作"
生成的是可审计的攻击样本集(落在配置同级目录,每条攻击带策略标注),跑完逐条看拦截结果。修完漏洞把这些样本并入日常回归集——红队样本就是最好的对抗回归用例,比手工编的"刁钻问题"覆盖面大得多。默认红队模型是 gpt-5.5 级别的强模型(源码里 REDTEAM_MODEL 可覆盖),攻击生成质量直接决定扫描有效性,本地小模型生成攻击样本的效果会明显打折。
五、接 CI:JUnit 报告 + 非零退出码
promptfoo 原生输出 junit,GitHub Actions 里一个 step 完事:
- uses: promptfoo/promptfoo-action@v1
with:
config: ./promptfooconfig.yaml
output: junit
失败即非零退出 → PR 被 block。这个 action 还支持 workflow_dispatch 的 files/base 输入做增量测试:只跑本次 PR 改动涉及的 prompt 文件对应的测试,全量回归留到 nightly。三个 CI 注意事项:
- 缓存策略:本地 eval 默认开缓存(相同输入直接命中),CI 上要显式管理缓存路径,否则排查"昨天过今天挂"时缓存会误导结论;改过 prompt 变量名或顺序,缓存 key 就变,命中率幻觉要警惕
- 并发与成本:矩阵规模 = prompts × providers × tests,200 条 × 2 prompt × 2 模型 = 800 次推理,rubric 的裁判调用再翻一倍。用
--max-concurrency限流,测试集按"PR 增量 + nightly 全量"分流 - 抖动处理:非确定性必然带来偶发失败。关键用例设
n: 3多次采样、按多数通过判定,别让 flaky 门禁把团队逼到"绕过 CI"
六、踩坑清单(按代价排序)
- 裁判与被测同源:模型自评偏好严重,裁判独立配置 + 具体 rubric
- 阈值拍脑袋:先跑样本看分数分布再定 threshold,一上来 0.9 只会得到天天红的门禁
- 只测生成不测链路:RAG 应用把 retriever 用 python provider 包一层,先断 retrieval 再断 generation,问题定位快一倍;redteam 里 rag-poisoning、rag-source-attribution 这类插件就是为检索链路准备的
- 红队报告没人看:把跑出来的真实攻击样本转成 assert 用例进回归集,报告变门禁
- 矩阵失控:不加节制地堆 prompt 变体 × 模型 × 测试数,成本线性爆炸,先小矩阵验证再放大
一组实测量级参考(200 条测试、2 模型、并发 8,数值取决于模型与网络):
| 方案 | 耗时 | 成本量级 | 能拦住什么 |
|---|---|---|---|
| 仅确定性断言 | 秒级 | ~0 | 关键词 / 结构回归 |
| + llm-rubric | 分钟级 | 低 | 语义质量回归 |
| + redteam 插件样本 | 十分钟级 | 中 | 注入 / 越狱 / 隐私泄露 |
| 全量接 PR 门禁 | 十分钟级 | 中 | 以上全部 |
收尾
promptfoo 把 LLM 测试从"人工抽查"变成"可配置、可量化、可进 CI"的工程件,核心心法就一条:断言分层——确定性断言打底,LLM 裁判只留给语义判据;红队样本转回归用例;矩阵对比让 prompt 迭代有数据可依。
进阶方向:用 JS/Python 写自定义 provider 封装内部模型网关;agent 场景用 tool-call 断言校验工具调用序列而非只看最终文本;测试集本身纳入版本管理,prompt 变更与测试变更同 PR 评审——把"测什么"和"怎么实现"一起纳入代码评审,这才是 LLM 测试工程化的终点。
转载自 CSDN-专业IT技术社区
原文链接:https://blog.csdn.net/m0_58868237/article/details/163625769



