LangChain4j 实战:JSON Schema、@Description、POJO 解析怎么配合
·
LangChain4j 面试题:结构化输出怎么做?JSON Schema、POJO 解析、异常兜底讲透
LangChain4j 的结构化输出很适合 Java 开发者,因为它可以把模型结果直接落到 POJO、record、enum 这些熟悉的类型里。
但真正上线时,难点不只是“能不能解析”,而是字段约束、模型能力边界和失败兜底这三件事要一起想清楚。
文章目录
- LangChain4j 面试题:结构化输出怎么做?JSON Schema、POJO 解析、异常兜底讲透
先看真实问题:为什么很多模型回答看起来正常,系统一接就开始出问题
在真实业务里,很多 AI 能力最后都不是给人直接读的,而是要进数据库、进规则引擎、进审批流。如果模型只是回了一段自然语言,系统后面往往根本接不住。
LangChain4j 的结构化输出价值就在这里:它可以把模型结果约束成 Java 对象,让后端的代码、表结构和流程节点真正接得起来。
- 字段名和枚举值不稳定,后面做规则判断很容易出错
- 结构化结果如果没有失败兜底,模型一旦格式异常,整条链路就会卡住
- 不是所有模型和模式都支持同样强的 JSON Schema 约束,设计时必须知道边界
一张表先看懂:结构化输出在项目里最关心的四件事
| 维度 | 怎么做 | 为什么 |
|---|---|---|
| Schema 约束 | 把输出定义成 POJO / enum / 集合 | 让模型回答更像系统结果而不是聊天内容 |
| 字段描述 | 用 @Description 和 @JsonProperty 补约束 | 尽量减少歧义和枚举飘移 |
| 模型能力 | 确认当前模型是否支持 JSON Schema | 不是所有提供商都一样稳 |
| 失败兜底 | 解析失败落异常表或走人工复核 | 保证主链路不要被一次格式异常卡死 |
举个具体例子:工单智能分流:把用户投诉直接提取成结构化对象
- 客服系统收到一段自然语言投诉后,不是直接给人看,而是先让模型抽取分类、优先级、是否转人工。
- LangChain4j 把返回结果映射成
TicketDecision,后面的规则引擎只认这个对象。 - 如果分类结果是
QUALITY且优先级是HIGH,系统直接走加急售后流程。 - 如果模型返回不完整或者解析失败,就写入异常表并打上待人工复核状态。
企业里的典型应用场景
- 售后工单智能分流:模型输出分类、优先级、转人工标记,后面直接驱动流程引擎和客服工作台。
- 合同审阅和法务摘要:从长文本里提取风险条款、责任方、截止时间,结果直接入库并挂到审批单。
- 风控审核场景:把用户申诉内容先转成结构化对象,再交给规则引擎和人工复核平台做后续判定。
如果按企业项目落地,我会这样走完整闭环
- 入口层先接收业务请求,做幂等校验、租户校验和基础字段补全,避免同一业务单重复调用模型。
- 编排层根据场景选择结构化 DTO 和模板,把系统提示词、字段枚举、兜底策略一起收敛在应用服务里。
- AI 调用层只负责拿到结构化对象,不直接写业务表,防止模型异常结果污染主数据。
- 持久化层把成功结果、失败结果、原始业务主键、调用版本一起落库,后面方便重放和做效果评估。
- 规则层基于结构化对象继续流转,例如是否自动派单、是否走高优先级审核、是否直接升级人工。
- 治理层补上审计日志、失败补偿、人工复核任务和指标监控,形成从调用到落地的完整闭环。
代码示例:AI Service 返回 POJO + JSON Schema 约束
Maven 依赖
<dependencyManagement>
<dependencies>
<dependency>
<groupId>dev.langchain4j</groupId>
<artifactId>langchain4j-bom</artifactId>
<version>1.11.0</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
<dependency>
<groupId>dev.langchain4j</groupId>
<artifactId>langchain4j</artifactId>
</dependency>
<dependency>
<groupId>dev.langchain4j</groupId>
<artifactId>langchain4j-open-ai</artifactId>
</dependency>
返回结构定义
@Description("工单分类结果")
public record TicketDecision(
@JsonProperty(required = true)
@Description("工单分类,只能是 REFUND、LOGISTICS、QUALITY、OTHER")
TicketCategory category,
@JsonProperty(required = true)
@Description("优先级,只能是 LOW、MEDIUM、HIGH")
TicketPriority priority,
@Description("是否需要转人工")
boolean needManualReview,
@Description("判断原因")
String reason
) {
}
enum TicketCategory {
REFUND, LOGISTICS, QUALITY, OTHER
}
enum TicketPriority {
LOW, MEDIUM, HIGH
}
AI Service 接口和装配
public interface TicketClassifier {
@SystemMessage("""
你是售后工单分流助手。
只能输出结构化结果,不要输出额外解释。
""")
TicketDecision classify(@UserMessage String ticketText);
}
@Configuration
public class TicketClassifierConfig {
@Bean
public TicketClassifier ticketClassifier() {
ChatModel chatModel = OpenAiChatModel.builder()
.apiKey(System.getenv("OPENAI_API_KEY"))
.modelName("gpt-4o-mini")
.supportedCapabilities(RESPONSE_FORMAT_JSON_SCHEMA)
.strictJsonSchema(true)
.build();
return AiServices.builder(TicketClassifier.class)
.chatModel(chatModel)
.build();
}
}
应用服务里的兜底处理
@Service
@RequiredArgsConstructor
public class TicketDecisionService {
private final TicketClassifier ticketClassifier;
private final TicketAiResultRepository ticketAiResultRepository;
public TicketDecision decide(Long ticketId, String ticketText) {
try {
TicketDecision decision = ticketClassifier.classify(ticketText);
ticketAiResultRepository.saveSuccess(ticketId, JsonUtils.toJson(decision));
return decision;
} catch (Exception ex) {
ticketAiResultRepository.saveFail(ticketId, ex.getMessage());
return new TicketDecision(
TicketCategory.OTHER,
TicketPriority.MEDIUM,
true,
"模型解析失败,已转人工复核"
);
}
}
}
企业级代码示例:企业里更常见的是 Facade + Orchestrator + ReviewTask 的闭环写法
结构化提取编排服务
@Slf4j
@Service
@RequiredArgsConstructor
public class TicketDecisionOrchestrator {
private final IdempotentGuard idempotentGuard;
private final TicketClassifier ticketClassifier;
private final TicketAiResultRepository ticketAiResultRepository;
private final TicketFlowDecisionService ticketFlowDecisionService;
private final ManualReviewTaskService manualReviewTaskService;
private final AiInvocationAuditService aiInvocationAuditService;
@Transactional(rollbackFor = Exception.class)
public TicketDecisionResult handle(TicketDecisionCommand command) {
idempotentGuard.check(
"AI:TICKET:DECISION:" + command.ticketId(),
Duration.ofMinutes(10)
);
long start = System.currentTimeMillis();
try {
TicketDecision decision = ticketClassifier.classify(command.ticketContent());
ticketAiResultRepository.save(TicketAiResultEntity.success(
command.ticketId(),
command.tenantId(),
JsonUtils.toJson(decision),
command.promptVersion()
));
TicketFlowAction flowAction = ticketFlowDecisionService.decide(command.ticketId(), decision);
if (decision.needManualReview()) {
manualReviewTaskService.create(
command.ticketId(),
"AI_STRUCTURE_REVIEW",
decision.reason()
);
}
aiInvocationAuditService.success(
command.ticketId(),
"TICKET_DECISION",
System.currentTimeMillis() - start
);
return new TicketDecisionResult(decision, flowAction);
} catch (Exception ex) {
log.error("ticket decision fail, ticketId={}", command.ticketId(), ex);
ticketAiResultRepository.save(TicketAiResultEntity.fail(
command.ticketId(),
command.tenantId(),
ex.getMessage(),
command.promptVersion()
));
manualReviewTaskService.create(
command.ticketId(),
"AI_STRUCTURE_FALLBACK",
"结构化提取失败,自动转人工"
);
aiInvocationAuditService.fail(
command.ticketId(),
"TICKET_DECISION",
ex.getMessage(),
System.currentTimeMillis() - start
);
return new TicketDecisionResult(
new TicketDecision(TicketCategory.OTHER, TicketPriority.MEDIUM, true, "模型异常,已走人工"),
TicketFlowAction.MANUAL_REVIEW
);
}
}
}
SQL 示例:结构化结果与异常表
create table ai_ticket_decision_log (
id bigint primary key auto_increment,
ticket_id bigint not null,
result_json json null,
parse_status varchar(32) not null comment 'SUCCESS/FAIL',
error_message varchar(500) null,
created_time datetime not null default current_timestamp
);
系统设计时我会优先拆哪几层
输出定义层
- 先定义 Java 类型,再倒推 prompt 和 JSON Schema,而不是反过来
- 复杂字段尽量收敛成 enum、boolean、嵌套对象,后端接起来更稳
AI Service 约束层
- 优先用模型原生支持的 JSON Schema,尽量少靠纯提示词约束格式
- 类和字段上补
@Description,能显著减少字段语义漂移
落库与补偿层
- 成功和失败都要落库,后面做回放、评测和误判分析时特别有用
- 核心链路不要把一次格式异常当成致命错误,必须有人工兜底入口
真正上线时最容易卡住的点
- 所有字段都设成可选,短期看成功率高,长期看后端会处理一堆‘看起来像成功其实缺字段’的脏数据。
- 只会写一个大 Prompt,不给类型和字段补描述,模型对边界字段会非常不稳定。
- 结构化输出失败后直接抛错,没有补偿记录,线上一旦出问题很难复盘。
监控和指标建议盯哪些
- 结构化解析成功率
- 字段缺失率、枚举越界率
- 人工复核触发率
- 不同模型的结构化稳定性和成本对比
如果面试官问我这块怎么设计,我会这样答
如果面试官问 LangChain4j 的结构化输出怎么落地,我会先讲这不是‘让模型回 JSON’这么简单,而是 Java 类型建模、模型能力判断、异常兜底三件事一起做。项目里我会先定义返回 POJO,再用 JSON Schema 和字段描述约束模型,最后把失败结果落库并转人工复核。这样这块能力才是真正可上线的。
结语
结构化输出的核心,不是让大模型看起来更规范,而是让它的结果真的能被 Java 系统稳定消费。
如果你们项目里已经做了 AI 提取,最容易出问题的是字段不稳定,还是解析失败后的补偿?
AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。
更多推荐

所有评论(0)