结构化输出-让模型交回能用的JSON

结构化输出:让模型交回能用的 JSON

业务要的是一个 Bean,模型给的却是一段散文。于是手工抠 JSON,抠出过一个坏引号、一个尾逗号、一段「好的,以下是 JSON」的开场白,每次出错的方式都不重样。这件事有标准解法:把 JSON Schema 当接口文档递过去,把解析失败当业务异常处理,模型交回来的东西就能直接进 Bean。

两条路线

第一条,约束生成。请求里带上 response_format 一类的参数,声明「输出必须符合这个 schema」,模型在解码时就被语法约束住,出来的就是合法 JSON。这条路开箱最省事,只是各家参数名不同。
第二条,工具伪装。把「输出结果」定义成一个工具,参数的 input_schema 就是你要的结构,模型写工单时自然产出符合 schema 的 JSON。这条路通用性最强,因为工具调用的 schema 约束各家都有,而且和工单循环天然一体。

供应商支持约束生成

要通用、或顺手用工具链

没有

通过

要一个结构化结果

走哪条路

response_format + schema

工具伪装:input_schema 就是输出结构

JSON 解析 + Bean 校验

通过了吗

错误回填,重试

进业务

两条路殊途同归,出口都是同一个动作:解析、校验、失败重试。

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」的代码换成这个循环,代码少一半,错误还看得见。

上一篇: 从demo到生产-Agent上线前的检查清单
下一篇: 流式输出的Java实战-SSE在Spring里怎么接
Deng桑
博客等级 码龄1年 1粉丝 44原创
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

个

红包个数最小为10个

元

红包金额最低5元

当前余额3.43元 前往充值 >
需支付:10.00元
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付元
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值