LangChain4j 结构化JSON输出实战:从原理到代码示例

后端 潘老师 2个月前 (02-20) 153 ℃ (0) 扫码查看

在开发与大语言模型(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进行复杂的交互,而是通过像PersonExtractorPerson这样具体的类来操作。这样一来,日常开发时,我们就不用太操心和LLM交互的那些复杂细节,能把更多精力放在业务逻辑上,开发效率也能大大提高。希望大家通过这篇文章,对LangChain4j的结构化JSON输出有更深入的理解,在实际项目中能灵活运用!如果在学习过程中有什么问题,欢迎随时交流。


版权声明:本站文章,如无说明,均为本站原创,转载请注明文章来源。如有侵权,请联系博主删除。
本文链接:https://www.panziye.com/back/14795.html
喜欢 (0)
请潘老师喝杯Coffee吧!】
分享 (0)
用户头像
发表我的评论
取消评论
表情 贴图 签到 代码

Hi,您需要填写昵称和邮箱!

  • 昵称【必填】
  • 邮箱【必填】
  • 网址【可选】