LangChain4j Java AI 应用开发实战(七):结构化输出实战 - 从非结构化文本提取 POJO 对象
系列篇章💥
前言
大模型擅长生成自然语言,但程序需要的是结构化数据。如何让 AI 返回 JSON、Java 对象而非自由文本?LangChain4j 提供了强大的结构化输出能力,能自动将非结构化文本解析为枚举、数值、日期、POJO 等 Java 类型。本文将深入讲解 @Description 注解的字段级语义说明、JSON Mode 的强制结构化输出、以及复杂嵌套对象的提取技巧。你将学会如何从简历中提取人员信息、从评论中分析情感、从合同中抽取关键条款,让 AI 真正成为数据提取的利器,大幅提升业务自动化水平。
一、为什么需要结构化输出?
1.1 非结构化文本的痛点
传统 AI 调用返回的是自由文本:
String response = chatModel.chat("从以下文本中提取姓名和年龄:张三今年25岁");
System.out.println(response);
// 输出:"姓名是张三,年龄是25岁。"
问题:
- ❌ 难以解析:需要用正则或 NLP 提取具体字段
- ❌ 格式不稳定:每次回答格式可能不同
- ❌ 无法直接使用:需要二次处理才能存入数据库
- ❌ 容易出错:文本变化导致解析失败
1.2 结构化输出的优势
使用结构化输出后:
Person person = extractor.extractPerson("张三今年25岁");
System.out.println(person.getName()); // 张三
System.out.println(person.getAge()); // 25
优势:
- ✅ 类型安全:编译期检查,IDE 智能提示
- ✅ 直接使用:无需解析,直接操作对象
- ✅ 格式稳定:始终返回一致的 JSON 结构
- ✅ 易于存储:可直接存入数据库或缓存
二、基础类型提取
2.1 枚举类型提取(情感分析)
static class Sentiment_Extracting_AI_Service_Example {
enum Sentiment {
POSITIVE, NEUTRAL, NEGATIVE
}
interface SentimentAnalyzer {
/**
* {{it}} 是特殊占位符,表示方法的第一个参数。
*/
@UserMessage("Analyze sentiment of {{it}}")
Sentiment analyzeSentimentOf(String text);
/**
* 返回 boolean 类型时,框架会将模型的 Yes/No 回答解析为 true/false。
*/
@UserMessage("Does {{it}} have a positive sentiment?")
boolean isPositive(String text);
}
public static void main(String[] args) {
SentimentAnalyzer sentimentAnalyzer = AiServices.create(SentimentAnalyzer.class, model);
Sentiment sentiment = sentimentAnalyzer.analyzeSentimentOf("It is good!");
System.out.println(sentiment); // POSITIVE
boolean positive = sentimentAnalyzer.isPositive("It is bad!");
System.out.println(positive); // false
}
}
工作原理:
模型接收 Prompt → 从 [POSITIVE, NEUTRAL, NEGATIVE] 中选择
↓
输出:"POSITIVE"
↓
LangChain4j 匹配枚举值
↓
返回 Sentiment.POSITIVE
应用场景:
- 情感分析(正面/负面/中性)
- 意图识别(查询/投诉/建议)
- 风险评级(低/中/高)
- 优先级分类(紧急/普通/低优)
2.2 列表枚举提取(多标签分类)
static class Hotel_Review_AI_Service_Example {
public enum IssueCategory {
MAINTENANCE_ISSUE, // 维护问题
SERVICE_ISSUE, // 服务问题
COMFORT_ISSUE, // 舒适度问题
FACILITY_ISSUE, // 设施问题
CLEANLINESS_ISSUE, // 清洁度问题
CONNECTIVITY_ISSUE, // 网络连接问题
CHECK_IN_ISSUE, // 入住办理问题
OVERALL_EXPERIENCE_ISSUE // 整体体验问题
}
interface HotelReviewIssueAnalyzer {
// |||{{it}}||| 使用分隔符包裹文本
@UserMessage("Please analyse the following review: |||{{it}}|||")
List<IssueCategory> analyzeReview(String review);
}
public static void main(String[] args) {
HotelReviewIssueAnalyzer analyzer = AiServices.create(HotelReviewIssueAnalyzer.class, model);
String review = "Our stay at hotel was a mixed experience. The location was perfect... " +
"However, we encountered several issues. The air conditioning... " +
"Additionally, the room service was slow...";
List<IssueCategory> issues = analyzer.analyzeReview(review);
System.out.println(issues);
// 输出:[MAINTENANCE_ISSUE, SERVICE_ISSUE, COMFORT_ISSUE, OVERALL_EXPERIENCE_ISSUE]
}
}
关键点:
- 返回
List<Enum>支持多标签分类 - 一个样本可同时属于多个类别
- 使用分隔符
|||帮助模型识别待分析内容
2.3 数值类型提取
static class Number_Extracting_AI_Service_Example {
interface NumberExtractor {
@UserMessage("Extract number from {{it}}")
int extractInt(String text);
@UserMessage("Extract number from {{it}}")
long extractLong(String text);
@UserMessage("Extract number from {{it}}")
BigInteger extractBigInteger(String text);
@UserMessage("Extract number from {{it}}")
float extractFloat(String text);
@UserMessage("Extract number from {{it}}")
double extractDouble(String text);
@UserMessage("Extract number from {{it}}")
BigDecimal extractBigDecimal(String text);
}
public static void main(String[] args) {
NumberExtractor extractor = AiServices.create(NumberExtractor.class, model);
String text = "After countless millennia of computation, the supercomputer Deep Thought finally announced "
+ "that the answer to the ultimate question of life, the universe, and everything was forty two.";
int intNumber = extractor.extractInt(text);
System.out.println(intNumber); // 42
double doubleNumber = extractor.extractDouble(text);
System.out.println(doubleNumber); // 42.0
BigDecimal bigDecimalNumber = extractor.extractBigDecimal(text);
System.out.println(bigDecimalNumber); // 42.0
}
}
支持的数值类型:
| 类型 | 适用场景 | 示例输入 | 输出 |
|---|---|---|---|
int |
整数 | “二十五岁” | 25 |
long |
大整数 | “订单号1234567890” | 1234567890L |
float/double |
浮点数 | “价格99.99元” | 99.99 |
BigDecimal |
高精度小数 | “金额1,234.56元” | 1234.56 |
BigInteger |
超大整数 | “天文数字” | 大数 |
智能解析:
- ✅ 阿拉伯数字:“42” → 42
- ✅ 中文数字:“四十二” → 42
- ✅ 英文数字:“forty two” → 42
- ✅ 带单位:“99.99元” → 99.99
2.4 日期时间提取
static class Date_and_Time_Extracting_AI_Service_Example {
interface DateTimeExtractor {
@UserMessage("Extract date from {{it}}")
LocalDate extractDateFrom(String text);
@UserMessage("Extract time from {{it}}")
LocalTime extractTimeFrom(String text);
@UserMessage("Extract date and time from {{it}}")
LocalDateTime extractDateTimeFrom(String text);
}
public static void main(String[] args) {
DateTimeExtractor extractor = AiServices.create(DateTimeExtractor.class, model);
String text = "The tranquility pervaded the evening of 1968, just fifteen minutes shy of midnight,"
+ " following the celebrations of Independence Day.";
LocalDate date = extractor.extractDateFrom(text);
System.out.println(date); // 1968-07-04
LocalTime time = extractor.extractTimeFrom(text);
System.out.println(time); // 23:45
LocalDateTime dateTime = extractor.extractDateTimeFrom(text);
System.out.println(dateTime); // 1968-07-04T23:45
}
}
智能解析能力:
- ✅ “1968年独立日” → 1968-07-04(美国独立日是7月4日)
- ✅ “差15分钟到午夜” → 23:45
- ✅ “明天下午3点” → 相对日期计算
- ✅ “下周一” → 自动推算日期
三、POJO 对象提取
3.1 基础 POJO 提取
static class POJO_Extracting_AI_Service_Example {
static class Person {
@Description("first name of a person")
private String firstName;
private String lastName;
private LocalDate birthDate;
@Override
public String toString() {
return "Person {" +
" firstName = \"" + firstName + "\"" +
", lastName = \"" + lastName + "\"" +
", birthDate = " + birthDate +
" }";
}
}
interface PersonExtractor {
@UserMessage("Extract a person from the following text: {{it}}")
Person extractPersonFrom(String text);
}
public static void main(String[] args) {
// 提取 POJO 时启用 JSON Mode
ChatModel model = OpenAiChatModel.builder()
.baseUrl("http://langchain4j.dev/demo/openai/v1")
.modelName("gpt-4o-mini")
.apiKey("demo")
.responseFormat("json_object") // 强制 JSON 输出
.timeout(ofSeconds(60))
.build();
PersonExtractor extractor = AiServices.create(PersonExtractor.class, model);
String text = "In 1968, amidst the fading echoes of Independence Day, "
+ "a child named John arrived under the calm evening sky. "
+ "This newborn, bearing the surname Doe, marked the start of a new journey.";
Person person = extractor.extractPersonFrom(text);
System.out.println(person);
// Person { firstName = "John", lastName = "Doe", birthDate = 1968-07-04 }
}
}
核心要素:
(1)@Description 注解
@Description("first name of a person")
private String firstName;
作用:
- 为字段添加语义说明
- 帮助模型理解如何提取
- 提升提取准确率
何时使用:
- ✅ 字段名不够清晰(如
name1,value) - ✅ 需要额外约束(如格式、范围)
- ✅ 多义词需要澄清
(2)JSON Mode
.responseFormat("json_object")
作用:
- 强制模型输出合法 JSON
- 避免自由文本格式
- 提高解析成功率
可选值:
| 值 | 说明 | 适用场景 |
|---|---|---|
text |
自由文本(默认) | 对话、创意写作 |
json_object |
JSON 对象 | POJO 提取 |
json_schema |
严格 JSON Schema | 复杂结构、生产环境 |
3.2 复杂 POJO 提取(带描述的食谱生成)
static class POJO_With_Descriptions_Extracting_AI_Service_Example {
static class Recipe {
@Description("short title, 3 words maximum")
private String title;
@Description("short description, 2 sentences maximum")
private String description;
@Description("each step should be described in 6 to 8 words, steps should rhyme")
private List<String> steps;
private Integer preparationTimeMinutes;
@Override
public String toString() {
return "Recipe {" +
" title = \"" + title + "\"" +
", description = \"" + description + "\"" +
", steps = " + steps +
", preparationTimeMinutes = " + preparationTimeMinutes +
" }";
}
}
interface Chef {
Recipe createRecipeFrom(String... ingredients);
}
public static void main(String[] args) {
ChatModel model = OpenAiChatModel.builder()
.baseUrl("http://langchain4j.dev/demo/openai/v1")
.modelName("gpt-4o-mini")
.apiKey("demo")
.responseFormat("json_object")
.timeout(ofSeconds(60))
.build();
Chef chef = AiServices.create(Chef.class, model);
Recipe recipe = chef.createRecipeFrom("cucumber", "tomato", "feta", "onion", "olives", "lemon");
System.out.println(recipe);
// Recipe {
// title = "Greek Salad",
// description = "A refreshing mix of veggies and feta cheese in a zesty dressing.",
// steps = [
// "Chop cucumber and tomato",
// "Add onion and olives",
// "Crumble feta on top",
// "Drizzle with dressing and enjoy!"
// ],
// preparationTimeMinutes = 10
// }
}
}
@Description 的高级用法:
@Description("short title, 3 words maximum")
private String title;
效果:
- 约束输出长度
- 指定格式要求
- 引导模型风格
更多示例:
@Description("phone number in format XXX-XXXX-XXXX")
private String phone;
@Description("email address, must contain @ symbol")
private String email;
@Description("rating from 1 to 5 stars")
private int rating;
@Description("list of tags, maximum 5 items")
private List<String> tags;
3.3 嵌套 POJO 提取
虽然示例代码中未展示,但 LangChain4j 支持嵌套对象提取:
@Data
public class Address {
@Description("省份")
private String province;
@Description("城市")
private String city;
@Description("详细地址")
private String street;
@Description("邮政编码")
private String zipCode;
}
@Data
public class Employee {
@Description("员工姓名")
private String name;
@Description("年龄")
private int age;
@Description("邮箱地址")
private String email;
@Description("居住地址")
private Address address; // 嵌套对象
@Description("联系电话列表")
private List<String> phones; // 列表字段
}
interface EmployeeExtractor {
@UserMessage("从以下文本中提取员工信息:{{text}}")
Employee extractEmployee(@V("text") String text);
}
public class NestedPOJOExample {
public static void main(String[] args) {
ChatModel model = OpenAiChatModel.builder()
.apiKey(System.getenv("OPENAI_API_KEY"))
.modelName("gpt-4o-mini")
.responseFormat("json_object")
.build();
EmployeeExtractor extractor = AiServices.create(EmployeeExtractor.class, model);
String text = "张三,28岁,邮箱zhangsan@example.com,住在北京市朝阳区长安街100号,邮编100000,电话13800138000和13900139000";
Employee employee = extractor.extractEmployee(text);
System.out.println(employee.getName()); // 张三
System.out.println(employee.getAddress().getCity()); // 北京
System.out.println(employee.getPhones().size()); // 2
}
}
关键点:
- ✅ 支持任意深度的嵌套
- ✅ 每个嵌套对象都可使用
@Description - ✅ 列表字段支持
List<POJO>
四、JSON Mode 详解
4.1 为什么需要 JSON Mode?
❌ 不使用 JSON Mode:
模型输出:
"根据文本分析,这个人叫张三,年龄25岁。"
解析失败:不是合法 JSON
✅ 使用 JSON Mode:
模型输出:
{
"name": "张三",
"age": 25
}
解析成功:直接映射到 Java 对象
4.2 三种响应格式对比
| 格式 | 说明 | 优点 | 缺点 |
|---|---|---|---|
| text | 自由文本 | 灵活、自然 | 难以解析 |
| json_object | JSON 对象 | 简单易用 | 无 Schema 验证 |
| json_schema | 严格 JSON Schema | 强验证、高可靠 | 部分模型不支持 |
4.3 json_object vs json_schema
(1)json_object(推荐大多数场景)
ChatModel model = OpenAiChatModel.builder()
.apiKey(System.getenv("OPENAI_API_KEY"))
.modelName("gpt-4o-mini")
.responseFormat("json_object") // 简单 JSON 模式
.build();
特点:
- ✅ 所有 GPT 模型支持
- ✅ 配置简单
- ✅ 适合大多数场景
- ❌ 不验证字段名称和类型
(2)json_schema(生产环境推荐)
ChatModel model = OpenAiChatModel.builder()
.apiKey(System.getenv("OPENAI_API_KEY"))
.modelName("gpt-4o")
.responseFormat("json_schema") // 严格 Schema 模式
.strictJsonSchema(true)
.build();
特点:
- ✅ 严格验证 JSON 结构
- ✅ 字段名称、类型必须匹配
- ✅ 可靠性最高
- ❌ 仅 GPT-4o 及更新模型支持
- ❌ 演示端点可能不完全支持
五、实战案例:简历信息提取系统
5.1 需求
从简历文本中提取结构化信息,包括:
- 个人信息(姓名、年龄、联系方式)
- 教育背景(学校、专业、学历)
- 工作经历(公司、职位、时间)
- 技能清单
5.2 完整代码
(1)定义 POJO
@Data
@AllArgsConstructor
@NoArgsConstructor
public class Education {
@Description("学校名称")
private String school;
@Description("专业名称")
private String major;
@Description("学历(本科/硕士/博士)")
private String degree;
@Description("毕业年份")
private Integer graduationYear;
}
@Data
@AllArgsConstructor
@NoArgsConstructor
public class WorkExperience {
@Description("公司名称")
private String company;
@Description("职位名称")
private String position;
@Description("开始时间(YYYY-MM)")
private String startDate;
@Description("结束时间(YYYY-MM,至今填\"至今\")")
private String endDate;
@Description("工作描述,简要说明职责和成就")
private String description;
}
@Data
@AllArgsConstructor
@NoArgsConstructor
public class Resume {
@Description("候选人姓名")
private String name;
@Description("年龄")
private Integer age;
@Description("手机号码")
private String phone;
@Description("邮箱地址")
private String email;
@Description("教育背景列表,按时间倒序")
private List<Education> educations;
@Description("工作经历列表,按时间倒序")
private List<WorkExperience> experiences;
@Description("技能清单,如 Java、Spring Boot、MySQL 等")
private List<String> skills;
}
(2)定义 AI Service
@AiService
public interface ResumeExtractor {
@SystemMessage("""
你是专业的简历信息提取助手。
任务:
1. 从简历文本中提取结构化信息
2. 保持信息准确,不要编造
3. 如果某项信息不存在,填 null
4. 日期格式统一为 YYYY-MM
""")
@UserMessage("请从以下简历中提取信息:\n\n{{resumeText}}")
Resume extractResume(@V("resumeText") String resumeText);
}
(3)Service 层
@Service
public class ResumeExtractionService {
@Autowired
private ResumeExtractor resumeExtractor;
public Resume extractResume(String resumeText) {
// 输入校验
if (resumeText == null || resumeText.length() < 50) {
throw new IllegalArgumentException("简历文本过短,无法提取");
}
try {
return resumeExtractor.extractResume(resumeText);
} catch (Exception e) {
throw new ServiceException("简历提取失败:" + e.getMessage(), e);
}
}
}
(4)Controller 层
@RestController
@RequestMapping("/api/resume")
public class ResumeController {
@Autowired
private ResumeExtractionService resumeService;
@PostMapping("/extract")
public ResponseEntity<Resume> extractResume(@RequestBody Map<String, String> request) {
String resumeText = request.get("text");
Resume resume = resumeService.extractResume(resumeText);
return ResponseEntity.ok(resume);
}
}
(5)使用示例
# 请求
curl -X POST http://localhost:8080/api/resume/extract \
-H "Content-Type: application/json" \
-d '{
"text": "张三,28岁,电话13800138000,邮箱zhangsan@example.com。\n\n教育背景:\n2014-2018 北京大学 计算机科学与技术 本科\n2018-2021 清华大学 软件工程 硕士\n\n工作经历:\n2021-07 至今 阿里巴巴 Java开发工程师 负责电商平台后端开发\n2018-07 至 2018-12 腾讯 实习工程师 参与微信小程序开发\n\n技能:Java, Spring Boot, MySQL, Redis, Docker"
}'
# 响应
{
"name": "张三",
"age": 28,
"phone": "13800138000",
"email": "zhangsan@example.com",
"educations": [
{
"school": "清华大学",
"major": "软件工程",
"degree": "硕士",
"graduationYear": 2021
},
{
"school": "北京大学",
"major": "计算机科学与技术",
"degree": "本科",
"graduationYear": 2018
}
],
"experiences": [
{
"company": "阿里巴巴",
"position": "Java开发工程师",
"startDate": "2021-07",
"endDate": "至今",
"description": "负责电商平台后端开发"
},
{
"company": "腾讯",
"position": "实习工程师",
"startDate": "2018-07",
"endDate": "2018-12",
"description": "参与微信小程序开发"
}
],
"skills": ["Java", "Spring Boot", "MySQL", "Redis", "Docker"]
}
六、常见问题与避坑指南
6.1 字段提取不准确
❌ 现象:
@Description("姓名")
private String name;
// 输入:"我叫张三"
// 输出:name = "我叫张三" ← 包含了多余文字
✅ 解决方案:优化 @Description
@Description("仅提取姓名,不包含'我叫'等前缀")
private String name;
6.2 JSON 解析失败
❌ 现象:
com.fasterxml.jackson.databind.JsonMappingException:
Unexpected token (START_ARRAY), expected START_OBJECT
原因:模型返回了数组而非对象
✅ 解决方案:
- 明确 Prompt
@UserMessage("请以 JSON 对象格式返回,不要返回数组或其他格式:{{text}}")
- 启用 JSON Mode
.responseFormat("json_object")
- 捕获异常并重试
try {
return extractor.extract(text);
} catch (Exception e) {
logger.warn("提取失败,重试中...", e);
return extractor.extract(text); // 重试一次
}
6.3 嵌套对象提取失败
❌ 现象:
嵌套字段为 null
原因:
- 模型不理解嵌套结构
- JSON Schema 过于复杂
✅ 解决方案:
- 简化嵌套层级
// ❌ 三层嵌套
class A { B b; }
class B { C c; }
class C { String value; }
// ✅ 扁平化
class A {
String bValue;
String cValue;
}
- 分步提取
// 第一步:提取基本信息
Person person = extractor.extractPerson(text);
// 第二步:提取地址
Address address = extractor.extractAddress(text);
// 手动组装
person.setAddress(address);
6.4 性能问题
❌ 现象:
POJO 提取耗时 5-10 秒
原因:
- JSON Mode 增加推理时间
- 复杂 Schema 计算量大
✅ 解决方案:
- 使用更快的模型
.modelName("gpt-4o-mini") // 比 gpt-4o 快 50%
- 异步处理
CompletableFuture<Resume> future = CompletableFuture.supplyAsync(() ->
resumeExtractor.extractResume(text)
);
// 继续处理其他任务
// ...
Resume resume = future.get(); // 需要时再获取结果
- 缓存结果
@Cacheable(value = "resumes", key = "#text.hashCode()")
public Resume extractResume(String text) {
return resumeExtractor.extractResume(text);
}
七、最佳实践
7.1 @Description 设计原则
✅ 好的 Description:
@Description("手机号码,11位数字,格式:1XXXXXXXXXX")
private String phone;
@Description("评分,1-5之间的整数,5分为最高")
private int rating;
@Description("邮箱地址,必须包含 @ 符号")
private String email;
原则:
- 明确格式要求
- 指定取值范围
- 提供示例(必要时)
❌ 差的 Description:
@Description("电话") // 太模糊
private String phone;
@Description("数据") // 无意义
private Object data;
7.2 模型选择策略
| 场景 | 推荐模型 | 原因 |
|---|---|---|
| 简单提取(姓名、年龄) | gpt-4o-mini | 成本低、速度快 |
| 复杂 POJO(嵌套对象) | gpt-4o | 理解能力强 |
| 高精度要求(金融、医疗) | gpt-4o + json_schema | 最可靠 |
| 批量处理(千级以上) | gpt-4o-mini | 成本可控 |
7.3 错误处理策略
@Service
public class RobustExtractor {
@Autowired
private ResumeExtractor extractor;
public Resume extractWithRetry(String text, int maxRetries) {
for (int i = 0; i < maxRetries; i++) {
try {
Resume resume = extractor.extractResume(text);
// 验证结果
if (validateResume(resume)) {
return resume;
}
logger.warn("第 {} 次提取结果无效,重试中...", i + 1);
} catch (Exception e) {
logger.warn("第 {} 次提取失败:{}", i + 1, e.getMessage());
}
}
throw new ServiceException("多次提取失败,请检查输入文本");
}
private boolean validateResume(Resume resume) {
// 基本验证
return resume.getName() != null && !resume.getName().isEmpty();
}
}
结语
通过本文的学习,你已经掌握了 LangChain4j 结构化输出的核心技术。从基础的枚举、数值、日期提取,到复杂的嵌套 POJO 解析,再到 JSON Mode 的强制结构化输出,这些技术能让 AI 从"聊天机器人"升级为"数据提取引擎"。记住,好的结构化输出依赖三个要素:清晰的 @Description 字段说明、合适的 JSON Mode 配置、以及健壮的错误处理机制。在实际项目中,建议优先使用 json_object 模式平衡性能与可靠性,对于金融、医疗等高精度场景再考虑 json_schema。

🎯🔖更多专栏系列文章:AI大模型提示工程完全指南、AI大模型探索之路(零基础入门)、AI大模型预训练微调进阶、AI大模型开源精选实践、AI大模型Spring AI开发实战🔥🔥🔥 其他专栏可以查看博客主页
🔔 关于作者:资深程序老猿,10年+架构经验,现专注 AIGC 探索与实践。
👍 若文章对你有所触动,恳请点赞 ⭐ 关注 ⭐ 收藏!AI 浪潮已至,愿与你同行。
AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。
更多推荐

所有评论(0)