知识库AgentLLM 与上下文LLM 与上下文结构化输出与可靠解析结构化输出与可靠解析
01 · LLM 与上下文LLM 与上下文
Roadmap 01核心Markdown58 min

结构化输出与可靠解析结构化输出与可靠解析

区分 JSON 模式、Schema 约束与工具调用,建立解析、领域校验、授权、幂等和回退的完整 Java 边界。区分 JSON 模式、Schema 约束与工具调用,建立解析、领域校验、授权、幂等和回退的完整 Java 边界。

#JSON SchemaJSON Schema#Structured OutputStructured Output#Tool CallingTool Calling#GuardrailGuardrail#JavaJava更新于 2026-08-16

专题导读

结构化输出的目标不是让模型“像普通 API 一样可靠”,而是把概率生成结果收敛为可验证的不可信输入。一个字符串即使是合法 JSON,也可能缺字段、金额越界、引用其他租户资源,或在重试时造成重复扣款。

Java 后端应把链路拆成独立关卡:

响应状态 → JSON 语法 → Schema 结构 → DTO 映射 → 领域不变量 → 鉴权/策略 → 幂等执行 → 审计与回退

任一关卡失败都不能默认进入副作用。结构化输出改善的是接口形状与解析成功率,不自动提升事实正确性,也不替代数据库约束、权限系统和人工审批。

供应商能力必须逐项核对:自然语言要求“只输出 JSON”、JSON mode、严格 Schema 输出、grammar constrained decoding、function/tool calling 并不是同一个能力。不同 API 对 JSON Schema 关键字的支持往往只是标准子集,且拒答、内容过滤、达到长度上限或网络中断仍可能导致拿不到业务 payload。

知识地图

层次 要回答的问题 典型失败 责任组件
传输/模型状态 是否正常完成生成 超时、拒答、过滤、截断 模型适配器
JSON 语法 是否为可解析 JSON 引号缺失、半截对象、重复键 有界 JSON 解析器
Schema 结构 字段、类型、枚举、边界是否匹配 缺 required、额外字段、数组过长 Schema validator / DTO 映射
领域语义 跨字段和业务事实是否成立 结束早于开始、金额超额度 Java 领域服务
安全授权 当前主体是否可执行 跨租户、越权工具、SSRF 参数 认证授权与策略层
副作用 重试是否会重复执行 重复发信、重复扣款 幂等存储与执行器
运营治理 是否可诊断与演进 死循环重试、Schema 漂移 指标、版本、回退队列

面试题详解

Q1:JSON 提示、JSON mode、严格结构化输出和工具调用有什么区别?

**核心回答:**它们提供的保证层次不同,必须按目标供应商的正式文档和实测判断,不能统称为“Schema 约束”。

能力 常见含义 通常不能保证的内容
Prompt 要求只输出 JSON 自然语言引导格式 JSON 语法、字段完整、类型正确
JSON mode 供应商保证或显著约束输出为合法 JSON 一定匹配业务 Schema、领域正确
Strict structured output 在供应商支持的 Schema 子集内约束输出 事实正确、业务规则、一定有 payload
Function/tool calling 模型选择工具并产生参数 工具确实应执行、参数已授权、所有实现都用约束解码
Grammar constrained decoding 解码时只允许符合语法的 token 路径 Schema 外的业务语义和权限

即使严格模式能保证成功 payload 符合受支持的 Schema,模型仍可能拒答、被过滤、超时或达到输出上限。应用首先检查响应状态,再解析 payload。

面试追问:tool calling 是否一定等于 strict structured output? 不一定。有的 API 支持严格工具参数,有的仅将 Schema 作为引导;能力和限制必须读取具体 API 版本文档。

常见错误回答:“写上只返回 JSON 就不会解析失败”“所有 function calling 都在解码阶段完全满足 JSON Schema”。

Q2:JSON Schema 标准与供应商支持子集是什么关系?

**核心回答:**JSON Schema Draft 2020-12 是正式规范,定义 core、validation 等词汇;模型 API 可能只接受其中一部分关键字,甚至附加自己的要求。声明使用 JSON Schema 不代表实现了完整标准。

常用关键字包括 typepropertiesrequiredenumconstitemsminItems/maxItems、数值和字符串边界,以及 additionalProperties。递归引用、条件 Schema、复杂正则、format、组合关键字等支持情况差异较大。

additionalProperties: false 可减少模型生成未知字段,但会增加演进成本:旧 Schema 无法接受新字段,需要版本路由或兼容窗口。format: "date-time" 在标准生态中也可能只是 annotation,是否执行断言取决于词汇和实现;服务端仍应使用 java.time 严格解析。

面试追问:如何确认目标 API 支持哪些关键字? 维护契约测试:对每个实际 Schema 在 CI/预生产调用目标 API,覆盖正常输出、拒答、边界值和不支持关键字;供应商升级时回归。

常见错误回答:“符合 2020-12 的 Schema 可直接用于任何模型供应商”“format 一定像数据库约束一样强制验证”。

Q3:为什么合法 JSON 仍然远远不够?

**核心回答:**合法 JSON 只通过语法层。可靠业务至少需要五层正确性:语法、结构、领域语义、安全授权与副作用控制。

例如:

JSON
{
  "action": "REFUND",
  "orderId": "order-of-another-tenant",
  "amount": 999999999.99,
  "currency": "CNY"
}

它可能是合法 JSON,也可能满足字段类型;但订单不属于当前租户、金额超过原支付额、状态不可退款,因此不能执行。Schema 擅长局部形状和部分边界,无法独立判断实时数据库状态、调用者权限和跨字段业务规则。

建议每层返回稳定错误码,而不是把所有异常都归为 INVALID_JSON。这有助于决定哪些错误可修复重试、哪些必须拒绝或人工处理。

面试追问:事实正确性属于哪一层? 通常是领域/证据层,需要查询权威数据、规则或人工复核;Schema 只能限制表示形状。

常见错误回答:“反序列化成 DTO 就说明数据可信”“Schema 验证通过即可直接调支付接口”。

Q4:Java DTO 应如何设计 missing、null、未知字段和枚举?

**核心回答:**必须先定义契约语义,再显式配置解析器。missing 表示字段未出现,JSON null 表示明确空值,两者是否等价取决于业务;Java primitive 无法表达缺失,因此关键可选字段通常需要包装类型或专门的状态类型。

推荐做法:

  • DTO 使用不可变 record,并在构造器或独立 validator 中检查基本不变量;
  • 对危险枚举采取 fail-closed,未知值不自动映射为默认执行动作;
  • 不将缺失金额默认为 0,不将未知操作默认为 APPROVE
  • 明确未知字段策略:严格拒绝可发现 Schema 漂移,但需要版本化;宽松忽略有兼容性优势,也可能掩盖模型错误;
  • 区分输入 DTO、领域命令与持久化实体,避免模型直接控制内部字段。

Jackson、Jakarta JSON Processing 等不是 Java 21 标准库。实际项目可选成熟实现,但要锁定版本、显式配置并做契约测试;Spring Boot 的默认配置也可能与原生库不同。

面试追问:为什么不直接反序列化为 JPA Entity? Entity 含数据库 ID、版本、关系和敏感字段,扩大 mass assignment 风险;应通过窄 DTO 映射为服务端控制的命令。

常见错误回答:“record 自动完成所有校验”“未知枚举直接映射到第一个值最兼容”。

Q5:JSON 解析器需要哪些资源与安全限制?

**核心回答:**模型输出仍是外部输入。解析前后要限制字节数、字符数、嵌套深度、对象属性数、数组长度和字符串长度,防止内存/CPU 放大;还要明确重复键、数字精度和多态反序列化策略。

RFC 8259 规定对象成员名称应唯一;当出现重复名称时,不同实现可能取最后一个、报错或保留全部,行为不可互操作。安全敏感系统应配置拒绝重复键,避免校验器看到一个值、执行器使用另一个值。

金额使用 BigDecimal 并限制 scale/precision,不能先读成 double 再转换。整数还要检查目标 Java 类型范围。禁止让不可信 JSON 选择任意 Java 类,避免开启不受约束的 polymorphic typing。解析器错误消息回传模型前应脱敏,不能泄漏类名、文件路径或内部 Schema 细节。

面试追问:HTTP 层已有最大响应大小,解析器还需限制吗? 需要纵深防御;压缩传输、字符展开、内部重放或绕过 HTTP 的消息链路都可能使单层限制失效。

常见错误回答:“JSON 只是文本,不会造成资源攻击”“金额字段用 double 足够”。

Q6:严格 Schema 输出为什么仍要服务端校验?

**核心回答:**严格输出最多约束“成功生成的 payload 是否符合受支持 Schema”;服务端仍需防御供应商状态、集成错误、Schema 版本错配、业务规则和权限问题。

服务端至少应再次检查:

  1. 响应是否正常完成,是否被截断、拒答或过滤;
  2. 收到的 Schema 版本是否是调用方预期版本;
  3. 解析和 DTO 映射是否按本地契约成功;
  4. 数字、日期、ID 等是否满足本地约束;
  5. 引用资源是否存在、版本是否仍有效;
  6. 当前主体是否有权执行;
  7. 幂等键是否与规范化命令一致。

这不是不信任某个供应商,而是分布式系统边界的基本原则:本服务对自身不变量负责。即使上游是普通微服务,关键写入也要本地校验。

面试追问:重复校验是否浪费性能? 与模型调用延迟和副作用风险相比,本地结构/领域校验通常成本很低;高并发时仍可通过编译 Schema、缓存 validator 等优化。

常见错误回答:“strict=true 后可删除所有 validator”“受限解码保证模型引用的订单一定存在”。

Q7:领域校验、授权和模型判断应如何分工?

**核心回答:**模型适合提取意图与候选参数;Java 领域服务负责确定性不变量;授权系统决定当前主体能否作用于具体资源。模型不能授予权限,也不能成为金额、库存或状态转换的事实来源。

以退款为例:模型可把“给昨天重复扣款的订单退款”解析为候选订单和原因;服务端根据已认证用户查询可见订单,校验支付状态、可退余额、币种、时效和审批阈值,再生成领域命令。tenantIdoperatorId、角色和最终金额上限应来自服务端上下文与权威系统。

高风险场景可采用 plan-then-execute:模型只生成计划;服务端展示目标和影响,用户确认后重新读取状态、重新鉴权并执行。确认之前的授权结果可能因权限或资源状态变化而失效。

面试追问:模型输出的数据库主键能直接使用吗? 只能作为不可信候选;查询必须带当前租户/主体范围并校验资源归属,最好让模型选择服务端先提供的有限 opaque ID。

常见错误回答:“prompt 已说明最大退款额,所以无需代码校验”“模型判断用户是管理员即可放行”。

Q8:哪些错误可以修复重试,哪些不可以?

核心回答:只有可恢复、尚未产生副作用、重试有合理成功概率的错误才适合有限修复。先分类,再决定动作。

错误 默认策略
瞬时网络失败且确认未开始执行 带退避有限重试
JSON/Schema 小范围错误 若非 strict 模式,可携带最小错误信息修复 1~2 次
长度截断 缩小任务或提高合法上限后重新生成,不能拼接猜测
模型拒答/内容过滤 遵循策略,不通过“修复 prompt”绕过
领域条件不满足 请求用户补充或确定性拒绝,通常不让模型反复猜
鉴权失败 直接拒绝并审计,不重试提升权限
副作用结果未知 先按幂等键查询执行状态,不能盲目重发

修复提示只包含必要的稳定错误码和字段路径,避免把堆栈、SQL 或敏感值回传。每次重试有次数、token、时长预算,并保留原 request/operation ID。具体上限应由离线评测和任务风险确定;许多场景会采用 1~2 次作为保守起点,但这不是协议或行业标准。

面试追问:能否用正则自动补引号和括号? 对低风险展示文本可谨慎处理;业务命令中启发式修复可能改变语义,应优先重新生成或进入人工流程,并重新完整校验。

常见错误回答:“解析失败就无限让模型修复”“鉴权失败也可以把错误发给模型再试一次”。

Q9:如何保证模型驱动的副作用幂等?

**核心回答:**复用 request ID 本身不会自动产生幂等。执行服务必须持久化幂等键、规范化命令摘要、状态和结果,并在同一业务边界内原子地完成“占用键 + 状态转换”或使用数据库唯一约束协调。

一个幂等状态机可包含 STARTED / SUCCEEDED / FAILED_RETRYABLE / FAILED_FINAL / UNKNOWN。同一幂等键携带不同命令摘要必须拒绝,避免 key 被错误复用。并发重复请求要么等待首次结果,要么返回已知状态。

第三方超时最棘手:本地不知道对方是否已执行。若下游支持幂等键,应传递稳定键并查询结果;若不支持,需要业务对账/补偿,不能宣称端到端 exactly-once。消息队列的 at-least-once 投递也要求消费者幂等。

面试追问:幂等键如何生成? 由服务端绑定业务操作语义,例如租户、目标资源、操作类型和客户端 operation ID;避免直接使用模型随意生成的随机值。

常见错误回答:“加 UUID 就幂等了”“超时后换一个 request ID 重试最安全”。

Q10:Schema 如何演进而不破坏生产调用?

**核心回答:**Schema 是版本化 API 契约。新增 optional 字段通常较兼容,但严格 additionalProperties: false 的旧消费者仍会拒绝;新增枚举值对使用穷举 switch 的旧代码也可能是破坏性变更。

常见策略:

  • 请求携带 schemaVersion,模型响应也映射到该版本;
  • 重大变更发布新版本,不复用旧字段语义;
  • 迁移期双读旧/新 DTO,必要时由确定性转换器升级;
  • 先升级消费者接受新版本,再切生产者;
  • 为每个版本维护 golden cases、边界值和供应商能力契约测试;
  • 记录各版本流量,确认无调用后再下线。

不要让模型自由决定 Schema 版本;版本由调用方根据部署与业务能力选择。Schema 生成代码、Java DTO 和文档最好来自同一权威定义或通过契约测试防漂移。

面试追问:把字段从 optional 改为 required 是否兼容? 对旧生产者通常不兼容;需要默认迁移、双版本或先保证所有生产者都已发送该字段。

常见错误回答:“只加字段永远向后兼容”“枚举新增值不会影响 Java 代码”。

Q11:结构化输出应观测哪些指标?

**核心回答:**不仅看 JSON parse success,还要按失败层次和最终业务结果观测。

建议记录:模型/快照、promptVersion、schemaVersion、响应状态、输入/输出 token、原始输出字节数、解析结果、Schema 错误码与字段路径、领域错误码、鉴权结果、修复次数、最终回退、幂等命中和副作用结果。指标按模型、版本、任务、语言和租户等级切片,但避免把敏感原文做高基数 label。

关键指标包括:

  • 正常 payload 到达率;
  • JSON 解析失败率与 Schema 失败率;
  • 领域校验失败率、鉴权拒绝率;
  • 首次成功率、修复后成功率、平均修复成本;
  • 长度截断、拒答、过滤和超时比例;
  • 幂等重复命中、结果未知和人工回退率;
  • 端到端业务正确率,而不只是格式成功率。

解析率高、领域失败率高,说明 Schema 没表达关键边界或 prompt 语义差;格式成功但业务事故增加,则说明执行层缺少授权、幂等或人工确认。

面试追问:是否保存原始模型输出? 按数据分级决定。排障有价值,但可能含个人信息、机密和注入内容;应最小化、加密、限权、设置保留期,指标优先记录错误码和 hash。

常见错误回答:“结构化输出只需要监控 HTTP 错误率”“把完整 JSON 放到 metric label 最方便”。

Q12:结构化输出进入下游时还有哪些注入风险?

**核心回答:**结构化不等于安全编码。字符串字段若进入 SQL、Shell、HTML、日志、URL、模板或消息系统,必须按目标解释上下文采取参数化、编码、allowlist 和资源限制。

  • SQL 使用参数化查询,不能让模型生成 SQL 片段拼接;
  • Shell 尽量不执行,必要时使用固定命令与参数 allowlist;
  • HTML 按输出上下文编码,不能因为来自 JSON 就直接插入页面;
  • URL 要校验 scheme、host、端口与解析后的地址,防 SSRF 和 DNS 重绑定;
  • 日志去除控制字符/换行注入并做敏感数据脱敏;
  • 文件路径在服务端固定根目录下解析、规范化并验证边界。

Schema 中的正则或 format: uri 不能独自解决这些问题。安全校验应尽量基于解析后的语义对象,并在真正使用点再次实施。

面试追问:模型输出了 allowlist 内 URL 就一定安全吗? 仍要在请求时验证解析结果、DNS/IP 范围、重定向链、响应大小和超时;网络出口策略是更强的防线。

常见错误回答:“JSON 已转义,所以不会 SQL 注入或 XSS”“Schema 校验通过的 URL 可以直接由服务器访问”。

Java 21 完整示例:四层校验与幂等执行

Java 21 标准库不内置通用 JSON Schema validator。下面把模型传输和 JSON 解码分别抽象为接口,真实项目可用目标供应商 API 和经过安全配置的 JSON 库实现;核心的状态检查、领域校验、授权与幂等执行只依赖标准库,不引用虚构 SDK。

JAVA
import java.math.BigDecimal;
import java.math.RoundingMode;
import java.util.Map;
import java.util.Objects;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;

public final class StructuredOutputDemo {
    enum FinishReason { STOP, LENGTH, REFUSED, FILTERED, FAILED }
    enum Action { REFUND }

    record RawModelResponse(String payload, FinishReason finishReason, String schemaVersion) {
        RawModelResponse {
            Objects.requireNonNull(finishReason);
            Objects.requireNonNull(schemaVersion);
            if (finishReason == FinishReason.STOP) {
                Objects.requireNonNull(payload, "正常完成时必须包含业务 payload");
            }
            // 拒答、过滤或失败状态允许没有 payload;调用方必须先检查 finishReason。
        }
    }

    record RefundPlan(Action action, String orderId, BigDecimal amount,
                      String currency, String reason) {
        RefundPlan {
            Objects.requireNonNull(action);
            Objects.requireNonNull(orderId);
            Objects.requireNonNull(amount);
            Objects.requireNonNull(currency);
            Objects.requireNonNull(reason);
        }
    }

    record Principal(String tenantId, String operatorId, Set<String> permissions) {
        Principal {
            permissions = Set.copyOf(permissions);
        }
    }

    record Order(String id, String tenantId, BigDecimal refundableAmount,
                 String currency, boolean refundable) {}

    record ExecutionResult(String operationId, String status, BigDecimal refundedAmount) {}

    /** 由真实模型 HTTP 适配器实现;响应状态必须映射为内部枚举。 */
    interface StructuredModelClient {
        RawModelResponse generate(String prompt, String schemaVersion) throws Exception;
    }

    /** 由真实 JSON parser + Schema validator 适配器实现。 */
    interface RefundPlanDecoder {
        RefundPlan decodeAndValidate(String json, String schemaVersion) throws Exception;
    }

    interface OrderRepository {
        Order findVisibleOrder(String tenantId, String orderId);
    }

    static final class RefundService {
        private static final String SCHEMA_VERSION = "refund-plan-v1";
        private final StructuredModelClient model;
        private final RefundPlanDecoder decoder;
        private final OrderRepository orders;
        private final Map<String, StoredResult> idempotency = new ConcurrentHashMap<>();

        RefundService(StructuredModelClient model, RefundPlanDecoder decoder,
                      OrderRepository orders) {
            this.model = Objects.requireNonNull(model);
            this.decoder = Objects.requireNonNull(decoder);
            this.orders = Objects.requireNonNull(orders);
        }

        ExecutionResult planAndExecute(Principal principal, String request,
                                       String operationId) throws Exception {
            requirePermission(principal, "refund:create");
            RawModelResponse raw = model.generate(request, SCHEMA_VERSION);
            if (raw.finishReason() != FinishReason.STOP) {
                throw new IllegalStateException("模型未正常完成: " + raw.finishReason());
            }
            if (!SCHEMA_VERSION.equals(raw.schemaVersion())) {
                throw new IllegalStateException("Schema 版本不匹配");
            }

            RefundPlan plan = decoder.decodeAndValidate(raw.payload(), SCHEMA_VERSION);
            ValidatedRefund command = validateDomain(principal, plan);
            String commandFingerprint = command.fingerprint();

            StoredResult existing = idempotency.putIfAbsent(
                operationId, new StoredResult(commandFingerprint, null));
            if (existing != null) {
                if (!existing.fingerprint().equals(commandFingerprint)) {
                    throw new IllegalStateException("幂等键被用于不同退款命令");
                }
                if (existing.result() == null) {
                    throw new IllegalStateException("相同操作正在执行或结果待确认");
                }
                return existing.result();
            }

            // 演示用内存状态不具备进程崩溃原子性;生产应使用数据库事务和唯一约束。
            ExecutionResult result = new ExecutionResult(
                operationId, "SUCCEEDED", command.amount());
            idempotency.put(operationId, new StoredResult(commandFingerprint, result));
            return result;
        }

        private ValidatedRefund validateDomain(Principal principal, RefundPlan plan) {
            if (plan.action() != Action.REFUND) {
                throw new IllegalArgumentException("不支持的操作");
            }
            Order order = orders.findVisibleOrder(principal.tenantId(), plan.orderId());
            if (order == null || !order.tenantId().equals(principal.tenantId())) {
                throw new SecurityException("订单不可见");
            }
            if (!order.refundable()) {
                throw new IllegalStateException("订单状态不可退款");
            }
            BigDecimal amount = plan.amount().setScale(2, RoundingMode.UNNECESSARY);
            if (amount.signum() <= 0 || amount.compareTo(order.refundableAmount()) > 0) {
                throw new IllegalArgumentException("退款金额越界");
            }
            if (!order.currency().equals(plan.currency())) {
                throw new IllegalArgumentException("币种不匹配");
            }
            return new ValidatedRefund(order.id(), principal.tenantId(), amount, order.currency());
        }

        private static void requirePermission(Principal principal, String permission) {
            if (!principal.permissions().contains(permission)) {
                throw new SecurityException("缺少权限: " + permission);
            }
        }
    }

    record ValidatedRefund(String orderId, String tenantId,
                           BigDecimal amount, String currency) {
        String fingerprint() {
            return String.join("|", tenantId, orderId, amount.toPlainString(), currency);
        }
    }

    record StoredResult(String fingerprint, ExecutionResult result) {}

    public static void main(String[] args) throws Exception {
        // 这些 lambda 是本地可运行的测试替身,不代表任何供应商 SDK。
        StructuredModelClient model = (prompt, schemaVersion) -> new RawModelResponse(
            "{\"action\":\"REFUND\",\"orderId\":\"o-1\",\"amount\":\"88.00\","
                + "\"currency\":\"CNY\",\"reason\":\"重复支付\"}",
            FinishReason.STOP, schemaVersion);
        RefundPlanDecoder decoder = (json, schemaVersion) -> new RefundPlan(
            Action.REFUND, "o-1", new BigDecimal("88.00"), "CNY", "重复支付");
        OrderRepository orders = (tenantId, orderId) ->
            new Order("o-1", "tenant-a", new BigDecimal("100.00"), "CNY", true);

        var service = new RefundService(model, decoder, orders);
        var principal = new Principal("tenant-a", "u-7", Set.of("refund:create"));
        ExecutionResult result = service.planAndExecute(
            principal, "退还订单 o-1 的重复支付 88 元", "op-20260816-001");
        System.out.println(result);
    }
}

**复杂度与成本模型:**不计外部模型和数据库 I/O,状态检查与领域校验是 O(1);JSON 解析至少与 payload 长度 L 成正比,即 O(L),空间通常也是 O(L)。内存幂等表平均查询为 O(1),但会无限增长且无法抵御进程崩溃,只适合演示。生产应使用有唯一约束、状态和过期策略的持久化表,并让幂等记录与本地业务写入处于同一数据库事务;跨外部系统仍需下游幂等键、状态查询和对账。

**重要边界:**演示中的 RefundPlanDecoder 是明确的集成端口,不是在声称 Java 标准库自带 JSON Schema。测试替身直接构造 record,仅用于展示后续关卡;生产实现必须真正解析原始 JSON、拒绝重复键、限制深度/长度并验证目标供应商支持的 Schema 版本。fingerprint 示例用于说明“键必须绑定命令”,实际应使用无歧义规范化编码和安全 hash,而不是简单分隔字符串。

场景设计题

场景:设计一个“自然语言申请退款”的可靠执行链路

需求:用户描述退款原因;模型提取订单、金额和原因;超过阈值需人工审批;供应商可能超时;同一请求不得重复退款。

推荐回答框架:

  1. 契约:由服务端拥有 refund-plan-v1,字段仅含候选订单、金额、币种、原因,不允许模型填写 tenant、operator、审批结果和支付渠道凭据。
  2. 模型调用:优先使用目标 API 明确支持的严格 Schema 子集;保存 model、prompt/schema version 与响应完成状态。能力不足时仍按不可信 JSON 处理。
  3. 有界解析:限制 payload、深度、字符串和数字精度;拒绝重复键、未知危险枚举和 Schema 漂移。
  4. 确定性校验:根据认证主体查询订单;验证租户、状态、原支付币种、可退余额、时效与风控规则。
  5. 审批:超过阈值时只创建审批单,不执行退款;审批时重新读取订单和权限,避免 TOCTOU。
  6. 幂等执行:服务端生成 operation ID;幂等记录绑定规范化命令摘要;本地事务使用唯一约束。向支付渠道传递稳定幂等键并支持结果查询。
  7. 超时与结果不明:先按幂等键查询支付状态,再决定重试;没有查询能力时进入对账队列,不自动重复扣/退。
  8. 回退:拒答、过滤、字段缺失或业务不确定时要求用户确认/补充;鉴权失败直接拒绝;修复重试有次数和 token 上限。
  9. 观测:区分模型状态、解析、Schema、领域、授权、审批、幂等和渠道结果;审计保存必要字段并脱敏。

**加分点:**指出“模型只生成计划,Java 服务执行命令”;严格结构化输出降低格式错误,但退款正确性来自权威数据、领域约束、审批和幂等,而不是模型置信度。

速记总结

  • Prompt JSON、JSON mode、strict Schema、grammar 和 tool calling 保证层次不同,必须核对目标 API。
  • JSON Schema 标准不等于供应商完整支持;递归、条件、正则、format 等尤其要做契约测试。
  • 可靠链路至少有响应状态、语法、Schema、DTO、领域、授权和幂等多个关卡。
  • 合法 JSON 不代表事实正确、资源存在、金额合理或当前用户有权执行。
  • Java 21 标准库不内置通用 JSON Schema validator;第三方解析器需显式配置、固定版本和测试。
  • 解析模型输出要限制大小、深度、数组、字符串、数字,并拒绝危险重复键和任意多态类型。
  • 金额使用 BigDecimal,并校验 precision、scale、币种和实时可退余额。
  • 只有可恢复且尚未产生副作用的错误才修复重试;拒答、鉴权失败和结果不明不能盲试。
  • request ID 不自动等于幂等;持久化记录必须绑定命令摘要、状态和结果。
  • Schema 演进要考虑严格未知字段与新增枚举值,使用版本路由、双读和契约测试。
  • 结构化字符串进入 SQL、HTML、Shell、URL、日志时,仍需针对使用上下文做参数化和编码。
  • 结构化输出提升接口可靠性,不提升事实正确性;高风险动作仍需权威查询与审批。

参考资料