本篇文件介绍LangChain4j如何将大模型返回进行格式化输出的,比如转换为List、Map或者自定义的POJO。
LangChain4j支持返回类型
如果要从 LLM接收结构化输出,可以将 AI Service 方法的返回类型更改为 String
或其他,目前支持如下类型的返回;
- 字符串
String
。 - 基本类型
boolean
/byte
/short
/int
/long
/float
/double
。 - 对象类型
Boolean
/Byte
/Short
/Integer
/Long
/Float
/Double
/BigDecimal
。 - 时间类型
Date
/LocalDate
/LocalTime
/LocalDateTime
。 - 集合类型
List<String>
/Set<String>
。 - 枚举类型
Enum
。 - 自定义 POJO。
- 自定义
Result<T>
。 - 大模型回复消息
AiMessage
。
结构化输出示例
布尔与枚举
@AiService
public interface SentimentAnalyzer {
@UserMessage("Does {{it}} have positive sentiment?")
boolean isPositive(String text);
@UserMessage("Analyze sentiment of {{it}}")
Sentiment analyzeSentimentOf(String text);
enum Sentiment {
POSITIVE, NEGATIVE, NEUTRAL
}
}
http测试 [http/sentiment-rest.http]
### 测试返回布尔值
GET http://localhost:8806/boolean?prompt=This is great!
### 测试返回布尔值
GET http://localhost:8806/boolean?prompt=It's awful!
### 测试返回枚举值
GET http://localhost:8806/enum?prompt=This is great!
### 测试返回枚举值
GET http://localhost:8806/enum?prompt=I don't know.
数字类型
package org.ivy.chatmemory.service;
import dev.langchain4j.service.UserMessage;
import dev.langchain4j.service.spring.AiService;
import java.math.BigDecimal;
import java.math.BigInteger;
@AiService
public interface NumberExtractor {
@UserMessage("Extract a number from {{it}}")
int extractInt(String text);
@UserMessage("Extract a long number from {{it}}")
long extractLong(String text);
@UserMessage("Extract a big integer from {{it}}")
BigInteger extractBigInteger(String text);
@UserMessage("Extract a float number from {{it}}")
float extractFloat(String text);
@UserMessage("Extract a double number from {{it}}")
double extractDouble(String text);
@UserMessage("Extract a big decimal from {{it}}")
BigDecimal extractBigDecimal(String text);
}
http测试[http/number-extractor-rest.http]
prompt: 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值
GET http://localhost:8806/int?prompt=
### 测试返回BigInteger值
GET http://localhost:8806/integer?prompt=
### 测试返回float值
GET http://localhost:8806/float?prompt=
### 测试返回double值
GET http://localhost:8806/double?prompt=
### 测试返回lōng值
GET http://localhost:8806/long?prompt=
### 测试返回BigDecimal值
GET http://localhost:8806/bigDecimal?prompt=xxx
Bean POJO
- 简单的方式
package org.ivy.chatmemory.service;
import dev.langchain4j.service.UserMessage;
import dev.langchain4j.service.spring.AiService;
import lombok.Getter;
import lombok.Setter;
import lombok.ToString;
import java.time.LocalDate;
@AiService
public interface PojoExtractor {
@UserMessage("Extract information about a person from {{it}}")
Person extractPerson(String text);
@Getter
@Setter
@ToString
class Person {
private String firstName;
private String lastName;
private LocalDate birthDate;
}
}
http测试 [http/pojo-extractor-rest.http]
GET localhost:8806/pojo?prompt="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."
accept: application/json
输出结果:
{
"firstName": "John",
"lastName": "Doe",
"birthDate": "1968-07-04"
}
- 带有@Description描述的pojo
package org.ivy.chatmemory.service;
import dev.langchain4j.model.input.structured.StructuredPrompt;
import dev.langchain4j.model.output.structured.Description;
import dev.langchain4j.service.spring.AiService;
import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.Setter;
import lombok.ToString;
import java.time.LocalDate;
import java.util.List;
@AiService
public interface PojoDescription {
// =========给出原材料创建菜肴的步骤和时间 =================
/**
* 给出一组原料,做出菜肴
*
* @param ingredients:菜肴的原料
* @return Recipe
*/
Recipe createRecipe(String... ingredients);
Recipe createRecipe(CreateRecipePrompt prompt);
/**
* 烹饪步骤、需要时间等
*/
@Getter
@Setter
@ToString
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 4 words, steps should be rhyme")
private List<String> steps;
private Integer preparationTimeMinutes;
}
@Getter
@AllArgsConstructor
@StructuredPrompt("Create a recipe of a {{dish}} that can be prepared using only {{ingredients}}")
class CreateRecipePrompt {
private String dish;
private List<String> ingredients;
}
http测试
### 黄瓜、番茄、羊奶酪、洋葱、橄榄
GET http://localhost:8806/recipe?prompt="cucumber", "tomato", "feta", "onion", "olives"
###
GET http://localhost:8806/recipe2?prompt=["cucumber", "tomato", "feta", "onion", "olives"]
日期与时间
package org.ivy.chatmemory.service;
import dev.langchain4j.service.UserMessage;
import dev.langchain4j.service.spring.AiService;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.time.LocalTime;
@AiService
public interface DateTimeExtractor {
@UserMessage("Extract date from {{it}}")
LocalDate extractDate(String text);
@UserMessage("Extract time from {{it}}")
LocalTime extractTime(String text);
@UserMessage("Extract date and time from {{it}}")
LocalDateTime extractDateTime(String text);
}
http测试[date-time-extractor-rest.http]
GET http://localhost:8806/extract-date?prompt=The tranquility pervaded the evening of 1968, just fifteen minutes shy of midnight," +
" following the celebrations of Independence Day.
### 测试返回 LocalTime 类型的时间
GET http://localhost:8806/extract-time?prompt=The tranquility pervaded the evening of 1968, just fifteen minutes shy of midnight," +
" following the celebrations of Independence Day.
### 测试返回 LocalDateTime 类型的时间
GET http://localhost:8806/extract-datetime?prompt=The tranquility pervaded the evening of 1968, just fifteen minutes shy of midnight," +
" following the celebrations of Independence Day.
Result<T>
package org.ivy.chatmemory.service;
import dev.langchain4j.model.input.structured.StructuredPrompt;
import dev.langchain4j.service.Result;
import dev.langchain4j.service.spring.AiService;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.Getter;
import java.time.LocalDate;
@AiService
public interface ResultExtractor {
Result<Film> film(FilmPrompt prompt);
// =============== 查询某个导演最受欢迎的电影,返回电影信息 ==========
@Getter
@AllArgsConstructor
@StructuredPrompt("请问{{director}}导演最受欢迎的电影是什么?哪年发行的,电影讲述的什么内容?")
class FilmPrompt {
private String director;
}
@Data
class Film {
private String title;
private String director;
private LocalDate publicationDate;
private String description;
}
}
http测试[result-extractor-rest.http]
### 张艺谋导演最受欢迎的电影
GET http://localhost:8806/film?prompt=张艺谋
结构化输出源码分析
OutputParser
OutputParser是所有Parser的统一接口,定义了两个方法,一个完成解析和对格式返回的描述。
package dev.langchain4j.model.output;
public interface OutputParser<T> {
/**
* 将文本转换为T类型
* @param text 需要转换的文本
* @return 转换后的类型.
*/
T parse(String text);
/**
* 需要转换格式的描述.
* @return 文本格式的描述.
*/
String formatInstructions();
}
实现类
ServiceOutputParser
真正逻辑实现的地方,重点看两个方法,一个是解析方法,一个是返回值描述方法。
public static String outputFormatInstructions(Class<?> returnType) {
// 返回类型为String或者大模型本身的类型,则不需要返回描述
if (returnType == String.class
|| returnType == AiMessage.class
|| returnType == TokenStream.class
|| returnType == Response.class) {
return "";
}
// 在定义方法时,返回时不允许为void类型,也是一个规则!!!!
if (returnType == void.class) {
throw illegalConfiguration("Return type of method '%s' cannot be void");
}
// 返回值是枚举类型,特殊处理一下
if (returnType.isEnum()) {
String formatInstructions = new EnumOutputParser(returnType.asSubclass(Enum.class)).formatInstructions();
return "\nYou must answer strictly in the following format: " + formatInstructions;
}
// 如果返回的类型是基本的类型,则直接从Map中获取,OUTPUT_PARSERS维护了类型与解析器的关系
OutputParser<?> outputParser = OUTPUT_PARSERS.get(returnType);
if (outputParser != null) {
String formatInstructions = outputParser.formatInstructions();
return "\nYou must answer strictly in the following format: " + formatInstructions;
}
// 返回值是List/Set类型
if (returnType == List.class || returnType == Set.class) {
return "\nYou must put every item on a separate line.";
}
// 最后是对象类型,返回Json
return "\nYou must answer strictly in the following JSON format: " + jsonStructure(returnType, new HashSet<>());
}
// 对Json描述进行处理
private static String jsonStructure(Class<?> structured, Set<Class<?>> visited) {
StringBuilder jsonSchema = new StringBuilder();
jsonSchema.append("{\n");
for (Field field : structured.getDeclaredFields()) {
String name = field.getName();
if (name.equals("__$hits$__") || java.lang.reflect.Modifier.isStatic(field.getModifiers())) {
// Skip coverage instrumentation field.
continue;
}
jsonSchema.append(format(""%s": (%s),\n", name, descriptionFor(field, visited)));
}
int trailingCommaIndex = jsonSchema.lastIndexOf(",");
if (trailingCommaIndex > 0) {
jsonSchema.delete(trailingCommaIndex, trailingCommaIndex +1);
}
jsonSchema.append("}");
return jsonSchema.toString();
}
此方法对返回值,一并发给大模型,然后大模型根据返回格式的说明 + 提示词,进行对应的返回。返回后需要调用parse方法进行转换
public static Object parse(Response<AiMessage> response, Class<?> returnType) {
if (returnType == Response.class) {
return response;
}
// 返回值 AiMessage类型
AiMessage aiMessage = response.content();
if (returnType == AiMessage.class) {
return aiMessage;
}
// 返回值是String类型
String text = aiMessage.text();
if (returnType == String.class) {
return text;
}
// 返回值是基本类型
OutputParser<?> outputParser = OUTPUT_PARSERS.get(returnType);
if (outputParser != null) {
return outputParser.parse(text);
}
// 返回值是List类型
if (returnType == List.class) {
return asList(text.split("\n"));
}
// 返回值是Set类型
if (returnType == Set.class) {
return new HashSet<>(asList(text.split("\n")));
}
// json转换为对象
return Json.fromJson(text, returnType);
}
执行流程
示例代码与总结
深度使用LangChain4j对结构化数据输出的使用,并详细分析了框架的实现原理及处理流程。整体的思路并不难,大家重点看如下几个类的源码就可以了。
- DefaultAiServices #build 方法,核心流程在此
- ServiceOutputParser 负责处理发送给大模型的格式化指令 以及 解析大模型返回的文本到对应的returnType
- OutputParser 接口及 14个实现类。
这几部分代码明白了,结构化数据返回就一目了然了。
如何学习AI大模型?
我在一线互联网企业工作十余年里,指导过不少同行后辈。帮助很多人得到了学习和成长。
我意识到有很多经验和知识值得分享给大家,也可以通过我们的能力和经验解答大家在人工智能学习中的很多困惑,所以在工作繁忙的情况下还是坚持各种整理和分享。但苦于知识传播途径有限,很多互联网行业朋友无法获得正确的资料得到学习提升,故此将并将重要的AI大模型资料包括AI大模型入门学习思维导图、精品AI大模型学习书籍手册、视频教程、实战学习等录播视频免费分享出来。
第一阶段: 从大模型系统设计入手,讲解大模型的主要方法;
第二阶段: 在通过大模型提示词工程从Prompts角度入手更好发挥模型的作用;
第三阶段: 大模型平台应用开发借助阿里云PAI平台构建电商领域虚拟试衣系统;
第四阶段: 大模型知识库应用开发以LangChain框架为例,构建物流行业咨询智能问答系统;
第五阶段: 大模型微调开发借助以大健康、新零售、新媒体领域构建适合当前领域大模型;
第六阶段: 以SD多模态大模型为主,搭建了文生图小程序案例;
第七阶段: 以大模型平台应用与开发为主,通过星火大模型,文心大模型等成熟大模型构建大模型行业应用。
👉学会后的收获:👈
• 基于大模型全栈工程实现(前端、后端、产品经理、设计、数据分析等),通过这门课可获得不同能力;
• 能够利用大模型解决相关实际项目需求: 大数据时代,越来越多的企业和机构需要处理海量数据,利用大模型技术可以更好地处理这些数据,提高数据分析和决策的准确性。因此,掌握大模型应用开发技能,可以让程序员更好地应对实际项目需求;
• 基于大模型和企业数据AI应用开发,实现大模型理论、掌握GPU算力、硬件、LangChain开发框架和项目实战技能, 学会Fine-tuning垂直训练大模型(数据准备、数据蒸馏、大模型部署)一站式掌握;
• 能够完成时下热门大模型垂直领域模型训练能力,提高程序员的编码能力: 大模型应用开发需要掌握机器学习算法、深度学习框架等技术,这些技术的掌握可以提高程序员的编码能力和分析能力,让程序员更加熟练地编写高质量的代码。
1.AI大模型学习路线图
2.100套AI大模型商业化落地方案
3.100集大模型视频教程
4.200本大模型PDF书籍
5.LLM面试题合集
6.AI产品经理资源合集
👉获取方式:
😝有需要的小伙伴,可以保存图片到wx扫描二v码免费领取【保证100%免费】🆓