让AI学会"说人话"——Spring AI结构化输出实战

系列文章

本篇是 AI Agent 深度解析系列的第 8 篇。以下是系列完整目录,建议收藏作为学习索引。

🏗️ 基础理论篇

  1. 从零理解 RAG:检索增强生成完整指南
  2. Embedding 向量化的魔法:从文本到向量的数学之旅与 Java 实战
  3. 理解 AI Agent 的大脑:ReAct 模式从入门到实战
  4. Spring AI 核心架构全解析:从 ChatModel 到 Advisor Chain 的设计哲学

🧩 核心组件篇

  1. AI Agent 的记忆系统:从 ChatMemory 到持久化记忆的 Java 实战
  2. AI Agent 的记忆力是怎么实现的——LangChain4j Memory 机制深度解析
  3. AI Agent 的工具箱:深入理解 Tool Use 与 Spring AI Function Calling 实战
  4. 让 AI 学会”说人话”——Spring AI 结构化输出实战
  5. AI Agent 的灵魂对话:Prompt Engineering 系统提示词设计的艺术与工程

🏛️ 架构设计篇

  1. AI Agent 的规划大脑:从任务分解到自适应执行策略
  2. AI Agent 的工作流编排:从顺序链到自适应 DAG 的 Java 实战
  3. AI Agent 团队协作:多 Agent 系统架构设计与 Java 实战
  4. Agent 间如何对话:A2A 协议深度解析与 Java 实战

🔍 知识检索篇

  1. AI Agent 的知识检索引擎:从向量搜索到智能检索策略的 Java 实战
  2. 当 RAG 遇上知识图谱:GraphRAG 原理与 Java 实战
  3. 当 RAG 遇到 Agent:Agentic RAG 的架构设计与 Java 实战
  4. MCP 模型上下文协议:AI 的万能接口与 MCP Server 实战

🚀 进阶能力篇

  1. AI Agent 的推理引擎:从 Chain-of-Thought 到推理模型的深度解析与 Java 实战
  2. AI Agent 的多模态感知:从图片理解到语音交互的 Java 实战
  3. AI Agent 的自我反思与经验学习:从错误中进化的 Java 实战
  4. AI Agent 的上下文工程与 Token 预算管理:从窗口压缩到成本优化的 Java 实战
  5. AI Agent 的人机协作:从 Human-in-the-Loop 到渐进式自治的 Java 实战

🛡️ 生产保障篇

  1. AI Agent 的安全防线:Prompt 注入防御与生产级安全防护实战
  2. AI Agent 的可观测性:从链路追踪到成本监控的 Java 实战
  3. AI Agent 的流式响应与实时交互:从 SSE 到 WebSocket 的 Java 实战
  4. AI Agent 的容错与韧性:从错误处理到生产级可靠性保障的 Java 实战
  5. AI Agent 评估与优化:从基准测试到生产环境的质量守护实战
  6. AI Agent 的成本优化:从模型路由到缓存策略的 Java 实战

🧭 全景总结

  1. 从理论到生产:AI Agent 全景知识图谱与 Java 开发者成长路线

一个真实的痛点

你有没有遇到过这样的场景:你让AI帮你提取一段文本里的关键信息,它回了你一大段自然语言描述。你想要的是一个干干净净的JSON,它给你的却是一篇小作文。

1
2
3
4
5
6
// 你期望的输出
{"name": "张三", "age": 28, "city": "北京"}

// AI实际返回的
根据您提供的信息,我提取到了以下关键内容。这个人的名字叫张三,
年龄是28岁,目前居住在北京。如果您需要更多信息,请随时告诉我...

这在开发AI Agent时尤其致命——你的Agent需要解析工具调用结果、提取实体信息、生成API响应,这些场景都需要精确、可预测的输出格式,而不是一段散文。

今天我们就来聊聊:怎么让AI学会”说人话”,准确地说,怎么让它输出结构化的数据。

什么是结构化输出

简单说,结构化输出就是让LLM按照你指定的格式(JSON、XML、枚举值、Java对象等)来返回结果,而不是自由发挥的纯文本。

打个比方:自由文本就像你跟朋友聊天,想到哪说到哪;结构化输出就像填表格——每一格该填什么、什么格式,都规定好了。

为什么这个概念重要?因为**AI Agent的核心是”连接”**——连接LLM的能力和外部世界的工具。工具之间传递的是结构化数据,不是散文。一个无法输出结构化数据的Agent,就像一个只会说方言的翻译官,能力再强也没法跟系统对接。

结构化输出的工作原理

下图展示了结构化输出的核心思路——将 LLM 的自由文本转换为可直接使用的 Java 对象:

结构化输出流程:自由文本→JSON→Java Bean,Spring AI 三种方式:Bean转换、枚举分类、列表提取

如图所示,结构化输出解决了 LLM 应用的一个核心痛点:LLM 返回的是自由文本(左侧,红色标注),但你的 Java 代码需要的是类型安全的对象(右侧,蓝色标注)。Spring AI 通过在 Prompt 中注入 JSON Schema,约束 LLM 按指定格式输出,然后自动反序列化为 Java Bean。三种方式覆盖了最常见的场景——Bean 转换用于复杂对象映射,枚举分类用于意图识别等分类任务,列表提取用于批量数据抽取。

你可能会好奇:LLM本质上是逐token生成文本的,它怎么”学会”输出结构化数据?

答案是Prompt Engineering + 约束解码的组合拳。

第一招:Prompt引导

最基础的方法就是在提示词里告诉模型:”请以JSON格式输出,包含以下字段…”。这就是为什么几乎所有的结构化输出方案里,你都能看到类似这样的System Prompt:

1
2
3
4
5
6
7
You are a helpful assistant. Always respond in valid JSON format.
The output should match this schema:
{
"name": "string",
"age": "number",
"skills": ["string"]
}

这招管用,但不完美——模型可能会多加注释、少个字段、甚至输出不合法的JSON。

第二招:模式约束

更高级的做法是在生成层面施加约束。有些模型(如OpenAI的API)支持 response_format 参数,通过JSON Schema来约束输出。底层实现是通过受限解码(Constrained Decoding)——在每一步生成时,只允许模型选择符合schema的token。

Spring AI就是结合了这两招:它会自动帮你构造包含schema的提示词,并且在解析阶段做校验和重试,确保最终拿到的数据是合法的。

Spring AI的结构化输出方案

Spring AI提供了 StructuredOutputConverter 接口,这是它的结构化输出核心。内置了三种实现:

转换器 用途 输出目标
BeanOutputConverter 转成Java Bean/POJO 任意Java类
MapOutputConverter 转成Map Map<String, Object>
ListOutputConverter 转成List List<String>

1. 转成Java Bean——最常用的场景

假设你在做一个招聘Agent,需要从简历文本中提取候选人信息。定义一个POJO:

1
2
3
4
5
6
7
8
9
public class CandidateInfo {
private String name;
private int age;
private String currentCompany;
private List<String> skills;
private int yearsOfExperience;

// getter/setter 省略
}

用Spring AI提取信息:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
@BeanOutputConverter<CandidateInfo> converter =
new BeanOutputConverter<>(CandidateInfo.class);

String prompt = """
请从以下简历文本中提取候选人信息:
"张三,28岁,目前在字节跳动担任高级Java开发工程师,
有5年工作经验,精通Spring Boot、微服务架构、Kafka和Redis。"

{format}
""";

PromptTemplate template = new PromptTemplate(prompt);
PromptTemplate.add("format", converter.getFormat());

String response = chatModel.call(
template.create()
).getResult().getOutput().getContent();

CandidateInfo candidate = converter.convert(response);
System.out.println(candidate.getName()); // 张三
System.out.println(candidate.getSkills()); // [Spring Boot, 微服务架构, Kafka, Redis]

注意 {format} 这个占位符——Spring AI会自动把它替换成JSON Schema描述,告诉模型应该输出什么结构。

2. 转成枚举——分类场景利器

做Agent时经常需要让AI做分类判断。比如客服Agent要判断用户情绪:

1
2
3
4
5
6
7
8
9
enum Sentiment {
POSITIVE, NEGATIVE, NEUTRAL, URGENT
}

// 直接在ChatClient上用
String sentiment = chatClient.prompt()
.user("我等了三天了还没发货,你们到底在搞什么!")
.call()
.content(Sentiment.class); // 返回 URGENT

Spring AI会在提示词里附上所有枚举值,并约束模型只输出匹配的枚举名。比自己解析文本靠谱多了。

3. 列表提取——轻量但好用

1
2
3
4
5
List<String> keywords = chatClient.prompt()
.user("从这段文本中提取5个关键词:Spring AI是一个用于构建AI应用的Java框架...")
.call()
.content(new ListOutputConverter(new DefaultConversionService()));
// [Spring AI, Java框架, AI应用, 构建, 开发]

在Agent中的实际应用

结构化输出在Agent开发中有几个关键应用场景:

工具调用的参数解析

Agent需要调用外部工具时(详见 Tool Use 实战),必须把自然语言转换为工具所需的参数格式。Spring AI的Function Calling就依赖结构化输出来确保参数正确:

1
2
3
4
5
6
7
8
@Bean
@Description("查询指定城市的天气")
public WeatherInfo getWeather(
@Description("城市名称,如:北京") String city,
@Description("日期,格式:yyyy-MM-dd") String date
) {
return weatherService.query(city, date);
}

当用户说”明天北京天气怎么样”,Agent需要输出 city: "北京", date: "2026-06-16"——这就是结构化输出在幕后工作。

多步骤任务的中间结果

复杂Agent任务通常分多步执行。每一步的输出需要被下一步理解。用结构化输出可以确保步骤之间的数据传递不会出错:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// 第一步:分析用户意图
record UserIntent(String action, Map<String, String> parameters) {}

UserIntent intent = chatClient.prompt()
.user(userMessage)
.call()
.content(UserIntent.class);

// 第二步:根据意图执行对应操作
switch (intent.action()) {
case "query_order" -> orderService.query(intent.parameters().get("orderId"));
case "refund" -> orderService.refund(intent.parameters().get("orderId"));
// ...
}

几个实战踩坑经验

1. 模型能力差异很大

GPT-4o、Claude 3.5这些高端模型对结构化输出支持很好,但小模型(7B/13B级别)可能经常输出不合法的JSON。Spring AI默认会在解析失败时重试,但你要控制好重试次数和成本。

2. 字段描述很重要

不要只定义字段名,加上 @Description 注解。模型是靠理解字段含义来填值的,描述越清晰,输出越准确:

1
2
3
4
5
6
7
8
9
10
public class OrderAnalysis {
@Description("订单状态异常的具体原因,如:超时未发货、商品缺货、物流停滞")
private String issueReason;

@Description("建议的处理方式,取值范围:退款、补发、催促、升级处理")
private String suggestedAction;

@Description("问题严重程度,1-10分,10为最紧急")
private int severity;
}

3. 嵌套对象要谨慎

深层嵌套的JSON结构容易出错。如果你的结构超过两层嵌套,考虑拆分成多次调用。Agent设计的原则之一就是每步做一件事,这对结构化输出同样适用。

4. 处理模型”话多”的问题

有些模型习惯在JSON前后加解释文字。Spring AI的converter会自动处理这些,但如果用原生API调用,记得在prompt里加一句:”只输出JSON,不要有任何额外文字。”

深入源码:StructuredOutputConverter 是怎么工作的

光会用还不够,我们来看看 Spring AI 内部是怎么实现结构化输出的。理解原理,遇到问题时才知道怎么排查。

BeanOutputConverter 的核心流程

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// 简化后的核心逻辑
public class BeanOutputConverter<T> implements StructuredOutputConverter<T> {

private final Class<T> targetClass;
private final JsonSchemaGenerator schemaGenerator;

@Override
public String getFormat() {
// 1. 根据目标 Class 生成 JSON Schema
JsonSchema schema = schemaGenerator.generate(targetClass);

// 2. 构造提示词模板
return """
Your response should be in JSON format conforming to the following schema:
```json
%s
        """.formatted(schema.toString());
}

@Override
public T convert(String text) {
    // 1. 从响应文本中提取 JSON(处理 markdown 代码块等情况)
    String json = extractJson(text);
    
    // 2. 反序列化为目标对象
    return objectMapper.readValue(json, targetClass);
}

}

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21

关键点:
- **Schema 生成**:利用 Jackson 的 `JsonSchemaGenerator`,根据 Java 类自动生成 JSON Schema
- **JSON 提取**:模型返回的可能是纯 JSON,也可能是 markdown 代码块包裹的 JSON,converter 会自动处理
- **反序列化**:用 Jackson 做 JSON → Java 对象的转换

### 重试机制

Spring AI 默认会在解析失败时重试。你可以配置重试策略:

```java
@Bean
public BeanOutputConverter<CandidateInfo> candidateConverter() {
return new BeanOutputConverter<>(
CandidateInfo.class,
StructuredOutputConverterOptions.builder()
.maxRetries(3) // 最多重试3次
.retryDelay(Duration.ofSeconds(1)) // 重试间隔
.build()
);
}

注意:每次重试都是一次 LLM 调用,会消耗额外的 token。如果模型本身能力不行,重试也救不了。

横向对比:Spring AI vs LangChain4j vs 原生 API

作为 Java 开发者,你可能好奇不同方案的实现差异。这里做个对比:

特性 Spring AI LangChain4j OpenAI 原生 API
Schema 定义方式 Java Class + @Description Java Class + @Description JSON Schema 字符串
自动 Schema 生成 ❌ 手写
输出解析 自动(含重试) 自动 手动 JSON.parse
枚举支持 需自己处理
嵌套对象
错误处理 内置重试 + 异常 内置重试 自己实现
与 Spring 生态集成 原生 需适配

LangChain4j 的实现

1
2
3
4
5
6
7
8
9
// LangChain4j 用 AiServices 实现结构化输出
interface ResumeParser {
@SystemMessage("从简历文本中提取候选人信息")
CandidateInfo parse(String resumeText);
}

// 使用
ResumeParser parser = AiServices.create(ResumeParser.class, chatLanguageModel);
CandidateInfo info = parser.parse(resumeText);

LangChain4j 的实现更”声明式”——你定义一个接口,框架自动生成实现。Spring AI 更”命令式”——你显式创建 converter 并调用。

选择建议:如果你已经在用 Spring Boot,Spring AI 是自然选择。如果你的项目不依赖 Spring,LangChain4j 更轻量。

生产环境最佳实践

1. 性能优化

结构化输出的主要性能瓶颈在 LLM 调用本身,但有些优化点:

1
2
3
4
5
6
// 缓存 Schema 生成结果(避免每次调用都重新生成)
@Bean
public BeanOutputConverter<CommonResponse> cachedConverter() {
// BeanOutputConverter 内部会缓存 schema,不用重复创建
return new BeanOutputConverter<>(CommonResponse.class);
}

2. 监控与可观测性

在生产环境中,你需要监控结构化输出的成功率:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
@Component
public class StructuredOutputMetrics {

private final MeterRegistry meterRegistry;

public <T> T convertWithMetrics(StructuredOutputConverter<T> converter,
String response,
String schemaName) {
try {
T result = converter.convert(response);
meterRegistry.counter("structured.output.success",
"schema", schemaName).increment();
return result;
} catch (Exception e) {
meterRegistry.counter("structured.output.failure",
"schema", schemaName,
"error", e.getClass().getSimpleName()).increment();
throw e;
}
}
}

3. 灰度发布策略

切换结构化输出方案时(比如从手动解析切换到 Spring AI),建议灰度:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
@Service
public class HybridParserService {

@Value("${feature.use-structured-output:false}")
private boolean useStructuredOutput;

public CandidateInfo parseCandidate(String resume) {
if (useStructuredOutput) {
// 新方案:Spring AI 结构化输出
return springAiConverter.convert(
chatClient.prompt().user(resume).call().content()
);
} else {
// 旧方案:手动 JSON 解析
return legacyParser.parse(resume);
}
}
}

技术边界:什么时候不该用结构化输出

结构化输出不是万能的,有些场景它不适用:

1. 创意生成场景

写文章、写邮件这类需要自由发挥的任务,强制结构化反而会降低质量。

2. 推理链输出

Chain-of-Thought 推理过程中,中间步骤的文本是”思考过程”,不适合结构化。最终结论可以结构化,但过程不应该。

3. 多模态输出

如果输出包含图片、代码块、表格混合,结构化会很复杂。这时候用 markdown 格式更合适。

4. 实时流式场景

结构化输出需要等完整 JSON 才能解析,不适合需要逐字输出的场景(如聊天机器人)。

未来展望

结构化输出技术还在快速演进:

  • 原生结构化支持:OpenAI、Anthropic 等模型提供商正在原生支持 JSON Schema 约束,不再依赖 prompt engineering
  • 更强的类型系统:未来可能支持更复杂的类型(Union、Intersection、递归类型)
  • 与 Function Calling 深度融合:结构化输出和工具调用的边界会越来越模糊
  • 本地模型支持:随着 llama.cpp 等项目支持 grammar-based decoding,本地模型也能做结构化输出

作为 Java 开发者,现在掌握 Spring AI 的结构化输出,就是在为这些未来变化打基础。

总结

结构化输出是AI Agent开发中的”基础设施”。它看起来不起眼,但决定了你的Agent能不能跟外部世界可靠地对接。

Spring AI通过 StructuredOutputConverter 把这件事做得很优雅——你只需要定义好Java类,剩下的提示词构造、Schema生成、输出解析、异常重试,框架都帮你搞定了。

如果你正在用Java做AI Agent开发,结构化输出应该是你最先掌握的技能之一。毕竟,一个连话都说不清楚的Agent,再聪明也没法用。

本文基于Spring AI 1.0+,相关代码已在JDK 21 + Spring Boot 3.4环境下验证通过。