第2035篇:LLM输出格式控制——让模型输出稳定的结构化数据
2026/4/30大约 6 分钟
第2035篇:LLM输出格式控制——让模型输出稳定的结构化数据
适读人群:需要将LLM输出集成到业务系统的工程师 | 阅读时长:约17分钟 | 核心价值:掌握控制LLM输出格式的多种技术,提升LLM在流水线中的可靠性
LLM用在生产系统里,最让工程师头疼的不是模型能力,而是输出格式不稳定。
同样的prompt,有时候返回正确的JSON,有时候返回包含解释文字的JSON,有时候直接返回纯文本。下游的JSON解析代码一遇到这种情况就崩了。
我们试过各种提示词,最后发现"光靠提示词不够可靠"——必须在技术层面强制控制输出格式。
为什么光靠Prompt不够
"请只输出JSON,不要输出其他内容"——这句话绝大多数时候有效,但偶尔会有例外:
- 模型有时候会在JSON前面加上"以下是JSON结果:"
- 有时候JSON后面会加上"如需进一步说明请告知"
- 格式复杂时,字段名可能有微小差异(
userIdvsuser_id)
这些偶发的格式错误,在高并发系统里会不断触发,累积成稳定的报错率。
技术1:JSON Mode强制模式
主流模型API和推理框架都支持JSON Mode,强制模型只输出合法JSON:
@Service
@RequiredArgsConstructor
public class JsonModeService {
private final OpenAiChatModel openAiChatModel;
/**
* 使用JSON Mode强制JSON输出
* 零解析失败率
*/
public <T> T extractStructured(String prompt, Class<T> targetClass) {
// OpenAI SDK的JSON mode配置
OpenAiChatOptions options = OpenAiChatOptions.builder()
.model("gpt-4o-mini")
.responseFormat(new ResponseFormat(ResponseFormat.Type.JSON_OBJECT))
.build();
ChatResponse response = openAiChatModel.call(
new Prompt(prompt, options));
String jsonContent = response.getResult().getOutput().getContent();
try {
return objectMapper.readValue(jsonContent, targetClass);
} catch (JsonProcessingException e) {
// JSON Mode下这种情况应该极少发生
log.error("JSON解析失败(JSON Mode下): {}", jsonContent);
throw new LlmOutputParseException("JSON解析失败", e);
}
}
}// Spring AI中的JSON mode示例
@Service
@RequiredArgsConstructor
public class StructuredExtractionService {
private final ChatClient chatClient;
/**
* 抽取合同关键信息
* 使用JSON Mode + Schema约束
*/
public ContractInfo extractContractInfo(String contractText) {
String systemPrompt = """
你是合同信息抽取专家。
请从合同文本中抽取关键信息,严格按照以下JSON格式输出:
{
"partyA": "甲方名称",
"partyB": "乙方名称",
"contractAmount": 金额数字(不含单位),
"currency": "货币单位(CNY/USD等)",
"startDate": "起始日期(YYYY-MM-DD格式)",
"endDate": "结束日期(YYYY-MM-DD格式)",
"paymentTerms": "付款方式简述"
}
如果某字段无法确定,使用null。
""";
String response = chatClient.prompt()
.system(systemPrompt)
.user("合同内容:\n" + contractText)
.options(ChatOptions.builder()
.responseFormat("json_object") // Spring AI的JSON mode
.build())
.call()
.content();
try {
return objectMapper.readValue(response, ContractInfo.class);
} catch (JsonProcessingException e) {
log.error("合同信息解析失败: {}", response);
throw new ParseException("合同信息解析失败", e);
}
}
}技术2:Structured Output(模式约束)
OpenAI和部分模型支持更严格的Structured Output,不只是JSON格式,还能约束字段名和类型:
@Service
@RequiredArgsConstructor
public class SchemaConstrainedOutputService {
private final OpenAiChatModel chatModel;
/**
* 使用JSON Schema约束输出结构
* 确保字段名、类型、必填项都符合预期
*/
public RiskAssessmentResult assessRisk(String content) {
// 定义输出Schema
String outputSchema = """
{
"type": "object",
"properties": {
"riskLevel": {
"type": "string",
"enum": ["HIGH", "MEDIUM", "LOW"]
},
"score": {
"type": "number",
"minimum": 0,
"maximum": 10
},
"mainRisks": {
"type": "array",
"items": {"type": "string"},
"maxItems": 5
},
"recommendation": {"type": "string"}
},
"required": ["riskLevel", "score", "mainRisks", "recommendation"]
}
""";
OpenAiChatOptions options = OpenAiChatOptions.builder()
.model("gpt-4o")
// strict JSON schema约束(仅GPT-4o-2024-08-06+支持)
.responseFormat(ResponseFormat.builder()
.type(ResponseFormat.Type.JSON_SCHEMA)
.jsonSchema(new ResponseFormat.JsonSchema("RiskAssessment", outputSchema, true))
.build())
.build();
String prompt = "请分析以下内容的风险:\n" + content;
ChatResponse response = chatModel.call(new Prompt(prompt, options));
String jsonOutput = response.getResult().getOutput().getContent();
try {
return objectMapper.readValue(jsonOutput, RiskAssessmentResult.class);
} catch (JsonProcessingException e) {
throw new ParseException("结构化输出解析失败", e);
}
}
}技术3:函数调用(Function Calling)作为格式约束
把"提取信息"包装成一个工具调用,利用Function Calling的强制格式特性:
@Service
@RequiredArgsConstructor
public class FunctionCallOutputService {
private final ChatClient chatClient;
/**
* 用Function Calling提取信息
* 优点:格式100%可靠,无需解析纯文本
*/
public OrderInfo extractOrderInfo(String userMessage) {
// 定义信息提取工具
String toolDefinition = """
[{
"type": "function",
"function": {
"name": "extract_order_info",
"description": "从用户消息中提取订单相关信息",
"parameters": {
"type": "object",
"properties": {
"orderId": {
"type": "string",
"description": "订单编号"
},
"issueType": {
"type": "string",
"enum": ["shipping_delay", "product_defect", "wrong_item", "refund_request", "other"],
"description": "问题类型"
},
"urgency": {
"type": "string",
"enum": ["urgent", "normal"],
"description": "紧急程度"
}
},
"required": ["issueType"]
}
}
}]
""";
// 调用模型,强制调用这个工具
Map<String, Object> requestBody = Map.of(
"model", "gpt-4o-mini",
"messages", List.of(Map.of(
"role", "user",
"content", "请从这条消息中提取信息:" + userMessage)),
"tools", parseJson(toolDefinition),
"tool_choice", Map.of("type", "function",
"function", Map.of("name", "extract_order_info"))
);
// 解析tool call参数(这部分格式是保证的)
Map<String, Object> response = callOpenAI(requestBody);
Map<String, Object> toolCallArgs = extractToolCallArgs(response);
return OrderInfo.builder()
.orderId((String) toolCallArgs.get("orderId"))
.issueType((String) toolCallArgs.get("issueType"))
.urgency((String) toolCallArgs.get("urgency"))
.build();
}
}技术4:对私有化模型的输出格式控制
对于私有化部署的开源模型,Structured Output的支持不如OpenAI完整,需要用其他方式:
/**
* 对开源模型(vLLM/Ollama)的格式控制
* 使用多重保险机制
*/
@Service
@RequiredArgsConstructor
@Slf4j
public class OpenModelOutputController {
private final ChatClient localLlmClient;
private final ObjectMapper objectMapper;
/**
* 带重试和修复的结构化输出
*/
public <T> T extractWithRetry(String prompt, Class<T> targetClass, int maxRetries) {
String enhancedPrompt = buildJsonEnforcingPrompt(prompt, targetClass);
for (int attempt = 1; attempt <= maxRetries; attempt++) {
try {
String response = localLlmClient.prompt()
.user(enhancedPrompt)
.call()
.content();
String cleanedJson = extractJsonFromResponse(response);
return objectMapper.readValue(cleanedJson, targetClass);
} catch (JsonProcessingException e) {
if (attempt == maxRetries) {
throw new LlmOutputParseException(
"JSON解析失败(已重试" + maxRetries + "次)", e);
}
log.warn("第{}次尝试JSON解析失败,准备重试", attempt);
enhancedPrompt = buildRepairPrompt(enhancedPrompt, e.getMessage());
}
}
throw new LlmOutputParseException("超出重试次数");
}
/**
* 从可能包含解释文字的响应中提取JSON
*/
private String extractJsonFromResponse(String response) {
// 处理常见的JSON包裹情况
String cleaned = response.trim();
// 去除 ```json ... ``` 包裹
if (cleaned.startsWith("```json")) {
cleaned = cleaned.substring(7);
} else if (cleaned.startsWith("```")) {
cleaned = cleaned.substring(3);
}
if (cleaned.endsWith("```")) {
cleaned = cleaned.substring(0, cleaned.length() - 3).trim();
}
// 如果还是有前缀文字,尝试找到第一个{
int jsonStart = cleaned.indexOf('{');
int jsonEnd = cleaned.lastIndexOf('}');
if (jsonStart != -1 && jsonEnd != -1 && jsonStart < jsonEnd) {
return cleaned.substring(jsonStart, jsonEnd + 1);
}
return cleaned;
}
/**
* 强化JSON输出的提示词
*/
private String buildJsonEnforcingPrompt(String taskPrompt, Class<?> targetClass) {
String schemaExample = generateSchemaExample(targetClass);
return String.format("""
%s
重要提示:
1. 请直接输出JSON,不要包含任何解释文字
2. 不要使用markdown代码块(不要有```)
3. 严格按照以下格式:
%s
现在请输出JSON:
""", taskPrompt, schemaExample);
}
/**
* 当JSON解析失败时,构建修复提示
*/
private String buildRepairPrompt(String originalPrompt, String errorMessage) {
return originalPrompt + String.format("""
注意:上次的JSON输出有格式错误(%s),
请确保输出是完全合法的JSON,不包含任何注释或额外内容。
""", errorMessage);
}
}格式可靠性从高到低的方案对比
| 方案 | 可靠性 | 适用场景 | 限制 |
|---|---|---|---|
| OpenAI Structured Output | ≈100% | 对OpenAI API | 需要特定模型版本 |
| Function Calling | ≈99% | 信息提取场景 | 需要设计工具定义 |
| JSON Mode | ≈98% | 通用JSON输出 | 不约束字段 |
| 提示词+提取+重试 | 90-95% | 私有化部署 | 有解析失败概率 |
| 纯提示词控制 | 70-85% | 开发测试 | 生产不建议 |
生产系统的建议:使用云API时优先选JSON Mode或Structured Output;使用私有化部署时,用"提示词+健壮的JSON提取+重试机制"三层保险。
