LangChain4J项目实战:手把手教你从0到1实现大模型应用开发

本文涉及所有源码及笔记分享如下
链接: https://pan.baidu.com/s/1r1CJQxBXs2wFzt7Jr6g2Bg?pwd=eyww 提取码: eyww

大模型部署

本机ollama部署

  • 下载Ollama
  • 打开models
  • 下载qwen3:0.6b
  • 命令行运行

阿里云百炼平台

  • 打开apifox进行调用
  • 复制url到地址框,在header里面填入Authorization,注意不要忽略Bearer以及后面的一个空格

  • 填入json请求数据

大模型调用

常见参数

响应数据

  • 什么是token信息:在LLM中,Token是大模型处理文本的基本单位,可以理解为模型看得懂的最小文本片段,用户输入的内容都需要转换为token,才能让大模型更好的处理。文本要转换成token,需要分词器,对于相同的文本片段,不同的分词器转换成的token数量不同
    • 英文:1token=4字符
    • 中文:1汉字=1~2个token

LangChain4j

会话功能

快速入门

新建project,选择maven项目,选择jdk17,选择quickstart

package org.ljj;

import dev.langchain4j.model.openai.OpenAiChatModel;

public class App {
    public static void main(String[] args) {
        // 初始化OpenAiChatModel对象
        OpenAiChatModel model = OpenAiChatModel.builder()
                .baseUrl("https://dashscope.aliyuncs.com/compatible-mode/v1")
                .apiKey(System.getenv("DASHSCOPE_API_KEY")) // 配置到系统的环境变量里面
                .modelName("qwen-plus")
                .logRequests(true) // 详细输出请求信息
                .logResponses(true) // 详细输出响应信息
                .build();

        // 使用模型进行对话
        String result = model.chat("你是谁");
        System.out.println(result);
    }
}

<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
  xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
  <modelVersion>4.0.0</modelVersion>

  <groupId>org.ljj</groupId>
  <artifactId>langchain4j-demo</artifactId>
  <version>1.0-SNAPSHOT</version>
  <packaging>jar</packaging>


  <name>langchain4j-demo</name>
  <url>http://maven.apache.org</url>

  <properties>
    <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
    <maven.compiler.source>17</maven.compiler.source>
    <maven.compiler.target>17</maven.compiler.target>
  </properties>

  <dependencies>
    <dependency>
      <groupId>junit</groupId>
      <artifactId>junit</artifactId>
      <version>3.8.1</version>
      <scope>test</scope>
    </dependency>
    <!--langchain4j依赖-->
    <dependency>
      <groupId>dev.langchain4j</groupId>
      <artifactId>langchain4j-open-ai</artifactId>
      <version>1.0.1</version>
    </dependency>
    <!--打印日志信息-->
    <dependency>
      <groupId>ch.qos.logback</groupId>
      <artifactId>logback-classic</artifactId>
      <version>1.5.18</version>
    </dependency>
  </dependencies>
</project>

Spring整合LangChain4j

新建项目

选择Spring Web,因为我们要开发一个web项目访问浏览器

引入maven

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>
    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>3.5.4</version>
        <relativePath/> <!-- lookup parent from repository -->
    </parent>
    <groupId>com.ljj</groupId>
    <artifactId>langchain4j-consultant</artifactId>
    <version>0.0.1-SNAPSHOT</version>
    <name>langchain4j-consultant</name>
    <description>langchain4j-consultant</description>
    <url/>
    <licenses>
        <license/>
    </licenses>
    <developers>
        <developer/>
    </developers>
    <scm>
        <connection/>
        <developerConnection/>
        <tag/>
        <url/>
    </scm>
    <properties>
        <java.version>17</java.version>
    </properties>
    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>

        <!--langchain4j起步依赖-->
        <dependency>
            <groupId>dev.langchain4j</groupId>
            <artifactId>langchain4j-open-ai-spring-boot-starter</artifactId>
            <version>1.0.1-beta6</version>
        </dependency>
    </dependencies>

    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
            </plugin>
        </plugins>
    </build>

</project>

编写chatcontroller

package com.example.langchain4jconsultant.controller;

import dev.langchain4j.model.openai.OpenAiChatModel;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;


@RestController
public class ChatController {

    @Autowired
    private OpenAiChatModel model;

    @RequestMapping("/chat")
    public String chat(String message){
        return model.chat(message);
    }

}

编写application.yml

langchain4j:
  open-ai:
    chat-model:
      base-url: https://dashscope.aliyuncs.com/compatible-mode/v1
      api-key: ${DASHSCOPE_API_KEY}
      model-name: qwen-plus
      log-requests: true # 开启打印详细请求日志
      log-responses: true # 开启打印详细响应日志

# 日志配置为DEBUG级别
logging:
  level:
    dev.langchain4j: DEBUG

启动application应用,浏览器访问,可以看到响应

同时控制台有详细日志打印

AiServices工具类

之前我们调用大模型都是使用model.chat(message)的方式,这个方式不是很方便,因为我们之后在调用工具的时候需要配置很多的参数,但是langchain4j给我们提供了AIService工具类,封装了model对象和其他的一些操作

引入aiservices依赖

<!--langchain4j的AIService相关依赖-->
        <dependency>
            <groupId>dev.langchain4j</groupId>
            <artifactId>langchain4j-spring-boot-starter</artifactId>
            <version>1.0.1-beta6</version>
        </dependency>

创建如图所示的三个类

AIServices工具类也可以声明式使用

修改两个类:1.注释掉Config类中的consultantService方法,2.在ConsultantService类中进行修改

package com.example.langchain4jconsultant.aiservice;


import dev.langchain4j.service.spring.AiService;
import dev.langchain4j.service.spring.AiServiceWiringMode;

// 方式一:手动装配,需要自己指定参数,为方便学习理解,先采用这种方式
@AiService(
        wiringMode = AiServiceWiringMode.EXPLICIT, // 手动装配,表示将来AiService工具类为我们创建代理对象的时候,装配的时候是手动装配还是自动装配。EXPLICIT表示手动装配
        chatModel = "openAiChatModel" // 注入模型,langchain4j会注入名为openAiChatModel的bean作为聊天模型,默认是类名的首字母小写
)

// 方式二:自动装配(两种方式二选一) 直接在接口类上方写一个注释@AiService
//@AiService
public interface ConsultantService {
    // 用于聊天的接口
    public String chat(String message);
}

流式调用

之前我们在yml文件中写的是chat-model,那么langchain4j就会为容器自动注入一个名为"OpenAiChatModel"的对象,这种类型的对象都是阻塞式调用

但是当我们改为"streaming-chat-model",那么就会注入一个OpenAiStreamChatModel对象,该对象调用的模型都是流式调用

和前端对接,写好前端页面代码之后直接访问http://localhost:8080/index.html

<!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="https://cdn.jsdelivr.net/npm/tailwindcss@2.2.19/dist/tailwind.min.css" rel="stylesheet">
    <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.0.0-beta3/css/all.min.css">
    <script src="https://cdn.jsdelivr.net/npm/vue@3.2.31/dist/vue.global.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>

执行效果:

消息注解

如果我们规定这个顾问只能回答志愿填报相关的问题,而对其他问题不予回答,那么我们就必须采用系统消息的消息注解

langchain4j中提供了两个注解来设置系统消息:一个是SystemMessage、一个是UserMessage

  • SystemMessage

第一种方式,直接写到括号里面;第二种方式,当我们的系统消息过长,我们可以把他们写到txt文件中再进行读取。

在ConsultantService的方法上面,进行注解

或者准备一个外部文件

  • UserMessage

langchain4j有两种用户消息的定义方式,一种是在括号内部写**{{it}}**,注意字母不能变;如果不想写it,就可以采用第二种方式,写个@V手动起名字,并且保证两处名字一致,就能获取得到。

我们可以看到他的回复并没有之前我们使用SystemMessage时候表现的出色,这是因为我们发送的是User消息

我们使用用户消息的第二种方式

会话记忆

初步实现会话记忆

大模型是不具备记忆能力的,要想让大模型记住之前聊天的内容,唯一的办法就是把之前的聊天内容与新的提示词一起发送给大模型

在之前我们为了演示记忆效果,采取手动拼接消息,现在我们采用langchain框架,可以实现自动记录聊天消息并且发送

整体流程如图所示

浏览器用户输入一条消息,web后端有一个存储容器,这条消息经过web后端被存到存储对象当中,再被发送给大模型,大模型做出响应,比如说“是的”,此时还是需要先经过web后端,存储到存储容器当中,再连并之前的消息一起发送给浏览器,这样循环往复,每一次消息的对话内容都能被双方完整接受到,实现了“记忆”的效果

关键是在配置类中配置的MessageWindowChatMemory会话记忆对象,然后在AiService注解中配置

效果可以看到真正实现了会话记忆,并且从控制台可以看到之前的对话都被当做记忆进行了携带

会话隔离

到此为止仅仅是实现了会话记忆,但是还有一个重要的点就是要实现会话隔离,不然即便在不同的用户端发送消息,所有用户的消息都会被当成一个用户的消息进行传输,如下面两张图片所示:在第二张图中我并没有说是哪个大学,但是他直接获取到之前的记忆进行了回答

出线上面的原因就是因为我们只向IOC容器中注入了一个chatMemory对象,所有会话使用的是同一个记忆对象,因此不同会话之间的记忆并没有做到隔离,通过下面的方式才能真正做到会话隔离

解决办法

定义会话对象提供者chatMemoryProvider,注意不是直接定义会话记忆对象,将来如果langchain4j没有找到指定id1的chatMemory对象,chatMemoryProvider就会调用get方法来获取一个新的chatMemory对象使用,参数就是Object类型的memoryId

在单击前端页面的加号,就会触发startNewConversation,该按钮以当前的时间戳来定义一个新的会话id,如果不单击这个按钮就不会新建会话,还是会沿用之前的id

控制台中信息可以看出成功做了隔离

持久化记忆

我们发现只有后端一重启服务,用户的会话信息就丢失了,在实际开发过程中,后端服务重启是很常见的,不能因为重启后端服务而丢失用户信息,下面是重启丢失的原因:

MessageWindowChatMemory这个对象内部有一个ChatMemoryStore成员变量,所以我们实现message的add方法底层其实是store帮我们实现的,所以我们需要分析这个store是如何帮我们实现的,ChatMemoryStore是一个接口,它里面提供了三个方法分别用于根据id得到、更新、删除会话记录,langchain4j给这个接口提供了两个实现类,一个是InMemoryChatMemoryStore,一个是SingleSlotChatMemoryStore;在我们的MessageWindowChatMemory的store中,默认使用的是SingleSlotChatMemoryStore存储会话记录,在其中维护了一个messages集合来存储会话对象,很明显是服务器内存存储,所以一旦当服务器重启,这些消息必然丢失,我们不能把消息存储在内存中,而是需要把他们存储在redis、mysql等地方

最直观的方法就是不让MessageWindowChatMemory使用SingleSlotChatMemoryStore来存储会话,我们要自己提供一个chatMemoryStore的实现类,在其中我们自己把消息存储到其他地方,然后再把我们自己写的chatMemoryStore的实现类对象交给MessageWindowChatMemory来使用即可

我们使用redis来实现

代码环节:

首先安装docker,之后配置镜像源,其次下载redis镜像

docker run --name redis -d -p 6379:6379 redis

docker ps查看已经启动的镜像

启动之后回到idea

下载依赖

<!--redis依赖-->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-redis</artifactId>
        </dependency>
# yml文件中补充redis
spring:
  data:
    redis:
      host: localhost
      port: 6379

创建一个repository包,在里面创建一个RedisChatMemoryStore.java

package com.example.langchain4jconsultant.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;

// 重写ChatMemoryStore接口,不在使用默认的SingleSlotChatMemoryStore
@Repository
public class RedisChatMemoryStore implements ChatMemoryStore {

    @Autowired
    private StringRedisTemplate stringRedisTemplate;

    @Override
    public List<ChatMessage> getMessages(Object memoryId) {

        // 根据memoryId从Redis中获取消息
        String json = stringRedisTemplate.opsForValue().get(memoryId.toString());
        // 把json数据转换成List<ChatMessage>类型,需要用到反序列化
        List<ChatMessage> chatMessages = ChatMessageDeserializer.messagesFromJson(json);
        return chatMessages;
    }

    @Override
    public void updateMessages(Object memoryId, List<ChatMessage> list) {
        // 更新会话消息
        // 1.把list数据转换成json数据
        String json = ChatMessageSerializer.messagesToJson(list);
        // 2.使用stringRedisTemplate的opsForValue().set()方法存储到Redis中, 存储时间设置为1天
        // memoryId是键,注意:memoryId需要转换成String类型,因为Redis的key是String类型
        stringRedisTemplate.opsForValue().set(memoryId.toString(), json, Duration.ofDays(1));
    }

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

然后在commonConfig中进行修改

可以看到成功在redis中存储了会话记录,键名就是memoryId

RAG知识库

原理

大模型训练用的是通用知识库,一般知识仅仅更新到几年前,为了获取最新消息,需要外挂一个知识库

整体原理就是我们将一个文档(document),通过文本分割器(text splitter),把大的文档分成小的片段(segments),然后这些小的文本片段需要用一种专门的模型叫做向量模型(Embedding model 专门擅长文本向量化的模型),把一个个的文本片段转换成向量,然后我们只需要把文本片段和向量一起存储到向量数据库中即可

也就是下面的流程:

用户向模型提问并且获得回答的流程如图所示:

用户输入的问题被向量模型转换为向量,再拿着去向量数据库中的每一条记录通过余弦相似度进行比对,把满足要求的向量筛选出来得到对应的文本片段,最后再把用户的问题以及文本片段共同组成要提交给大模型的消息,让大模型根据消息生成回答即可

快速入门

  • 存储(构建向量数据库操作对象,把数据向量化之后存储到向量数据库中)

第一步引入依赖

第二步将本地存储知识的文档加载到内存当中,每一个文档都会生成一个Document对象用来记录文档的内容,

第三步构建向量数据库操作对象,langchain4j给我们提供了一个操作内存版本的向量数据库的类,这个类叫做InMemoryEmbeddingStore,

第四步把内存中的文本对象切割成一个一个的小文本片段,然后使用向量模型把这些片段存储到向量数据库之后,langchain4j为我们提供了一个类叫做EmbeddingStoreIngestor,它把很多操作细节都封装起来了,可以帮我们快速完成这一步的操做,我们需要构建这个对象,构建的时候需要告诉他我要把向量化的数据存储到哪里,也就是把我们刚才创建的InMemoryEmbeddingStore这个对象设置进去就可以了,然后调用ingest方法,把documents对象传进去,方法内部会使用内置的文本分割器先分隔,然后调用内置的向量模型向量化,把结果存储到向量数据库当中。

  • 检索(构建向量数据库检索对象,用于从向量数据库中检索出对于的文本片段)

首先构建向量数据库检索对象,langchain4j提供的检索对象叫做EmbeddingStoreContentRetrieve,对象调用三个方法:

第一个是embeddingstore告诉对象从哪里检索

第二个调用minStore方法设置最小的余弦相似度的值,检索的时候会把用户的问题向量化,然后与数据库中已经存在的向量计算余弦相似度,值越大,相似度越高,使用minStore设置一个值可以保证检索出来的值和用户的问题相关度很高

第三个maxResult是设置最大的检索出来的文本数量,会选取分数(相关性)最高的几个片段,因为如果把所有检索出来的都发送个大模型,那么token消耗很大,并且一些分数很低的片段被发送给大模型会影响大模型最终生成的结果

最后在@AiService注解中配置我们的retriever

代码部分:

  • 在Config中进行编辑,两个方法,一个是用于向量存储的EmbeddingStore,一个是用于检索的ContentRetriever

  • 在AiService注解中进行retriever的配置

核心API

整体流程:

文档加载器

文档解析器

    // 存储:构建EmbeddingStore对象(向量数据库操作对象)
    @Bean
    public EmbeddingStore store(){
        // 1.加载文档进内存
//        List<Document> documents = ClassPathDocumentLoader.loadDocuments("content");
        List<Document> documents = ClassPathDocumentLoader.loadDocuments("content", new ApachePdfBoxDocumentParser());
//        List<Document> documents = FileSystemDocumentLoader.loadDocuments("D:\\java_code\\langchain4j-consultant\\src\\main\\resources\\content");
        //2.构建向量数据库操作对象
        InMemoryEmbeddingStore store = new InMemoryEmbeddingStore();
        //3.构建一个EmbeddingStoreIngetor
        EmbeddingStoreIngestor ingestor = EmbeddingStoreIngestor.builder()
                .embeddingStore(store)
                .build();
        ingestor.ingest(documents);
        return store;
    }
文本分割器

注意各种文本分割器虽然是按照不同的标准(段落,行,句子等等)对文本进行的分割,但是分割后的结果并不是进行向量化的文本片段,而我们要进行向量化的文本片段是根据这六部分组合而来的,通常情况下,langchian4j是允许我们指定文本片段的字符容量的。

比如我们按照段落分割文本:默认单个文本片段的最大字符数量是300,在组合文本片段的时候,一个文本片段可以放多个原来的段落,但是所有段落的字数综合加起来不能超过300

这里我们针对最后一种DocumentSplitters.recursive(…)进行讨论,他是递归分割器,他组合了段落分割器、行分割器、句子分割器和词分割器,会按照优先级进行分割。比如上图还想再放入第三个段落是放不下的,他就会进一步把这个段落分成行,如果还是放不下就分成句子,还是放不下就分成词语。

如果我们不想设置默认字符数300,我想多设置一些字符,我们可以切换文本分割器。一共有两步骤:1、构建文本分割器对象,2、设置文本分割器对象

设置片段之间的重叠字符的个数是为了保证片段之间的连贯性。

CommonConfig.java

向量模型

使用通义千问为我们提供的向量模型,还是先=需要在yml文件中进行指定

之后把该对象交给EmbeddingStoreIngestor和EmbeddingStoreContentRetriver即可

代码部分:

  • yml文件进行补充

向量数据库操作

首先下载镜像

下载依赖之后,修改yml文件:

修改代码

    @Autowired
    private RedisEmbeddingStore redisEmbeddingStore; // 使用Redis作为向量数据库

    // 存储:构建EmbeddingStore对象(向量数据库操作对象)
    @Bean
    public EmbeddingStore store(){
        // 1.加载文档进内存
//        List<Document> documents = ClassPathDocumentLoader.loadDocuments("content");
        List<Document> documents = ClassPathDocumentLoader.loadDocuments("content", new ApachePdfBoxDocumentParser());
//        List<Document> documents = FileSystemDocumentLoader.loadDocuments("D:\\java_code\\langchain4j-consultant\\src\\main\\resources\\content");
//        List<Document> documents = FileSystemDocumentLoader.loadDocuments("D:\\java_code\\langchain4j-consultant\\src\\main\\resources\\content", new ApachePdfBoxDocumentParser());
        //2.构建向量数据库操作对象,操作的是内存版本的向量数据库
//        InMemoryEmbeddingStore store = new InMemoryEmbeddingStore();

        // 构建文本分割器对象
        DocumentSplitter ds = DocumentSplitters.recursive(300, 100);

        //3.构建一个EmbeddingStoreIngetor
        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()
                .maxResults(3)
                .minScore(0.5)
//              .embeddingStore(store)
                .embeddingStore(redisEmbeddingStore)
                .embeddingModel(embeddingModel)
                .build();
    }

启动之后,效果如图所示

当我们把所有的pdf文档放进去之后,启动会报错

原因是百炼平台对一次性发送给大模型要向量化处理的文本片段的数量进行了限制,不限制的话处理过程和占用内存很大

那么我们就需要在代码中进行配置max-segment-per-batch,告诉后端服务一次最多给向量模型发送几个片段,一次发不完,多发几次就可以了

可以看到成功存储了很多向量化之后的片段

遗留问题

因为我们给方法标注了@Bean注解,意味着每一次我们启动应用,方法就会重启一次,那么就会重新进行分段、向量化等操作(如CommonConfig中的store、contentRetriever方法),十分浪费资源,因为我们已经把向量化之后的数据存储到外部向量数据库中了(Redis中已经有数据),所以我们就不需要让这个方法每次都执行了,那我们就可以把方法上面的@Bean注释掉,这些方法在重启之后就不会被二次执行了

但是上面这种节约资源的方法还是需要在有@Bean的情况下第一次执行了向量化存储之后才可以采纳(取消@Bean注解)

Tools工具

以前叫做Function Calling,现在叫做Toolings

准备工作

mysql数据库

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文件增加三个依赖

yml文件配置mysql数据库连接信息、驼峰命名自动转换

配置实体类

package com.example.langchain4jconsultant.pojo;

import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;

import java.time.LocalDateTime;

@Data // 生成get/set方法
@NoArgsConstructor // 无参构造函数
@AllArgsConstructor // 全参构造函数
public class Reservastion {

    // bigint用Long
    private Long id;
    private String name;
    private String gender;
    private String phone;
    // datetime用LocalDateTime
    private LocalDateTime communicationTime;
    // int用Integer
    private Integer estimatedScore;
}

配置mapper

package com.example.langchain4jconsultant.mapper;

import com.example.langchain4jconsultant.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);
}

配置service,通常配置service需要配置接口和impl实现类,但是这里比较简单,所以直接写成实现类

package com.example.langchain4jconsultant.service;

import com.example.langchain4jconsultant.mapper.ReservationMapper;
import com.example.langchain4jconsultant.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 addReservation(Reservation reservation) {
        reservationMapper.insert(reservation);
    }


    // 2.根据手机号查询预约信息
    public Reservation getReservationByPhone(String phone) {
        return reservationMapper.findByPhone(phone);
    }


}

测试——单元测试

package com.example.langchain4jconsultant;


import com.example.langchain4jconsultant.pojo.Reservation;
import com.example.langchain4jconsultant.service.ReservationService;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;

import java.time.LocalDateTime;

@SpringBootTest
public class ReservationServiceTest {

    @Autowired
    private ReservationService reservationService;

    // 测试添加
    @Test
    void testAddReservation() {
        // 创建一个Reservation对象
        Reservation reservation = new Reservation(null, "张三", "男", "12345678901", LocalDateTime.now(), "北京", 5);
        // 调用ReservationService的addReservation方法
        reservationService.addReservation(reservation);
    }

    // 测试查询
    @Test
    void testGetReservationByPhone() {
        // 调用ReservationService的getReservationByPhone方法
        Reservation reservation = reservationService.getReservationByPhone("12345678901");
        // 验证返回的结果是否正确
        if (reservation != null) {
            System.out.println("预约信息: " + reservation);
        } else {
            System.out.println("未找到预约信息");
        }
    }

}

原理

为了实现实时将用户的数据存入数据库,就需要AI能够自动调用函数接口实现数据的存储

当我们加入Tools之后,整个工作流程就会发生一些改变:当用户把问题发送给AI应用,在AI应用的内部需要组织交给大模型的数据,而这些数据里边,需要描述清楚我们AI应用中有哪些函数能够被大模型调用。每一个函数的描述需要包含三个部分,分别是方法的名称、方法的作用以及方法的入参。当AI应用把这些数据发送给大模型了以后,大模型会根据用户的问题以及上下文去拆解任务,从而判断是否需要调用函数。如果有函数需要调用,则需要把调用的函数的名称以及调用时需要用到的参数给它准备好,一并响应给AI应用。AI应用接收到响应了之后,需要执行对应的函数并得到结果。接下来需要把得到的结果和之前的信息一块儿组织好,再发送给大模型应用。需要注意的是,由于在一次任务的处理过程中可能需要根据顺序调用多个函数,所以当大模型接收到AI应用发送的数据之后,它会继续拆解任务,如果发现还要调用其他的函数,那这一块就会重复4.1~4.4这几个步骤,直到无需函数的调用,最终把生成的结果响应给AI应用,然后呢,由AI应用再发送给用户。

这就是增加了function calling或者是tools工具之后整个AI应用的工作流程,好消息是,下面这些工作launchain4j都能够帮我们自动的完成。对于我们来说,只需要按照langchain4j的规则描述清楚有哪些方法可以被大模型调用,方法的名字是什么,方法的作用是什么,以及都需要哪些参数。

实现

@Tool注解用于对方法的作用进行描述

@P注解用于对方法的参数进行描述

还要在tool方法的上面添加@Component以便于被IOC容器识别

package com.example.langchain4jconsultant.tool;

import com.example.langchain4jconsultant.pojo.Reservation;
import com.example.langchain4jconsultant.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;


    // 工具方法:添加预约信息
    @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.addReservation(reservation);
    }


    // 工具方法:根据手机号查询预约信息
    @Tool("根据考生手机号查询预约信息")
    public Reservation getReservationByPhone(
            @P("考生手机号") String phone
    ) {
        return reservationService.getReservationByPhone(phone);
    }


}

下一步我们需要配置工具方法,我们需要把上述工具方法告诉langchain4j,这样才能自动找到工具方法,根据反射获取这些信息,最后组织数据发送给大模型

@AiService(
        wiringMode = AiServiceWiringMode.EXPLICIT, // 手动装配,表示将来AiService工具类为我们创建代理对象的时候,装配的时候是手动装配还是自动装配。EXPLICIT表示手动装配
        chatModel = "openAiChatModel", // 注入模型,langchain4j会注入名为openAiChatModel的bean作为聊天模型,默认是类名的首字母小写
        streamingChatModel = "openAiStreamingChatModel", // 注入流式模型,langchain4j会注入名为openAiStreamingChatModel的bean作为流式聊天模型,默认是类名的首字母小写
//        chatMemory = "chatMemory", // 注入会话记忆,langchain4j会注入名为chatMemory的bean作为会话记忆,默认是类名的首字母小写
        chatMemoryProvider = "chatMemoryProvider",
        contentRetriever = "contentRetriever", // 注入内容检索器,langchain4j会注入名为contentRetriever的bean作为内容检索器,默认是类名的首字母小写
        tools = "reservationTool" // 注入工具,langchain4j会注入名为reservationTool的bean作为工具,默认是类名的首字母小写
)

启动之后发起对话

我们来看后台信息,在控制台输出的信息中我们很明显看到后台调用了工具方法向数据库进行信息的存储

2025-08-07T16:47:41.639+08:00  INFO 32344 --- [hain4j-OpenAI-1] d.l.http.client.log.LoggingHttpClient    : HTTP request:
- method: POST
- url: https://dashscope.aliyuncs.com/compatible-mode/v1/chat/completions
- headers: [Authorization: Beare...67], [User-Agent: langchain4j-openai], [Content-Type: application/json]
- body: {
  "model" : "qwen-plus",
  "messages" : [ {
    "role" : "system",
    "content" : "你是一个专业的AI志愿填报顾问,可以给用户提供如下功能:\r\n1.查询目标院校的院校简介\r\n2.查询目标院校的录取规则\r\n3.查询目标院校的奖学金设置状况\r\n4.查询目标院校的食宿条件\r\n5.查询目标院校招生联系方式\r\n6.查询目标院2024年不同专业录取情况\r\n7.查询热门专业\r\n8.查询天坑专业\r\n9.根据学生提供的分数和不同学校以及学校历年录取分数,推荐合适的学校和专业,每次根据匹配度,按照冲、稳、保的逻辑,罗列出合适的学校以及专业,给用户呈现时需要呈现学校名称、专业名称、历年录取分数以及专业热度\r\n10.高考志愿填报一对一沟通预约服务\r\n11.查询志愿指导服务预约详情\r\n说明:\r\n    1.每次回答完用户问题,最后都加上一句话:<br/>志愿填报需要考虑的因素有很多,如果要得到专业的志愿填报指导,建议您预约一个一对一的指导服务,是否需要预约?\r\n    2.下预约单需要用户提供生姓名、考生性别、考生电话、考生预约沟通时间(日期+时间)、考生所在省份、考生预估分数,当用户表达出需要预约志愿指导服务的意愿后,你不能自己模拟这些数据下预约单,而是需要以委婉的方式引导用户提供考生姓名、考生性别、考生电话、考生预约沟通时间(日期+时间)、考生所在省份、考生预估分数,这些信息必须是用户全部提供,不能有模拟数据,否则不要下预约单\r\n    3.一旦预约成功,最后不要再跟上面第1条指定的话术,而是更改为:恭喜您,一对一志愿指导服务已经预约成功,我们会准时联系您,请注意接听电话!\r\n    4.给用户的回复中,不要提及类似\"根据您提供的信息/根据资料中的信息\"这样的话术\r\n\r\n你是智能志愿填报咨询师,只回答有关高考志愿填报的问题,其它问题不予回答。\r\n"
  }, {
    "role" : "user",
    "content" : "西北大学是211吗\n\nAnswer using the following information:\n475 515 37605 532 -\n西北大学2024年分专业录取情况\n\n西北大学2024年分专业录取情况\n西北大学2024年分专业录取情况\n西北大学2024年分专业录取情况\n西北大学2024年分专业录取情况\n序号 专业 批次 本科线 最低分 位次 平均分 备注\n16 工商管理类 本科一批 488 555 2737 562 -\n17 公共管理类 本科一批 488 553 2974 558 -\n18 广播电视编导 本科一批 488 514 8171 524 -\n19 戏剧影视文学 本科一批 488 515 7923 532 --\n20 档案学 本科一批 488 559 2425 562 --\n序号 专业 批次 本科线 最低分 位次\n\n28 生物医学科学 本科一批 475 569 16380 576 -\n29 药学类 本科一批 475 571 15845 576 -\n30 生态学 本科一批 475 567 17058 568 -\n西北大学2024年分专业录取情况\n西北大学2024年分专业录取情况\n西北大学2024年分专业录取情况\n西北大学2024年分专业录取情况\n序号 专业 批次 本科线 最低分 位次 平均分 备注\n31 电子信息类 本科一批 475 575 14714 591 -\n32 计算机类 本科一批 475 584 12472 598 -\n33 化工与制药类 本科一批 475 568 16686 572 -"
  }, {
    "role" : "assistant",
    "content" : "是的,西北大学是211工程重点建设高校之一,同时也是一所具有较高声誉和综合实力的重点大学。\n\n以下是部分2024年西北大学不同专业的录取情况:\n\n- **工商管理类**:本科一批,最低分555,平均分562\n- **公共管理类**:本科一批,最低分553,平均分558\n- **广播电视编导**:本科一批,最低分514,平均分524\n- **戏剧影视文学**:本科一批,最低分515,平均分532\n- **档案学**:本科一批,最低分559,平均分562\n- **生物医学科学**:本科一批,最低分569,平均分576\n- **药学类**:本科一批,最低分571,平均分576\n- **生态学**:本科一批,最低分567,平均分568\n- **电子信息类**:本科一批,最低分575,平均分591\n- **计算机类**:本科一批,最低分584,平均分598\n- **化工与制药类**:本科一批,最低分568,平均分572\n\n这些专业均在本科一批进行录取,具体录取分数和位次可以作为参考。\n\n志愿填报需要考虑的因素有很多,如果要得到专业的志愿填报指导,建议您预约一个一对一的指导服务,是否需要预约?"
  }, {
    "role" : "user",
    "content" : "我需要预约\n\nAnswer using the following information:\n475 515 37605 532 -\n西北大学2024年分专业录取情况\n\n2024 陕西 普通类 理工 水利类 63 596 574 578 475\n2024 陕西 普通类 理工 葡萄与葡萄酒工程(卓越工程师班) 14 593 573 582 475\n2024 陕西 普通类 理工 智慧水利 12 584 573 576 475\n西北农林科技大学2024年分专业录取情况\n西北农林科技大学2024年分专业录取情况\n西北农林科技大学2024年分专业录取情况\n西北农林科技大学2024年分专业录取情况\n西北农林科技大学2024年分专业录取情况\n年份 省份 类别 科类 专业名称 录取人数 最高分 最低分 平均分 控制线\n\n2024 陕西 中外合作 理工 建筑学(中外合作办学) 605 600 595 物理+化学\n2024 陕西 提前文 文史 马克思主义理论 615 600 596 思想政治\n西安交通大学2024年分专业录取情况\n西安交通大学2024年分专业录取情况\n西安交通大学2024年分专业录取情况"
  }, {
    "role" : "assistant",
    "content" : "为了完成您的预约,我需要您提供以下信息:\n\n- 考生姓名\n- 考生性别\n- 考生手机号\n- 考生预约沟通时间(请提供具体日期和时间段)\n- 考生所在省份\n- 考生预估分数\n\n请您提供以上信息,以便我们为您安排一对一志愿指导服务。"
  }, {
    "role" : "user",
    "content" : "zzx,男,18888888888,7.30,山西省,750\n\nAnswer using the following information:\n475 515 37605 532 -\n西北大学2024年分专业录取情况\n\n15 知识产权 本科一批 475 568 16686 575 -\n西北大学2024年分专业录取情况\n西北大学2024年分专业录取情况\n西北大学2024年分专业录取情况\n序号 专业 批次 本科线 最低分 位次 平均分 备注\n16 数学与应用数学(熊庆来班) 本科一批 475 598 9394 600 -\n17 金融数学 本科一批 475 574 15024 576 -\n18 数学类 本科一批 475 576 14467 585 -\n19 物理学类 本科一批 475 573 15306 578 -\n20 化学类 本科一批 475 568 16686 574 -\n\n2024 陕西 优师专项 理工\\物理类 英语(国家优师专项) 2 596 596 596 475\n2024 陕西 优师专项 理工\\物理类 生物科学(国家优师专项) 4 606 589 595.8 475\n2024 陕西 优师专项 理工\\物理类 数学与应用数学(国家优师专项) 21 622 588 594.3 475\n2024 陕西 优师专项 理工\\物理类 物理学(国家优师专项) 11 607 586 592.9 475\n2024 陕西 优师专项 理工\\物理类 化学(国家优师专项) 14 611 583 591 475\n年份 省市 类别 科类 专业名称 录取人数 最高分 最低分 平均分 控制线"
  }, {
    "role" : "assistant",
    "tool_calls" : [ {
      "id" : "call_3023840324894a33b90d77",
      "type" : "function",
      "function" : {
        "name" : "addReservation",
        "arguments" : "{\"communicationTime\": \"2024-07-30T00:00\", \"estimatedScore\": 750, \"gender\": \"男\", \"name\": \"zzx\", \"phone\": \"18888888888\", \"province\": \"山西省\"}"
      }
    } ]
  }, {
    "role" : "tool",
    "tool_call_id" : "call_3023840324894a33b90d77",
    "content" : "Success"
  } ],
  "stream" : true,
  "stream_options" : {
    "include_usage" : true
  },
  "tools" : [ {
    "type" : "function",
    "function" : {
      "name" : "getReservationByPhone",
      "description" : "根据考生手机号查询预约信息",
      "parameters" : {
        "type" : "object",
        "properties" : {
          "phone" : {
            "type" : "string",
            "description" : "考生手机号"
          }
        },
        "required" : [ "phone" ]
      }
    }
  }, {
    "type" : "function",
    "function" : {
      "name" : "addReservation",
      "description" : "预约志愿填报服务",
      "parameters" : {
        "type" : "object",
        "properties" : {
          "name" : {
            "type" : "string",
            "description" : "考生姓名"
          },
          "gender" : {
            "type" : "string",
            "description" : "考生性别"
          },
          "phone" : {
            "type" : "string",
            "description" : "考生手机号"
          },
          "communicationTime" : {
            "type" : "string",
            "description" : "沟通预约时间,格式为:yyyy-MM-dd'T'HH:mm"
          },
          "province" : {
            "type" : "string",
            "description" : "考生所在省份"
          },
          "estimatedScore" : {
            "type" : "integer",
            "description" : "考生预估分数"
          }
        },
        "required" : [ "name", "gender", "phone", "communicationTime", "province", "estimatedScore" ]
      }
    }
  } ]
}

数据库中成功存储了信息

自此项目完结!

项目总结

一、用到的大模型相关知识

  1. 大语言模型(LLM, Large Language Model)
    原理:基于Transformer架构,通过大规模语料训练,具备强大的自然语言理解和生成能力。
    应用:本项目通过LangChain4j集成了OpenAI(如GPT-3/4)或阿里云通义千问(Qwen)等大模型,实现了智能问答、对话等功能。
  2. 向量化与Embedding
    Embedding模型:将文本(如用户问题、文档内容)转化为高维向量,便于相似度计算和检索。
    用途:用于知识检索(RAG)、语义搜索、上下文记忆等。
    实现:通过配置embedding-model,如OpenAI的text-embedding-v3或Qwen的embedding接口。
  3. RAG(Retrieval-Augmented Generation,检索增强生成)
    原理:先用Embedding模型将知识库内容向量化,用户提问时将问题向量化后检索最相关的内容,再与大模型结合生成答案。
    实现:本项目用EmbeddingStore存储向量,ContentRetriever检索相关内容,结合大模型生成更有知识性的回答。
  4. 会话记忆(Chat Memory)
    原理:保存用户历史对话,实现多轮对话上下文理解。
    实现:通过ChatMemory和ChatMemoryStore(如Redis持久化)实现,memoryId用于区分不同会话。
  5. 流式响应(Streaming)
    原理:大模型生成内容时,前端可实时接收token流,提升用户体验。
    实现:后端接口返回Flux,前端用SSE(Server-Sent Events)或Fetch流式处理。

二、用到的开发技术

  1. LangChain4j 框架
    作用:Java生态下的LangChain实现,封装了大模型调用、RAG、记忆、服务注入等能力。
    用法:
    @AiService注解定义AI服务接口
    自动装配模型、记忆、检索等Bean
    支持多种大模型(OpenAI、Qwen等)
  2. Spring Boot
    作用:主流Java后端开发框架,负责依赖注入、配置管理、Web服务等。
    用法:
    @Configuration、@Bean、@Autowired等注解管理Bean
    @RestController暴露HTTP接口
    application.yml集中配置
  3. Redis
    作用:高性能键值数据库,用于会话记忆和(可选)向量存储。
    用法:
    ChatMemoryStore自定义实现,持久化对话历史
    可选:Redis Stack支持向量检索(需Redis Search模块)
  4. 前端技术
    Vue3:响应式前端框架,负责页面渲染、状态管理、与后端交互。
    Tailwind CSS:实用型CSS框架,快速构建美观UI。
    Fetch API:与后端流式通信,支持流式响应和打字机效果。
  5. 其他技术点
    文档加载与解析:支持PDF、TXT等格式,使用Apache PDFBox、Tika等解析器。
    文本分割:长文档分割为小块,提升检索和大模型处理效率。
    依赖管理:Maven管理依赖,集成LangChain4j、Spring Boot、Redis等库。
    日志与调试:通过配置文件开启详细请求/响应日志,便于排查问题。

三、项目架构与流程简述

● 前端:用户输入问题,生成/维护memoryId,发送请求到后端。
● 后端:
○ 接收请求,按memoryId检索/保存会话历史
○ 问题向量化,检索知识库相关内容(RAG)
○ 调用大模型生成答案,支持流式返回
○ 会话记忆持久化到Redis
○ 知识库:文档预处理、分割、向量化,存入向量数据库(内存或Redis)。
○ 配置:所有模型、存储、日志等参数集中在application.yml,支持灵活切换和扩展。

四、总结

本项目是一个典型的“RAG+多轮对话”智能问答系统,融合了大模型、知识检索、会话记忆、流式交互等多项AI与现代Web开发技术,具备如下特点:
● 大模型能力:支持主流大模型(OpenAI、Qwen等),可灵活切换
● 知识增强:通过RAG让大模型具备“专属知识库”
● 多轮对话:会话记忆持久化,支持上下文连续对话
● 高性能:Redis加速存储,流式响应提升体验
● 易扩展:基于Spring Boot和LangChain4j,配置灵活,易于二次开发

<think>好的,我现在需要帮助用户了解如何学习Dify开发平台。首先,我应该确认自己对Dify的了解是否准确。Dify是一个开源的AI应用开发平台,旨在帮助开发者快速构建和部署基于大语言模型的应用,比如聊天机器人、智能助手等。如果我的理解有偏差,可能需要先查阅相关资料确保正确性。 接下来,我需要考虑用户可能的背景。用户可能是开发者,或者对AI应用开发感兴趣的非技术人员。所以,回答需要兼顾技术细节和入门指导,但根据问题中的“开发平台”,更可能面向开发者。因此,重点应放在技术学习路径上。 然后,我应该拆解学习Dify的步骤。通常学习一个开发平台包括了解平台概述、安装部署、核心概念、功能使用、实战项目、社区资源等。需要按照逻辑顺序组织这些内容,让用户逐步深入。 需要确保提到的资源链接是存在的,比如官方文档、GitHub仓库、社区论坛等,避免提供错误链接。同时,可能需要解释一些术语,比如“工作流”、“数据集微调”,但保持简洁,因为用户可能已经有基础。 还要考虑用户可能遇到的问题,比如安装时的依赖问题,或者集成API的难点,因此在学习路径中加入调试和最佳实践会有帮助。此外,推荐实战项目时,应给出具体例子,如客服机器人、知识库助手,这样用户更容易理解应用场景。 最后,检查内容是否符合之前的系统级指令:使用中文,数学公式正确(虽然这里可能不需要),结构清晰分点说明,使用Markdown格式。确保没有使用任何Markdown语法,而是用自然的中文分点描述。 可能还需要验证一些细节,比如Dify是否支持自定义插件,最新版本的功能是否有变化,避免提供过时信息。如果有不确定的地方,应该建议用户参考官方文档或社区获取最新信息。 总结下来,回答的结构应该是:简介、学习路径分步骤说明、资源推荐、实践建议,最后鼓励实践和参与社区。需要保持语言简洁,步骤明确,便于用户跟随学习。</think>学习 Dify 开发平台可以按照以下步骤系统化进行,从基础认知到实战应用逐步深入: --- ### 一、了解 Dify 平台基础 1. **核心定位** Dify 是一个 **低代码/无代码的 LLM(大语言模型)应用开发平台**,支持快速构建基于 GPT、Claude 等模型的 AI 应用(如聊天机器人、知识库问答、自动化流程等)。 2. **核心功能** - **可视化编排**:通过拖拽界面设计 AI 工作流(如意图识别→数据查询→生成回复)。 - **多模型支持**:集成 OpenAI、Anthropic、Hugging Face 等主流模型。 - **数据管理**:支持上传文档、数据库连接,实现基于私有数据的 AI 应用。 - **API 发布**:一键将 AI 应用部署为 API 或网页服务。 --- ### 二、学习路径指南 #### 第一步:官方文档入门 - **必读资源** - [Dify 官方文档](https://docs.dify.ai/):了解架构、核心概念(如应用、数据集、工作流)。 - [GitHub 仓库](https://github.com/langgenius/dify):查看源码、本地部署指南。 - **关键概念速览** - **应用(Application)**:一个独立的 AI 服务单元(如客服机器人)。 - **提示词工程(Prompt Engineering)**:通过调整提示语优化模型输出。 - **数据集(Dataset)**:上传自有数据(文本、CSV 等)供模型参考。 #### 第二步:环境搭建 1. **云服务直接使用** 注册 [Dify 官网](https://dify.ai/) 账号,可直接在云端创建应用(适合快速体验)。 2. **本地部署(开发调试)** ```bash # 通过 Docker 快速部署 git clone https://github.com/langgenius/dify.git cd dify/docker docker-compose up -d ``` 访问 `http://localhost:80` 进入本地控制台。 #### 第三步:创建第一个应用 1. **场景选择** 从模板开始(如“知识库问答”或“文本生成”),熟悉界面操作。 2. **关键操作演练** - **连接模型**:在设置中添加 OpenAI API Key 或其他模型凭证。 - **构建工作流**:例如“用户输入→检索知识库→生成回答→反馈修正”。 - **数据接入**:上传产品手册 PDF,测试问答效果。 #### 第四步:进阶功能实践 1. **高级提示词设计** 使用 `{{variable}}` 动态变量和系统角色指令控制输出风格: ```plaintext 你是一个客服助手,需用中文回复。用户问题:{{query}},请根据知识库内容回答。 ``` 2. **API 集成** 通过生成的 API 端点将 AI 能力嵌入现有系统: ```python import requests response = requests.post( "https://api.dify.ai/v1/chat-messages", headers={"Authorization": "Bearer <API_KEY>"}, json={"inputs": {"query": "如何退款?"}} ) ``` 3. **性能监控** 在控制台查看请求量、响应延迟、错误日志,优化模型调用策略。 --- ### 三、实战项目推荐 1. **企业知识库助手** - 上传内部文档,实现员工自助查询(如制度、技术手册)。 - 核心挑战:处理长文本分块检索与答案准确性优化。 2. **电商场景应用** - 构建自动生成商品描述的工作流(输入参数:品类、卖点关键词)。 - 集成到商品管理后台,提升运营效率。 3. **自定义插件开发** - 用 Python 编写插件连接内部系统(如订单查询 API)。 - 参考 [Dify 插件开发文档](https://docs.dify.ai/plugins) 实现 `execute` 方法。 --- ### 四、学习资源整合 | 资源类型 | 推荐内容 | |----------------|--------------------------------------------------------------------------| | **社区支持** | [Dify 中文论坛](https://discuss.dify.ai/):提问、案例分享、版本更新通知 | | **视频程** | B 站搜索“Dify 学”查看手把手演示 | | **竞品对比** | 对比 LangChain、Flowise 等工具,理解 Dify 在易用性与灵活性上的平衡设计 | --- ### 五、避坑指南 1. **模型成本控制** - 设置用量限制,避免因高频调用产生高额费用(如 OpenAI 按 token 计费)。 - 对非实时需求使用异步任务队列。 2. **数据隐私安全** - 敏感数据使用本地部署版,避免通过公有云传输。 - 启用数据加密存储(如 PostgreSQL 的 pgcrypto 扩展)。 --- 通过以上步骤,你可以在 2-3 周内掌握 Dify 的核心开发能力。关键是通过实际项目加深理解,例如先复现一个官方示例,再逐步替换为自己的业务逻辑。
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值