前面的学习已经覆盖了 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/activatepython -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 jsonimport osimport sysfrom pathlib import Pathfrom openai import OpenAIALLOWED = {"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 jsonfrom pathlib import Pathbefore = 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 负责定义协议、保存证据和判断是否通过。以后修改提示词、模型或生成参数时,不必凭感觉判断“好像更好了”,而是可以用同一组输入重新实验、比较并发现退化。