前面的系列已经分别介绍了 API 调用、提示词、结构化输出、错误处理、日志和评估。本篇把这些能力组合成一个小项目:输入一条中文客服消息,模型将它归入固定意图,程序校验结果并记录耗时;随后用一组带标签的样例做简单回归测试。重点不是追求复杂架构,而是建立“模型给候选、程序做约束、测试发现退化”的基本工程闭环。

先定义任务和边界

本项目只识别四种意图:退款物流账户其他。模型不能直接退款、查询订单或修改账户,它只返回分类结果和一句理由。真正的业务动作仍应由普通 Python 代码调用受控接口完成,这样即使模型判断错误,也不会直接造成外部副作用。

程序的数据流是:读取一条消息 → 组装提示词 → 调用模型 → 解析 JSON → 检查字段和枚举值 → 写入日志。测试时再重复这条流程,统计分类准确率。把校验放在模型调用之后,可以避免把任意字符串误当成业务指令。

准备环境和配置

在独立虚拟环境中安装官方 Python SDK:

1
2
3
python -m venv .venv
source .venv/bin/activate
python -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 json
import logging
import os
import sys
import time

from dotenv import load_dotenv
from openai import OpenAI

load_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 调用。本文用一个小型分类器串起了输入、提示词、结构化解析、业务校验、日志和回归测试:模型负责提出候选判断,代码负责限制结果,测试负责持续发现退化。沿着这个闭环继续扩展时,可以加入重试、人工复核或指标存储,但每一步都应保持边界清晰、结果可验证。