结构化输出与函数调用
让模型产 JSON、它会愉快地产出“差不多 JSON”:一个尾逗号、一个不带引号的键、第一个花括号前一句友好的话。自由形式采样总能发出非法词元、因为采样器对你的 schema 一无所知。结构化输出合上这道缝、而函数调用是同一机制戴的另一顶帽子。
为什么自由形式 JSON 会坏
每个生成的词元都是从分布的一次采样。每一步都有一些概率质量——小、但温度高于 0 时永不为零——落在会破坏你格式的词元上:先闭后开的括号、数组最后一个元素后面的逗号、拼成 True 的小写 true(训练数据里烤进的 Python 习惯)。把“几乎总是有效”乘过 200 个输出词元、你得到间歇的、不可复现的破损——最坏的一种 bug。连温度 0 也只让输出确定、不保证 schema 有效。
解法阶梯
| 级 | 机制 | 保证 | 在哪 |
|---|---|---|---|
| 仅提示词("reply with valid JSON") | 好好请求 | 无——采样仍可能出错 | 任何引擎 |
| JSON 模式 | 提示词加限定 JSON 语法 | 语法上有效的 JSON;形状无保证 | OpenAI 兼容 API、Ollama format: "json" |
| Schema 约束 | Schema → 语法 → logits 掩码 | 输出永远解析并匹配 schema | llama.cpp(json_schema / GBNF)、vLLM guided_json、Ollama structured outputs、outlines / xgrammar |
每上一级花灵活性、买确定性。消费者是代码时就用顶级。
语法约束解码
把 schema 编译成语法。每个解码步、每个无法延展合法文档的词元在采样之前被把 logit 设为 −infinity。非法输出不是不太可能——是不可能的。llama.cpp 附带 GBNF 语法;十进制数语法就一行:
root ::= "-"? [0-9]+ ("." [0-9]+)?
同一想法包住 JSON Schema。对本地 llama.cpp 服务器(OpenAI 兼容端点):
import json, requests
schema = {
"type": "object",
"properties": {
"title": {"type": "string"},
"component": {"type": "string",
"enum": ["auth", "upload", "ui", "api"]},
"severity": {"type": "string",
"enum": ["low", "medium", "high"]}
},
"required": ["title", "component", "severity"],
"additionalProperties": False
}
r = requests.post("http://localhost:8080/v1/chat/completions", json={
"messages": [{"role": "user",
"content": "Extract the bug: " + report}],
"response_format": {
"type": "json_schema",
"json_schema": {"name": "bug", "strict": True,
"schema": schema}
},
"temperature": 0
})
bug = json.loads(r.json()["choices"][0]["message"]["content"])
Ollama 在 format 字段放 JSON Schema 做同一件事;vLLM 用 guided_json。同一机制、三种拼法——你引擎的确切字段名住在 引擎参数。
约束不等于正确
语法保证形状、不保证真相。模型可能为崩溃报告发 {"severity": "low"}、或用填充物塞满字符串字段。掩码也会伤开放字段:当模型想要段落换行而语法禁止换行时、质量下降。实用规则:
- 值空间已知时用窄枚举替代自由字符串。
- 你消费的每个字段都用
required;additionalProperties: false阻止发明。 - 字符串字段保持短;把长散文引出约束响应之外。
- 反正把一个期望输出的示例放进提示词——它引导内容、语法做不到。
函数与工具调用
工具调用是结构化输出加一个约定:模型返回调用哪个函数带什么参数;你的代码执行它并追加结果。OpenAI tools 形状是本地引擎模仿的事实方言:
{
"tools": [{
"type": "function",
"function": {
"name": "get_weather",
"description": "Current weather for a city",
"parameters": {
"type": "object",
"properties": {
"city": {"type": "string"},
"unit": {"type": "string", "enum": ["C", "F"]}
},
"required": ["city"]
}
}
}]
}
模型在消息里以 tool_calls 回复;你把输出作为 role: "tool" 消息返回、循环继续。模型为特定工具方言受训——Hermes 和 Qwen 血统模板在本地引擎上支持最好(llama.cpp 带 --jinja、Ollama、vLLM 带自动工具选择)。没受工具训练的模型无论你怎么约束都产出带幻觉参数名的可信调用、所以先为工作挑模型:选择模型。
同时校验解析和内容
每次调用后两道检查。第一、解析——json.loads 成功(语法通常保证这个;JSON 模式不总是)。第二、语义:必需键在场、枚举在域内、数字合理、字符串非空。失败时、带着喂回提示词的校验错误重试一次——约束重试便宜且通常一次弹跳就成功。
正是这个循环——发一个调用、校验、执行、观察——是智能体整天的营生:智能体。