前面的系列已经分别介绍了提示词、消息历史、结构化输出、错误处理、评估和安全边界。现在把这些知识组合成一个小而完整的项目:输入一封邮件,判断它属于 sales、support 或 other,同时给出简短理由;如果模型返回的类别不合法,程序不猜测,而是把任务交给人工复核。重点不是让模型“看起来聪明”,而是让每个结果都能解释、校验和处理。
先定义边界和结果
邮件分拣是一个适合入门的任务,因为类别有限,且业务动作清楚:sales 可以进入销售队列,support 可以进入支持队列,无法确定的内容进入人工队列。模型只负责提出候选结果,Python 负责检查边界。这样可以把概率性的生成和确定性的业务规则分开。
我们规定模型必须返回三个字段:category、reason 和 needs_review。category 只能是三个枚举值,reason 不能是空字符串;如果类别是 other,或模型明确表示不确定,就必须复核。不要把“置信度”直接当作事实,除非你已经用固定测试集验证过它具有可用的校准意义。
准备环境和密钥
创建虚拟环境并安装官方 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 的 input 和 output_text。不同模型对结构化输出参数的支持可能不同,运行前应查看所选服务的官方文档;本文把最关键的 JSON 解析和业务校验保留在本地,即使服务端没有提供严格 JSON 模式,也不会直接把未经检查的文本交给下游系统。
编写模型调用和本地校验
新建 email_triage.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
| import json import os import sys from typing import Any
from openai import OpenAI
ALLOWED = {"sales", "support", "other"}
def ask_model(client: OpenAI, email: str) -> dict[str, Any]: response = client.responses.create( model=os.environ["MODEL_NAME"], input=[ { "role": "system", "content": ( "你是邮件分拣器。只返回 JSON,不要 Markdown。" '格式为 {"category":"sales|support|other",' '"reason":"不超过30字的理由",' '"needs_review":true或false}。' "无法确定时使用 other,并将 needs_review 设为 true。" ), }, {"role": "user", "content": email}, ], max_output_tokens=120, ) try: value = json.loads(response.output_text) except json.JSONDecodeError as exc: raise ValueError("模型没有返回合法 JSON") from exc if not isinstance(value, dict): raise ValueError("JSON 顶层必须是对象") return value
def validate(value: dict[str, Any]) -> dict[str, Any]: category = value.get("category") reason = value.get("reason") review = value.get("needs_review") if category not in ALLOWED: raise ValueError(f"未知类别:{category!r}") if not isinstance(reason, str) or not reason.strip(): raise ValueError("reason 必须是非空字符串") if not isinstance(review, bool): raise ValueError("needs_review 必须是布尔值") result = { "category": category, "reason": reason.strip(), "needs_review": review or category == "other", } return result
def main() -> None: if len(sys.argv) != 2 or not sys.argv[1].strip(): raise SystemExit("用法:python email_triage.py '邮件正文'") if not os.environ.get("OPENAI_API_KEY"): raise SystemExit("请先设置 OPENAI_API_KEY") if not os.environ.get("MODEL_NAME"): raise SystemExit("请先设置 MODEL_NAME")
client = OpenAI() try: result = validate(ask_model(client, sys.argv[1])) except (ValueError, KeyError) as exc: result = { "category": "other", "reason": f"自动结果未通过校验:{exc}", "needs_review": True, } print(json.dumps(result, ensure_ascii=False))
if __name__ == "__main__": main()
|
关键点有三个。第一,提示词限制了输出,但限制不是校验,json.loads 和字段检查才是程序的边界。第二,异常分支没有重试到“总会成功”,而是生成一个明确的人工复核结果;真实系统还可以把原邮件和错误代码写入安全的任务队列。第三,other 强制复核,体现了宁可降低自动化率,也不要把不确定邮件错误分发的策略。
如何验证而不伪造结果
先做不需要网络的语法检查:
1
| python -m py_compile email_triage.py
|
然后准备三条固定样例,分别覆盖销售咨询、售后问题和无法判断的内容。每次修改提示词或模型,只改变一个变量,并把程序输出保存下来,与预先写好的期望类别比较。不要把一次成功调用当成测试;至少要包含同义表达、缺少上下文和混合意图等容易出错的样例。
还可以把 ask_model 替换成一个返回字典的假实现,专门测试 validate:合法类别应通过,未知类别、空理由、字符串形式的 "true" 都应进入复核。这类单元测试不消耗 API 额度,也能验证最重要的安全边界。文章不展示虚构的模型输出,实际结果取决于你配置的服务和测试邮件。
常见问题
为什么不直接相信 JSON? JSON 只说明语法正确,不说明类别合法。{"category":"finance"} 仍然可能让下游路由失败,所以必须做枚举和类型校验。
为什么不让模型直接调用队列或发邮件? 分拣结果会影响后续业务,入门项目应先输出建议,再由确定性的程序或人工审批执行动作。工具调用和权限控制应在后续主题中单独设计。
出现 JSON 解析失败怎么办? 先记录错误类型而不是原样记录可能包含隐私的邮件内容;可以有限次数重试,仍失败就人工复核。重试不能替代兜底,也要设置超时和预算。
理由是否可信? 理由是模型生成的解释,不是决策证据。若业务需要审计,应同时保存输入摘要、提示词版本、模型名、时间和校验结果,并按隐私要求控制保存范围。
小结
这个项目只有一个核心能力:把一封邮件转换成经过本地规则校验的分拣建议。模型负责理解开放文本,Python 负责枚举、类型、异常和人工兜底。今后增加类别、接入数据库或加入队列时,仍应保持这条边界:先定义可验证的结果,再让模型参与其中,而不是把未经检查的生成文本直接当作业务事实。