把 AI Evals 放进 CI:从人工感觉到回归门禁
一次让我栽跟头的上线
去年秋天,我把一个 Prompt 的措辞改了三处——把「请你」换成了「请」,删掉一句多余的开场白,觉得这样更干净。改完跑了几轮手工测试,核心场景都没问题,直接合进主分支。第二天早上,客服同事找到我:有一批用户的退款请求,AI 不再主动确认订单号了,直接跳到「已为您处理」。问题不在主路径上,藏在一个我之前手工测试没覆盖到的分支条件里。Prompt 措辞微调,模型的输出分布跟着偏了。
这件事让我彻底放弃了一种幻想:「改动小,看一眼就行。」LLM 不是传统函数,它不会因为一行 typo 报 500,它会悄悄漂移,而且漂移的样子看起来「也说得通」。传统单元测试抓不住这种退化,人工抽查覆盖面又太窄。我们需要一套能跟着每次 PR 自动跑的质量门禁——这就是 AI Evals 进 CI 要解决的事情。
为什么 AI 输出需要不一样的测试策略
传统软件的输出是确定的:同样的输入,同样的代码路径,同样的结果。单元测试写一次,能稳定跑很久。LLM 应用打破了这个假设,至少引入了三层不确定性:
第一层是采样随机性。即使 temperature 设成 0,不同模型版本、不同时间点的推理结果仍可能不同。模型升级后,同一个 Prompt 的输出分布会发生偏移,这不是 bug,是模型本身的统计特性。
第二层是评估模糊性。传统测试的 assert 是精确匹配——字符串相等、数值大于某个阈值。AI 输出的「好坏」往往是语义层面的:回答是否切题、事实是否准确、语气是否合适。这些判断没有唯一正确答案。
第三层是变更连锁反应。Prompt 改动、模型版本升级、RAG 检索策略调整、工具调用逻辑变化,任何一个变量的修改都可能让原本正常的输出发生退化,而且退化通常不是整体变差,而是某些特定场景悄悄失灵。
这意味着 AI 质量保障不能只靠一组固定的 assert。它需要一套分层评估机制:先用确定性检查过滤掉结构错误,再用启发式方法评估语义质量,最后用 LLM-as-a-judge 甚至人工复核处理主观维度。把这套机制嵌进 CI,每次变更都自动跑一遍,退化的发现点就能从「用户投诉」前移到「PR 评审阶段」。
三层评估架构:从快到慢、从便宜到贵
把 eval 放进 CI 的第一个挑战是成本和速度。如果对每条样例都调用最强模型做 LLM-as-a-judge 评分,一个 200 条的测试集可能跑一次就要几十美元和半小时。CI 等不起,账单也扛不住。
务实的做法是分层过滤,让大多数样例在低成本层就能通过或失败,只有边界情况才进入高成本评估。
┌─────────────────────────────────────────────────────┐
│ Layer 1:确定性检查(毫秒级,零 API 成本) │
│ · JSON Schema 校验 │
│ · 正则匹配关键字段 │
│ · 输出长度约束 │
│ · 禁止词/敏感词过滤 │
├─────────────────────────────────────────────────────┤
│ Layer 2:启发式评分(百毫秒级,低成本 embedding) │
│ · 语义相似度(embedding cosine similarity) │
│ · ROUGE / BLEU 等传统 NLP 指标 │
│ · 检索召回率(RAG 场景) │
├─────────────────────────────────────────────────────┤
│ Layer 3:LLM-as-a-judge(秒级,较高成本) │
│ · 用独立模型按评分 rubric 打分 │
│ · temperature 设为 0,保证评分可复现 │
│ · 多维度:准确性、完整性、语气、安全性 │
├─────────────────────────────────────────────────────┤
│ Layer 4:人工复核(分钟到天级,最高成本) │
│ · 高风险场景的最终兜底 │
│ · LLM-as-a-judge 评分不确定时的仲裁 │
│ · 新增失败样例回流到 golden dataset │
└─────────────────────────────────────────────────────┘每层的职责不同。Layer 1 负责拦住「硬伤」——JSON 解析失败、输出为空、包含禁用词,这类问题不需要任何模型判断,毫秒级就能得出结论。Layer 2 用 embedding 或传统 NLP 指标做语义级别的粗筛,成本只有 LLM 调用的几十分之一。Layer 3 才是真正的 LLM-as-a-judge,处理那些需要「理解」才能判断的维度。Layer 4 是兜底,处理高风险决策和评分争议。
实际运行中,一个 PR 的 eval 大概 60-70% 的样例在 Layer 1 就能得出结论,20-25% 在 Layer 2 解决,只有 10-15% 需要进入 Layer 3。这样整体成本可以控制在一个可接受的范围内。
CI 集成模式对比
把 eval 接入 CI 有几种不同的路径,适用的团队规模和工具链各不相同。
| 维度 | 原生 GitHub Actions | 专用平台(Braintrust 等) | 开源自建(Promptfoo 等) |
|---|---|---|---|
| 配置方式 | YAML workflow 手写 | SDK + 平台 UI | YAML 配置 + CLI |
| 结果展示 | Actions 日志 / PR comment | 平台仪表盘 + PR 自动评论 | 自建报告页 / HTML 导出 |
| 实验追踪 | 无,需自行实现 | 内置,自动关联 git commit | 依赖本地缓存或自建 |
| 成本 | 仅 LLM API 费用 | 平台订阅费 + API 费用 | 基础设施成本 + API 费用 |
| 适用场景 | 小团队、快速起步 | 中等规模、需要协作 | 有运维能力、数据敏感 |
| 安全测试 | 需自行集成 | 部分平台内置红队测试 | 开源红队插件可用 |
我见过三个团队用三种完全不同的方式落地,效果都不错,关键是匹配团队现状。
案例一:五人团队用 GitHub Actions 快速起步
一个做客服 AI 的小团队,最初全靠创始人手动测。他们的第一步非常简单:在仓库里维护一个 evals/golden.json,里面 50 条高价值测试用例,每条包含输入、期望输出类型和评分规则。GitHub Actions workflow 在每次 PR 修改 prompts/ 目录下文件时自动触发,用一个 Python 脚本跑全部样例,评分结果输出为 JUnit XML 格式,GitHub 原生就能识别。
# .github/workflows/eval.yml — 最简 CI eval 配置
name: AI Evals
on:
pull_request:
paths:
- 'prompts/**'
- 'evals/**'
jobs:
eval:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-python@v5
with:
python-version: '3.12'
- run: pip install -r evals/requirements.txt
- name: Run golden dataset evals
env:
OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }}
run: python evals/run_evals.py --dataset evals/golden.json --threshold 0.85这个脚本的核心逻辑很直接:跑完所有样例后,计算平均分,低于阈值就返回非零退出码,pipeline 失败。
# evals/run_evals.py — 核心评分逻辑(简化)
import json, sys, openai
def run_single_eval(case: dict) -> float:
response = openai.chat.completions.create(
model="gpt-4.1-mini",
temperature=0,
messages=[
{"role": "system", "content": load_prompt(case["prompt_version"])},
{"role": "user", "content": case["input"]}
]
)
output = response.choices[0].message.content
return score_output(output, case["expected"], case["rubric"])
def main():
dataset = json.load(open(sys.argv[--dataset]))
results = [run_single_eval(case) for case in dataset]
mean_score = sum(results) / len(results)
print(f"Mean score: {mean_score:.3f}")
# 低于阈值则 pipeline 失败
sys.exit(0 if mean_score > 0.85 else 1)就这么粗糙的配置,上线第一个月就拦住了三次退化。其中一次是有人把 system prompt 里一句「如果不确定,请告诉用户你需要更多信息」删掉了,改成了更简洁的「直接回答」。eval 平均分从 0.91 掉到 0.78,因为模型在不确定场景下开始编造答案。
案例二:中等规模团队用 Braintrust 做实验追踪
一个 20 人的 AI 产品团队,Prompt 迭代频率高,经常同时跑好几组实验。他们用 Braintrust 作为 eval 平台,最大的收益不是 CI 门禁本身,而是实验追踪——每次 eval 运行都自动关联 git commit,可以回溯任何一个 commit 对应的评分变化。
Braintrust 的 GitHub Action 会在 PR 下自动发评论,列出每个测试用例的得分变化。评审者不需要打开额外页面,直接在 PR 里就能看到哪些用例变好了、哪些变差了。
# 使用 Braintrust 的 CI 集成
- name: Run Braintrust evals
uses: braintrustdata/braintrust-eval-action@v2
with:
project: customer-support-ai
dataset: golden-v3
# 与 main 分支基线对比,回归超过 5% 则失败
regression_threshold: 0.05
env:
BRAINTRUST_API_KEY: ${{ secrets.BRAINTRUST_API_KEY }}与手写脚本最大的区别在于对比机制。手写脚本只能判断「这次得分是否高于绝对阈值」,Braintrust 能判断「这次得分相比基线下降了多少」。这对 Prompt 迭代特别重要——有时候绝对分数看起来还行,但相比上一个版本已经悄悄退化了。
案例三:数据敏感场景用 Promptfoo 自建
一个金融领域的团队,合规要求所有评测数据不能离开自有基础设施。他们选了 Promptfoo,一个开源的 eval 框架,用 YAML 定义测试套件,所有数据存在自己的 S3 里。
# promptfooconfig.yaml — 金融场景 eval 配置
prompts:
- file://prompts/financial_advisor.yaml
providers:
- id: openai:gpt-4.1
config:
temperature: 0
tests:
- vars:
question: 我有 50 万存款,应该怎么配置?
assert:
- type: contains-json
- type: llm-rubric
value: 回答必须包含风险提示,不得给出具体收益率承诺
- type: not-contains
value: 保证收益、稳赚不赔
threshold: 0.9Promptfoo 的好处是配置声明化,产品和业务同事也能看懂测试用例。红队测试功能是另一个加分项——可以自动生成 prompt injection 和 jailbreak 用例,检查模型在对抗场景下的安全性。
评估流程全景
把整个 CI eval 流程画出来,大致是这样的:
这个流程有几个关键设计点。首先,路径过滤——只有改动了 prompts 或模型配置的 PR 才触发 eval,避免无谓的 API 调用。其次,分层执行——Layer 1 失败就立刻停下,不浪费后续层的时间。第三,失败报告不是简单的 pass/fail,而是附带每个用例的输入、输出、期望和评分细节,让开发者能快速定位问题。最后,人工复核环节把争议案例的处理闭环,而不是让评分偏差反复阻塞 pipeline。
评分方法对比
不同评分方法的适用场景、成本和可靠性差异很大。选错了方法,要么评分不准,要么成本失控。
| 评分方法 | 适用场景 | 成本 | 可靠性 | 典型延迟 |
|---|---|---|---|---|
| 精确匹配 | 格式化输出、分类标签 | 极低 | 高(但覆盖窄) | <1ms |
| 正则/关键词 | 禁止词检测、必填字段 | 极低 | 中 | <1ms |
| ROUGE / BLEU | 摘要、翻译等参考文本对比 | 低 | 中(与人类判断相关性一般) | ~10ms |
| Embedding 相似度 | 语义等价判断 | 低 | 中高 | ~50ms |
| LLM-as-a-judge | 开放式问答、语气、准确性 | 高 | 中高(需校准) | 1-5s |
| 人工评审 | 高风险决策、评分争议 | 极高 | 最高(但难以规模化) | 分钟到天 |
LLM-as-a-judge 是目前处理开放式输出最实用的方法,但它不是万能药。Galtea 的实践指南提到一个关键数字:LLM judge 与人类专家的 Pearson 相关系数需要达到 0.7 以上,评分才有实际参考价值。低于这个阈值,judge 本身的判断就和人类不一致,用它做的门禁反而会引入噪声。
校准 judge 的方法也不复杂:先收集 50-100 条由领域专家打过分的样例,让 LLM judge 对同样的样例评分,然后比较两者的 Pearson 相关系数。如果相关性不够,调整 judge 的评分 prompt——通常需要更明确地定义每个分数档位的标准,减少模糊地带。
评分器设计对比
不同评分器设计的代码结构和适用场景也不同。以下是三种常见模式的对比。
# 模式一:硬编码规则评分 —— 快但脆弱
def rule_scorer(output: str, expected: dict) -> float:
score = 0.0
if expected["required_field"] in output:
score += 0.3
if len(output) <= expected["max_length"]:
score += 0.2
if not any(bad_word in output for bad_word in expected["banned_words"]):
score += 0.2
if json.loads(output).get("status") == "success":
score += 0.3
return score
# 优点:零成本,毫秒级
# 缺点:规则写死,覆盖面窄,语义变化抓不到# 模式二:Embedding 相似度评分 —— 语义级,低成本
from openai import OpenAI
from numpy import dot
from numpy.linalg import norm
client = OpenAI()
def embedding_scorer(output: str, expected: dict) -> float:
output_emb = client.embeddings.create(
input=output, model="text-embedding-3-small"
).data[0].embedding
expected_emb = client.embeddings.create(
input=expected["reference_answer"], model="text-embedding-3-small"
).data[0].embedding
similarity = dot(output_emb, expected_emb) / (
norm(output_emb) * norm(expected_emb)
)
return similarity
# 优点:能捕捉语义等价,成本低(约 $0.02 / 1M tokens)
# 缺点:只衡量相似度,不判断事实准确性# 模式三:LLM-as-a-judge 评分 —— 灵活但需校准
JUDGE_PROMPT = """你是一个评分员。请根据以下 rubric 对回答打分(1-5 分):
5 分:回答准确、完整,语气适当,无安全问题
4 分:基本准确,有小的遗漏但不影响理解
3 分:部分准确,关键信息有缺失
2 分:明显错误或答非所问
1 分:完全不可用或包含有害内容
用户问题:{question}
模型回答:{answer}
参考答案:{reference}
请严格按以下 JSON 格式输出:
{{"score": <分数>, "reason": "<一句话理由>"}}"""
def judge_scorer(output: str, expected: dict) -> float:
response = client.chat.completions.create(
model="gpt-4.1-mini",
temperature=0,
messages=[{
"role": "user",
"content": JUDGE_PROMPT.format(
question=expected["question"],
answer=output,
reference=expected["reference_answer"]
)
}]
)
result = json.loads(response.choices[0].message.content)
return result["score"] / 5.0
# 优点:能处理开放式评估,覆盖主观维度
# 缺点:成本较高,需要定期校准,存在位置偏差和冗长偏差三种模式不是互斥的。实际项目中,我推荐组合使用:先用规则评分过滤硬伤,再用 embedding 相似度做语义粗筛,最后对关键场景用 LLM-as-a-judge 做精细评估。
Golden Dataset 维护策略
Golden dataset 是整个 eval 体系的地基。dataset 质量不行,评分再精确也没用——垃圾进,垃圾出。
| 维护维度 | 推荐做法 | 常见错误 |
|---|---|---|
| 初始规模 | 50-100 条高价值真实案例 | 一上来追求 1000+ 条,质量参差不齐 |
| 来源 | 生产日志 + 历史 bug + 边界用例 | 只靠团队脑补,缺少真实场景 |
| 标注标准 | 单一领域专家做二分类(通过/不通过) | 多人标注不统一,Likert 量表过度细化 |
| 更新频率 | 每个迭代周期从生产反馈中补充 | 建完就不管,半年后与实际分布脱节 |
| 版本管理 | 与 prompt 一起纳入 git 版本控制 | dataset 散落在文档和表格里,无法追溯 |
| 场景覆盖 | 主路径 + 边界 + 安全对抗 + 拒答场景 | 只覆盖 happy path |
一个常见的误区是追求 dataset 的「大而全」。实际上,50 条精心挑选、标注清晰的用例,比 500 条随手凑数的用例有用得多。优先覆盖这几类场景:用户最常见的请求(主路径)、曾经出过 bug 的输入(回归保护)、对抗性输入(安全兜底)、以及模型应该拒绝回答的场景(合规保护)。
成本控制与波动处理
CI eval 的成本主要来自 LLM API 调用。如果不加控制,一个 200 条的测试集每次 PR 都跑一次,一个月的 API 费用可以轻松跑到几百甚至上千美元。
# 成本控制配置示例
eval_config:
# 分层执行,减少高成本调用
layers:
deterministic:
enabled: true
timeout_ms: 500
heuristic:
enabled: true
min_similarity: 0.75 # 低于此值直接失败,不调 LLM
llm_judge:
enabled: true
model: gpt-4.1-mini # 用小模型做 judge,不必用最强模型
temperature: 0 # 保证评分可复现
max_concurrent: 5 # 并发控制
# 缓存策略
cache:
enabled: true
directory: .eval-cache
ttl_hours: 24 # 相同输入 24 小时内复用结果
# 采样策略
sampling:
full_run: false # PR 阶段不全量跑
changed_prompt_cases: true # 只跑受影响的用例
smoke_test_count: 20 # 每次 PR 跑 20 条烟雾测试
full_run_schedule: daily # 全量跑留给每日定时任务几个实用的成本控制手段:
第一,缓存 API 响应。同一个输入在 24 小时内不太会因为外部变化而改变输出,缓存可以大幅减少重复调用。Promptfoo 的 .promptfoo/cache 就是这个思路。
第二,路径过滤触发。只有 prompts 目录或模型配置变更才触发 eval,改个 README 不需要跑。
第三,烟雾测试替代全量。日常 PR 只跑 20 条最关键的用例,全量 200 条留给每日定时任务或发版前的全量回归。
第四,用小模型做 judge。LLM-as-a-judge 不需要用和生成模型同级别的模型。GPT-4.1-mini 做 judge 的评分结果和 GPT-4.1 差异很小,但成本只有十分之一。
第五,temperature 设为 0。评分阶段不需要创造性,确定性输出保证同样的输入每次得到同样的分数,减少评分波动。
让失败可行动
Eval 失败最让人沮丧的不是红灯本身,而是「不知道哪里出了问题」。一个设计良好的 eval 失败报告应该包含这些信息。
| 信息维度 | 必须包含 | 作用 |
|---|---|---|
| 输入内容 | 完整的用户输入 | 复现问题 |
| 模型输出 | 完整的模型响应 | 定位偏差方向 |
| 期望结果 | golden dataset 中的参考答案 | 对比差距 |
| 评分详情 | 各维度得分和 judge 理由 | 理解失败原因 |
| 版本信息 | prompt 版本、模型版本、commit hash | 追溯变更源头 |
| 历史对比 | 与上一次运行的分数变化 | 判断是退化还是波动 |
| 复现命令 | 本地重跑该用例的命令 | 开发者自行调试 |
Braintrust 在这方面的做法值得参考——PR 评论里直接列出每个退化用例的输入、输出、期望和评分变化,评审者不用离开 GitHub 就能看到完整上下文。手写脚本的话,至少要做到把失败样例的输出保存为 artifact,方便下载查看。
上线检查清单
把 AI Evals 放进 CI 之前,逐项检查这些要点。
基础设施
- CI 环境能访问 LLM API(API Key 已配置为 secret)
- 网络出口策略允许访问模型提供商的 API endpoint
- 超时和重试策略已配置,避免偶发网络问题导致 pipeline 误失败
- API 调用有并发控制,不会触发速率限制
Golden Dataset
- 至少 50 条高价值测试用例,覆盖主路径、边界和安全场景
- 每条用例有明确的评分标准(参考答案或 rubric)
- 标注由领域专家完成,不是随手编写
- Dataset 与 prompt 一起纳入版本控制
评分体系
- 分层评分已实现,确定性检查在前,LLM judge 在后
- LLM-as-a-judge 的评分 prompt 经过校准,与人类专家 Pearson 相关 ≥ 0.7
- 评分阈值已设定,且与团队达成共识(建议初始 0.85)
- Judge 的 temperature 设为 0,保证评分可复现
成本控制
- PR 阶段使用烟雾测试(20 条左右),非全量运行
- API 响应已启用缓存,相同输入不重复调用
- 路径过滤已配置,无关变更不触发 eval
- 有每日/每周全量回归任务,覆盖完整 dataset
失败处理
- 失败报告包含输入、输出、期望、评分和版本信息
- 有明确的 owner 负责处理 eval 失败(不是无人认领的红灯)
- 评分争议有人工复核通道,不会永久阻塞 pipeline
- 失败样例有回流机制,确认后的退化案例加入 golden dataset
持续演进
- 每个迭代从生产反馈中补充新用例到 golden dataset
- 定期(月度)审查评分阈值是否需要调整
- LLM judge 的评分质量定期重新校准(模型升级后必须重做)
- 团队成员知道如何新增 eval 用例、如何本地重跑
写在最后
把 AI Evals 放进 CI 不是终点,它是一个持续演进的质量信号。传统的 CI 是一个门——通过就放行,不通过就阻塞。AI eval 更像是一个仪表盘,它在每次变更时告诉你质量的变化趋势:这个 Prompt 改动让回答更精准了,还是让某些场景退化了一点?这次模型升级整体表现更好,但在安全拒答上变弱了?
一开始不需要追求完美。50 条用例、一个分层评分脚本、一个 GitHub Actions workflow,这就够了。跑起来之后,团队自然会知道哪些用例需要补充、评分标准需要调整、阈值需要校准。重要的不是第一次搭建得多完善,而是让「每次变更都自动验证质量」成为习惯。
那个让我栽跟头的 Prompt 改动,后来被加进了 golden dataset。每次有人修改那个 prompt 文件,CI 都会自动验证:退款场景下,模型是否仍然会确认订单号。
参考资料
- Best AI Eval Tools for CI/CD Pipelines (2026 Review) — Braintrust
- Ultimate Guide to CI/CD for LLM Evaluation — Latitude.so
- How to Add LLM Evaluations to CI/CD Pipelines — Arize AI
- The Complete Guide for LLM Evaluations in 2026 — Galtea Blog
- LLM-as-a-Judge in 2026: Top Evaluation Techniques — DeepEval
- 7 LLM Evaluation & Testing Tools Compared (2026) — Rhesis AI
- LLM Evaluation: Frameworks, Metrics, and Best Practices — FutureAGI
- awesome-ai-eval — GitHub