基于🦜☕️ LangChain4j 实现问答机器人

376 阅读5分钟

ddcd7e962198ed56347f875627c0ffd3.jpeg

2022 年 11 月 30 日,OpenAI 发布了基于 GPT-3.5 模型调优的新一代对话式AI模型 ChatGPT。随后的一段时间,国内外各大厂商都纷纷跟进,以至于在 2023 年间出现了“百模大战”这一历史节点。

如今的大语言模型(Large Language Model,简称LLM),无论是在底层的基座,还是下游的应用都取得了很大的成功。其中,在2022年同期发布的 LangChain 框架作为LLM应用层开发框架,备受追捧。不过,最开始的时候,并没有 Java 语言的实现。

“一个访问HTTPS的接口的轮子罢了,有什么困难?”

是的,所以23年末,就出现了 LangChain4J 这款产品。Spring 家族也在 24 年中旬发布了Spring AI的 M1 (Milestone 1)版本。

让主 Java 语言的开发者喊出那句话:"Make Java Great Again!"。

目录

  1. 关于这只“鹦鹉”
  2. 快速演示
  3. 机器人实战

关于这只“鹦鹉”

在官方的GitHub下面,有人提问(issues/8673):

LangChain 的 logo 有什么含义?

只有一个非官方的回答:

AI 大型语言模型经常被比作或称为“随机鹦鹉”,这是一种花哨的说法,即它们可以生成模仿人类输出的文本,但它们对生成的内容没有任何真正的“理解”。这个库的作用是将这些 “鹦鹉” 链接在一起,以获得更有意义和有用的输出。

其实这也隐喻着 LLM 的一个本质:模仿🦜与思维链🔗

LangChain4j并没有像原框架那么完善的体系,不过对于简化 Java 在 LLMs 的开发已经非常足够。

image-20241111225308541.png

快速上手

如果只想快速体验一下 AI 应用开发,只需要一个简易的 Maven 即可。

配置

只需要官方的 OpenAI 包即可使用。这里的 LLM 的选择并不强制绑定国外,国内对于 API 的兼容性还是比较好的。

    <dependency>
	    <groupId>dev.langchain4j</groupId>
	    <artifactId>langchain4j-open-ai</artifactId>
	</dependency>

key的选择

对于 API Key,官方提供了一个演示的“后门”,只需要把 Key 的值设置为 Demo 即可。当然,这是一个限量的,并且依赖比较自由的网络。

对于大多数而言,选择一个国内的大模型更为便捷。以下使用腾讯的hunyuan-lite作为演示。

image-20241111231436557.png

将 key 放环境变量(可选),放在代码中也是可以的

image-20241111231546798.png

编码

由于太过简化,直接上代码演示:

#main 中的逻辑,省去了非核心代码。

// 从系统变量中取得key
String apiKey = System.getenv("hunyuan_key");
// 配置基础模型,此处使用了builder设计模式,链式调用。
OpenAiChatModel model = OpenAiChatModel.builder()
  .apiKey(apiKey)
  .modelName("hunyuan-lite")
  .baseUrl("https://api.hunyuan.cloud.tencent.com/v1")
  .build();
// 发送请求,并将答案赋给answer
String answer = model.generate("如何高效学习");
// 引入log是为了方便后续调试。使用System.out.println输出也是没有问题的!
log.info(answer);

hunyuan-lite是免费模型,在功能和速度上有一点限制。再加上是直接输出,可能需要等待一点时间:

稍后可在控制台看到类似的以下输出:

image-20241111232743287.png

流式回答

在与 ChatGPT 进行对话时,可以观察到它是一个字一个字的“敲”出来。避开 LLM 的原理不谈,Java 该如何实现呢?

框架已经想好了这一部分,详细说明查看 dev.langchain4j.model.StreamingResponseHandler

public interface StreamingResponseHandler<T> {
  // 拿到响应时的处理
  void onNext(String token);
  // 响应结束时的处理
  default void onComplete(Response<T> response) {}
  // 出现异常的处理
  void onError(Throwable error);
}

对于这样的接口实现,可以选择实现类-> 匿名实现类->lambda表达式->方法引用。

最后得到如下代码:

String apiKey = System.getenv("hunyuan_key");
OpenAiStreamingChatModel openAiStreamingChatModel = OpenAiStreamingChatModel.builder()
  .apiKey(apiKey)
  .modelName("hunyuan-lite")
  .baseUrl("https://api.hunyuan.cloud.tencent.com/v1")
  .build();

openAiStreamingChatModel.generate("如何高效学习", onNext(System.out::print));

效果演示

展示.gif

机器人实战

接下来进入机器人搭建的实战

初始化

虽说不强制绑定Spring,但就目前环境来说,开发一个应用还是离不开他。为了能够让前端能更好的呈现效果,还需要引入Flux相关依赖:

     <dependency>
         <groupId>dev.langchain4j</groupId>
         <artifactId>langchain4j-reactor</artifactId>
         <version>0.35.0</version>
     </dependency>
     <dependency>
         <groupId>org.springframework.boot</groupId>
         <artifactId>spring-boot-starter-webflux</artifactId>
     </dependency>

AI 服务

在boot项目中,如果想全局任意位置调用一个服务,需要使用@Bean注解。AI 也可以如此。

 @Configuration
 public class HunyuanStreamModelConfig {
 
     @Bean
     OpenAiStreamingChatModel getHunyuanAI() {
         String apiKey = System.getenv("hunyuan_key");
         OpenAiStreamingChatModel openAiStreamingChatModel = OpenAiStreamingChatModel.builder()
                 .apiKey(apiKey)
                 .modelName("hunyuan-lite")
                 .baseUrl("https://api.hunyuan.cloud.tencent.com/v1")
                 .build();
         return openAiStreamingChatModel;
     }
 }
 ...
 // 使用
 @Resource
 private OpenAiStreamingChatModel model;

而现实情况是,有时需要记忆功能,有时需要嵌入功能。一个固定了的模型对象有点捉襟见肘,以上这种不是很推荐。

对于LangChain4j 来说,他们提供的AI Service 是一个简单模板接口。通过注解加入出参,完成了提示词模板,格式化输出等操作。

 public interface Assistant {
 ​
   @UserMessage("下面的描述正确吗?{{message}}")
   boolean isRight(String message);

流式输出以及接收

本篇主题不为前端,但简单介绍一下如何对流式问题做出响应。

前端处理

 const response = await fetch('/ai-answer?question=如何进行高效的学习');
 if (!response.ok) {
   throw new Error('网络请求错误');
 }
 const reader = response.body.getReader();
 const decoder = new TextDecoder('utf-8');
 let result = '';
 while (true) {
   const { done, value } = await reader.read();
   if (done) break;
   result += decoder.decode(value, { stream: true });
   // 动态更新页面内容
   document.getElementById('answer').innerHTML += decoder.decode(value, { stream: true }).replaceAll("data:","");
 }

后端的接口为“/ai-answer”,所以在第一行使用fetch发出请求。8-9 行为正式处理流。

后端Controller

 @GetMapping(value = "/ai-answer", produces = MediaType.TEXT_EVENT_STREAM_VALUE)
 public Flux<String> streamAnswers(String question) {
     return getStreamingAnswer(question);
 }
 ​
 Flux<String> getStreamingAnswer(String question) {
     String apiKey = System.getenv("hunyuan_key");
     OpenAiStreamingChatModel openAiStreamingChatModel = OpenAiStreamingChatModel.builder()
             .apiKey(apiKey)
             .modelName("hunyuan-lite")
             .baseUrl("https://api.hunyuan.cloud.tencent.com/v1")
             .build();
     Assistant assistant = AiServices.create(Assistant.class, openAiStreamingChatModel);
 ​
     return assistant.chat(question);
 }

演示效果

展示.gif