在模型应用的早期阶段,把自然语言回答显示在页面上通常已经足够。但一旦模型参与邮件分类、工单分派、内容审核、数据抽取或自动化编排,输出就不再只是给人阅读的文本,而是下游程序要直接消费的数据。
这时,最常见的问题并不是模型完全没有理解任务,而是结果不满足程序约定:字段缺失、枚举值拼写不一致、数字被包装成字符串、JSON 外混入解释文字,或者模型返回了语法正确但业务上不允许的内容。仅靠提示词要求“请严格输出 JSON”并不能形成可靠的接口契约。
更稳妥的做法是把模型调用看作一个不完全可靠的外部服务,在边界建立四层保护:请求约束、语法解析、结构校验和业务判定。校验失败时只进行有限重试;仍然失败,则进入明确的人工复核或规则降级路径。
核心原理
结构化输出包含三个不同层次,不能混为一谈。
- 序列化正确:响应可以被 JSON 解析器读取。
- 结构正确:字段类型、必填项、数组元素和枚举值符合 Schema。
- 业务正确:例如分类结果与邮件内容相符,置信度不能替代证据,敏感操作还要经过人工审批。
JSON Schema 主要解决第二层问题。它能描述对象属性、字段类型、必填字段以及取值范围,但不能证明模型的结论真实,也不能替代权限检查和业务规则。因此,应用应当先解析,再用 Schema 校验,最后执行业务规则。
重试也需要有边界。语法错误或字段缺失通常适合让模型按照错误信息重新生成;认证失败、请求被拒绝、配额耗尽或服务不可用,则应根据错误类型采用退避、切换供应商或直接降级。无条件重试会放大延迟、费用和重复副作用。
在 API 接入层,可以使用 HaerAPI 作为一种待评估的模型接口来源,但实际请求格式、兼容范围、可用模型和错误语义必须以其当前文档为准。
设计一个可验证契约
下面以“工单分类”为例。分类服务只允许返回三个类别,并要求给出简短理由和 0 到 1 之间的置信度。示例中的模型地址和密钥均通过环境变量提供,未假定某个供应商一定支持特定的结构化输出参数。
建议先定义一个与业务无关的稳定数据模型:
from pydantic import BaseModel, Field
from typing import Literal
class TicketResult(BaseModel):
category: Literal["billing", "technical", "other"]
confidence: float = Field(ge=0.0, le=1.0)
reason: str = Field(min_length=1, max_length=300)
schema = TicketResult.model_json_schema()
这里的 Literal 限制枚举值,Field 限制数值和文本边界。Schema 应该由代码模型生成,而不是在多个文件里手工维护,否则字段变更容易只改了一处。
如果使用的模型 API 支持 JSON Schema 或工具调用,应在请求中传入该契约;如果只支持普通文本生成,则应把 Schema 的关键约束写入提示词,并把本地校验视为必经步骤。两种方式都不能省略服务端校验。
可执行实现
以下代码使用 Python 标准库完成 HTTP 请求,用 Pydantic 做结果校验。接口路径、请求字段和响应字段采用常见的 OpenAI 兼容形态仅作示例;部署前必须按照实际 API 文档调整。
import json
import os
import time
from urllib.request import Request, urlopen
from urllib.error import HTTPError, URLError
from pydantic import ValidationError
API_BASE = os.environ["MODEL_API_BASE"].rstrip("/")
API_KEY = os.environ["MODEL_API_KEY"]
MODEL = os.environ["MODEL_NAME"]
SYSTEM = """你是工单分类器。只返回 JSON 对象,不要输出 Markdown 或额外解释。
category 只能是 billing、technical、other;confidence 是 0 到 1 的数字;
reason 是不超过 300 字的分类依据。"""
def call_model(ticket: str, repair: str = "") -> dict:
payload = {
"model": MODEL,
"temperature": 0,
"messages": [
{"role": "system", "content": SYSTEM},
{"role": "user", "content": f"工单内容:{ticket}\n{repair}"}
]
}
request = Request(
f"{API_BASE}/chat/completions",
data=json.dumps(payload).encode("utf-8"),
headers={
"Authorization": f"Bearer {API_KEY}",
"Content-Type": "application/json"
},
method="POST"
)
with urlopen(request, timeout=30) as response:
body = json.loads(response.read().decode("utf-8"))
content = body["choices"][0]["message"]["content"]
return json.loads(content)
def classify(ticket: str) -> TicketResult | None:
repair = ""
for attempt in range(2):
try:
raw = call_model(ticket, repair)
result = TicketResult.model_validate(raw)
if result.category == "other" and result.confidence > 0.9:
# 示例业务规则:高置信度的兜底分类需要人工确认
return None
return result
except (json.JSONDecodeError, ValidationError) as exc:
repair = (
"上一次结果未通过校验。只修复格式和字段约束,"
f"不要增加字段。校验错误摘要:{str(exc)[:500]}"
)
time.sleep(0.5 * (attempt + 1))
except (HTTPError, URLError, TimeoutError):
break
return None
result = classify("本月账单重复扣款,请核对并退款")
if result is None:
print("进入人工复核或规则队列")
else:
print(result.model_dump_json())
安装依赖并设置环境变量:
python -m pip install pydantic
export MODEL_API_BASE="https://example.invalid/v1"
export MODEL_API_KEY="replace-with-an-environment-secret"
export MODEL_NAME="replace-with-a-documented-model"
python app.py
示例中的地址、模型名和密钥只是占位符。不要把密钥写入源代码、镜像层、前端代码或日志;生产环境还应使用密钥管理系统,并限制出站请求的目标范围。
让重试真正可控
第一,按错误类型分类。解析失败和 Schema 失败可以重试一次;超时是否重试要结合请求是否已经在服务端执行;认证错误不应重试;限流错误可根据响应提供的等待时间退避。实际错误码和响应头要以服务文档为准。
第二,重试提示词只携带必要的错误摘要。不要把完整响应、用户隐私或内部堆栈原样发回模型。错误信息也应截断长度,避免修复请求反而消耗大量上下文。
第三,处理重复副作用。分类本身通常是幂等的,但“模型判断后自动退款”“自动发送邮件”等动作不是。应给每个业务请求建立唯一 request_id,把模型结果和动作状态持久化,在执行动作前检查幂等键,并让高风险动作经过人工审批。
第四,记录可审计字段:请求 ID、模型标识、Schema 版本、校验结果、重试次数、耗时、降级原因和脱敏后的输入摘要。不要默认记录完整的用户内容,日志留存期限也应符合组织的数据政策。
常见问题
只使用正则表达式提取 JSON 可以吗? 不建议作为主方案。模型可能输出嵌套对象、转义字符或多个代码片段,正则很容易误截断。应优先使用 JSON 解析器,再进行 Schema 校验。
temperature=0 是否保证结果完全一致? 不能据此承诺完全一致。采样参数只是影响因素之一,模型服务、请求并发和后端实现也可能影响结果。因此仍需校验、幂等和降级。
Schema 校验通过就能自动执行吗? 不能。通过只说明形状和部分范围符合约定,仍要检查权限、资源状态、业务规则、敏感信息和人工审批条件。
为什么不无限重试? 因为失败可能来自服务不可用、契约不匹配或输入本身无法判定。无限重试会造成延迟和成本不可控,还可能重复触发外部动作。生产系统应设置最大次数、总超时和熔断策略。
模型不支持原生结构化输出怎么办? 可以使用严格提示词加本地解析校验,但可靠性取决于模型和任务复杂度。对高风险流程,应增加规则抽取、人工复核或改用明确支持结构化约束的接口,并在上线前用真实脱敏样本验证。
总结
模型输出要进入软件系统,关键不是把提示词写得更长,而是建立可验证的边界:用数据模型定义契约,用解析器处理语法,用 Schema 检查结构,用业务规则判断是否允许执行,再用有限重试、幂等和降级保证链路可恢复。
这套方法也让模型供应商更容易替换。应用只依赖内部统一的数据模型和错误语义,具体 API 的请求格式、模型能力和限流策略集中在适配层处理。最终,模型负责生成候选结果,系统负责验证、授权、记录和决定是否执行。