章
目
录
在开发与大语言模型(LLM)交互的应用时,获取结构化的输出至关重要。今天咱们就深入探讨LangChain4j如何实现结构化JSON输出,通过实际的代码示例,让大家掌握如何从LLM获取特定格式的响应,并将其填充到模型对象中。这篇文章会涵盖响应模型的定义、接口和服务的使用、错误处理等关键内容,非常适合想要深入了解LangChain4j在结构化输出方面应用的朋友。
一、响应模型的定义
假设在我们的应用程序里有一个Person
记录,这个记录就像是一个用来装个人信息的“小盒子”,它包含了name
(姓名)、age
(年龄)、city
(城市)和country
(国家)这四个字段。我们希望从LLM模型的响应中提取这些信息,为每一个响应创建一个新的Person
对象。定义Person
记录的代码如下:
record Person(String name, int age, String country, String city) {}
这个record
关键字就像是一个快捷方式,用它定义的Person
记录,简洁地描述了我们需要的个人信息结构。
二、提取器接口与AiServices的运用
在LangChain4j里,有个很实用的功能,和HttpExchange
或者Retrofit
有点像,我们可以创建带有期望API的声明式接口,然后让LangChain4j帮我们生成一个实现这个接口的对象(代理对象)。这个代理对象可厉害啦,它能把和LLM交互时那些复杂的操作都“藏起来”,还能帮我们完成一些常用的重复性任务,比如给LLM格式化输入内容,还有解析LLM返回的输出结果。
下面来看个例子,我们创建一个PersonExtractor
接口,这个接口里有个方法,专门用来从一段没有特定格式的文本里,提取出结构化的JSON格式的个人信息。这里的@UserMessage
注解就像是给LLM的一个“小纸条”,上面写着提取信息的具体要求,比如让它只用JSON格式返回信息,而且不要带任何Markdown格式的标记。代码如下:
interface PersonExtractor {
@UserMessage("""
Extract the name, age. city and country of the person described below.
Return only JSON, without any markdown markup surrounding it.
Here is the document describing the person:
---
{{it}}
""")
Person extract(String text);
}
接下来,我们可以用AiServices.create()
方法创建这个接口的代理对象,这个方法利用了Java的动态代理机制。在创建代理对象之前,我们得先创建一个聊天语言模型实例,这里以OpenAI的gpt-3.5-turbo
模型为例。代码如下:
// 创建聊天语言模型实例,使用OpenAI的gpt-3.5-turbo模型,并配置API密钥、开启请求和响应日志记录
ChatLanguageModel model = OpenAiChatModel.builder()
.apiKey(OPENAI_API_KEY)
.modelName(OpenAiChatModelName.GPT_3_5_TURBO)
.logRequests(true)
.logResponses(true)
.build();
// 创建PersonExtractor接口的代理对象,将接口和模型实例传入
PersonExtractor personExtractor = AiServices.create(PersonExtractor.class, model);
现在,我们就可以用personExtractor
这个对象来调用extract()
方法啦。把一段描述人的文本作为参数传进去,它就能返回一个Person
对象。比如:
// 定义一段描述人的文本
String inputText = """
Charles Brown, aged 56, resides in the United Kingdom. Originally
from a small town in Devon Charles developed a passion for history
and archaeology from an early age, which led him to pursue a career
as an archaeologist specializing in medieval European history.
He completed his education at the University of Oxford, where
he earned a degree in Archaeology and History.
""";
// 调用extract方法,提取信息并返回Person对象
Person person = personExtractor.extract(inputText);
// 打印Person对象,查看提取的信息
System.out.println(person);
运行这段代码后,程序输出的内容里,会看到输入的提示自动包含了根据Person
记录生成的期望JSON格式。同时,在日志里还能看到详细的请求和响应信息,方便我们了解程序和LLM之间的“交流”过程。
三、处理响应解析错误
不是所有的LLM模型都能乖乖按照我们的要求返回严格的JSON格式响应。比如说,Gemini模型就喜欢把JSON内容包在Markdown代码块里。这样一来,当程序在后台把JSON转换成Java对象(POJO)时,就会出错。
遇到这种情况,我们有几种解决办法。第一种方法是,先把响应内容当成普通字符串提取出来,而不是直接转换成Person
记录,然后把那些多余的Markdown标记或者其他不需要的格式去掉。代码示例如下:
// 假设response是从模型返回的原始响应字符串
String cleanedResponse = response.replaceAll("^```json\n|```$", "");
// 去掉Markdown代码块标记
Person person = objectMapper.readValue(cleanedResponse, Person.class);
还有一种方法,我们可以试着把模型的temperature
(温度系数)和topK
这两个参数的值调低。这两个参数就像是模型的“小开关”,调低它们的值,像Gemini这样的模型生成的输出就会更“听话”,更严格地按照我们的指令来,也就是提高输出的确定性。不过要注意,把这两个参数调低,模型生成多样化、有创意的响应的能力就会变弱。所以,当严格遵循指令比创意更重要的时候,我们才选择这种方法。
四、完整示例代码
下面是一个完整的示例代码,展示了如何严格按照JSON格式获取模型响应,并把响应内容填充到模型POJO里。这个示例主要是为了帮助大家快速上手,实际开发中,大家要记得把代码写得更模块化,做好代码的打包和日志记录工作。
import dev.langchain4j.model.chat.ChatLanguageModel;
import dev.langchain4j.model.openai.OpenAiChatModel;
import dev.langchain4j.model.openai.OpenAiChatModelName;
import dev.langchain4j.service.AiServices;
import dev.langchain4j.service.UserMessage;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.ApplicationRunner;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.annotation.Bean;
// 标记这是一个Spring Boot应用的主配置类
@SpringBootApplication
public class StructuredResponseApplication {
// 应用程序的入口方法,启动Spring Boot应用
public static void main(String[] args) {
SpringApplication.run(StructuredResponseApplication.class);
}
// 通过注解从配置文件中获取OpenAI API密钥
@Value("${OPENAI_API_KEY}")
private String OPENAI_API_KEY;
// 定义Person记录,包含姓名、年龄、国家和城市字段
record Person(String name, int age, String country, String city) {}
// 定义PersonExtractor接口,用于从文本中提取Person信息
interface PersonExtractor {
@UserMessage("""
Extract the name, age. city and country of the person described below.
Return only JSON, without any markdown markup surrounding it.
Here is the document describing the person:
---
{{it}}
""")
Person extract(String text);
}
// 定义一个Bean,用于在应用启动时执行特定逻辑
@Bean("structuredResponseApplicationRunner")
ApplicationRunner applicationRunner() {
return args -> {
// 创建OpenAI聊天语言模型实例,配置API密钥等参数
ChatLanguageModel model = OpenAiChatModel.builder()
.apiKey(OPENAI_API_KEY)
.modelName(OpenAiChatModelName.GPT_3_5_TURBO)
.logRequests(true)
.logResponses(true)
.build();
// 创建PersonExtractor接口的代理对象
PersonExtractor personExtractor = AiServices.create(PersonExtractor.class, model);
// 定义输入文本,描述一个人的信息
String inputText = """
Charles Brown, aged 56, resides in the United Kingdom. Originally
from a small town in Devon Charles developed a passion for history
and archaeology from an early age, which led him to pursue a career
as an archaeologist specializing in medieval European history.
He completed his education at the University of Oxford, where
he earned a degree in Archaeology and History.
""";
// 调用extract方法提取信息,得到Person对象
Person person = personExtractor.extract(inputText);
// 打印Person对象,查看提取的信息
System.out.println(person);
};
}
}
五、总结
在LangChain4j中,使用AiServices
这种方式和强类型对象打交道,好处可不少。它让我们不用直接和LLM进行复杂的交互,而是通过像PersonExtractor
和Person
这样具体的类来操作。这样一来,日常开发时,我们就不用太操心和LLM交互的那些复杂细节,能把更多精力放在业务逻辑上,开发效率也能大大提高。希望大家通过这篇文章,对LangChain4j的结构化JSON输出有更深入的理解,在实际项目中能灵活运用!如果在学习过程中有什么问题,欢迎随时交流。