让 LLM 输出能被程序消费的数据,是 Agent 系统的地基。这里的地基有三种浇法,可靠性完全不同。
三种方式,三种可靠性
第一种:在提示词里写"请输出 JSON"。 这是最常见的做法,也是最脆的。失败模式我全见过:首尾带 markdown 围栏、字段名大小写漂移、字符串里出现未转义引号、中文全角标点混入、以及"好的,以下是您要的 JSON:“这样的开场白。
第二种:Function Calling / Tool Use。 把输出目标声明为工具的参数 schema,由推理侧保证结构合法。这是当前的主流答案——模型在"填参数"而不是"写文章”,结构可靠性大幅提升。
第三种:受控解码(Structured Output / JSON Mode)。 在解码阶段用 schema 约束候选 token,语法上不可能产生非法输出。OpenAI 的 response_format: json_schema、各家推理引擎的 guided decoding(GBNF、XGrammar)都属于此类。可靠性最高,代价是schema 表达能力有限(复杂 oneOf、超深嵌套可能不支持)。
工程选型:优先受控解码,其次 tool call,最后才靠提示词。
校验 + 重试环:不信任何一层
即使有受控解码,语义校验依然必要——结构合法不代表值合法(枚举拼错、日期格式、数值范围)。标准姿势是 Pydantic 校验加带错误反馈的重试:
from pydantic import BaseModel, ValidationError, field_validator
class Review(BaseModel):
risk: str
confidence: float
@field_validator("confidence")
@classmethod
def clamp(cls, v: float) -> float:
assert 0.0 <= v <= 1.0, "confidence 必须在 0-1 之间"
return v
def extract(llm_call, text, retries=2):
for i in range(retries + 1):
try:
return Review.model_validate_json(llm_call(text))
except ValidationError as e:
text = f"{text}\n\n上次输出违反约束:{e}\n请修正后重新输出,仅输出 JSON。"
raise RuntimeError("结构化输出重试耗尽")
关键细节:重试时把校验错误原文回传给模型——模型修自己错误的能力远强于"再试一次"。
流式场景:部分解析
Agent 界面常需要边生成边渲染。JSON 流式解析的正确工具是增量解析器(partial JSON parser),按前缀解析出已闭合的字段先渲染,而不是等全文。长任务里这决定了体验:用户在 2 秒内看到第一个字段,还是 20 秒后看到全部。
Schema 设计的三条经验
- 字段名自解释:
risk_level优于rl——模型对语义化名称的遵从度明显更高 - 用枚举收窄自由度:能
"enum": ["high","medium","low"]就不要自由文本 - 禁止模型填你不想要的:加
"additionalProperties": false,并给可选字段默认值,避免模型硬填
结构化输出是 LLM 与程序之间的契约层。契约由 schema 定义、由解码保证、由校验兜底——三层都做,才敢让 Agent 的输出直接进生产链路。