LangChain4j 让 AI 返回 Java 对象

LangChain4j 让 AI 返回 Java 对象

薛定谔的汪 Lv5

本文介绍如何 LangChain4j 的结构化输出,让 AI 返回 Java 对象, 这是 LangChain4j 最强大的功能之一!你可以让 AI 直接返回 JSON,并自动映射成 Java 对象。

一、痛点:AI返回字符串,然后呢?

上一篇文章中,我们让AI回答了一个问题,返回的是String。但在实际业务中,我们想要的往往不止是一段文字:

  • 做一个情感分析,想要返回POSITIVENEGATIVENEUTRAL枚举
  • 做信息抽取,想要直接得到一个Person对象(包含姓名、年龄、地址)
  • 做意图识别,想要返回结构化的意图分类和参数

如果每次都解析字符串,代码会变成这样:

java

1
2
3
String response = ai.chat("张三,25岁,住在北京");
// 然后手动解析:找名字、找年龄、找地址...
// 又臭又长,还容易出错

LangChain4j直接解决了这个问题:你告诉它你要什么类型,它自动把AI输出转成那个类型。支持String、基本类型、枚举、List、Map,以及自定义POJO。

二、开箱即用的类型支持

2.1 基本类型和包装类

最简单的用法:直接让AI返回intbooleandouble等。

java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
@AiService
public interface NumberExtractor {

// 提取数字
@UserMessage("从「{{it}}」中提取数字")
int extractInt(String text);

// 判断情感倾向
@UserMessage("判断「{{it}}」是否具有正面情感")
boolean isPositive(String text);

// 提取金额
@UserMessage("从「{{it}}」中提取金额数字")
BigDecimal extractPrice(String text);
}

使用示例:

java

1
2
3
4
5
6
7
8
@Autowired
private NumberExtractor extractor;

public void demo() {
int age = extractor.extractInt("我今年25岁"); // 返回 25
boolean positive = extractor.isPositive("今天天气真好"); // 返回 true
BigDecimal price = extractor.extractPrice("这件衣服100元"); // 返回 100
}

2.2 枚举类型

适合情感分析、意图识别等场景:

java

1
2
3
4
5
6
7
8
9
10
11
12
@AiService
public interface SentimentAnalyzer {

@UserMessage("分析「{{it}}」的情感倾向")
Sentiment analyze(String text);

enum Sentiment {
POSITIVE, // 正面
NEGATIVE, // 负面
NEUTRAL // 中性
}
}

调用效果:

java

1
2
Sentiment result = analyzer.analyze("这个产品太棒了!");
// result = Sentiment.POSITIVE

2.3 List和Set集合

当AI需要返回多个结果时非常实用:

java

1
2
3
4
5
6
7
8
9
@AiService
public interface KeywordExtractor {

@UserMessage("从「{{it}}」中提取关键词,返回JSON数组格式")
List<String> extractKeywords(String text);

@UserMessage("从「{{it}}」中提取所有提到的城市")
Set<String> extractCities(String text);
}

💡 小技巧:在@UserMessage中提示AI返回JSON数组格式,成功率会大幅提升。

三、核心技巧:自定义POJO对象

这才是真正的杀手锏。定义一个普通的Java类,LangChain4j会自动把AI输出映射成这个对象。

3.1 基础用法

java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// 1. 定义POJO
@Data
public class Person {
private String name;
private int age;
private String city;
}

// 2. 定义AI服务接口
@AiService
public interface PersonExtractor {

@UserMessage("从以下文本中提取人物信息:「{{it}}」")
Person extractFrom(String text);
}

调用一下看看效果:

java

1
2
3
4
5
6
String text = "我叫张三,今年28岁,在上海工作。";
Person person = extractor.extractFrom(text);

System.out.println(person.getName()); // 张三
System.out.println(person.getAge()); // 28
System.out.println(person.getCity()); // 上海

就这么简单!LangChain4j内部使用Jackson/Gson自动解析JSON,你完全不用操心解析逻辑。

注意:

💡 虽然我们定义的枚举类型是英文,但是 AI 会自动将提示词文本信息中的中文信息与英文做适配,查看请求日志会发现LangChain4j 帮我们加了:

You must answer strictly in the following JSON format: {\n"name": (type: string),\n"age": (type: integer),\n"city": (type: string)\n}”

AI 在训练时做了大量了中英文翻译对照,所以对于一些常见的单词是可以做适配的。

3.2 @Description:给字段加”说明书”

AI毕竟不是人,有时候不知道字段的含义。@Description注解可以给字段添加说明,帮助AI更准确地填充数据:

java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
@Data
public class OrderInfo {

@Description("订单号,通常以ORD开头")
private String orderId;

@Description("订单金额,单位:元")
private BigDecimal amount;

@Description("订单状态,可选值:待支付(PENDING_PAYMENT)、已支付(PAID)、已取消(CANCELLED)")
private String status;

@Description("下单时间,格式:yyyy-MM-dd")
private LocalDate orderDate;
}

@Description就像是在告诉AI:”这个字段是干这个用的,你看着填”。对于复杂的业务场景,这个注解能显著提高输出准确率。

3.3 嵌套对象

POJO里面套POJO?没问题,LangChain4j完全支持:

java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
@Data
public class Article {
private String title;
private String content;
private Author author; // 嵌套对象
private List<String> tags; // 集合类型
}

@Data
public class Author {
private String name;
private String email;
}

@AiService
public interface ArticleParser {
@UserMessage("解析以下文章,提取标题、正文、作者信息和标签:{{it}}")
Article parse(String text);
}

四、进阶技巧:处理复杂场景

4.1 @V注解:动态模板变量

@V注解用于将方法参数绑定到提示词模板中的变量{{变量名}}

java

1
2
3
4
5
6
7
8
9
10
11
12
13
@AiService
public interface FlexibleExtractor {

@UserMessage("从「{{text}}」中提取{{fieldName}}信息,只返回该字段的值")
String extractField(
@V("text") String text,
@V("fieldName") String fieldName
);
}

// 使用
String name = extractor.extractField("姓名:李四,年龄:30", "姓名"); // 返回"李四"
String age = extractor.extractField("姓名:李四,年龄:30", "年龄"); // 返回"30"
1
2
3
4
5
6
7
8
9
10
11
12
13
@AiService
public interface FlexibleExtractionService {

@SystemMessage("提取文本中的信息,按照以下格式返回JSON:{{format}}")
Person extractWithFormat(
@UserMessage String text,
@V("format") String format // 动态传入字段说明
);
}

// 使用时
String format = "name(姓名), age(年龄), city(城市), email(邮箱)";
Person person = service.extractWithFormat("张三,25岁,北京人,邮箱zhang@example.com", format);

4.2 @StructuredPrompt:结构化提示词

当提示词比较复杂时,可以封装成一个带注解的类:

java

1
2
3
4
5
6
7
8
9
10
11
12
@Data
@StructuredPrompt("根据中国{{lawName}}法律,解答以下问题:{{question}},字数控制在{{wordLimit}}字以内")
public class LegalPrompt {
private String lawName;
private String question;
private int wordLimit;
}

@AiService
public interface LegalAssistant {
String ask(LegalPrompt prompt);
}

这样调用起来更优雅:

java

1
2
3
4
5
6
LegalPrompt prompt = new LegalPrompt();
prompt.setLawName("民法典");
prompt.setQuestion("合同违约如何处理?");
prompt.setWordLimit(500);

String answer = assistant.ask(prompt);

4.3 Result:获取更多元信息

如果除了AI的回答内容,你还想要token消耗、调用原因等信息,可以用Result<T>包装:

java

1
2
3
4
5
6
7
8
9
10
@AiService
public interface Analyzer {
Result<Sentiment> analyzeWithDetails(String text);
}

// 使用
Result<Sentiment> result = analyzer.analyzeWithDetails("这个产品太棒了!");
Sentiment sentiment = result.content(); // 实际内容
TokenUsage tokens = result.tokenUsage(); // token消耗
FinishReason reason = result.finishReason(); // 完成原因

Result<T>会保留完整的调用上下文,非常适合生产环境的监控和调试。

4.4 开启JSON模式提高准确性

对于支持JSON模式的模型(如OpenAI、通义千问),可以开启严格JSON模式,强制AI输出合法的JSON:

java

1
2
3
4
5
6
OpenAiChatModel model = OpenAiChatModel.builder()
.apiKey("your-api-key")
.modelName("gpt-4o-mini")
.responseFormat("json_schema") // 开启JSON模式
.strictJsonSchema(true) // 严格校验
.build();

开启后,AI输出不符合JSON规范时会自动重试或报错,大大提高了结构化输出的成功率。

附: {{it}} 在 LangChain4j 中的含义

是 LangChain4j 提示词模板中的一个**特殊占位符变量**,表示"当前输入参数"。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22

1. 为什么会有 `{{it}}`?

在 `@UserMessage` 注解中,你可以动态插入方法参数的值。最常用的是**命名变量**(如 ```{{text}}```),但当方法只有一个参数且没有指定变量名时,框架会自动将这个参数绑定到 ```{{it}}```。

简单说:**```{{it}}``` 就是"它",代表那个唯一的输入参数**。

2. 具体用法对比

2.1 只有一个参数,没写 `@V`

```java
@AiService
public interface MyAssistant {
// 只有一个参数,可以直接用 {{it}}
@UserMessage("翻译成中文:{{it}}")
String translate(String text);
}

// 调用时,text的值会自动替换 {{it}}
translate.translate("Hello World");
// 实际发送给AI的提示词:翻译成中文:Hello World

2.2 只有一个参数,但写了 @V

1
2
3
4
5
6
7
@AiService
public interface MyAssistant {

// 用 @V 指定了变量名为 content,这时就不能用 {{it}} 了
@UserMessage("翻译成中文:{{content}}")
String translate(@V("content") String text);
}

2.3 多个参数,必须用命名变量

1
2
3
4
5
6
7
8
9
10
11
@AiService
public interface MyAssistant {

// 多个参数时,必须用 @V 指定变量名,不能用 {{it}}
@UserMessage("把{{source}}翻译成{{target}}:{{content}}")
String translate(
@V("content") String text,
@V("source") String sourceLang,
@V("target") String targetLang
);
}
  1. 与其他语法的对比
写法 含义 适用场景
{{it}} 代表唯一的那个参数 只有一个参数,且没写 @V
{{变量名}} 代表 @V("变量名") 绑定的参数 多个参数,或想明确命名
{{?变量名}} 可选参数,如果为null则替换为空 参数可能为空时
  1. 实际代码示例

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    // 正确示例1:参数名随意,用 {{it}} 表示它
    @UserMessage("分析:{{it}}")
    String analyze(String anyName); // anyName 自动绑定到 it

    // 正确示例2:明确指定变量名
    @UserMessage("分析:{{content}}")
    String analyze(@V("content") String text); //

    // 正确示例3:利用编译参数 -parameters,可以省略 @V
    // 前提是 Maven/Gradle 配置了 -parameters 编译选项
    @UserMessage("分析:{{text}}")
    String analyze(String text); // 参数名 text 自动作为变量名

最佳实践建议

  1. 单参数简单场景:用 {{it}} 最简洁
  2. 多参数或复杂场景:用 @V 命名变量,代码可读性更好
  3. 团队协作项目:建议统一使用 @V 命名变量,避免 {{it}} 带来的困惑
1
2
3
4
5
6
7
// 推荐:清晰明确
@UserMessage("从「{{content}}」中提取{{field}}")
String extract(@V("content") String text, @V("field") String field);

// 可以但不推荐:it 语义不清晰
@UserMessage("从「{{it}}」中提取信息")
String extract(String text);

五、实战:完整的信息抽取示例

把上面学到的综合起来,实现一个完整的信息抽取服务:

java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
// POJO定义
@Data
public class ContactInfo {
@Description("联系人姓名")
private String name;

@Description("手机号码,11位数字")
private String phone;

@Description("电子邮箱地址")
private String email;

@Description("公司名称")
private String company;

@Description("职位")
private String position;
}

// AI服务接口
@AiService
public interface ContactExtractor {

@SystemMessage("你是一个专业的信息抽取助手,擅长从非结构化文本中提取结构化信息。")
@UserMessage("从以下文本中提取联系人信息,以JSON格式返回:\n{{it}}")
ContactInfo extract(@V("it") String text);

// 批量提取版本
@UserMessage("从以下文本中提取所有联系人的信息,以JSON数组格式返回:\n{{it}}")
List<ContactInfo> extractAll(String text);
}

// Controller使用
@RestController
public class ContactController {

@Autowired
private ContactExtractor extractor;

@PostMapping("/extract")
public ContactInfo extract(@RequestBody String text) {
return extractor.extract(text);
}
}

输入一段非结构化文本:

“张小明,电话18888888888,邮箱zhangxm@example.com,就职于某某科技有限公司,担任技术经理。”

输出就是一个完整的ContactInfo对象,所有字段都已填好。

六、注意事项与最佳实践

6.1 给AI足够的提示

结构化输出的关键在于提示词要清晰。如果AI总是填不对,试试:

  • @UserMessage中明确说明输出格式
  • 使用@Description给字段加说明
  • 在SystemMessage中强调”只返回JSON,不要有其他内容”

6.2 字段命名用英文

AI训练数据中英文字段名更常见。如果你的POJO字段是中文名,AI可能不知道该怎么填。

6.3 处理异常情况

AI的输出不一定100%符合预期,建议加一层兜底。

七、总结

LangChain4j的结构化输出能力,让Java开发者可以像调用普通方法一样使用大模型——输入文本,输出对象。核心要点:

场景 推荐方案
简单类型(数字、布尔) 直接用intboolean作为返回类型
枚举类型 定义enum,AI自动匹配
集合类型 List<T>Set<T>
复杂对象 自定义POJO + @Description
需要token等元信息 Result<T>包装
动态提示词 @V绑定参数
  • Title: LangChain4j 让 AI 返回 Java 对象
  • Author: 薛定谔的汪
  • Created at : 2025-03-15 18:01:54
  • Updated at : 2026-04-07 19:24:34
  • Link: https://www.zhengyk.cn/2025/03/15/ai/langchain4j_02_pojo/
  • License: This work is licensed under CC BY-NC-SA 4.0.
Comments