当我们在做大模型相关的对话的时候,模型的回答格式是不固定的,如果是人看的话,都是可以看懂的,但是如果我们靠代码来处理这些结果的话就比较费劲了。所以,在大模型的应用开发中,结构化输出至关重要。
一、什么是结构化输出
所谓结构化输出,就是让大模型按照我们要求的格式来做输出。比较常用的格式就是 JSON,因为他最灵活。比如:
请给我推荐几本 Java 相关的书,用 JSON 格式输出,包括书名、作者名、价格
{"java_books_recommendation": [{"title": "Effective Java 中文版(原书第3版)","author": "[美] 约书亚·布洛克(Joshua Bloch)","price_cny": 119,"publisher": "机械工业出版社","level": "中高级","description": "Java之父强推,90条编写高质量代码的最佳实践"},{"title": "Java核心技术·卷I(原书第12版)","author": "[美] 凯·S.霍斯特曼(Cay S. Horstmann)","price_cny": 149,"publisher": "机械工业出版社","level": "入门-进阶","description": "Java领域的'新华字典',涵盖Java 17新特性"},{"title": "深入理解Java虚拟机(第3版)","author": "周志明","price_cny": 129,"publisher": "机械工业出版社","level": "中高级","description": "国产JVM经典,详解内存模型、垃圾收集、类加载机制"}],"note": "价格为参考定价(人民币),实际购买时电商平台常有折扣优惠。建议根据当前学习阶段选择对应难度的书籍。[[1]][[21]][[32]]"}
二、Spring AI 实现方案
StructuredOutputConverter 接口
在 Spring AI 中,为了方便我们做结构化输出,也提供了一个 StructuredOutputConverter 的接口:
//// Source code recreated from a .class file by IntelliJ IDEA// (powered by FernFlower decompiler)//package org.springframework.ai.converter;import org.springframework.core.convert.converter.Converter;public interface StructuredOutputConverter<T> extends Converter<String, T>, FormatProvider {}
他的原理很简单,在 LLM 调用前,Converter 向提示中添加格式指令,为模型提供明确的指导,以生成所需的输出结构。
在 LLM 调用后,Converter 将模型的输出文本转换为结构化类型的示例。此转换过程涉及解析原始文本输出并将其映射到相应的结构化数据表示,例如 JSON、XML 或特定领域的数据结构。
以上是 Spring AI 的文档中写的,当然,后面我们会通过代码带大家看看具体实现。
BeanOutputConverter 原理
BeanOutputConverter 这个类中重写了一个 getFormat 方法,可以看到方法定义:
/*** 提供预期的响应格式约束说明,指示大语言模型必须严格遵循生成的 JSON Schema 进行输出。* 该提示词旨在消除模型的自由发挥,确保返回纯净、可直接被代码解析的 JSON 数据。** @return 包含格式要求的提示词(Prompt)字符串*/@Overridepublic String getFormat() {// 定义提示词模板,用于强制规范 LLM 的输出行为String template = """Your response should be in JSON format.Do not include any explanations, only provide a RFC8259 compliant JSON response following this format without deviation.Do not include markdown code blocks in your response.Remove the ```json markdown from the output.Here is the JSON Schema instance your output must adhere to:```%s```""";// 将当前实例持有的 JSON Schema 字符串注入到 %s 占位符中,// 拼接成完整的格式约束指令并返回,供 Spring AI 发送给模型return String.format(template, this.jsonSchema);}
补充说明(Spring AI 上下文):防 Markdown 包裹:提示词中明确要求 Do not include markdown code blocks 和 Remove the ``json,是因为多数 LLM 默认会用 ``json ... ``` 包裹输出。这会导致下游的Jackson或Gson解析器直接报错。该写法是 Spring AI 中保证BeanOutputConverter/JsonOutputConverter 稳定工作的标准实践。
通过DEBUG就可以知道它的占位符:
Your response should be in JSON format.Do not include any explanations, only provide a RFC8259 compliant JSON response following this format without deviation.Do not include markdown code blocks in your response.Remove the ```json markdown from the output.Here is the JSON Schema instance your output must adhere to:```{"$schema" : "https://json-schema.org/draft/2020-12/schema","type" : "object","additionalProperties" : false}```
BeanOutputConverter 如何使用
那么,在代码中我们如何把这段提示词加到我们的 Prompt 呢,很简单,用之前讲的提示词模板就可以了。
单个对象转换
@Testvoiddemo1_SingleObjectConversion() {System.out.println("========== 示例1:单个对象转换 ==========");ChatClient chatClient = chatClientBuilder.build();// 创建转换器,指定目标类型BeanOutputConverter<User> converter = new BeanOutputConverter<>(User.class);// 构建提示词,要求返回 JSON 格式String prompt = """请根据以下描述创建一个用户信息,并以 JSON 格式返回:姓名:张三年龄:28岁邮箱:zhangsan@example.com电话:13800138000城市:北京请只返回 JSON 格式的数据,不要添加其他说明。""";System.out.println("\n发送的提示词:\n" + prompt);// 调用 AI 并转换为对象User user = chatClient.prompt().user(prompt).call().entity(converter);System.out.println("\n转换后的对象:");System.out.println("• 姓名: " + user.getName());System.out.println("• 年龄: " + user.getAge());System.out.println("• 邮箱: " + user.getEmail());System.out.println("• 电话: " + user.getPhone());System.out.println("• 城市: " + user.getCity());System.out.println("\n💡 说明:");System.out.println("• BeanOutputConverter 自动处理 JSON 解析");System.out.println("• entity() 方法直接返回 Java 对象");System.out.println("• 无需手动解析 JSON 字符串");}
执行结果:
========== 示例1:单个对象转换 ==========发送的提示词:请根据以下描述创建一个用户信息,并以 JSON 格式返回:姓名:张三年龄:28岁邮箱:zhangsan@example.com电话:13800138000城市:北京请只返回 JSON 格式的数据,不要添加其他说明。转换后的对象:• 姓名: 张三• 年龄: 28• 邮箱: zhangsan@example.com• 电话: 13800138000• 城市: 北京💡 说明:• BeanOutputConverter 自动处理 JSON 解析• entity() 方法直接返回 Java 对象• 无需手动解析 JSON 字符串Process finished with exit code 0
使用 PromptTemplate + BeanOutputConverter
@Testvoid withPromptTemplateAndConverter() {ChatClient chatClient = chatClientBuilder.build();// 创建转换器BeanOutputConverter<Book> converter = new BeanOutputConverter<>(Book.class);// 创建提示词模板(从文件加载或硬编码)String templateText = """请生成一本关于{topic}的技术书籍信息,以 JSON 格式返回。要求:- title: 书名(要有吸引力)- author: 作者(该领域的专家)- price: 价格(50-150元之间的数字)- isbn: ISBN号(真实格式,如 978-7-xxx-xxxxx-x)- description: 简介(突出书籍特色,50字以内)返回格式示例:{{"title": "书名","author": "作者","price": 99.0,"isbn": "978-7-111-11111-1","description": "简介"}}请只返回 JSON 数据,不要添加其他说明。""";// 使用 Map 替换变量Map<String, Object> variables = Map.of("topic", "微服务架构");// 手动替换模板变量(或使用 PromptTemplate)String prompt = templateText.replace("{topic}", variables.get("topic").toString());System.out.println("\n生成的提示词:\n" + prompt);// 调用 AI 并转换为对象Book book = chatClient.prompt().user(prompt).call().entity(converter);System.out.println("\n生成的图书信息:");System.out.println("• 书名: " + book.getTitle());System.out.println("• 作者: " + book.getAuthor());System.out.println("• 价格: ¥" + book.getPrice());System.out.println("• ISBN: " + book.getIsbn());System.out.println("• 简介: " + book.getDescription());System.out.println("\n\n💡 说明:");System.out.println("• PromptTemplate 负责动态生成提示词");System.out.println("• BeanOutputConverter 负责解析 JSON 响应");System.out.println("• 两者结合实现灵活的结构化输出");System.out.println("• 可以在模板中包含 JSON 格式示例");}
执行结果:
生成的提示词:请生成一本关于微服务架构的技术书籍信息,以 JSON 格式返回。要求:- title: 书名(要有吸引力)- author: 作者(该领域的专家)- price: 价格(50-150元之间的数字)- isbn: ISBN号(真实格式,如 978-7-xxx-xxxxx-x)- description: 简介(突出书籍特色,50字以内)返回格式示例:{{"title": "书名","author": "作者","price": 99.0,"isbn": "978-7-111-11111-1","description": "简介"}}请只返回 JSON 数据,不要添加其他说明。生成的图书信息:• 书名: 微服务实战:从设计到高可用运维• 作者: 张磊• 价格: ¥89.0• ISBN: 978-7-302-56789-4• 简介: 聚焦真实生产场景,涵盖架构设计、服务治理、可观测性与弹性伸缩全链路实践。💡 说明:• PromptTemplate 负责动态生成提示词• BeanOutputConverter 负责解析 JSON 响应• 两者结合实现灵活的结构化输出• 可以在模板中包含 JSON 格式示例Process finished with exit code 0
这里需要注意一下,BeanOutputConverter只能针对String做转换,Flux是不支持的。这个也能理解,肯定要拿到完整结果才能转换成bean。
深入 StructuredOutputConverter 原理
以上代码,就是 SpringAI 帮我们封装好的,可以直接通过 entity 方法实现 JSON 原数据的解析和封装,深入下 entity 方法,看看原理,也把我们之前挖的坑(StructuredOutputConverter原理)给他埋上。
doSingleWithBeanOutputConverter
entity 方法只有三行,第一行是参数合法性检测,第二行是 new 一个 BeanOutputConverter,第三行是调用 doSingleWithBeanOutputConverter 方法
/*** 将 AI 模型的响应内容自动转换并反序列化为指定的 Java 实体对象。* <p>* 该方法为 Spring AI 提供的便捷封装。内部会基于传入的 Class 类型创建 {@link BeanOutputConverter},* 自动反射提取该类的字段结构并生成 JSON Schema,随后将该 Schema 注入到系统提示词中约束模型输出。* 模型返回 JSON 字符串后,底层将自动完成格式校验与 Jackson 反序列化,直接返回强类型实例。** @param <T> 目标实体类型* @param type 目标实体的 Class 对象,必须提供且不能为 null* @return 反序列化后的实体对象;若模型未返回内容或解析失败,可能返回 {@code null}* @throws IllegalArgumentException 当传入的 type 为 null 时抛出*/@Nullablepublic <T> T entity(Class<T> type) {// 1. 防御性编程:校验目标类型参数合法性Assert.notNull(type, "type cannot be null");// 2. 实例化 Bean 输出转换器// 内部会自动调用上一个问题中的 getFormat() 方法,将 Class 的 JSON Schema 注入 PromptBeanOutputConverter<T> outputConverter = new BeanOutputConverter(type);// 3. 委托底层统一执行链路:// 组装 Prompt -> 调用大模型 -> 拦截原始响应 -> 提取 JSON -> 反序列化为目标 Bean -> 返回return (T) this.doSingleWithBeanOutputConverter(outputConverter);}
其中BeanOutputConverter的构造函数可以简单看一眼,他是依赖了ParameterizedTypeReference创建的,这玩意不展开了,我们也可以直接用ParameterizedTypeReference.forType(clazz)来把一个任意Class转成BeanOutputConverter需要的东西。
/*** 便捷的构造方法,用于创建指定目标类型的 {@link BeanOutputConverter} 实例。* <p>* 该方法接收一个普通的 {@link Class} 对象,并通过 Spring 的 {@link ParameterizedTypeReference}* 进行包装,以规避 Java 运行时的泛型擦除(Type Erasure)问题。* 包装后的类型引用将传递给主构造器,确保底层 JSON 反序列化框架(如 Jackson)* 能够准确识别并还原完整的泛型类型结构。** @param clazz 目标实体或泛型类型的 Class 对象*/public BeanOutputConverter(Class<T> clazz) {// 将 Class<T> 转换为 ParameterizedTypeReference<T>,并委托调用内部主构造器。// 这是 Spring 生态中处理运行时泛型类型的标准模式,// 用于向底层 ObjectMapper 传递精确的类型签名,防止集合/嵌套泛型反序列化失败。this(ParameterizedTypeReference.forType(clazz));}
关键技术点说明:
代码片段 | 作用与背景 |
ParameterizedTypeReference.forType(clazz) | Java 在编译后会擦除泛型信息(如 List 变成 List)。Spring 通过该工具类捕获并保留完整的类型签名,供 Jackson/Gson 在运行时正确反序列化。 |
this(...) 构造器委托 | 避免代码重复。实际的核心逻辑(如 Schema 生成、解析器初始化)通常集中在接受 ParameterizedTypeReference 的主构造器中,此构造器仅作为用户友好的便捷入口。 |
与 entity() 方法的关联 | 在上一段提供的 entity(Class type) 方法正是通过此构造器实例化转换器,三者共同构成 Spring AI 中 Class → Schema → Prompt → LLM → JSON → Bean 的完整自动化链路。 |
接着就是非常重要的doSingleWithBeanOutputConverter方法了。这两段代码共同构成了 Spring AI 中 请求上下文注入 → 可观测性包装 → Advisor 责任链执行 → 响应反序列化 的核心执行链路。
方法一:doSingleWithBeanOutputConverter
/*** 执行单次对话请求,并将模型输出通过结构化转换器解析为目标 Java 对象。* <p>* 该方法负责在请求上下文中动态注入格式约束与 JSON Schema,* 随后调用带可观测性包装的底层聊天方法,最后提取响应文本并执行类型转换。** @param <T> 目标对象类型* @param outputConverter 结构化输出转换器(通常为 BeanOutputConverter)* @return 转换后的目标对象;若模型响应为空则返回 {@code null}*/@Nullableprivate <T> T doSingleWithBeanOutputConverter(StructuredOutputConverter<T> outputConverter) {// 1. 注入 Prompt 级格式约束:若转换器提供了格式模板,则存入请求上下文// 下游 Advisor 或 Provider 会读取此属性,将其拼接到 System/User Prompt 中if (StringUtils.hasText(outputConverter.getFormat())) {this.request.context().put(ChatClientAttributes.OUTPUT_FORMAT.getKey(), outputConverter.getFormat());}// 2. 注入 API 级结构化输出 Schema:// 若上下文已开启原生结构化输出支持(如 OpenAI response_format/json_schema),// 且当前为 BeanOutputConverter,则提取其反射生成的 JSON Schema 存入上下文。// 启用后,底层模型客户端将优先使用 Provider 原生 API 传递 schema,而非依赖 Prompt 约束。// OUTPUT_FORMAT("spring.ai.chat.client.output.format"),// STRUCTURED_OUTPUT_SCHEMA("spring.ai.chat.client.structured.output.schema"),// STRUCTURED_OUTPUT_NATIVE("spring.ai.chat.client.structured.output.native");if (this.request.context().containsKey(ChatClientAttributes.STRUCTURED_OUTPUT_NATIVE.getKey())&& outputConverter instanceof BeanOutputConverter beanOutputConverter) {this.request.context().put(ChatClientAttributes.STRUCTURED_OUTPUT_SCHEMA.getKey(), beanOutputConverter.getJsonSchema());}// 3. 调用带可观测性(Tracing/Metrics)包装的对话执行方法,获取完整 ChatResponseChatResponse chatResponse = this.doGetObservableChatClientResponse(this.request).chatResponse();// 4. 从响应中提取纯文本内容(兼容多消息/多 Choice 场景)String stringResponse = getContentFromChatResponse(chatResponse);// 5. 若内容非空,则委托转换器进行 JSON 清洗与 Jackson 反序列化;否则返回 nullreturn stringResponse == null ? null : outputConverter.convert(stringResponse);}
方法二:doGetObservableChatClientResponse
/*** 执行实际的 LLM 调用请求,并全程包裹 Micrometer Observation 可观测性上下文。* <p>* 该方法不直接发起 HTTP 请求,而是通过 Advisor Chain(责任链)驱动调用流程,* 同时自动记录请求/响应元数据、耗时指标、错误率与分布式追踪链路,* 符合 OpenTelemetry / Micrometer 规范,便于接入 Prometheus、Grafana 或 APM 系统。** @param chatClientRequest 已组装完成并注入上下文属性的客户端请求对象* @return 包含模型响应的 ChatClientResponse 包装对象*/private ChatClientResponse doGetObservableChatClientResponse(ChatClientRequest chatClientRequest) {// 1. 提取输出格式信息,作为遥测元数据(Metadata/Tags)的一部分String outputFormat = (String) chatClientRequest.context().getOrDefault(ChatClientAttributes.OUTPUT_FORMAT.getKey(), (Object) null);// 2. 构建 Observation 上下文:绑定请求、Advisor 链快照、流式标识(此处固定 false)及格式ChatClientObservationContext observationContext = ChatClientObservationContext.builder().request(chatClientRequest).advisors(this.advisorChain.getCallAdvisors()).stream(false).format(outputFormat).build();// 3. 创建 Observation 实例:关联 Spring AI 预设的遥测文档规范与指标约定Observation observation = ChatClientObservationDocumentation.AI_CHAT_CLIENT.observation(this.observationConvention,DefaultChatClient.DEFAULT_CHAT_CLIENT_OBSERVATION_CONVENTION,() -> observationContext,this.observationRegistry);// 4. 在 Observation 作用域内执行实际调用:// 触发 advisorChain.nextCall(),依次经过 Retry、Logging、Prompt 增强等拦截器,// 最终抵达底层 ChatModel Provider 发起 HTTP 请求。// 调用完成后将响应绑定至观察上下文,供指标采集、日志导出与链路追踪使用。ChatClientResponse chatClientResponse = (ChatClientResponse) observation.observe(() -> {ChatClientResponse response = this.advisorChain.nextCall(chatClientRequest);observationContext.setResponse(response);return response;});// 5. 防御性返回:若责任链中断或未返回响应,则构造空对象避免上游 NPEreturn chatClientResponse != null ? chatClientResponse : ChatClientResponse.builder().build();}
Spring AI entity() 完整执行链路全景图
结合上面提供的三段代码,一次 chatClient.prompt().entity(MyBean.class) 的底层流转如下:
阶段 | 核心动作 | 对应代码/组件 |
1. 类型解析 | 通过 Class 生成 ParameterizedTypeReference,规避泛型擦除 | BeanOutputConverter(Class) |
2. Schema 注入 | 反射生成 JSON Schema → 调用 getFormat() 拼接 Prompt → 存入 request.context() | doSingleWithBeanOutputConverter() 前半段 |
3. 可观测包装 | 创建 Micrometer Observation 上下文,绑定请求元数据与 Advisor 链 | doGetObservableChatClientResponse() |
4. 责任链执行 | advisorChain.nextCall() 依次触发拦截器 → 抵达 Provider → HTTP 调用 LLM | observation.observe(() -> ...) 内部 |
5. 响应提取 | 从 ChatResponse 提取纯文本,过滤 Markdown 与无关解释 | getContentFromChatResponse() |
6. 反序列化 | 调用 outputConverter.convert() → JSON Schema 校验 → Jackson 映射为 MyBean | doSingleWithBeanOutputConverter() 尾部 |
提示:Spring AI 1.x 强烈推荐使用 entity() 系列方法替代手动 ObjectMapper 解析。它通过 StructuredOutputConverter 抽象了 Prompt 约束 / 原生 Schema / 响应清洗 / 类型安全 四重保障,大幅降低生产环境中的 LLM 输出不可控风险。
转成List和Map
以上演示了如何把模型结果转成Bean,但是有时候我们返回的一组Bean,那么就需要转成List或者Map了,这部分Spring AI也是支持的。
因为StructuredOutputConverter的实现类,除了BeanOutputConverter之外,还有ListOutputConverter、MapOutputConverter这两个。
List<String> result = chatClient.prompt("请帮我推荐几本java相关的书").system("你是一个专业的图书推荐人员").call().entity(new ListOutputConverter(new DefaultConversionService()));Map<String,Object> result = chatClient.prompt("请帮我推荐几本java相关的书").system("你是一个专业的图书推荐人员").call().entity(new MapOutputConverter());
但是,这两种用法,都不支持转成List和Map<String,Bean>,只能转成List和Map<String,Object>,也就是说最终转成的内容并不是我们想要的,比如我们要一个完成的book,他可能只输出了一个书名的List。
我感觉不太好用,我一般不怎么用这两个,List的话我反而下面这种用法比较多,可以拿到一个我想要的list,但是他底层也是用的BeanOutputConverter:
List<Book> result = chatClient.prompt("请帮我推荐几本java相关的书").system("你是一个专业的图书推荐人员").call().entity(new ParameterizedTypeReference<List<Book>>() {});
或者如果你在提示词中能给出更明确的关于map的定义:
Map<String, Object> book = chatClient.prompt("请给我推荐几本心理学有关的书,书的内容包括书名、作者、价格、上市时间等信息,以书名作为key,书的信息作为value").call().entity(new MapOutputConverter());
三、LangChain 实现方案
LangChain 提供了更灵活的结构化输出方案,核心是 Pydantic + with_structured_output。
基础用法:Pydantic + with_structured_output
from langchain_openai import ChatOpenAIfrom pydantic import BaseModel, Fieldfrom typing import List, Optional# 1. 定义输出结构(使用 Pydantic)class Book(BaseModel):title: str = Field(description="书名,要有吸引力")author: str = Field(description="作者姓名")price: float = Field(description="价格,单位:人民币元", ge=0, le=1000)isbn: Optional[str] = Field(description="ISBN号,格式如 978-7-xxx-xxxxx-x")description: str = Field(description="书籍简介,50字以内", max_length=50)level: str = Field(description="适合读者水平", enum=["入门", "进阶", "中高级", "专家"])class BookRecommendation(BaseModel):books: List[Book] = Field(description="推荐的书籍列表", min_length=1, max_length=5)note: Optional[str] = Field(description="补充说明或购买建议")# 2. 初始化模型并绑定结构化输出llm = ChatOpenAI(model="gpt-4o", temperature=0)structured_llm = llm.with_structured_output(BookRecommendation)# 3. 调用(自动返回 Pydantic 对象,无需手动解析)result = structured_llm.invoke("请推荐3本适合中级开发者学习的Java书籍")# 4. 直接使用结构化数据for book in result.books:print(f"书名: {book.title} by {book.author} - ¥{book.price}")print(f" {book.description}\n")
进阶用法:Function Calling 方式(兼容旧模型)
from langchain_core.prompts import ChatPromptTemplatefrom langchain_core.output_parsers import PydanticOutputParser# 1. 定义输出结构(同上)class Book(BaseModel):title: strauthor: strprice: float# 2. 创建 Parserparser = PydanticOutputParser(pydantic_object=Book)# 3. 构建 Prompt,自动注入格式指令prompt = ChatPromptTemplate.from_messages([("system", "你是一个专业的图书推荐助手。{format_instructions}"),("human", "推荐一本关于{topic}的Java书籍"),])# 4. 组装链条chain = prompt | llm | parser# 5. 调用result = chain.invoke({"topic": "并发编程","format_instructions": parser.get_format_instructions()})print(result.title) # 直接访问属性
流式结构化输出(Streaming)
# 注意:结构化输出 + 流式需要模型支持(如 gpt-4o)structured_llm = ChatOpenAI(model="gpt-4o").with_structured_output(Book)# 流式调用,逐个 token 接收for chunk in structured_llm.stream("推荐一本Java入门书籍"):# chunk 是部分解析的 Pydantic 对象if chunk.title: # 当字段被填充时处理print(f"\r书名: {chunk.title}", end="", flush=True)# 或使用 astream 异步流式async for chunk in structured_llm.astream("推荐一本设计模式书籍"):process_partial_result(chunk)
复杂嵌套结构示例
from typing import List, Dict, Optionalfrom pydantic import BaseModel, Fieldclass Chapter(BaseModel):title: strkey_points: List[str] = Field(description="本章核心知识点", min_items=3)difficulty: str = Field(enum=["★", "★★", "★★★", "★★★★", "★★★★★"])estimated_hours: float = Field(description="预计学习时长(小时)", ge=0.5)class CourseOutline(BaseModel):course_name: strtarget_audience: str = Field(description="目标学员群体")total_chapters: int = Field(ge=3, le=20)chapters: List[Chapter]prerequisites: Optional[List[str]] = Field(description="前置知识要求")learning_outcomes: List[str] = Field(description="学完能掌握的技能", min_items=3)# 使用llm = ChatOpenAI(model="gpt-4o").with_structured_output(CourseOutline)outline = llm.invoke("设计一个《Spring Boot 微服务实战》课程大纲")# 直接访问嵌套属性for i, chapter in enumerate(outline.chapters, 1):print(f"{i}. {chapter.title} [{chapter.difficulty}] - {chapter.estimated_hours}h")for point in chapter.key_points:print(f" - {point}")
LangChain 关键特性对比
特性 | with_structured_output | PydanticOutputParser | Tool/Function Calling |
易用性 | 高 | 中 | 中 |
稳定性 | 高(原生支持) | 中(依赖Prompt) | 高 |
流式支持 | 支持(需模型支持) | 不支持 | 部分支持 |
复杂嵌套 | 支持(Pydantic 原生) | 支持 | 支持 |
模型兼容 | OpenAI/Anthropic 等新模型 | 所有支持 JSON 的模型 | 支持 function calling 的模型 |
错误重试 | 自动重试(可配置) | 需手动实现 | 框架内置重试 |
四、三种实现路径深度对比
路径一:Prompt 约束(通用但脆弱)
# 纯 Prompt 方式prompt = """请输出严格的 JSON 格式,不要包含任何解释:{"books": [{"title": "书名", "author": "作者", "price": 99}]}"""
优点:所有模型通用,零配置
缺点:
模型可能输出 ```json 包裹 可能添加"好的,这是您要的..."等解释 复杂结构容易格式错误 无自动重试机制
路径二:Tool Calling / Function Calling(推荐)
# LangChain 自动将 Pydantic 转为 function schematools = [StructuredTool.from_function(func=lambda **kwargs: kwargs, # 虚拟函数,只取参数name="output_book",description="输出书籍信息",args_schema=Book # Pydantic 类)]llm_with_tools = llm.bind_tools(tools)
优点:
模型"理解"这是在调用函数,输出更稳定 支持自动重试和参数校验 可结合业务逻辑(真实函数执行)
缺点:
需要模型支持 function calling 配置稍复杂
路径三:Native Structured Output(最稳定)
# OpenAI 原生 API(Spring AI / LangChain 自动适配)response = client.chat.completions.create(model="gpt-4o",messages=[...],response_format={"type": "json_schema","json_schema": {"name": "book_recommendation","schema": Book.model_json_schema(), # Pydantic 生成"strict": True # 强制校验}})
优点:
模型层强制校验,100% 符合 Schema 无需解析 Markdown,直接返回纯净 JSON 错误时模型会自动重试修正
缺点:
仅部分新模型支持(gpt-4o, claude-3.5, gemini-1.5-pro 等) Schema 变更需重新部署
五、JSON Schema 最佳实践(避坑指南)
必填字段 + 约束条件
{"type": "object","properties": {"price": {"type": "number","minimum": 0,"maximum": 1000,"description": "价格,单位:人民币元"}},"required": ["title", "author", "price"],"additionalProperties": false}
关键约束:
required: 明确必填字段,避免模型"偷懒" additionalProperties: false: 禁止模型添加无关字段 minimum/maximum: 业务规则前置,减少后校验 enum: 限定可选值,避免自由文本
数组长度控制
{"books": {"type": "array","items": { "$ref": "#/$defs/Book" },"minItems": 1,"maxItems": 5,"description": "推荐1-5本书,不要过多"}}
经验:明确数量限制能显著提升输出质量,避免模型"啰嗦"或"敷衍"
嵌套结构 + $defs 复用
{"$defs": {"Author": {"type": "object","properties": {"name": {"type": "string"},"bio": {"type": "string", "maxLength": 100}},"required": ["name"]}},"properties": {"books": {"type": "array","items": {"type": "object","properties": {"title": {"type": "string"},"author": {"$ref": "#/$defs/Author"}}}}}}
优势:
避免重复定义,维护更简单 模型更容易理解结构关系 生成的代码类型更安全
六、常见坑点与解决方案
坑1:模型输出包含 Markdown 代码块
模型可能返回:
{"title": "Effective Java"}
解决方案:
Spring AI:BeanOutputConverter 的 getFormat() 已内置过滤指令 LangChain:with_structured_output 自动处理 手动方案:正则提取 r'```json\s*(.*?)\s*```'
坑2:模型添加解释性文字
好的,这是为您推荐的书籍:{"books": [...]}希望这些建议对您有帮助!
解决方案:
Prompt 中强调: Do not include any explanations, only provide JSON使用 Native Structured Output(最可靠) 后处理:用 JSON 解析器尝试提取,失败则重试
坑3:泛型类型擦除(Java)
// 错误:无法识别泛型List<Book> result = chatClient.call().entity(List.class);// 正确:使用 ParameterizedTypeReferenceList<Book> result = chatClient.call().entity(new ParameterizedTypeReference<List<Book>>() {});
坑4:字段名不一致(snake_case vs camelCase)
# Pydantic 配置:自动转换class Book(BaseModel):class Config:alias_generator = to_camel # JSON 用 camelCase,Python 用 snake_casepopulate_by_name = True// Jackson 配置@JsonNaming(PropertyNamingStrategies.SnakeCaseStrategy.class)public class Book {private String bookTitle; // Java: bookTitle, JSON: book_title}
七、生产环境建议
推荐架构
应用层• 定义 Pydantic/Java Bean• 业务校验 + 降级策略|框架层(Spring AI / LangChain)• StructuredOutputConverter• 自动重试 + 超时控制• 可观测性埋点|模型层• 优先 Native Structured Output• 降级:Function Calling → Prompt• 多模型路由(成本/质量平衡)
关键配置建议
# application.yml (Spring AI)spring:ai:chat:client:structured-output:native: true # 优先使用原生支持fallback: prompt # 降级策略retry:max-attempts: 3backoff:initial-interval: 1sobservation:enabled: true # 开启 Micrometer 埋点# LangChain 配置from langchain.globals import set_verbose, set_debugset_verbose(True) # 开发环境开启调试# 重试配置from tenacity import stop_after_attempt, wait_exponentialstructured_llm = llm.with_structured_output(Book,method="function_calling", # 显式指定方式retry_policy={"max_retries": 3,"backoff_factor": 2})
监控指标建议
指标 | 说明 | 告警阈值 |
structured_output.success_rate | 结构化解析成功率 | < 95% |
structured_output.retry_count | 平均重试次数 | > 1.5 |
structured_output.latency_p99 | P99 延迟 | > 5s |
structured_output.schema_violation | Schema 校验失败数 | > 0 |
八、总结:如何选择
按场景选择
场景 | 推荐方案 | 理由 |
快速原型/个人项目 | Prompt 约束 + 手动解析 | 零配置,快速验证想法 |
企业内部系统 | Spring AI BeanOutputConverter | Java 生态友好,类型安全 |
Python AI 应用 | LangChain + with_structured_output | 语法简洁,生态完善 |
高可靠性生产系统 | Native Structured Output + 降级策略 | 稳定性优先,自动容错 |
复杂业务逻辑 | Function Calling + 真实函数执行 | 结构化输出 + 业务执行一体化 |
核心原则
尽早结构化:在 Prompt 设计阶段就明确输出格式,而非事后解析 类型即文档:Pydantic/Java Bean 既是代码类型,也是给模型的 Schema 文档 防御性编程:永远假设模型会输出错误格式,做好重试和降级 可观测性先行:结构化输出的成功率、延迟是关键业务指标,必须监控
一句话总结
结构化输出不是"可选项",而是大模型应用工程化的"基础设施"。用 Spring AI 的 entity() 或 LangChain 的 with_structured_output,让模型输出从"人类可读"升级为"机器可执行",是构建可靠 AI 应用的第一步。
延伸阅读:
Spring AI 官方文档:StructuredOutputConverter LangChain Docs: Structured Output OpenAI API: Structured Outputs JSON Schema 规范:https://json-schema.org
夜雨聆风