结构化输出:让模型交回能用的 JSON
业务要的是一个 Bean,模型给的却是一段散文。于是手工抠 JSON,抠出过一个坏引号、一个尾逗号、一段「好的,以下是 JSON」的开场白,每次出错的方式都不重样。这件事有标准解法:把 JSON Schema 当接口文档递过去,把解析失败当业务异常处理,模型交回来的东西就能直接进 Bean。
两条路线
第一条,约束生成。请求里带上 response_format 一类的参数,声明「输出必须符合这个 schema」,模型在解码时就被语法约束住,出来的就是合法 JSON。这条路开箱最省事,只是各家参数名不同。
第二条,工具伪装。把「输出结果」定义成一个工具,参数的 input_schema 就是你要的结构,模型写工单时自然产出符合 schema 的 JSON。这条路通用性最强,因为工具调用的 schema 约束各家都有,而且和工单循环天然一体。
两条路殊途同归,出口都是同一个动作:解析、校验、失败重试。
Java 侧的三件事
解析用库。Jackson 或 Gson 把 JSON 绑到 Bean,别手写字符串切割,坏引号的教训一次就够。
校验用 Bean Validation。@NotNull、@Pattern 这些注解就是 schema 在 Java 侧的对应物,模型说格式对不对不算数,你的校验器说了算。
重试要带错误回填。解析或校验失败时,把「缺哪个字段、哪个格式不对」包进下一轮消息发回去,模型看到具体错误后改对的概率远高于「再来一次」。这和工单循环里「错误包成 tool_result」是同一个动作。
亲手跑
下面用一段 Java 演示完整的「解析、校验、失败重试」循环:mock 模型第一次交回缺字段的 JSON,错误回填后第二次改对。直接运行 java StructuredOut.java(JDK 11+)。
// 直接运行:java StructuredOut.java
// 结构化输出的重试循环:解析、校验、错误回填。
public class StructuredOut {
record WeatherResult(String city, Integer temperature, String condition) {
}
// mock 模型:第一次缺字段,收到错误后改对(模拟真实模型的纠错行为)
static String mockModel(int attempt) {
if (attempt == 1) {
return "{\"city\": \"北京\", \"condition\": \"晴\"}"; // 缺 temperature
}
return "{\"city\": \"北京\", \"temperature\": 25, \"condition\": \"晴\"}";
}
// 演示级解析,生产用 Jackson
static WeatherResult parse(String json) {
String city = extract(json, "city");
String temp = extract(json, "temperature");
String condition = extract(json, "condition");
if (city == null || temp == null || condition == null) {
throw new IllegalArgumentException(
"缺字段:" + (city == null ? "city " : "")
+ (temp == null ? "temperature " : "")
+ (condition == null ? "condition" : ""));
}
return new WeatherResult(city, Integer.valueOf(temp), condition);
}
static String extract(String json, String key) {
int i = json.indexOf("\"" + key + "\"");
if (i < 0) {
return null;
}
int colon = json.indexOf(":", i);
int end = json.indexOf(",", colon);
if (end < 0) {
end = json.indexOf("}", colon);
}
return json.substring(colon + 1, end).replace("\"", "").trim();
}
public static void main(String[] args) {
String feedback = "";
for (int attempt = 1; attempt <= 3; attempt++) {
String raw = mockModel(attempt);
System.out.println("第 " + attempt + " 次收到:" + raw + feedback);
try {
WeatherResult result = parse(raw);
System.out.println("解析成功,进业务:" + result);
return;
} catch (IllegalArgumentException e) {
feedback = " <- 上次的错误:" + e.getMessage() + ",请补全后重发";
System.out.println("校验失败:" + e.getMessage() + ",错误回填给模型");
}
}
System.out.println("重试用尽,降级走兜底逻辑。");
}
}
跑完看第二次的输出,错误信息回填之后模型补上了 temperature,这就是「带错误的重试」和「盲目重试」的差别。记住一句话:把 JSON Schema 当接口文档,把解析失败当业务异常。
适用边界
这一篇讲的是结构化输出的闭环。流式场景下的增量 JSON 解析、嵌套数组的 schema 设计,以及各供应商 response_format 的字段差异,是另外的题。演示里的字符串解析只图看清形状,生产环境请用 JSON 库。跑完把你手里最脏的那段「从模型输出里抠 JSON」的代码换成这个循环,代码少一半,错误还看得见。



被折叠的 条评论
为什么被折叠?



