大型语言模型是“输入文本、输出文本”的系统。一旦下游代码需要根据某个字段进行路由、持久化某个值,或基于结果做出分支决策,这些文本就必须被转换为具有明确类型的记录。结构化输出正是弥合这一鸿沟的手段:模型被引导生成符合特定模式的文本,而应用程序则将其解析回类型化的对象,以便代码库中的其他部分能够像处理任何其他领域类型一样对其进行操作。
Spring AI 直接在上公开结构化输出ChatClient流畅的 API 通过.entity(…)您可以为希望返回的形状定义一个 Java 类型,Spring AI 会负责其余工作:系统会根据您的类型生成 JSON 模式,模型会被指示遵循该模式,并且响应会被反序列化为您的类型。
一、简单例子
1、输出普通单个对象
先定义个record
public record Topic(String topic,//问题List<String> infos//响应信息) {}
用.entity设置
@GetMapping("/ask3")public Topic ask3(String topic){Topic result = openaiChatClient.prompt().system("您是一位资深系统架构师") //系统 提示词 ,这里如果设置了会覆盖创建chatClient那里设置的系统提示词.user(u -> u.text("请介绍一下{topic}的三个优点").param("topic",topic)).call().entity(Topic.class);return result;}
请求可以看到 结果
{"topic":"PHP的三个优点","infos":["语法简单且上手快,适合快速开发Web应用","与HTML集成方便,模板开发效率高","生态成熟且部署成本低,拥有丰富的框架和开源资源"]}
我们也可以看下大模型输入输出
request: ChatClientRequest[prompt=Prompt{messages=[SystemMessage{textContent='您是一位资深系统架构师', messageType=SYSTEM, metadata={messageType=SYSTEM}}, UserMessage{content='请介绍一下php的三个优点', metadata={messageType=USER}, messageType=USER}], modelOptions=org.springframework.ai.openai.OpenAiChatOptions@51fa12f0}, context={spring.ai.chat.client.output.format=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","properties" : {"infos" : {"type" : "array","items" : {"type" : "string"}},"topic" : {"type" : "string"}},"required" : [ "infos", "topic" ],"additionalProperties" : false}}]
可以看到是我们的提示词要求大模型返回固定的json格式的字符串
response: {"metadata" : {"empty" : false,"id" : "chatcmpl-8b06354f-89b9-9e8f-b70f-57c396fda6f2","model" : "qwen3.8-flash","promptMetadata" : [ ],"rateLimit" : {"requestsLimit" : 0,"requestsRemaining" : 0,"requestsReset" : "PT0S","tokensLimit" : 0,"tokensRemaining" : 0,"tokensReset" : "PT0S"},"usage" : {"promptTokens" : 259,"completionTokens" : 135,"totalTokens" : 394,"cacheReadInputTokens" : 0,"nativeUsage" : {"completion_tokens" : 135,"prompt_tokens" : 259,"total_tokens" : 394,"completion_tokens_details" : {"reasoning_tokens" : 68,"valid" : true,"text_tokens" : 135},"prompt_tokens_details" : {"cached_tokens" : 0,"valid" : true,"text_tokens" : 259},"valid" : true}}},"result" : {"metadata" : {"contentFilters" : [ ],"empty" : true,"finishReason" : "STOP"},"output" : {"media" : [ ],"messageType" : "ASSISTANT","metadata" : {"role" : "assistant","messageType" : "ASSISTANT","finishReason" : "STOP","refusal" : "","annotations" : [ { } ],"index" : 0,"id" : "chatcmpl-8b06354f-89b9-9e8f-b70f-57c396fda6f2","reasoningContent" : "我们需要回答用户:请介绍一下php的三个优点。要求 JSON 格式,RFC8259,不能 markdown,必须符合 schema:对象含 infos 字符串数组和 topic 字符串,additionalProperties false。必须只输出 JSON。需要中文内容。三个优点。确保无解释。工具已调用满足要求。最终只 JSON。"},"text" : "{\n \"topic\": \"PHP的三个优点\",\n \"infos\": [\n \"语法简单且上手快,适合快速开发Web应用\",\n \"与HTML集成方便,模板开发效率高\",\n \"生态成熟且部署成本低,拥有丰富的框架和开源资源\"\n ]\n}","toolCalls" : [ ]}},"results" : [ {"metadata" : {"contentFilters" : [ ],"empty" : true,"finishReason" : "STOP"},"output" : {"media" : [ ],"messageType" : "ASSISTANT","metadata" : {"role" : "assistant","messageType" : "ASSISTANT","finishReason" : "STOP","refusal" : "","annotations" : [ { } ],"index" : 0,"id" : "chatcmpl-8b06354f-89b9-9e8f-b70f-57c396fda6f2","reasoningContent" : "我们需要回答用户:请介绍一下php的三个优点。要求 JSON 格式,RFC8259,不能 markdown,必须符合 schema:对象含 infos 字符串数组和 topic 字符串,additionalProperties false。必须只输出 JSON。需要中文内容。三个优点。确保无解释。工具已调用满足要求。最终只 JSON。"},"text" : "{\n \"topic\": \"PHP的三个优点\",\n \"infos\": [\n \"语法简单且上手快,适合快速开发Web应用\",\n \"与HTML集成方便,模板开发效率高\",\n \"生态成熟且部署成本低,拥有丰富的框架和开源资源\"\n ]\n}","toolCalls" : [ ]}} ]}
2、输出List结果
先定义pojo
public class Book {@JsonPropertyDescription("作者名称")private String a;@JsonPropertyDescription("书本名称")private String b;public String getA() {return a;}public void setA(String a) {this.a = a;}public String getB() {return b;}public void setB(String b) {this.b = b;}@Overridepublic String toString() {return "Book{" +"a='" + a + '\'' +", b='" + b + '\'' +'}';}}
记得最好用@JsonPropertyDescription标注这个字段是干什么的,这样给大模型的提示词也会加上这个
"items" : {"type" : "object","properties" : {"a" : {"type" : "string","description" : "作者名称"},"b" : {"type" : "string","description" : "书本名称"}},
如果不定义可能大模型理解不了就会出错。
请求
@GetMapping("/ask4")public List<Book> ask4(String topic){List<Book> result = openaiChatClient.prompt().system("您是一位资深书本推荐家") //系统 提示词 ,这里如果设置了会覆盖创建chatClient那里设置的系统提示词.user(u -> u.text("请推荐关于{topic}类型的三本书").param("topic",topic)).call().entity(new ParameterizedTypeReference<List<Book>>() {});return result;}
结果
[{"a":"辰东","b":"遮天"},{"a":"唐家三少","b":"斗罗大陆"},{"a":"天蚕土豆","b":"斗破苍穹"}]
看看LLM输出
"finishReason" : "STOP","index" : 0,"annotations" : [ { } ],"id" : "chatcmpl-7fb3afd2-75c0-9cab-a910-a6b5023899fa","reasoningContent" : "我们需要回答用户:请推荐关于玄幻类型的三本书。响应必须 JSON 格式,RFC8259 兼容,不要解释,不要 markdown 代码块。需要符合 schema:数组,每项对象只有 a(作者名称)和 b(书本名称),required a,b,additionalProperties false。需要选择玄幻类型三本书。考虑中文玄幻/奇幻网文:辰东《遮天》、唐家三少《斗罗大陆》、天蚕土豆《斗破苍穹》。这些都是玄幻类型。输出纯 JSON 数组。确保无代码块。"},"text" : "[\n {\n \"a\": \"辰东\",\n \"b\": \"遮天\"\n },\n {\n \"a\": \"唐家三少\",\n \"b\": \"斗罗大陆\"\n },\n {\n \"a\": \"天蚕土豆\",\n \"b\": \"斗破苍穹\"\n }\n]","toolCalls" : [ ]}
