LangChain4j 快速入门:搭建第一个 AI 小助手

130 阅读6分钟

1、langchain4j分为高阶API和低阶API

  • 低阶API:按照我的理解,就是大部分功能封装好了(调用大模型等操作),但是一些配置信息,还是需要自己定制,配置
  • 高阶API:按照我的理解,就是所有功能基本都封装好了类似mybatis-plus,减少重复造轮子

低阶API、需要引入的maven坐标

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

高阶API,需要引入的maven坐标

<dependency>
    <groupId>dev.langchain4j</groupId>
    <artifactId>langchain4j</artifactId>
    <version>1.0.0-beta3</version>
</dependency>

也可以引入bom物料清单,进行版本控制

<dependencyManagement>
    <dependencies>
        <dependency>
            <groupId>dev.langchain4j</groupId>
            <artifactId>langchain4j-bom</artifactId>
            <version>1.0.0-beta3</version>
            <type>pom</type>
            <scope>import</scope>
        </dependency>
    </dependencies>
</dependencyManagement>

2、低阶API调用方式

  • 首先配置聊天模型,这里我用的是阿里的百炼
  • System.getenv("aliAi-key"):把阿里的百炼KEY,配置到系统环境变量,防止泄漏
@Bean
public ChatModel chatModel() {
    return OpenAiChatModel.builder()
            .apiKey(System.getenv("aliAi-key"))
            .modelName("qwen-plus")
            .logRequests(true)
            .logResponses(true)
            .baseUrl("https://dashscope.aliyuncs.com/compatible-mode/v1")
            .build();
}
  • 通过模型对象,直接调用
 @Autowired
 private ChatModel chatModel;
 
@RequestMapping(value = "/prompt/chat3")
public String chat3(String prompt) throws IOException {
    PromptTemplate promptTemplate = PromptTemplate.from("根据中国{{legal}}法律,解答以下问题:{{question}}");
    Prompt apply = promptTemplate.apply(Map.of("legal", "知识产权", "question", "什么是知识产权?"));

    UserMessage userMessage = apply.toUserMessage();


    PromptTemplate promptTemplate2 = PromptTemplate.from("你是一个专业的法律助手,请根据用户输入的问题会打法律相关信息");
    //填写对应的模板参数
    Prompt apply2 = promptTemplate2.apply("");
    SystemMessage systemMessage = apply2.toSystemMessage();
    //如果不需要模板,也可以直接创建
    SystemMessage systemMessage1 = new SystemMessage("你是一个专业的法律助手,请根据用户输入的问题会打法律相关信息");
    ChatResponse chatResponse = chatModel.chat(systemMessage,userMessage);
    return chatResponse.aiMessage().text();
}

3、高阶API调用

  • 定义接口
  • 这里的UserMessage、SystemMessage注解,就是提示词,告诉大模型这个助手的基本信息
  • SystemMessage:系统提示词,一般是我们自己定义,最好不要让用户可以自定义,不然就会出现“sql注入”,用户可以改变这个系统提示词的的含义,大模型就会理解错误
  • UserMessage:一般就是用户自己输入的问题了,也可以像我一样,通过模板的方式,能更好的让大模型理解
  • @V:这个注解,就是把函数的入参,与模板的占位符匹配,最后替换成用户的问题,发送给大模型
public interface ChatAssistant {


    @SystemMessage("你是一位专业的中国法律顾问,只回答与中国有关的法律问题。输出限制:对于其他领域的问题禁止回答,直接返回‘抱歉,我只能回答中国法律相关问题。’")
    @UserMessage("请回答以下法律问题:{{question}},字数控制在{{length}}以内,以{{format}}格式输出")
    String chat(@V("question") String question, @V("length") int length, @V("format") String format); // userMessage 包含 "{{country}}" 模板变量

    String chat2(LawPrompt lawPrompt); // userMessage 包含 "{{country}}" 模板变量
}
  • 添加接口和模型直接关联的配置
  • langchain4j,会自动生成一个代理对象,实现接口的所有方式
@Bean("chat")
public ChatAssistant chatAssistant(ChatModel chatModel) {
    return AiServices.create(ChatAssistant.class, chatModel);
}
  • 实现调用
@RequestMapping(value = "/prompt/chat")
public String chatImage(String prompt) throws IOException {
    log.info("prompt1: {}", prompt);
    String chat = chatAssistant.chat("什么是知识产权?", 100, "md");
    String chat2 = chatAssistant.chat("什么是java?", 100, "md");
    String chat3 = chatAssistant.chat("介绍一下水果西瓜和芒果", 100, "md");
    String chat4 = chatAssistant.chat("飞机发动机原理", 100, "md");

    return "answer01:" + chat + "\nanswer2" + chat2 + "\nanswer3" + chat3 + "\nanswer4" + chat4;
}

调用日志截图

  • langchain4j会把系统消息,用户消息打包成不同的角色参数发送给大模型

image.png

  • 更进一步:@AiService 方式实现:与spring-boot集成
  • 引入maven坐标
<dependency>
    <groupId>dev.langchain4j</groupId>
    <artifactId>langchain4j-spring-boot-starter</artifactId>
    <version>1.0.0-beta3</version>
</dependency>
  • 配置文件配置参数
langchain4j:
  open-ai:
    chat-model:
      api-key: ${aliAi-key}
      base-url: https://dashscope.aliyuncs.com/compatible-mode/v1
      model-name: qwen-plus
  • 直接使用:无需过多配置:springboot-starter简直是好东西,减少了一堆配置
@AiService
interface Assistant {

    @SystemMessage("You are a polite assistant")
    String chat(String userMessage);
}

@RestController
@Slf4j
public class LangChain4JBootController {
    @Autowired
    private ChatAssistant chatAssistant;

    @RequestMapping("/lc4f/boot/advanced")
    public String hello(@RequestParam(value = "question", defaultValue = "你是谁") String question) {
        String result = chatAssistant.chat(question);
        log.info("result:{}", result);
        return result;
    }
}

4、想加上记忆功能怎么加?

LangChain4j提供了2种开箱即用的记忆功能

  • MessageWindowChatMemory:保留最近的N条消息
  • TokenWindowChatMemory:保留最近的N个令牌

定义接口,@MemoryId int memoryId,就是用户的ID,为每个用户提供记忆功能

public interface ChatMemoryAssistant {
    String chatWithMemory(@MemoryId int memoryId, @UserMessage String userMessage);
}

配置如下

  • 通过chatMemoryProvider设置记忆模式,其中TokenWindowChatMemory的记忆模式,需要设置一个Tokenizer来计算每个ChatMessage中的令牌数,用来制定淘汰哪些数据。
@Bean("chatMemoryWindowChatMemory")
public ChatMemoryAssistant chatMemoryWindowChatMemory(ChatModel chatModel) {
    return AiServices
            .builder(ChatMemoryAssistant.class)
            .chatModel(chatModel)
            .chatMemoryProvider(memoryId -> MessageWindowChatMemory.withMaxMessages(10))
            .build();
}

@Bean("chatMemoryTokenWindowChatMemory")
public ChatMemoryAssistant chatMemoryTokenWindowChatMemory(ChatModel chatModel) {
    return AiServices
            .builder(ChatMemoryAssistant.class)
            .chatModel(chatModel)
            .chatMemoryProvider(memoryId -> TokenWindowChatMemory.withMaxTokens(1000,new OpenAiTokenCountEstimator("gpt-4")))
            .build();
}

调用方式

  • 这样,大模型即记住了你的历史聊天记录
@RequestMapping(value = "/memory/chat3")
public String chatMemoryTokenWindowChatMemory(String prompt) throws IOException {
    log.info("prompt3: {}", prompt);

    String chat = chatMemoryTokenWindowChatMemory.chatWithMemory(1, "你好,我叫java");
    String s = chatMemoryTokenWindowChatMemory.chatWithMemory(1, "我叫什么啊?");

    String chat2 = chatMemoryTokenWindowChatMemory.chatWithMemory(2, "你好,我叫Cpp");
    String s2 = chatMemoryTokenWindowChatMemory.chatWithMemory(2, "我叫什么啊?");
    return "answer03:" + chat + "\nanswer4" + s + "\nanswer5" + chat2 + "\nanswer6" + s2;
}

让我们来看看,大模型是怎么记忆的?

  • 可以看到,是把之前发送的消息,通通放在一起,再一次发送给大模型,大模型就知道之前用户发送了什么消息,就是这么纯粹,我把所有消息都告诉你,你不就记住了!!!哈哈哈
  • 但是这样就会有个后果:如果你配置的withMaxMessages数值过大,那么用户的消息在一直增加,到最后发送给大模型的消息就会是一个超长的文本,这样消耗的token太多,以及响应肯定会慢很多
  • 我想到的解决办法是,把最近的几次用户的消息让大模型总结一下,“压缩用户的消息”,这样应该能解决部分问题

image.png

5、怎么持久化用户的记忆呢?

通过观察源码发现:默认使用的事内存存储,系统只要关机重启,用户的历史记忆就消失了

image.png

image.png

通过redis持久化存储 添加上chatMemoryStore存储功能的实现

  • 在配置聊天接口代理实现的时候,添加存储实现类
@Bean
public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory redisConnectionFactory) {
    RedisTemplate<String, Object> template = new RedisTemplate<>();
    template.setConnectionFactory(redisConnectionFactory);
    template.setKeySerializer(new StringRedisSerializer());
    template.setValueSerializer(new GenericJackson2JsonRedisSerializer());
    template.setHashKeySerializer(new StringRedisSerializer());
    template.setHashValueSerializer(new GenericJackson2JsonRedisSerializer());
    //让前面配置生效
    template.afterPropertiesSet();
    return template;
}
  • 实现redis存储:主要实现ChatMemoryStore类:提供消息的CRUD功能
    • getMessages:获取消息
    • updateMessages:更新消息
    • deleteMessages:删除消息
package paperfly.persistence;

import dev.langchain4j.data.message.ChatMessage;
import dev.langchain4j.data.message.ChatMessageDeserializer;
import dev.langchain4j.data.message.ChatMessageSerializer;
import dev.langchain4j.store.memory.chat.ChatMemoryStore;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Component;

import java.util.List;


@Component
public class RedisChatMemoryStore implements ChatMemoryStore {

    @Autowired
    private RedisTemplate<String, String> redisTemplate;
    private static final String KEY_PREFIX = "chat_memory:";

    @Override
    public List<ChatMessage> getMessages(Object memoryId) {
        String retValue = redisTemplate.opsForValue().get(KEY_PREFIX + memoryId);
        return ChatMessageDeserializer.messagesFromJson(retValue);
    }

    @Override
    public void updateMessages(Object memoryId, List<ChatMessage> messages) {
        String retValue = ChatMessageSerializer.messagesToJson(messages);
        redisTemplate.opsForValue().set(KEY_PREFIX + memoryId, retValue);
    }

    @Override
    public void deleteMessages(Object memoryId) {
        redisTemplate.delete(KEY_PREFIX + memoryId);
    }
}
@Bean("chatPersistenceWindowChatMemory")
public ChatPersistenceAssistant chatPersistenceWindowChatMemory(ChatModel chatModel, RedisChatMemoryStore store) {
    return AiServices
            .builder(ChatPersistenceAssistant.class)
            .chatModel(chatModel)
            .chatMemoryProvider(memoryId -> {
                return MessageWindowChatMemory
                        .builder()
                        .id(memoryId)
                        .maxMessages(10)
                        .chatMemoryStore(store)
                        .build();
            })
            .build();
}