客服工单最怕的不是模型"答错一句话",而是它给出一段看起来合理的说明,程序却从中猜错优先级。通俗做法是:要求模型只交 JSON(JavaScript Object Notation,轻量数据格式),再让代码验证它。Gemini API 的 Structured Outputs 能按 JSON Schema 生成结果;本篇用 Python 把退款、账号和普通咨询分流,并在模型输出越界时拒绝自动流转。你会得到一条能复制、能超时、能降级的最小链路。
为什么不直接解析一段自然语言
结构化输出的作用是规定"输出长什么样",不是保证事实正确。Schema 让字段和枚举更稳定;业务规则仍要由服务端负责。本文把 priority 限在 low、normal、high,把 queue 限在三个队列。模型若返回未知值,程序转到人工队列。当前官方文档把它用于抽取、分类和 Agent 工作流,并通过 response_format 的 application/json 与 schema 配置。
#mermaid-svg-CjF8aEJ6kPGLcEw9{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-CjF8aEJ6kPGLcEw9 .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-CjF8aEJ6kPGLcEw9 .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-CjF8aEJ6kPGLcEw9 .error-icon{fill:#552222;}#mermaid-svg-CjF8aEJ6kPGLcEw9 .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-CjF8aEJ6kPGLcEw9 .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-CjF8aEJ6kPGLcEw9 .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-CjF8aEJ6kPGLcEw9 .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-CjF8aEJ6kPGLcEw9 .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-CjF8aEJ6kPGLcEw9 .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-CjF8aEJ6kPGLcEw9 .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-CjF8aEJ6kPGLcEw9 .marker{fill:#333333;stroke:#333333;}#mermaid-svg-CjF8aEJ6kPGLcEw9 .marker.cross{stroke:#333333;}#mermaid-svg-CjF8aEJ6kPGLcEw9 svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-CjF8aEJ6kPGLcEw9 p{margin:0;}#mermaid-svg-CjF8aEJ6kPGLcEw9 .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-CjF8aEJ6kPGLcEw9 .cluster-label text{fill:#333;}#mermaid-svg-CjF8aEJ6kPGLcEw9 .cluster-label span{color:#333;}#mermaid-svg-CjF8aEJ6kPGLcEw9 .cluster-label span p{background-color:transparent;}#mermaid-svg-CjF8aEJ6kPGLcEw9 .label text,#mermaid-svg-CjF8aEJ6kPGLcEw9 span{fill:#333;color:#333;}#mermaid-svg-CjF8aEJ6kPGLcEw9 .node rect,#mermaid-svg-CjF8aEJ6kPGLcEw9 .node circle,#mermaid-svg-CjF8aEJ6kPGLcEw9 .node ellipse,#mermaid-svg-CjF8aEJ6kPGLcEw9 .node polygon,#mermaid-svg-CjF8aEJ6kPGLcEw9 .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-CjF8aEJ6kPGLcEw9 .rough-node .label text,#mermaid-svg-CjF8aEJ6kPGLcEw9 .node .label text,#mermaid-svg-CjF8aEJ6kPGLcEw9 .image-shape .label,#mermaid-svg-CjF8aEJ6kPGLcEw9 .icon-shape .label{text-anchor:middle;}#mermaid-svg-CjF8aEJ6kPGLcEw9 .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-CjF8aEJ6kPGLcEw9 .rough-node .label,#mermaid-svg-CjF8aEJ6kPGLcEw9 .node .label,#mermaid-svg-CjF8aEJ6kPGLcEw9 .image-shape .label,#mermaid-svg-CjF8aEJ6kPGLcEw9 .icon-shape .label{text-align:center;}#mermaid-svg-CjF8aEJ6kPGLcEw9 .node.clickable{cursor:pointer;}#mermaid-svg-CjF8aEJ6kPGLcEw9 .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-CjF8aEJ6kPGLcEw9 .arrowheadPath{fill:#333333;}#mermaid-svg-CjF8aEJ6kPGLcEw9 .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-CjF8aEJ6kPGLcEw9 .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-CjF8aEJ6kPGLcEw9 .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-CjF8aEJ6kPGLcEw9 .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-CjF8aEJ6kPGLcEw9 .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-CjF8aEJ6kPGLcEw9 .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-CjF8aEJ6kPGLcEw9 .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-CjF8aEJ6kPGLcEw9 .cluster text{fill:#333;}#mermaid-svg-CjF8aEJ6kPGLcEw9 .cluster span{color:#333;}#mermaid-svg-CjF8aEJ6kPGLcEw9 div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-CjF8aEJ6kPGLcEw9 .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-CjF8aEJ6kPGLcEw9 rect.text{fill:none;stroke-width:0;}#mermaid-svg-CjF8aEJ6kPGLcEw9 .icon-shape,#mermaid-svg-CjF8aEJ6kPGLcEw9 .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-CjF8aEJ6kPGLcEw9 .icon-shape p,#mermaid-svg-CjF8aEJ6kPGLcEw9 .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-CjF8aEJ6kPGLcEw9 .icon-shape .label rect,#mermaid-svg-CjF8aEJ6kPGLcEw9 .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-CjF8aEJ6kPGLcEw9 .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-CjF8aEJ6kPGLcEw9 .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-CjF8aEJ6kPGLcEw9 :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 通过
失败/超时
工单文本
Gemini JSON Schema
解析JSON
枚举与业务规则
进入对应队列
人工复核队列
环境准备
使用 Python 3.10+。安装依赖:python -m pip install google-genai。把密钥放入环境变量:export GEMINI_API_KEY='你的密钥'。模型名示例为文档中的 gemini-3.8-flash;你的账号可用模型与配额仍需在控制台确认。保存为 ticket_router.py,运行 python ticket_router.py。示例未在本次任务中实际调用线上 API;本地仅完成语法检查,目标环境安装依赖后应再作一次无敏感数据的联调。
python
import json
import os
from google import genai
ALLOWED_QUEUES = {"refund", "account", "general"}
ALLOWED_PRIORITY = {"low", "normal", "high"}
SCHEMA = {"type": "object", "properties": {
"queue": {"type": "string", "enum": sorted(ALLOWED_QUEUES)},
"priority": {"type": "string", "enum": sorted(ALLOWED_PRIORITY)},
"reason": {"type": "string"}},
"required": ["queue", "priority", "reason"]}
def safe_route(raw: str) -> dict:
try:
data = json.loads(raw)
except json.JSONDecodeError:
return {"queue": "human_review", "reason": "JSON无法解析"}
if data.get("queue") not in ALLOWED_QUEUES:
return {"queue": "human_review", "reason": "未知队列"}
if data.get("priority") not in ALLOWED_PRIORITY:
return {"queue": "human_review", "reason": "未知优先级"}
return data
def classify(ticket: str) -> dict:
if not os.getenv("GEMINI_API_KEY"):
return {"queue": "human_review", "reason": "缺少GEMINI_API_KEY"}
client = genai.Client()
try:
result = client.interactions.create(
model="gemini-3.8-flash", input=ticket,
response_format={"type": "text", "mime_type": "application/json",
"schema": SCHEMA}, timeout=20)
return safe_route(result.output_text)
except Exception as exc:
return {"queue": "human_review", "reason": f"调用失败: {type(exc).__name__}"}
if __name__ == "__main__":
print(classify("订单重复扣款,请尽快退款"))
assert safe_route('{"queue":"refund","priority":"high","reason":"重复扣款"}')["queue"] == "refund"
代码分两层:SCHEMA 提前缩小模型输出空间;safe_route 再做独立验证,不能因为 API 承诺 JSON 就省略它。timeout=20 是为了不让一个慢请求占住 Web 工作线程;生产环境还应设置 HTTP 客户端级超时、重试次数和熔断。genai.Client() 从环境读取密钥,不要把密钥写进仓库。
预期输出(有密钥且调用成功时)类似 {'queue': 'refund', 'priority': 'high', 'reason': '...'};没配密钥则是人工复核。最常见的坑有:一,Schema 只约束格式,不会自动核验订单真伪;二,枚举改了却没同步服务端白名单;三,超时后盲目重试,造成同一工单被重复创建;四,把模型理由直接展示给用户而泄露内部规则。
适合把"先分给谁处理"自动化,不适合直接批准退款或关闭投诉。工程化时,把原始文本、模型版本、schema 版本、路由结果和人工改判一起记录;每周抽样计算错分率。5分钟实践:新增 security 队列,并写一个断言确保模型返回 delete_database 时永远进入 human_review。
你的工单里,哪一个字段一旦分类错就不该自动流转?
关注「蜗牛聊AI」,一起看懂技术变化背后的真正机会。
本文首发于 java4u.cn,转载请注明出处。