提示评测自动化:把 Prompt 质量变成可回归测试
很多人调 Prompt 的体验是这样的:改一版、肉眼看几个例子觉得「好多了」,过两周再改,又觉得「好像不如上次」。问题不在模型,而在评测方式——我们一直用「人眼对比」代替「可复现的测试」。本文把 Prompt 工程里的评测自动化讲清楚:如何设计评测集、如何用代码跑断言、如何用 LLM 当裁判、如何把分数接进 CI 做回归,最后介绍几个现成框架。
为什么 Prompt 也要有评测
Prompt 本质是一段「代码」:它控制模型的输入、约束输出格式、定义角色与边界。既然代码要写单元测试,Prompt 也该有对应的测试。人眼比对至少有三个硬伤:
- 不可复现:同一版 Prompt,今天和一周后看同一个输出,你的判断标准已经漂移了。没有固定用例,就没有「这次比上次好」的客观依据。
- 版本漂移:Prompt 被多人改过几次后,你根本不知道哪次改动让某类输入变差了。没有基线,回滚都无从回滚。
- 团队协作需要基线:当三个人都在改同一个系统 Prompt,争论「哪种写法更好」没有意义,需要的是「在 fixed 评测集上,谁的版本综合分更高」。
把评测做成可回归的测试后,每次改动都能跑同一套用例,输出一张分数表。质量从「感觉还行」变成「通过率 92% 升到 95%」。
评测集设计:golden set 与评测维度
固定用例集(golden set)
评测集是若干条「输入加期望」的固定样本。每条样本至少包含:
input:喂给模型的输入(用户问题、上下文、变量等)。expect:对输出的约束或参考答案。约束可以是「必须包含某关键字」「必须能解析为合法 JSON」「应该与参考答案语义相近」。
建议把评测集单独存成文件(JSON 或 YAML),和程序代码一起进版本库,这样改 Prompt 时评测集不动,保证可比性。
[
{
"id": "case_001",
"input": "把『今天天气不错』翻译成英文",
"expect": {
"contains": ["The weather"],
"format": "plain_text"
}
},
{
"id": "case_002",
"input": "提取订单信息:用户张三,金额 199 元,地址北京",
"expect": {
"json_schema": {
"type": "object",
"required": ["name", "amount", "city"],
"properties": {
"name": {"type": "string"},
"amount": {"type": "number"},
"city": {"type": "string"}
}
}
}
}
]
四个常用评测维度
- 相关性:输出是否真正回答了输入的问题,没有跑题。
- 格式合规:是否如期返回 JSON、是否包含必要字段、是否遵守长度限制。
- 安全性:是否拒绝违规请求、是否产生有害内容、是否存在提示注入泄漏。
- 成本:单次调用的 token 消耗与延迟(延迟通常不作为通过门槛,但可作为监控指标)。
前三个维度通常作为「是否通过」的硬性断言,第四个维度作为趋势监控。
用代码跑评测:pytest 化与 LLM 裁判
思路是把每条用例写成一个 pytest 测试用例,在用例里调用模型,再对返回文本做断言。这样你能直接复用 pytest 的收集、并行、失败报告与 CI 集成能力。
用断言约束输出
下面这个例子覆盖三种常见约束:关键字包含、正则匹配、JSON Schema 校验。
import json
import re
import pytest
from jsonschema import validate
from openai import OpenAI
client = OpenAI() # 读取环境变量 OPENAI_API_KEY;兼容端点见后文
def call_model(prompt: str, user_input: str):
resp = client.chat.completions.create(
model="gpt-4o-mini",
messages=[
{"role": "system", "prompt": prompt},
{"role": "user", "content": user_input},
],
temperature=0,
)
return resp.choices[0].message.content
def test_contains_keyword():
out = call_model("只翻译下面这句话,不要解释。", "今天天气不错")
assert "The weather" in out
def test_regex_match():
out = call_model("只输出一个邮箱地址,不要其他内容。", "我的邮箱是 a@b.com")
assert re.search(r"[\w.]+@[\w.]+", out) is not None
def test_json_schema():
schema = {
"type": "object",
"required": ["name", "amount", "city"],
"properties": {
"name": {"type": "string"},
"amount": {"type": "number"},
"city": {"type": "string"},
},
}
out = call_model(
"提取订单字段并只返回 JSON。",
"用户张三,金额 199 元,地址北京",
)
data = json.loads(out) # 若模型返回带 json 代码围栏,需先剥离
validate(instance=data, schema=schema)
注意:模型常把 JSON 包在 Markdown 代码围栏里返回。生产断言里要先做「去围栏」清洗,再 json.loads,否则会因为格式噪声误判。
用 LLM-as-Judge 打分
有些质量(相关性、安全性、语气)无法用正则或 Schema 表达,适合交给另一个模型当裁判。这是有学术依据的:Zheng 等的论文《Judging LLM-as-a-Judge with MT-Bench and Chatbot Arena》(arXiv 2306.05685,NeurIPS 2023)指出,像 GPT-4 这样的强模型做裁判,与人类偏好的一致率能超过 80%,和人类之间的互评一致率处于同一水平。该论文也提醒裁判存在位置偏差、冗长偏差与自增强偏差,因此裁判 Prompt 应当要求「先给理由、再给分数」,并尽量固定裁判模型。
下面给出一个可运行示例,调用 OpenAI 或兼容 API 做裁判,对「输出相关性」打出 1 到 5 分。
import os
from openai import OpenAI
"兼容端点示例:把 base_url 指向你自己的网关即可,例如本地 Ollama、vLLM 等"
client = OpenAI(
api_key=os.environ.get("OPENAI_API_KEY", "sk-待核实"),
base_url=os.environ.get("OPENAI_BASE_URL", "https://api.openai.com/v1"),
)
def llm_judge(question: str, answer: str, rubric: str):
judge_prompt = f"""你是评分裁判。请先写一句评分理由,再在最后一行只输出 1 到 5 的整数分数。
评分标准:{rubric}
用户问题:{question}
模型回答:{answer}
"""
resp = client.chat.completions.create(
model=os.environ.get("JUDGE_MODEL", "gpt-4o"),
messages=[{"role": "user", "content": judge_prompt}],
temperature=0,
)
text = resp.choices[0].message.content
# 取最后一行里的首个数字作为分数
last_line = [line for line in text.strip().splitlines() if line.strip()][-1]
score = int(re.search(r"[1-5]", last_line).group())
return score
def test_relevance_by_judge():
question = "用一句话解释什么是向量数据库"
answer = call_model("用一句话解释什么是向量数据库。", question)
score = llm_judge(
question,
answer,
"答案是否准确、切题,且在一句话内说清核心概念",
)
assert score >= 4
用法提示:裁判模型建议用比被评模型更强的模型;对关键场景可做「两个裁判互评取一致」,缓解单一裁判的位置偏差。裁判调用的单价(待核实,取决于所用模型与调用量)。
评分指标
把单条断言汇总成可比较的指标,常见有五类:
- 精确匹配(Exact Match):输出与标准答案逐字符一致。适合分类、固定模板场景,但自然语言任务太严,通常不单独使用。
- 包含率(Contains / Recall):输出是否覆盖期望关键字或关键事实。实现简单,适合格式与要点检查。
- 结构化校验:用 JSON Schema、正则或类型解析校验输出结构。能稳定拦截「格式崩了」这类硬错误。
- 语义相似(Semantic Similarity):把输出与参考答案各自 embedding 后算余弦相似度,缓解表述差异。下面的示例用 embedding 接口计算。
import os
from openai import OpenAI
client = OpenAI(api_key=os.environ.get("OPENAI_API_KEY", "sk-待核实"))
def cosine(a, b):
dot = sum(x * y for x, y in zip(a, b))
na = sum(x * x for x in a) ** 0.5
nb = sum(y * y for y in b) ** 0.5
return dot / (na * nb)
def semantic_similarity(text_a: str, text_b: str):
emb = []
for t in (text_a, text_b):
r = client.embeddings.create(
model=os.environ.get("EMBED_MODEL", "text-embedding-3-small"),
input=t,
)
emb.append(r.data[0].embedding)
return cosine(emb[0], emb[1])
def test_semantic_close():
ref = "向量数据库用于存储高维向量的嵌入表示,支持按相似度检索。"
got = call_model("一句话说明向量数据库用来干什么。", "向量数据库是做什么的")
assert semantic_similarity(ref, got) >= 0.82 # 阈值按业务自定,待核实
- 裁判一致性(Judge Agreement):多个裁判或裁判与人类标注之间的吻合度,可用一致率、Cohen’s kappa 等度量。它衡量的是「评测本身可不可信」,建议在正式把 LLM 裁判纳入门槛前先人工抽检一批,确认一致率达标。
回归与 CI:每次改 Prompt 跑全套用例
把评测接进 CI,核心是「修改 Prompt 即触发测试,未达门槛则失败」。下面是一段 GitHub Actions 配置示例,它在 push 或针对 prompt 文件的改动时运行 pytest。
name: prompt-eval
on:
push:
paths:
- "prompts/**"
- "tests/test_prompt_eval.py"
pull_request:
paths:
- "prompts/**"
jobs:
eval:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-python@v5
with:
python-version: "3.11"
- run: pip install -r requirements.txt
- name: Run prompt evaluation
env:
OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }}
JUDGE_MODEL: gpt-4o
run: pytest tests/test_prompt_eval.py --junitxml=report.xml
- name: Upload report
if: always()
uses: actions/upload-artifact@v4
with:
name: eval-report
path: report.xml
门槛策略建议分层:格式类与安全性断言「零容忍」,必须全过;相关性、语义相似这类软指标设最低通过率(例如 90%),低于门槛就让流水线失败。这样你得到的是「可回归」而不是「可观赏」的评测。
评测框架简介
自己写 pytest 适合轻量场景,团队规模化后可以借助成熟框架:
- promptfoo:开源(MIT 许可,仓库声明已并入 OpenAI)的 LLM 评测与红队工具,用声明式
promptfooconfig.yaml定义 Prompt、模型与断言,支持 GitHub Actions 等 CI 集成,本地运行、Prompt 不出本机。安装为npm install -g promptfoo,常用命令promptfoo init、promptfoo eval、promptfoo view,也提供pip install promptfoo与brew install promptfoo。支持 OpenAI、Anthropic、Azure、Bedrock、Ollama 等大量 provider(已核验于官方仓库)。 - LangSmith datasets:LangChain 生态的评测平台,可把数据集在线管理、跑在线评测并对比不同 Prompt 版本;在线服务,涉及用量计费(定价待核实)。
- Ragas:专注 LLM 应用(尤其 RAG 管线)的评测库,提供忠实度(Faithfulness)、回答相关性(Response Relevancy)、上下文精度(Context Precision)、上下文召回(Context Recall)、上下文实体召回(Context Entities Recall)等指标,也覆盖 Agent、SQL、摘要等场景;以 experiments 为先,可与 LangChain、LlamaIndex 集成(已核验于官方文档)。
选型参考:纯 Prompt 回归、想要本地与 CI 友好,先看 promptfoo;做 RAG 质量,看 Ragas;已经在用 LangChain 并想要托管数据集与版本对比,看 LangSmith。
小结
- Prompt 也是「代码」,应当有可复现、可回归的测试,而不是靠人眼比对。
- 评测集(golden set)要固定进版本库,覆盖相关性、格式合规、安全性、成本四个维度。
- 用 pytest 把每条用例写成断言:关键字包含、正则、JSON Schema 校验负责「硬约束」。
- 相关性、语气等软质量用 LLM-as-Judge 打分,但要留意位置/冗长/自增强偏差,并先验证裁判与人类的一致率。
- 把 pytest 接进 CI,格式与安全零容忍、软指标设最低通过率,未达门槛即失败,质量才算「可回归」。
- 规模化可引入 promptfoo、Ragas、LangSmith 等框架,按场景选型。
参考与延伸阅读
- promptfoo 官方仓库与文档(GitHub: promptfoo/promptfoo;官网 promptfoo.dev):CLI/库、MIT 开源、已并入 OpenAI、provider 与 CI 集成说明 —— 已核验。
- Ragas 官方文档(docs.ragas.io):RAG 与 LLM 应用评测指标、experiments 工作流、框架集成 —— 已核验。
- Zheng 等《Judging LLM-as-a-Judge with MT-Bench and Chatbot Arena》,arXiv:2306.05685,NeurIPS 2023:LLM 裁判与人类偏好一致率超 80%,及位置/冗长/自增强偏差的讨论 —— 已核验。
- LangSmith datasets(docs.smith.langchain.com):在线评测与数据集管理 —— 官网存在,具体定价待核实。
- 文中涉及的模型名称、embedding 相似度阈值(如 0.82)、裁判最低分门槛(如 4 分)均按示例给出,实际数值与 API 单价待核实。