本文介绍如何 LangChain4j 的结构化输出,让 AI 返回 Java 对象, 这是 LangChain4j 最强大的功能之一!你可以让 AI 直接返回 JSON,并自动映射成 Java 对象。
一、痛点:AI返回字符串,然后呢?
上一篇文章中,我们让AI回答了一个问题,返回的是String。但在实际业务中,我们想要的往往不止是一段文字:
- 做一个情感分析,想要返回
POSITIVE、NEGATIVE、NEUTRAL枚举
- 做信息抽取,想要直接得到一个
Person对象(包含姓名、年龄、地址)
- 做意图识别,想要返回结构化的意图分类和参数
如果每次都解析字符串,代码会变成这样:
java
1 2 3
| String response = ai.chat("张三,25岁,住在北京");
|
LangChain4j直接解决了这个问题:你告诉它你要什么类型,它自动把AI输出转成那个类型。支持String、基本类型、枚举、List、Map,以及自定义POJO。
二、开箱即用的类型支持
2.1 基本类型和包装类
最简单的用法:直接让AI返回int、boolean、double等。
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岁"); boolean positive = extractor.isPositive("今天天气真好"); BigDecimal price = extractor.extractPrice("这件衣服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("这个产品太棒了!");
|
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
| @Data public class Person { private String name; private int age; private String city; }
@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()); 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", "年龄");
|
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(); 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") .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 {
@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 {
@UserMessage("把{{source}}翻译成{{target}}:{{content}}") String translate( @V("content") String text, @V("source") String sourceLang, @V("target") String targetLang ); }
|
- 与其他语法的对比
| 写法 |
含义 |
适用场景 |
{{it}} |
代表唯一的那个参数 |
只有一个参数,且没写 @V |
{{变量名}} |
代表 @V("变量名") 绑定的参数 |
多个参数,或想明确命名 |
{{?变量名}} |
可选参数,如果为null则替换为空 |
参数可能为空时 |
实际代码示例
1 2 3 4 5 6 7 8 9 10 11 12
| @UserMessage("分析:{{it}}") String analyze(String anyName);
@UserMessage("分析:{{content}}") String analyze(@V("content") String text);
@UserMessage("分析:{{text}}") String analyze(String text);
|
最佳实践建议
- 单参数简单场景:用
{{it}} 最简洁
- 多参数或复杂场景:用
@V 命名变量,代码可读性更好
- 团队协作项目:建议统一使用
@V 命名变量,避免 {{it}} 带来的困惑
1 2 3 4 5 6 7
| @UserMessage("从「{{content}}」中提取{{field}}") String extract(@V("content") String text, @V("field") String field);
@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
| @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; }
@AiService public interface ContactExtractor { @SystemMessage("你是一个专业的信息抽取助手,擅长从非结构化文本中提取结构化信息。") @UserMessage("从以下文本中提取联系人信息,以JSON格式返回:\n{{it}}") ContactInfo extract(@V("it") String text); @UserMessage("从以下文本中提取所有联系人的信息,以JSON数组格式返回:\n{{it}}") List<ContactInfo> extractAll(String text); }
@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开发者可以像调用普通方法一样使用大模型——输入文本,输出对象。核心要点:
| 场景 |
推荐方案 |
| 简单类型(数字、布尔) |
直接用int、boolean作为返回类型 |
| 枚举类型 |
定义enum,AI自动匹配 |
| 集合类型 |
用List<T>、Set<T> |
| 复杂对象 |
自定义POJO + @Description |
| 需要token等元信息 |
用Result<T>包装 |
| 动态提示词 |
用@V绑定参数 |