前言

前面我们已经讲了 Prompt Engineering。

Prompt 可以让模型更容易按要求输出,但 Agent 项目不能只依赖“模型看起来回答得不错”。

因为后端真正需要处理的是确定的数据。

例如用户说:

帮我查订单 10001 的物流。

如果模型只返回:

用户想查询订单物流。

这个结果对后端来说还不够。

后端更希望拿到:

{
  "intent": "QUERY_LOGISTICS",
  "orderNo": "10001",
  "needMoreInfo": false
}

这样系统才能继续决定:

  1. 调用哪个工具
  2. 传什么参数
  3. 是否需要追问用户
  4. 是否允许执行当前操作
  5. 如何记录日志和审计信息

这一篇我们学习结构化输出,让模型结果从“自然语言”变成“后端可校验、可处理、可追踪的数据”。


一、什么是结构化输出

结构化输出指的是:让模型按照预先定义的数据结构返回结果。

常见格式包括:

JSON
枚举
表格
固定字段
函数参数
工具调用参数

例如意图识别任务。

用户输入:

订单 10001 到哪了?

模型输出:

{
  "intent": "QUERY_LOGISTICS",
  "orderNo": "10001",
  "needMoreInfo": false
}

再比如工单创建任务。

用户输入:

我无法登录系统,今天上午开始一直提示密码错误。

模型输出:

{
  "category": "LOGIN",
  "priority": "HIGH",
  "summary": "用户无法登录系统,持续提示密码错误",
  "needManualReview": false
}

文字说明

结构化输出的重点不是“让模型输出 JSON 很酷”。

而是让模型的结果能够进入后端业务流程。

自然语言 -> 结构化结果 -> 参数校验 -> 业务处理 -> 最终响应

二、为什么 Agent 一定要重视结构化输出

如果只让模型返回自然语言,后端很难稳定处理。

例如下面两段回答表达的是同一个意思:

用户想看订单物流。
该请求属于物流查询,订单编号为 10001。
建议调用订单物流接口。

后端如果想从这些文本中提取订单号和意图,就又要调用一次模型,或者写大量字符串判断。

这会带来:

  1. 逻辑不稳定
  2. 规则难维护
  3. 异常场景难处理
  4. 无法做严格校验
  5. 无法安全调用工具
  6. 日志和审计不清晰

结构化输出可以把结果变成明确字段:

{
  "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 数据的规则说明。

它可以定义:

  1. 哪些字段必须存在
  2. 字段是什么类型
  3. 枚举值有哪些
  4. 字符串格式是什么
  5. 数字范围是多少
  6. 是否允许额外字段

订单意图识别的 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 次
失败后走降级逻辑

否则可能出现:

  1. Token 成本不断增加
  2. 用户一直等待
  3. Agent 进入循环
  4. 系统响应变慢

十一、结构化输出的降级策略

如果模型连续输出失败,可以采用降级方案。

例如:

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. 为什么不能直接相信模型参数

因为模型可能:

  1. 提取错订单号
  2. 错把用户名当订单号
  3. 编造不存在的 ID
  4. 给出越权操作参数
  5. 混淆日期和金额

所以工具执行前仍然需要参数校验和权限校验。


十五、实际开发建议

  1. 需要进入后端流程的模型结果,优先使用结构化输出。

  2. 输出结果至少经过 JSON 解析、字段校验、业务校验三层处理。

  3. 高风险操作必须在结构化结果校验通过后才允许继续执行。

  4. 模型修复失败后要有降级策略,不能无限重试。

  5. 将失败输出记录下来,后续用于优化 Prompt 和测试集。

  6. 不同模型平台的结构化输出能力不同,业务层应依赖自己的 DTO 和校验规则,而不是直接绑定某个平台格式。


十六、总结

这一篇我们完成了从“模型自然语言回答”到“后端可处理业务结果”的关键一步。

核心流程是:

模型输出
    |
JSON 解析
    |
字段校验
    |
业务校验
    |
工具调用或业务处理

结构化输出让 Agent 的行为更加可控,也让工具调用、工作流路由、意图识别和任务执行具备工程化基础。

下一篇我们开始学习 Tool Calling,让 Agent 根据结构化决策调用真实、受控的业务能力。

Logo

Agent 垂直技术社区,欢迎活跃、内容共建。

更多推荐