前面的学习已经覆盖了 API 调用、提示词、结构化输出、错误处理、日志和评估基础。综合项目不应该只是把这些概念堆在一起,而要解决一个实际问题:提示词或模型改动后,如何知道已有能力没有被悄悄破坏?本篇用 Python 写一个小型离线回归实验器。它先读取固定测试集,再调用模型生成结构化结果,最后用本地规则检查结果并保存证据。这里的“离线”指评估阶段读取已保存的结果,不代表第一次生成时不需要网络。

先定义输入和判定标准

我们做一个简单的意图分类器:把用户句子分为 refund、shipping 或 other。模型负责给出候选分类,Python 负责检查 JSON 格式、标签是否合法,并计算准确率。这样即使模型输出自然语言发生变化,评估也只依赖稳定的字段。

新建 cases.jsonl,每行是一条独立 JSON:

1
2
3
{"id":"1","text":"我想申请退款","expected":"refund"}
{"id":"2","text":"快递现在到哪里了?","expected":"shipping"}
{"id":"3","text":"你们周末营业吗?","expected":"other"}

测试集不是越大越好。入门阶段先覆盖每个标签的典型样例和容易混淆的样例,并且把它固定下来。每次只修改一个变量,例如提示词或模型,才能把结果变化归因到具体改动。

准备环境和密钥

使用虚拟环境安装 OpenAI 官方 Python SDK:

1
2
3
4
5
python -m venv .venv
source .venv/bin/activate
python -m pip install openai
export OPENAI_API_KEY="替换为你的真实密钥"
export MODEL_NAME="替换为你可用的模型名称"

密钥只从环境变量读取,不写入源码、测试集或结果文件。若使用其他服务,先查其官方文档确认是否支持同样的 Responses API 字段,再调整客户端配置。

编写回归实验器

创建 regression_lab.py:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
import json
import os
import sys
from pathlib import Path

from openai import OpenAI

ALLOWED = {"refund", "shipping", "other"}


def load_cases(path: Path) -> list[dict]:
cases = []
for line_number, line in enumerate(path.read_text(encoding="utf-8").splitlines(), 1):
if not line.strip():
continue
item = json.loads(line)
if not isinstance(item.get("id"), str):
raise ValueError(f"第 {line_number} 行缺少字符串 id")
if not isinstance(item.get("text"), str) or not item["text"].strip():
raise ValueError(f"第 {line_number} 行 text 无效")
if item.get("expected") not in ALLOWED:
raise ValueError(f"第 {line_number} 行 expected 无效")
cases.append(item)
if not cases:
raise ValueError("测试集为空")
return cases


def classify(client: OpenAI, text: str) -> dict:
response = client.responses.create(
model=os.environ["MODEL_NAME"],
instructions=(
"你是意图分类器。只能返回 JSON,不要 Markdown。"
'格式必须是 {"label":"refund|shipping|other"}。'
),
input=text,
)
return json.loads(response.output_text)


def run(cases: list[dict]) -> dict:
client = OpenAI(api_key=os.environ["OPENAI_API_KEY"])
results = []
for case in cases:
try:
output = classify(client, case["text"])
label = output.get("label")
valid = label in ALLOWED
results.append({
"id": case["id"],
"expected": case["expected"],
"predicted": label,
"valid": valid,
"correct": valid and label == case["expected"],
})
except (json.JSONDecodeError, KeyError, TypeError, ValueError) as exc:
results.append({
"id": case["id"],
"expected": case["expected"],
"predicted": None,
"valid": False,
"correct": False,
"error": type(exc).__name__,
})
correct = sum(item["correct"] for item in results)
return {
"model": os.environ["MODEL_NAME"],
"total": len(results),
"correct": correct,
"accuracy": correct / len(results),
"results": results,
}


def main() -> None:
if len(sys.argv) != 3:
raise SystemExit("用法:python regression_lab.py cases.jsonl report.json")
cases = load_cases(Path(sys.argv[1]))
report = run(cases)
Path(sys.argv[2]).write_text(
json.dumps(report, ensure_ascii=False, indent=2) + "\n",
encoding="utf-8",
)
print(f"完成 {report['total']} 条,准确率 {report['accuracy']:.1%}")


if __name__ == "__main__":
main()

运行:

1
python regression_lab.py cases.jsonl report.json

程序实际调用模型,输出会随模型和服务状态变化,因此本文不伪造某个准确率。可验证的结果是:命令成功后生成 report.json,其中保存模型名、总数、正确数、准确率以及每条样例的判定。运行前必须确认当前模型支持 responses.create 和 output_text;这两个字段应以所用 SDK 和服务商的官方文档为准。

为什么要把验证放在本地

模型返回的 JSON 不是天然可信的。json.loads 负责检查语法,label in ALLOWED 负责检查业务协议,label == expected 才负责检查任务正确性,三者是不同层次的问题。若模型返回解释文字、拼错标签或遗漏字段,程序会把它记为失败,而不是把异常结果当成成功。

报告中的 valid 和 correct 也要分开看。一个结果可能是合法 JSON,但标签仍然错;反过来,格式错误时连业务判断都无法进行。生产系统可以继续增加延迟、费用、重试次数和错误类型,但每个指标都应有清晰定义。

做第二次实验并离线比较

复制一份报告后再修改提示词,例如加入三个标签的中文解释,然后再次运行。离线比较只读取报告,不再调用模型:

1
2
3
4
5
6
7
8
9
10
11
import json
from pathlib import Path

before = json.loads(Path("report-before.json").read_text(encoding="utf-8"))
after = json.loads(Path("report-after.json").read_text(encoding="utf-8"))

print("旧准确率:", before["accuracy"])
print("新准确率:", after["accuracy"])
for old, new in zip(before["results"], after["results"]):
if old != new:
print(old["id"], old.get("predicted"), "->", new.get("predicted"))

比较时不要只看平均准确率。三条样例全部正确,不等于真实业务可靠;应继续增加边界案例,并关注某一个重要类别是否退化。更进一步,可以规定最低通过线,例如准确率低于 0.9 就让 CI 失败,但阈值必须结合测试集规模,否则一条样例的变化就会造成夸大的波动。

常见问题

为什么不直接比较完整文本? 自然语言有多种正确表达,全文比较容易误报。分类任务应比较经过校验的字段;摘要、改写等任务则需要人工抽样或专门的评价规则。

模型输出不是合法 JSON 怎么办? 当前实验器记录失败,不自动猜测或截取文本。自动修复可以作为后续能力,但修复后的结果必须再次解析和校验,并记录原始失败类型,不能静默掩盖问题。

测试集能否包含真实用户数据? 可以在完成脱敏、授权和访问控制后谨慎使用。入门示例使用人工编写的无敏感样例,避免把个人信息发送给外部模型或写进日志。

小结

这个项目把固定测试集、结构化输出、本地校验、结果记录和离线比较串成了一条最小回归链路。模型负责提出候选答案,Python 负责定义协议、保存证据和判断是否通过。以后修改提示词、模型或生成参数时,不必凭感觉判断“好像更好了”,而是可以用同一组输入重新实验、比较并发现退化。