客服系统最危险的自动化,不是模型答错一句话,而是把"退款 9999 元"当成真指令一路执行。结构化输出(Structured Outputs)能要求模型按 JSON Schema 返回字段;但它不是业务真相。你还需要第二道本地门:金额、枚举值和缺失字段必须由程序复核。本篇用一个可复制的 Python 骨架完成这件事。
官方 Responses API 参考说明,json_schema 可用于受约束的 JSON 输出,且较旧的 json_object 不推荐优先使用。模型、权限与配额以你的账号为准。示例为了可离线验证,先用模拟响应;接入时只替换 call_model,密钥仅从环境变量读取。
#mermaid-svg-3EttiatIhgJhuPWx{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-3EttiatIhgJhuPWx .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-3EttiatIhgJhuPWx .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-3EttiatIhgJhuPWx .error-icon{fill:#552222;}#mermaid-svg-3EttiatIhgJhuPWx .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-3EttiatIhgJhuPWx .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-3EttiatIhgJhuPWx .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-3EttiatIhgJhuPWx .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-3EttiatIhgJhuPWx .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-3EttiatIhgJhuPWx .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-3EttiatIhgJhuPWx .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-3EttiatIhgJhuPWx .marker{fill:#333333;stroke:#333333;}#mermaid-svg-3EttiatIhgJhuPWx .marker.cross{stroke:#333333;}#mermaid-svg-3EttiatIhgJhuPWx svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-3EttiatIhgJhuPWx p{margin:0;}#mermaid-svg-3EttiatIhgJhuPWx .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-3EttiatIhgJhuPWx .cluster-label text{fill:#333;}#mermaid-svg-3EttiatIhgJhuPWx .cluster-label span{color:#333;}#mermaid-svg-3EttiatIhgJhuPWx .cluster-label span p{background-color:transparent;}#mermaid-svg-3EttiatIhgJhuPWx .label text,#mermaid-svg-3EttiatIhgJhuPWx span{fill:#333;color:#333;}#mermaid-svg-3EttiatIhgJhuPWx .node rect,#mermaid-svg-3EttiatIhgJhuPWx .node circle,#mermaid-svg-3EttiatIhgJhuPWx .node ellipse,#mermaid-svg-3EttiatIhgJhuPWx .node polygon,#mermaid-svg-3EttiatIhgJhuPWx .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-3EttiatIhgJhuPWx .rough-node .label text,#mermaid-svg-3EttiatIhgJhuPWx .node .label text,#mermaid-svg-3EttiatIhgJhuPWx .image-shape .label,#mermaid-svg-3EttiatIhgJhuPWx .icon-shape .label{text-anchor:middle;}#mermaid-svg-3EttiatIhgJhuPWx .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-3EttiatIhgJhuPWx .rough-node .label,#mermaid-svg-3EttiatIhgJhuPWx .node .label,#mermaid-svg-3EttiatIhgJhuPWx .image-shape .label,#mermaid-svg-3EttiatIhgJhuPWx .icon-shape .label{text-align:center;}#mermaid-svg-3EttiatIhgJhuPWx .node.clickable{cursor:pointer;}#mermaid-svg-3EttiatIhgJhuPWx .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-3EttiatIhgJhuPWx .arrowheadPath{fill:#333333;}#mermaid-svg-3EttiatIhgJhuPWx .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-3EttiatIhgJhuPWx .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-3EttiatIhgJhuPWx .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-3EttiatIhgJhuPWx .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-3EttiatIhgJhuPWx .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-3EttiatIhgJhuPWx .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-3EttiatIhgJhuPWx .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-3EttiatIhgJhuPWx .cluster text{fill:#333;}#mermaid-svg-3EttiatIhgJhuPWx .cluster span{color:#333;}#mermaid-svg-3EttiatIhgJhuPWx 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-3EttiatIhgJhuPWx .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-3EttiatIhgJhuPWx rect.text{fill:none;stroke-width:0;}#mermaid-svg-3EttiatIhgJhuPWx .icon-shape,#mermaid-svg-3EttiatIhgJhuPWx .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-3EttiatIhgJhuPWx .icon-shape p,#mermaid-svg-3EttiatIhgJhuPWx .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-3EttiatIhgJhuPWx .icon-shape .label rect,#mermaid-svg-3EttiatIhgJhuPWx .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-3EttiatIhgJhuPWx .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-3EttiatIhgJhuPWx .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-3EttiatIhgJhuPWx :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 通过
拒绝
用户文本
模型按 JSON Schema 提取
JSON 对象
本地字段/金额校验
进入人工队列
记录原因并回问
环境准备
需要 Python 3.10+。演示没有第三方依赖;真实请求可安装 pip install openai,并设置 export OPENAI_API_KEY='...'。不要把密钥写进源码。下面代码的超时参数放在真实调用处,避免网络问题无限等待。
python
import json, os
from decimal import Decimal
ALLOWED = {"refund", "shipping", "other"}
MAX_REFUND = Decimal("500.00")
SCHEMA = {"type": "object", "properties": {
"category": {"type": "string"}, "amount": {"type": "string"},
"summary": {"type": "string"}}, "required": ["category", "summary"]}
def call_model(text: str) -> dict:
# 真实接入:确认 os.getenv("OPENAI_API_KEY") 非空,
# 使用客户端 timeout=20,并传 text.format={type: "json_schema", ...}。
if not os.getenv("OPENAI_API_KEY"):
return {"category": "refund", "amount": "128.50", "summary": text}
raise RuntimeError("示例未在本次任务中实际调用线上 API")
def validate(ticket: dict) -> tuple[bool, str]:
if not isinstance(ticket.get("summary"), str) or not ticket["summary"].strip():
return False, "缺少摘要"
if ticket.get("category") not in ALLOWED:
return False, "未知分类"
if ticket.get("category") == "refund":
try:
amount = Decimal(ticket.get("amount", "-1"))
except Exception:
return False, "金额不是十进制数"
if not (Decimal("0") < amount <= MAX_REFUND):
return False, "金额超出自动队列范围"
return True, "QUEUED_FOR_REVIEW"
if __name__ == "__main__":
ticket = call_model("订单 A12 重复扣款,请退 128.50 元")
ok, reason = validate(ticket)
print(json.dumps({"ticket": ticket, "ok": ok, "reason": reason}, ensure_ascii=False))
运行 python ticket.py,在未设置密钥时预期看到 ok: true 和 QUEUED_FOR_REVIEW;它只进入人工队列,不会退款。关键点是 Schema 解决形状,本地 validate 解决业务边界。常见失败有三类:模型漏字段,要求 Schema 的 required 字段并回问;金额带"元"或逗号,拒绝后让模型只输出数字字符串;网络慢或 429,真实调用设 20 秒超时并指数退避,不要在超时后盲目重试有副作用的动作。
适合把自由文本变成工单、表单和审核草稿;不适合拿模型结果直接放款、删库或处理医疗结论。工程化时,为每条记录保存原文、模型版本、Schema 版本和校验结果;再用脱敏历史样本统计拒绝率。5 分钟实践:新增 priority 枚举,只让 high 工单走人工复核,写一个断言验证未知优先级会被拒绝。
你的工单系统里,哪一个字段绝不能只相信模型输出?
关注「蜗牛聊AI」,一起看懂技术变化背后的真正机会。
本文首发于 java4u.cn,转载请注明出处。