目前市面上有关java调用大模型的工具库,主流的有两种, 一种是LangChain4j, 一种是SpringAI。

一、快速入门

1.引入依赖

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

2.配置api-key

使用阿里云百炼平台的大模型时,需要指定大模型的url地址,百炼平台的API-KEY,以及调用的模型名称。这里的API-KEY可以直接写死到代码中,也可以配置到操作系统的环境变量中,然后通过代码获取再使用。推荐把API-KEY配置到系统的环境变量中再使用,因为如果直接写死在代码里面,会存在API-KEY泄露的风险。所以在写代码前,请先在系统的用户变量中创建一个名字叫API-KEY的环境变量,值就是在百炼平台申请的api-key。最后一定记得重启IDEA!

3 构建聊天对象OpenAiChatModel,因为OpenAI是行业的先驱,所以使用阿里云平台的大模型都一般兼容OpenAI的规范,所以langchain4j就可以使用OpenAI这个:

构建OpenAiChatModel对象

OpenAiChatModel model = OpenAiChatModel.builder()
        .baseUrl("https://dashscope.aliyuncs.com/compatible-mode/v1")//url参考百炼平台API文档
        .apiKey(System.getenv("API-KEY"))//获取环境变量API-KEY使用
        .modelName("qwen-plus")//设置模型名称
        .build();

交互:

        //1.构建OpenAiChatModel对象
        OpenAiChatModel model = OpenAiChatModel.builder()
                .baseUrl("https://dashscope.aliyuncs.com/compatible-mode/v1")
                .apiKey(System.getenv("API-KEY"))
                .modelName("qwen-plus")
                .build();

        //2.调用chat方法,交互
        String result = model.chat("今天几号了");
        System.out.println(result);

4.配置日志信息

为了查看与大模型交互过程中具体发送的请求消息和大模型响应的数据,可以打开日志开关,我们只需要在构建OpenAiChatModel对象的时候调用logRequests和logResponses方法设置一下即可。

但注意要引入日志依赖:

<dependency>
    <groupId>ch.qos.logback</groupId>
    <artifactId>logback-classic</artifactId>
    <version>1.4.11</version>
</dependency>

所以这样就可以调用大模型了,但是在实际中并不是这样调用的,这样不适合复炸场景,我们需要把sqring和langchain4j结合起来

二、Spring整合LangChain4j

1.创建一个springboot项目,引入langchain4j起步依赖(spring完成了自动装配),注意springboot的版本必须兼容,我用的是3.5.11。

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

2.在application.yml中配置调用大模型的信息

langchain4j:
  open-ai:
    chat-model:
      base-url: https://dashscope.aliyuncs.com/compatible-mode/v1
      api-key: ${API-KEY}
      model-name: qwen-plus
      log-requests: true #请求消息日志
      log-responses: true #响应消息日志
logging:
  level:
    dev.langchain4j: debug #日志级别

起步依赖会检测到配置信息,自动的往IOC容器中注入一个OpenAiChatModel对象。就相当于第一步创建了一个对象,然后用的时候我们可以直接注入,使用chat方法即可。

@RestController
public class ChatController {
    @Autowired
    private OpenAiChatModel model;
    @RequestMapping("/chat")
    public String chat(String message){
        String result = model.chat(message);
        return result;
    }
}

三、AiServices工具类(前提是配置了yaml和引入第一个自动装配依赖)

在之前的案例中,访问大模型是借助于OpenAiChatModel的chat方法完成的。其实这种方式在实际开发中并不是很常用,因为如果使用这种方式调用大模型,将来完成一些高阶的功能,比如会话记忆/RAG知识库/Tools工具的时候,完成起来是比较复杂的,为了简化我们程序员的使LangChain4j提供了AiServices工具类,封装了有关model对象和其它一些功能的操作。

1.引入依赖:

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

2.写一个接口封装聊天方法

public interface ConsultantService {
    //用于聊天的方法,message为用户输入的内容
    public String chat(String message);
}

3.使用AiServices工具类创建接口的动态代理对象(写一个配置类,@bean返回这个接口,在里面配置详细信息),因为后续spring要利用这个动态代理对象对输入等做处理。

@Configuration
public class CommonConfig {
    @Autowired
    private OpenAiChatModel model;
    @Bean         
    public ConsultantService consultantService() {
        ConsultantService cs = AiServices.builder(ConsultantService.class)
                .chatModel(model)//设置对话时使用的模型对象
                .build();
        return cs;
    }
}

4.ChatController中注入ConsultantService并使用

@RestController
public class ChatController {
    @Autowired
    private ConsultantService consultantService;
    @RequestMapping("/chat")
    public String chat(String message){
        String chat = consultantService.chat(message);
        return chat;
    }
}

5.简化:不用配置bean了,想为哪个接口创建代理对象,只需要在该接口上添加@AiService注解并指定要使用的模型,将来LangChain4j扫描到该注解后会自动的创建该接口的代理对象并注入到IOC容器中。这样就可以直接在controller里面使用了。至于这个注解的参数,肯定就要能够表示以前bean里面的信息

其实现在就是模型用上面,对吧,上面的配置文件中模型就是springboot自动装配好的,在这个注解里:wiringMode用于指定装配模式,默认的取值为AiServiceWiringMode.AUTOMATIC表示自动装配的意思,这里设置为手动装配:AiServiceWiringMode.EXPLICIT。chatModel注解用于指定对话时需要使用的模型对象在IOC容器中的名字,由于IOC容器中Bean对象的名字默认是类名首字母小写,所以这里的取值为 openAiChatModel。(其实这两个是一样的,因为自动装配的bean就是openAiChatModel,这两个完全一样)。

四、流式调用,调用大模型有两种方式:流式调用和阻塞式调用。阻塞式调用, 结果是一次性响应的, 流式调用就好像是一点点生成的。一般情况如果阻塞调用很影响用户体验,所以,接下来我们学习如何使用LangChain4j发起流式调用。

1.引入依赖

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

2.yaml文件模型使用流式的:其实就变了第三行

langchain4j:
  open-ai:
    streaming-chat-model: #流式模型配置
      base-url: https://dashscope.aliyuncs.com/compatible-mode/v1
      api-key: ${API-KEY}
      model-name: qwen-plus
      log-requests: true
      log-responses: true

3.修改接口注解:

ConsultantService中的chat方法的返回值类型,需要修改为支持流式处理的类型Flux,同时还需要在AiService注解中,通过streamingChatModel属性, 配置一下流式调用的模型对象,值为openAistreamingChatModel

@AiService(
        streamingChatModel = "openAiStreamingChatModel"
)
public interface ConsultantService {
    public Flux<String> chat(String message);
}

4.调整ChatController中的代码,方法的返回值类型,需要修改为支持流式处理的类型Flux里面的泛型是以前的那个类型

@RestController
public class ChatController {
    @Autowired
    private ConsultantService consultantService;

    @RequestMapping(value = "/chat",produces = "text/html;charset=utf-8")
    public Flux<String> chat(String message){
        Flux<String> result = consultantService.chat(message);
        return result;
    }
}

produces = "text/html;charset=utf-8"是解决乱码问题。

 所以现在为止要完成一个简单的功能实际上就是这么几步:

第一步就是创建一个springboot项目,注意版本。

第二步就是引入四个个依赖,一个是spring自动装配openai模型的依赖,一个是aiservice的依赖

        <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>

两个是流式处理的依赖

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

第三步就是去写一个yaml配置文件,里面就是自动装配的大模型的信息还有日志信息,当然这个是阿里云的格式,如果使用olama本地部署有自己的方式

langchain4j:
  open-ai:
    streaming-chat-model: #流式模型配置
      base-url: https://dashscope.aliyuncs.com/compatible-mode/v1
      api-key: ${API-KEY}
      model-name: qwen-plus
      log-requests: true #请求消息日志
      log-responses: true #响应消息日志
logging:
  level:
    dev.langchain4j: debug #日志级别

第四步就是写一个接口,里面就是一些要实先ai的接口,然后在这个类上面加aiservice的注解,当然我们不写那个指定模型的参数了,因为不写就是代表使用自动装配的模型。并且使用参数表示流式处理,然后模型的返回值要求flux。

@AiService(
        streamingChatModel = "openAiStreamingChatModel"
)
public interface ConsultantService {
    public Flux<String> chat(String message);
}

第五步就是在controller里面调用即可,很简单,要注意返回值类型也是Flux。

五、消息注解:

将来开发的项目叫做Ai志愿填报顾问,它只能回答志愿填报相关的问题,如果用户问其他的问题,则不予回答。比如你问它特朗普靠谱吗?现在的助手:

但我们是不想要这种效果的,只要求它回答特殊的问题,如果要实现这样的效果,我们就需要通过设定系统消息的方式来完成了。在LangChain4j中,提供了两个有关设置消息的注解, 一个是SystemMessage, 另外一个是UserMessage。

1. SystemMessage

顾名思义,它是用于设置系统消息的,可以直接在接口的方法上添加这个注解,在注解中书写系统消息即可。当然了, 如果的系统消息很长, 直接在代码中写不方便,它还提供了另外一种使用方式,通过fromResource属性,指定一个外部的文件。这样我们就可以把系统消息一次性的写入到外部文件中,管理起来也比较方便。

@AiService(
        streamingChatModel = "openAiStreamingChatModel"
)
//@AiService
public interface ConsultantService {
    //@SystemMessage("你是志愿填报助手,只可以回答志愿填报方面的问题")
    @SystemMessage(fromResource = "system.txt")
    public Flux<String> chat(String message);
}

2.UserMessage(用的少,就是相当于拼接了)

假设现在没有SystemMessage,那么可以借助于UserMessage注解完成同样的效果,可以在用户消息前后,拼接提前预设的内容下面给出一个使用示例:


@UserMessage("你是东哥的助手小月月,温柔貌美又多金。{{it}}") 
public Flux<String> chat(String message);

上面示例中的参数message是用户传递的消息,在使用UserMessage注解的时候,可以通过{{it}}的方式, 动态的获取到用户传递的消息,然后再往它的前后拼接上预设的内容即可,想拼什么拼什么。这个花括号内的it是固定的,不能随便写。假设不想使用it这个名字,langchain4j提供了一个V注解,用于解决这个问题。我们在参数前面通过V注解给这个参数起一个名字,然后在花括号内写上同样的名字就能获取到了,下面是一个使用示例:

@UserMessage("你是东哥的助手小月月,温柔貌美又多金。{{msg}}")
public Flux<String> chat(@V("msg") String message);

六、会话记忆

6.1会话记忆原理

通过下面这幅图解释LangChain4j是如何实现会话记忆效果的。图中三个框, 分别代表浏览器、web后端和大模型。借助于langchian4j可以准备一个专门用于存储会话记录的存储对象。

当用户问西北大学是211吗?它会把消息传递给后端,后端接收到消息后,会自动把消息存放到存储对象中,然后再获取存储对象中记录的所有会话消息,一块发送给大模型,当然现在存储对象中只记录了一条消息,所以只把一条消息发送给大模型。大模型根据接收到的消息,生成答案,比如说是的,再把答案响应给web后端,此时web后端会把得到的响应消息往存储对象中拷贝一份,然后再把响应消息发送给用户。

用户接收到答案后,接着问,是985吗?这条消息发送给web后端后,web后端依然会自动的把消息存放到存储对象中,此时存储对象中就存放了三条消息了,紧接着获取到存储对象中所有的会话消息,一并发送给大模型,这一次大模型就能够根据用户发送的所有会话记录进行推断回答了,这就是会话记忆的原理!

6.2会话记忆基本实现

langchain4j提供了一个接口叫做ChatMemory,该接口中提供了add方法用于添加一条记录,messages方法用于获取所有的会话记录,clear方法用于清除所有的会话记录,这里还有一个id方法,它是用于唯一的标识一个存储对象,这个id暂时我们用不着,等会儿讲解会话记忆隔离的时候会用

public interface ChatMemory {
    Object id();//记忆存储对象的唯一标识

    void add(ChatMessage var1);//添加一条会话记忆

    List<ChatMessage> messages();//获取所有会话记忆

    void clear();//清除所有会话记忆
}

然后我们就可以自己去实现这个接口,但我们先演示一下基本的使用,所以我们会使LangChain4j提供的该接口的两个实现类,一个是TokenWindowChatMemory,另外一个MessageWindowCha

tMemory, 暂时先使用MessageWindowChatMemory来存储会话记录。

6.2.1配置MessageWindowChatMemory

我们需要在CommonConfig类中,构建MessageWindowChatMemory对象,并注入到IOC容器中。

@Bean
public ChatMemory chatMemory() {
    return MessageWindowChatMemory.builder()
            .maxMessages(20)//最大保存的会话记录数量
            .build();
}

构建的时候我们可以指定该对象中最大的会话存储数量这是因为首先大模型的上下文不是无限的,一般目前大模型支持的上下文最大在10w个token左右,发的太多了大模型吃不消。另外一个原因是如果会话记录存储的太多,费用就会越贵。所以这里需要设置一个合适的数量,一般设置20就够了。如果要存储的消息超过了20条,那么最早存储的消息就会被淘汰,在存储对象中最多保留最新的20条消息。

6.2.2配置会话记忆对象

然后需要在ConsultantService接口上的AiService注解中借助于chatMemory属性完成配置,值就是IOC容器中ChatMemory对象的名字

@AiService(
        streamingChatModel = "openAiStreamingChatModel",
        chatMemory = "chatMemory"//配置会话记忆对象
)
public interface ConsultantService {
    @SystemMessage(fromResource = "system.txt")
    public Flux<String> chat(String message);
}

6.3实现会话隔离

刚才借助于MessageWindowChatMemory实现了会话记忆的效果, 但是还是有一些问题。当不同的用户访问我们的程序时,无法区分不同用户的会话记录,因为刚才实现的会话记忆,所有用户存储会话记录都是用的是同一个会话记忆对象,所以会话记忆并没有做到隔离。如何实现?

其实ChatMemory接口提供了一个id方法,需要借助它来完成会话记忆隔离。

在LangChain4j中可以准备一个容器,专门用于存储当前程序中所有的会话记忆对象。假设有一个用户访问我们的程序,此时它除了要把用户问题message携带给后端,还需要携带一个memoryId,假设它携带的memoryId为1,此时LangChain4j会先从容器中找有没有一个ChatMemory对象的id为1,如果有就使用,但如没有会新创建一个ChatMemory对象,并把当前的memoryId 1 设置给这个ChatMemory对象,并把会话记录存储到该对象中使用。

假设又有一个用户访问我们的程序,它携带的memoryId为2,同样的,LangChain4j也会从容器中找有没有一个ChatMemory对象的id为2,很显然还是没有,所以会创建一个新的ChatMemory对象,并把memoryId 2设置给这个ChatMemory对象,并把会话记录存储到该对象中使用。

注意,假设第二个用户继续访问我们的程序,它携带了同样的memoryId 2给后端,此时LangChain4j从容器中查找的时候发现已经存在一个ChatMemory对象的id为2,所以直接复用这个已经存在的ChatMemory对象,这样我们就可以借助于ChatMemory的id值实现不同会话之间的记忆隔离效果。

6.3.1定义会话记忆对象提供者

LangChain4j中提供了一个类ChatMemoryProvider,将来LangChain4j如果从容器中没有找到指定id的ChatMemory对象,就会调用ChatMemoryProvider对象的get方法获取一个新的ChatMemory对象使用,因此我们需要提供这个ChatMemoryProvider对象,实现get方法。这里的get方法,会接收一个参数,这个参数就是memoryId,返回一个结果就是ChatMemory对象。我们只需要在get方法中写清楚根据memoryId如何构建ChatMemory对象并返回的逻辑即可。当然这里我们依然构建的是MessageWindowChatMemory对象,只不过我们在构建的时候,除了要指定最大的会话记录数量外,还需要把memoryId设置给当前的ChatMemory对象。

@Bean
public ChatMemoryProvider chatMemoryProvider() {
    ChatMemoryProvider chatMemoryProvider = new ChatMemoryProvider() {
        @Override
        public ChatMemory get(Object memoryId) {
            return MessageWindowChatMemory.builder()
                    .id(memoryId)//id值
                    .maxMessages(20)//最大会话记录数量
                    .build();
        }
    };
    return chatMemoryProvider;
}

6.3.2 配置会话记忆对象提供者

需要在AiService注解中,借助于chatMemoryProvider这个属性指定会话记忆对象提供者,跟之前的套路都是一样的,只不过我们既然提供了ChatMemoryProvider,之前提供的这个公有的ChatMemory就没有必要了,可以把它注释掉。

@AiService(
        streamingChatModel = "openAiStreamingChatModel",
        //chatMemory = "chatMemory",
        chatMemoryProvider = "chatMemoryProvider"//配置会话记忆对象
)
public interface ConsultantService {
    @SystemMessage(fromResource = "system.txt")
    public Flux<String> chat(String message);
}

6.3.3修改接口与控制类,传入id这个参数。

在ConsultantService接口中,给chat方法添加一个参数memoryId,并且需要添加注解@MemoryId明确的告诉LangChain4j,这个参数就是用于标识ChatMemory对象的id值,r'h就拿这个参数的值去容器中帮我匹配对象的ChatMemory对象,如果匹配到就复用,如果没有匹配到就调用ChatMe

moryProvider对象的get方法获取一个新的使用。

这里有个小细节要注意,如果chat方法只有一个参数,那langchain4j会默认把这个参数当做用户消息来处理,如果chat方法有多个参数,就必须手动的指定哪个参数对应的是用户消息,所以需要在message参数前面手动的添加UserMessage注解,用于标识message对应的就是用户消息。

@AiService(
        streamingChatModel = "openAiStreamingChatModel",
        //chatMemory = "chatMemory",
        chatMemoryProvider = "chatMemoryProvider"//配置会话记忆对象
)
public interface ConsultantService {
    @SystemMessage(fromResource = "system.txt")
    public Flux<String> chat(@MemoryId String memoryId, @UserMessage String message);
}
@RequestMapping(value = "/chat",produces = "text/html;charset=utf-8")
public Flux<String> chat(String memoryId,String message){
    Flux<String> result = consultantService.chat(memoryId,message);
    return result;
}

6.4会话记忆持久化

刚才完成了会话记忆隔离,其实会话记忆有瑕疵,只要后端重启,会话记忆就没有了。这是因为MessageWindowChatMemory是使用集合存储会话消息的,这是内存存储,一旦当服务器重启后这些消息必然会丢失!(解释:构建的用于存储会话记录的对象是MessageWindowChatMemo

ry,而这个对象内部维护了一个成员变量ChatMemoryStore,其实我们使用MessageWindowCh

atMemory对象的add方法添加会话记录的时候,真正用于存储的对象是这个ChatMemoryStore,ChatMemoryStore是一个接口,它里面提供了getMessages、updateMessages、deleteMessages方法分别用于根据memoryId获取会话记录,根据memoryId更新会话记录以及根据memoryId删除会话记录。LangChain4j为该接口提供了两个实现类,分别是InMemoryChatMemoryStore和SingleSlotMemoryStore。而我们MessageWindowChatMemory中默认使用的Store对象就是这个SingleChatMemoryStore。而在SingleSlotChatMemoryStore中维护了一个集合对象messages,它就是使用这个集合存储会话消息的,所以很明显这是内存存储,一旦当服务器重启后这些消息必然会丢失!)

接下来要做的事情就是将会话记录持久化存储到外部的存储器中,比如mysql、redis、mongo等等都可以。最直观的解决思路就是自己提供一个ChatMemoryStore的实现类,在实现类中把消息存储到其它地方,然后再把自己提供的ChatMemoryStore交给MessageWindowChatMemory即可。我们把会话记录存储在redis中。

6.4.1引入redis依赖,配置yaml

6.4.2提供ChatMemory实现类操作redis

定义实现类实现ChatMemory接口,重写getMessages、updateMessages、deleteMessages方法,用于操作redis,并且把实现类的对象注入到IOC容器中。

@Repository
public class RedisChatMemoryStore implements ChatMemoryStore {
    //注入RedisTemplate
    @Autowired
    private StringRedisTemplate redisTemplate;
    @Override
    public List<ChatMessage> getMessages(Object memoryId) {
        //获取会话消息
        String json = redisTemplate.opsForValue().get(memoryId);
        //把json字符串转化成List<ChatMessage>
        List<ChatMessage> list = ChatMessageDeserializer.messagesFromJson(json);
        return list;
    }

    @Override
    public void updateMessages(Object memoryId, List<ChatMessage> list) {
        //更新会话消息
        //1.把list转换成json数据
        String json = ChatMessageSerializer.messagesToJson(list);
        //2.把json数据存储到redis中
        redisTemplate.opsForValue().set(memoryId.toString(),json, 30, TimeUnit.MINUTES);
    }

    @Override
    public void deleteMessages(Object memoryId) {
        //删除会话消息
        redisTemplate.delete(memoryId.toString());
    }
}

看见没,左边就是传过来的id,右边就是一个列表,存放历史记录。(之后如果有需求可以实现历史记录查询的功能,比如首先可以先把所有的id查出来,然后再根据id去redis里面查询历史记录)

6.4.3 配置ChatMemoryStore

将自己实现的ChatMemoryStore配置给MessageWindowChatMemory对象使用。就多加一行即可。

@Autowired
private ChatMemoryStore redisChatMemoryStore;

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

截至到目前为止,实现会话记忆与隔离的功能:以前的

 以现在为止要完成一个简单的功能实际上就是这么几步:

第一步就是创建一个springboot项目,注意版本。

第二步就是引入四个个依赖,一个是spring自动装配openai模型的依赖,一个是aiservice的依赖

        <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>

两个是流式处理的依赖

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

第三步就是去写一个yaml配置文件,里面就是自动装配的大模型的信息还有日志信息,当然这个是阿里云的格式,如果使用olama本地部署有自己的方式

langchain4j:
  open-ai:
    streaming-chat-model: #流式模型配置
      base-url: https://dashscope.aliyuncs.com/compatible-mode/v1
      api-key: ${API-KEY}
      model-name: qwen-plus
      log-requests: true #请求消息日志
      log-responses: true #响应消息日志
logging:
  level:
    dev.langchain4j: debug #日志级别

第四步就是写一个接口,里面就是一些要实先ai的接口,然后在这个类上面加aiservice的注解,当然我们不写那个指定模型的参数了,因为不写就是代表使用自动装配的模型。并且使用参数表示流式处理,然后模型的返回值要求flux。

@AiService(
        streamingChatModel = "openAiStreamingChatModel"
)
public interface ConsultantService {
    public Flux<String> chat(String message);
}

第五步就是在controller里面调用即可,很简单,要注意返回值类型也是Flux。

加上:

第六步:自己写一个ChatMemory接口的实现类,用于实现会话记忆的持久化

第七步:配置ChatMemoryProvider作为会话记忆隔离持久化对象

@Autowired
private ChatMemoryStore redisChatMemoryStore;//自己写好的实现类

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

第八步,在ai方法接口上参数chatMemoryProvider = "chatMemoryProvider"配置这个会话记忆对象,并且用@MemoryId指定那个是这个memoryid,@UserMessage指定那个是客户传过来的话,可以加上系统提示词

@AiService(
        streamingChatModel = "openAiStreamingChatModel",
        //chatMemory = "chatMemory",
        chatMemoryProvider = "chatMemoryProvider"//配置会话记忆对象
)
public interface ConsultantService {
    @SystemMessage(fromResource = "system.txt")
    public Flux<String> chat(@MemoryId String memoryId, @UserMessage String message);
}

七、RAG向量库

目前还存在一个问题,无法查询各个高校2024年最新的录取分数,其原因在于大模型最后一次训练是一个结束时间的,而且大模型训练的数据也是有限的,一些内部的敏感数据和新数据,大模型是无法感知的。所以就要使用RAG相关的知识了。就相当于给模型加了一个外挂知识库。

7.1RAG原理

RAG全称为 Retrieval Augmented Generation,翻译过来是检索增强生成,简单理解就是通过检索外部知识库的方式增强大模型的生成能力。这是传统的流程:

一旦外挂了知识库后, 整个工作流程会发生一些变化。

当用户把问题发送给AI应用,AI应用会先根据用户的问题从知识库中检索对应的知识片段,得到知识片段后AI应用需要结合用户的问题以及知识库中检索到的知识片段组织要发送给大模型的消息。我们需要关注的核心有两个:一个是知识库应该怎么搭建,另外一个是如何从知识库中检索出用户问题相关的知识片段。这个知识库一般采取的是一种特殊的数据库,叫向量数据库(衡量两个向量的相似度是使用余弦相似度,两个向量的余弦相似度越大,向量方向越接近,两点之间的距离也就越小。)。目前市面上常见的向量数据库有很多,比如Milvus、Chroma、Pinecone这些专用的向量数据库,还有一些传统的数据库做了向量化扩展,比如redis提供了RedisSearch用于完成向量存储,PostgreSql提供了pgvetor用于完成向量存储,不管是哪一种向量数据库原理都是一样的,使用也都大差不差。

RAG中如何使用向量数据库存储数据:

首先需要把最新的数据或者专业的数据存储到文档(Document)中,接下来借助于文本分割器(Text Splitter)把一个大的文档分割成一个一个小的文本片段(Segments),然后这些小的文本片段要使用一种专门的大模型:向量模型(Embedding Model),不同的大模型擅长的领域不一样,有擅长文本处理的、有擅长图片处理的,其中就有一种大模型擅长文本向量化。借助于向量模型把一个一个的文本片段转换成向量,接下来把每一个向量和其对应的文本片段一块存储到向量数据库中。比如:

所以这样就可以把一些专业知识外挂到这个数据库中。

如何从向量数据库中检索出跟用户问题相关的文本片段:

用户提交的消息需要使用向量模型转换为向量,接下来拿着该向量和向量数据库中已经存在的向量进行比对,计算他们之间的余弦相似度,把满足要求的向量筛选出来得到其对应的文本片段,最后结合用户提交的消息和从向量数据库中检索到的文本片段,组织数据发送给大模型。比如:用户给一个你爱上班吗,最后发给大模型的其实是,我爱上班,上班真好,我爱工作,你爱上班吗。

7.2快速入门

涉及到几个步骤用到的工具

7.2.1存储知识:

使用最简单的一个向量数据库:angchain4j-easy-rag,这是一个简易版本的rag实现方案,这个依赖中提供了内存版的向量数据库和向量模型供我们使用。

<dependency>
    <groupId>dev.langchain4j</groupId>
    <artifactId>langchain4j-easy-rag</artifactId>
    <version>1.0.1-beta6</version>
</dependency>

LangChain4j提供的ClassPathDocumentLoader可以快速的将指定目录下的文档加载进内存中,并且每一个文档,都会对应的生成一个Document对象来记录文档的内容。这一部分工作需要在CommonConfig.java中完成。

@Bean
public EmbeddingStore store(){
    //1.加载文档进内存
    List<Document> documents = ClassPathDocumentLoader.loadDocuments("content");
    return null;
}

7.2.2构建向量数据库操作对象EmbeddingStore(其实这一步就是在创建一个向量数据库可以理解,因为下一个要配置检索器肯定要知道检索那个向量数据库

在我们引入的简单依赖中已经提供了一个用于操作内存版本的向量数据库的类InmemoryEm

beddingStore,Inmemory是内存的意思,Embedding翻译过来是嵌入/向量的意思,Store是存储的意思,顾名思义,操作内存向量数据库。我们只要new出来一个对象即可。

@Bean
public EmbeddingStore store(){
    //1.加载文档进内存
    List<Document> documents = ClassPathDocumentLoader.loadDocuments("content");
    //2.构建向量数据库操作对象  操作的是内存版本的向量数据库
    InMemoryEmbeddingStore store = new InMemoryEmbeddingStore();
    return store;
}

7.2.3切割文档、向量化并存储到向量数据库(这一步其实就是对已有的document进行处理,然后变成向量放在那个数据库里面)

LangChain4j中给我们提供了一个类EmbeddingStoreIngestor,

它把很多细节都封装起来了,可以帮我们快速的完成这一步的操作。首先我们构建EmbeddingStoreIngestor对象,构建的时候告诉它我要把向量化的数据存储到哪里,也就是把第二步构建的数据库操作对象给它,接下来调用它的ingest方法,把需要存储数据的文档对象documents给它传递进去。在这个方法的内部会使用它内置的文本分割器先分割,然后使用内置的向量模型完成向量化,最后再把向量存储到向量数据库中。

@Bean
public EmbeddingStore store(){
    //1.加载文档进内存
    List<Document> documents = ClassPathDocumentLoader.loadDocuments("content");
    //2.构建向量数据库操作对象  操作的是内存版本的向量数据库
    InMemoryEmbeddingStore store = new InMemoryEmbeddingStore();
    //3.构建一个EmbeddingStoreIngestor对象,完成文本数据切割,向量化, 存储
    EmbeddingStoreIngestor ingestor = EmbeddingStoreIngestor.builder()
            .embeddingStore(store)
            .build();
    ingestor.ingest(documents);
    return store;
}

这样就完成了存储,得到了一个包含想要向量的向量数据库。

7.2.4检索

7.2.4.1 构建检索对象

LangChain4j提供的向量数据库检索对象叫做EmbeddingStoreContentRetriever,构建的时候我们可以设置三个内容。第一个得调用embeddingStore方法告诉它从哪里检索,其实就是我们刚才构建的这个InmemoryEmbeddingStore给他即可;第二个我们可以设置一下最小余弦相似度的值,之前我们讲过检索的时候会把用户的问题向量化,然后与向量数据库中已经存在的向量计算余弦相似度,值越大,相似度越高,这里通过minScore方法设置一个最低的相似度分数,可以确保检索出来的内容跟用户问题的相关度比较高;第三个可以设置一个最大检索出来的片段数量值,因为将来如果检索出来的片段太多,一并发送给大模型,token的消耗是比较大的,而且分数低的片段你发送给大模型还会影响生成的结果,这里通过maxResults方法设置最大的片段数量后,它会保留分数最高的前几个片段使用。这些操作也是在CommonConfig.java中完成

@Bean
public ContentRetriever contentRetriever(EmbeddingStore store){
    return EmbeddingStoreContentRetriever.builder()
            .embeddingStore(store)//设置向量数据库操作对象
            .minScore(0.5)//设置最小分数
            .maxResults(3)//设置最大片段数量
            .build();
}

7.2.4.2 配置检索对象

在@AiService注解里参数contentRetriever = "contentRetriever"即可配置检对象。

总结:其实就是配置了一个向量数据库和一个检索对象,然后把这个检索对象给了接口。其中在配置向量数据库的时候有很多细节,会在7.3中讲到

7.3细节的选择------核心API

7.2的初步使用,我们配置了两个对象,其实这些细节都在第一个配置对象里。第一步我们首先加载,然后自带的emsi对象里面有自己的分割器,向量模型。

现在我们可以自己控制里面的细节。

流程就是:首先我们需要在项目中准备存储数据的文档,这些文档需要使用文档加载器 Document Loader 加载进内存,由于加载的过程中需要解析文档的内容,所以还要使用到文档解析器来解析文档的内容,最后在内存中生成一个一个的Document对象用于记录文档的内容。由于每个Document对象中记录的是对应文档中的全部内容,如果我们直接把整个文档的内容一次性向量化存储到向量数据库中,不利于检索,所以这些文档对象,需要使用文档分割器 Document Splitter分割成一个一个的文本片段,而每一个文本片段只是记录整个文档中的一小部分内容,这样将来根据用户问题检索相关片段的时候就会更精准。这些文本片段需要使用向量模型转化为一个一个向量,之前讲过其实就是一串一串的数字记录的是不同维度的坐标,LangChain4j中提供了Embedding对象用于记录这些坐标,因此这里得到的是一个一个的Embedding对象。最后再使用EmbeddingStore这种向量数据库操作对象将向量和对应的文本片段存储到向量数据库中。

在整个流程中,主要用到了文档加载器、文档解析器、文档分割器、向量模型以及向量数据库操作对象这五类API,

7.3.1文档加载器

文档加载器的作用是把磁盘或者网络中的数据加载进程序。LangChain4j提供了多个文档加载器,其中常见的有以下三种:

  • FileSystemDocumentLoader, 根据本地磁盘绝对路径加载

  • ClassPathDocumentLoader,相对于类路径加载

  • UrlDocumentLoader,根据url路径加载

比如可以把配置里面的文档加载器变成本地磁盘绝对路径加载

@Bean
public EmbeddingStore store(){
    //1.加载文档进内存
    //List<Document> documents = ClassPathDocumentLoader.loadDocuments("content");
    List<Document> documents = FileSystemDocumentLoader.loadDocuments("C:\\Users\\Administrator\\ideaProjects\\consultant\\src\\main\\resources\\content");
    //2.构建向量数据库操作对象  操作的是内存版本的向量数据库
    InMemoryEmbeddingStore store = new InMemoryEmbeddingStore();
    //3.构建一个EmbeddingStoreIngestor对象,完成文本数据切割,向量化, 存储
    EmbeddingStoreIngestor ingestor = EmbeddingStoreIngestor.builder()
            .embeddingStore(store)
            .build();
    ingestor.ingest(documents);
    return store;
}

7.3.2文档解析器

文档解析器就是用于解析文档中的内容,把原本非纯文本数据转化成纯文本。比如初始的文档是pdf格式的,它的内容就不是纯文本的,此时需要借助于文档解析器将非纯文本数据转化成纯文本。在LangChain4j中提供了几个常用的文档解析器:

  • TextDocumentParser,解析纯文本格式的文件

  • ApachePdfBoxDocumentParser,解析pdf格式文件

  • ApachePoiDocumentParser,解析微软的office文件,例如DOC、PPT、XLS

  • ApacheTikaDocumentParser(默认),几乎可以解析所有格式的文件

由于默认的ApacheTikaDocumentParser虽然可以解析所有格式的文件,但是它可能在纯PDF文件方面的表现没有那么优秀,或者使用起来没有那么方便,此时我们可以将默认的解析器切换成ApachePdfBoxDocumentParser,具体的操作如下:

引入依赖

<dependency>
    <groupId>dev.langchain4j</groupId>
    <artifactId>langchain4j-document-parser-apache-pdfbox</artifactId>
    <version>1.0.1-beta6</version>
 </dependency>

配置的时候指定解析器

@Bean
public EmbeddingStore store(){
    //1.加载文档进内存
    //List<Document> documents = ClassPathDocumentLoader.loadDocuments("content");
    //加载文档的时候指定解析器
    List<Document> documents = ClassPathDocumentLoader.loadDocuments("content",new ApachePdfBoxDocumentParser());
    //2.构建向量数据库操作对象  操作的是内存版本的向量数据库
    InMemoryEmbeddingStore store = new InMemoryEmbeddingStore();
    //3.构建一个EmbeddingStoreIngestor对象,完成文本数据切割,向量化, 存储
    EmbeddingStoreIngestor ingestor = EmbeddingStoreIngestor.builder()
            .embeddingStore(store)
            .build();
    ingestor.ingest(documents);
    return store;
}

7.3.3文档分割器

文档分割器主要用于把一个大的文档切割成一个一个的小片段。在langchain4j中提供了多种文档分割器,大概有以下7种:

  • DocuemntByParagraphSplitter,按照段落分割文本

  • DocumentByLineSplitter,按照行分割文本

  • DocumentBySentenceSplitter,按照句子分割文本

  • DocumentByWordSplitter,按照词分割文本

  • DocumentByCharacterSplitter,按照固定数量的字符分割文本

  • DocumentByRegexSplitter,按照正则表达式分割文本

  • DocumentSplitters.recursive(…)(默认),递归分割器,优先段落分割,再按照行分割,再按照句子分割,再按照词分割

举一个例子比如段落分割文本,比如DocumentByParagraphSplitter把文档分割成6个部分,但是这每一部分并不是将来进行向量化的文本片段,文本片段是根据这6部分的内容组合而成的。通常情况下LangChain4j是允许我们指定文本片段的字符容量的,假设我指定单个文本片段的字符容量为300,那么在组合文本片段的时候,第一部分的自然段和第二部分的自然段的字符总和不到300,可以放到同一个文本片段中,但是加上第三部分的自然段,字符总和超过了300,那么第三部分的自然段就不能再放到这个文本片段中了,而是放到下一个新的文本片段中。此时如果是递归分割器的话它会继续使用行分割器,把第三个自然段进一步分割,尝试把得到的内容放到当前文本片段中,如果还是不行,再按照句子分割,这就是它的作用。

默认使用的是递归分割器,默认使用的单个文本最大字符个数就是300,当然如果不想使用默认的切割器,觉得300个字符太少了,想多设置一点儿(其实很简单就是自己构建一个文本分割器,然后给EmbeddingStoreIngestor,因为EmbeddingStoreIngestor实现的就是包含分割功能,现在我们不让它使用默认的,使用我们自定义的。通过documentSplitter方法指定即可):

step1:构建文本分割器对象:通过DocumentSplitters.recursive(,)构建自定义配置

DocumentSplitter ds = DocumentSplitters.recursive(
  每个片段最大容纳的字符, 
  两个片段之间重叠字符的个数
);

使用构建的时候需要指定每个片段最大容纳的字符数量和两个片段之间重叠字符的个数,第一个好理解,第二个参数是为了什么:比如这样一段话:

假如有一篇以高考为题目的散文需要存储到向量数据库中,将来分割后得到的两个文本片段,第一个片段里写到高考.....而第二个片段中完全没有出现高考相关的字眼,去检索高考相关的内容时第二个片段将不会被检索出来,但实质上按照语义它是应该被检索出来的。解决的办法就是让两个片段存储的内容有重叠的部分,上一个片段的末尾与下一个片段的开头重复,这样就可以保持语义的连贯性了。比如高考不是重点, 而是起点...这句话存储到第二个片段的开头就能解决这个问题,第二个参数就是用于指定重叠部分字符的数量。

step2:配置文本分割器对象,就在3里面多加一个参数指定即可。

@Bean
public EmbeddingStore store(){
    //1.加载文档的时候指定解析器
    List<Document> documents = ClassPathDocumentLoader.loadDocuments("content",new ApachePdfBoxDocumentParser());
    //2.构建向量数据库操作对象  操作的是内存版本的向量数据库
    InMemoryEmbeddingStore store = new InMemoryEmbeddingStore();
    //构建文档分割器对象
    DocumentSplitter ds = DocumentSplitters.recursive(500,100);
    //3.构建一个EmbeddingStoreIngestor对象,完成文本数据切割,向量化, 存储
    EmbeddingStoreIngestor ingestor = EmbeddingStoreIngestor.builder()
            .embeddingStore(store)
            .documentSplitter(ds) 
            .build();
    ingestor.ingest(documents);
    return store;
}

7.3.4向量模型

当时并没有指定这个向量模型,因为它被封装到EmbeddingStoreIngestor中了,现在我们其实可以自己控制模型

step1:修改yaml文件,这次是embedding-model,其他的一样。

langchain4j:
  open-ai:
    embedding-model:
      base-url: https://dashscope.aliyuncs.com/compatible-mode/v1
      api-key: ${API-KEY}
      model-name: text-embedding-v3
      log-requests: true
      log-responses: true

step2:设置使用的向量数据模型

当配置完毕后,LangChain4j会自动的根据我们的配置信息往IOC容器中注入一个EmbeddingMo

del对象供我们使用,所以接下来只需要把先注入这个对象,然后把这个EmbeddingModel对象交给我们配置的数据库(EmbeddingStoreIngestor)和检索bean(EmbeddingStoreContentRetriever

)即可,一个是存储的时候使用,一个是检索的时候使用。

@Autowired
private EmbeddingModel embeddingModel;

@Bean
public EmbeddingStore store(){
    //1.加载文档进内存
    //List<Document> documents = ClassPathDocumentLoader.loadDocuments("content");
    //加载文档的时候指定解析器
    List<Document> documents = ClassPathDocumentLoader.loadDocuments("content",new ApachePdfBoxDocumentParser());
    //2.构建向量数据库操作对象  操作的是内存版本的向量数据库
    InMemoryEmbeddingStore store = new InMemoryEmbeddingStore();
    //构建文档分割器对象
    DocumentSplitter ds = DocumentSplitters.recursive(500,100);
    //3.构建一个EmbeddingStoreIngestor对象,完成文本数据切割,向量化, 存储
    EmbeddingStoreIngestor ingestor = EmbeddingStoreIngestor.builder()
            .embeddingStore(store)
            .documentSplitter(ds) 
            .embeddingModel(embeddingModel)
            .build();
    ingestor.ingest(documents);
    return store;
}

@Bean
public ContentRetriever contentRetriever(EmbeddingStore store){
    return EmbeddingStoreContentRetriever.builder()
            .embeddingStore(store)
            .minScore(0.5)
            .maxResults(3)
            .embeddingModel(embeddingModel)
            .build();
}

7.3.5向量数据库,我们之前配置的数据库是一个简单的基于内存的数据库:

如果使用内存向量数据库,一旦服务器重启数据就丢失了,得重新加载文档、重新向量化,这样每次启动都会比较耗时,每次启动都会使用百炼平台提供的向量模型完成向量化,是收费的,所以我们就要完成数据的一个持久化。常见的向量数据库有Milvus、Chroma、Pinecone、RediSearch以及pgvector, 用哪一种都行,LangChain4j对这些向量数据库都做了支持。这次采用redisearch存储向量数据。

step1:安装redisearch,不说了,很简单docker安装即可。

step2:引入依赖:

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

step3:配置向量数据库连接信息

注意这里的配置和之前配置的redis不相干,这里配置的是langchain4j.community下的,而之前配置的是spring.data下的。当引入的起步依赖检测这一段配置信息后,会自动的往IOC容器中注入一个RedisEmbeddingStore对象,这个对象实现了EmbeddingStore接口,封装了操作redissearch的API,我们可以直接使用。

langchain4j:
  community:
    redis:
      host: localhost
      port: 6379
      password: 1234
      index-name: "doc_index" # 自定义向量索引名
      batch-size: 10        # 每次仅提交 10 个片段

注意:需要配置index-name来指定我们向量化都存储的索引名,因为redis是索引数据结构,并且一定要配置batch-size,因为你的文本分割器之后的片段有很多,但是不可能一次全部处理完毕,会超出大模型的限制,所以配置这个

step4:注入并且配置

就是吧那个红色的框里面的换成我们自动注入的这个:

@Autowired
private RedisEmbeddingStore redisEmbeddingStore;

@Bean
public EmbeddingStore store(){//embeddingStore的对象, 这个对象的名字不能重复,所以这里使用store
    //1.加载文档进内存
    List<Document> documents = ClassPathDocumentLoader.loadDocuments("content",new ApachePdfBoxDocumentParser());

    //2.构建文档分割器对象
    DocumentSplitter ds = DocumentSplitters.recursive(500,100);
    //3.构建一个EmbeddingStoreIngestor对象,完成文本数据切割,向量化, 存储
    EmbeddingStoreIngestor ingestor = EmbeddingStoreIngestor.builder()
            //.embeddingStore(store)
            .embeddingStore(redisEmbeddingStore)
            .documentSplitter(ds)
            .embeddingModel(embeddingModel)
            .build();
    ingestor.ingest(documents);
    return redisEmbeddingStore;
}

@Bean
public ContentRetriever contentRetriever(/*EmbeddingStore store*/){
    return EmbeddingStoreContentRetriever.builder()
            .embeddingStore(redisEmbeddingStore)
            .minScore(0.5)
            .maxResults(3)
            .embeddingModel(embeddingModel)
            .build();
}

step5:小tips以上保存在了redissearch里,项目启动第一次的时候就已经完成了,之后不需要做了,所以启动结束后要把第一个配置数据库给注释掉,只保留第二个检索的bean即可。

总结:目前为止要使用一个成熟的RAG就是:之前的;

第一步就是创建一个springboot项目,注意版本。

第二步就是引入四个个依赖,一个是spring自动装配openai模型的依赖,一个是aiservice的依赖

        <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>

两个是流式处理的依赖

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

第三步就是去写一个yaml配置文件,里面就是自动装配的大模型的信息还有日志信息,当然这个是阿里云的格式,如果使用olama本地部署有自己的方式

langchain4j:
  open-ai:
    streaming-chat-model: #流式模型配置
      base-url: https://dashscope.aliyuncs.com/compatible-mode/v1
      api-key: ${API-KEY}
      model-name: qwen-plus
      log-requests: true #请求消息日志
      log-responses: true #响应消息日志
logging:
  level:
    dev.langchain4j: debug #日志级别

第四步就是写一个接口,里面就是一些要实先ai的接口,然后在这个类上面加aiservice的注解,当然我们不写那个指定模型的参数了,因为不写就是代表使用自动装配的模型。并且使用参数表示流式处理,然后模型的返回值要求flux。

@AiService(
        streamingChatModel = "openAiStreamingChatModel"
)
public interface ConsultantService {
    public Flux<String> chat(String message);
}

第五步就是在controller里面调用即可,很简单,要注意返回值类型也是Flux。

第六步:自己写一个ChatMemory接口的实现类,用于实现会话记忆的持久化

第七步:配置ChatMemoryProvider作为会话记忆隔离持久化对象

@Autowired
private ChatMemoryStore redisChatMemoryStore;//自己写好的实现类

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

第八步,在ai方法接口上参数chatMemoryProvider = "chatMemoryProvider"配置这个会话记忆对象,并且用@MemoryId指定那个是这个memoryid,@UserMessage指定那个是客户传过来的话,可以加上系统提示词

@AiService(
        streamingChatModel = "openAiStreamingChatModel",
        //chatMemory = "chatMemory",
        chatMemoryProvider = "chatMemoryProvider"//配置会话记忆对象
)
public interface ConsultantService {
    @SystemMessage(fromResource = "system.txt")
    public Flux<String> chat(@MemoryId String memoryId, @UserMessage String message);
}

加上:

第九步,看想使用什么类型的向量数据库,先安装,然后引入对应的依赖,就比如redisse

arch那个吧

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

第十步就是配置使用的向量模型

langchain4j:
  open-ai:
    streaming-chat-model: #流式模型配置
      base-url: https://dashscope.aliyuncs.com/compatible-mode/v1
      api-key: ${API-KEY}
      model-name: qwen-plus
      log-requests: true #请求消息日志
      log-responses: true #响应消息日志
  community:
    redis:
      host: localhost
      port: 6379
      password: 1234
      index-name: "doc_index" # 自定义向量索引名
      batch-size: 10        # 每次仅提交 10 个片段
logging:
  level:
    dev.langchain4j: debug #日志级别

第十一步就是去写两个Bean一个代表数据库,一个代表检索的,它们的基本构造如下:

@Autowired
private EmbeddingModel embeddingModel;
@Autowired
private RedisEmbeddingStore redisEmbeddingStore;

@Bean
public EmbeddingStore store(){//embeddingStore的对象, 这个对象的名字不能重复,所以这里使用store
    //1.加载文档进内存
    List<Document> documents = ClassPathDocumentLoader.loadDocuments("content",new ApachePdfBoxDocumentParser());

    //2.构建文档分割器对象
    DocumentSplitter ds = DocumentSplitters.recursive(500,100);
    //3.构建一个EmbeddingStoreIngestor对象,完成文本数据切割,向量化, 存储
    EmbeddingStoreIngestor ingestor = EmbeddingStoreIngestor.builder()
            //.embeddingStore(store)
            .embeddingStore(redisEmbeddingStore)
            .documentSplitter(ds)
            .embeddingModel(embeddingModel)
            .build();
    ingestor.ingest(documents);
    return redisEmbeddingStore;
}

@Bean
public ContentRetriever contentRetriever(/*EmbeddingStore store*/){
    return EmbeddingStoreContentRetriever.builder()
            .embeddingStore(redisEmbeddingStore)
            .minScore(0.5)
            .maxResults(3)
            .embeddingModel(embeddingModel)
            .build();
}

第十一步,在接口方法上声明检索对象(第二个配置的bean-ContentRetriever对象)

@AiService(
        streamingChatModel = "openAiStreamingChatModel",
        chatMemoryProvider = "chatMemoryProvider",//配置会话记忆提供者对象
        contentRetriever = "contentRetriever"//配置向量数据库检索对象
)
//@AiService
public interface ConsultantService {
    //用于聊天的方法  
    @SystemMessage(fromResource = "system.txt")
    public Flux<String> chat(@MemoryId String memoryId, @UserMessage String message);
}

8.Tools工具(function callin)

如果在的程序中添加了function calling功能,,那整个工作流程会发生一些改变

当用户把问题发送给AI应用,在AI应用的内部需要组织提交给大模型的数据(包括讲过的rag知识库),而这些数据中需要描述清楚我们的AI应用中有哪些函数能够被大模型调用,这就是tools工具。每一个函数的描述都包含三个部分,方法名称、方法作用、方法入参。当AI应用把这些数据发送给大模型后,大模型会先根据用户的问题以及上下文拆解任务,从而判断是否需要调用函数,如果有函数需要调用,则把需要调用的函数的名称,以及调用时需要使用的参数准备好一并响应给AI应用。AI应用接收到响应后需要执行对应的函数,得到对应的结果,接下来把得到的结果和之前信息一块组织好再发送给大模型。

这里需要注意的是由于在一次任务的处理过程中可能需要根据顺序调用多个函数,所以当大模型接收到AI应用发送的数据继续拆解任务,如果发现还需要调用其他的函数,则会重复4.1~4.4这几个步骤,直到无需调用函数,最终把生成的结果响应该AI应用,并由AI应用发送给用户。

这就是增加了function calling 或者 Tools工具后整个AI应用的工作流程,比之前要复杂不少,不过好消息是下面的这些工作LangChain4j都能帮我们自动的完成,我们只需要按照LangChain4j的规则描述清楚有哪些方法可以被大模型调用,方法名的名字是什么、有什么作用、以及都需要哪些参数

8.1准备基础:mysql的准备,引入依赖,配置连接信息,写pojo实体类,写三层接口

8.2准备工具方法:

LangChain4j提供了Tool注解用于对方法的作用进行描述,还有P注解用于对方法的参数进行描述,将来LangChain4j就能通过反射的方式获取到Tool注解中的作用描述、P注解中的参数描述、以及方法的名称,组织数据(通过用户给的信息,得到@P注解描述的信息,然后封装在后面的参数中),一并发送给大模型。

import com.itheima.consultant.pojo.Reservation;
import com.itheima.consultant.service.ReservationService;
import dev.langchain4j.agent.tool.P;
import dev.langchain4j.agent.tool.Tool;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;

import java.time.LocalDateTime;

@Component
public class ReservationTool {
    @Autowired
    //引入服务层
    private ReservationService reservationService;

    //1.工具方法: 添加预约信息
    @Tool("预约志愿填报服务")
    public void  addReservation(
            @P("考生姓名") String name,
            @P("考生性别") String gender,
            @P("考生手机号") String phone,
            @P("预约沟通时间,格式为: yyyy-MM-dd'T'HH:mm") LocalDateTime communicationTime,
            @P("考生所在省份") String province,
            @P("考生预估分数") Integer estimatedScore
    ){
        //构建实体类
        Reservation reservation = new Reservation(null,name,gender,phone, communicationTime,province,estimatedScore);
        reservationService.insert(reservation);
    }
    //2.工具方法: 查询预约信息
    @Tool("根据考生手机号查询预约单")
    public Reservation findReservation(@P("考生手机号") String phone){
        return reservationService.findByPhone(phone);
    }
}

8.3配置工具方法

配置的方法和之前的类似,在AiService注解中过一个叫做tools的属性完成配置,值写上包含了工具方法的Bean对象的名字即可。

@AiService(
        streamingChatModel = "openAiStreamingChatModel",
        chatMemoryProvider = "chatMemoryProvider",//配置会话记忆提供者对象
        contentRetriever = "contentRetriever",//配置向量数据库检索对象
        tools = "reservationTool"
)
//@AiService
public interface ConsultantService {
    //用于聊天的方法
    @SystemMessage(fromResource = "system.txt")
    public Flux<String> chat(@MemoryId String memoryId, @UserMessage String message);
}

总结:这块很简单那就是在原有的基础上,写一个tools工具类,然后类里面是方法,用@tools和@p注解标明详细信息,然后完成调用逻辑即可。然后在接口上面声明这个工具类即可。


Logo

有“AI”的1024 = 2048,欢迎大家加入2048 AI社区

更多推荐