从 Java 类型自动生成 JSON Schema,注入提示词,解析 LLM 输出为强类型对象
LLM 默认返回自由文本(natural language),但很多场景需要机器可解析的结构化数据。结构化输出就是通过在提示词中注入 JSON Schema,引导 LLM 返回符合特定格式的 JSON,再将其反序列化为强类型 Java 对象。
结构化输出端到端流程:
┌─────────────┐ ┌──────────────────┐ ┌───────────────────┐ ┌─────────────────┐
│ 1. 配置 │ │ 2. 生成 Schema │ │ 3. 注入 System │ │ 4. 解析输出 │
│ outputType │ ──→ │ Java Class → │ ──→ │ Prompt │ ──→ │ JSON → Object │
│ = Report.class│ │ JSON Schema │ │ │ │ │
└─────────────┘ └──────────────────┘ └───────────────────┘ └─────────────────┘
详细步骤:
Step 1: Agent 配置指定 outputType = Report.class
Step 2: BeanOutputConverter 自动分析 Report 类的字段
生成 JSON Schema:
{
"type": "object",
"properties": {
"title": { "type": "string" },
"score": { "type": "integer" },
"tags": { "type": "array", "items": { "type": "string" } }
},
"required": ["title", "score"]
}
Step 3: OutputSchemaInjector 将 Schema 追加到 System Prompt:
"... 你的回答必须严格遵循以下 JSON Schema: {schema}"
Step 4: StructuredOutputConverter 解析 LLM 返回的 JSON
→ 剥离可能的 markdown 代码块
→ 反序列化为 Report 对象
静态工具类,负责将 JSON Schema 注入到 System Prompt 中。
public class OutputSchemaInjector {
// 注入模板
private static final String SCHEMA_TEMPLATE = """
你的回答必须是一个严格符合以下 JSON Schema 的 JSON 对象。
不要包含任何额外文字说明,只输出纯 JSON。
JSON Schema:
%s
""";
// 两种 Schema 来源:
// 1. 自动生成: 从 Java Class 自动推导
public static String inject(String systemPrompt, Class<?> outputType) {
BeanOutputConverter<?> converter = new BeanOutputConverter<>(outputType);
String schema = converter.getJsonSchema();
return systemPrompt + String.format(SCHEMA_TEMPLATE, schema);
}
// 2. 手动指定: 直接传入 schema 字符串
public static String inject(String systemPrompt, String schemaJson) {
return systemPrompt + String.format(SCHEMA_TEMPLATE, schemaJson);
}
}
泛型转换器,包装 Spring AI 的 BeanOutputConverter,增加了 markdown 代码块剥离和优雅降级能力。
public class StructuredOutputConverter<T> {
private final BeanOutputConverter<T> delegate;
public StructuredOutputConverter(Class<T> targetType) {
this.delegate = new BeanOutputConverter<>(targetType);
}
public T convert(String llmOutput) {
try {
// Step 1: 剥离 markdown 代码块(如果存在)
String cleanJson = stripMarkdownCodeBlock(llmOutput);
// Step 2: 委托给 BeanOutputConverter 解析
return delegate.convert(cleanJson);
} catch (Exception e) {
// Step 3: 优雅降级 — 返回 null,不抛异常
log.warn("结构化输出解析失败: {}", e.getMessage());
return null;
}
}
public String getJsonSchema() {
return delegate.getJsonSchema();
}
}
| 特性 | 说明 |
|---|---|
| Markdown 剥离 | 自动去除 ```json...``` 包裹 |
| 优雅降级 | 解析失败返回 null,不抛出异常 |
| 泛型支持 | 支持任意 Java 类型作为目标类型 |
| Schema 获取 | 代理 BeanOutputConverter 的 Schema 生成 |
LLM 经常在 JSON 外面包裹 markdown 代码块,即使提示词明确要求"不要包含额外文字"。这是一个常见的 LLM 行为,需要在解析前主动处理。
// LLM 实际输出(常见情况):
```json
{"title": "分析报告", "score": 85, "tags": ["AI", "Agent"]}
```
// stripMarkdownCodeBlock 处理逻辑:
private String stripMarkdownCodeBlock(String text) {
if (text == null) return null;
String trimmed = text.trim();
// 检测是否以 ``` 开头
if (!trimmed.startsWith("```")) return trimmed;
// 找到第一个换行符(跳过 ```json / ```JSON 等标记)
int firstNewline = trimmed.indexOf('\n');
if (firstNewline == -1) return trimmed;
// 截取 ``` 之后、结尾 ``` 之前的内容
String content = trimmed.substring(firstNewline + 1);
if (content.endsWith("```")) {
content = content.substring(0, content.length() - 3);
}
return content.trim();
}
// 处理后的干净 JSON:
{"title": "分析报告", "score": 85, "tags": ["AI", "Agent"]}
```json...``` 代码块包裹 JSON 输出。这是因为 LLM 在训练数据中看到大量 markdown 格式的代码展示,形成了"展示代码时用代码块包裹"的强烈倾向。与其依赖提示词控制 LLM 行为,不如在解析端做防御性处理。
StructuredOutputConverter 是 Spring AI BeanOutputConverter 的增强包装器(Decorator 模式)。BeanOutputConverter 提供了核心能力:Java Class → JSON Schema 生成、JSON → Java Object 反序列化。我们在此基础上增加了两个关键能力:① markdown 代码块剥离(处理 LLM 的常见行为);② 优雅降级(catch 异常返回 null)。这使得在生产环境中使用结构化输出更加健壮。