前面的系列已经分别介绍了 API 调用、提示词、结构化输出、错误处理、日志和评估。本篇把这些能力组合成一个小项目:输入一条中文客服消息,模型将它归入固定意图,程序校验结果并记录耗时;随后用一组带标签的样例做简单回归测试。重点不是追求复杂架构,而是建立“模型给候选、程序做约束、测试发现退化”的基本工程闭环。
先定义任务和边界 本项目只识别四种意图:退款、物流、账户、其他。模型不能直接退款、查询订单或修改账户,它只返回分类结果和一句理由。真正的业务动作仍应由普通 Python 代码调用受控接口完成,这样即使模型判断错误,也不会直接造成外部副作用。
程序的数据流是:读取一条消息 → 组装提示词 → 调用模型 → 解析 JSON → 检查字段和枚举值 → 写入日志。测试时再重复这条流程,统计分类准确率。把校验放在模型调用之后,可以避免把任意字符串误当成业务指令。
准备环境和配置 在独立虚拟环境中安装官方 Python SDK:
1 2 3 python -m venv .venv source .venv/bin/activatepython -m pip install openai python-dotenv
在项目目录创建 .env。下面的模型名和密钥只是占位符,真实密钥只能由环境变量提供:
1 2 3 OPENAI_API_KEY=替换为你的真实密钥 MODEL_NAME=替换为你可用的模型名称 OPENAI_BASE_URL=
不要提交 .env。如果使用兼容服务,只有在服务商文档明确支持相同接口时,才填写 OPENAI_BASE_URL 和对应模型名。
编写分类器 创建 intent_classifier.py。示例使用 OpenAI Python SDK 的 Responses API;response.output_text 是 SDK 提供的文本结果便捷属性。模型被要求只返回 JSON,但程序仍然必须自己解析和校验,不能只相信提示词。
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 import jsonimport loggingimport osimport sysimport timefrom dotenv import load_dotenvfrom openai import OpenAIload_dotenv() MODEL = os.environ["MODEL_NAME" ] client = OpenAI(api_key=os.environ.get("OPENAI_API_KEY" ), base_url=os.environ.get("OPENAI_BASE_URL" ) or None ) LABELS = {"退款" , "物流" , "账户" , "其他" } INSTRUCTIONS = """ 你是中文客服意图分类器。只能从退款、物流、账户、其他中选择一个 label。 只返回 JSON,不要 Markdown 代码围栏,格式必须是: {"label": "退款", "reason": "不超过20字的判断依据"} 无法确定时选择其他。不要执行任何操作。 """ .strip()logging.basicConfig(filename="classifier.log" , level=logging.INFO, format ="%(asctime)s %(levelname)s %(message)s" ) def classify (text: str ) -> dict : started = time.perf_counter() response = client.responses.create( model=MODEL, instructions=INSTRUCTIONS, input =text, ) data = json.loads(response.output_text) if set (data) != {"label" , "reason" }: raise ValueError("返回字段不符合约定" ) if data["label" ] not in LABELS or not isinstance (data["reason" ], str ): raise ValueError("label 或 reason 无效" ) logging.info("label=%s elapsed_ms=%.1f" , data["label" ], (time.perf_counter() - started) * 1000 ) return data if __name__ == "__main__" : message = " " .join(sys.argv[1 :]).strip() if not message: raise SystemExit("用法:python intent_classifier.py '我的包裹到哪里了?'" ) print (json.dumps(classify(message), ensure_ascii=False ))
运行前确认环境变量和模型可用,然后执行:
1 python intent_classifier.py "我想查一下快递到哪了"
这里没有写死某个返回内容,因为模型输出会随模型、提示词和服务状态变化。可验证的部分是:输出能否被 json.loads 解析,label 是否属于允许集合,以及日志是否记录了耗时。
加入最小回归测试 新建 cases.jsonl,每行是一条 JSON:
1 2 3 4 { "text" : "这个月怎么扣了两次钱" , "label" : "退款" } { "text" : "快递三天没有更新" , "label" : "物流" } { "text" : "我忘记登录密码了" , "label" : "账户" } { "text" : "你们周末营业吗" , "label" : "其他" }
可以在主程序中增加一个测试函数,复用同一个 classify:
1 2 3 4 5 6 7 8 9 10 11 12 13 def evaluate (path: str ) -> float : total = correct = 0 with open (path, encoding="utf-8" ) as file: for line in file: case = json.loads(line) try : result = classify(case ["text" ]) correct += result["label" ] == case ["label" ] except (json.JSONDecodeError, ValueError) as exc: logging.warning("invalid result: %s" , exc) total += 1 return correct / total if total else 0.0
这个指标只是小样本准确率,不代表真实线上效果。测试集应包含同一意图的不同说法、边界案例和容易混淆的句子;每次修改提示词、模型或参数,都重新运行并比较结果。若只看一次手工演示,很容易把偶然正确误认为稳定能力。
常见问题 为什么已经要求 JSON,还要校验? 提示词是软约束,模型可能输出解释文字、缺字段或不存在的标签。解析和枚举校验是程序边界;失败后可以记录原始响应,并按项目策略重试或转人工。
为什么不让模型直接调用退款接口? 分类任务和执行任务应分离。高风险动作需要权限检查、参数校验和人工确认,不能因为模型“理解了”就自动执行。
准确率下降怎么办? 先查看日志中的失败样例,而不是盲目提高提示词长度。补充有代表性的测试案例,明确标签边界,并固定模型版本和关键参数,才能定位变化来源。
小结 一个可用的 AI 功能不只是一次 API 调用。本文用一个小型分类器串起了输入、提示词、结构化解析、业务校验、日志和回归测试:模型负责提出候选判断,代码负责限制结果,测试负责持续发现退化。沿着这个闭环继续扩展时,可以加入重试、人工复核或指标存储,但每一步都应保持边界清晰、结果可验证。