前面的系列已经介绍了模型调用、结构化输出、错误处理、模型适配和可观测性。本篇把这些知识组合成一个小项目:用户输入一段问题描述,模型给出分类、优先级和下一步建议,程序再负责校验、记录和输出。它不是一个自动执行工单的系统,而是一个“生成候选分诊结果、由程序守住边界”的最小实现。
先定义分诊结果 先把问题分成四个队列:billing(账单)、technical(技术)、account(账户)和 other(其他)。每条结果包含 category、priority、summary、next_action 四个字段。优先级只能是 low、medium 或 high,摘要和行动建议必须是非空字符串。
为什么不直接保存模型返回的文本?因为自然语言的格式和含义都可能漂移。比如“无法登录且今天要提交报表”既可能被分到技术,也可能被分到账户;程序至少要保证它最终落在允许的枚举值中。分类本身仍然需要人工抽查,校验只能保证形状和边界,不能证明判断一定正确。
本项目的处理链是:读取输入、调用模型、解析 JSON、校验字段、写入 JSONL 日志。每一步都可以单独替换,后续要接入数据库或工单系统时,也不必让业务代码依赖某一家模型服务。
准备环境与密钥 在项目目录创建虚拟环境,并安装官方 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="替换为你可用的模型名称"
密钥只从环境变量读取,不能写进源码、配置文件或日志。模型名称也通过环境变量传入,避免把某个具体模型写死。不同模型服务对结构化输出的支持和参数命名可能不同,实际使用前应查看所选服务的官方 Python SDK 文档;下面只依赖客户端返回文本的通用部分。
编写最小分诊器 新建 issue_triage.py。请求层使用 Responses API 的 Python SDK 写法,response.output_text 作为模型生成的文本。如果你使用的服务不是该 SDK,只需要替换 call_model,校验和日志部分可以保持不变。
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 import jsonimport osimport sysfrom datetime import datetime, timezonefrom pathlib import Pathfrom openai import OpenAICATEGORIES = {"billing" , "technical" , "account" , "other" } PRIORITIES = {"low" , "medium" , "high" } SYSTEM = """你是问题分诊助手。只返回 JSON 对象,不要 Markdown 代码围栏。 格式必须是: {"category":"billing|technical|account|other", "priority":"low|medium|high", "summary":"一句中文摘要", "next_action":"一句可执行的下一步建议"} 不确定时使用 other 和 medium,不要添加额外字段。""" def call_model (client: OpenAI, text: str ) -> str : response = client.responses.create( model=os.environ["MODEL_NAME" ], instructions=SYSTEM, input =text, ) return response.output_text def validate (value: object ) -> dict [str , str ]: fields = {"category" , "priority" , "summary" , "next_action" } if not isinstance (value, dict ) or set (value) != fields: raise ValueError("返回结果的字段不符合约定" ) if value["category" ] not in CATEGORIES: raise ValueError("category 不是允许的枚举值" ) if value["priority" ] not in PRIORITIES: raise ValueError("priority 不是允许的枚举值" ) for name in ("summary" , "next_action" ): if not isinstance (value[name], str ) or not value[name].strip(): raise ValueError(f"{name} 必须是非空字符串" ) return value def main () -> None : if len (sys.argv) != 2 : raise SystemExit("用法:python issue_triage.py '问题描述'" ) client = OpenAI() raw = call_model(client, sys.argv[1 ]) result = validate(json.loads(raw)) record = { "created_at" : datetime.now(timezone.utc).isoformat(), "input" : sys.argv[1 ], "result" : result, } with Path("triage.jsonl" ).open ("a" , encoding="utf-8" ) as file: file.write(json.dumps(record, ensure_ascii=False ) + "\n" ) print (json.dumps(result, ensure_ascii=False , indent=2 )) if __name__ == "__main__" : main()
validate 是这个例子的核心。json.loads 只负责把字符串变成 Python 对象,并不代表内容可信;随后检查顶层类型、字段集合、枚举值和字符串非空,才能把模型输出转换成程序认可的数据。使用 set(value) != fields 还可以拒绝模型悄悄增加的字段,避免下游误把未经设计的数据当成正式字段。
执行示例:
1 python issue_triage.py "昨晚扣款成功,但订阅页面仍显示未付款"
这里不预先写死输出,因为模型结果会受模型、提示词和服务配置影响。实际运行后,终端会打印通过校验的 JSON,triage.jsonl 会追加一条包含输入、结果和 UTC 时间的记录。JSONL 比覆盖一个大 JSON 文件更适合逐条追加,也方便之后按行回放和排查。
让失败变得可诊断 当前代码在 JSON 格式错误、网络异常或校验失败时会直接退出。对于最小示例这是有意的:先让失败暴露出来,再决定恢复策略。生产代码可以在边界处分别捕获异常,并记录错误类型,而不是无条件重试所有错误:
1 2 3 4 5 6 7 8 9 try : raw = call_model(client, text) result = validate(json.loads(raw)) except json.JSONDecodeError as exc: print (f"模型没有返回合法 JSON:{exc} " , file=sys.stderr) raise SystemExit(2 ) except ValueError as exc: print (f"分诊结果未通过校验:{exc} " , file=sys.stderr) raise SystemExit(3 )
网络超时、限流和临时服务错误可以采用有限次数的指数退避;JSON 格式错误则更适合携带校验错误再次请求,或转人工处理。不要把所有失败都重试,否则一个稳定的业务错误会变成更多请求和更高成本。日志中也应避免保存不必要的个人信息,必要时先脱敏。
常见问题 为什么不让模型直接创建工单? 因为模型输出属于不完全可信的候选结果。创建工单、发送通知等有副作用的动作应放在独立函数中,并增加权限、人工确认或幂等检查;本例只写本地日志,故意把风险限制在可回滚范围内。
分类错了,字段校验有什么用? 校验不能解决语义错误,但可以阻止非法队列、缺失字段和空建议进入下游。要改进分类质量,需要准备带人工标签的测试集,定期检查准确率和高优先级样本,而不是只看一次成功运行。
为什么不用正则表达式解析 JSON? JSON 是有嵌套和转义规则的数据格式,正则容易遗漏边界情况。先使用 json.loads,再做业务校验,职责更清楚。若服务支持严格的结构化输出,也应结合本地校验,而不是完全依赖服务端约束。
小结 问题分诊器展示了一条可复用的 AI 应用边界:模型负责从自然语言生成候选判断,Python 负责约束数据、记录过程和控制副作用。最小项目已经具备结构化输出、适配层、校验、日志和错误分类几个关键部件。下一步可以加入人工确认界面、离线测试集和有限重试,但每次扩展都应先保留输入、原始响应摘要与最终结果之间的可追踪关系。