首页技术栈归档照片墙音乐日记随想收藏夹友链留言关于

Spring AI 与 LangChain4j

写作时间:2026-07-07

SpringAi简介

‌Spring AI‌是一个面向AI工程的应用框架,旨在将Spring生态系统的设计原则(如可移植性和模块化)扩展至人工智能领域。Spring AI支持多种主流大模型平台,如OpenAI、Microsoft、Amazon、Google和Huggingface,并提供灵活的Prompt Template8和模型输出解析能力,支持多模态生成式AI能力,如对话、文生图、文生语音等‌12。

主要功能

  1. ‌多模态生成‌:支持对话、文生图、文生语音等多种模态的生成式AI能力。
  2. ‌大模型服务对接‌:提供多种大模型服务对接能力,包括业界大多数主流大模型服务。
  3. ‌灵活的Prompt Template和模型输出解析‌:支持灵活的Prompt Template和模型输出解析Output Parsing能力。
  4. ‌多模态生成式AI‌:支持同步和流式调用,同时也支持传递特定模型的定制参数。
  5. ‌RAG能力‌:支持RAG(Retrieval-Augmented Generation)能力的基础组件,包括DocumentLoader、TextSpillter、EmobeddingClient、VectorStore等。
  6. ‌AI Spring Boot Starter‌:支持AI Spring Boot Starter实现配置自动装配。

SpringAi官网:https://spring.io/projects/spring-ai

Spring集成OpenAi实现对话功能

在集成OpenAi需要设置key来源,可以从网站中注册获取

网站:https://api.xty.app/register?aff=PuZD

三个配置在Spring配置文件中如下

#配置的地址
spring.ai.openai.base-url=https://api.xty.app
#访问的key也就是秘钥
spring.ai.openai.api-key=sk-1xN8TRjXsEMC4iAceYJHUdeXvc83rYUa1ifNjmsBMVHiDc2V
#选择的jpt    
spring.ai.openai.chat.options.model=gpt-3.5-turbo

导入依赖

        <dependency>
            <groupId>org.springframework.ai</groupId>
            <artifactId>spring-ai-openai-spring-boot-starter</artifactId>
        </dependency>

进行简单测试

@RestController
@CrossOrigin
public class OpenAiController {


    private final OpenAiChatModel chatModel;

    @Autowired
    public OpenAiController(OpenAiChatModel openAiChatModel) {
        this.chatModel = openAiChatModel;
    }

    @GetMapping("/ai/generate")
    public Map<String,String> generate(@RequestParam(value = "message", defaultValue = "讲个笑话") String message) {
        return Map.of("generation", this.chatModel.call(message));
    }

    @GetMapping(value = "/ai/generateStream",produces = MediaType.TEXT_EVENT_STREAM_VALUE  )
    public Flux<String> generateStream(@RequestParam(value = "message", defaultValue = "讲个笑话") String message) {
//        Prompt prompt = new Prompt(new UserMessage(message));
        Flux<String> stream = chatModel.stream(message);
        return stream;
    }
}

测试结果

前端代码

<template>
  <div class="common-layout">
    <el-container>
      <el-header>
        <div style="text-align: center;">
         <h3> CHAT_JPT接入</h3>
        </div>


      </el-header>
      <el-main>
        <div style="text-align: center;">
          <span v-for="(val,i) in res" :key="i">{{val}}</span>
        </div>

      </el-main>
      <el-footer>
        <div style="text-align: center;">
          <el-input  v-model="msg" style="width: 240px" :autosize="{ minRows: 2, maxRows: 4 }" type="textarea"  placeholder="Please input"/>
        </div>
        <div style="text-align: center;margin-top: 10px;">
          <el-button type="success" @click="sendMsg">提问</el-button>
        </div>

      </el-footer>
    </el-container>
  </div>
</template>
<script setup lang="ts">
import { nextTick, reactive, ref } from "vue";
import { CircleCloseFilled } from '@element-plus/icons-vue'
import type { FormInstance } from 'element-plus'
import { ElMessageBox, ElMessage, ElNotification } from 'element-plus'
import http from "@/http";
const msg = ref('')
const res = ref([])
res.value = []
let count=0;
const sendMsg = () => {


  //接收后台的事件流
  let  source= new EventSource(`http://localhost:8084/ai/generateStream?message=${msg.value}`)
  source.onmessage=(event)=>{
    //由于EventSource不知道这个后台数据是否已经传输完成,回反复传输数据,但是会给你末尾两个空串

    if(event.data===null||event.data===''){
       count++;
    }
    //如果执行了两次关闭即可
    if(count==2){
      source.close();
      count=0;

    }
    res.value.push(event.data)
  }
}
</script>
<style scoped>

</style>

Spring集成OpenAi实现图片生成功能

#都可以不写
#开启AI图片功能,默认是开启的
spring.ai.openai.image.enabled=true
#生图用的模型,有默认模型
spring.ai.openai.image.options.model=

controller代码


@RestController
@CrossOrigin
public class OpenAiimagsController {

    @Autowired
   private OpenAiImageModel aiImageModel;


    @GetMapping(value = "/ai/image",produces = "text/html")
    public String getImage(@RequestParam  String image){
        //请求deepSeek模型
        ImageResponse response = aiImageModel.call(new ImagePrompt(image));
        //返回图片
        Image output = response.getResult().getOutput();
        //获取图片的地址
        String url = output.getUrl();
        //获取图片的base64编码
        return  "<img src='"+url+"'>";
    }
}

浏览器请求

ollama本地模型

安装ollama模型

官网:https://ollama.com/download/mac

github:https://gitcode.com/gh_mirrors/oll/ollama?utm_source=csdn_github_accelerator

安装

1.选择ollama安装路径

2.在该路径打开cmd命令输入OllamaSetup.exe /DIR=加上指定安装路径

3.安装成功输入ollama -v查看是否安装成功

4.由于ollama修改了位置,但是模型安装位置并没有修改还是安装在C盘,则需要配置环境变量,新建 OLLAMA_MODELS 环境变量,建议配置在系统变量中

5,以管理员方式打开CMD运行 setx OLLAMA_MODELS D:\ruanjian\OllamaModel :注意这里需要重启一下ollama,下载路径才生效

6.下载deepseek本地模型:ollama pull deepseek-r1:1.5b 正常下载到路径

7.模型正常启动 ollama pull deepseek-r1:1.5b 这个可以不pull可以直接run没有会直接下载

8.安装 Chatbox 可视化界面

官网:https://chatboxai.app/zh

下载之后直接双击选择安装路径即可

9.配置自己的本地模型即可

ollama 常用命令

ollama serve      # 启动服务端:启动 Ollama 后台服务(API 接口)
ollama create     # 创建模型:通过 Modelfile 自定义创建一个新模型
ollama show       # 查看信息:显示某个模型的详情、配置、系统提示词
ollama run        # 运行模型:直接启动聊天对话(最常用)
ollama stop       # 停止模型:停止正在后台运行的模型
ollama pull       # 拉取模型:从仓库下载模型到本地
ollama push       # 推送模型:把本地模型上传到 Ollama 仓库
ollama list       # 列出模型:查看本地已下载的所有模型
ollama ps         # 查看进程:查看当前正在运行中的模型
ollama cp         # 复制模型:复制一个模型并改名
ollama rm         # 删除模型:从本地删除模型(释放空间)
ollama help       # 帮助命令:查看所有命令说明

java连接本地模型

依赖

        <dependency>
            <groupId>org.springframework.ai</groupId>
            <artifactId>spring-ai-ollama-spring-boot-starter</artifactId>
            <version>1.0.0-M6</version>
        </dependency>

配置

spring:
  ai:
    ollama:
      base-url: http://localhost:11434
      chat:
        model: deepseek-r1:1.5b  # 与本地模型名称对应
@RestController
@RequestMapping("/ai")
public class ChatController {

    @Autowired
    private  OllamaChatModel chatClient;


     @GetMapping("/chat")
    public String chat(@RequestParam(value = "message") String message) {
            // 调用 ChatClient 生成响应,并以 Flux<String>(响应流)形式返回
          return   chatClient.call(message);
    }
}

大模型规范接口

目前大多数大模型都遵循OpenAI的接口规范,是基于Http协议的接口。因此请求路径、参数、返回值信息都是类似的,可能会有一些小的差别。具体需要查看大模型的官方API文档。

我们以DeepSeek官方给出的文档为例:

curl -X POST https://api.deepseek.com/chat/completions \
  -H "Content-Type: application/json" \
  -H "Authorization: Bearer <DeepSeek API Key>" \
  -d '{
          "model": "deepseek-chat",
          "messages": [
              {
                  "role": "system",
                  "content": "You are a helpful assistant."
              },
              {
                  "role": "user",
                  "content": "Hello!"
              }
          ],
          "stream": false
      }'

接口说明

  • 请求方式:通常是POST,因为要传递JSON风格的参数
  • 请求URL:与平台有关
    • DeepSeek官方平台:https://api.deepseek.com/chat/completions
    • 阿里云百炼平台:https://dashscope.aliyuncs.com/compatible-mode/v1
    • 本地ollama部署的模型:http://localhost:11434
  • 请求头:开放平台都需要提供API_KEY来校验权限,本地ollama则不需要
    • Content-Type: application/json,请求参数的格式,必须是application/json,稍后解释
    • Authorization: Bearer ,上一节创建的API_KEY
  • 请求参数:JSON格式:
    • {
          "model": "deepseek-chat",
          "messages": [
            {"role": "system", "content": "You are a helpful assistant."},
            {"role": "user", "content": "Hello!"}
          ],
          "stream": false
      }
      
    • model:模型名称,DeepSeek支持deepseek-reasoner和deepseek-chat两者模型

    • messages:发送给大模型的消息,[]是数组的意思,里面可以有多条消息。消息结构:

      • content:是消息的内容
      • role:消息的角色,有system、user、assisant三种角色
        • system:是给大模型设定一个角色,比如你让她扮演你的奶奶,让她哄你睡觉
        • user:就是用户提问的问题
        • assisant:是大模型的回答
    • stream:true,代表响应结果流式返回;false,代表响应结果一次性返回,但需要等待

注意,这里请求参数中的messages是一个消息数组,而且其中的消息要包含两个属性:

  • role:消息对应的角色
  • content:消息内容

其中System和User消息的内容,也被称为提示词(Prompt),也就是用户发送给大模型的指令。

  • System提示词,是系统指令,给大模型设定一个角色,比如你让她扮演你的奶奶,让她哄你睡觉
  • User提示词,是用户指令,也就是用户向大模型的提问或命令

提示词角色

通常消息的角色有三种:

角色 描述 示例
system 优先于user指令之前的指令,也就是给大模型设定角色和任务背景的系统指令 你是一个乐于助人的编程助手,你以小团团的风格来回答用户的问题。
user 终端用户输入的指令(类似于你在ChatGPT聊天框输入的内容) 写一首关于Java编程的诗
assistant 由大模型生成的消息,可能是上一轮对话生成的结果 注意,用户可能与模型产生多轮对话,每轮对话模型都会生成不同结果。

其中System类型的消息非常重要!影响了后续AI会话的行为模式。

会话记忆问题

大模型的API接口是"无状态"的,服务端不会记录用户请求的上下文。因此我们调用API接口与大模型对话时,每一次对话信息都不会保留,多次对话之间都是独立的,没有关联的。

因此大模型并不知道之前的聊天历史,也就是说大模型是没有记忆的。

测试,我询问AI一个问题:12个苹果分给3个人,每人能分几个?

AI的答案是:每人可以分到4个苹果。

我们接着问:如果是分给4个人呢?,由于AI没有记忆,它不知道我是接着上一题问的,因此不知道要分的是12个苹果,答案就有问题:

可以看到,AI完全不知道我们聊天的背景是上一次的分12个苹果。

那么,如何才能让AI具备记忆呢?

要想让大模型有记忆,必须在每次请求时,将之前所有对话的历史拼接好,传递给对话API接口。

要想让AI具备记忆,就必须把对话历史都添加到请求体中的messages数组中,像这样:

{
    "model": "deepseek-chat",
    "messages": [
      {"role": "system", "content": "You are a helpful assistant."},
      {"role": "user", "content": "12个苹果分给3个人,每人能分几个?直接告诉我答案"},
      {"role": "assistant", "content": "每人可以分到4个苹果。"},
      {"role": "user", "content": "如果是分给4个人呢?"}
    ],
    "stream": false
  }

结果

AI网址

准备工作:

问小白:https://www.wenxiaobai.com/chat/200006

sealos:https://hzh.sealos.run/signin

cursor:https://www.cursor.com/cn

Spring AI Alibaba

SpringAIAlibaba基本介绍

Spring AI Alibaba 是阿里云基于Spring AI 官方标准深度打造的企业级 AI 应用开发框架,核心价值是:一套标准 API,屏蔽不同大模型的底层差异,让 Java 开发者像写 Spring Boot 业务代码一样开发 AI 应用。

底层兼容 Spring AI 全量 API,无缝对接 Spring Boot、Spring Cloud 微服务生态,Java 开发者零学习成本迁移 深度整合阿里云灵积 DashScope 平台,原生支持通义千问全系列模型、DeepSeek 等国产主流大模型,国内访问稳定、合规性拉满 2026 最新版本核心升级:Agent Skills 技能体系、Multi-agent 多智能体并行执行、Graph 工作流并行条件边、异步工具执行、流式全链路优化,是国内 Java 生态最成熟的 Agent 开发框架 云原生友好:完美适配 Spring Boot Actuator、Micrometer 监控、Resilience4j 熔断降级,完全符合大厂生产级开发规范

SpringAIAlibaba基本使用

1.导入依赖

          <!-- Spring AI Alibaba 核心依赖 -->
        <dependency>
            <groupId>com.alibaba.cloud.ai</groupId>
            <artifactId>spring-ai-alibaba-agent-framework</artifactId>
            <version>1.1.2.0</version>
        </dependency>
        <dependency>
            <groupId>com.alibaba.cloud.ai</groupId>
            <artifactId>spring-ai-alibaba-starter-dashscope</artifactId>
            <version>1.1.2.1</version>
        </dependency>
            <!-- Spring Web 必备,提供HTTP接口能力 -->
            <dependency>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-starter-web</artifactId>
            </dependency>
            <!--  Lombok -->
            <dependency>
                <groupId>org.projectlombok</groupId>
                <artifactId>lombok</artifactId>
                <optional>true</optional>
            </dependency>

LangChain4j

LangChain4j介绍

LangChain4j 的目标是简化将大语言模型(LLM - Large Language Model)集成到 Java 应用程序中的过程。

官网:https://docs.langchain4j.dev

与大型语言模型和向量数据库的便捷交互

通过统一的应用程序编程接口(API),可以轻松访问所有主要的商业和开源大型语言模型以及向量数据库,使你能够构建聊天机器人、智能助手等应用。

专为 Java 打造

借助Spring Boot 集成,能够将大模型集成到ava 应用程序中。大型语言模型与 Java 之间实现了双向集成:你可以从 Java 中调用大型语言模型,同时也允许大型语言模型反过来调用你的 Java 代码

智能代理、工具、检索增强生成(RAG)

为常见的大语言模型操作提供了广泛的工具,涵盖从底层的提示词模板创建、聊天记忆管理和输出解析,到智能代理和检索增强生成等高级模式。

LangChain4j基本使用

配置

langchain4j:    #是框架的根配置节点,表明后续配置属于相关设置。
  open-ai:       #子节点,指定配置的是 OpenAI 兼容的模型服务
    chat-model:    #表示配置的是聊天模型相关参数
      base-url:     https://dashscope.aliyuncs.com/compatible-mode/v1
      #聊天模型的 API 基础地址
      api-key: sk-2b2747e2966c41718405c76318be157a
      #访问该 API 服务的身份验证密钥
      model-name: qwen-plus
      log-requests: true
      log-responses: true

logging:
  level:
    dev.langchain4j: debug

依赖

        <!-- 基于open-ai的langchain4j接口:ChatGPT、deepseek都是open-ai标准下的大模型 -->
        <dependency>
            <groupId>dev.langchain4j</groupId>
            <artifactId>langchain4j-open-ai-spring-boot-starter</artifactId>
          <version>1.0.0-beta3</version>
        </dependency>
        <!-- 覆盖logback的依赖配置,使用兼容 SLF4J 2.x 的版本 -->
        <dependency>
            <groupId>ch.qos.logback</groupId>
            <artifactId>logback-classic</artifactId>
            <version>1.5.18</version>
        </dependency>

测试代码

        //初始化模型
        OpenAiChatModel model = OpenAiChatModel.builder()
                //LangChain4j提供的代理服务器,该代理服务器会将演示密钥替换成真实密钥, 再将请求转发给OpenAI API
                .baseUrl("http://langchain4j.dev/demo/openai/v1") //设置模型api地址(如果apiKey="demo",则可省略baseUrl的配置)
                .apiKey("demo") //设置模型apiKey  //这里可以获取环境变量就是  直接配置在环境变量即可 .apiKey(System.getenv("API_KEY"));
                .modelName("gpt-4o-mini") //设置模型名称
                .logRequests(true)  //打印日志
                .logResponses(true)  //打印日志
                .build();

        //向模型提问
        String answer = model.chat("你好");
        //输出结果
        System.out.println(answer);

Ai Service基本使用

依赖

<!--        &lt;!&ndash; 基于open-ai的langchain4j接口:ChatGPT、deepseek都是open-ai标准下的大模型 &ndash;&gt;-->
        <dependency>
            <groupId>dev.langchain4j</groupId>
            <artifactId>langchain4j-open-ai-spring-boot-starter</artifactId>
            <version>1.0.1-beta6</version>
        </dependency>

        <!--Aiservice工具类-->
        <dependency>
            <groupId>dev.langchain4j</groupId>
            <artifactId>langchain4j-spring-boot-starter</artifactId>
            <version>1.0.1-beta6</version>
        </dependency>

config类

@Configuration
public class CommonConfig {

    @Autowired
    private OpenAiChatModel modesl;
    @Bean
    public ConsultantService consultantService(){
        ConsultantService consultantService = AiServices.builder(ConsultantService.class)
                .chatModel(modesl)
                .build();
        return consultantService;
    }
}

Service类

package com.yuan.langchain4j.aiservice;

public interface ConsultantService {

    //用于聊天的方法
    public String chat(String message);

}

Contorller类

@RestController
public class ChatController {

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

}

测试

声明式使用方式不需要在CommonConfig类中写这么麻烦,直接在接口@AiService就可以了

打开CommonConfig类,把consultantService方法注释掉

在ConsultantService接口中,类名的上方添加如下代码

@AiService(
        wiringMode = AiServiceWiringMode.EXPLICIT,//手动装配
        chatModel = "openAiChatModel",//指定模型
        )

也可以直接写@AiService 不用带括号里的东西。直接写这个@AiService注解是默认装配,可以不用多余的去写 wiringMode = AiServiceWiringMode.EXPLICIT

但是现在为了教学懂得其中原理,要一步一步人人真真的写过程,方便理解,不能简略。

@AiService
public interface ConsultantService {

    //用于聊天的方法
    public String chat(String message);

}

LangChain4j流式调用

流式调用的方法

首先添加如下依赖,点击maven刷新依赖

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

配置流式配置

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
      #配置流式配置如下
    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

修改Service配置

@AiService(
        wiringMode = AiServiceWiringMode.EXPLICIT,//手动装配
        chatModel = "openAiChatModel",//指定模型
        streamingChatModel = "openAiStreamingChatModel" //流式调用
)
//@AiService
public interface ConsultantService {
    //用于聊天的方法
//    public String chat(String message);
    public Flux<String> chat(String message);
}

修改Conterller

@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;
    }
}

LangChain4j消息注解

@SystemMessage通常用于标记系统级别的消息,这类消息一般由系统自身发出,用于提供背景信息、设定对话规则、定义 AI 的行为模式

@UserMessage用于标记用户发送的消息,即对话中用户输入的内容,是 AI 需要响应或处理的核心信息。例如用户提问 “如何设置快捷键”,这条内容会被标记为@UserMessage。

@UserMessage("你是东哥的助手小月月,温柔貌美又多金。{{it}}")

{{it}}是个占位符,比如实际传入的数据是 “今天天气真好” 那么最终生成的消息会是:

"你是东哥的助手小月月,温柔貌美又多金。今天天气真好"

如果想把{{it}}改成其他的,可以这样写 @V("msg")是一个自定义注解

public Flux<String> chat(@V("msg")String message);

把system.txt复制到resources包下

@AiService(
        wiringMode = AiServiceWiringMode.EXPLICIT,//手动装配
        chatModel = "openAiChatModel",//指定模型
        streamingChatModel = "openAiStreamingChatModel" //流式调用
)
//@AiService
public interface ConsultantService {
    //用于聊天的方法
//    public String chat(String message);
    @SystemMessage(fromResource = "system.txt")
    public Flux<String> chat(String message);
}

LangChain4j会话记忆

CommonConfig类中加入如下代码 用于让代码记住历史会话

  //构建回答记忆对象
    @Bean
    public ChatMemory chatMemory(){//用于管理聊天对话的上下文记忆。
        //采用窗口机制来管理对话历史,通过构建器模式(Builder Pattern)来进行实例化
        MessageWindowChatMemory memory = MessageWindowChatMemory.builder()
                .maxMessages(20)//记住最多20条对话
                .build();
        return memory;
    }

ConsultantService接口中的@AiService()中加入如下代码,记得前面多个逗号

 chatMemory = "chatMemory"//指定记忆对象

会话记忆隔离

在ConsultantService接口中的@AiService()中最后一行chatMemory

注释掉,添加如下一行

chatMemoryProvider = "chatMemoryProvider"//配置会话记忆提供者对象

ConsultantService接口中把chat方法修改为如下,注意@UserMessage只能在一个类中出现一次,否会报错

public Flux<String> chat(/*@V("msg")*/@MemoryId String memoryId,@UserMessage String message);

Flux:方法的返回类型。用于处理流式数据。这个方法可能先返回 “正在思考...”,过一会儿又返回 “你的问题是...”,最后返回 “答案是...”—— 这就是多个字符串按顺序异步过来的过程。

@MemoryId*:标记该参数用于标识会话记忆,框架会使用这个ID来识别和维护不同用户的会话上下文。*

String memoryId:用于传入会话ID,与@MemoryId注解配合使用。

CommonConfig类中加入如下方法

    @Bean
    public ChatMemoryProvider chatMemoryProvider(){
        ChatMemoryProvider chatMemoryProvider = new ChatMemoryProvider() {
            @Override
            public ChatMemory get(Object memoryId) {
                return MessageWindowChatMemory.builder()//使用构建器模式创建并返回一个MessageWindowChatMemory对象。
                        .id(memoryId)//设置唯一标识符为传入的memoryId,这样可以区分不同的对话会话
                        .maxMessages(20)//设置消息窗口的最大容量为20条,限制对话历史的长度
                        .build();//调用构建器的build()方法,完成MessageWindowChatMemory对象的创建
            }
        };
        return chatMemoryProvider;//结束匿名内部类的定义,并返回创建好的ChatMemoryProvider对象
    }

这段代码的主要作用是创建一个能够根据会话ID提供相应聊天记忆的服务提供者,具体有以下几个目的:

会话隔离:通过memoryId参数,为不同的用户或会话提供独立的对话记忆,确保对话上下文不会混淆。

记忆持久化:记忆对象使用Redis存储,能够将对话历史持久化到Redis数据库中,避免应用重启后对话历史丢失。

支持多用户场景:在多用户或多会话的场景下,能够为每个会话提供独立且持久的对话记忆,使AI服务能够记住与每个用户的对话历史。

在ChatController类中添加一个memoryId

Flux<String> result = consultantService.chat(memoryId,message);

pom中添加redis依赖

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>

配置redis连接信息,在application.yml中添加

spring:
  data:
    redis:
      host: localhost
      port: 6379
      password:123456

根据视频提示创建repository包,包下创建RedisChatMemoryStore类,实现ChatMemoryStore接口,按下Alt+Insert实现3个方法。嫌麻烦的直接复制如下代码

@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, Duration.ofDays(1));
    }
    @Override
    public void deleteMessages(Object memoryId) {
        redisTemplate.delete(memoryId.toString());
    }
}

在CommonConfig类中添加Redis存储对象

@Autowired
    private ChatMemoryStore redisChatMemoryStore;

.maxMessages(20)下方插入这行代码

 .chatMemoryStore(redisChatMemoryStore)//设置聊天内存存储库为RedisChatMemoryStore,用于存储和检索对话历史

@Repository注解的作用

主要用于标识数据访问层(DAO 层)负责与数据库进行交互,执行 CRUD(创建、读取、更新、删除)操作。当 Spring 容器进行组件扫描时,会检测带有@Repository注解的类,并将其注册为 SpringBean

LangChain4j RAG知识库

2.1.1****什么是RAG知识库?

RAG 是让 AI 查 “知识库” 回答的技术,不用重新训练 AI。它先把要参考的资料拆成小块存起来,方便快速找。用户提问时,先从存的资料里找出相关内容。把找到的资料和问题一起给 AI,让 AI 结合资料回答。这样 AI 回答更准,还能引用具体来源,避免瞎编。

2.1.2****什么是向量数据库?

**向量距离越小,相似度越大。**向量数据库就是专门存“向量”这种特殊数据的库。向量是把文字、图片这些东西转成的一串数字,能代表它们的特征。它不像普通数据库比文字、数字,而是算向量间的相似度,比如找和“猫”的向量最像的“橘猫”向量。查的时候快,还能精准匹配相关数据,常用来给AI找资料、做图片搜索这些场景。

先拿文档Document ,用文本分割器Text Splitter把它拆成小段Segments。这些小段送到嵌入模型Model Embedding,模型会把每段转成叫“嵌入”Embeddings的数字向量。最后,小段本身和对应的嵌入向量一起存到嵌入存储Embedding Store里,这样之后找相关内容时,就能通过向量快速匹配啦。

RAG知识库基本使用

引入依赖,刷新maven

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

加载知识数据文档,resources下创建content文件夹,文件夹下粘贴文件

CommonConfig类中添加如下代码

    //构建向量数据库操作对象
    @Bean
    public EmbeddingStore store(){//embeddingStore的对象, 这个对象的名字不能重复,所以这里使用store
        //1.加载文档进内存,从类路径下的 "content" 目录加载所有文档 将加载的文档存储在 documents 列表中
        List<Document> documents = ClassPathDocumentLoader.loadDocuments("content");
         //2.创建一个内存中的向量存储实例 提供存储向量数据的容器
        InMemoryEmbeddingStore store = new InMemoryEmbeddingStore();

        //3.构建一个EmbeddingStoreIngestor对象,完成文本数据切割,向量化, 存储
        EmbeddingStoreIngestor ingestor = EmbeddingStoreIngestor.builder()
                .embeddingStore(store)//为构建器设置之前创建的向量存储对象 告诉处理器要将向量存储到哪里
                .build();//创建 EmbeddingStoreIngestor 实例
        ingestor.ingest(documents);//完成从原始文档到向量数据的转换和存储
        return store;//返回配置好的向量存储对象 使其成为 Spring 容器中的 Bean
    }
    //构建向量数据库检索对象
    @Bean
    //定义一个返回 ContentRetriever 的方法,方法参数中注入了之前创建的EmbeddingStore对象 创建用于检索向量数据的对象
    public ContentRetriever contentRetriever(EmbeddingStore store){
        return EmbeddingStoreContentRetriever.builder()//创建检索器的构建器
                .embeddingStore(store)//设置检索器要使用的向量存储对象,告诉检索器从哪里获取数据
                .minScore(0.5)//设置检索 最小相似度 0.5
                .maxResults(3)//设置最多返回 3 条检索结果。控制返回结果的数量
                .build();//完成构建,返回配置好的 ContentRetriever 对象
    }

在ConsultantService接口中的@AiService()中最后一行添加如下一行

contentRetriever = "contentRetriever"//配置向量数据库检索对象

RAG数据库核心

文档加载器

Document Loader,用于把磁盘或者网络中的数据加载进程序

  1. FileSystemDocumentLoader,根据本地磁盘绝对路径加载
List<Document> documents = FileSystemDocumentLoader.loadDocuments("C:\\ideaProjects\\consultant\\src\\main\\resources\\content");
  1. classPathDocumentLoader,相对于类路径加载
List<Document> documents = ClassPathDocumentLoader.loadDocuments("content");
  1. UrlDocumentLoader,根据url路径加载

文档解析器/文本分割器

Document Parser,用于解析文档加载器加载进内存的内容,把数据转化成纯文本

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

ApachePdfBoxDocumentParser,解析PDF格式文件

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

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

解析pdf格式文件

        <!--pdf解析器依赖-->
        <dependency>
            <groupId>dev.langchain4j</groupId>
            <artifactId>langchain4j-document-parser-apache-pdfbox</artifactId>
            <version>1.0.1-beta6</version>
        </dependency>

把pdf文件复制到content包下

注意这个new ApachePdfBoxDocumentParser()只解析PDF格式文件

运行项目,前端测试pdf内容是否被大模型识别

文本分割器

DocuemntByParagraphSplitter,按照段落分割文本

DocumentByLineSplitter,按照行分割文本

DocumentBySentenceSplitter,按照句子分割文本

DocumentByWordSplitter,按照词分割文本

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

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

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

在CommonConfig类的构建向量数据库操作对象的方法中添加如下代码

 DocumentSplitter ds = DocumentSplitters.recursive( 500, 100);//创建一个递归文档分割器 用于将文档切分成较小的块
.documentSplitter(ds)//设置文档分割器 用于将文档切分成较小的块

向量模型

所谓向量是指这段语言的方向。比如“这个菜不好吃”和“我以后不想点这个菜”虽然表达不同 但是表达的意思是相似的。向量模型可以对文本片段闻闻尝尝知道这句话的感情色彩 真正意指。向量模型就是把文字变成一长串有意义的数字,让电脑能“算”出文字之间的相似度。苹果”和“橙子”的坐标很接近(都是水果,甜度酸度差不多),所以它们意思是相似的。

配置向量模型步骤

配置向量模型

text-embedding-v3

embedding-model: #配置向量模型
  base-url:     https://dashscope.aliyuncs.com/compatible-mode/v1
  api-key: sk-2b2747e2ae6c41718405c763108e157a            
  model-name: text-embedding-v3 #向量模型名,通用文本向量v3
  log-requests: true
  log-responses: true
  max-segments-per-batch: 10

CommonConfig类中插入如下代码

@Autowired
private EmbeddingModel embeddingModel;//向量模型

构建向量库操作对象和构建检索对象的方法种插入如下代码

.embeddingModel(embeddingModel)//设置嵌入模型 用于将文本转换为向量表示

向量化数据存到RedisSearch

为什么要把向量化数据存到*RedisSearch

RedisSearch 可实现毫秒级向量检索,能满足实时推荐、问答系统等低延迟场景需求,不用额外搭复杂向量数据库;相比专门的向量数据库按向量数量或查询次数收费,RedisSearch 可部署在自有服务器,长期使用能大幅降低付费成本,尤其数据量大时更明显;Redis支持把向量数据和元数据持久化到磁盘

步骤

打开Docker桌面端后打开CMD

停止原有的redis镜像: cmd运行

docker stop redis 

删除原有的redis镜像:

docker rm redis

删除有的redis容器

docker rm -f redis-vector

安装带有向量化功能的redis: ,不确定是否安装成功可以再运行一次,耐心等待5min

docker run --name redis-vector -d -p 6379:6379 redislabs/redisearch

在pom.xml中引入依赖

<!--引入langchain4j对于redis向量数据库的支持-->
<dependency>
    <groupId>dev.langchain4j</groupId>                        
    <artifactId>langchain4j-community-redis-spring-boot-starter</artifactId>
    <version>1.0.1-beta6</version>
</dependency>

application.yml中插入如下内容。注意,是在langchain4j的子节点,也就是和open-ai: 同级的节点

  community:
    redis:
      host: localhost
      port: 6379

在CommoConfig类下插入如下

@Autowired
private RedisEmbeddingStore redisEmbeddingStore;

这是以前的EmbeddingStore,只能操作内存版本的向量数据库。需要注释掉

//InMemoryEmbeddingStore store = new InMemoryEmbeddingStore();

现在我们要操作的是research,把后面的store都改成redisEmbeddingStore

并且把ContentRetriever方法参数注入的store对象注释掉

现在有个问题,百炼平台一次性吃进去的文档有限制,不可能所有pdf一次性喂进去。需要限制数量。在application.yml中添加如下内容

max-segments-per-batch: 10 # 每个批次最多处理的文本段数

还有个问题,每次启动项目,并没有新的志愿填报文件加进来。但是文本早就处理好了。每次启动一遍再处理一次没有意义。事实上不是每次启动项目,每次都会把处理好的文本段再次处理,那性能上太浪费了。事实上配置好Redis后,处理好的数据就存在这里。所以可以把@Bean注释掉。避免 Spring 容器在启动时自动执行该方法,从而防止重复处理已存入 Redis 的文本数据

@Bean注解用于告诉 Spring:“这个方法返回的对象需要被纳入容器管理,并且在容器启动时执行该方法来创建对象”。

LangChain4j Tools工具

说白话就是和AI对话提供预约,将数据存储到数据库中

SQL

create database  if  not exists volunteer;
use volunteer;
create table if not exists reservation
(
    id                 bigint  primary key auto_increment   not null comment '主键ID',
    name               varchar(50) not null comment '考生姓名',
    gender             varchar(2)  not null comment '考生性别',
    phone              varchar(20) not null comment '考生手机号',
    communication_time datetime    not null comment '沟通时间',
    province           varchar(32) not null comment '考生所处的省份',
    estimated_score    int         not null comment '考生预估分数'
)

pom.xml引入如下依赖

 <!--lombok-->
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
        </dependency>
        <!--mybatis-->
        <dependency>
            <groupId>org.mybatis.spring.boot</groupId>
            <artifactId>mybatis-spring-boot-starter</artifactId>
            <version>3.0.3</version>
        </dependency>
        <!--mysql-->
        <dependency>
            <groupId>com.mysql</groupId>
            <artifactId>mysql-connector-j</artifactId>
        </dependency>

如下内容粘贴到application.yml中。注意datasource:

是在spring节点下,mybatis:是在根节点

  datasource:
    driver-class-name: com.mysql.cj.jdbc.Driver #mysql驱动类
    url: jdbc:mysql://localhost:3307/volunteer?serverTimezone=Asia/Shanghai&useSSL=false&allowPublicKeyRetrieval=true
    username: root
    password: 1234
mybatis:
  configuration:
    map-underscore-to-camel-case: true #开启驼峰-→下划线 命名映射

新建pojo包,包下建Reservation类,作为预约信息的实体类

代码如下

@Data
@NoArgsConstructor
@AllArgsConstructor
public class Reservation {
    private Long id;
    private String name;
    private String gender;
    private String phone;
    private LocalDateTime communicationTime;
    private String province;
    private Integer estimatedScore;
}

新建mapper包,包下建ReservationMapper类,作用是sql语句操作数据库。类名上面加个@Mapper。 代码如下

   //1.添加预约信息
    @Insert("insert into reservation(name,gender,phone,communication_time,province,estimated_score) values(#{name},#{gender},#{phone},#{communicationTime},#{province},#{estimatedScore})")
    void insert(Reservation reservation);
    //2.根据手机号查询预约信息
    @Select("select * from reservation where phone=#{phone}")
    Reservation findByPhone(String phone);

新建service包,包下建ReservationService类,作用是处理预约信息的业务逻辑

类名上面加个注解@Service,让Spring理解是个服务类

类中代码如下

  @Autowired
    private ReservationMapper reservationMapper;
    //1.添加预约信息的方法
    public void insert(Reservation reservation) {
        reservationMapper.insert(reservation);
    }
    //2.查询预约信息的方法(根据手机号查询)
    public Reservation findByPhone(String phone) {
        return reservationMapper.findByPhone(phone);
    }

Tools工具 P22

ConsultantService接口上的@AiService()中添加如下一行,记得带逗号

tools = "reservationTool"//配置工具对象

新建tools包,包下建ReservationTool类。

大模型会从用户输入中提取对应信息,自动填充参数,调用方法时创建 Reservation 对象,再通过 reservationService 保存到存储中,实现自动处理预约的功能。

    @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") String communicationTime,
            @P("考生所在省份") String province,
            @P("考生预估分数") Integer estimatedScore
    ) {Reservation reservation = new Reservation(null, name, gender, phone, LocalDateTime.parse(communicationTime), province, estimatedScore);
        reservationService.insert(reservation);
    }

    //2.工具方法: 查询预约信息
    //@Tool 注解标明这是查询工具及功能,@P 注解说明参数需要考生手机号,大模型会从用户输入提取手机号,自动传入方法,调用 reservationService 的查询方法获取并返回预约单,实现自动查询功能。
    @Tool("根据考生手机号查询预约单")
    public Reservation findReservation(@P("考生手机号") String phone) {return reservationService.findByPhone(phone);}

@Tool("预约志愿填报服务")这个注解能让大模型能认出这个方法是可用工具,调用时会按指定名称来。目的是让大模型知道哪些方法能调用,方便在处理任务时,自动选用合适工具完成操作,不用手动写调用逻辑,让集成工具更方便。

@P("考生姓名") String name,这个注解能让大模型明白这个 String name 参数需要啥值,会从输入里提取 "考生姓名" 相关信息填进去。能让大模型自动识别并填充方法参数,不用手动处理参数映射

测试

LangChain4j完整代码

后端代码

依赖

 <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>
<!--        &lt;!&ndash; 基于open-ai的langchain4j接口:ChatGPT、deepseek都是open-ai标准下的大模型 &ndash;&gt;-->
        <dependency>
            <groupId>dev.langchain4j</groupId>
            <artifactId>langchain4j-open-ai-spring-boot-starter</artifactId>
            <version>1.0.1-beta6</version>
        </dependency>
        <!--Aiservice工具类-->
        <dependency>
            <groupId>dev.langchain4j</groupId>
            <artifactId>langchain4j-spring-boot-starter</artifactId>
            <version>1.0.1-beta6</version>
        </dependency>
        <!--langchain4j流式调用依赖-->
        <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>
        <!-- 覆盖logback的依赖配置,使用兼容 SLF4J 2.x 的版本 -->
        <dependency>
            <groupId>ch.qos.logback</groupId>
            <artifactId>logback-classic</artifactId>
            <version>1.5.18</version>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
      <!--redis-->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-redis</artifactId>
        </dependency>
<!--        &lt;!&ndash;引入langchain4j对于redis向量数据库的支持&ndash;&gt;-->
<!--        <dependency>-->
<!--            <groupId>dev.langchain4j</groupId>-->
<!--            <artifactId>langchain4j-community-redis-spring-boot-starter</artifactId>-->
<!--            <version>1.0.1-beta6</version>-->
<!--        </dependency>-->
        <!--引入知识库rag-->
        <dependency>
            <groupId>dev.langchain4j</groupId>
            <artifactId>langchain4j-easy-rag</artifactId>
            <version>1.0.1-beta6</version>
        </dependency>
        <!--pdf解析器依赖-->
        <dependency>
            <groupId>dev.langchain4j</groupId>
            <artifactId>langchain4j-document-parser-apache-pdfbox</artifactId>
            <version>1.0.1-beta6</version>
        </dependency>
        <!--lombok-->
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
        </dependency>
        <!--mybatis-->
        <dependency>
            <groupId>org.mybatis.spring.boot</groupId>
            <artifactId>mybatis-spring-boot-starter</artifactId>
            <version>3.0.3</version>
        </dependency>
        <!--mysql-->
        <dependency>
            <groupId>com.mysql</groupId>
            <artifactId>mysql-connector-j</artifactId>
        </dependency>
    </dependencies>

aiservice

package com.yuan.langchain4j.aiservice;
import dev.langchain4j.service.MemoryId;
import dev.langchain4j.service.SystemMessage;
import dev.langchain4j.service.UserMessage;
import dev.langchain4j.service.spring.AiService;
import dev.langchain4j.service.spring.AiServiceWiringMode;
import reactor.core.publisher.Flux;
@AiService(
        wiringMode = AiServiceWiringMode.EXPLICIT,//手动装配
        chatModel = "openAiChatModel",//指定模型
        streamingChatModel = "openAiStreamingChatModel" ,//流式调用
//        chatMemory = "chatMemory",//指定记忆对象
        chatMemoryProvider = "chatMemoryProvider",//配置会话记忆提供者对象
        contentRetriever = "contentRetriever",//配置向量数据库检索对象
        tools = "reservationTool"//配置工具对象
)
//@AiService
public interface ConsultantService {
    //用于聊天的方法
//    public String chat(String message);
    @SystemMessage(fromResource = "system.txt")
    public Flux<String> chat(/*@V("msg")*/@MemoryId String memoryId, @UserMessage String message);

}

配置类

package com.yuan.langchain4j.config;
import dev.langchain4j.data.document.Document;
import dev.langchain4j.data.document.DocumentSplitter;
import dev.langchain4j.data.document.loader.ClassPathDocumentLoader;
import dev.langchain4j.data.document.parser.apache.pdfbox.ApachePdfBoxDocumentParser;
import dev.langchain4j.data.document.splitter.DocumentSplitters;
import dev.langchain4j.memory.ChatMemory;
import dev.langchain4j.memory.chat.ChatMemoryProvider;
import dev.langchain4j.memory.chat.MessageWindowChatMemory;
import dev.langchain4j.rag.content.retriever.ContentRetriever;
import dev.langchain4j.rag.content.retriever.EmbeddingStoreContentRetriever;
import dev.langchain4j.store.embedding.EmbeddingStore;
import dev.langchain4j.store.embedding.EmbeddingStoreIngestor;
import dev.langchain4j.store.embedding.inmemory.InMemoryEmbeddingStore;
import dev.langchain4j.store.memory.chat.ChatMemoryStore;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

import java.util.List;

@Configuration
public class CommonConfig {
/*    @Autowired
    private OpenAiChatModel modesl;
    @Bean
    public ConsultantService consultantService(){
        ConsultantService consultantService = AiServices.builder(ConsultantService.class)
                .chatModel(modesl)
                .build();
        return consultantService;
    }*/

//构建回答记忆对象
//    @Bean
//    public ChatMemory chatMemory(){//用于管理聊天对话的上下文记忆。
//        //采用窗口机制来管理对话历史,通过构建器模式(Builder Pattern)来进行实例化
//        MessageWindowChatMemory memory = MessageWindowChatMemory.builder()
//                .maxMessages(20)//记住最多20条对话
//                .build();
//        return memory;
//    }

    @Autowired
    private ChatMemoryStore redisChatMemoryStore;
//    @Autowired
//    private EmbeddingModel embeddingModel;//向量模型

// 向量化数据存到RedisSearch
//    @Autowired
//    private RedisEmbeddingStore redisEmbeddingStore;

    @Bean
    public ChatMemoryProvider chatMemoryProvider(){
        ChatMemoryProvider chatMemoryProvider = new ChatMemoryProvider() {
            @Override
            public ChatMemory get(Object memoryId) {
                return MessageWindowChatMemory.builder()//使用构建器模式创建并返回一个MessageWindowChatMemory对象。
                        .id(memoryId)//设置唯一标识符为传入的memoryId,这样可以区分不同的对话会话
                        .maxMessages(20)//设置消息窗口的最大容量为20条,限制对话历史的长度
                        .chatMemoryStore(redisChatMemoryStore)//设置聊天内存存储库为RedisChatMemoryStore,用于存储和检索对话历史
                        .build();//调用构建器的build()方法,完成MessageWindowChatMemory对象的创建
            }
        };
        return chatMemoryProvider;//结束匿名内部类的定义,并返回创建好的ChatMemoryProvider对象
    }
    //构建向量数据库操作对象
    @Bean
    public EmbeddingStore store(){//embeddingStore的对象, 这个对象的名字不能重复,所以这里使用store
        //1.加载文档进内存,从类路径下的 "content" 目录加载所有文档 将加载的文档存储在 documents 列表中
       //  List<Document> documents = ClassPathDocumentLoader.loadDocuments("content");
       // Document Loader,用于把磁盘或者网络中的数据加载进程序FileSystemDocumentLoader,根据本地磁盘绝对路径加载
       // List<Document> documents = FileSystemDocumentLoader.loadDocuments("E:\\work\\Learningmaterials\\Learningmaterials\\扩展组件接入\\接入代码\\springbootqt\\LangChain4j\\src\\main\\resources\\content");
       //从类路径的 content 目录加载文件,用 PDF 解析器处理,结果存到 documents 列表里。
        List<Document> documents = ClassPathDocumentLoader.loadDocuments("content",new ApachePdfBoxDocumentParser());
        //2.创建一个内存中的向量存储实例 提供存储向量数据的容器
        InMemoryEmbeddingStore store = new InMemoryEmbeddingStore();
        //创建一个递归文档分割器 用于将文档切分成较小的块 最多500个字段重复字段为100
        DocumentSplitter ds = DocumentSplitters.recursive( 500, 100);
        //3.构建一个EmbeddingStoreIngestor对象,完成文本数据切割,向量化, 存储
        EmbeddingStoreIngestor ingestor = EmbeddingStoreIngestor.builder()
                .embeddingStore(store)//为构建器设置之前创建的向量存储对象 告诉处理器要将向量存储到哪里

//                . embeddingStore(redisEmbeddingStore)
                .documentSplitter(ds)//设置文档分割器
                .build();//创建 EmbeddingStoreIngestor 实例
        ingestor.ingest(documents);//完成从原始文档到向量数据的转换和存储
//        return redisEmbeddingStore;//返回配置好的向量存储对象 使其成为 Spring 容器中的 Bean
        return store;
    }
    //构建向量数据库检索对象
    @Bean
    //定义一个返回 ContentRetriever 的方法,方法参数中注入了之前创建的EmbeddingStore对象 创建用于检索向量数据的对象
    public ContentRetriever contentRetriever(EmbeddingStore store){
        return EmbeddingStoreContentRetriever.builder()//创建检索器的构建器
                .embeddingStore(store)//设置检索器要使用的向量存储对象,告诉检索器从哪里获取数据
                .minScore(0.5)//设置检索 最小相似度 0.5
                .maxResults(3)//设置最多返回 3 条检索结果。控制返回结果的数量
//                .embeddingModel(embeddingModel)//设置嵌入模型 用于将文本转换为向量表示
                .build();//完成构建,返回配置好的 ContentRetriever 对象
    }
}

Controller

package com.yuan.langchain4j.controller;
import com.yuan.langchain4j.aiservice.ConsultantService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import reactor.core.publisher.Flux;
@RestController
public class ChatController {
    @Autowired
    private ConsultantService consultantService;
    @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;
    }
}

mapper

package com.yuan.langchain4j.mapper;
import com.yuan.langchain4j.pojo.Reservation;
import org.apache.ibatis.annotations.Insert;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Select;
@Mapper
public interface ReservationMapper {
    //1.添加预约信息
    @Insert("insert into reservation(name,gender,phone,communication_time,province,estimated_score) values(#{name},#{gender},#{phone},#{communicationTime},#{province},#{estimatedScore})")
    void insert(Reservation reservation);
    //2.根据手机号查询预约信息
    @Select("select * from reservation where phone=#{phone}")
    Reservation findByPhone(String phone);
}

pojo

package com.yuan.langchain4j.pojo;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.time.LocalDateTime;
@Data
@NoArgsConstructor
@AllArgsConstructor
public class Reservation {
    private Long id;
    private String name;
    private String gender;
    private String phone;
    private LocalDateTime communicationTime;
    private String province;
    private Integer estimatedScore;
}

Repository

package com.yuan.langchain4j.repository;
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.StringRedisTemplate;
import org.springframework.stereotype.Repository;
import java.time.Duration;
import java.util.List;

/**
 * @Repository注解的作用
 * 主要用于标识数据访问层(DAO 层)负责与数据库进行交互,
 * 执行 CRUD(创建、读取、更新、删除)操作。当 Spring 容器进行组件扫描时,
 * 会检测带有@Repository注解的类,并将其注册为 SpringBean
 */
@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中  ,存储时间为1天
        redisTemplate.opsForValue().set(memoryId.toString(),json, Duration.ofDays(1));
    }
    @Override
    public void deleteMessages(Object memoryId) {
        redisTemplate.delete(memoryId.toString());
    }
}

service

package com.yuan.langchain4j.service;
import com.yuan.langchain4j.mapper.ReservationMapper;
import com.yuan.langchain4j.pojo.Reservation;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
@Service
public class ReservationService {
    @Autowired
    private ReservationMapper reservationMapper;
    //1.添加预约信息的方法
    public void insert(Reservation reservation) {
        reservationMapper.insert(reservation);
    }
    //2.查询预约信息的方法(根据手机号查询)
    public Reservation findByPhone(String phone) {
        return reservationMapper.findByPhone(phone);
    }
}

toos类

package com.yuan.langchain4j.tools;
import com.yuan.langchain4j.pojo.Reservation;
import com.yuan.langchain4j.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") String communicationTime,
            @P("考生所在省份") String province,
            @P("考生预估分数") Integer estimatedScore
    ){
        Reservation reservation = new Reservation(null,name,gender,phone, LocalDateTime.parse(communicationTime),province,estimatedScore);
        reservationService.insert(reservation);
    }
    //2.工具方法: 查询预约信息
    @Tool("根据考生手机号查询预约单")
    public Reservation findReservation(@P("考生手机号") String phone){
        return reservationService.findByPhone(phone);
    }
}

yml

langchain4j:
  #ai配置
  open-ai:
    chat-model:
      base-url: https://dashscope.aliyuncs.com/compatible-mode/v1
      api-key: sk-6c70387ce0c9401483bc2bd2cfd2779b
      model-name: qwen-plus
      log-requests: true
      log-responses: true
      #流式调用
    streaming-chat-model:
      base-url: https://dashscope.aliyuncs.com/compatible-mode/v1
      api-key: sk-6c70387ce0c9401483bc2bd2cfd2779b
      model-name: qwen-plus
      log-requests: true
      log-responses: true
      #配置向量模型
    embedding-model:
      base-url: https://dashscope.aliyuncs.com/compatible-mode/v1
      api-key: sk-6c70387ce0c9401483bc2bd2cfd2779b
      model-name: text-embedding-v3 #向量模型名,通用文本向量v3
      log-requests: true
      log-responses: true
      max-segments-per-batch: 10  #每个批次的最大段数为10个





logging:
  level:
    dev.langchain4j: debug

#redis配置
spring:
  data:
    redis:
      host: localhost
      port: 6379
      password:
  datasource:
    driver-class-name: com.mysql.cj.jdbc.Driver
    url: jdbc:mysql://localhost:3306/volunteer?serverTimezone=Asia/Shanghai&useSSL=false&allowPublicKeyRetrieval=true
    username: root
    password: root

前端代码

<!DOCTYPE html>
<html lang="zh-CN">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>AI志愿填报顾问</title>
    <link href="min.css" rel="stylesheet">
    <link rel="stylesheet" href="all_min.css">
    <script src="min.js"></script>
    <style>
        body {
            font-family: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, sans-serif;
        }

        /* 滚动条样式 */
        ::-webkit-scrollbar {
            width: 6px;
        }

        ::-webkit-scrollbar-track {
            background: #f1f1f1;
        }

        ::-webkit-scrollbar-thumb {
            background: #c1c1c1;
            border-radius: 3px;
        }

        ::-webkit-scrollbar-thumb:hover {
            background: #a8a8a8;
        }

        /* 输入框自适应高度 */
        textarea {
            min-height: 44px;
            max-height: 200px;
            transition: height 0.2s;
        }

        /* 加载动画 */
        @keyframes pulse {
            0%, 100% {
                opacity: 0.5;
            }
            50% {
                opacity: 1;
            }
        }

        .animate-pulse {
            animation: pulse 1.5s infinite;
        }

        .delay-100 {
            animation-delay: 0.1s;
        }

        .delay-200 {
            animation-delay: 0.2s;
        }

        /* 打字机效果 */
        .typing-cursor::after {
            content: "|";
            animation: blink 1s step-end infinite;
        }

        @keyframes blink {
            from, to {
                opacity: 1;
            }
            50% {
                opacity: 0;
            }
        }
    </style>
</head>
<body>
<div id="app" class="flex flex-col h-screen bg-gray-50">
    <!-- 顶部导航栏 -->
    <header class="bg-white shadow-sm py-3 px-4 flex items-center justify-between">
        <div class="flex items-center">
            <div class="text-xl font-bold text-blue-600">AI志愿填报顾问</div>
        </div>
        <div class="flex items-center space-x-3">
            <button
                    @click="startNewConversation"
                    class="ml-2 p-3 rounded-lg bg-green-500 hover:bg-green-600 text-white" style="width: 50px">
                <i class="fas fa-plus"></i>
            </button>
            <button @click="toggleDarkMode" class="p-2 rounded-full hover:bg-gray-100">
                <i :class="darkMode ? 'fas fa-moon text-gray-600' : 'fas fa-sun text-gray-600'"></i>
            </button>
        </div>
    </header>

    <!-- 聊天内容区域 -->
    <main class="flex-1 overflow-y-auto p-4 space-y-6" ref="chatContainer" :class="{ 'bg-gray-800': darkMode }">
        <div v-for="(message, index) in messages" :key="index" class="max-w-3xl mx-auto">
            <div :class="['flex', message.role === 'user' ? 'justify-end' : 'justify-start']">
                <div :class="['flex items-start space-x-3', message.role === 'user' ? 'flex-row-reverse space-x-reverse' : '']">
                    <div :class="['w-8 h-8 rounded-full flex items-center justify-center',
                                    message.role === 'user' ? 'bg-blue-100 text-blue-600' : 'bg-green-100 text-green-600',
                                    darkMode && message.role === 'assistant' ? 'bg-gray-700 text-green-400' : '']">
                        <i :class="message.role === 'user' ? 'fas fa-user' : 'fas fa-robot'"></i>
                    </div>
                    <div :class="['p-3 rounded-lg max-w-lg',
                                    message.role === 'user'
                                        ? 'bg-blue-500 text-white'
                                        : darkMode
                                            ? 'bg-gray-700 text-gray-100 border-gray-600'
                                            : 'bg-white shadow border border-gray-100']">
                        <div v-if="message.role === 'assistant' && message.isLoading" class="flex space-x-2">
                            <div :class="['w-2 h-2 rounded-full', darkMode ? 'bg-gray-400' : 'bg-gray-300', 'animate-pulse']"></div>
                            <div :class="['w-2 h-2 rounded-full', darkMode ? 'bg-gray-400' : 'bg-gray-300', 'animate-pulse delay-100']"></div>
                            <div :class="['w-2 h-2 rounded-full', darkMode ? 'bg-gray-400' : 'bg-gray-300', 'animate-pulse delay-200']"></div>
                        </div>
                        <div v-else class="whitespace-pre-wrap">
                                <span v-for="(char, charIndex) in message.content" :key="charIndex"
                                      :class="{'opacity-0': charIndex >= message.visibleChars, 'fade-in': charIndex < message.visibleChars}">
                                    {{ char }}
                                </span>
                            <span v-if="message.isStreaming" class="typing-cursor"></span>
                        </div>
                    </div>
                </div>
            </div>
        </div>
    </main>

    <!-- 输入框区域 -->
    <footer :class="['border-t p-4', darkMode ? 'bg-gray-800 border-gray-700' : 'bg-white border-gray-200']">
        <div class="max-w-3xl mx-auto relative">
            <div class="flex items-center">
                    <textarea
                            v-model="userInput"
                            @keydown.enter.exact.prevent="sendMessage"
                            @keydown.ctrl.enter.exact.prevent="sendMessage"
                            @keydown.esc.exact="stopResponse"
                            placeholder="输入您的问题..."
                            :class="['flex-1 border rounded-lg py-3 px-4 pr-12 focus:outline-none focus:ring-2 resize-none',
                                darkMode
                                    ? 'bg-gray-700 border-gray-600 text-white focus:ring-blue-400 placeholder-gray-400'
                                    : 'border-gray-300 focus:ring-blue-500 focus:border-transparent']"
                            rows="1"
                            ref="textarea"
                            @input="adjustTextareaHeight"
                    ></textarea>
                <!-- 新建会话按钮 -->

                <button
                        @click="isLoading ? stopResponse() : sendMessage()"
                        :disabled="!userInput.trim() && !isLoading"
                        :class="['ml-2 p-3 rounded-lg',
                                isLoading
                                    ? 'bg-red-500 hover:bg-red-600 text-white'
                                    : 'bg-blue-500 hover:bg-blue-600 text-white',
                                'disabled:opacity-50 disabled:cursor-not-allowed']"
                >
                    <i :class="isLoading ? 'fas fa-stop' : 'fas fa-paper-plane'"></i>
                </button>
            </div>
        </div>
    </footer>
</div>

<script>
    const {createApp, ref, nextTick, onMounted, watch} = Vue;

    createApp({
        setup() {
            const messages = ref([]);
            const userInput = ref('');
            const isLoading = ref(false);
            const chatContainer = ref(null);
            const textarea = ref(null);
            const darkMode = ref(false);

            const memoeryId = ref(Date.now().toString());
            let controller = null;
            let typingInterval = null;
            let currentTypingIndex = 0;

            // 调整文本区域高度
            const adjustTextareaHeight = () => {
                const textareaEl = textarea.value;
                textareaEl.style.height = 'auto';
                textareaEl.style.height = `${Math.min(textareaEl.scrollHeight, 200)}px`;
            };

            // 滚动到底部
            const scrollToBottom = () => {
                nextTick(() => {
                    if (chatContainer.value) {
                        chatContainer.value.scrollTop = chatContainer.value.scrollHeight;
                    }
                });
            };

            // 切换暗黑模式
            const toggleDarkMode = () => {
                darkMode.value = !darkMode.value;
                localStorage.setItem('darkMode', darkMode.value);
            };

            // 新建会话
            const startNewConversation = () => {
                // 清空聊天记录
                messages.value = [];
                // 生成新的 memoryId
                memoeryId.value = Date.now().toString();
                // 添加欢迎消息
                messages.value.push({
                    role: 'assistant',
                    content: '你好!我是传智教育提供的AI志愿填报顾问,请问有什么能帮到您?',
                    isLoading: false,
                    visibleChars: 0,
                    isStreaming: false
                });
                // 确保欢迎消息完全可见
                messages.value[0].visibleChars = messages.value[0].content.length;
                // 滚动到底部
                scrollToBottom();
                // 聚焦输入框
                nextTick(() => {
                    textarea.value.focus();
                });
            };

            // 模拟逐字打印效果
            const startTypingEffect = (messageIndex) => {
                const message = messages.value[messageIndex];
                if (!message || message.visibleChars >= message.content.length) {
                    clearInterval(typingInterval);
                    typingInterval = null;
                    messages.value[messageIndex].isStreaming = false;
                    return;
                }

                messages.value[messageIndex].visibleChars++;
                scrollToBottom();
            };

            // 发送消息
            const sendMessage = async () => {
                if (!userInput.value.trim() || isLoading.value) return;

                // 中止之前的请求
                if (controller) {
                    controller.abort();
                }
                controller = new AbortController();

                const userMessage = {
                    role: 'user',
                    content: userInput.value.trim(),
                    isLoading: false,
                    visibleChars: userInput.value.trim().length,
                    isStreaming: false
                };

                messages.value.push(userMessage);

                const assistantMessage = {
                    role: 'assistant',
                    content: '',
                    isLoading: true,
                    visibleChars: 0,
                    isStreaming: true
                };

                messages.value.push(assistantMessage);

                userInput.value = '';
                adjustTextareaHeight();
                scrollToBottom();

                isLoading.value = true;

                try {
                    const response = await fetch(`/chat?message=${encodeURIComponent(userMessage.content)}&memoryId=${memoeryId.value}`, {
                        signal: controller.signal
                    });

                    if (!response.ok) {
                        throw new Error(`HTTP error! status: ${response.status}`);
                    }

                    const reader = response.body.getReader();
                    const decoder = new TextDecoder('utf-8');
                    let buffer = '';
                    let messageIndex = messages.value.length - 1;

                    // 先清除之前的打字效果
                    if (typingInterval) {
                        clearInterval(typingInterval);
                        typingInterval = null;
                    }

                    // 开始流式处理
                    while (true) {
                        const {done, value} = await reader.read();
                        if (done) break;

                        const chunk = decoder.decode(value, {stream: true});
                        buffer += chunk;

                        // 直接更新内容
                        messages.value[messageIndex].content = buffer;
                        messages.value[messageIndex].isLoading = false;

                        // 启动打字效果
                        if (!typingInterval) {
                            typingInterval = setInterval(() => {
                                startTypingEffect(messageIndex);
                            }, 20); // 调整这个值可以改变打字速度
                        }

                        scrollToBottom();
                    }

                } catch (error) {
                    if (error.name === 'AbortError') {
                        console.log('请求被用户中止');
                    } else {
                        console.error('请求出错:', error);
                        const lastMessage = messages.value[messages.value.length - 1];
                        lastMessage.content = '抱歉,请求过程中出现错误: ' + error.message;
                        lastMessage.visibleChars = lastMessage.content.length;
                    }
                } finally {
                    const lastMessage = messages.value[messages.value.length - 1];
                    lastMessage.isLoading = false;
                    lastMessage.isStreaming = false;

                    // 确保所有字符都可见
                    if (lastMessage.visibleChars < lastMessage.content.length) {
                        lastMessage.visibleChars = lastMessage.content.length;
                    }

                    isLoading.value = false;
                    controller = null;

                    if (typingInterval) {
                        clearInterval(typingInterval);
                        typingInterval = null;
                    }

                    scrollToBottom();
                }
            };

            // 停止响应
            const stopResponse = () => {
                if (controller) {
                    controller.abort();
                    const lastMessage = messages.value[messages.value.length - 1];
                    lastMessage.isLoading = false;
                    lastMessage.isStreaming = false;

                    if (lastMessage.visibleChars < lastMessage.content.length) {
                        lastMessage.visibleChars = lastMessage.content.length;
                    }

                    isLoading.value = false;
                    controller = null;

                    if (typingInterval) {
                        clearInterval(typingInterval);
                        typingInterval = null;
                    }
                }
            };

            // 初始化
            onMounted(() => {
                // 检查暗黑模式偏好
                darkMode.value = localStorage.getItem('darkMode') === 'true' ||
                    (window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches);

                // 添加欢迎消息
                messages.value.push({
                    role: 'assistant',
                    content: '你好!我是传智教育提供的AI志愿填报顾问,请问有什么能帮到您?',
                    isLoading: false,
                    visibleChars: 0,
                    isStreaming: false
                });

                // 确保欢迎消息完全可见
                messages.value[0].visibleChars = messages.value[0].content.length;
                scrollToBottom();

                // 聚焦输入框
                nextTick(() => {
                    textarea.value.focus();
                });
            });

            // 监听消息变化自动滚动
            watch(messages, scrollToBottom, {deep: true});

            return {
                messages,
                userInput,
                isLoading,
                darkMode,
                chatContainer,
                textarea,
                sendMessage,
                stopResponse,
                toggleDarkMode,
                adjustTextareaHeight,
                startNewConversation
            };
        }
    }).mount('#app');
</script>
</body>
</html>
avatar

yuanyourdomain

写代码,做研究,记录生活。

RECOMMENDED

MyBatis 动态 SQL

2026-07-08

Nginx 基础入门

2026-07-08

Maven 多模块与私服

2026-07-08