LangChain4j 结构化输出(Structured Outputs)—— 小白也能懂的通俗版

LangChain4j 结构化输出(Structured Outputs)—— 小白也能懂的通俗版
先搞懂什么是结构化输出结构化输出就是让 LLM不只生成自由文本而是按照你指定的 JSON 格式返回数据方便直接映射到 Java 对象。打个比方普通的 LLM 像一个写作文的学生你说什么他就答什么结构化输出让他变成填表格的员工——表格里有几栏、每栏填什么类型他都严格按规矩来。⚡核心结论一句话想让 LLM 稳定返回结构化数据优先用 JSON Schema如果模型支持其次用提示 JSON 模式最后才用纯提示。在 AI Service 里只需把返回类型改成 POJO/List/Enum 等框架自动搞定一切。 三种方法可靠性排序排名方法可靠性适用场景JSON Schema最高 ✅推荐模型支持时首选提示 JSON 模式中等OpenAI response_format: json_object纯提示最低 fallback模型不支持前两种时的兜底方案一、JSON Schema —— 最可靠的方法原理LLM 提供商 API 有一个专用字段叫responseFormat你在里面指定一个完整的 JSON Schema。LLM 看到后必须按这个 schema 生成合法 JSON否则 API 会拒绝。关键点不需要在你的提示词里写任何格式要求schema 是通过 API 的专用参数传递的。支持的模型Azure OpenAI · Google AI Gemini · Mistral · Ollama · OpenAI在 ChatModel 中使用 JSON Schema完整示例从一段非结构化文本中提取 Person 对象// Step 1: 定义 JSON SchemaResponseFormatresponseFormatResponseFormat.builder().type(ResponseFormatType.JSON)// ← 告诉 LLM 我要 JSON.jsonSchema(JsonSchema.builder().name(Person)// ← OpenAI 需要指定名称.rootElement(JsonObjectSchema.builder().addStringProperty(name).addIntegerProperty(age).addNumberProperty(height).addBooleanProperty(married).required(name,age,height,married)// ← 必填项要显式声明.build()).build()).build();// Step 2: 准备用户消息UserMessageuserMessageUserMessage.from( John is 42 years old and lives an independent life. He stands 1.75 meters tall. Currently unmarried. );// Step 3: 发送请求ChatRequestchatRequestChatRequest.builder().responseFormat(responseFormat).messages(userMessage).build();ChatResponsechatResponsemodel.chat(chatRequest);StringoutputchatResponse.aiMessage().text();// 输出: {name:John,age:42,height:1.75,married:false}// Step 4: 解析为 Java 对象PersonpersonnewObjectMapper().readValue(output,Person.class);重要提醒根元素必须是JsonObjectSchemaGemini 除外它也允许 JsonEnumSchema 和 JsonArraySchema必填属性必须显式指定不指定的话会被当作可选JsonSchemaElement 类型速查表所有类型都继承自JsonSchemaElement接口JsonSchemaElement 子类对应 Java 类型快捷方法JsonObjectSchema自定义对象 / POJO.addStringProperty()等JsonStringSchemaString, char.addStringProperty(name)JsonIntegerSchemaint, long, BigInteger.addIntegerProperty(age)JsonNumberSchemafloat, double, BigDecimal.addNumberProperty(height)JsonBooleanSchemaboolean.addBooleanProperty(married)JsonEnumSchemaenum.addEnumProperty(status, List.of(...))JsonArraySchemaList, Set, Array.items(itemSchema)JsonReferenceSchema递归引用嵌套同类型对象JsonAnyOfSchema多态union typeCircle OR RectangleJsonNullSchema可空类型null 值支持JsonRawSchema原始 JSON Schema 字符串直接使用已有的 schema 文件JsonObjectSchema 的三种添加属性方式方式一批量添加 properties MapMapString,JsonSchemaElementpropsMap.of(city,JsonStringSchema.builder().description(城市名).build(),unit,JsonEnumSchema.builder().enumValues(CELSIUS,FAHRENHEIT).build());JsonObjectSchemarootJsonObjectSchema.builder().addProperties(props).required(city).build();方式二逐个添加 addPropertyJsonObjectSchemarootJsonObjectSchema.builder().addProperty(city,citySchema).addProperty(temperatureUnit,tempUnitSchema).required(city).build();方式三使用快捷方法 addXxxProperty最简洁JsonObjectSchemarootJsonObjectSchema.builder().addStringProperty(city,The city for the forecast).addEnumProperty(temperatureUnit,List.of(CELSIUS,FAHRENHEIT)).required(city).build();JsonReferenceSchema —— 支持递归比如 Person 类有SetPerson children字段Stringreferenceperson;// 引用名必须在 schema 中唯一JsonObjectSchemaschemaJsonObjectSchema.builder().addStringProperty(name).addProperty(children,JsonArraySchema.builder().items(JsonReferenceSchema.builder().reference(reference).build()).build()).required(name,children).definitions(Map.of(reference,/* 重复的定义 */)).build();注意目前仅 Azure OpenAI、Mistral 和 OpenAI 支持。JsonAnyOfSchema —— 支持多态性JsonSchemaElementcircleJsonObjectSchema.builder().addNumberProperty(radius).build();JsonSchemaElementrectJsonObjectSchema.builder().addNumberProperty(width).addNumberProperty(height).build();JsonSchemaElementshapeJsonAnyOfSchema.builder().anyOf(circle,rect).build();// 最终输出: {shapes:[{radius:5},{width:10,height:20}]}注意目前仅 OpenAI 和 Azure OpenAI 支持。JsonRawSchema —— 使用现成的 JSON Schema 字符串varrawSchema { $schema: http://json-schema.org/draft-07/schema#, type: object, properties: { city: { type: string } }, required: [city] } ;JsonRawSchemaschemaJsonRawSchema.from(rawSchema);添加描述 —— 提高成功率JsonStringSchema.builder().description(The name of the person, for example: John Doe).build();除 JsonReferenceSchema 外的所有类型都支持 description。ChatModel 中使用 JSON Schema 的限制限制说明模型支持仅 Azure OpenAI、Google AI Gemini、Mistral、Ollama、OpenAI流式模式❌ OpenAI 暂不支持JsonReferenceSchema仅 Azure OpenAI、Mistral、OpenAIJsonAnyOfSchema仅 OpenAI、Azure OpenAI各模型的启用方式模型启用方式OpenAI (新模型).supportedCapabilities(RESPONSE_FORMAT_JSON_SCHEMA).strictJsonSchema(true)OpenAI (旧模型).responseFormat(json_object)Azure OpenAI.responseFormat(new ChatCompletionsJsonResponseFormat())Vertex AI Gemini.responseMimeType(application/json)或.responseSchema(SchemaHelper.fromClass(Person.class))Google AI Gemini.responseFormat(ResponseFormat.JSON)二、在 AI Services 中使用 JSON Schema —— 一行代码搞定这才是真正爽的地方。不需要手动构建 JsonSchema只需要把方法返回类型改成你想要的 Java 类型在 ChatModel 配置中启用 JSON Schema 支持// Step 1: 定义接口返回类型就是你要的结构化数据interfacePersonExtractor{UserMessage(从以下文本中提取人员信息{{it}})PersonextractPersonFrom(Stringtext);}// Step 2: 创建模型并启用 JSON Schema 支持ChatModelchatModelOpenAiChatModel.builder().apiKey(System.getenv(OPENAI_API_KEY)).modelName(gpt-4o-mini).supportedCapabilities(RESPONSE_FORMAT_JSON_SCHEMA)// ← 关键.strictJsonSchema(true)// ← 严格模式.build();// Step 3: 创建 AI 服务PersonExtractorextractorAiServices.create(PersonExtractor.class,chatModel);// Step 4: 调用Personpersonextractor.extractPersonFrom( John is 42 years old. He stands 1.75m tall. Currently unmarried. );// person.nameJohn, age42, height1.75, marriedfalse框架会自动根据 Person 类的结构生成对应的 JSON Schema → 发送给 LLM → 将返回的 JSON 反序列化为 Person 对象。AI Services 中的限制限制说明必须显式启用.supportedCapabilities(RESPONSE_FORMAT_JSON_SCHEMA)流式模式❌ 不适用嵌套类型只能包含标量、enum、嵌套 POJO、List/Set/数组递归仅 Azure OpenAI、Mistral、OpenAI多态性❌ 尚不支持抽象类/接口❌ 必须是具体类三、提示 JSON 模式 纯提示提示 JSON 模式结合自然语言指令和 responseFormat 参数比纯提示更可靠但不如 JSON Schema 严格。纯提示兜底方案当 JSON Schema 不可用时AI Service 自动生成格式指令附加到 UserMessage 末尾然后尝试将 LLM 输出解析为目标类型。缺点是不可靠LLM 可能偶尔忽略格式要求。四、支持的类型对照表返回类型JSON Schema纯提示备注POJO✅✅最常用ListPOJO, SetPOJO✅❌不能用于纯提示Enum✅✅ListEnum, SetEnum✅✅ListString, SetString✅✅boolean, Boolean✅✅int, Integer✅✅long, Long✅✅float, Float✅✅double, Double✅✅byte, Byte❌✅仅提示short, Short❌✅仅提示BigInteger❌✅仅提示BigDecimal❌✅仅提示Date❌✅仅提示LocalDate❌✅仅提示LocalTime❌✅仅提示LocalDateTime❌✅仅提示Map?, ?❌✅仅提示五、实际例子一览// 提取单个 PersonPersonextractPersonFrom(Stringtext);// 提取多个 PersonSetPersonextractPeopleFrom(Stringtext);// 情感分类SentimentextractSentimentFrom(Stringtext);ListSentimentextractSentimentsFrom(Stringtext);// 生成大纲ListStringgenerateOutlineFor(Stringtopic); 面试高频追问Q1JSON Schema 和工具调用的区别是什么答JSON Schema 关注输出格式——强制 LLM 返回符合特定结构的 JSON 数据工具调用关注执行动作——让 LLM 决定是否需要调用外部函数/API。前者被动接收结构化数据后者主动触发操作。Q2为什么有些类型不支持 JSON Schema答因为 JSON Schema 规范本身有限制。比如 byte/short 在 JSON 中没有原生对应类型Date/LocalDateTime 需要先序列化为字符串。这些只能通过纯提示让 LLM猜格式。Q3AI Service 在 JSON Schema 不可用时怎么办答自动降级到纯提示模式——自动生成格式指令附加到 UserMessage 末尾收到 LLM 输出后尝试解析为目标类型。但这不够可靠最好确保模型支持 JSON Schema。Q4如何启用 JSON Schema 的严格模式答OpenAI 模型上设置.strictJsonSchema(true)迫使 LLM 严格按照 schema 定义生成输出不允许额外字段或不匹配的类型。Q5JsonReferenceSchema 和 JsonAnyOfSchema 的区别答JsonReferenceSchema 用于递归对象包含自身类型的子对象如 Person.childrenJsonAnyOfSchema 用于多态一个字段可以是多种不同类型之一如 Shape 可以是 Circle 或 Rectangle。✅ 总结结构化输出的三种方法按可靠性排序JSON Schema最强 ✅—— 通过 API 专用参数指定 schemaLLM 必须遵守提示 JSON 模式中等—— 结合自然语言指令和 responseFormat 参数纯提示兜底—— 只在提示词里写请用 JSON 格式回复在 AI Service 中使用 JSON Schema 非常简单定义接口返回类型为 POJO/List/Enum 等在 ChatModel 配置中启用 RESPONSE_FORMAT_JSON_SCHEMA框架自动生成 schema、发送请求、解析结果注意事项不是所有 Java 类型都支持 JSON Schemabyte/short/Date 等仅限提示模式流式模式下不支持 JSON Schema递归和多态性支持有限仅部分模型返回类型必须是具体类不支持接口或抽象类掌握结构化输出之后你可以轻松构建信息抽取、数据分类、文档摘要等各种需要 LLM 返回结构化数据的场景。