传统单元测试通常检查确定性的输入输出:给函数一个参数,断言返回值等于预期。但大模型任务具有概率性,答案可能存在多种合理表达,供应商、模型版本、提示词、上下文长度和采样参数的变化,都可能改变结果。
因此,生产系统需要回答的不是“这次调用有没有报错”,而是更具体的三个问题:
- 关键任务是否仍然满足业务约束?
- 模型升级后,哪些样本变差了?
- 质量提升是否以不可接受的延迟、成本或安全风险为代价?
Golden Set 可以理解为一组经过人工确认的代表性样本。它不是“标准答案库”的简单堆积,而是对真实业务分布的可版本化抽样。每条样本至少应包含输入、必须满足的条件、禁止出现的条件,以及必要时的参考答案。
先定义评测契约
评测的第一步不是选择模型,而是把“好结果”写成可检查的契约。例如客服摘要任务可以规定:必须保留用户诉求和处理结论;不能虚构订单状态;输出必须是合法 JSON;摘要长度不能超过指定上限。
建议把样本存为 JSONL,每行一条,便于 Git 追踪、增量评测和失败样本定位:
{
"id":"refund-001","input":"用户说商品已退回,但七天后仍未收到退款。","must_include":["退款进度"],"must_not_include":["已完成退款"],"format":"json"}
{
"id":"refund-002","input":"用户询问如何修改收货地址。","must_include":["修改地址"],"must_not_include":["保证可以修改"],"format":"json"}
样本不应只覆盖“正常提问”。至少要加入边界输入、空字段、错别字、长文本、冲突信息和潜在诱导内容。样本数量没有脱离业务的通用标准;在样本很少时,分数波动会很大,评测结果只能作为趋势信号,不能被解释为稳定的总体准确率。
用确定性规则先筛一遍
很多约束不需要另一个模型判断。格式、字段、关键词、长度和敏感操作可以用程序直接检查,结果更容易复现,也不会产生额外模型费用。下面是一个最小评测器:
# evaluate.py
import json
import os
import sys
from pathlib import Path
def check_case(case: dict, output: str) -> list[str]:
errors = []
for text in case.get("must_include", []):
if text not in output:
errors.append(f"缺少必要内容: {text}")
for text in case.get("must_not_include", []):
if text in output:
errors.append(f"出现禁止内容: {text}")
if len(output) > int(os.getenv("MAX_OUTPUT_CHARS", "1200")):
errors.append("输出超过长度限制")
if case.get("format") == "json":
try:
json.loads(output)
except json.JSONDecodeError:
errors.append("不是合法 JSON")
return errors
def main() -> int:
result_file = Path(sys.argv[1])
cases = [json.loads(line) for line in Path("golden.jsonl").read_text().splitlines()]
results = json.loads(result_file.read_text())
failed = []
for case in cases:
errors = check_case(case, results[case["id"]])
if errors:
failed.append({
"id": case["id"], "errors": errors})
report = {
"total": len(cases), "failed": len(failed), "failures": failed}
Path("evaluation-report.json").write_text(json.dumps(report, ensure_ascii=False, indent=2))
print(json.dumps(report, ensure_ascii=False))
return 1 if failed else 0
if __name__ == "__main__":
raise SystemExit(main())
这个脚本假设模型调用阶段已经把结果写入 results.json,格式是 {"样本 id":"模型输出"}。把调用和评分分开,有两个好处:同一批模型结果可以重复评分;规则变更时不必重新消耗 API 配额。
把模型调用封装成可替换端点
模型调用层应只负责请求、超时、重试和结果归档,不应把供应商细节散落到评测逻辑中。下面以常见的 OpenAI 兼容请求形态举例。实际字段、路径和模型名必须以所选服务的当前文档为准,不能因为“兼容”就假设所有参数都可用。
# run_eval.py
import json
import os
from pathlib import Path
import httpx
BASE_URL = os.environ["MODEL_BASE_URL"].rstrip("/")
API_KEY = os.environ["MODEL_API_KEY"]
MODEL = os.environ["MODEL_NAME"]
def ask(text: str) -> str:
payload = {
"model": MODEL,
"temperature": 0,
"messages": [
{
"role": "system", "content": "只输出符合要求的结果。"},
{
"role": "user", "content": text},
],
}
with httpx.Client(timeout=45) as client:
response = client.post(
f"{BASE_URL}/chat/completions",
headers={
"Authorization": f"Bearer {API_KEY}"},
json=payload,
)
response.raise_for_status()
data = response.json()
return data["choices"][0]["message"]["content"]
cases = [json.loads(x) for x in Path("golden.jsonl").read_text().splitlines()]
outputs = {
}
for case in cases:
outputs[case["id"]] = ask(case["input"])
Path("results.json").write_text(json.dumps(outputs, ensure_ascii=False, indent=2))
例如,若选择 HaerAPI 作为中转或模型接入层,只有在其当前文档明确说明兼容上述请求路径和字段时,才应将 MODEL_BASE_URL 配置为对应地址;模型标识、计费方式、限流策略和数据处理范围都应以实际文档及合同为准。
本地运行时可以这样注入配置,密钥不会进入代码或仓库:
export MODEL_BASE_URL="https://your-endpoint.example/v1"
export MODEL_API_KEY="replace-with-environment-secret"
export MODEL_NAME="your-model-id"
python run_eval.py
python evaluate.py results.json
在 CI 中设置质量门禁
最简单的门禁是“失败样本数不得超过阈值”。对于关键流程,可以要求零失败;对于开放式生成任务,则可设定分层阈值,并把失败明细作为构建产物保存。
# .github/workflows/llm-eval.yml
name: llm-eval
on: [pull_request]
jobs:
evaluate:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-python@v5
with:
python-version: "3.12"
- run: pip install httpx
- name: Run golden set
env:
MODEL_BASE_URL: ${
{
secrets.MODEL_BASE_URL }}
MODEL_API_KEY: ${
{
secrets.MODEL_API_KEY }}
MODEL_NAME: ${
{
vars.MODEL_NAME }}
run: python run_eval.py
- name: Check contract
run: python evaluate.py results.json
- name: Upload report
if: always()
uses: actions/upload-artifact@v4
with:
name: llm-evaluation-report
path: evaluation-report.json
生产中还应记录每次评测的提交号、模型标识、提示词版本、参数、耗时、token 用量和失败原因。若接口返回这些字段,应原样归档;若不返回,就不要自行推断。为了降低成本,可以在拉取请求阶段运行小型冒烟集,合并或定时任务运行完整集,但两者必须使用清晰的样本标签,避免把部分通过误读为整体通过。
评分器和人工复核如何配合
规则评分适合检查硬约束,不适合判断语义是否自然、答案是否真正解决问题。模型评分器可以辅助处理开放式任务,但它本身也会漂移,必须固定评分提示词、输出结构和判定标准,并定期抽样由人工复核。不要把单一模型的自评结果当作客观真值。
更稳妥的流程是:先用规则筛掉格式和安全问题,再对剩余样本做语义评分;当新版本出现“总分上升但关键类别下降”时,按类别查看混淆和失败案例,而不是只看一个平均数。每个修复过的线上问题都应回流 Golden Set,并标注来源和预期行为,这样评测集才会随着真实风险增长。
常见问题
为什么同样输入每次结果仍可能不同?
temperature=0 只能降低一部分随机性,不能保证不同服务、不同模型版本或并发条件下完全一致。评测应关注契约是否满足,并保留模型和配置元数据。
Golden Set 越大越好吗?
不一定。重复、低价值样本会增加成本,却不一定提高覆盖率。应按业务类别、风险等级和历史失败类型分层,优先补充能区分版本质量的样本。
CI 失败是否意味着模型一定变差?
不一定。也可能是接口字段变化、网络超时、样本更新、解析器缺陷或服务限流。报告必须区分业务断言失败、调用失败和评测程序失败。
可以把用户数据直接放进评测集吗?
应先完成脱敏、授权和访问控制,并确认数据处理要求。无法确认合规边界时,使用合成数据或经过审批的最小化样本,不要把生产日志直接上传到第三方接口。
总结
Golden Set + CI 的核心价值,不是给大模型制造一个看似精确的分数,而是建立可追溯的变化检测机制:用版本化样本描述真实风险,用确定性规则守住硬约束,用受控的语义评测覆盖开放问题,再把失败案例持续回流。模型、端点或提示词发生变化时,团队因此能够看到具体受影响的样本和原因,并在质量、成本、延迟与合规之间作出有证据的决策。