结构化输出与可靠解析结构化输出与可靠解析
区分 JSON 模式、Schema 约束与工具调用,建立解析、领域校验、授权、幂等和回退的完整 Java 边界。区分 JSON 模式、Schema 约束与工具调用,建立解析、领域校验、授权、幂等和回退的完整 Java 边界。
专题导读
结构化输出的目标不是让模型“像普通 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 不代表实现了完整标准。
常用关键字包括 type、properties、required、enum、const、items、minItems/maxItems、数值和字符串边界,以及 additionalProperties。递归引用、条件 Schema、复杂正则、format、组合关键字等支持情况差异较大。
additionalProperties: false 可减少模型生成未知字段,但会增加演进成本:旧 Schema 无法接受新字段,需要版本路由或兼容窗口。format: "date-time" 在标准生态中也可能只是 annotation,是否执行断言取决于词汇和实现;服务端仍应使用 java.time 严格解析。
面试追问:如何确认目标 API 支持哪些关键字? 维护契约测试:对每个实际 Schema 在 CI/预生产调用目标 API,覆盖正常输出、拒答、边界值和不支持关键字;供应商升级时回归。
常见错误回答:“符合 2020-12 的 Schema 可直接用于任何模型供应商”“
format一定像数据库约束一样强制验证”。
Q3:为什么合法 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 版本错配、业务规则和权限问题。
服务端至少应再次检查:
- 响应是否正常完成,是否被截断、拒答或过滤;
- 收到的 Schema 版本是否是调用方预期版本;
- 解析和 DTO 映射是否按本地契约成功;
- 数字、日期、ID 等是否满足本地约束;
- 引用资源是否存在、版本是否仍有效;
- 当前主体是否有权执行;
- 幂等键是否与规范化命令一致。
这不是不信任某个供应商,而是分布式系统边界的基本原则:本服务对自身不变量负责。即使上游是普通微服务,关键写入也要本地校验。
面试追问:重复校验是否浪费性能? 与模型调用延迟和副作用风险相比,本地结构/领域校验通常成本很低;高并发时仍可通过编译 Schema、缓存 validator 等优化。
常见错误回答:“strict=true 后可删除所有 validator”“受限解码保证模型引用的订单一定存在”。
Q7:领域校验、授权和模型判断应如何分工?
**核心回答:**模型适合提取意图与候选参数;Java 领域服务负责确定性不变量;授权系统决定当前主体能否作用于具体资源。模型不能授予权限,也不能成为金额、库存或状态转换的事实来源。
以退款为例:模型可把“给昨天重复扣款的订单退款”解析为候选订单和原因;服务端根据已认证用户查询可见订单,校验支付状态、可退余额、币种、时效和审批阈值,再生成领域命令。tenantId、operatorId、角色和最终金额上限应来自服务端上下文与权威系统。
高风险场景可采用 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。
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,而不是简单分隔字符串。
场景设计题
场景:设计一个“自然语言申请退款”的可靠执行链路
需求:用户描述退款原因;模型提取订单、金额和原因;超过阈值需人工审批;供应商可能超时;同一请求不得重复退款。
推荐回答框架:
- 契约:由服务端拥有
refund-plan-v1,字段仅含候选订单、金额、币种、原因,不允许模型填写 tenant、operator、审批结果和支付渠道凭据。 - 模型调用:优先使用目标 API 明确支持的严格 Schema 子集;保存 model、prompt/schema version 与响应完成状态。能力不足时仍按不可信 JSON 处理。
- 有界解析:限制 payload、深度、字符串和数字精度;拒绝重复键、未知危险枚举和 Schema 漂移。
- 确定性校验:根据认证主体查询订单;验证租户、状态、原支付币种、可退余额、时效与风控规则。
- 审批:超过阈值时只创建审批单,不执行退款;审批时重新读取订单和权限,避免 TOCTOU。
- 幂等执行:服务端生成 operation ID;幂等记录绑定规范化命令摘要;本地事务使用唯一约束。向支付渠道传递稳定幂等键并支持结果查询。
- 超时与结果不明:先按幂等键查询支付状态,再决定重试;没有查询能力时进入对账队列,不自动重复扣/退。
- 回退:拒答、过滤、字段缺失或业务不确定时要求用户确认/补充;鉴权失败直接拒绝;修复重试有次数和 token 上限。
- 观测:区分模型状态、解析、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、日志时,仍需针对使用上下文做参数化和编码。
- 结构化输出提升接口可靠性,不提升事实正确性;高风险动作仍需权威查询与审批。
参考资料
- RFC 8259:The JavaScript Object Notation (JSON) Data Interchange Format
- JSON Schema Draft 2020-12 Core Specification
- JSON Schema Draft 2020-12 Validation Specification
- OWASP GenAI Security Project:Improper Output Handling
- OWASP:Deserialization Cheat Sheet
- OWASP:Server Side Request Forgery Prevention Cheat Sheet
- OpenAI:Structured Outputs
- Google AI for Developers:Structured output
- Anthropic:Tool use
- Java 21 API:BigDecimal