前面的系列已经覆盖了从 API 调用、消息历史和生成参数,到结构化输出、错误处理、评估、日志与安全边界的基础知识。现在把这些能力组合成一个很小但实用的项目:输入一段任务说明,调用模型生成草稿,再用 Python 执行可重复的质量检查,只有通过检查才返回成功退出码。它不是“让模型自己判断自己写得好不好”,而是把模型当作候选内容生成器,把确定性的规则留给程序。

先定义门禁的职责

假设我们要生成一段产品更新说明。门禁要求输出必须满足三个条件:包含指定关键词、长度在范围内、不能出现内部占位符。模型负责组织自然语言,Python 负责检查这些条件。这样做的好处是规则可读、可测试,也能在 CI 或批处理脚本中使用。

门禁并不能证明事实正确。关键词存在不代表内容真实,长度合适也不代表表达清晰。因此,检查结果应理解为“满足了已声明的机械条件”,而不是质量的全部。涉及发布、付款或数据修改时,还需要人工审批和更严格的业务校验。

准备环境和输入

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

1
2
3
python -m venv .venv
source .venv/bin/activate
python -m pip install openai

密钥和模型名只从环境变量读取:

1
2
export OPENAI_API_KEY="替换为你的真实密钥"
export MODEL_NAME="替换为你可用的模型名称"

新建 request.json,它是可审计的输入快照:

1
2
3
4
5
6
{
"task": "写一段面向普通用户的产品更新说明,说明搜索速度提升和新增导出功能。",
"required_terms": ["搜索速度", "导出"],
"min_length": 80,
"max_length": 300
}

真实项目还应限制输入文件大小、校验字段类型,并对可能包含个人信息的内容脱敏。不要把密钥放进 JSON、源码或 Git。

编写最小质量门禁

创建 quality_gate.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
import json
import os
import sys
import time
from pathlib import Path

from openai import OpenAI


def load_request(path: str) -> dict:
data = json.loads(Path(path).read_text(encoding="utf-8"))
if not isinstance(data.get("task"), str) or not data["task"].strip():
raise ValueError("task 必须是非空字符串")
terms = data.get("required_terms", [])
if not isinstance(terms, list) or not all(isinstance(x, str) for x in terms):
raise ValueError("required_terms 必须是字符串列表")
minimum, maximum = data.get("min_length", 1), data.get("max_length", 2000)
if not (isinstance(minimum, int) and isinstance(maximum, int)
and 0 < minimum <= maximum):
raise ValueError("长度范围无效")
return data


def generate(task: str) -> str:
client = OpenAI(api_key=os.environ["OPENAI_API_KEY"])
response = client.responses.create(
model=os.environ["MODEL_NAME"],
instructions="你是中文产品文案助手。只返回正文,不要标题、解释或 Markdown 代码围栏。",
input=task,
)
return response.output_text.strip()


def check(text: str, request: dict) -> list[str]:
problems = []
if not (request["min_length"] <= len(text) <= request["max_length"]):
problems.append("长度不在允许范围内")
missing = [term for term in request["required_terms"] if term not in text]
if missing:
problems.append("缺少关键词:" + "、".join(missing))
if "TODO" in text or "待补充" in text:
problems.append("包含未完成占位符")
return problems


def main() -> int:
if len(sys.argv) != 2:
print("用法:python quality_gate.py request.json", file=sys.stderr)
return 2
request = load_request(sys.argv[1])
started = time.perf_counter()
text = generate(request["task"])
problems = check(text, request)
elapsed = (time.perf_counter() - started) * 1000
print(text)
print(f"\n检查耗时:{elapsed:.0f} ms", file=sys.stderr)
if problems:
print("门禁失败:" + ";".join(problems), file=sys.stderr)
return 1
print("门禁通过", file=sys.stderr)
return 0


if __name__ == "__main__":
raise SystemExit(main())

运行命令是:

1
python quality_gate.py request.json

这里没有预先编造模型输出。成功时,终端会显示本次 API 实际返回的正文,并在标准错误中显示“门禁通过”;失败时会列出具体规则。退出码 0 表示通过,1 表示模型返回了但未通过规则,2 表示命令行用法错误。脚本没有自动修改文本,因为自动修复可能改变原意,且会掩盖规则设计问题。

为什么把生成和检查分开

generate 只负责网络调用,check 是一个纯函数:同一段文本和同一份请求配置,总会得到同样的检查结果。纯函数很容易用固定字符串测试,不需要每次消耗 API 额度。例如:

1
2
3
request = {"required_terms": ["搜索速度"], "min_length": 5, "max_length": 20}
assert check("搜索速度明显提升", request) == []
assert check("导出功能上线", request) == ["缺少关键词:搜索速度"]

实际项目可以把这部分放入测试文件,再增加敏感词、必需标题、行数和格式检查。规则应该来自业务需求,而不是为了让某一次模型输出“恰好通过”临时添加。

加上有限重试,而不是无限重试

网络超时或服务限流属于调用层问题,可以在 generate 外层增加有限重试;内容不合格则是质量层问题,两者不应混在一起。重试必须有次数上限和退避等待,例如最多两次,并在日志中记录错误类型、尝试次数和耗时。认证失败通常不应重试,应该直接提示检查环境变量。生产代码还应设置请求超时,避免命令一直等待。

如果希望对不合格文本重新生成,应明确记录“第几次尝试”和每次失败原因,并设置总预算。更稳妥的方式是把失败样本保存为评估数据,而不是无条件反复请求,直到碰巧通过。

常见问题

为什么通过关键词检查仍可能是错误内容? 因为字符串匹配只能检查表面条件,不能验证事实。需要外部数据、引用核对或人工复审时,必须把这些步骤设计成独立环节。

中文长度为什么直接使用 len? Python 的字符串长度按 Unicode 字符计数,适合这个简单门禁,但它不等同于用户看到的排版宽度,也不等同于 token 数。若限制来自 API 上下文窗口,应使用对应 tokenizer 或保守预算。

模型返回空字符串怎么办? strip 后应把空结果视为失败,并记录服务响应中的可用诊断信息;不要把空内容当作成功。若使用其他服务商,应先查其官方文档确认响应文本字段,不要凭字段名称猜测。

如何避免敏感信息进入日志? 不打印密钥,不把完整请求和响应无期限保存;对用户输入、异常信息和持久化日志做脱敏、访问控制与保留期限设计。

小结

这个小项目把一次模型调用接入了一个可验证的程序边界:环境变量提供配置,模型生成候选文本,纯函数执行确定性检查,退出码把结果交给脚本或 CI。它没有把简单规则包装成“模型评审”,也没有伪造运行结果。后续扩展时,可以加入固定评测集、结构化结果、有限重试和人工审批,但每增加一个环节,都应保留输入、规则和失败原因,让 AI 功能能够被测试、追踪和复盘。