结构化输出:让 Agent 稳定返回 JSON、字段和业务结果
前言
前面我们已经讲了 Prompt Engineering。
Prompt 可以让模型更容易按要求输出,但 Agent 项目不能只依赖“模型看起来回答得不错”。
因为后端真正需要处理的是确定的数据。
例如用户说:
帮我查订单 10001 的物流。
如果模型只返回:
用户想查询订单物流。
这个结果对后端来说还不够。
后端更希望拿到:
{
"intent": "QUERY_LOGISTICS",
"orderNo": "10001",
"needMoreInfo": false
}
这样系统才能继续决定:
- 调用哪个工具
- 传什么参数
- 是否需要追问用户
- 是否允许执行当前操作
- 如何记录日志和审计信息
这一篇我们学习结构化输出,让模型结果从“自然语言”变成“后端可校验、可处理、可追踪的数据”。
一、什么是结构化输出
结构化输出指的是:让模型按照预先定义的数据结构返回结果。
常见格式包括:
JSON
枚举
表格
固定字段
函数参数
工具调用参数
例如意图识别任务。
用户输入:
订单 10001 到哪了?
模型输出:
{
"intent": "QUERY_LOGISTICS",
"orderNo": "10001",
"needMoreInfo": false
}
再比如工单创建任务。
用户输入:
我无法登录系统,今天上午开始一直提示密码错误。
模型输出:
{
"category": "LOGIN",
"priority": "HIGH",
"summary": "用户无法登录系统,持续提示密码错误",
"needManualReview": false
}
文字说明
结构化输出的重点不是“让模型输出 JSON 很酷”。
而是让模型的结果能够进入后端业务流程。
自然语言 -> 结构化结果 -> 参数校验 -> 业务处理 -> 最终响应
二、为什么 Agent 一定要重视结构化输出
如果只让模型返回自然语言,后端很难稳定处理。
例如下面两段回答表达的是同一个意思:
用户想看订单物流。
该请求属于物流查询,订单编号为 10001。
建议调用订单物流接口。
后端如果想从这些文本中提取订单号和意图,就又要调用一次模型,或者写大量字符串判断。
这会带来:
- 逻辑不稳定
- 规则难维护
- 异常场景难处理
- 无法做严格校验
- 无法安全调用工具
- 日志和审计不清晰
结构化输出可以把结果变成明确字段:
{
"intent": "QUERY_LOGISTICS",
"orderNo": "10001"
}
后端就可以根据 intent 决定工具,根据 orderNo 查询真实数据。
三、一个意图识别的完整例子
假设我们要做订单 Agent,支持以下意图:
QUERY_ORDER 查询订单
QUERY_LOGISTICS 查询物流
APPLY_REFUND 申请退款
CREATE_TICKET 创建售后工单
UNKNOWN 无法识别
先定义 Java 枚举。
package com.example.agent.model;
public enum OrderIntent {
QUERY_ORDER,
QUERY_LOGISTICS,
APPLY_REFUND,
CREATE_TICKET,
UNKNOWN
}
再定义模型输出对象。
package com.example.agent.model;
import jakarta.validation.constraints.NotNull;
import jakarta.validation.constraints.Pattern;
public record OrderIntentResult(
@NotNull(message = "intent 不能为空")
OrderIntent intent,
@Pattern(
regexp = "^\\d{1,20}$",
message = "订单号格式不正确"
)
String orderNo,
@NotNull(message = "needMoreInfo 不能为空")
Boolean needMoreInfo,
String questionToUser
) {
}
文字说明
这里有几个字段。
intent
表示用户意图。
orderNo
表示提取到的订单号。
needMoreInfo
表示是否还需要用户补充信息。
questionToUser
如果信息不足,可以告诉前端应该追问什么。
例如用户只说:
帮我查一下订单。
模型可以返回:
{
"intent": "QUERY_ORDER",
"orderNo": null,
"needMoreInfo": true,
"questionToUser": "请提供订单号。"
}
四、Prompt 中如何约束 JSON 输出
如果模型平台没有提供专门的结构化输出能力,可以先通过 Prompt 约束。
你是订单意图识别器。
任务:
根据用户输入识别订单相关意图,并提取订单号。
输出规则:
1. 只输出一个合法 JSON 对象。
2. 不要输出 Markdown 代码块。
3. 不要输出解释文字。
4. intent 只能是:
QUERY_ORDER、
QUERY_LOGISTICS、
APPLY_REFUND、
CREATE_TICKET、
UNKNOWN。
5. 无法确认订单号时,orderNo 返回 null。
6. 信息不足时,needMoreInfo 返回 true,并填写 questionToUser。
7. 不得编造订单号。
用户输入:
订单 10001 的物流到哪里了?
期望输出:
{
"intent": "QUERY_LOGISTICS",
"orderNo": "10001",
"needMoreInfo": false,
"questionToUser": null
}
文字说明
Prompt 约束可以提高模型返回合法 JSON 的概率。
但它不能保证 100% 成功。
模型仍然可能输出:
下面是识别结果:
{
"intent": "QUERY_LOGISTICS"
}
所以后端必须继续做解析和校验。
五、JSON Schema 是什么
JSON Schema 可以理解为 JSON 数据的规则说明。
它可以定义:
- 哪些字段必须存在
- 字段是什么类型
- 枚举值有哪些
- 字符串格式是什么
- 数字范围是多少
- 是否允许额外字段
订单意图识别的 Schema 可以写成:
{
"type": "object",
"additionalProperties": false,
"required": [
"intent",
"orderNo",
"needMoreInfo",
"questionToUser"
],
"properties": {
"intent": {
"type": "string",
"enum": [
"QUERY_ORDER",
"QUERY_LOGISTICS",
"APPLY_REFUND",
"CREATE_TICKET",
"UNKNOWN"
]
},
"orderNo": {
"type": [
"string",
"null"
]
},
"needMoreInfo": {
"type": "boolean"
},
"questionToUser": {
"type": [
"string",
"null"
]
}
}
}
文字说明
很多模型平台支持通过 JSON Schema 或类似能力约束输出。
它比单纯在 Prompt 中写:
请输出 JSON
更稳定。
不过不同模型平台的参数名称、SDK 写法不完全一样。
实际接入时,需要根据当前模型平台的文档和 SDK 进行适配。
六、模型输出后的第一层校验:JSON 解析
模型返回 JSON 后,后端第一步是解析。
package com.example.agent.service;
import com.example.agent.model.OrderIntentResult;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
@Service
@RequiredArgsConstructor
public class AgentResultParser {
private final ObjectMapper objectMapper;
public OrderIntentResult parseOrderIntent(String modelOutput) {
try {
return objectMapper.readValue(modelOutput, OrderIntentResult.class);
} catch (JsonProcessingException e) {
throw new IllegalArgumentException("模型输出不是合法 JSON", e);
}
}
}
文字说明
这里使用 Jackson 的:
objectMapper.readValue(...)
将模型输出转换成 Java 对象。
如果模型输出不是合法 JSON,例如:
订单意图是 QUERY_ORDER。
就会抛出异常。
这一步只能证明:
输出格式像 JSON
还不能证明业务字段正确。
七、第二层校验:字段和格式校验
解析成功后,还需要校验字段规则。
package com.example.agent.service;
import com.example.agent.model.OrderIntentResult;
import jakarta.validation.ConstraintViolation;
import jakarta.validation.Validator;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
import java.util.Set;
@Service
@RequiredArgsConstructor
public class AgentResultValidator {
private final Validator validator;
public void validateOrderIntent(OrderIntentResult result) {
Set<ConstraintViolation<OrderIntentResult>> violations = validator.validate(result);
if (!violations.isEmpty()) {
String message = violations.iterator().next().getMessage();
throw new IllegalArgumentException("模型输出字段校验失败:" + message);
}
}
}
文字说明
这里使用了前面项目扩展系列讲过的 Validation。
例如模型返回:
{
"intent": "QUERY_LOGISTICS",
"orderNo": "abc",
"needMoreInfo": false,
"questionToUser": null
}
虽然它是合法 JSON,但订单号不符合:
纯数字,长度 1 到 20 位
就应该被拒绝。
这说明:
合法 JSON != 合法业务结果
八、第三层校验:业务规则校验
即使字段格式正确,也不代表业务合理。
例如模型返回:
{
"intent": "QUERY_LOGISTICS",
"orderNo": null,
"needMoreInfo": false,
"questionToUser": null
}
这在格式上没有问题,但物流查询通常需要订单号。
可以继续做业务校验:
package com.example.agent.service;
import com.example.agent.model.OrderIntent;
import com.example.agent.model.OrderIntentResult;
import org.springframework.stereotype.Service;
@Service
public class AgentBusinessValidator {
public void validateOrderIntentBusiness(OrderIntentResult result) {
boolean needOrderNo = result.intent() == OrderIntent.QUERY_ORDER
|| result.intent() == OrderIntent.QUERY_LOGISTICS
|| result.intent() == OrderIntent.APPLY_REFUND;
if (needOrderNo
&& result.orderNo() == null
&& !Boolean.TRUE.equals(result.needMoreInfo())) {
throw new IllegalArgumentException("当前意图缺少订单号,且未要求用户补充信息");
}
}
}
文字说明
模型输出需要经过三层检查:
第一层:是不是合法 JSON
第二层:字段格式是否合法
第三层:业务逻辑是否合理
最终才能进入后续工具调用。
九、完整处理流程
整个结构化输出流程可以理解成:
用户输入
|
模型生成 JSON
|
JSON 解析
|
字段格式校验
|
业务规则校验
|
决定是否调用工具
|
返回结果或要求用户补充信息
例如用户输入:
帮我查订单 10001 的物流。
模型输出:
{
"intent": "QUERY_LOGISTICS",
"orderNo": "10001",
"needMoreInfo": false,
"questionToUser": null
}
后端校验通过后,才可以调用:
queryOrderLogistics(orderNo = 10001)
十、解析失败后能不能让模型修复
可以,但必须限制次数。
例如第一次模型输出:
订单号是 10001,意图是查询物流。
后端解析失败后,可以再发一次受控提示:
上一次输出不符合 JSON 格式。
请严格按照以下字段重新输出合法 JSON:
intent、orderNo、needMoreInfo、questionToUser。
不要输出任何解释文字。
文字说明
重试可以提高成功率,但不能无限重试。
建议:
最多修复 1 次
最多重试 1 次
失败后走降级逻辑
否则可能出现:
- Token 成本不断增加
- 用户一直等待
- Agent 进入循环
- 系统响应变慢
十一、结构化输出的降级策略
如果模型连续输出失败,可以采用降级方案。
例如:
1. 返回“当前无法识别请求,请换一种表达方式”
2. 转人工处理
3. 使用简单规则兜底
4. 只返回普通文本回答,不执行工具
5. 记录失败样本,用于后续优化 Prompt
对于涉及高风险操作的场景,例如:
退款
支付
删除数据
修改权限
创建订单
如果结构化输出不通过,不应该继续执行操作。
应该直接停止并提示用户重新确认。
十二、结构化输出适合哪些场景
1. 意图识别
{
"intent": "QUERY_ORDER"
}
2. 参数提取
{
"orderNo": "10001",
"phone": null
}
3. 工单分类
{
"category": "LOGIN",
"priority": "HIGH"
}
4. 内容审核
{
"riskLevel": "MEDIUM",
"reason": "包含疑似敏感信息"
}
5. 工具调用参数
{
"tool": "queryProductStock",
"arguments": {
"productId": 1001,
"warehouseId": 2
}
}
6. 工作流路由
{
"nextStep": "HUMAN_REVIEW"
}
十三、什么时候不适合强行结构化输出
不是所有场景都需要 JSON。
例如:
总结一篇文章
生成产品介绍文案
解释一个技术概念
进行普通对话
这些场景的核心是自然语言表达。
如果强行要求复杂 JSON,反而会增加开发和维护成本。
一个实用判断标准:
后端需要继续根据模型结果执行确定逻辑 -> 使用结构化输出
只需要展示给用户阅读 -> 自然语言输出即可
十四、常见问题
1. 模型返回 Markdown 代码块怎么办
例如:
{
"intent": "QUERY_ORDER"
}
可以在 Prompt 中明确禁止代码块。
后端也可以在解析前做有限处理,例如去掉最外层 Markdown 标记。
但不要写过于宽松的字符串修复逻辑,否则会掩盖模型输出问题。
2. 模型返回了不存在的枚举值
例如:
{
"intent": "CHECK_DELIVERY"
}
但系统只支持:
QUERY_LOGISTICS
这时应该通过枚举反序列化或字段校验拒绝结果,而不是猜测它想表达什么。
3. 模型输出合法 JSON,但字段少了
例如:
{
"intent": "QUERY_ORDER"
}
可以通过 JSON Schema、Java Validation 或必填字段校验拦截。
4. 为什么不能直接相信模型参数
因为模型可能:
- 提取错订单号
- 错把用户名当订单号
- 编造不存在的 ID
- 给出越权操作参数
- 混淆日期和金额
所以工具执行前仍然需要参数校验和权限校验。
十五、实际开发建议
-
需要进入后端流程的模型结果,优先使用结构化输出。
-
输出结果至少经过 JSON 解析、字段校验、业务校验三层处理。
-
高风险操作必须在结构化结果校验通过后才允许继续执行。
-
模型修复失败后要有降级策略,不能无限重试。
-
将失败输出记录下来,后续用于优化 Prompt 和测试集。
-
不同模型平台的结构化输出能力不同,业务层应依赖自己的 DTO 和校验规则,而不是直接绑定某个平台格式。
十六、总结
这一篇我们完成了从“模型自然语言回答”到“后端可处理业务结果”的关键一步。
核心流程是:
模型输出
|
JSON 解析
|
字段校验
|
业务校验
|
工具调用或业务处理
结构化输出让 Agent 的行为更加可控,也让工具调用、工作流路由、意图识别和任务执行具备工程化基础。
下一篇我们开始学习 Tool Calling,让 Agent 根据结构化决策调用真实、受控的业务能力。
更多推荐


所有评论(0)