当前位置: 首页 > news >正文

Java开发者の模型召唤术:LangChain4j咏唱指南(二)

Java开发者の模型召唤术:LangChain4j咏唱指南(二)


往期回顾:

Java开发者の模型召唤术:LangChain4j咏唱指南(一)_langchain4j 千问-CSDN博客


上期博客中简单的为大家介绍了langchain4j是什么、java 集成 langchain4j、集成阿里云百炼平台的各个模型以及本地安装的ollama模型的调用流程。

那么本期教程将会与大家分享常规的Spring-boot集成、流式输出、记忆对话、function-call、预设角色等更深层次的知识,码字不易,各位喜欢请一键三连~


1.springboot 项目集成langchain4j

1.1创建项目

不再过多介绍,重要的事情说三遍

! JDK选择17

! JDK选择17

! JDK选择17

本人spring-boot 版本使用的是3.1.5

1.2引入依赖

spring-boot-starter-web

spring-boot-starter-test

spring相关的基础依赖不在过多赘述

核心依赖包有以下两个:

<langchain4j.version>1.0.0-beta2</langchain4j.version>
<!--阿里云百炼平台qwen-boot-starter-->  
<dependency>
            <groupId>dev.langchain4j</groupId>
            <artifactId>langchain4j-community-dashscope-spring-boot-starter</artifactId>
            <version>${langchain4j.version}</version>
        </dependency>
<!--langchain4j基础依赖-->
        <dependency>
            <groupId>dev.langchain4j</groupId>
            <artifactId>langchain4j</artifactId>
            <version>${langchain4j.version}</version>
        </dependency>

同时引入依赖版本自动化管理

 <dependencyManagement>
        <dependencies>
            <dependency>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-dependencies</artifactId>
                <version>${spring-boot.version}</version>
                <type>pom</type>
                <scope>import</scope>
            </dependency>
            <!--        自动化langchain4j-community依赖版本管理-->
            <dependency>
                <groupId>dev.langchain4j</groupId>
                <artifactId>langchain4j-community-bom</artifactId>
                <version>${langchain4j.version}</version>
                <type>pom</type>
                <scope>import</scope>
            </dependency>

        </dependencies>
    </dependencyManagement>

1.3引入配置

配置文件 application.properties

server.port=8888
# langchain4j-qwen
langchain4j.community.dashscope.chat-model.api-key=sk-XXXXX
langchain4j.community.dashscope.chat-model.model-name=qwen-max


#sse qianwen
langchain4j.community.dashscope.streaming-chat-model.api-key=sk-XXXX
## qwq-32b  deepseek 都不支持function call(deepseek-v3)
langchain4j.community.dashscope.streaming-chat-model.model-name= qwen-max


1.4编写controller

1.4.1 最简单的例子

编写controller 进行简单的对话测试,那么先来个最简单的叭,代码示例

@RestController
@RequestMapping("/ai/simple")
public class LangChain4jController {

    @Resource
    private QwenChatModel qwenChatModel;


    @GetMapping("/doAsk")
    public String langChain4j(@RequestParam(defaultValue = "你是谁?") String question) {
        return qwenChatModel.chat(question);
    }
}

可以看到能够请求到qwen,得到返回内容

请添加图片描述

1.4.2 自定义注解实现预设角色

基础大模型是没有目的性的, 你聊什么给什么,但是如果我们开发的是一个星座智能助手, 我需要他以一个星座专家的角色跟我对话, 那么就需要进行一些预设角色的实现与设定。

各位小伙伴但凡是接触各大智能体API调用的时候,一般是使用promt进行与各大只能体平台进行交互,那么在交互过程中一般会通过promt指定模型角色,然后在调用过程中通过传入到SystemMessage中,搭配用户的问题传入userMessage中实现与模型有效的沟通。

langchain4j其实是有类似于SystemMessage的注解,标注模型是什么角色,通过**@SystemMessage**注解进行使用。但是此处的话,我们进行自定义注解的方式进行自定义这一行为

首先自定义一个注解SystemPrompt

/**
 * @version 1.0
 * @Author jerryLau
 * @Date 2025/3/24 8:45
 * @注释 自定义注解 用于指定系统提示词
 */

@Target({ElementType.METHOD, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
public @interface SystemPrompt {
    String systemPrompt() default "你是一个星座专家";

}

定义aiService接口,定义执行方式

/**
 * @version 1.0
 * @Author jerryLau
 * @Date 2025/3/24 8:41
 * @注释 aiService 接口
 */

public interface AiService {

    /***
     * 提问 ai 方法 同步方法
     * @param question
     * @return
     * @throws NoSuchMethodException
     */
    @SystemPrompt()
    ChatResponse doAsk(String question) throws NoSuchMethodException;
}

创建impl实现具体的调用方式

/**
 * @version 1.0
 * @Author jerryLau
 * @Date 2025/3/24 10:46
 * @注释
 */
@Service
public class AiServiceImpl implements AiService {

    @Resource
    private QwenChatModel qwenChatModel;

    /***
     * 提问 ai 方法 同步阻塞方法
     * @param question 问题
     * @return
     * @throws NoSuchMethodException
     */
    @Override
    public ChatResponse doAsk(String question) throws NoSuchMethodException {

        //反射获取注释中的内容
        Method doAsk = AiService.class.getMethod("doAsk", String.class);
        SystemPrompt annotation = doAsk.getAnnotation(SystemPrompt.class);
        if (annotation != null) {
            String s = annotation.systemPrompt();
            List<ChatMessage> messages = new ArrayList<>();
            //传入SystemMessage和userMessage
            messages.add(SystemMessage.from(s));
            messages.add(UserMessage.from(question));
            ChatResponse chat = qwenChatModel.chat(messages);
            //返回回答结果
            return chat;
        } else {
            throw new NoSuchMethodException("no annotation");
        }
    }
}

这一套下来,可以在controller中添加请求方法

 @GetMapping("/doAsk2")
    public String langChain4j2(@RequestParam(defaultValue = "你是谁?") String question) throws NoSuchMethodException {
        System.out.println("进入doAsk2方法");
        ChatResponse aiMessageResponse = aiService.doAsk(question);
        System.out.println(aiMessageResponse);
        return aiMessageResponse.aiMessage().text();

    }

当我们再次请求“你是谁”的时候,会发现智能体的回答不同于1.3.1中简单的回答,已经发生了一些变化。

请添加图片描述

1.4.3 非阻塞式调用

好多小伙伴可能不大了解什么是阻塞什么是非阻塞,那么我们先观察一下下面的两个gif图对比一下

请添加图片描述

请添加图片描述

可以比较直观的看到第一个gif图在请求的时候,响应过程是 -> -> -> boom 一下子蹦出整个响应内容,而第二个过程感觉像是这样的 ->A…->B… ->C…

哈哈哈 上面是比较通俗的感觉,那么什么是阻塞调用什么是非阻塞调用呢?

其实在各个智能体API调用时,一般都会分为阻塞和非阻塞,小伙伴们应该都了解,在智能体调用时,有个叫做token的东西,简单理解就类似于字符这样,通常一句话可能被分开成为好几个部分,比如“你好,我是千问,我是你的ai智慧助手”,会在智能体回复你之前被划分为【你好;我;是;千问;我是;你的;ai ; 智慧助手;】每次智能体响应会相应其中的一个词语。阻塞式响应的话,智能体回答我们的问题时,一般是收集所有的片段,等待片段收集完成后,归接在一起返回相应给我们。而非阻塞式响应的话,就是实时的返回智能体的响应,不做最后的等待归集。因此就会出现上面两个gif中示例中感官上的区别。

那么应该怎实现sse即流式实时响应呢?

首先要实现sse对于spring-boot项目而言,当然是先引入依赖了,先引入web flux的依赖,支持流式响应:

 <!--        springboot flux-->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-webflux</artifactId>
        </dependency>

在之前aiService中添加流式请求的方法

    /***
     * 提问 ai 方法 sse方法
     * @param question
     * @return
     * @throws NoSuchMethodException
     */

    Flux<String> doAskSSE(String question) ;

impl实现具体的流式响应的方法

 /***
     * 提问 ai 方法 sse方法
     * @param question
     * @return
     * @throws NoSuchMethodException
     */
    @Override
    public Flux<String> doAskSSE(String question)  {

        Flux<String> flux = Flux.create(emitter -> {

            qwenStreamingChatModel.chat(question, new StreamingChatResponseHandler() {
                @Override
                public void onPartialResponse(String partialResponse) {
                    emitter.next(partialResponse);
                }

                @Override
                public void onCompleteResponse(ChatResponse completeResponse) {
                    emitter.complete();
                }

                @Override
                public void onError(Throwable error) {
                    emitter.error(error);
                }
            });


        });
        return flux;
    }

至此大功告成,运行后在浏览器调用,效果应该和上述第二个gif效果一致。

2.记忆对话

简单的springboot集成、阻塞式调用及流式调用已经大功告成。不知道小伙伴们有没有发现下面这个问题:

问题分析:

以上述阻塞案例为例子,我将代码搬过来,加了调用次数,换了接口路径为:/ai/memoryChart/doAsk

我现在有以下调用流程:

1-调用接口告诉qwen 我是jerry

2-调用qwen 问我是谁?

第一次调用:

请添加图片描述

第二次调用:

请添加图片描述

很显然,qwen不知道我的名字,但是我明明在第一次对话中告诉过他了呀;可见对于qwen来说,他暂时是没有记忆的,我们要进行一点改造使得他知道我是谁。

那么改造的方式呢有两种,第一种是将第一次的上下文再通过一定的方式再喂给它,具体的形式如下

2.1手动获取上下文并传递给模型

我们创建一个新的方法,书写如下代码,通过写死用户消息及系统打印的方式,打印模型给到我们的返回值,将第一次的返回消息及问题一起放入第二次的请求参数中,手动喂给qwen上下文:

 @GetMapping("/doAskWithMemory1")
    public String langChain4j_memory() {
        UserMessage userMessage = new UserMessage("halo 我是 jerry");
        ChatResponse chat1Resp = qwenChatModel.chat(userMessage);
        AiMessage aiMessage = chat1Resp.aiMessage();

        System.out.println("第一次调用返回:" + aiMessage.text());

        System.out.println("--------------------");

        String question2 = "我是谁?";

        ChatResponse chat2Resp = qwenChatModel.chat(userMessage, aiMessage, UserMessage.from(question2));

        System.out.println("第二次调用返回:" + chat2Resp.aiMessage().text());


        return "langChain4j_memory方法结束";
    }

我们看看执行结果:

由于我们直接进行系统打印,请求返回将不在重要,直接看控制台out输出即可

请添加图片描述

此时发现,通过手动给qwen模型喂上下文的方式,使得他拥有了一些记忆,也知道了我是谁。但是大家不妨想一想,如果有一些复杂的逻辑结构,难道要将每一次的问题结果做收集,再喂给大模型嘛?这样未免太麻烦了叭。

所以这个方式,可以但是不推荐。⚠️

2.2Chat 记忆缓存组件✅

记忆缓存是聊天系统中的一个重要组件,用于存储和管理对话的上下文信息。它的主要作用是让AI助手能够”记住”之前的对话内容,从而提供连贯和个性化的回复。

ChatMemory是一个用于管理聊天上下文的组件,它可以解决以下问题:

  • 防止上下文超出大模型的token限制
  • 隔离不同用户的上下文信息
  • 简化ChatMessage的管理

其核心可以理解为以下几点:

  • 容器管理: 管理ChatMessage的生命周期
  • 淘汰机制: 防止存储过多消息
  • 持久化: 避免聊天上下文丢失

摘自[Chat 记忆缓存 - 零基础入门Java AI](https://javaai.pig4cloud.com/docs/07Chat 记忆)

对于ChatMemory而言,Langchain4j提供了两种实现方式,每一种呢都有自己的特点与应用方式

1.MessageWindowChatMemory

开箱即用,简单实现

MessageWindowChatMemory是一个基于消息数量的简单实现。维护一个固定大小的消息窗口,保存最近的N条交互信息,确保系统不会处理过长的上下文,同时保留关键的历史信息。如果窗口大小是5,那么保存最近的5条消息,新的消息进来后,最旧的那条会被丢掉。这样做的好处是防止内存或处理负担过重,尤其是在长时间对话中,避免累积太多数据

特点:

  • 易于理解和实现
  • 基于消息数量进行管理
  • 适用于不需要精确token控制的场景

那么对于性能优化的小伙伴来说,可能得注意以下几点

实践:

  • 评估窗口大小:通过A/B测试选择最佳N值,例如监控任务完成率与响应时间。
  • 异常处理:窗口大小为0时禁用历史记录,或抛出配置错误。
  • 性能监控:在高并发场景下,监测内存使用与消息处理延迟,优化数据结构(如使用循环缓冲区)
2.TokenWindowChatMemory

TokenWindowChatMemory以token数量为限制的窗口。也就是说,它不会限制消息的数量,而是限制总token数,动态维护上下文窗口,确保总Token数不超过预设阈值(如LLM模型的最大输入限制),这样能更精确地控制内存和模型输入的长度。

特点:

  1. Token容量限制
    • 设定总Token上限(如4096 Token),新消息加入时,若总Token数超过阈值,按时间顺序淘汰最早的消息,直至满足限制。
    • 相比固定消息数量的窗口,更精准适配模型输入长度限制(如GPT-4的上下文窗口)。
  2. 动态调整
    • 消息可能因Token长度差异被部分保留(如截断长消息)或完全移除,需权衡完整性与容量。

实践:

  • 大语言模型(LLM)集成:适配模型的固定Token输入限制(如GPT-3的2048 Token)。
  • 变长消息处理:消息长度差异大时(如用户发送长文本或短指令),避免固定条数窗口导致的Token不可控。
  • 成本敏感场景:按Token计费的API调用(如OpenAI),精确控制输入长度以降低成本。

2.3记忆实现-共享记忆

那么在此我们简单的实现一下基于MessageWindowChatMemory的记忆存储,让我们的大模型拥有记忆。

那么官方提供了如下的集成代码,首先呢声明一个配置类

/**
 * @version 1.0
 * @Author jerryLau
 * @Date 2025/3/26 13:33
 * @注释 ai配置类
 */
@Configuration
public class AIConf {
    
}

然后再配置类中声明一个ChatAssistant接口

/***
     * 聊天助手接口
     */
    public interface ChatAssistant {

        /***
         * 普通聊天 非隔离上下文
         * @param question
         * @return
         */
        String chat(String question);

        /***
         * 流式输出 非隔离上下文
         * @param question
         * @return
         */
        TokenStream streamChat(String question);
    }

然后通过@Bean注解,将这个ChatAssitant当成一个SpringBean

    /***
     * 注入聊天服务  非隔离上下文
     * @param chatLanguageModel
     * @param streamingChatLanguageModel
     * @return
     */
    @Bean
    public ChatAssistant chatAssistant(ChatLanguageModel chatLanguageModel,
                                       StreamingChatLanguageModel streamingChatLanguageModel,
                                       ToolService toolService) {

        //使用简单的ChatMemory  - MessageWindowChatMemory来保存聊天记录
        MessageWindowChatMemory messageWindowChatMemory = MessageWindowChatMemory.builder()
                .maxMessages(10).build(); //最多保存10条记录

        ChatAssistant build = AiServices.builder(ChatAssistant.class)
                .chatLanguageModel(chatLanguageModel)
                .streamingChatLanguageModel(streamingChatLanguageModel)
                .chatMemory(messageWindowChatMemory)
                .build();

        return build;
    }

那具体的实现逻辑解释如下:

1-当Spring Boot应用启动时,会加载配置类AIConf,并尝试创建Bean。被注释的@Bean方法会接收ChatLanguageModel和StreamingChatLanguageModel作为参数,这两个是与AI模型交互的组件,像前面用到的qwenChatModel,qwenStreamingChatModel分别实现了这两个接口。

2-创建了一个MessageWindowChatMemory实例,设置最大保存10条消息

3-通过LangChain4j 的 AiServices 类负责为接口创建动态代理。核心逻辑在 build(),这里使用 JDK 动态代理 (Proxy.newProxyInstance),所有接口方法的调用会被路由到自定义的 InvocationHandler,当Controller中调用 chatAssistant.chat(question) 时,动态代理会触发 InvocationHandler.invoke(),其中会判断当前传入的是什么模型以及是否存在记忆存储,在底层拼装记忆,并存储新的记忆后,调用传入模型的chat方法进行请求得到结果。

那么我们就可以定义controller进行使用定义的ChatAssistant

 @Resource
    private AIConf.ChatAssistant chatAssistant;

    /***
     * 记忆调用 2
     * 使用langchain4j 自带的记忆化工具进行上下文记忆
     * @param
     * @return
     */
    @GetMapping("/doAskWithMemory2")
    public String langChain4j_memory2(@RequestParam(defaultValue = "halo 我是jerry") String question) {
        String chat = chatAssistant.chat(question);
        return chat;
    }

请添加图片描述

请添加图片描述

可以看到,qwen现在知道我是谁了

但是伴随着上述方式的实现,又出现了一个问题,小伙伴们可以再想想看,我上面两次都是通过idea插件apifox实现的接口调用,qwen知道我是谁了,那么如果我用浏览器调用这个接口,模拟一个新用户进行使用,发送请求询问“我是谁”,有会出现什么样的结果呢

在这里插入图片描述

通过实践我们发现,即使是一个新用户,我询问qwen我是谁,qwen仍然知道我是之前的jerry,这显然是不大合理的。那么对于上述的记忆的代码实现而言,虽然实现了记忆存储但是不同的用户之间没有隔离,怎么实现不同用户之间的记忆隔离呢,继续往下看~

2.4记忆实现-非共享记忆

在进行记忆存储时,会有一个类似于数据库主键的id,叫做memoryId,在进行记忆实现时,对于不同德用户而言,带一个唯一的id,在记忆存储时,根据这个id作为唯一标识进行存储,这样一来是不是实现了不同用户的记忆分离。那我们来实现一下

chatAssistant 添加一个新的方法,参数包括一个memoryId 和 一个用户消息

 /***
         * 普通聊天 隔离上下文 通过id 进行上下文隔离 达到不通用户聊天互不影响的效果
         * @param id 上下文id
         * @param question 用户问题
         * @return
         */
        String chat(@MemoryId int id, @UserMessage String question);

注入的bean需要做id的配置

 /***
     * 注入聊天服务  隔离上下文
     * @param chatLanguageModel
     * @param streamingChatLanguageModel
     * @return
     */
    @Bean
    public ChatAssistant chatAssistant(ChatLanguageModel chatLanguageModel, StreamingChatLanguageModel streamingChatLanguageModel) {

        //使用简单的ChatMemory  - MessageWindowChatMemory来保存聊天记录
//        MessageWindowChatMemory.builder().maxMessages(10).id(memoryId).build()
        ChatAssistant build = AiServices.builder(ChatAssistant.class)
                .chatLanguageModel(chatLanguageModel)
                .streamingChatLanguageModel(streamingChatLanguageModel)
                .chatMemoryProvider(memoryId -> MessageWindowChatMemory.builder().maxMessages(10).id(memoryId).build())
                .build();

        return build;
    }

通过

 .chatMemoryProvider(memoryId -> MessageWindowChatMemory.builder().maxMessages(10).id(memoryId).build())

实现自定义memoryId作为用户记忆消息id,实现用户消息记忆的分离存储,接下来编写controller层代码进行实现调用

 /***
     * 记忆调用 2
     * 使用langchain4j 自带的记忆化工具进行上下文记忆
     * @param
     * @return
     */
    @GetMapping("/doAskWithMemory2")
    public String langChain4j_memory2(@RequestParam(defaultValue = "halo 我是jerry") String question, @RequestParam long id) {
        String chat = chatAssistant.chat(id, question);
        return chat;
    }

那我们模拟1号玩家,先告诉模型我叫jerry,再接着问我是谁

在这里插入图片描述

再模拟2号玩家,接着问我是谁,发现模型不知道我是谁,在像模型介绍我是谁后,模型知道了我是谁

在这里插入图片描述
在这里插入图片描述

至此呢,我们实现了用户的隔离,即不同的用户可以单独的运用模型进行相关操作。

2.5记忆持久化存储

那么对于上面的记忆相关的内容,是存在内存变量中的,程序在启停时,内存变量中的数据也会被刷掉。于是我们想到的能不能将这些记忆话的内容进行持久化的存储,比如说存储到量数据库。

而此处我们选择将数据存储在一个mapDB中,要实现自定义的记忆持久化,首先要实现langchain4j提供的记忆持久化的接口ChatMemoryStore

//ChatMemoryStore源码,提供了获取,更新,删除等基本操作
import dev.langchain4j.data.message.ChatMessage;
import java.util.List;

public interface ChatMemoryStore {
    List<ChatMessage> getMessages(Object var1);

    void updateMessages(Object var1, List<ChatMessage> var2);

    void deleteMessages(Object var1);
}

我们自定义一个PersistentChatMemoryStore 实现ChatMemoryStore接口

/***
 * @version 1.0
 * @Author jerryLau
 * @Date 2025/3/26 13:33
 * @注释 持久化内存
 */
public class PersistentChatMemoryStore implements ChatMemoryStore {


    // 创建数据库chat-memory.db
    DB db = DBMaker.fileDB("langchain4j-springBoot-demo/src/main/java/com/jerry/langchain4jspringbootdemo/store/db/chat-memory.db")
            .transactionEnable()
            .make();
    //创建集合 messages,数据key为int,value为 string类型
    private final Map<Integer, String> map = db.hashMap("messages", INTEGER, STRING).createOrOpen();

    /***
    * 从map 中获取记忆信息
    * key 是 memoryId
    */
    @Override
    public List<ChatMessage> getMessages(Object memoryId) {
        String json = map.get((int) memoryId);
        return messagesFromJson(json);
    }

     /***
    * 更新map 中记忆信息
    * key 是 memoryId
    */
    @Override
    public void updateMessages(Object memoryId, List<ChatMessage> messages) {
        String json = messagesToJson(messages);
        map.put((int) memoryId, json);
        db.commit();
    }

     /***
    * 从map 中删除记忆信息
    * key 是 memoryId
    */
    @Override
    public void deleteMessages(Object memoryId) {
        map.remove((int) memoryId);
        db.commit();
    }

那么在aiConf中将设置好的PersistentChatMemoryStore注解进入

/***
     * 注入聊天服务  隔离上下文 - 持久化聊天上下文记录-mapdb
     * @param chatLanguageModel
     * @param streamingChatLanguageModel
     * @return
     */
    @Bean
    public ChatAssistant chatAssistantWithStore(ChatLanguageModel chatLanguageModel, StreamingChatLanguageModel streamingChatLanguageModel) {

        //使用mapdb 来保存聊天记录

        PersistentChatMemoryStore persistentChatMemoryStore = new PersistentChatMemoryStore();

        ChatMemoryProvider chatMemoryProvider = (memoryId) -> {
            return MessageWindowChatMemory.builder()
                    .maxMessages(10)
                    .id(memoryId)
                    .chatMemoryStore(persistentChatMemoryStore)
                    .build();
        };

        ChatAssistant build = AiServices.builder(ChatAssistant.class)
                .chatLanguageModel(chatLanguageModel)
                .streamingChatLanguageModel(streamingChatLanguageModel)
                .chatMemoryProvider(chatMemoryProvider)
                .build();

        return build;
    }

controller中的请求方法还是与langChain4j_memory2一致,在发送请求后,看到在相应的目录路径下生成了 一个名为chat-memory.db的数据库文件

在这里插入图片描述

但是有个问题哈 这个数据库文件 应该怎么打开,本人试着用dateDrip打开但是没打开,有没有大佬指导一下⚠️⚠️⚠️

最后实在是没办法了,我直接写了个main,将数据读了出来,代码供各位参考:

/**
 * @version 1.0
 * @Author jerryLau
 * @Date 2025/3/26 17:12
 * @注释 读取mapdb文件
 */

public class MapDBViewer {
    public static void main(String[] args) {
        // 打开数据库文件(只读模式,避免意外修改)
        DB db = DBMaker
                .fileDB("langchain4j-springBoot-demo/src/main/java/com/jerry/langchain4jspringbootdemo/store/db/chat-memory.db")
                .readOnly()
                .make();

        // 获取存储的Map(假设数据存储在名为"myMap"的HTreeMap中)
        HTreeMap<Integer, String> map = db.hashMap("messages")
                .keySerializer(org.mapdb.Serializer.INTEGER)
                .valueSerializer(org.mapdb.Serializer.STRING)
                .createOrOpen();


        // 遍历并打印所有键值对
        map.forEach((key, value) -> System.out.println("Key: " + key.toString() + ", Value: " + value));

        // 关闭数据库
        db.close();
    }
}

读取到的内容如下:

Key: 11112, Value: [{“contents”:[{“text”:“为我讲个笑话吧”,“type”:“TEXT”}],“type”:“USER”},{“text”:“当然可以,接下来是一个轻松的笑话:\n\n为什么电脑经常生病?\n\n因为它的窗户(Windows)总是开着!”,“type”:“AI”}]

3.Function-call(Tools)

LangChain4j 的 Tools 机制 是一种让开发者将自定义 Java 方法动态暴露给大语言模型(LLM)的框架,允许 LLM 调用外部工具(如计算、API 接口、数据库查询等),突破纯文本生成的限制。

例如,我们知道大型语言模型本身并不擅长数学。如果您的用例偶尔涉及数学计算,您可能希望为 LLM 提供一个“数学工具”。通过在向 LLM 发出的请求中声明一个或多个工具,它就可以在认为合适时调用其中一个。给定一个数学问题以及一组数学工具,LLM 可能会决定为了正确回答该问题,它应该首先调用所提供的数学工具之一。

3.1简单算数

例如我们提供两个数学方法:

@Service
public class ToolService {

    @Tool("Sums 2 given numbers")
    double sum(double a, double b) {
        return a + b;
    }

    @Tool("Returns a square root of a given number")
    double squareRoot(double x) {
        return Math.sqrt(x);
    }
}

修改aiConf,为注入的bean 添加tools,

 @Bean
    public ChatAssistant chatAssistant(ChatLanguageModel chatLanguageModel,
                                       StreamingChatLanguageModel streamingChatLanguageModel,
                                       ToolService toolService) {

        //使用简单的ChatMemory  - MessageWindowChatMemory来保存聊天记录
        MessageWindowChatMemory messageWindowChatMemory = MessageWindowChatMemory.builder()
                .maxMessages(10).build(); //最多保存10条记录

        ChatAssistant build = AiServices.builder(ChatAssistant.class)
                .chatLanguageModel(chatLanguageModel)
                .streamingChatLanguageModel(streamingChatLanguageModel)
                .chatMemory(messageWindowChatMemory)
                .tools(toolService) //添加tools
                .build();

        return build;
    }

继续使用langChain4j_memory2方法调用,会发现LLM通过toolsService的sum方法返回3.2,再结合LLM进行结果输出,存在控制台输出以及结果输出

在这里插入图片描述

正如你 所见,当大型语言模型能够使用工具时,它可以在适当的时候决定调用其中一个工具。

3.2更加复杂的案例

@Tools非常强大的功能。在上个简单的示例中,我们为语言模型提供了基本的数学工具,但想象一下,如果我们给它提供例如 sendEmail(发送电子邮件)这样的工具,并且给出这样的查询:“我的朋友想了解人工智能领域的近期新闻。将简短摘要发送至 friend@email.com”,那么它就可以使用 googleSearch 工具查找近期新闻,然后进行总结,并通过 sendEmail 工具将摘要发送出去。

我们简单的实现这个例子,优先创建一个tool

 @Tool("Send the short recent news in any field to the user.")
    public String generateAI(@P("mailAddr") String mailAddress, @P("filed") String filed) throws MessagingException {
        System.out.println("输入:" + mailAddress);
        System.out.println("输入:" + filed);

        UserMessage userMessage = new UserMessage("generate the short recent news in " + filed + " field");
        ChatResponse chat1Resp = qwenChatModel.chat(userMessage);
        String text = chat1Resp.aiMessage().text();

        boolean b = mailService.sendEmail(
                "XXXX@qq.com", "QQ邮箱的授权码", mailAddress
                , "Short recent news in " + filed + " filed", text);
//mailAddress
        if (b) {
            System.out.println("邮件发送成功!");
        }
        return "邮件发送成功!";
    }

具体实现发送邮件的方法mailService.sendEmail

/**
 * @version 1.0
 * @Author jerryLau
 * @Date 2025/3/28 11:18
 * @注释 邮件服务
 */
@Service
public class MailService {

    public boolean sendEmail(
                             String addr, String password, // 此处 password 实际是授权码
                             String toAddress, String subject, String message)
            throws MessagingException {


        Properties props = new Properties();
        props.put("mail.smtp.host", "smtp.qq.com");
        props.put("mail.smtp.port", "465");
        props.put("mail.smtp.auth", "true");
        props.put("mail.smtp.ssl.enable", "true"); // 开启ssl 验证
        props.put("mail.smtp.socketFactory.class", "javax.net.ssl.SSLSocketFactory");
        props.put("mail.smtp.socketFactory.port", "465");
//        props.put("mail.debug", "true"); // 启用调试日志

        Authenticator authenticator = new Authenticator() {
            protected PasswordAuthentication getPasswordAuthentication() {
                return new PasswordAuthentication(addr, password); // 密码参数传入授权码
            }
        };

        Session session = Session.getInstance(props, authenticator);

        try {
            Message mimeMessage = new MimeMessage(session);
            mimeMessage.setFrom(new InternetAddress(addr));
            mimeMessage.setRecipient(Message.RecipientType.TO, new InternetAddress(toAddress));
            mimeMessage.setSubject(subject);
            mimeMessage.setText(message);

            Transport.send(mimeMessage);
            System.out.println("邮件发送成功!");
            return true;
        } catch (MessagingException e) {
            throw new RuntimeException(e);
        }

    }

}

在这里插入图片描述

查看邮件发现,邮件也正常收到了

在这里插入图片描述

4.预设角色SystemMessage

在1.4.2中其实已经通过自定义注解的方式实现了系统角色预定。其实Langchain4j也提供了自己的注解配置实现系统预定角色–@SystemMessage

在这里插入图片描述


那么至此呢我们已经学习完了Spring-boot集成Langchain4j以及阿里云百炼模型。感兴趣的同学可以自己体会应用啦

码字不易,各位观众老爷如果喜欢,希望一键三连哈~~~


参考文档:

Introduction | LangChain4j官方文档

Langchain4j中文版本文档


个人代码demo

后面有时间了,我再整理放一下~~~

http://www.dtcms.com/a/98921.html

相关文章:

  • Python 笔记 (二)
  • Python导论
  • HTTP介绍以及(GET/POST/PUT/DELETE)应用介绍
  • Kubernetes》》K8S》》Deployment 、Pod、Rs 、部署 nginx
  • 【C++重点】虚函数与多态
  • 责任链模式_行为型_GOF23
  • MQTT之重复消息(5、TCP重连和MQTT重连)
  • 【研究方向】联邦|自然语言
  • 自动关机监控器软件 - 您的电脑节能助手
  • JavaScript中集合常用操作方法详解
  • RHINO 转 STL,解锁 3D 打印与工业应用新通道
  • QT图片轮播器(QT实操学习2)
  • Windows 下 Rust 快速安装指南
  • puppeteer+express服务端导出页面为pdf
  • JavaScript中的Math对象和随机数
  • [ 春秋云境 ] Initial 仿真场景
  • Linux系统中应用端控制串口的基本方法
  • GEO(生成引擎优化)实施策略全解析:从用户意图到效果追踪
  • CANoe入门——CANoe的诊断模块,调用CAPL进行uds诊断
  • 鸿蒙项目源码-外卖点餐-原创!原创!原创!
  • 【算法】二分查找总结篇
  • Java网页消息推送解决方案
  • 累积分布策略思路
  • ModuleNotFoundError: No module named ‘ml_logger.logbook‘
  • 组件组合和Context API在React中的应用
  • Go 语言规范学习(4)
  • 从系统架构、API对接核心技术、业务场景设计及实战案例四个维度,深度解析1688代采系统
  • 征程 6E mipi tx 系列之方案介绍
  • 知能行每日刷题
  • 【2.项目管理】2.7 进度控制习题-2