一、为什么“看起来是 JSON”仍然不够
在 OPC中国 的智能体实践中,常见场景是把一段自然语言转为可执行数据:从客户需求中生成工单、从会议纪要提取待办、从邮件中归类工单优先级。
最早的实现通常是提示模型“请返回 JSON”,然后直接 json.loads()。这能跑通演示,却很难支撑真实业务。原因至少有四个:
- 返回内容不一定是合法 JSON,可能混入解释文字或 Markdown;
- 合法 JSON 不代表字段正确,例如
priority被写成业务不认识的urgent; - 字段正确也不代表业务允许,例如截止日期早于今天;
- 即使模型输出可用,调用方仍应保留可追踪的原始输入、校验结果和最终动作。
因此,可靠链路的目标不是“让模型永远不犯错”,而是让错误在进入下游系统前被识别、纠正或明确拒绝。
阿里云百炼提供 JSON Mode,使模型返回可解析的标准 JSON 字符串。开启方式是在请求中设置 response_format={"type":"json_object"},且消息中必须包含 JSON 一词。阿里云百炼:结构化输出
二、推荐的五层防线
可以把一次结构化输出处理拆成五层:
| 层次 | 解决的问题 | 典型手段 |
| 输出约束 | 减少非 JSON 文本 | JSON Mode + 明确提示词 |
| 语法解析 | 判断内容能否被解析 | json.loads() |
| Schema 校验 | 字段、类型、枚举是否合法 | JSON Schema / Pydantic / 自定义校验 |
| 业务校验 | 数据在当前业务中是否允许 | 权限、日期、库存、状态机校验 |
| 失败处置 | 失败时如何恢复或停止 | 有限重试、人工确认、错误日志 |
这五层各自承担不同责任。模型负责生成候选值;服务端负责决定候选值能否被使用。不要把 Schema 当成业务规则的替代品,更不能让模型的输出直接触发写库、发消息或扣费等动作。
三、先定义“业务能接受什么”
以“从一句需求创建待办”为例,假设业务只接受下面三个字段:
{
"title": "整理客户需求",
"priority": "high",
"due_date": "2026-08-01"
}
一个足够小的 Schema 应明确:
title必须是非空文本;priority只能是low、medium、high;due_date必须是 ISO 日期;- 不接受未定义字段,避免模型悄悄增加
assignee、price等可能影响业务的内容。
这里有一个经验:字段数量越少、语义越单一,模型越稳定,后端也越容易维护。不要试图用一次生成填满工单的所有可选项;把不确定的信息留给后续流程确认,通常更安全。
四、最小可运行示例:解析、校验与一次纠错
下面的示例不依赖模型 API,专注于下游守卫逻辑。第一次候选结果把优先级写成 urgent,校验会拒绝;第二次候选结果才会被接受。
import json
from dataclasses import dataclass
from datetime import date
ALLOWED_PRIORITY = {"low", "medium", "high"}
@dataclass(frozen=True)
class Ticket:
title: str
priority: str
due_date: str
def validate_schema(payload: object) -> Ticket:
if not isinstance(payload, dict):
raise ValueError("根节点必须是对象")
if set(payload) != {"title", "priority", "due_date"}:
raise ValueError("字段必须且只能包含 title、priority、due_date")
if payload["priority"] not in ALLOWED_PRIORITY:
raise ValueError("priority 必须是 low、medium 或 high")
due = date.fromisoformat(payload["due_date"])
if due < date.today():
raise ValueError("due_date 不能早于今天")
return Ticket(**payload)
def parse_and_validate(raw: str) -> Ticket:
return validate_schema(json.loads(raw))
配套代码会按下面的方式运行:
attempt=1 rejected=priority 必须是 low、medium 或 high
attempt=2 accepted=Ticket(title='整理客户需求', priority='high', due_date='2026-08-01')
完整可运行文件见文末链接。示例中的校验比代码片段更严格:它还检查空标题、字段集合和日期格式。
五、在百炼中请求 JSON Mode
若使用 OpenAI 兼容接口,调用核心是把 response_format 加入请求,并在提示词明确要求输出 JSON。以下是精简示意;模型名称、地域域名和鉴权方式请以控制台及当前文档为准。
from openai import OpenAI
client = OpenAI(api_key="YOUR_API_KEY", base_url="YOUR_BASE_URL")
response = client.chat.completions.create(
model="YOUR_MODEL",
messages=[
{"role": "system", "content": "你是工单提取器,只输出 JSON。"},
{"role": "user", "content": "请在 2026-08-01 前整理客户需求,优先级高。"},
],
response_format={"type": "json_object"},
)
raw_json = response.choices[0].message.content
拿到 raw_json 后,仍应调用前节的 parse_and_validate()。阿里云文档同样建议使用 JSON Schema 等工具继续检查字段缺失、类型错误或格式问题。结构化输出校验建议
需要特别注意:JSON Mode 对提示词有约束,且部分“思考模式”与 JSON Mode 的兼容性有限。遇到错误时,不应靠盲目重试解决,先检查 response_format、提示词中的 JSON 关键词以及所选模型的能力说明。阿里云百炼错误码说明
六、纠错重试应该短、具体、可观测
校验失败后,可以把错误摘要连同原任务重新交给模型,例如:
上次 JSON 未通过校验:priority 必须是 low、medium 或 high。
请仅输出修正后的 JSON;字段必须且只能包含 title、priority、due_date。
这里有三个边界:
- 最多重试 1—2 次:无限重试会放大成本,也掩盖提示词或数据定义的问题;
- 只反馈可操作错误:给出“枚举不合法”比笼统说“格式不对”更有效;
- 记录失败样本:统计失败类型,才能判断该收紧 Schema、改提示词,还是调整前端输入。
重试后的内容必须重新走完解析、Schema 与业务校验;不能因为“已经重试过”就降低门槛。
七、业务校验:结构正确不等于可以执行
假设模型返回:
{"title":"关闭客户账号","priority":"high","due_date":"2026-08-01"}
从 Schema 看,它完全合法;从业务角度却可能是高风险操作。实际系统还应检查:调用者是否有权限、账号是否处于可关闭状态、是否需要二次确认、是否存在幂等键。
推荐做法是把模型输出先写入“待确认动作”或草稿表,再由确定性代码完成权限、状态与副作用检查。固定步骤多、约束强的任务,也可以交给工作流承载,以便让执行路径更可控、可复现。阿里云百炼将工作流定位为适合固定流程自动化的应用模式。应用类型介绍
八、上线前的最小测试集
不要只用一条成功样本验证结构化输出。至少准备以下用例:
| 用例 | 预期 |
| 正常输入 | 返回完整且可执行的数据 |
| 缺少关键信息 | 返回空值或待确认状态,不编造 |
| 非法枚举 | 被 Schema 拒绝并进入有限纠错 |
| 过期日期 | 被业务校验拒绝 |
| 高风险动作 | 不直接执行,转人工确认 |
| 多轮失败 | 输出明确失败原因并保留日志 |
用例不需要一开始很多,但应覆盖“能成功”和“必须失败”两类路径。后者往往决定系统能否安全进入生产环境。
结语
结构化输出的价值不在于让模型生成一段漂亮 JSON,而在于把自然语言能力接入确定性系统时,仍能保留边界、校验和追溯。
对 OPC中国 而言,这套做法尤其适合一人或小团队:先用一个窄场景跑通“生成—校验—纠错—确认”的闭环,再逐步增加字段和动作。少一些侥幸解析,多一些服务端守卫,智能体才真正能承担稳定工作。