Java AI案例

img

LangChain4j

LLMs: Large Language Models大型语言模型

LangChain是一个用于开发由语言模型驱动的应用程序的框架。我们相信,最强大和不同的应用程序不仅将通过 API 调用语言模型,还将:
数据感知:将语言模型与其他数据源连接在一起。
主动性:允许语言模型与其环境进行交互。
因此,LangChain框架的设计目标是为了实现这些类型的应用程序。

langchain4j langchain for java 目标是简化将大型语言模型集成到Java应用程序中的过程

功能:
与大语言模型和向量数据库的便捷交互
专为java语言打造,轻松继承到springboot项目中
智能代理、工具、检索增强生成(RAG)

LangChain4j的应用业务

1.通过聊天访问业务数据,包括搜索、查询和分析,以及生成新的内容,例如回答问题、生成文章、提供建议等
简化用户的业务流程,提高用户体验,降低成本,提高效率,创建订单/取消订单等

2.处理大量非结构化数据,转为结构化数据,以便进行分析和操作,例如,文件,网页中提取关键信息,结合业务分析数据,并提取有用的原始信息。
客户评价,竞品页面关键信息、简历关键信息等

3.图片/文字生成/博客/文章

4.信息转化,日报总结

LangChain4J的AiServices(人工智能服务)

可以将AI Service视为应用程序中服务层的一个组件

常见功能:
1.格式化LLM的输入
2.解析LLM的输出

高级功能:
1.聊天记忆
2.Tools
3.RAG


在springboot使用LangChain4J接入阿里百炼平台中的千问大模型

聊天

<dependencies>
    <!-- langchain依赖包 AI Services API-->
    <dependency>
        <groupId>dev.langchain4j</groupId>
        <artifactId>langchain4j</artifactId>
    </dependency>
    <!--   langchain ai services 集成到springboot项目依赖     -->
    <dependency>
        <groupId>dev.langchain4j</groupId>
        <artifactId>langchain4j-spring-boot-starter</artifactId>
    </dependency>
    <!-- 阿里百炼依赖-->
    <dependency>
        <groupId>dev.langchain4j</groupId>
        <artifactId>langchain4j-community-dashscope-spring-boot-starter</artifactId>
    </dependency>
    <!-- open ai机构依赖包-->
    <dependency>
        <groupId>dev.langchain4j</groupId>
        <artifactId>langchain4j-open-ai-spring-boot-starter</artifactId>
    </dependency>
</dependencies>
<dependencyManagement>
    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-dependencies</artifactId>
            <version>${spring-boot.version}</version>
            <type>pom</type>
            <scope>import</scope>
        </dependency>
        <dependency>
            <groupId>dev.langchain4j</groupId>
            <artifactId>langchain4j-bom</artifactId>
            <version>1.0.0-beta3</version>
            <type>pom</type>
            <scope>import</scope>
        </dependency>
        <dependency>
            <groupId>dev.langchain4j</groupId>
            <artifactId>langchain4j-community-bom</artifactId>
            <version>1.0.0-beta3</version>
            <type>pom</type>
            <scope>import</scope>
        </dependency>
    </dependencies>
</dependencyManagement>

创建assitant代理接口,指定需要代理的大模型为qwenChatModel

@AiService(wiringMode = EXPLICIT, chatModel = "qwenChatModel")
public interface Assistant {
    String chat(String userMessage);
}

配置阿里百炼聊天大模型信息

# 阿里百炼平台api-key
langchain4j.community.dashscope.chat-model.api-key=${DASHSCOPE_API_KEY}
# 指定一个聊天大模型 千问
langchain4j.community.dashscope.chat-model.model-name=qwen-max

测试:

import com.ygjk.model.Assistant;
import lombok.extern.slf4j.Slf4j;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;

@Slf4j
@SpringBootTest
public class AiServiceTests {

    @Autowired
    private Assistant assistant;

    @Test
    public void test() {
        String ask = "你是谁";
        log.info("ask:{}", ask);
        String result = assistant.chat(ask);
        log.info("result:{}", result);
    }

}

img

api-key: 申请
model-name:
img


qwenChatModel:
img

@AiService(wiringMode = EXPLICIT, chatModel = "qwenChatModel"): Ai Services人工智能服务代理对象,代理了qwenChatModel大模型

创建AiServiceFactory对象,通过该对象创建Ai Services人工智能服务代理对象AssistantAssistant代理了qwenChatModel大模型
img

AiServiceFactory: return builder.build();->创建代理对象->invok执行ChatResponse chatResponse = DefaultAiServices.this.context.chatModel.chat(chatRequest);
img


生成图片

import dev.langchain4j.community.model.dashscope.WanxImageModel;
import lombok.extern.slf4j.Slf4j;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.test.context.SpringBootTest;

@Slf4j
@SpringBootTest
public class QwenPicTests {

    @Value("${DASHSCOPE_API_KEY}")
    private String apiKey;

    @Test
    public void test() {
        String prompt = "画面风格是运用松散笔触和明亮色彩营造出瞬间印象与自然氛围的莫奈画风。远景视角,场景是一片宁静的湖泊,湖水清澈如镜,倒映着周围的青山和蓝天白云。湖岸边生长着嫩绿的青草和五彩斑斓的野花,微风拂过,花朵轻轻摇曳。湖的一侧有一片茂密的森林,树木郁郁葱葱,阳光透过树叶的缝隙洒下,形成斑驳的光影。构图上,湖泊占据画面中心,周围的森林和花草环绕,增强画面的层次感和空间感。";
        WanxImageModel aliWanModel = WanxImageModel.builder()
                .apiKey(apiKey)
                .modelName("wanx2.1-t2i-plus")
                .build();
        log.info(aliWanModel.generate(prompt).content().url().toString());
    }
}

生成结果:
img


聊天记忆

第二次会话中,大模型不会记录了上一次会话的内容,不能根据上次会话内容进行回答
img

传统聊天记忆实现

将历史问答内容,传入本次提问中

ChatMessage ask1 = UserMessage.userMessage("我是kevin");
AiMessage answer1 = qwenChatModel.chat(ask1).aiMessage();
log.info("answer1" + answer1.text());
ChatMessage ask2 = UserMessage.userMessage("我是谁");
String answer2 = qwenChatModel.chat(Arrays.asList(
        ask1
        , answer1
        , ask2)).aiMessage().text();
log.info("answer2:{}", answer2);

img

AI Services的聊天会话

1.创建记忆bean

@Configuration
public class ChatMemoryConfig {
    @Bean
    public ChatMemory chatMemory() {
        // 创建一个基于消息窗口的聊天记忆记录10个聊天会话
        return MessageWindowChatMemory.withMaxMessages(10);
    }
}

2.指定人工智能服务记录会话记忆对象
img
3.测试
img


隔离聊天记忆

因为每个用户都需要自己的实例来ChatMemory维护各自的对话,因此需要对聊天记忆记性隔离

1.创建ChatMemoryProvider实例
ChatMemoryProvider为函数式接口,因此允许使用lambda表达式定义实现类

import dev.langchain4j.memory.chat.ChatMemoryProvider;
import dev.langchain4j.memory.chat.MessageWindowChatMemory;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@Configuration
public class ChatMemoryProviderConfig {

    @Bean
    public ChatMemoryProvider chatMemoryProvider() {
        // 为不同的 memoryId 创建不同的 ChatMemory
        return memoryId -> MessageWindowChatMemory.builder().id(memoryId).maxMessages(10).build();
    }

}

2.为人工智能服务添加聊天记忆提供者对象,其中的chatMemory就可以舍弃了,需要借助chatMemoryProvider为用户创建一个chatMemory

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

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

@AiService(wiringMode = EXPLICIT
        , chatModel = "qwenChatModel"
        , chatMemoryProvider = "chatMemoryProvider"
)
public interface AssistantChatMemory {
    String chat(@MemoryId String userId, @UserMessage String userMessage);
}

3.测试

import com.ygjk.model.AssistantChatMemory;
import lombok.extern.slf4j.Slf4j;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;

@Slf4j
@SpringBootTest
public class AiServiceMemoryProviderTests {

    @Autowired
    private AssistantChatMemory assistant;

    @Test
    public void test() {
        String answer1 = assistant.chat("userId1", "请叫我kevin");
        log.info("answer1:{}", answer1);
        String answer2 = assistant.chat("userId1", "我是谁");
        log.info("answer2:{}", answer2);

        String answer3 = assistant.chat("userId2", "请叫我tom");
        log.info("answer3:{}", answer3);
        String answer4 = assistant.chat("userId2", "我是谁");
        log.info("answer4:{}", answer4);
    }
}

img

实现原理

service/DefaultAiServices.class:121-代理对象在执行聊天时,会获取memoryId对应的chatMemory对象
img
获取该chatMemory对象的聊天记录
img
更新聊天记录默认存储对象SingleSlotChatMemoryStore中的聊天记录List
img
在发起提问时,仍然是把历史记录填充本次会话中
img
再把新的回复更新到chatMemory
img

更新存储方式为CurrentHashMap应对用户多线程场景下对话

@Bean
public ChatMemoryProvider chatMemoryProvider() {
    // 为不同的 memoryId 创建不同的 ChatMemory
    return memoryId -> MessageWindowChatMemory.builder().id(memoryId).maxMessages(10)
            .chatMemoryStore(new InMemoryChatMemoryStore())
            .build();
}

img


聊天记录持久化方案

默认情况下聊天记录存储在内存中,占用宝贵资源,容易丢失。对聊天记录的持久化是非常有必要的

1.MongDB
文档型数据库,数据以 JSON - like 的文档形式存储,具有高度的灵活性和可扩展性。它
不需要预先定义严格的表结构,适合存储半结构化或非结构化的数据。
2.Cassandra
特点:是一种分布式的 NoSQL 数据库,具有高可扩展性和高可用性,能够处理大规模的分布
式数据存储和读写请求。适合存储海量的、时间序列相关的数据。
适用场景:对于大型的聊天应用,尤其是用户量众多、聊天数据量巨大且需要分布式存储和处
理的场景,Cassandra 能够有效地应对高并发的读写操作。例如,一些面向全球用户的社交媒
体平台,其聊天数据需要在多个节点上进行分布式存储和管理,Cassandra 可以提供强大的支
持。

持久化到MongoDB

<!--    mongodb依赖    -->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-data-mongodb</artifactId>
</dependency>
# mongodb
spring.data.mongodb.uri=mongodb://localhost:27017/chat_memory_db

1.存储消息实体collection

import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
import org.bson.types.ObjectId;
import org.springframework.data.annotation.Id;
import org.springframework.data.mongodb.core.mapping.Document;

import java.io.Serializable;

@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
@Document("chat_messages")
public class ChatMessageDO implements Serializable {

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

}

2.实现持久化存储对象

import com.ygjk.entity.ChatMessageDO;
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.ArrayList;
import java.util.List;
import java.util.Objects;

@Component
public class PersistentChatMemoryMongoStore implements ChatMemoryStore {

    @Autowired
    private MongoTemplate mongoTemplate;

    @Override
    public List<ChatMessage> getMessages(Object memoryId) {
        ChatMessageDO chatMessageDO = mongoTemplate.findOne(new Query().addCriteria(Criteria.where("memoryId")
                .is(memoryId)), ChatMessageDO.class);
        if (Objects.isNull(chatMessageDO)) {
            return new ArrayList();
        }
        return ChatMessageDeserializer.messagesFromJson(chatMessageDO.getContent());
    }

    @Override
    public void updateMessages(Object memoryId, List<ChatMessage> list) {
        Update update = new Update();
        update.set("content", ChatMessageSerializer.messagesToJson(list));
        mongoTemplate.upsert(Query.query(Criteria.where("memoryId").is(memoryId)), update, ChatMessageDO.class);
    }

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

}

3.记忆提供者指定存储对象persistentChatMemoryStore

import com.ygjk.component.PersistentChatMemoryMongoStore;
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;

@Configuration
public class ChatMemoryProviderConfig {

    @Autowired
    private PersistentChatMemoryMongoStore persistentChatMemoryStore;

    @Bean
    public ChatMemoryProvider chatMemoryProvider() {
        // 为不同的 memoryId 创建不同的 ChatMemory
        return memoryId -> MessageWindowChatMemory.builder().id(memoryId).maxMessages(10)
                .chatMemoryStore(persistentChatMemoryStore)
                .build();
    }
}

4.测试

@Test
public void test() {
    String answer1 = assistant.chat("userId1", "请叫我kevin");
    log.info("answer1:{}", answer1);
    String answer2 = assistant.chat("userId1", "我是谁");
    log.info("answer2:{}", answer2);

    String answer3 = assistant.chat("userId2", "请叫我tom");
    log.info("answer3:{}", answer3);
    String answer4 = assistant.chat("userId2", "我是谁");
    log.info("answer4:{}", answer4);
}

img


提示词

添加系统提示词

@SystemMessage("你是一个京东电商客服"): 添加系统提示词
img
img

当如果更换系统提示词,聊天记录中会把之前的系统提示词删除,并且消除记忆
img

指定日期

deepseek中询问今天日期,显然不能够准确说明日期
img

大模型中并不知道当前日期是多少
img

在LangChain4J中可以使用占位符作为提示词,告诉大模型今天日期
@SystemMessage("你是一个拼多多电商客服,今天日期{{current_date}}")
img


模板解析:
dev.langchain4j.model.input.PromptTemplate: 对占位符进行赋值
img
把系统提示词中的占位符替换为值
img

指定文本内容为系统提示词

@SystemMessage(fromResource = "system_prompt_message.txt")
img

添加用户提示词

使用@UserMessage添加用户提示词,@UserMessage中添加占位符: {{占位单词}}
用户提问提问字段前使用@V("占位单词")标记为参数值为占位符的值

@UserMessage("我是kevin,{{userMessage}}")
String chat(@MemoryId String userId, @V("userMessage") String userMessage);

每次会话都会带有用户提示词
img

当cha方法中只有一个参数时,可以不用添加@V注解,@UserMessage中使用{{it}}占位符

@UserMessage("我是kevin,{{it}}")
String chat(String userMessage);

系统提示词和用户提示词多参数使用

系统提示词
img
用户提示词

/**
 * 多个参数使用
 *
 * @return
 */
@UserMessage("我是kevin,我的用户ID: {{userId}},我的姓名: {{name}},{{userMessage}}")
@SystemMessage(fromResource = "system_prompt_message2.txt")
String chat(@MemoryId String userId, @V("userId") String id, @V("name") String userName
        , @V("userMessage") String userMessage);

img
img

注意

提示词对于大模型处理问题来说是一个很关键的数据,提示词可以影响回答领域范围,回答结果,回答语气等。重要性类似于“钥匙对锁”或“指令对计算机”——它决定了模型如何理解任务、生成内容的方向和质量。

提示词并不是一蹴而就的,而是不断优化进行的

优化提示词的策略

明确目标:先定义清楚需要模型完成什么任务
提供示例:用Few-Shot Prompting(示例引导)提高准确性
分步思考:对复杂任务使用Chain-of-Thought(逐步推理)
迭代优化:根据输出结果调整提示词,逐步细化
利用系统消息(部分模型支持):预先设定角色和规则


Function Calling函数

Function Calling也叫Tools工具
大模型对于数学运算时会出问题,因此需要借助严格的数学公式,让大模型根据公式进行计算

使用tools

定义一个tool

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

@Component
public class CalculatorTools {
    @Tool
    double sum(double a, double b) {
        System.out.println("调用加法运算");
        return a + b;
    }

    @Tool
    double squareRoot(double x) {
        System.out.println("调用平方根运算");
        return Math.sqrt(x);
    }
}

人工智能服务中指定tool

@AiService(wiringMode = EXPLICIT
        , chatModel = "qwenChatModel"
        , chatMemoryProvider = "chatMemoryProvider"
        , tools = "calculatorTools"
)
public interface ToolsAssistant {
    String chat(@MemoryId Long userId, @UserMessage String userMessage);
}

img
会话内容:
img

处理实现流程

创建AiServicesAutoConfig
img
img
创建人工智能服务实例,完成人工智能服务实例中的模型组装
img
LangChain4j带着用户问题和tool中的方法,请求大语言模型会话
img
千文大模型发起http请求
img
千问大模型返回可以使用的函数
img
LangChain4j根据大模型返回的可用函数,并使用其计算结果
img
带着计算结果再次请求大模型,LanChain4j解析大模型返回结果,展示给用户
img


@tool注解的属性

name: 工具名称
value: 工具详细描述

属性值的描述有利于大模型快速定位工具

@Tool(name = "加法运算", value = "计算两个数之和并返回结果")
private double sum(double a, double b) {
    System.out.println("调用加法运算");
    return a + b;
}

@P注解对工具方法的参数描述

@P: 对工具方法参数数据进行标注,以便大模型识别

@Tool(name = "加法运算", value = "计算两个数之和并返回结果")
private double sum(
        @P(value = "数字1", required = true) double a
        , @P(value = "数字2", required = true) double b) {
    System.out.println("调用加法运算");
    return a + b;
}

工具中添加memoryId,用于区分多个用户使用不同的工具逻辑

@ToolMemoryId: 指定了chat方法中的memoryId透传到该工具中

@Tool(name = "加法运算", value = "计算两个数之和并返回结果")
private double sum(
        @ToolMemoryId Long memoryId
        , @P(value = "数字1", required = true) double a
        , @P(value = "数字2", required = true) double b) {
    log.info("用户ID: {} 调用该方法需要自定义逻辑", memoryId);
    return a + b;
}

打印结果: 用户ID: 2 调用该方法需要自定义逻辑


整合案例

@AiService(wiringMode = EXPLICIT
        , chatModel = "qwenChatModel"
        , chatMemoryProvider = "medicalXiaozhiProvider"
        , tools = "appointmentTools"
)
public interface MedicalXiaozhiAgent {

    @SystemMessage(fromResource = "xiaozhi_prompt_template.txt")
    String chat(@MemoryId Long memoryId, @UserMessage String userMessage);
}

【查询有无号源,创建预约,取消预约】tool

import cn.hutool.json.JSONUtil;
import com.ygjk.entity.Appointment;
import com.ygjk.service.AppointmentService;
import dev.langchain4j.agent.tool.P;
import dev.langchain4j.agent.tool.Tool;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;

@Slf4j
@Component
public class AppointmentTools {

    @Autowired
    private AppointmentService appointmentService;

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

    @Tool(name = "取消预约挂号", value = "根据参数,查询预约是否存在,如果存在则删除预约记录并返回取消预约成功,否则返回取消预约失败")
    public String cancelAppointment(Appointment appointment) {
        log.info("【取消预约挂号】入参: {} ", JSONUtil.toJsonStr(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
    ) {
        log.info("【查询是否有号源】入参:科室: {} 日期: {} 时间: {} 医生名称: {} ", name, time, doctorName);
        // todo 业务逻辑处理
        return true;
    }
}

测试对话

img
log: 【查询是否有号源】入参:科室: 内科 日期: 上午 时间: null 医生名称: {}


img
会话内容
img
查看数据创建预约成功
img


img
会话内容
img


img
日志:

2025-04-29T17:27:55.868+08:00  INFO 18748 --- [java-ai] [nio-8080-exec-1] com.ygjk.tools.AppointmentTools          : 【查询是否有号源】入参:科室: 内科 日期: 上午 时间: null 医生名称: {} 
2025-04-29T17:33:01.534+08:00  INFO 18748 --- [java-ai] [nio-8080-exec-6] com.ygjk.tools.AppointmentTools          : 【预约挂号】入参: {"id":1,"username":"kevin","idCard":"310000000000000000","department":"内科","date":"2025-04-30","time":"上午","doctorName":"张医生"} 
==>  Preparing: SELECT id,username,id_card,department,date,time,doctor_name FROM appointment WHERE (username = ? AND id_card = ? AND department = ? AND date = ? AND time = ?)
==> Parameters: kevin(String), 310000000000000000(String), 内科(String), 2025-04-30(String), 上午(String)
<==      Total: 0
==>  Preparing: INSERT INTO appointment ( username, id_card, department, date, time, doctor_name ) VALUES ( ?, ?, ?, ?, ?, ? )
==> Parameters: kevin(String), 310000000000000000(String), 内科(String), 2025-04-30(String), 上午(String), 张医生(String)
<==    Updates: 1
2025-04-29T17:39:06.293+08:00  INFO 18748 --- [java-ai] [io-8080-exec-10] com.ygjk.tools.AppointmentTools          : 【预约挂号】入参: {"id":1,"username":"kevin","idCard":"310000000000000000","department":"内科","date":"2025-04-30","time":"上午","doctorName":"张医生"} 
==>  Preparing: SELECT id,username,id_card,department,date,time,doctor_name FROM appointment WHERE (username = ? AND id_card = ? AND department = ? AND date = ? AND time = ?)
==> Parameters: kevin(String), 310000000000000000(String), 内科(String), 2025-04-30(String), 上午(String)
<==    Columns: id, username, id_card, department, date, time, doctor_name
<==        Row: 3, kevin, 310000000000000000, 内科, 2025-04-30, 上午, 张医生
<==      Total: 1
2025-04-29T17:44:06.216+08:00  INFO 18748 --- [java-ai] [nio-8080-exec-3] com.ygjk.tools.AppointmentTools          : 【取消预约挂号】入参: {"id":1,"username":"kevin","idCard":"310000000000000000","department":"内科","date":"2025-04-30","time":"上午","doctorName":"张医生"} 
==>  Preparing: SELECT id,username,id_card,department,date,time,doctor_name FROM appointment WHERE (username = ? AND id_card = ? AND department = ? AND date = ? AND time = ?)
==> Parameters: kevin(String), 310000000000000000(String), 内科(String), 2025-04-30(String), 上午(String)
<==    Columns: id, username, id_card, department, date, time, doctor_name
<==        Row: 3, kevin, 310000000000000000, 内科, 2025-04-30, 上午, 张医生
<==      Total: 1
==>  Preparing: DELETE FROM appointment WHERE id=?
==> Parameters: 3(Long)
<==    Updates: 1

检索增强生成RAG

如何让大模型回答专业领域的知识

LLM的知识仅限于它所训练的数据。 如果你想让LLM了解特定领域的知识或专有数据,你可以:
1.使用RAG
2.使用你的数据微调LLM
3.结合RAG和微调

提示词知识库tools的value描述,对大模型分析与处理结果具有重要影响
提高大模型准确性工具: RAG向量数据库

微调大模型

在现有大模型的基础上,使用小规模的特定任务数据进行再次训练,调整模型参数,让模型更精确地处
理特定领域或任务的数据。更新需重新训练,计算资源和时间成本高。
优点:一次会话只需一次模型调用,速度快,在特定任务上性能更高,准确性也更高。
缺点:知识更新不及时,模型训成本高、训练周期长。
应用场景:适合知识库稳定、对生成内容准确性和风格要求高的场景,如对上下文理解和语言生成
质量要求高的文学创作、专业文档生成等

RAG

Retrieval Augmented Generation检索增强生成

将原始问题以及提示词信息发送给大语言模型之前,先通过外部知识库检索相关信息,然后将检索结果
和原始问题一起发送给大模型,大模型依据外部知识库再结合自身的训练数据,组织自然语言回答问
题。通过这种方式,大语言模型可以获取到特定领域的相关信息,并能够利用这些信息进行回复。
优点:数据存储在外部知识库,可以实时更新,不依赖对模型自身的训练,成本更低。
缺点:需要两次查询:先查询知识库,然后再查询大模型,性能不如微调大模型
应用场景:适用于知识库规模大且频繁更新的场景,如企业客服、实时新闻查询、法律和医疗领域
的最新知识问答等。

常用方法:
RAG常用方法: 全文(关键词)搜索。 这种方法通过将问题和提示词中的关键词与知识库文档数据库进行匹配来搜
索文档。根据这些关键词在每个文档中的出现频率和相关性对搜索结果进行排序。
向量搜索: 也被称为 “语义搜索”。文本通过 嵌入模型 被转换为 数字向量 。然后,它根据查询向量
与文档向量之间的余弦相似度或其他相似性 / 距离度量来查找和排序文档,从而捕捉更深层次的语
义含义。
混合搜索: 结合多种搜索方法(例如,全文搜索 + 向量搜索)通常可以提高搜索的效果。

传统向量Vectors:
a是一个从 (100, 50) 到 (-50, -50) 的向量,b 是一个从 (0, 0) 到 (100, -50) 的向量。

维度Dimensions: 每个数值向量都有 x 和 y 坐标(或者在多维系统中是 x、y、z,...)。x、y、z... 是这个向量
空间的轴,称为维度

如何将向量的概念扩展到非数值实体上呢(例如文本)?

例如,汽车
轮子: 4
会否需要油: yes
在陆地运动: yes
最多做几个人: 5

我们称汽车向量(4,yes,yes,5),向量的每个纬度代表数据不同特性,纬度越多对事物描述越精确

RAG的过程

Rag分为2个不同的阶段: 索引和检索

索引阶段

在索引阶段,对知识库文档进行预处理,可实现检索阶段的高效搜索。

加载知识库文档 ==> 将文档中的文本分段 ==> 利用向量大模型将分段后的文本转换成向量 ==> 将向量存
入向量数据库

img

为什么要进行文本分段?

大语言模型(LLM)的上下文窗口有限,所以整个知识库可能无法全部容纳其中。
1.你在提问中提供的信息越多,大语言模型处理并做出回应所需的时间就越长。
2.你在提问中提供的信息越多,花费也就越多。
3.提问中的无关信息可能会干扰大语言模型,增加产生幻觉(生成错误信息)的几率。

我们可以通过将知识库分割成更小、更易于理解的片段来解决这些问题。

检索阶段

通过向量模型将用户查询转换成向量 ==> 在向量数据库中根据用户查询进行相似度匹配 ==> 将用户查询
和向量数据库中匹配到的相关内容一起交给LLM处理

img


文档加载器

常见文档加载器

1.来自 langchain4j 模块的文件系统文档加载器(FileSystemDocumentLoader)
2.来自 langchain4j 模块的类路径文档加载器(ClassPathDocumentLoader)
3.来自 langchain4j 模块的网址文档加载器(UrlDocumentLoader)
4.来自 langchain4j-document-loader-amazon-s3 模块的亚马逊 S3 文档加载器(AmazonS3DocumentLoader)
5.来自 langchain4j-document-loader-azure-storage-blob 模块的 Azure Blob 存储文档加载器(AzureBlobStorageDocumentLoader)
6.来自 langchain4j-document-loader-github 模块的 GitHub 文档加载器(GitHubDocumentLoader)
7.来自 langchain4j-document-loader-google-cloud-storage 模块的谷歌云存储文档加载器(GoogleCloudStorageDocumentLoader)
8.来自 langchain4j-document-loader-selenium 模块的 Selenium 文档加载器(SeleniumDocumentLoader)
9.来自 langchain4j-document-loader-tencent-cos 模块的腾讯云对象存储文档加载器(TencentCosDocumentLoader)

测试文档加载FileSystemDocumentLoader与解析TextDocumentParser

import dev.langchain4j.data.document.Document;
import dev.langchain4j.data.document.loader.FileSystemDocumentLoader;
import dev.langchain4j.data.document.parser.TextDocumentParser;
import org.junit.jupiter.api.Test;
import org.springframework.boot.test.context.SpringBootTest;

import java.nio.file.FileSystems;
import java.nio.file.PathMatcher;
import java.util.List;

@SpringBootTest
public class RAGTest {
    @Test
    public void testReadDocument() {
        // 从一个目录中加载所有的.txt文档
        PathMatcher pathMatcher = FileSystems.getDefault().getPathMatcher("glob:*.pdf");
        List<Document> documents = FileSystemDocumentLoader.loadDocuments("src/main/resources/document",
                pathMatcher, new TextDocumentParser());
        for (Document document : documents) {
            System.out.println(document.text());
        }
    }
}

*.pdf能加载,但是TextDocumentParser无法解析
img

langchain4j解析pdf

1.来自 langchain4j 模块的文本文档解析器(TextDocumentParser),它能够解析纯文本格式的文件(例如 TXT、HTML、MD 等)。
2.来自 langchain4j-document-parser-apache-pdfbox 模块的 Apache PDFBox 文档解析器(ApachePdfBoxDocumentParser),它可以解析 PDF 文件。
3.来自 langchain4j-document-parser-apache-poi 模块的 Apache POI 文档解析器(ApachePoiDocumentParser),它能够解析微软办公软件的文件格(例如 DOC、DOCX、PPT、PPTX、XLS、XLSX 等)。
4.来自 langchain4j-document-parser-apache-tika 模块的 Apache Tika 文档解析器(ApacheTikaDocumentParser),它可以自动检测并解析几乎所有现有的文件格式。

<dependency>
    <groupId>dev.langchain4j</groupId>
    <artifactId>langchain4j-document-parser-apache-pdfbox</artifactId>
</dependency>
@Test
public void testParsePDF() {
    // 从一个目录中加载所有的.txt文档
    PathMatcher pathMatcher = FileSystems.getDefault().getPathMatcher("glob:*.pdf");
    List<Document> documents = FileSystemDocumentLoader.loadDocuments("src/main/resources/document",
            pathMatcher, new ApachePdfBoxDocumentParser());
    for (Document document : documents) {
        System.out.println(document.metadata());
        System.out.println(document.text());
    }
}

img

文档分隔(segment阶段)

LangChain4j 有一个 “文档分割器”(DocumentSplitter)接口,并且提供了几种开箱即用的实现方式:
按段落文档分割器(DocumentByParagraphSplitter)
按行文档分割器(DocumentByLineSplitter)
按句子文档分割器(DocumentBySentenceSplitter)
按单词文档分割器(DocumentByWordSplitter)
按字符文档分割器(DocumentByCharacterSplitter)
按正则表达式文档分割器(DocumentByRegexSplitter)
递归分割:DocumentSplitters.recursive (...)
默认情况下每个文本片段最多不能超过300个token

测试向量转换和向量存储

Embedding (Vector) Stores 常见的意思是 “嵌入(向量)存储” 。在机器学习和自然语言处理领域,
Embedding 指的是将数据(如文本、图像等)转换为低维稠密向量表示的过程,这些向量能够保留数据
的关键特征。而 Stores 表示存储,即用于存储这些嵌入向量的系统或工具。它们可以高效地存储和检索
向量数据,支持向量相似性搜索,在文本检索、推荐系统、图像识别等任务中发挥着重要作用。

原始片段: Segment -> 向量模型: Embedding(Vector) Model -> 实体向量: Embeddings

向量存储: 将原始片段和实体向量一起存储

LangChain4j支持的向量存储方式: https://docs.langchain4j.dev/integrations/embedding-stores/

使用简单的内存向量存储

@Test
public void testReadDocumentAndStore() {
    //使用FileSystemDocumentLoader读取指定目录下的知识库文档
    //并使用默认的文档解析器对文档进行解析(TextDocumentParser)
    Document document = FileSystemDocumentLoader.loadDocument("src/main/resources/document/人工智能.md");
    //为了简单起见,我们暂时使用基于内存的向量存储
    InMemoryEmbeddingStore<TextSegment> embeddingStore = new InMemoryEmbeddingStore<>();
    //ingest
    //1、分割文档:默认使用递归分割器,将文档分割为多个文本片段,每个片段包含不超过 300个token,并且有 30 个token的重叠部分保证连贯性
    //DocumentByParagraphSplitter(DocumentByLineSplitter(DocumentBySentenceSplitter(DocumentByWordSplitter)))
    //2、文本向量化:使用一个LangChain4j内置的轻量化向量模型对每个文本片段进行向量化
    //3、将原始文本和向量存储到向量数据库中(InMemoryEmbeddingStore)
    EmbeddingStoreIngestor.ingest(document, embeddingStore);
    //查看向量数据库内容
    System.out.println(embeddingStore);
}

错误: ai.djl.engine.EngineException: Failed to load Huggingface native library.
djl和huggingface库的版本是兼容问题

<dependency>
    <groupId>ai.djl.huggingface</groupId>
    <artifactId>tokenizers</artifactId>
    <version>0.28.0</version>
</dependency>

将文档切割了很片段存储在内存中,每个片段包含,原始数据向量数据以及片段ID
img


过程

初始化EmbeddingStoreIngestor中的成员,其中文档分隔器使用DocumentByParagraphSplitter(按段落分隔)
分割器对象
向量模型对象
向量存储对象
img

分隔并通过向量模型进行转化为向量,然后进行存储
img

自定义分割器

@Test
public void testCustomReadDocumentAndStore() {
    //使用FileSystemDocumentLoader读取指定目录下的知识库文档
    //并使用默认的文档解析器对文档进行解析(TextDocumentParser)
    Document document = FileSystemDocumentLoader.loadDocument("src/main/resources/document/人工智能.md");
    //为了简单起见,我们暂时使用基于内存的向量存储
    InMemoryEmbeddingStore<TextSegment> embeddingStore = new InMemoryEmbeddingStore<>();
    //自定义文档分割器
    //按段落分割文档:每个片段包含不超过 300个token,并且有 30个token的重叠部分保证连贯性
    //注意:当段落长度总和小于设定的最大长度时,就不会有重叠的必要。
    DocumentByParagraphSplitter documentSplitter = new DocumentByParagraphSplitter(
            300,
            30,
            new HuggingFaceTokenizer());
    // 提取向量并存储
    EmbeddingStoreIngestor
            .builder()
            .embeddingStore(embeddingStore)
            .documentSplitter(documentSplitter)
            .build()
            .ingest(document);
}

Token用量计算

Token: 用来表示自然语言基本单位,平台根据token单位进行计费

阿里百炼模型token计费: https://help.aliyun.com/zh/model-studio/models?spm=5176.28197581.d_model-market.1.58275a9eygpxaG#bb0ffee88bwnk

LangChian4j默认token计算: HuggingFaceTokenizer

阿里云token计算:

import dev.langchain4j.community.model.dashscope.QwenTokenizer;
import dev.langchain4j.data.message.UserMessage;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.test.context.SpringBootTest;

@SpringBootTest
public class CalcTokenTest {

    @Value("${DASHSCOPE_API_KEY}")
    private String apiKey;

    @Test
    public void testTokenCount() {
        String text = "这是一个示例文本,用于测试 token 长度的计算。";
        UserMessage userMessage = UserMessage.userMessage(text);
        //计算 token 长度
        QwenTokenizer tokenizer = new QwenTokenizer(apiKey, "qwen-max");
        // HuggingFaceTokenizer tokenizer = new HuggingFaceTokenizer();
        int count = tokenizer.estimateTokenCountInMessage(userMessage);
        System.out.println("token长度:" + count);
    }
}

img


期望的文本片段最大大小

  1. 模型上下文窗口:如果你使用的大语言模型(LLM)有特定的上下文窗口限制,这个值不能超过模
    型能够处理的最大 token 数。例如,某些模型可能最大只能处理 2048 个 token,那么设置的文本片
    段大小就需要远小于这个值,为后续的处理(如添加指令、其他输入等)留出空间。通常,在这种
    情况下,你可以设置为 1000 - 1500 左右,具体根据实际情况调整。

  2. 数据特点:如果你的文档内容较为复杂,每个段落包含的信息较多,那么可以适当提高这个值,
    比如设置为 500 - 800 个 token,以便在一个文本片段中包含相对完整的信息块。相反,如果文档段
    落较短且信息相对独立,设置为 200 - 400 个 token 可能就足够了。

  3. 检索需求:如果希望在检索时能够更精确地匹配到相关信息,较小的文本片段可能更合适,这样
    可以提高信息的粒度。例如设置为 200 - 300 个 token。但如果更注重获取完整的上下文信息,较大
    的文本片段(如 500 - 600 个 token)可能更有助于理解相关内容。

重叠部分大小

  1. 上下文连贯性:重叠部分的主要作用是提供上下文连贯性,避免因分割导致信息缺失。如果文档
    内容之间的逻辑联系紧密,建议设置较大的重叠部分,如 50 - 100 个 token,以确保相邻文本片段
    之间的过渡自然,模型在处理时能够更好地理解上下文。
  2. 数据冗余:然而,设置过大的重叠部分会增加数据的冗余度,可能导致处理时间增加和资源浪
    费。因此,需要在上下文连贯性和数据冗余之间进行平衡。一般来说,20 - 50 个 token 的重叠是比
    较常见的取值范围。
  3. 模型处理能力:如果使用的模型对输入的敏感性较高,较小的重叠部分(如 20 - 30 个 token)可能
    就足够了,因为过多的重叠可能会引入不必要的干扰信息。但如果模型对上下文依赖较大,适当增
    加重叠部分(如 40 - 60 个 token)可能会提高模型的性能。
    例如,在处理一般性的文本资料,且使用的模型上下文窗口较大(如 4096 个 token)时,设置文本片段
    最大大小为 600 - 800 个 token,重叠部分为 30 - 50 个 token 可能是一个不错的选择。但最终的设置还需
    要通过实验和实际效果评估来确定,以找到最适合具体应用场景的参数值。

案例实现RAG

知识库加载到向量存储,人工智能服务引用该向量存储

@Bean
public ContentRetriever contentRetrieverXiaozhi() {
    //使用FileSystemDocumentLoader读取指定目录下的知识库文档
    //并使用默认的文档解析器对文档进行解析
    Document document1 = FileSystemDocumentLoader.loadDocument("src/main/resources/document/医院信息.md");
    Document document2 = FileSystemDocumentLoader.loadDocument("src/main/resources/document/科室信息.md");
    Document document3 = FileSystemDocumentLoader.loadDocument("src/main/resources/document/神经内科.md");
    List<Document> documents = Arrays.asList(document1, document2, document3);
    //使用内存向量存储
    InMemoryEmbeddingStore<TextSegment> embeddingStore = new InMemoryEmbeddingStore<>();
    //使用默认的文档分割器
    EmbeddingStoreIngestor.ingest(documents, embeddingStore);
    //从嵌入存储(EmbeddingStore)里检索和查询内容相关的信息
    return EmbeddingStoreContentRetriever.from(embeddingStore);
}
@AiService(wiringMode = EXPLICIT
        , chatModel = "qwenChatModel"
        , chatMemoryProvider = "medicalXiaozhiProvider"
        , tools = "appointmentTools"
        , contentRetriever = "contentRetrieverXiaozhi"
)

修改工具value提示词,告诉大模型从向量存储中寻找答案

如果用户没有提供具体的医生姓名,请从向量存储中找到一位医生。

@Tool(name = "预约挂号", value = "根据参数,先执行工具方法queryDepartment查询是否可预约,并直接给用户回答是否可预约,并让用户确认所有预约信息,用户确认后再进行预约。如果用户没有提供具体的医生姓名,请从向量存储中找到一位医生。")

测试

知识库中的信息,更加专业准确的回答客户问题
img


img
img


向量模型和向量存储

向量模型

阿里通用文本向量模型: https://bailian.console.aliyun.com/?tab=model#/model-market/detail/text-embedding-v3

使用通用文本向量 text-embedding-v3,维度1024,维度越多,对事务的描述越精准,信息检索的精度越

#集成阿里通义千问-通用文本向量-v3
langchain4j.community.dashscope.embedding-model.api-key=${ali.api.key}
langchain4j.community.dashscope.embedding-model.model-name=text-embedding-v3
import dev.langchain4j.data.embedding.Embedding;
import dev.langchain4j.model.embedding.EmbeddingModel;
import dev.langchain4j.model.output.Response;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;

@SpringBootTest
public class EmbeddingTest {
    @Autowired
    private EmbeddingModel embeddingModel;

    @Test
    public void testEmbeddingModel() {
        Response<Embedding> embed = embeddingModel.embed("你好");
        System.out.println("向量维度:" + embed.content().vector().length);
        System.out.println("向量输出:" + embed.toString());
    }
}

img


向量存储

之前我们使用的是InMemoryEmbeddingStore作为向量存储,但是不建议在生产中使用基于内存的向量存
储。因此这里我们使用Pinecone作为向量数据库。

Pinecone的使用

pinecone官方网站
注册账号后保存api-key

新增向量和使用向量

类似创建一个数据库
img
添加一个向量
img
查询向量,得分高的相似度就高
img

得分的含义

在向量检索场景中,当我们把查询文本转换为向量后,会在嵌入存储( EmbeddingStore )里查找与之
最相似的向量(这些向量对应着文档片段等内容)。为了衡量查询向量和存储向量之间的相似程度,会
使用某种相似度计算方法(例如余弦相似度等)来得出一个数值,这个数值就是得分。得分越高,表明
查询向量和存储向量越相似,对应的文档片段与查询文本的相关性也就越高。

得分作用

筛选结果:
通过设置 minScore 阈值,能够过滤掉那些与查询文本相关性较低的结果。在代码
里, minScore(0.8) 意味着只有得分大于等于 0.8 的结果才会被返回,低于这个阈值的结果会被
舍弃。这样可以确保返回的结果是与查询文本高度相关的,提升检索结果的质量。
控制召回率和准确率:
调整 minScore 的值可以在召回率和准确率之间进行权衡。如果把阈值设
置得较低,那么更多的结果会被返回,召回率会提高,但可能会包含一些相关性不太强的结果,导
致准确率下降;反之,如果把阈值设置得较高,返回的结果数量会减少,准确率会提高,但可能会
遗漏一些相关的结果,使得召回率降低。在实际应用中,需要根据具体的业务需求来合理设置
minScore 的值。

示例说明

假设我们有一个关于水果的文档集合,嵌入存储中存储了这些文档片段的向量。当我们使用 “苹果的营养
价值” 作为查询文本时,向量检索会计算查询向量与存储向量的相似度得分。如果 minScore 设置为
0.8,那么只有那些与 “苹果的营养价值” 相关性非常高的文档片段才会被返回,而一些只简单提及苹果但
没有详细讨论其营养价值的文档片段可能由于得分低于 0.8 而不会被返回。

简单理解向量存储的作用

向量模型把知识库片段转化为向量,这些向量存储到向量存储空间中(Pinecone),存储空间并提供根据关键字搜索相似度高的向量

测试向量存储Pinecone

LangChian4j集成Pinecone

1.引用依赖

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

2.创建向量存储对象

import dev.langchain4j.data.segment.TextSegment;
import dev.langchain4j.model.embedding.EmbeddingModel;
import dev.langchain4j.store.embedding.EmbeddingStore;
import dev.langchain4j.store.embedding.pinecone.PineconeEmbeddingStore;
import dev.langchain4j.store.embedding.pinecone.PineconeServerlessIndexConfig;
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;

@Configuration
public class EmbeddingStoreConfig {

    @Value("${pinecone.api.key}")
    private String pineconeApiKey;
    @Autowired
    private EmbeddingModel embeddingModel;

    @Bean
    public EmbeddingStore<TextSegment> embeddingStore() {
        //创建向量存储
        EmbeddingStore<TextSegment> embeddingStore = PineconeEmbeddingStore.builder()
                // 使用Pinecone的API密钥
                .apiKey(pineconeApiKey)
                //  使用Pinecone的索引名称 如果指定的索引不存在,将创建一个新的索引
                .index("xiaozhi-index")
                // 如果指定的名称空间不存在,将创建一个新的名称空间
                .nameSpace("xiaozhi-namespace")
                .createIndex(PineconeServerlessIndexConfig.builder()
                        // 指定索引部署在 AWS 云服务上。
                        .cloud("AWS")
                        // 指定索引所在的 AWS 区域为 us-east-1。
                        .region("us-east-1")
                        // 指定索引的向量维度,该维度与embeddedModel生成的向量维度相同。
                        .dimension(embeddingModel.dimension())
                        .build())
                .build();
        return embeddingStore;
    }
}

dimension: 向量纬度,与向量模型生成的向量纬度相同

测试:
1.将文本转为片段
2.通过向量模型将片段转为向量
3.将向量存入向量数据库

import dev.langchain4j.data.embedding.Embedding;
import dev.langchain4j.data.segment.TextSegment;
import dev.langchain4j.model.embedding.EmbeddingModel;
import dev.langchain4j.store.embedding.EmbeddingStore;
import lombok.extern.slf4j.Slf4j;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;

@Slf4j
@SpringBootTest
public class PineconeStoreTest {

    @Autowired
    private EmbeddingModel embeddingModel;
    @Autowired
    private EmbeddingStore embeddingStore;

    /**
     * 将文本转换成向量,然后存储到pinecone中
     * <p>
     * 参考:
     * https://docs.langchain4j.dev/tutorials/embedding-stores
     */
    @Test
    public void testPineconeEmbeded() {
        // 1.将文本转为片段
        TextSegment segment1 = TextSegment.from("我喜欢羽毛球");
        // 2.通过向量模型将片段转为向量
        Embedding embedding1 = embeddingModel.embed(segment1).content();
        // 3.将向量存入向量数据库
        embeddingStore.add(embedding1, segment1);
        TextSegment segment2 = TextSegment.from("今天天气很好");
        Embedding embedding2 = embeddingModel.embed(segment2).content();
        embeddingStore.add(embedding2, segment2);
    }
}

存入向量数据库Pinecone
img

相似度匹配

接收请求获取问题,将问题转换为向量,在 Pinecone 向量数据库中进行相似度搜索,找到最相似的文本
片段,并将其文本内容返回给客户端。

1.通过大模型将提问转成向量数据
2.创建搜索请求对象
3.请求向量数据库进行搜索,根据搜索请求searchRequest在向量存储中进行相似度搜索,返回结果

import dev.langchain4j.data.embedding.Embedding;
import dev.langchain4j.data.segment.TextSegment;
import dev.langchain4j.model.embedding.EmbeddingModel;
import dev.langchain4j.store.embedding.EmbeddingMatch;
import dev.langchain4j.store.embedding.EmbeddingSearchRequest;
import dev.langchain4j.store.embedding.EmbeddingSearchResult;
import dev.langchain4j.store.embedding.EmbeddingStore;
import lombok.extern.slf4j.Slf4j;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;

@Slf4j
@SpringBootTest
public class PineconeStoreTest {

    @Autowired
    private EmbeddingModel embeddingModel;
    @Autowired
    private EmbeddingStore embeddingStore;

    /**
     * Pinecone-相似度匹配
     */
    @Test
    public void embeddingSearch() {
        // 1.通过大模型将提问转成向量数据
        Embedding queryEmbedding = embeddingModel.embed("你最喜欢的运动是什么?").content();
        // 2.创建搜索请求对象
        EmbeddingSearchRequest searchRequest = EmbeddingSearchRequest.builder()
                .queryEmbedding(queryEmbedding)
                .maxResults(1) //匹配最相似的一条记录
                //.minScore(0.8)
                .build();
        // 3.请求向量数据库进行搜索,根据搜索请求searchRequest在向量存储中进行相似度搜索,返回结果
        EmbeddingSearchResult<TextSegment> searchResult =
                embeddingStore.search(searchRequest);
        //searchResult.matches():获取搜索结果中的匹配项列表。
        //.get(0):从匹配项列表中获取第一个匹配项
        EmbeddingMatch<TextSegment> embeddingMatch = searchResult.matches().get(0);
        //获取匹配项的相似度得分
        System.out.println(embeddingMatch.score()); // 0.8144288515898701
        //返回文本结果
        System.out.println(embeddingMatch.embedded().text());
    }
}

img


案例集成向量数据库

知识库存储到向量数据库中

import dev.langchain4j.data.document.Document;
import dev.langchain4j.data.document.loader.FileSystemDocumentLoader;
import dev.langchain4j.model.embedding.EmbeddingModel;
import dev.langchain4j.store.embedding.EmbeddingStore;
import dev.langchain4j.store.embedding.EmbeddingStoreIngestor;
import lombok.extern.slf4j.Slf4j;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;

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

@Slf4j
@SpringBootTest
public class KnowledgeToPineconeStoreTest {

    @Autowired
    private EmbeddingModel embeddingModel;
    @Autowired
    private EmbeddingStore embeddingStore;

    @Test
    public void testUploadKnowledgeLibrary() {
        // 使用FileSystemDocumentLoader读取指定目录下的知识库文档
        // 并使用默认的文档解析器对文档进行解析
        Document document1 = FileSystemDocumentLoader.loadDocument("src/main/resources/document/医院信息.md");
        Document document2 = FileSystemDocumentLoader.loadDocument("src/main/resources/document/科室信息.md");
        Document document3 = FileSystemDocumentLoader.loadDocument("src/main/resources/document/神经内科.md");
        List<Document> documents = Arrays.asList(document1, document2, document3);
        // 文本向量化并存入向量数据库:将每个片段进行向量化,得到一个嵌入向量
        EmbeddingStoreIngestor
                .builder()
                .embeddingStore(embeddingStore)
                .embeddingModel(embeddingModel)
                .build()
                .ingest(documents);
    }
}

知识库转为向量存储到向量数据库中
img

将案例中内存向量存储改为Pinecone向量数据库

@Autowired
private EmbeddingStore embeddingStore;
@Autowired
private EmbeddingModel embeddingModel;
@Bean
public ContentRetriever contentRetrieverXiaozhiPincone() {
    // 创建一个 EmbeddingStoreContentRetriever 对象,用于从嵌入存储中检索内容
    return EmbeddingStoreContentRetriever
            .builder()
            // 设置用于生成嵌入向量的嵌入模型
            .embeddingModel(embeddingModel)
            // 指定要使用的嵌入存储
            .embeddingStore(embeddingStore)
            // 设置最大检索结果数量,这里表示最多返回 1 条匹配结果
            .maxResults(1)
            // 设置最小得分阈值,只有得分大于等于 0.8 的结果才会被返回
            .minScore(0.8)
            // 构建最终的 EmbeddingStoreContentRetriever 实例
            .build();
}

人工智能服务指定向量存储实例

@AiService(wiringMode = EXPLICIT
        , chatModel = "qwenChatModel"
        , chatMemoryProvider = "medicalXiaozhiProvider"
        , tools = "appointmentTools"
        , contentRetriever = "contentRetrieverXiaozhiPincone"
)

使接口流式输出

大模型的流式输出是指大模型在生成文本或其他类型的数据时,不是等到整个生成过程完成后再一次性
返回所有内容,而是生成一部分就立即发送一部分给用户或下游系统,以逐步、逐块的方式返回结果。
这样,用户就不需要等待整个文本生成完成再看到结果。通过这种方式可以改善用户体验,因为用户不
需要等待太长时间,几乎可以立即开始阅读响应。

添加依赖

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

修改大模型配置为流式输出

# 阿里百炼平台api-key-流式
langchain4j.community.dashscope.streaming-chat-model.api-key=${ali.api.key}
# 阿里百炼平台大模型名称-流式
langchain4j.community.dashscope.streaming-chat-model.model-name=qwen-max

agent中引用流式输出大模型,修改返回类型为流式对象

qwenStreamingChatModel实例通过配置已经初始化了,可以直接使用beanName
返回类型为流式对象

@AiService(wiringMode = EXPLICIT
        , streamingChatModel = "qwenStreamingChatModel"
        , chatMemoryProvider = "medicalXiaozhiProvider"
        , tools = "appointmentTools"
        , contentRetriever = "contentRetrieverXiaozhiPincone"
)

修改返回类型为Flux,并使接口以text/stream;charset=utf-8类型进行编码,浏览器也使之解码

@AiService(wiringMode = EXPLICIT
        , streamingChatModel = "qwenStreamingChatModel"
        , chatMemoryProvider = "medicalXiaozhiProvider"
        , tools = "appointmentTools"
        , contentRetriever = "contentRetrieverXiaozhiPincone"
)
public interface MedicalXiaozhiAgent {
    @SystemMessage(fromResource = "xiaozhi_prompt_template.txt")
    Flux<String> chat(@MemoryId Long memoryId, @UserMessage String userMessage);
}

@PostMapping(value = "/chat", produces = "text/stream;charset=utf-8")


img
预约数据
img

posted @ 2025-04-30 16:15  ethanx3  阅读(464)  评论(0)    收藏  举报