LangChain4j 结构化JSON输出实战:从原理到代码示例
在开发与大语言模型(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("^```jsonn|```$", ""); // 去掉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输出有更深入的理解,在实际项目中能灵活运用!如果在学习过程中有什么问题,欢迎随时交流。