Langchain4j框架快速入门

写在最前面:langchain4j为我们提供了两种服务:

  • 基本服务:向LLM提问,LLM回答;
  • 高级服务:保存聊天信息,集成小工具,RAG。

本文主要讲的是笔者个人学习时的记录:基本服务+持久化聊天信息,后续集成小工具与RAG会另起新篇。

注意,想要使用这个框架必须要将你的JDK版本更新为17+,springAi也是同理。这是最基本的硬性要求

 

1.基础使用:springboot快速集成简单大模型

导入依赖:注意,springboot最好为3.2.0+,至少3.0.2是会报错的

<dependency>
<groupId>dev.langchain4j</groupId>
<artifactId>langchain4j-open-ai-spring-boot-starter</artifactId>
<version>1.0.1-beta6</version>
</dependency>
<dependency>
<groupId>dev.langchain4j</groupId>
<artifactId>langchain4j-spring-boot-starter</artifactId>
<version>1.0.1-beta6</version>
</dependency>

通过yaml配置大模型相关信息

langchain4j:
  open-ai:
    chat-model:
      api-key: sk-xxxxx
      base-url: https://dashscope.aliyuncs.com/compatible-mode/v1
      model-name: qwen-plus

完成上述两部配置后就可以在controller直接注入一个简单的model并使用了

    @Autowired
    OpenAiChatModel openAiChatModel;
    @RequestMapping(value = "/ask",produces = "text/html;charset=UTF-8")
    public String ask(@RequestParam(value = "message") String message){
        return openAiChatModel.chat(message);
    }

 再进阶一点,我们通过@AiService注解,实现service层。在springboot中,自动配置会根据注解创建bean。无需调用AiServices.create(),在需要的地方注入即可正常使用该方法。

@AiService
public interface Service {
    public String chat(String prompt);
}

aiService是如何工作的?该接口通过反射生成一个代理对象,我们输入的是str,aiService会自动将其转换为userMessage并调用chatLanguageModel。ai输出内容后,再调用chatLanguageModel返回aiMessage,将其转换为str并返给用户。

2.流式调用

大模型免不了流式调用,langchain4j也给了相关的解决方案:

导入依赖:

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

重写yaml,将上述的chat-model修改为streaming-chat-model即可

这时,controller层简单调用的时候也要修改为使用@AiService注解的service

@AiService
public interface Service {
    public Flux<String> chat(String prompt);
}

将controller层返回值修改为Flux<String>即可正常使用流式调用效果

3.角色设定和后端实现效果

发送的请求:

 流式返回的内容后端效果图:

  ServerSentEvent { event = null, data = "{"choices":[{"finish_reason":null,"logprobs":null,"delta":{"content":"当然"},"index":0}],"object":"chat.completion.chunk","usage":null,"created":1750758228,"system_fingerprint":null,"model":"qwen-plus","id":"chatcmpl-8ea45301-7fed-95cf-92a0-33f2b72ea838"}" }

 

角色设定可以在chat方法上使用@SystemMessage注解,来给你的大模型设定一个预先的提示词。有两种设定方法:一种是将res下的静态文本导入,适用于较长提示词,一种是直接写,适用于较短的提示词

@SystemMessage(fromResource = "system.txt")
@SystemMessage("你是一个C语言大佬,你不屑于写java,更不屑于写springboot")

注:chatMessage目前共有四种类型:userMessage、aiMessage、systemMessage、customMessage(不常用,且仅ollama支持)。

  • user/aiMessage:即用户与ai发出的消息。使用@userMessage注解即提问会使用一个包含变量(即用户输入内容,固定为{{it}})的模版,
  • systemMessage:这个一般由开发人员设定,LLM一般比其他消息更关注systemMessage,因此这一内容最好不要交给最终用户自定义。在存储与使用方面,systemMessage也有不同的处理待遇:一旦添加,在对话中systemMessage总会保留(不会被淘汰策略淘汰)并且唯一,

效果展示,之后的对话中系统中的role除了user和assitant还会多一个system,即系统设定:

 AI关于这个问题的回答:

**C ** Java Spring Boot + MB --- ## C Spring Boot 3 Spring Boot Java ### Spring Boot 3 Spring Boot 2 | | Spring Boot 2.x | Spring Boot 3.x | |------|------------------|------------------| | JDK | JDK 17 | 使 JDK 17+ JDK 19/20 | | Jakarta EE 9+ | 使 `javax.*` | 使 `jakarta.*` | | GraalVM||NativeImage||||||JakartaEE9+||Servlet6.0JPA3.1||||Hibernate6SpringFramework6||Web|Tomcat9Jetty9|Tomcat10+Jetty11+|---##-JVMSpringBoot3-**GraalVMNativeImage**-****-MBvsMB-Serverless>JavaC西---##C||||------|----------||SpringBoot3|Java||SpringBoot3|GraalVMNativeImage尿||SpringBoot3|||SpringBoot3C/C++||---##>**SpringBoot3**C/C++Rust---CTCPSpringBoot

 

4.会话记忆

在官方文本中,记忆和历史是不同的:历史是指保证所有消息完整无缺;记忆是指根据算法修改历史(淘汰、总结、过滤与注入一些消息)。

大模型调用也需要对应的会话记忆:

  • 能够顺着上一次问题继续提问,例如:问题1:你觉得西湖醋鱼好吃吗?问题2:那酸菜鱼呢?如果没有会话记忆,那么大模型很难理解第二个问题是什么意思
  • 能够为不同用户的不同会话提供隔离的记忆。
  • 能够在后端重启后仍然提供相应的记忆。

因此我们需要一个ChatMemory来存储记忆,langchain4j中提供了对应的接口,并提供了两种解决方案:

在MessageWindowChatMemory的建造者模式中,提供了id、maxMessages、store三个功能。他们分别解决了:会话id问题、会话长度问题、会话存储信息问题。

官方对两种chatMemory提供了解释:两者都是作为滑动窗口运行

messageWindow是保留最近N条消息,淘汰旧消息,但每条消息包含的令牌数不一样,因此他适合用于原型设计阶段使用

tokenWindow是保留最近N个令牌,根据需要淘汰旧消息,他需要一个tokenizer计算每个chatMessage的令牌数,设计相对更复杂,但是与LLM处理更贴合

第一个问题的解决方案:

在Ioc容器内注入一个chatMemory,将msg存入其中,保证记忆

  @Bean
    ChatMemory chatMemory(){
        return MessageWindowChatMemory.builder().maxMessages(20).build();
    }

(这里注入后@AiService方法会自动将chatMemory装配到对应的接口中,保证beanName和接口的大小写区别以及其他正确,无需再次手动处理)

这里看一下创建的源码:

public static class Builder {
        private Object id = "default";
        private Integer maxMessages;
        private ChatMemoryStore store;

        public Builder() {
        }

        public Builder id(Object id) {
            this.id = id;
            return this;
        }

        public Builder maxMessages(Integer maxMessages) {
            this.maxMessages = maxMessages;
            return this;
        }

        public Builder chatMemoryStore(ChatMemoryStore store) {
            this.store = store;
            return this;
        }

        private ChatMemoryStore store() {
            return (ChatMemoryStore)(this.store != null ? this.store : new SingleSlotChatMemoryStore(this.id));
        }

        public MessageWindowChatMemory build() {
            return new MessageWindowChatMemory(this);
        }
    }

builder中一共提供几个参数:id,默认情况可以设置为default;maxMessage无默认值;store,如果没提供,默认情况下使用SingleSlotMemoryStore。

第二个问题的解决方案:

为每个用户的每次创建时提供一个独立的ID,每次提问传递的参数除了msg,还有一个独特的ID。这里我使用的ID设置方法是创建时的时间戳+userId

@AiService
public interface Service {
    public Flux<String> chat(@MemoryId String memodryId,@UserMessage String prompt);
}

langchain中的@MemoryId会识别对应的Id,以及msg使用@UserMessage

此外,还需要在Ioc中注入新的bean:ChatMemoryProvider

   @Bean
    ChatMemoryProvider chatMemoryProvider(){
        ChatMemoryProvider chatMemoryProvider = new ChatMemoryProvider() {
            @Override
            public ChatMemory get(Object memoryId) {
                return MessageWindowChatMemory.builder()
                        .id(memoryId)
                        .maxMessages(20)
                        .build();
            }
        };
        return chatMemoryProvider;
    }

(这里注入后@AiService也会自动识别该bean,无需手动处理)

这里我又读了读源码:

 抛开我手写的redisChatMemoryStore不谈,系统默认提供了InMemory和SingleSlot两种方法,那么他们有什么区别呢?

InMemory提供的存储方法是基于ConcurrentHashMap的;而SingleSlot是ArrayList

public class InMemoryChatMemoryStore implements ChatMemoryStore {
    private final Map<Object, List<ChatMessage>> messagesByMemoryId = new ConcurrentHashMap();
class SingleSlotChatMemoryStore implements ChatMemoryStore {
    private List<ChatMessage> messages = new ArrayList();
    private final Object memoryId;

这两种方法的情况是不同的:singleSlot只支持单个memory的存储,因此如果使用这个memoryStore,我们必须建立多个memory,也就是使用了上述的provider;

而InMemory支持了多个memory的存储,所以说如果使用这个方法,我们是不需要使用provider的。通过源码中的clear方法我们也可以简单的看出来这个问题:

SingleSlot:全部清除

    public void deleteMessages(Object memoryId) {
        this.checkMemoryId(memoryId);
        this.messages = new ArrayList();
    }

InMemory:清除对应的ID

 public void deleteMessages(Object memoryId) {
        this.messagesByMemoryId.remove(memoryId);
    }

第三个问题的解决方案:

其实读完上述源码,我们可以得出结论:平时使用的时候还得是用InMemory,但是其实这个InMemory有一个问题,一旦后端重启,数据就都没了。因此我们需要持久化存储。

例子中,我们使用redis作为持久化存储的数据库

继承ChatMemoryStore接口重写三个接口即可,这里langchain4j还提供了一些小方法:

        List<ChatMessage> list = ChatMessageDeserializer.messagesFromJson(json);

        String json = ChatMessageSerializer.messagesToJson(list);

这两个方法可以快速实现chatMessage与json的切换,具体方法如下:

@Repository
public class RedisChatMemoryStore implements ChatMemoryStore {
    @Autowired
    StringRedisTemplate redisTemplate;
    @Override
    public List<ChatMessage> getMessages(Object memoryId) {
        String json = redisTemplate.opsForValue().get(memoryId.toString());
        List<ChatMessage> list = ChatMessageDeserializer.messagesFromJson(json);
        return list;
    }

    @Override
    public void updateMessages(Object memoryId, List<ChatMessage> list) {
        String json = ChatMessageSerializer.messagesToJson(list);
        redisTemplate.opsForValue().set(memoryId.toString(),json, Duration.ofDays(1));
    }

    @Override
    public void deleteMessages(Object memoryId) {
        redisTemplate.delete(memoryId.toString());
    }
}

5.结构化输出:

langchain4j支持的返回类型不止有String类型的非结构化文本,也提供几种其他的返回方式:这里使用官网提供的例子作为展示

1.boolean

interface SentimentAnalyzer {
    @UserMessage("{{it}}是表达的积极情绪吗?")
    boolean isPositive(String text);
}
SentimentAnalyzer sentimentAnalyzer
= AiServices.create(SentimentAnalyzer.class, model); boolean positive = sentimentAnalyzer.isPositive("哇哦,真是太棒啦!"); // true

2.enum

enum Priority {
    CRITICAL, HIGH, LOW
}
interface PriorityAnalyzer {   
    @UserMessage("分析一下下列问题的优先级: {{it}}")
    Priority analyzePriority(String issueDescription);
}
PriorityAnalyzer priorityAnalyzer = AiServices.create(PriorityAnalyzer.class, model);
Priority priority = priorityAnalyzer.analyzePriority("主支付网关已关闭,客户无法进行交易。");
// CRITICAL

3.pojo

class Person {
    @Description("first name of a person") // 您可以添加可选描述,帮助 LLM 更好地理解
    String firstName;
    String lastName;
    LocalDate birthDate;
    Address address;
}

@Description("an address") // 您可以添加可选描述,帮助 LLM 更好地理解
class Address {
    String street;
    Integer streetNumber;
    String city;
}

interface PersonExtractor {
    @UserMessage("Extract information about a person from {{it}}")
    Person extractPersonFrom(String text);
}

PersonExtractor personExtractor = AiServices.create(PersonExtractor.class, model);

String text = """
            In 1968, amidst the fading echoes of Independence Day,
            a child named John arrived under the calm evening sky.
            This newborn, bearing the surname Doe, marked the start of a new journey.
            He was welcomed into the world at 345 Whispering Pines Avenue
            a quaint street nestled in the heart of Springfield
            an abode that echoed with the gentle hum of suburban dreams and aspirations.
            """;

Person person = personExtractor.extractPersonFrom(text);

System.out.println(person); // Person { firstName = "John", lastName = "Doe", birthDate = 1968-07-04, address = Address { ... } }

 

6.最后看一下@AiService注解

先看看源码,默认情况下,写入模式是自动的,其实内容无需我们处理,但是如果有时找不到对应的bean(大概率是你命名有问题)那就需要手动去写

@Service
@Target({ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
public @interface AiService {
    AiServiceWiringMode wiringMode() default AiServiceWiringMode.AUTOMATIC;

    String chatModel() default "";

    String streamingChatModel() default "";

    String chatMemory() default "";

    String chatMemoryProvider() default "";

    String contentRetriever() default "";

    String retrievalAugmentor() default "";

    String moderationModel() default "";

    String[] tools() default {};
}

举个手动写的例子,手动写的内容是在Ioc容器内可以找到的beanName

@AiService(
        wiringMode = AiServiceWiringMode.EXPLICIT,//手动装配
        chatModel = "openAiChatModel",//指定阻塞时使用的模型,这里是都是从IOC容器里取的,因此记得设置Bean
        streamingChatModel = "openAiStreamingChatModel",//指定流式使用时的模型
        chatMemoryProvider = "chatMemoryProvider"//配置会话记忆对象
)

 

posted @ 2025-06-24 17:54  天启A  阅读(273)  评论(0)    收藏  举报