结构化输出
大约 4 分钟
第六章:结构化输出与JSON约束
6.1 为什么结构化输出是“从 Demo 到生产”的分水岭?
如果你只把模型当成“返回一段字符串的黑盒”,你很快会遇到:
- 想做自动化:需要把模型结果写入数据库、触发工作流、生成工单
- 想做治理:需要判断风险等级、命中哪个策略、是否允许工具执行
- 想做稳定性:需要准确区分“模型没回答好”还是“解析失败”
这些都要求模型输出可稳定解析的数据结构(通常是 JSON)。
6.2 一个可复制运行的例子:把需求文本解析成任务卡片
目标:用户输入一段需求描述,模型输出一个 JSON 任务卡片,包含字段:
titlepriority(P0/P1/P2)owneracceptanceCriteria(验收标准数组)
6.2.1 数据结构
package com.example.saa.api.dto;
import java.util.List;
public record TaskCard(
String title,
String priority,
String owner,
List<String> acceptanceCriteria
) {
}6.2.2 Controller:强约束输出为 JSON,并做解析兜底
package com.example.saa.api;
import com.example.saa.api.dto.TaskCard;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import java.util.Objects;
import org.springframework.ai.chat.client.ChatClient;
import org.springframework.http.MediaType;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RestController;
@RestController
public class StructuredOutputController {
private final ChatClient chatClient;
private final ObjectMapper objectMapper;
public StructuredOutputController(ChatClient chatClient, ObjectMapper objectMapper) {
this.chatClient = chatClient;
this.objectMapper = objectMapper;
}
@PostMapping(value = "/tasks/parse", consumes = MediaType.TEXT_PLAIN_VALUE, produces = MediaType.APPLICATION_JSON_VALUE)
public TaskCard parse(@RequestBody String requirement) throws JsonProcessingException {
String raw = callModel(requirement);
String json = extractJsonObject(raw);
try {
return objectMapper.readValue(json, TaskCard.class);
} catch (JsonProcessingException first) {
String repaired = callModelRepair(requirement, raw);
String repairedJson = extractJsonObject(repaired);
return objectMapper.readValue(repairedJson, TaskCard.class);
}
}
private String callModel(String requirement) {
return chatClient.prompt()
.system("""
你是一个需求分析助手。
你必须只输出一个 JSON 对象,不要输出任何额外文本,不要使用 Markdown 代码块。
JSON 字段:
- title: string
- priority: "P0" | "P1" | "P2"
- owner: string
- acceptanceCriteria: string array
""")
.user(Objects.toString(requirement, ""))
.call()
.content();
}
private String callModelRepair(String requirement, String badOutput) {
return chatClient.prompt()
.system("""
你是一个严格的 JSON 修复器。
你必须输出一个 JSON 对象,并且保证能被标准 JSON 解析器解析。
不要输出任何额外文本,不要使用 Markdown。
""")
.user("""
需求文本:
%s
上一次输出(不合规):
%s
请只返回修复后的 JSON:
""".formatted(Objects.toString(requirement, ""), Objects.toString(badOutput, "")))
.call()
.content();
}
private String extractJsonObject(String text) {
if (text == null) {
throw new IllegalArgumentException("模型输出为空");
}
int start = text.indexOf('{');
int end = text.lastIndexOf('}');
if (start < 0 || end < 0 || end <= start) {
throw new IllegalArgumentException("未找到 JSON 对象边界");
}
return text.substring(start, end + 1).trim();
}
}6.2.3 测试请求
curl -X POST http://localhost:8080/tasks/parse \
-H 'Content-Type: text/plain' \
--data '我们要做一个订单查询页:支持按订单号搜索、展示状态与物流信息。优先级最高,负责人是 Alice。验收标准:能查到A1001;错误订单号给出友好提示;接口耗时P95小于200ms。'6.3 让结构化输出更稳的三条经验
6.3.1 把“输出格式约束”写进 system
不要把约束放在 user 里。system 更稳定、更像规则层。
6.3.2 字段少而清晰
字段越多、越复杂,模型越容易漏字段或输出不合法 JSON。建议:
- 先把最关键字段跑通(title/priority)
- 再逐步加字段(验收标准、风险、依赖)
6.3.3 解析失败要有兜底与治理
生产里解析失败不是“异常”,而是常态风险。建议至少做:
- 抽取 JSON 边界(避免模型夹杂额外文字导致解析失败)
- 修复重试(让模型把“坏输出”修成“可解析 JSON”)
- 失败归因(记录:哪个模型、哪个 prompt、失败率)
6.4 本章小结
你已经掌握了把模型输出变成“可编程结果”的核心套路:
- 强约束:system 中要求只输出 JSON
- 可解析:ObjectMapper 解析到 record
- 可治理:解析失败自动修复重试
下一章我们会进入 RAG:用 DashScope Embeddings 把你的知识变成可检索上下文,解决“模型幻觉”和“答非所问”的生产痛点。 点击这里👇🏻获取:100万QPS短链系统、复杂的商城微服务系统、智能翻译助手AI Agent、SaaS点餐系统、刷题吧小程序、商城系统、秒杀系统、AI项目、代码生成神器、苏三demo项目、智能天气播报AI Agent、智能代码审查AI Agent等 10 个项目的:项目源代码、开发教程和技术答疑
