前言

​ 该篇文章乃是Springboot搭配LangChain4j实现AI聊天小助手的学习文章。
​ bilibili教程地址:【小智医疗

​ 该教程的技术清单如下:

序号 简述
1. 基于SpringBoot3.2.6整合LangChain4j(1.0.0-beta3)
2. DeepSeek、Ollama、阿里千问等大模型接入
3. ChatMemory实现聊天记忆隔离、MongoDB聊天记忆持久化
4. Prompt模板 - 系统提示词使用
5. Function Calling自定义业务逻辑
6. ElasticSearch8向量存储
7. 对话流式输出

一、springboot3项目+LangChain4j测试案例

1.1 JDK17环境

由于springboot3项目的最低兼容JDK版本是JDK17,所以需要先安装JDK17.

1.2 pom.xml文件添加依赖

pom.xml文件里添加如下依赖:

		<properties>
        <maven.compiler.source>17</maven.compiler.source>
        <maven.compiler.target>17</maven.compiler.target>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
        <spring-boot.version>3.2.6</spring-boot.version>
        <knife4j.version>4.3.0</knife4j.version>
        <langchain4j.version>1.0.0-beta3</langchain4j.version>
        <mybatis-plus.version>3.5.11</mybatis-plus.version>
    </properties>

    <dependencies>
        <!-- web应用程序核心依赖 -->
        <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>
        <!-- 前后端分离中的后端接口测试工具 -->
        <dependency>
            <groupId>com.github.xiaoymin</groupId>
            <artifactId>knife4j-openapi3-jakarta-spring-boot-starter</artifactId>
            <version>${knife4j.version}</version>
        </dependency>
    </dependencies>


    <dependencyManagement>
        <dependencies>
            <!--引入SpringBoot依赖管理清单-->
            <dependency>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-dependencies</artifactId>
                <version>${spring-boot.version}</version>
                <type>pom</type>
                <scope>import</scope>
            </dependency>
        </dependencies>
    </dependencyManagement>

1.3 application.properties文件增加启动端口

# 端口
server.port = 8080

1.4 启动类

package com.dzb.langchain4j;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.ConfigurableApplicationContext;

@SpringBootApplication
public class AiLangChain4jApplication {

    public static void main(String[] args) {
        ConfigurableApplicationContext run = SpringApplication.run(AiLangChain4jApplication.class, args);
        System.out.println("启动成功!!!");
    }

}

1.5 访问knife4j接口文档页面

​ 访问knife4j接口文档页面:http://localhost:8080/doc.html

https://images.cnblogs.com/cnblogs_com/duanxiaobiao/2460494/o_250607142040_springboot3-knife4j-doc.png

1.6 创建gpt-4o-mini 模型测试用例

接入任何一个大模型都需要先去申请apiKey。我们暂时没有密钥,可以先使用LangChain4j 提供的演示密钥,这个密钥是免费的,有使用配额限制,且仅限于 gpt-4o-mini 模型。

1.6.1 pom.xml文件添加LangChain4j相关依赖

​ 此时已经将langchain4j-open-aiz直接改成了langchain4j-open-ai-spring-boot-starter.

		<dependencies>
          <!-- 基于open-ai的langchain4j接口:ChatGPT、deepseek都是open-ai标准下的大模型 -->
        <dependency>
            <groupId>dev.langchain4j</groupId>
            <artifactId>langchain4j-open-ai-spring-boot-starter</artifactId>
        </dependency>
    </dependencies>

    <dependencyManagement>
        <dependencies>
            <!--引入langchain4j依赖管理清单-->
            <dependency>
                <groupId>dev.langchain4j</groupId>
                <artifactId>langchain4j-bom</artifactId>
                <version>${langchain4j.version}</version>
                <type>pom</type>
                <scope>import</scope>
            </dependency>
        </dependencies>
    </dependencyManagement>

1.6.2 测试用例

package com.dzb.langchain4j;

import dev.langchain4j.model.openai.OpenAiChatModel;
import org.junit.jupiter.api.Test;
import org.springframework.boot.test.context.SpringBootTest;

/**
 * @author: dzbiao
 * @CreateTime: 2025/06/07 22:26
 * @Description:
 */
@SpringBootTest
public class LLMTest {

    /**
     * gpt-4o-mini语言模型接入测试
     */
    @Test
    public void testGPTDemo() {

        /**
         * gpt-4o-mini演示apiKey必须是demo,modelName必须是gpt-4o-mini
         */
        OpenAiChatModel model = OpenAiChatModel.builder()
                .baseUrl("http://langchain4j.dev/demo/openai/v1")
                .apiKey("demo")
                .modelName("gpt-4o-mini")
                .build();
        String answer = model.chat("请问你是谁啊?");
        System.out.println(answer);
    }
}

执行结果:

https://images.cnblogs.com/cnblogs_com/duanxiaobiao/2460494/o_250607144051_springboot3-gpt-4o-mini-test.png

二、DeepSeek、Ollama、阿里千问等大模型接入

2.1 DeekSeek 接入

2.1.1 DeekSeek官网注册

访问DeekSeek官网:https://www.deepseek.com/ ,点击右上角的【API开放平台】注册账号,获取base_url和api_key,充值1块钱。

https://images.cnblogs.com/cnblogs_com/duanxiaobiao/2460494/o_250607145358_springboot3-deepseek-registe.png

https://images.cnblogs.com/cnblogs_com/duanxiaobiao/2460494/o_250607145555_springboot3-deepseek-apikey.png

2.1.2 application.properties文件添加deepseek配置

# langchain4j测试模型(使用自己申请的apiKey)
DEEP_SEEK_API_KEY = sk-cf1a************94674e7e9
langchain4j.open-ai.chat-model.base-url = https://api.deepseek.com
langchain4j.open-ai.chat-model.api-key = ${DEEP_SEEK_API_KEY}
langchain4j.open-ai.chat-model.model-name = deepseek-chat
#请求和响应日志
langchain4j.open-ai.chat-model.log-requests = true
langchain4j.open-ai.chat-model.log-responses = true
#启用日志debug级别
logging.level.root = debug

2.1.3 测试案例

package com.dzb.langchain4j;

import dev.langchain4j.model.openai.OpenAiChatModel;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;

/**
 * @author: dzbiao
 * @CreateTime: 2025/06/07 22:26
 * @Description:
 */
@SpringBootTest
public class LLMTest {

    @Autowired
    private OpenAiChatModel openAiChatModel;

    @Test
    public void testDeepSeekAiChatModel() {
        //向模型提问
        String answer = openAiChatModel.chat("你好");
        //输出结果
        System.out.println(answer);
    }
  
}

2.2 下载Ollama,并在Ollama中下载Deeeepseek-r1:1.5b

2.2.1 下载Ollama和DeepSeek

官网: https://ollama.com

https://images.cnblogs.com/cnblogs_com/duanxiaobiao/2460494/o_250607153652_springboot3-ollama.png

Ollama安装成功之后,执行命令:ollama run deepseek-r1:1.5运行大模型

https://images.cnblogs.com/cnblogs_com/duanxiaobiao/2460494/o_250607154118_springboot3-ollama-deepseek-run.png

谷歌浏览器安装插件:Page Assist AI模型的Web-UI

https://images.cnblogs.com/cnblogs_com/duanxiaobiao/2460494/o_250607154748_springboot3-ollama-page-assist-plugin.png

Web-UI页面如下:

https://images.cnblogs.com/cnblogs_com/duanxiaobiao/2460494/o_250607155118_springboot3-ollama-page-assist-web-ui.png

下载ollama可能需要比较高的电脑配置,低配置的可以忽略ollama这个大模型环节,其实和deepseek是一样的道理。

2.2.2 Ollama下的deepseek模型测试用例接入

2.2.2.1 依赖
<!-- 接入ollama -->
<dependency>
	<groupId>dev.langchain4j</groupId>
	<artifactId>langchain4j-ollama-spring-boot-starter</artifactId>
</dependency>
2.2.2.2 配置模型参数
#ollama
langchain4j.ollama.chat-model.base-url = http://localhost:11434
langchain4j.ollama.chat-model.model-name = deepseek-r1:1.5b
langchain4j.ollama.chat-model.log-requests = true
langchain4j.ollama.chat-model.log-responses = true
2.2.2.3测试案例
package com.dzb.langchain4j;

import dev.langchain4j.model.openai.OpenAiChatModel;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;

/**
 * @author: dzbiao
 * @CreateTime: 2025/06/07 22:26
 * @Description:
 */
@SpringBootTest
public class LLMTest {

    /**
     * ollama接入
     */
    @Autowired
    private OllamaChatModel ollamaChatModel;
  
    @Test
    public void testOllama() {
        //向模型提问
        String answer = ollamaChatModel.chat("你好");
        //输出结果
        System.out.println(answer);
    }
  
}

2.3 阿里百炼平台模型

2.3.1 申请免费体验,领取免费额度

访问:阿里百炼模型页面,点击右上角【新用户开通即享超千万免费tokens】,领取各个模型的tokens额度。

https://images.cnblogs.com/cnblogs_com/duanxiaobiao/2460494/o_250607161934_springboot3-alibailian-register.png

领取完成后,在API-Key管理页面创建自己的API-Key

https://images.cnblogs.com/cnblogs_com/duanxiaobiao/2460494/o_250607162333_springboot3-alibailian-api-key.png

2.3.2 接入阿里百炼上的大模型

2.3.2.1 依赖
<dependencies>
  
	<!-- 接入阿里云百炼平台 -->
	<dependency>
		<groupId>dev.langchain4j</groupId>
		<artifactId>langchain4j-community-dashscope-spring-boot-starter</artifactId>
	</dependency>
  
</dependencies>


<dependencyManagement>
	<dependencies>
    
		<!--引入百炼依赖管理清单-->
		<dependency>
			<groupId>dev.langchain4j</groupId>
			<artifactId>langchain4j-community-bom</artifactId>
			<version>${langchain4j.version}</version>
			<type>pom</type>
			<scope>import</scope>
		</dependency>
    
  </dependencies>
</dependencyManagement>
2.3.2.2 配置模型参数
# 阿里百炼平台(API-KEY替换成自己的)
DASH_SCOPE_API_KEY = sk-3f0ebccc3*******c5a728e8974
langchain4j.community.dashscope.chat-model.api-key = ${DASH_SCOPE_API_KEY}
langchain4j.community.dashscope.chat-model.model-name = qwen-max

2.3.2.3 配置模型参数

package com.dzb.langchain4j;

import dev.langchain4j.community.model.dashscope.QwenChatModel;
import dev.langchain4j.model.ollama.OllamaChatModel;
import dev.langchain4j.model.openai.OpenAiChatModel;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;

/**
 * @author: dzbiao
 * @CreateTime: 2025/06/07 22:26
 * @Description:
 */
@SpringBootTest
public class LLMTest {

    /**
     * 通义千问大模型
     */
    @Autowired
    private QwenChatModel qwenChatModel;

    @Test
    public void testDashScopeQwen() {
        //向模型提问
        String answer = qwenChatModel.chat("你好");
        //输出结果
        System.out.println(answer);
    }
}

2.3.3 通义万象生成图片测试

		@Value("${DASH_SCOPE_API_KEY:}")
    private String dashScopeWanxApiKey;
    
    @Test
    public void testDashScopeWanx(){
        WanxImageModel wanxImageModel = WanxImageModel.builder()
                .modelName("wanx2.1-t2i-plus")
                .apiKey(dashScopeWanxApiKey)
                .build();
        Response<Image> response = wanxImageModel
                .generate("奇幻森林精灵:在一片弥漫着轻柔薄雾的古老森林深处," +
                        "阳光透过茂密枝叶洒下金色光斑。一位身材娇小、长着透明薄翼的精灵少女站在一朵硕大的蘑菇上。" +
                        "她有着海藻般的绿色长发,发间点缀着蓝色的小花,皮肤泛着珍珠般的微光。" +
                        "身上穿着由翠绿树叶和白色藤蔓编织而成的连衣裙,手中捧着一颗散发着柔和光芒的水晶球," +
                        "周围环绕着五彩斑斓的蝴蝶,脚下是铺满苔藓的地面,蘑菇和蕨类植 物丛生,营造出神秘而梦幻的氛围。");
                System.out.println(response.content().url());
    }

三、ChatMemory实现聊天记忆隔离、MongoDB聊天记忆持久化

教程中创建AIService的过程,我就省略了,详情去教程看下,或者参考老师提供的pdf教程文档。

3.1 使用AIService实现简单的聊天记忆

3.1.1 引入langchain4j高级功能依赖

<!--langchain4j高级功能-->
<dependency>
	<groupId>dev.langchain4j</groupId>
	<artifactId>langchain4j-spring-boot-starter</artifactId>
</dependency>

3.1.2 创建MemoryChatAssistant接口

​ 使用@AIService注解,并且使用qwenChatModel千问大模型

package com.dzb.langchain4j.common.assistant;

import dev.langchain4j.service.spring.AiService;

import static dev.langchain4j.service.spring.AiServiceWiringMode.EXPLICIT;

/**
 * @author: dzbiao
 * @CreateTime: 2025/06/08 10:21
 * @Description:
 */
@AiService(wiringMode = EXPLICIT, chatModel = "qwenChatModel", chatMemory = "chatMemory")
public interface MemoryChatAssistant {

    String chat(String message);

}

3.1.3 配置MemoryChatAssistantConfig

@Configuration
public class MemoryChatAssistantConfig {
  
    @Bean
    ChatMemory chatMemory() {
        //设置聊天记忆记录的message数量
        return MessageWindowChatMemory.withMaxMessages(10);
    }

}

3.1.4测试案例

package com.dzb.langchain4j;

import com.dzb.langchain4j.common.assistant.Assistant;
import com.dzb.langchain4j.common.assistant.MemoryChatAssistant;
import dev.langchain4j.community.model.dashscope.QwenChatModel;
import dev.langchain4j.memory.chat.MessageWindowChatMemory;
import dev.langchain4j.service.AiServices;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;

/**
 * @author: dzbiao
 * @CreateTime: 2025/06/08 10:13
 * @Description:
 */
@SpringBootTest
public class AIServiceTest {

    @Autowired
    private MemoryChatAssistant memoryChatAssistant;

    @Test
    public void testChatMemory4() {
        String answer1 = memoryChatAssistant.chat("我是环环");
        System.out.println(answer1);
        String answer2 = memoryChatAssistant.chat("我是谁");
        System.out.println(answer2);
    }

}

3.2 使用聊天ID隔离聊天记忆

​ 为每个用户的新聊天或者不同的用户区分聊天记忆

3.2.1 创建SeparateChatAssistant接口

package com.dzb.langchain4j.common.assistant;

import dev.langchain4j.service.MemoryId;
import dev.langchain4j.service.UserMessage;
import dev.langchain4j.service.spring.AiService;

import static dev.langchain4j.service.spring.AiServiceWiringMode.EXPLICIT;

/**
 * @author: dzbiao
 * @CreateTime: 2025/06/08 10:29
 * @Description:
 */
@AiService(
        wiringMode = EXPLICIT,
        // 这个地方一定要注意,pdf文档中是错误的
        chatModel = "qwenChatModel",
        chatMemoryProvider = "chatMemoryProvider"
)
public interface SeparateChatAssistant {
    
    /**
     * 分离聊天记录
     * @param memoryId 聊天id
     * @param userMessage 用户消息
     * @return
     */
    String chat(@MemoryId int memoryId, @UserMessage String userMessage);
    
}

3.2.2 配置ChatMemoryProvider

@Configuration
public class SeparateChatAssistantConfig {
    
    @Bean
    ChatMemoryProvider chatMemoryProvider() {
        return memoryId -> MessageWindowChatMemory.builder()
                .id(memoryId)
                .maxMessages(10)
                .build();
    }
    
}

3.2.3 测试案例

package com.dzb.langchain4j;

import com.dzb.langchain4j.common.assistant.Assistant;
import com.dzb.langchain4j.common.assistant.MemoryChatAssistant;
import com.dzb.langchain4j.common.assistant.SeparateChatAssistant;
import dev.langchain4j.community.model.dashscope.QwenChatModel;
import dev.langchain4j.memory.chat.MessageWindowChatMemory;
import dev.langchain4j.service.AiServices;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;

/**
 * @author: dzbiao
 * @CreateTime: 2025/06/08 10:13
 * @Description:
 */

@SpringBootTest
public class AIServiceTest {

    @Autowired
    private SeparateChatAssistant separateChatAssistant;

    @Test
    public void testChatMemory5() {
        String answer1 = separateChatAssistant.chat(1,"我是环环");
        System.out.println(answer1);
        String answer2 = separateChatAssistant.chat(1,"我是谁");
        System.out.println(answer2);
        String answer3 = separateChatAssistant.chat(2,"我是谁");
        System.out.println(answer3);
    }

}

3.3 MongoDB持久化聊天记忆

3.3.1 MongoDB安装

安装流程就不列了,安装完成后使用mongodb-compass,对数据进行增删改查。

3.3.2 springboot3集成MongoDB

<!-- Spring Boot Starter Data MongoDB -->
<dependency>
	<groupId>org.springframework.boot</groupId>
	<artifactId>spring-boot-starter-data-mongodb</artifactId>
</dependency>

3.3.3 application.properties添加远程配置

# MongoDB
spring.data.mongodb.uri=mongodb://localhost:27017/chat_memory_db

3.3.4 新建实体类

@Data
@AllArgsConstructor
@NoArgsConstructor
@Document("chat_messages")
public class ChatMessages {

    //唯一标识,映射到 MongoDB 文档的 _id 字段
    @Id
    private ObjectId id;

    private int messageId;

    private String content; //存储当前聊天记录列表的json字符串

}

3.3.5 创建一个类实现ChatMemoryStore接口

package com.dzb.langchain4j.store;

import com.dzb.langchain4j.entity.ChatMessages;
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.mongodb.core.MongoTemplate;
import org.springframework.data.mongodb.core.query.Criteria;
import org.springframework.data.mongodb.core.query.Query;
import org.springframework.data.mongodb.core.query.Update;
import org.springframework.stereotype.Component;

import java.util.LinkedList;
import java.util.List;

/**
 * @author: dzbiao
 * @CreateTime: 2025/06/08 11:07
 * @Description:
 */
@Component
public class MongoChatMemoryStore implements ChatMemoryStore {

    @Autowired
    private MongoTemplate mongoTemplate;


    @Override
    public List<ChatMessage> getMessages(Object memoryId) {
        Criteria criteria = Criteria.where("memoryId").is(memoryId);
        Query query = new Query(criteria);
        ChatMessages chatMessages = mongoTemplate.findOne(query, ChatMessages.class);
        if(chatMessages == null) {
            return new LinkedList<>();
        }
        return ChatMessageDeserializer.messagesFromJson(chatMessages.getContent());
    }


    @Override
    public void updateMessages(Object memoryId, List<ChatMessage> messages) {
        Criteria criteria = Criteria.where("memoryId").is(memoryId);
        Query query = new Query(criteria);
        Update update = new Update();
        update.set("content", ChatMessageSerializer.messagesToJson(messages));
        //根据query条件能查询出文档,则修改文档;否则新增文档
        mongoTemplate.upsert(query, update, ChatMessages.class);
    }


    @Override
    public void deleteMessages(Object memoryId) {
        Criteria criteria = Criteria.where("memoryId").is(memoryId);
        Query query = new Query(criteria);
        mongoTemplate.remove(query, ChatMessages.class);
    }

}

3.3.6 SeparateChatAssistantConfig中,添加MongoChatMemoryStore对象的配置

@Configuration
public class SeparateChatAssistantConfig {

    //注入持久化对象
    @Autowired
    private MongoChatMemoryStore mongoChatMemoryStore;

    @Bean
    ChatMemoryProvider chatMemoryProvider() {
        return memoryId -> MessageWindowChatMemory.builder()
                .id(memoryId)
                .maxMessages(10)
                .chatMemoryStore(mongoChatMemoryStore)//配置持久化对象
                .build();
    }

}

3.3.7 执行测试代码

@SpringBootTest
public class AIServiceTest {

    @Autowired
    private SeparateChatAssistant separateChatAssistant;

    @Test
    public void testChatMemory5() {
        String answer1 = separateChatAssistant.chat(1,"我是环环");
        System.out.println(answer1);
        String answer2 = separateChatAssistant.chat(1,"我是谁");
        System.out.println(answer2);
        String answer3 = separateChatAssistant.chat(2,"我是谁");
        System.out.println(answer3);
    }

}

效果:

四、Prompt模板 - 系统提示词使用

@SystemMessage 设定角色,塑造AI助手的专业身份,明确助手的能力范围

@UserMessage:用户提示词注解,获取用户输入

4.1@SystemMessage和@UserMessage注解增加提示词

4.1.1 @SystemMessage

SeparateChatAssistant类的chat方法上添加@SystemMessage注解

@AiService(wiringMode = EXPLICIT, chatModel = "qwenChatModel", chatMemoryProvider = "chatMemoryProvider")
public interface SeparateChatAssistant {

    /**
     * 分离聊天记录
     * @param memoryId 聊天id
     * @param userMessage 用户消息
     * @return
     */
    @SystemMessage("你是我的好朋友,请用东北话回答问题。") //系统消息提示词
    String chat(@MemoryId int memoryId, @UserMessage String userMessage);

}

4.1.2 @UserMessage

在MemoryChatAssistant 的 chat 方法中添加注解

@AiService(wiringMode = EXPLICIT, chatModel = "qwenChatModel", chatMemory = "chatMemory")
public interface MemoryChatAssistant {

    @UserMessage("你是我的好朋友,请用上海话回答问题,并且添加一些表情符号。 {{it}}") //{{it}}表示这里唯一的参数的占位符
    String chat(String message);

}

新建PromptTest测试类,执行以下测试类

package com.dzb.langchain4j;

import com.dzb.langchain4j.common.assistant.MemoryChatAssistant;
import com.dzb.langchain4j.common.assistant.SeparateChatAssistant;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;

/**
 * @author: dzbiao
 * @CreateTime: 2025/06/08 11:53
 * @Description:
 */
@SpringBootTest
public class PromptTest {

    @Autowired
    private SeparateChatAssistant separateChatAssistant;

    
    @Test
    public void testSystemMessage() {
        String answer = separateChatAssistant.chat(3,"今天几号");
        System.out.println(answer);
    }
    

    @Autowired
    private MemoryChatAssistant memoryChatAssistant;
    @Test
    public void testUserMessage() {
        String answer = memoryChatAssistant.chat("我是环环");
        System.out.println(answer);
    }
    
}

4.2 Prompt模板文件使用提示词

@SystemMessage 注解还可以从资源中加载提示模板

@AiService(wiringMode = EXPLICIT, chatModel = "qwenChatModel", chatMemoryProvider = "chatMemoryProvider")
public interface SeparateChatAssistant {

    /**
     * 分离聊天记录
     * @param memoryId 聊天id
     * @param userMessage 用户消息
     * @return
     */
    @SystemMessage(fromResource = "my-prompt-template.txt")
//    @SystemMessage("你是我的好朋友,请用东北话回答问题。今天是{{current_date}}")
    String chat(@MemoryId int memoryId, @UserMessage String userMessage);

}

在resources目录下增加一个文件:my-prompt-template.txt,下面是该文件的内容。

你是我的好朋友,请用东北话回答问题,回答问题的时候适当添加表情符号。

继续执行:PromptTest.testSystemMessage()方法。

至于**@V**注解,此处省略。

4.3 创建硅谷小智

4.3.1 创建硅谷小智XiaozhiAgent接口

由于之前在阿里百炼中领取了不少额度,所以chatModel 选择qwenChatModel

package com.dzb.langchain4j.common.assistant;

import dev.langchain4j.service.MemoryId;
import dev.langchain4j.service.SystemMessage;
import dev.langchain4j.service.UserMessage;
import dev.langchain4j.service.spring.AiService;

import static dev.langchain4j.service.spring.AiServiceWiringMode.EXPLICIT;

/**
 * @author: dzbiao
 * @CreateTime: 2025/06/08 12:10
 * @Description:
 */
@AiService(
        wiringMode = EXPLICIT,
        chatModel = "qwenChatModel",
        chatMemoryProvider = "chatMemoryProviderXiaozhi")
public interface XiaozhiAgent {
    
    @SystemMessage(fromResource = "xiaozhi-prompt-template.txt")
    String chat(@MemoryId Long memoryId, @UserMessage String userMessage);
    
}

4.3.2 resources目录下新增zhaozhi-prompt-template.txt提示词模板文件

​ 这个文件中,调整了部分内容.

你的名字是“硅谷小智”,你是一家名为“北京智慧医疗二院”的智能客服。
你是一个训练有素的医疗顾问和医疗伴诊助手。
你态度友好、礼貌且言辞简洁。

1、请仅在用户发起第一次会话时,和用户打个招呼,并介绍你是谁。

2、作为一个训练有素的医疗顾问:
请基于当前临床实践和研究,针对患者提出的特定健康问题,提供详细、准确且实用的医疗建议。请同时考虑可能的病
因、诊断流程、治疗方案以及预防措施,并给出在不同情境下的应对策略。对于药物治疗,请特别指明适用的药品名
称、剂量和疗程。如果需要进一步的检查或就医,也请明确指示。

3、作为医疗伴诊助手,你可以回答用户就医流程中的相关问题,主要包含以下功能:
AI分导诊:根据患者的病情和就医需求,智能推荐最合适的科室。
AI挂号助手:实现智能查询是否有挂号号源服务;实现智能预约挂号服务;实现智能取消挂号服务。

4、你必须遵守的规则如下:
在获取挂号预约详情或取消挂号预约之前,你必须确保自己知晓用户的姓名(必选)、身份证号(必选)、预约科室
(必选)、预约日期(必选,格式举例:2025-04-14)、预约时间(必选,格式:上午 或 下午)、预约医生(可选)。
当被问到其他领域的咨询时,要表示歉意并说明你无法在这方面提供帮助。

5、请在回答的结果中适当包含一些轻松可爱的图标和表情。

6、今天是 {{current_date}}。

4.3.3 配置持久化和记忆隔离Config

package com.dzb.langchain4j.common.config;

import com.dzb.langchain4j.store.MongoChatMemoryStore;
import dev.langchain4j.memory.chat.ChatMemoryProvider;
import dev.langchain4j.memory.chat.MessageWindowChatMemory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

/**
 * @author: dzbiao
 * @CreateTime: 2025/06/08 12:13
 * @Description:
 */
@Configuration
public class XiaozhiAgentConfig {
    @Autowired
    private MongoChatMemoryStore mongoChatMemoryStore;
    
    @Bean
    ChatMemoryProvider chatMemoryProviderXiaozhi() {
        return memoryId -> MessageWindowChatMemory.builder()
                .id(memoryId)
                .maxMessages(20)
                .chatMemoryStore(mongoChatMemoryStore)
                .build();
    }
    
}

4.3.4 新增XiaozhiController和ChatForm

@Data
public class ChatForm {

    /**
     * //对话id
     */
    private Long memoryId;

    /**
     * 用户问题
     */
    private String message;

}
package com.dzb.langchain4j.controller;

import com.dzb.langchain4j.common.assistant.XiaozhiAgent;
import com.dzb.langchain4j.entity.ChatForm;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

/**
 * @author: dzbiao
 * @CreateTime: 2025/06/01 00:00
 * @Description:
 */
@Tag(name = "硅谷小智")
@RestController
@RequestMapping("/xiaozhi")
public class XiaozhiController {


    @Autowired
    private XiaozhiAgent xiaozhiAgent;
    

    @Operation(summary = "对话")
    @PostMapping(value = "/chat")
    public String chat(@RequestBody ChatForm chatForm) {
        return xiaozhiAgent.chat(chatForm.getMemoryId(), chatForm.getMessage());
    }
    

}

启动项目在,在http://localhost:8080/doc.html#/home 中 和小助手聊天。

五、Function Calling自定义业务逻辑

​ Function Calling 函数调用 也叫 Tools 工具,例如,大语言模型本身并不擅长数学运算。如果应用场景中偶尔会涉及到数学计算,我们可以为他提供一个 “数学工具”。当我们提出问题时,大语言模型会判断是否使用某个工具。

5.1 计算工具

5.1.1 CalculatorTools计算工具类

创建一个tools包,新创建一个计算工具类CalculatorTools,用以提供给大模型调用计算。

package com.dzb.langchain4j.common.tools;

import dev.langchain4j.agent.tool.P;
import dev.langchain4j.agent.tool.Tool;
import dev.langchain4j.agent.tool.ToolMemoryId;
import org.springframework.stereotype.Component;

/**
 * @author: dzbiao
 * @CreateTime: 2025/06/01 00:12
 * @Description:
 */
@Component
public class CalculatorTools {


    @Tool(name = "加法", value = "返回两个参数相加之和")
    double sum(@ToolMemoryId int memoryId,
               @P(value = "加数1", required = true) double a,
               @P(value = "加数2", required = true) double b) {
        System.out.println("调用加法运算:" + memoryId);
        return a + b;
    }

    @Tool(name = "平方根", value = "返回给定参数的平方根")
    double squareRoot(@ToolMemoryId int memoryId,
                      double x) {
        System.out.println("调用平方根运算" + memoryId);
        return Math.sqrt(x);
    }

}

5.1.2 配置工具类

在SeparateChatAssistant中添加tools属性配置

@AiService(
		wiringMode = EXPLICIT,
		chatModel = "qwenChatModel",
		chatMemoryProvider = "chatMemoryProvider",
		tools = "calculatorTools" //配置tools
)

5.1.3 测试类

package com.dzb.langchain4j;

import com.dzb.langchain4j.common.assistant.SeparateChatAssistant;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;

/**
 * @author: dzbiao
 * @CreateTime: 2025/06/01 00:10
 * @Description:
 */
@SpringBootTest
public class ToolsTest {


    @Autowired
    private SeparateChatAssistant separateChatAssistant;

    @Test
    public void testCalculatorTools() {
        String answer = separateChatAssistant.chat(1, "1+2等于几,475695037565的平方根是多少?");
        //答案:3,689706.4865
        System.out.println(answer);
    }
}

5.2 对接业务系统(工具) - 预约业务

预约业务的实现**

这部分我们实现硅谷小智的查询订单、预约订单、取消订单的功能

5.2.1 创建MySQL数据库表

CREATE TABLE `appointment` (
		`id` BIGINT NOT NULL AUTO_INCREMENT,
		`username` VARCHAR(50) NOT NULL,
		`id_card` VARCHAR(18) NOT NULL,
		`department` VARCHAR(50) NOT NULL,
		`date` VARCHAR(10) NOT NULL,
		`time` VARCHAR(10) NOT NULL,
		`doctor_name` VARCHAR(50) DEFAULT NULL,
		PRIMARY KEY (`id`)
);

5.2.2 引入依赖

<!-- Mysql Connector -->
<dependency>
	<groupId>com.mysql</groupId>
	<artifactId>mysql-connector-j</artifactId>
</dependency>

<!--mybatis-plus 持久层-->
<dependency>
	<groupId>com.baomidou</groupId>
	<artifactId>mybatis-plus-spring-boot3-starter</artifactId>
	<version>${mybatis-plus.version}</version>
</dependency>

5.2.3 配置数据库连接

# 基本数据源配置
spring.datasource.url=jdbc:mysql://localhost:3306/guiguxiaozhi?
useUnicode=true&characterEncoding=UTF-8&serverTimezone=Asia/Shanghai&useSSL=false
spring.datasource.username=root
spring.datasource.password=rootroot
spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver
# 开启 SQL 日志打印
mybatis-plus.configuration.log-impl=org.apache.ibatis.logging.stdout.StdOutImpl

5.2.4 创建实体类Appointment

package com.dzb.langchain4j.entity;

import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableId;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;

/**
 * @author: dzbiao
 * @CreateTime: 2025/06/01 22:54
 * @Description:
 */
@Data
@AllArgsConstructor
@NoArgsConstructor
public class Appointment {

    @TableId(type = IdType.AUTO)
    private Long id;

    private String username;

    private String idCard;

    private String department;

    private String date;

    private String time;

    private String doctorName;

}

5.2.5 Mapper

package com.dzb.langchain4j.mapper;

import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.dzb.langchain4j.entity.Appointment;
import org.apache.ibatis.annotations.Mapper;

/**
 * @author: dzbiao
 * @CreateTime: 2025/06/01 22:55
 * @Description:
 */
@Mapper
public interface AppointmentMapper extends BaseMapper<Appointment> {


}

resources目录下增加mapper目录,并在此文件夹下增加AppointmentMapper.xml

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
        "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.dzb.langchain4j.mapper.AppointmentMapper">


</mapper>

5.2.6 Service

public interface AppointmentService extends IService<Appointment> {
    /**
     *
     * @param appointment
     * @return
     */
    Appointment getOne(Appointment appointment);


}
package com.dzb.langchain4j.service.impl;

import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.dzb.langchain4j.entity.Appointment;
import com.dzb.langchain4j.mapper.AppointmentMapper;
import com.dzb.langchain4j.service.AppointmentService;
import org.springframework.stereotype.Service;

/**
 * @author: dzbiao
 * @CreateTime: 2025/06/01 22:57
 * @Description:
 */
@Service
public class AppointmentServiceImpl extends ServiceImpl<AppointmentMapper, Appointment> implements AppointmentService {
    /**
     * 查询订单是否存在
     * @param appointment
     * @return
     */
    @Override
    public Appointment getOne(Appointment appointment) {
        LambdaQueryWrapper<Appointment> queryWrapper = new LambdaQueryWrapper<>();
        queryWrapper.eq(Appointment::getUsername, appointment.getUsername());
        queryWrapper.eq(Appointment::getIdCard, appointment.getIdCard());
        queryWrapper.eq(Appointment::getDepartment, appointment.getDepartment());
        queryWrapper.eq(Appointment::getDate, appointment.getDate());
        queryWrapper.eq(Appointment::getTime, appointment.getTime());
        Appointment appointmentDB = baseMapper.selectOne(queryWrapper);
        return appointmentDB;
    }
}

5.2.7 AppointmentTools工具类

package com.dzb.langchain4j.common.tools;

import com.dzb.langchain4j.entity.Appointment;
import com.dzb.langchain4j.service.AppointmentService;
import dev.langchain4j.agent.tool.P;
import dev.langchain4j.agent.tool.Tool;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;

/**
 * @author: dzbiao
 * @CreateTime: 2025/06/01 23:03
 * @Description:
 */
@Component
public class AppointmentTools {


    @Autowired
    private AppointmentService appointmentService;


    @Tool(name = "预约挂号", value = "根据参数,先执行工具方法queryDepartment查询是否可预约,并直 接给用户回答是否可预约,并让用户确认所有预约信息,用户确认后再进行预约。")
    public String bookAppointment(Appointment appointment) {
        //查找数据库中是否包含对应的预约记录
        Appointment appointmentDB = appointmentService.getOne(appointment);
        if (appointmentDB == null) {
            appointment.setId(null);//防止大模型幻觉设置了id
            if (appointmentService.save(appointment)) {
                return "预约成功,并返回预约详情";
            } else {
                return "预约失败";
            }
        }
        return "您在相同的科室和时间已有预约";
    }

    @Tool(name = "取消预约挂号", value = "根据参数,查询预约是否存在,如果存在则删除预约记录并返回取 消预约成功,否则返回取消预约失败")
    public String cancelAppointment(Appointment appointment) {
        Appointment appointmentDB = appointmentService.getOne(appointment);

        if (appointmentDB != null) {
            //删除预约记录
            if (appointmentService.removeById(appointmentDB.getId())) {
                return "取消预约成功";
            } else {
                return "取消预约失败";
            }
        }
        //取消失败
        return "您没有预约记录,请核对预约科室和时间";
    }


    @Tool(name = "查询是否有号源", value="根据科室名称,日期,时间和医生查询是否有号源,并返回给用 户")
    public boolean queryDepartment(
            @P(value = "科室名称") String name,
            @P(value = "日期") String date,
            @P(value = "时间,可选值:上午、下午") String time,
            @P(value = "医生名称", required = false) String doctorName
    ) {
        System.out.println("查询是否有号源");
        System.out.println("科室名称:" + name);
        System.out.println("日期:" + date);
        System.out.println("时间:" + time);
        System.out.println("医生名称:" + doctorName);
        //TODO 维护医生的排班信息:
        //如果没有指定医生名字,则根据其他条件查询是否有可以预约的医生(有返回true,否则返回false);
        //如果指定了医生名字,则判断医生是否有排班(没有排版返回false)
        //如果有排班,则判断医生排班时间段是否已约满(约满返回false,有空闲时间返回true)
        return true;
    }


}

5.2.8 配置Tools

在XiaozhiAgent 中添加 tools 配置

@AiService(
        wiringMode = EXPLICIT,
        chatModel = "qwenChatModel",
        chatMemoryProvider = "chatMemoryProviderXiaozhi",
        tools = "appointmentTools"  // 预约业务工具
)

5.2.9测试

https://images.cnblogs.com/cnblogs_com/duanxiaobiao/2460494/o_250608050710_springboot3-approve-quest1.png

https://images.cnblogs.com/cnblogs_com/duanxiaobiao/2460494/o_250608051002_springboot3-approve-quest2.png

六、ElasticSearch8向量存储

教程中,我们可以看到,老师是使用Pinecone 做向量数据库存储向量数据的,这种方式需要翻Q,ElasticSearch比较普遍。ES8中支持向量查询。

6.1 安装ElasticSearch8.12.0

ElasticSearch8.12 官网下载地址: https://www.elastic.co/cn/downloads/past-releases/elasticsearch-8-12-0

但是当我下载完文件之后,我打开文件中的jdk.app文件时,发现需要使用JDK21的版本,所以还需要安装JDK21的环境

https://images.cnblogs.com/cnblogs_com/duanxiaobiao/2460494/o_250608060250_springboot3-elasticsearch8-jdk21.png

由于我是Mac M1 , 所以在本地安装了三种JDK(jdk8、jdk17, jdk21). 而且是多环境快速切换的。因此,打开一个ITerm 终端,先将JDK切换成21的版本,并且同时在该终端窗口内,进入elasticsearch8.12.0文件夹内,执行命令:./bin/elasticsearch进行启动。

https://images.cnblogs.com/cnblogs_com/duanxiaobiao/2460494/o_250608060809_springboot3-elasticsearch8-run.png

由于某些原因,启动报错,于是就将config/elasticsearch.yml中的部分配置做了调整

# 改成本地IP
network.host: 191.161.1.116

# Enable security features(改成False)
xpack.security.enabled: false

xpack.security.enrollment.enabled: true

# Enable encryption for HTTP API client connections, such as Kibana, Logstash, and Agents
xpack.security.http.ssl:
  # (改成False)
  enabled: false
  keystore.path: certs/http.p12

# Enable encryption and mutual authentication between cluster nodes
xpack.security.transport.ssl:
  enabled: true
  verification_mode: certificate
  keystore.path: certs/transport.p12
  truststore.path: certs/transport.p12
# Create a new cluster with the current node only
# Additional nodes can still join the cluster later

# Allow HTTP API connections from anywhere
# Connections are encrypted and require user authentication
http.host: 0.0.0.0

成功页面:

https://images.cnblogs.com/cnblogs_com/duanxiaobiao/2460494/o_250608061103_springboot3-elasticsearch8-success.png

6.2 安装kibana

​ kibana的安装流程就忽略了。

6.3 测试ElasticSearch8的向量搜索功能

根据官网给的步骤进行创建索引、添加数据、查询。官网

PUT my-image-index
{
  "mappings": {
    "properties": {
       "image-vector": {
        "type": "dense_vector",
        "dims": 3,
        "index": true,
        "similarity": "l2_norm"
      },
      "file-type": {
        "type": "keyword"
      },
      "title": {
        "type": "text"
      }
    }
  }
}

POST my-image-index/_bulk?refresh=true
{ "index": { "_id": "1" } }
{ "image-vector": [1, 5, -20], "file-type": "jpg", "title": "mountain lake" }
{ "index": { "_id": "2" } }
{ "image-vector": [42, 8, -15], "file-type": "png", "title": "frozen lake"}
{ "index": { "_id": "3" } }
{ "image-vector": [15, 11, 23], "file-type": "jpg", "title": "mountain lake lodge" }


POST my-image-index/_search
{
  "size" : 3,
  "query" : {
    "knn": {
      "field": "image-vector",
      "query_vector": [-5, 9, -12],
      "num_candidates": 10
    }
  }
}

如果上述DSL能正常执行的话,就表明ES8支持向量查询。

6.4 阿里通义千问 text-embedding-v3 通用文本向量-v3模型

我们需要使用text-embedding-v3 向量模型,知道文本生成的向量的维度是多少,方便我们在ES中设置索引的dims属性值。

#集成阿里通义千问-通用文本向量-v3
DASH_SCOPE_API_KEY = sk-3f0ebcc********ae84c5a728e8974
langchain4j.community.dashscope.embedding-model.api-key = ${DASH_SCOPE_API_KEY}
langchain4j.community.dashscope.embedding-model.model-name = text-embedding-v3
package com.dzb.langchain4j;

import dev.langchain4j.community.model.dashscope.QwenEmbeddingModel;
import dev.langchain4j.data.embedding.Embedding;
import dev.langchain4j.data.segment.TextSegment;
import dev.langchain4j.model.embedding.EmbeddingModel;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.test.context.SpringBootTest;

/**
 * @author: dzbiao
 * @CreateTime: 2025/06/02 17:15
 * @Description:
 */
@SpringBootTest
public class EmbeddingTest {

    @Value("${langchain4j.community.dashscope.embedding-model.api-key:}")
    private String apiKey;

    @Value("${langchain4j.community.dashscope.embedding-model.model-name:}")
    private String modelName;

    @Test
    public void testEmbeddingModel(){
        EmbeddingModel embeddingModel = new QwenEmbeddingModel(null, apiKey, modelName);
        TextSegment queryTextSegment = TextSegment.from("你好");
        Embedding queryEmbedding = embeddingModel.embed(queryTextSegment).content();
        System.out.println("向量维度:" + queryEmbedding.vector().length);
        System.out.println("向量输出:" + queryEmbedding);
    }

}

输出结果是:1024

https://images.cnblogs.com/cnblogs_com/duanxiaobiao/2460494/o_250608063442_springboot3-embedding-length.png

6.5 创建fruit-index-3 索引,承载小智AI的向量查询

6.5.1 fruit-index-3索引创建

索引名称没有来得及调整,只是名称而已,无关紧要。

PUT fruit-index-3
{
  "mappings": {
    "properties": {
      "vector": {
        "type": "dense_vector",
        "dims": 1024,
        "index": true,
        "similarity": "l2_norm"
      },
      "text": {
        "type": "text"
      },
      "metadata": {
        "type": "object"
      }
    }
  }
}

# 查询
GET fruit-index-3/_search

6.5.2 项目引入ElasticSearch8

6.5.2.1 pom.xml引入依赖

​ 教程中引入pdf解析依赖,langchain4j-easy-rag等依赖,本文档没有叙述,详细请看教程。依赖直接在这里引入了。

			<!--解析pdf文档-->
        <dependency>
            <groupId>dev.langchain4j</groupId>
            <artifactId>langchain4j-document-parser-apache-pdfbox</artifactId>
        </dependency>

        <!--简单的rag实现-->
        <dependency>
            <groupId>dev.langchain4j</groupId>
            <artifactId>langchain4j-easy-rag</artifactId>
        </dependency>

        <dependency>
            <groupId>dev.langchain4j</groupId>
            <artifactId>langchain4j-elasticsearch</artifactId>
            <exclusions>
                <exclusion>
                    <groupId>co.elastic.clients</groupId>
                    <artifactId>elasticsearch-java</artifactId>
                </exclusion>
            </exclusions>
        </dependency>
        
        <dependency>
            <groupId>org.elasticsearch.client</groupId>
            <artifactId>elasticsearch-rest-client</artifactId>
            <version>8.17.0</version>
        </dependency>

        <dependency>
            <groupId>co.elastic.clients</groupId>
            <artifactId>elasticsearch-java</artifactId>
            <version>8.17.0</version>
        </dependency>

        <dependency>
            <groupId>com.alibaba</groupId>
            <artifactId>fastjson</artifactId>
            <version>1.2.54</version>
        </dependency>
es.host =  127.0.0.1
es.port =  9200
es.index-name = fruit-index-3
6.5.2.2 增加RestClientConfig配置
package com.dzb.langchain4j.common.config;

import lombok.extern.slf4j.Slf4j;
import org.apache.http.HttpHost;
import org.elasticsearch.client.RestClient;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

/**
 * @author: dzbiao
 * @CreateTime: 2025/06/02 23:17
 * @Description:
 */
@Slf4j
@Configuration
public class RestClientConfig {

    @Value("${es.host:127.0.0.1}")
    private String host;

    @Value("${es.port:9200}")
    private Integer port;


    @Bean
    public RestClient client() {
        return RestClient.builder(new HttpHost(host, port, "http")).build();
    }

}

ES插入测试

package com.dzb.langchain4j;

import dev.langchain4j.community.model.dashscope.QwenEmbeddingModel;
import dev.langchain4j.data.embedding.Embedding;
import dev.langchain4j.data.segment.TextSegment;
import dev.langchain4j.model.embedding.EmbeddingModel;
import dev.langchain4j.model.output.Response;
import dev.langchain4j.store.embedding.elasticsearch.ElasticsearchEmbeddingStore;
import jakarta.annotation.Resource;
import org.elasticsearch.client.RestClient;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.test.context.SpringBootTest;

/**
 * @author: dzbiao
 * @CreateTime: 2025/06/02 17:15
 * @Description:
 */
@SpringBootTest
public class EmbeddingTest {

    @Value("${langchain4j.community.dashscope.embedding-model.api-key:}")
    private String apiKey;

    @Value("${langchain4j.community.dashscope.embedding-model.model-name:}")
    private String modelName;

    @Value("${es.index-name:}")
    private String indexName;

    @Resource
    private RestClient restClient;

    
    @Test
    public void elasticsearchEmbeddingStoreTest() {
        TextSegment textSegment = TextSegment.from("我爱吃苹果啊!!!你知道吗?");
        EmbeddingModel embeddingModel = new QwenEmbeddingModel(null, apiKey, modelName);
        Response<Embedding> embed = embeddingModel.embed(textSegment);

        ElasticsearchEmbeddingStore embeddingStore = ElasticsearchEmbeddingStore
                .builder()
                .restClient(restClient)
                .indexName(indexName)
                .build();
        embeddingStore.add(embed.content(), textSegment);
        System.out.println("已添加..............................................................");

    }
}

https://images.cnblogs.com/cnblogs_com/duanxiaobiao/2460494/o_250608072620_springboot3-elasticsearch8-vector-add.png

解析md文档,插入ES数据库

package com.dzb.langchain4j;

import dev.langchain4j.community.model.dashscope.QwenEmbeddingModel;
import dev.langchain4j.data.document.Document;
import dev.langchain4j.data.document.loader.FileSystemDocumentLoader;
import dev.langchain4j.data.embedding.Embedding;
import dev.langchain4j.data.segment.TextSegment;
import dev.langchain4j.model.embedding.EmbeddingModel;
import dev.langchain4j.model.output.Response;
import dev.langchain4j.store.embedding.EmbeddingStoreIngestor;
import dev.langchain4j.store.embedding.elasticsearch.ElasticsearchEmbeddingStore;
import jakarta.annotation.Resource;
import org.elasticsearch.client.RestClient;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.test.context.SpringBootTest;

import java.util.Arrays;
import java.util.List;

/**
 * @author: dzbiao
 * @CreateTime: 2025/06/02 17:15
 * @Description:
 */
@SpringBootTest
public class EmbeddingTest {

    @Value("${langchain4j.community.dashscope.embedding-model.api-key:}")
    private String apiKey;

    @Value("${langchain4j.community.dashscope.embedding-model.model-name:}")
    private String modelName;

    @Value("${es.index-name:}")
    private String indexName;

    @Resource
    private RestClient restClient;


    @Test
    public void elasticsearchMdStoreTest() {
        Document document1 = FileSystemDocumentLoader.loadDocument("/Users/workspace/Downloads/knowledge/医院信息.md");
        Document document2 = FileSystemDocumentLoader.loadDocument("/Users/workspace/Downloads/knowledge/科室信息.md");
        Document document3 = FileSystemDocumentLoader.loadDocument("/Users/workspace/Downloads/knowledge/神经内科.md");
        List<Document> documents = Arrays.asList(document1, document2, document3);

        EmbeddingModel embeddingModel = new QwenEmbeddingModel(null, apiKey, modelName);
        ElasticsearchEmbeddingStore elasticsearchEmbeddingStore = ElasticsearchEmbeddingStore
                .builder()
                .restClient(restClient)
                .indexName(indexName)
                .build();
        EmbeddingStoreIngestor ingestor = EmbeddingStoreIngestor.builder()
                .embeddingModel(embeddingModel)
                .embeddingStore(elasticsearchEmbeddingStore)
                .build();
        ingestor.ingest(documents);
        System.out.println("Md文档内容已添加..............................................................");
    }



}

https://images.cnblogs.com/cnblogs_com/duanxiaobiao/2460494/o_250608073515_springboot3-elasticsearch8-md-content.png

6.5.3 修改XiaozhiAgentConfig

package com.dzb.langchain4j.common.config;

import com.dzb.langchain4j.store.MongoChatMemoryStore;
import dev.langchain4j.memory.chat.ChatMemoryProvider;
import dev.langchain4j.memory.chat.MessageWindowChatMemory;
import dev.langchain4j.model.embedding.EmbeddingModel;
import dev.langchain4j.rag.content.retriever.ContentRetriever;
import dev.langchain4j.rag.content.retriever.EmbeddingStoreContentRetriever;
import dev.langchain4j.store.embedding.elasticsearch.ElasticsearchConfigurationKnn;
import dev.langchain4j.store.embedding.elasticsearch.ElasticsearchEmbeddingStore;
import org.elasticsearch.client.RestClient;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

/**
 * @author: dzbiao
 * @CreateTime: 2025/06/08 12:13
 * @Description:
 */
@Configuration
public class XiaozhiAgentConfig {
    @Autowired
    private MongoChatMemoryStore mongoChatMemoryStore;

    @Autowired
    private RestClient restClient;

    @Value("${es.index-name:}")
    private String indexName;

    @Autowired
    private EmbeddingModel embeddingModel;


    @Bean
    ChatMemoryProvider chatMemoryProviderXiaozhi() {
        return memoryId -> MessageWindowChatMemory.builder()
                .id(memoryId)
                .maxMessages(20)
                .chatMemoryStore(mongoChatMemoryStore)
                .build();
    }


    @Bean
    ContentRetriever contentRetrieverXiaozhi() {
        ElasticsearchEmbeddingStore elasticsearchEmbeddingStore = ElasticsearchEmbeddingStore
                .builder()
                .configuration(ElasticsearchConfigurationKnn.builder().numCandidates(1024).build())
                .restClient(restClient)
                .indexName(indexName)
                .build();


        // 创建一个 EmbeddingStoreContentRetriever 对象,用于从嵌入存储中检索内容
        return EmbeddingStoreContentRetriever
                .builder()
                // 设置用于生成嵌入向量的嵌入模型
                .embeddingModel(embeddingModel)
                // 指定要使用的嵌入存储
                .embeddingStore(elasticsearchEmbeddingStore)
                // 设置最大检索结果数量,这里表示最多返回 1 条匹配结果
                .maxResults(1)
                // 设置最小得分阈值,只有得分大于等于 0.4 的结果才会被返回(TODO:0.8的时候好像查不到?)
                .minScore(0.4)
                // 构建最终的 EmbeddingStoreContentRetriever 实例
                .build();
    }

}

6.5.4XiaoZhiAgent配置向量存储

@AiService(
        wiringMode = EXPLICIT,
        chatModel = "qwenChatModel",
        chatMemoryProvider = "chatMemoryProviderXiaozhi",
        tools = "appointmentTools",
        contentRetriever = "contentRetrieverXiaozhi" //配置向量存储
)

启动应用,问小智:小智小智,我有点感冒,想要去医院,你能告诉我医院的门诊开放时间和地址吗?

https://images.cnblogs.com/cnblogs_com/duanxiaobiao/2460494/o_250608074620_springboot3-elasticsearch-md-opentime-address.png

这个门诊的开放时间,是我特意把md文档里的时间改成了8: 54 - 18:30,是鉴别AI是否去查了本地ES向量数据库。从结果可知,和预期一样。完美!

七、对话流式输出

稍微对接口返回类型进行改造。

7.1 引入依赖

<!--流式输出-->
<dependency>
	<groupId>org.springframework.boot</groupId>
	<artifactId>spring-boot-starter-webflux</artifactId>
</dependency>

<dependency>
	<groupId>dev.langchain4j</groupId>
	<artifactId>langchain4j-reactor</artifactId>
</dependency>

7.2 配置流式输出模型

application.properties 增加

#集成阿里通义千问-流式输出
langchain4j.community.dashscope.streaming-chat-model.api-key=${DASH_SCOPE_API_KEY}
langchain4j.community.dashscope.streaming-chat-model.model-name=qwen-plus

7.3 修改 XiaozhiAgent 配置

修改 XiaozhiAgent 中 chatModel 改为 streamingChatModel = “qwenStreamingChatModel”

chat 方法的返回值为 Flux

@AiService(
        wiringMode = EXPLICIT,
//        chatModel = "qwenChatModel",
        streamingChatModel = "qwenStreamingChatModel",
        chatMemoryProvider = "chatMemoryProviderXiaozhi",
        tools = "appointmentTools",
        contentRetriever = "contentRetrieverXiaozhi" //配置向量存储
)
public interface XiaozhiAgent {

    @SystemMessage(fromResource = "xiaozhi-prompt-template.txt")
    Flux<String> chat(@MemoryId Long memoryId, @UserMessage String userMessage);

}

修改 XiaozhiController 中 chat 方法的返回值为 Flux ,并添加 produces 属性

@Operation(summary = "对话")
@PostMapping(value = "/chat", produces = "text/stream;charset=utf-8")
public Flux<String> chat(@RequestBody ChatForm chatForm) {
  return xiaozhiAgent.chat(chatForm.getMemoryId(), chatForm.getMessage());
}

7.4 运行前端工程

cd xiaozhi-ui
npm i
npm run dev

效果:

https://images.cnblogs.com/cnblogs_com/duanxiaobiao/2460494/o_250608080357_springboot3-ai-chat-web-ui.png

代码地址:https://gitee.com/duanxiaobiao/springboot3-ai-model/tree/master/src

完结撒花!!!💐💐💐

我的博客园

Logo

技术共进,成长同行——讯飞AI开发者社区

更多推荐