shiLangChain4j 是一个开源的 Java 库,旨在简化将大型语言模型(LLM)集成到 Java 应用中的工作。

与大型语言模型和向量数据库的便捷交互
通过统一的应用程序编程接口( API ),可以轻松访问所有主要的商业和开源大型语言模型以及向量数据
库,使你能够构建聊天机器人、智能助手等应用。
专为 Java 打造
借助 Spring Boot 集成,能够将大模型集成到 ava 应用程序中。大型语言模型与 Java 之间实现了双向集
成:你可以从 Java 中调用大型语言模型,同时也允许大型语言模型反过来调用你的 Java 代码
智能代理、工具、检索增强生成( RAG
为常见的大语言模型操作提供了广泛的工具,涵盖从底层的提示词模板创建、聊天记忆管理和输出解
析,到智能代理和检索增强生成等高级模式

创建项目及相关配置

创建好springboot项目后,导入

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

其中,knife4j是一款无需前端就可以调用后端接口测试的工具,调试好端口号之后,在页面输入localhost8080/doc.html即可进入knife4j调式页面

导入langchain4j依赖

LangChain4j 具有模块化设计,包括:
1. langchain4j-core 模块,它定义了核心抽象概念(如聊天语言模型和嵌入存储)及其 API
2. langchain4j 模块,包含有用的工具,如文档加载器、聊天记忆实现,以及诸如人工智能服务等
高层功能。
3. 大量的 langchain4j-  open-ai 模块,每个模块都将各种大语言模型提供商和嵌入存储集成到
LangChain4j 中。你可以独立使用 langchain4j-{ 集成 } 模块。如需更多功能,只需导入主 langchain4j
依赖项即可。
若引入了LangChain4j 的依赖清单则不需要维护 LangChain4j所有依赖的版本号
<!-- 基于open-ai的langchain4j接口:ChatGPT、deepseek都是open-ai标准下的大模型 -->
        <dependency>
            <groupId>dev.langchain4j</groupId>
            <artifactId>langchain4j-open-ai</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>

接入测试的LLM

https://docs.langchain4j.dev/get-started官方文档

接入任何大模型之前,都要获得对应LLM的api,langchain4j规定了引入大模型的规范,并且提供可测试的LLM。

OpenAiChatModel model = OpenAiChatModel.builder()
    .apiKey(apiKey)
    .modelName("gpt-4o-mini")
    .build();

OpenAiChatModel model = OpenAiChatModel.builder()
    .baseUrl("http://langchain4j.dev/demo/openai/v1")
    .apiKey("demo")
    .modelName("gpt-4o-mini")
    .build();

SpringBoot整合

若想更好的使用langchain4j,需要将依赖换成langchain4j-open-ai-spring-boot-starter,并且在properties里配置参数,下面是官方文档提供的5个参数,前3个是必须要填的,后面两个是打印请求和相应日志

langchain4j.open-ai.chat-model.base-url=http://langchain4j.dev/demo/openai/v1
langchain4j.open-ai.chat-model.api-key=demo
langchain4j.open-ai.chat-model.model-name=gpt-4o-mini
#请求和响应日志
langchain4j.open-ai.chat-model.log-requests=true
langchain4j.open-ai.chat-model.log-responses=true
#启用日志debug级别
logging.level.root=debug

整合完成后,就可以直接通过@autowired依赖注入的方式调用语言大模型,并不需要再去自己配置一边,可以观察前后差异

/**
     * gpt-4o-mini语言模型接入测试
     */
    @Test
    public void testGPTDemo() {
        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);
    }
    /**
     * 整合SpringBoot
     */
    @Autowired
    private OpenAiChatModel openAiChatModel;
    @Test
    public void testSpringBoot() {
        //向模型提问
        String answer = openAiChatModel.chat("你好");
        //输出结果
        System.out.println(answer);
    }

接入Deepseek

Comparison Table of all supported Language Models | LangChain4j我们可以看到官方文档指出哪些大模型的接入,其中deepseek和gpt都是遵循open-ai的规范。

从deepseek官网申请到API之后会得到一个key,如果直接在properties文件中设置,会有泄露的风险,所以在环境变量中加入自己的DEEP_SEEK_API_KEY这个变量,在引用就能防止泄露

langchain4j.open-ai.chat-model.base-url=https://api.deepseek.com/v1
langchain4j.open-ai.chat-model.api-key=${DEEP_SEEK_API_KEY}
langchain4j.open-ai.chat-model.model-name=deepseek-chat

由于OpenAiChatModel继承了chatlanguageModel,所以我们可以将实例更换为chatlanguageModel,因为chatlanguageModel可以接入更多的LLM,而OpenAiChatModel仅支持deepseek和chatgpt

输出

Ollama本地部署 

Ollama 是一个本地部署大模型的工具。使用 Ollama 进行本地部署有以下多方面的原因:
  • 数据隐私与安全:对于金融、医疗、法律等涉及大量敏感数据的行业,数据安全至关重要。
  • 离线可用性:在网络不稳定或无法联网的环境中,本地部署的 Ollama 模型仍可正常运行。
  • 降低成本:云服务通常按使用量收费,长期使用下来费用较高。而 Ollama 本地部署,只需一次性投 入硬件成本,对于需要频繁使用大语言模型且对成本敏感的用户或企业来说,能有效节约成本。
  • 部署流程简单:只需通过简单的命令 “ollama run < 模型名>”,就可以自动下载并运行所需的模型。
  • 灵活扩展与定制:可对模型微调,以适配垂直领域需求。
官网: https://ollama.com/
在CMD种输入ollama,证明安装成功

然后再ollama官网找到deepseek下载命令行,从远程服务器种拉取deepseek-R1。
ollama run deepseek-r1:1.5b

之后我们也可以用上述命令行,在cmd中进行交互 

这样,Ollama的本地部署就完成了

整合Ollama

Ollama | LangChain4j官方文档

<!-- 接入ollama -->
<dependency>
    <groupId>dev.langchain4j</groupId>
    <artifactId>langchain4j-ollama-spring-boot-starter</artifactId>
</dependency>
langchain4j.ollama.chat-model.base-url=http://localhost:11434
langchain4j.ollama.chat-model.model-name=llama3.1
langchain4j.ollama.chat-model.temperature=0.8
langchain4j.ollama.chat-model.timeout=PT60S

baseurl:ollama的本地端口

model-name:要从Ollama 服务器使用的模型的名称。

temperature:控制生成的响应的随机性。较高的值(例如 1.0)会产生更多样化的输出,而较低的值(例如 0.2)会产生更具确定性的响应。

timeout:允许 API 调用完成的最长时间。

这时,我们因为部署了两个LLM,这时候就会出现报错,及chatlanguage接口不知道应该注入哪一个llm,所以就需要屏蔽其中一个llm或者实现两个llm各自的接口

​ 

阿里百炼

阿里云百炼是 2023 推出的。它集成了阿里的通义系列大模型和第三方大模型,涵盖文本图像、音视频等不同模态。
功能优势 :集成超百款大模型 API ,模型选择丰富; 5-10 分钟就能低代码快速构建智能体,应用构 建高效;提供全链路模型训练、评估工具及全套应用开发工具,模型服务多元;在线部署可按需扩 缩容,新用户有千万 token 免费送,业务落地成本低。
支持接入的模型列表: https://help.aliyun.com/zh/model-studio/models
模型广场: https://bailian.console.aliyun.com/?productCode=p_efm#/model-market

接入阿里百炼通义千问qwen-max模型

<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>
#阿里百炼平台
langchain4j.community.dashscope.chat-model.api-key=${DASH_SCOPE_API_KEY}
langchain4j.community.dashscope.chat-model.model-name=qwen-max

人工智能服务 AIService

AIService使用面向接口和动态代理的方式完成程序的编写,更灵活的实现高级功能。

LangChain4j 中我们使用 AIService 完成复杂操作。底层组件将由 AIService 进行组装。
AIService 可处理最常见的操作:
  • 为大语言模型格式化输入内容
  • 解析大语言模型的输出结果
  • 聊天记忆 Chat memory
  • 工具 Tools
  • 检索增强生成 RAG

 创建AIservice,测试交互

<!--langchain4j高级功能-->
<dependency>
<groupId>dev.langchain4j</groupId>
<artifactId>langchain4j-spring-boot-starter</artifactId>
</dependency
public interface Assistant {
    String chat(String userMessage);
}

    @SpringBootTest
public class AIServiceTest {
    @Autowired
    private QwenChatModel qwenChatModel;
    @Test
    public void testChat() {
        //创建AIService
        Assistant assistant = AiServices.create(Assistant.class, qwenChatModel);
        //调用service的接口
        String answer = assistant.chat("Hello");
        System.out.println(answer);
}
}
@AiService(wiringMode = EXPLICIT, chatModel = "qwenChatmodel")
public interface Assistant {
    String chat(String userMessage);
}
    
@Autowired
private Assistant assistant;
@Test
public void testAi1(){
    String answer = assistant.chat("你是谁?");
    System.out.println(answer);
}

更推荐下面这种方式,采取@Aiservice的方式,这样在用例中就可以直接注意注解标记过的方法

AiServices 组装 Assistant 接口以及其他组件 ,并使用反射机制创建一个实现 Assistant 接口的 代理对象
这个代理对象会处理输入和输出的所有转换工作。在这个例子中, chat 方法的输入是一个字符串,但是大
模型需要一个 UserMessage 对象。所以,代理对象将这个字符串转换为 UserMessage ,并调用聊天语
言模型。 chat 方法的输出类型也是字符串,但是大模型返回的是 AiMessage 对象,代理对象会将其转换
为字符串。
简单理解就是:代理对象的作用是输入转换和输出转换

聊天记忆 

若只是单纯的调用Api进行交互,LLM是没有上下文记忆的

我们可以采用以下方式实现上下文记忆 

首先,会将用户发送的信息封装在usermessage1中,然后发送给AI,再从ChatResponse中提取aimessage对象并输出,然后再第二次封装是,要将上文中的usermessage1、aimessage以及userMessage2 一并封装发送才可以

@Autowired
    private QwenChatModel qwenChatModel;
    @Test
    public void testChatMemory2() {
        //第一轮对话
        UserMessage userMessage1 = UserMessage.userMessage("我是环环");
        ChatResponse chatResponse1 = qwenChatModel.chat(userMessage1);
        AiMessage aiMessage1 = chatResponse1.aiMessage();
        //输出大语言模型的回复
        System.out.println(aiMessage1.text());
        //第二轮对话
        UserMessage userMessage2 = UserMessage.userMessage("你知道我是谁吗");
        ChatResponse chatResponse2 = qwenChatModel.chat(Arrays.asList(userMessage1,
                aiMessage1, userMessage2));
        AiMessage aiMessage2 = chatResponse2.aiMessage();
        //输出大语言模型的回复
        System.out.println(aiMessage2.text());
    }

 但上述实现方式复杂,每一次都要将上次发送和收到的消息进行封装在发送

使用AIService实现聊天记忆

AIService 由多个组件(大模型,聊天记忆,等)组成的时候,我们就可以称他为 智能体 了,会自动注入LLM模型,以及聊天记忆
@AiService(
        wiringMode = EXPLICIT,
        chatModel = "qwenChatModel",
        chatMemory = "chatMemory"
)
public interface MemoryChatAssistant {
    String chat(String message);
}
@Configuration
public class MemoryChatAssistantConfig {
    @Bean
    ChatMemory chatMemory() {
//设置聊天记忆记录的message数量
        return MessageWindowChatMemory.withMaxMessages(10);
    }
}

上述创建了配置类,也就是将chatMemory封装成bean注入到Aiservice中,并且规定上下文的100各消息都会包含

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

隔离聊天记忆

确保不同用户使用的上下文记忆是不一样的,所以定义Assistant时,方法中要定义一个memoryId的参数,确定每个记忆上下文的Id

同时配置Config类,这样就创建了一个可以自动注意memoryId的chatmemory对象,并且这个对象通过chatmemoryPrrovider形式提供给前面的Assistant智能体的

@AiService(
        wiringMode = EXPLICIT,
        chatModel = "qwenChatModel",
        chatMemoryProvider = "chatMemoryProvider"
)
public interface SeparateChatAssistant {
    /**
     * 分离聊天记录
     * @param memoryId 聊天id
     * @param userMessage 用户消息
     * @return
     */
    String chat(@MemoryId int memoryId, @UserMessage String userMessage);
}


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

测试:可以根据输出结果看到,两个上下文的记忆被分开了

 @Autowired
    private SeparateChatAssistant separateChatAssistant;
    @Test
    public void test5(){
        String a = separateChatAssistant.chat(1,"我是章三");
        System.out.println(a);
        String b = separateChatAssistant.chat(1,"你知道我是谁么?");
        System.out.println(b);
        String a1 = separateChatAssistant.chat(2,"我是王四");
        System.out.println(a1);
        String b1 = separateChatAssistant.chat(2,"你知道我是谁么?");
        System.out.println(b1);
    }

 原理:再chatmemory底层有一个store字段,定义了两种存储方式,一种基于数组,一种基于哈希图,都是内存存储,优点是读取查找快,缺点是无法持久化存储,因为内存存储再每次重启后都会消失。

持久化聊天记忆 Persistence

默认情况下,聊天记忆存储在内存中。如果需要持久化存储,可以实现一个自定义的聊天记忆存储类, 以便将聊天消息存储在你选择的任何持久化存储介质中。
MySQL
特点 :关系型数据库。支持事务处理,确保数据的一致性和完整性,适用于结构化数据的存储和查询。
适用场景 :如果聊天记忆数据结构较为规整,例如包含固定的字段如对话 ID 、用户 ID 、时间戳、消息内容等,且需要进行复杂的查询和统计分析,如按用户统计对话次数、按时间范围查
询特定对话等, MySQL 是不错的选择。
Redis
特点 :内存NoSQL:数据库,读写速度极高。它适用于存储热点数据,并且支持多种数据结构,如字符 串、哈希表、列表等,方便对不同类型的聊天记忆数据进行处理。
适用场景 :对于实时性要求极高的聊天应用,如在线客服系统或即时通讯工具, Redis 可以快速存储和获取最新的聊天记录,以提供流畅的聊天体验。

MongoDB

特点 :文档型NoSQL数据库,数据以 JSON - like 的文档形式存储,具有高度的灵活性和可扩展性。它 不需要预先定义严格的表结构,适合存储半结构化或非结构化的数据。
适用场景 :当聊天记忆中包含多样化的信息,如文本消息、图片、语音等多媒体数据,或者消
息格式可能会频繁变化时, MongoDB 能很好地适应这种灵活性。
MongoDB 使用集合( Collections 来组织文档( Documents ),每个文档都是由键值对组成
数据库( Database :存储数据的容器,类似于关系型数据库中的数据库。
集合( Collection :数据库中的一个集合,类似于关系型数据库中的
文档( Document :集合中的一个数据记录,类似于关系型数据库中的行 row )以 BSON 格式
存储。

基于MangoDB持久化聊天

首先,创建实体类ChatMessages,用于封装消息

@Data
@AllArgsConstructor
@NoArgsConstructor
@Document("chat_messages")

public class ChatMessages {
    //唯一标识,映射到 MongoDB 文档的 _id 字段
    @Id
    private ObjectId messageId;
    //用于区分上下文ID
    private String memoryId;
    //private Long messageId;
    private String content; //存储当前聊天记录列表的json字符串
}

然后实现创建持久化类MemoryChatMemoryStore,并实现ChatMemoryStore接口并重写其中的方法,实现ManggoDB的增删改查操作

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

最后,被隔离记忆的数据会持久化的存储在mangoDb中

提示词Prompt

系统提示词

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

比如我们为AI设置一个东北人的角色,可以看到AI带着东北口音的回答,并且我们可以在ManggoDB中看到消息属性,可以看到预设的提示词在最前面,消息类型是System。

!!若更换的提示词,即使将聊天记忆都存储在一个上下文中,AI也会失去记忆。

@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);
}
@SpringBootTest
public class PromptTest {

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

若提示词过多,我们可以设置专门的文件来保存提示词,并且设置身份是传入文件即可,当预设的身份涉及多个参数时,我们要采取 @V 来指定要传递参数的名称

你是我的好朋友,我是{{username}},我的年龄是{{age}},请用东北话回答问题,回答问题的时候适当添加表情
符号。
今天是 {{current_date}}


@SystemMessage(fromResource = "my-prompt-template.txt")//系统消息提示词
    String chat(@MemoryId int memoryId,
                @UserMessage String userMessage,
                @V("username") String username,
                @V("age") int age);



@SpringBootTest
public class PromptTest {

    @Autowired
    private SeparateChatAssistant separateChatAssistant;
    @Test
    public void testSystemMessage() {
        String answer = separateChatAssistant.chat(4,"我是谁,到今天我出生多久了?","zk",134);
        System.out.println(answer);
    }
}

用户提示词 

@UserMessage 获取用户输入的,使用该注解,chat()方法只能传递message一个参数,并且用户提示词时在内存中存储,无法在MangoDB中查看
@AiService(
        wiringMode = EXPLICIT,
        chatModel = "qwenChatModel",
        chatMemory = "chatMemory"
)
public interface MemoryChatAssistant {
    @UserMessage("你是我的好朋友,请用上海话回答问题,并且添加一些表情符号。 {{it}}") //{{it}}表示这里唯一的参数的占位符
    String chat(String message);
}



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

可以看见上述用户消息中,每一个用户消息都包含用户提示词的内容,所以当用户提示词不长的时候,我们可以采取上述方式

如果要传递多个参数,要用@V注解进行传递

实战:小智AI

创建智能体

@AiService(
                wiringMode = EXPLICIT,
                chatModel = "qwenChatModel",
                chatMemoryProvider = "chatMemoryProviderXiaozhi"
        )
        public interface XiaoZhiAgent {
            @SystemMessage(fromResource = "xiaozhi-prompt.txt")
            String chat(@MemoryId String memoryId, @UserMessage String userMessage);

        }

提示词模板

你的名字是“硅谷小智”,你是一家名为“北京协和医院”的智能客服。
你是一个训练有素的医疗顾问和医疗伴诊助手。
你态度友好、礼貌且言辞简洁。
1、请仅在用户发起第一次会话时,和用户打个招呼,并介绍你是谁。
2、作为一个训练有素的医疗顾问:
尚硅⾕
3、配置小智助手
配置持久化和记忆隔离
4、封装对话对象
请基于当前临床实践和研究,针对患者提出的特定健康问题,提供详细、准确且实用的医疗建议。请同时考虑可能的病
因、诊断流程、治疗方案以及预防措施,并给出在不同情境下的应对策略。对于药物治疗,请特别指明适用的药品名
称、剂量和疗程。如果需要进一步的检查或就医,也请明确指示。
3、作为医疗伴诊助手,你可以回答用户就医流程中的相关问题,主要包含以下功能:
AI分导诊:根据患者的病情和就医需求,智能推荐最合适的科室。
AI挂号助手:实现智能查询是否有挂号号源服务;实现智能预约挂号服务;实现智能取消挂号服务。
4、你必须遵守的规则如下:
在获取挂号预约详情或取消挂号预约之前,你必须确保自己知晓用户的姓名(必选)、身份证号(必选)、预约科室
(必选)、预约日期(必选,格式举例:2025-04-14)、预约时间(必选,格式:上午 或 下午)、预约医生(可
选)。
当被问到其他领域的咨询时,要表示歉意并说明你无法在这方面提供帮助。
5、请在回答的结果中适当包含一些轻松可爱的图标和表情。
6、今天是 {{current_date}}。

相关配置信息:用mongodb储存上下文信息,上下文记忆20次对话为界限等信息

@Configuration
public class XiaoZhiAgentConfig {
    @Autowired
    //使用manggoDB作为存储聊天的位置,以及使用mango的增辖改查方式
    private MongoChatMemoryStore mongoChatMemoryStore;
    @Bean
    ChatMemoryProvider chatMemoryProviderXiaozhi() {
        return memoryId -> MessageWindowChatMemory.builder()
                .id(memoryId)
                .maxMessages(20)
                .chatMemoryStore(mongoChatMemoryStore)
                .build();
    }
}

封装对话对象

@Data
public class ChatForm {
    private Long memoryId;//对话id
    private String message;//用户问题

}

实现controller层

@Tag(name = "硅谷小智")
@RestController
@RequestMapping("/xiaozhi")
public class XiaozhiController {
    @Autowired
    private XiaoZhiAgent xiaozhiAgent;
    @Operation(summary = "对话")
    @PostMapping("/chat")
    public String chat(@RequestBody ChatForm chatForm) {
        return xiaozhiAgent.chat(String.valueOf(chatForm.getMemoryId()), chatForm.getMessage());
    }
}

我们就可以在http://localhost:8080/doc.html页面中测试

Tools工具类

大语言模型本身并不擅长数学运算。如果应用场景中偶尔会涉及到数学计算,我们可以为他提供
一个 数学工具 。当我们提出问题时,大语言模型会判断是否使用某个工具。
创建工具类
@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);
    }
}

配置工具类:在智能体中加入tools参数,让智能体知道可以调用相关的工具进行计算

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

然后我们测试一下,可以看到AI回答是,会调用计算工具类的方法。

@SpringBootTest
public class calculateTest {

    @Autowired
    private XiaoZhiAgent xiaoZhiAgent;
    @Test
    public void Tset(){
        String answer = xiaoZhiAgent.chat(String.valueOf(1),"45555+12123是多少?23423856289658的平方根又是多少?");
        System.out.println(answer);
    }
}

整体的流程是:LLM根据用户的问题,判断需不需要调用工具,若需要调用,则返回需要调用的工具,由Langchain4j通过Tools工具调用相关的工具类,工具类将对应的计算结果返回给LLM,LLM通过结合两个内容来回答问题

例如,我们计算45555+12123+2342342+235235是多少?因为我们在定义sum方法的时候传递的时ab两个参数,所以LLM将4个数的相加拆分为三次sum方法的调用

一个方法名无法准确的描述该方法的功能

@Tool 注解有两个可选字段:
name (工具名称) :工具的名称。如果未提供该字段,方法名会作为工具的名称。
value (工具描述) :工具的描述信息。
方法参数可以选择使用 @P 注解进行标注。
@P 注解有两个字段:
value :参数的描述信息,这是必填字段。
required :表示该参数是否为必需项,默认值为 true ,此为可选字段。
如果你的 AIService 方法中有一个参数使用 @MemoryId 注解,那么你也可以使用 @ToolMemoryId 注解 , @Tool 方法中的一个参数。提供给 AIService 方法的值将自动传递给 @Tool 方法。如果你有多个用户, 或每个用户有多个聊天记忆,并且希望在 @Tool 方法中对它们进行区分,那么这个功能会很有用。
@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, 
            @P(value = "被平方根数") double x) {
        System.out.println("调用平方根运算 " + memoryId);
        return Math.sqrt(x);
    }
}

实现预约挂号功能 

实现硅谷小智的查询订单、预约订单、取消订单的功能

Mysql

CREATE DATABASE `guiguxiaozhi`;
USE `guiguxiaozhi`;
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`)
);
<!-- 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>

entity类

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

 Service和impl

@Service
public interface AppointmentService extends IService<Appointment> {
    Appointment getOne(Appointment appointment);
}
@Service
public  class AppointmentServiceImpl extends ServiceImpl<AppointmentMapper, Appointment> implements AppointmentService {
    /**
    * 查询订单是否存在
    * @param
    * @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;
        }
}

controller

@Tag(name = "硅谷小智")
@RestController
@RequestMapping("/xiaozhi")
public class XiaozhiController {
    @Autowired
    private XiaoZhiAgent xiaozhiAgent;
    @Operation(summary = "对话")
    @PostMapping("/chat")
    public String chat(@RequestBody ChatForm chatForm) {
        return xiaozhiAgent.chat(chatForm.getMemoryId(), chatForm.getMessage());
    }
}

创建Tools工具类实现AI预约订单功能

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

在智能体中更新tools,!!!

  • AppointmentTools作为Bean名称appointmentTools才可以正确注册。bean名称都是小写

@AiService(
        wiringMode = EXPLICIT,
         chatModel = "qwenChatModel",
         chatMemoryProvider = "chatMemoryProviderXiaozhi",
         tools = "appointmentTools")

测试,我们在网页上预约挂号后,会在数据库中保存

RAG检索增强生成

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

微调大模型

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

RAG:是一种结合信息检索生成模型的技术,旨在通过外部知识库增强生成内容的准确性和专业性。

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

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

索引

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

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

将文本转化为向量

文档加载器(Document Loader):从多种来源和格式中加载原始文档数据。

解析器(Parser):将原始文档转换为结构化文本,提取关键元素。

分割器(Splitter):将长文本切分为适合模型处理的语义块

常见文档分割器

LangChain4j 有一个 文档分割器 DocumentSplitter )接口,并且提供了几种开箱即用的实现方式:
按段落文档分割器( DocumentByParagraphSplitter
按行文档分割器( DocumentByLineSplitter
按句子文档分割器( DocumentBySentenceSplitter
按单词文档分割器( DocumentByWordSplitter
按字符文档分割器( DocumentByCharacterSplitter
按正则表达式文档分割器( DocumentByRegexSplitter

首先文本转化为向量需要文档加载器 Document Loader对文档进行文本读取

下面代码是对一段测试的文本进行读取

@SpringBootTest
public class RAGTest {
@Test
public void testReadDocument() {
//使用FileSystemDocumentLoader读取指定目录下的知识库文档
//并使用默认的文档解析器TextDocumentParser对文档进行解析
    Document document = FileSystemDocumentLoader.loadDocument("D:/BaiduNetdiskDownload/资料/knowledge/knowledge/测试.txt");
    System.out.println(document.text());
    }
}

读取结果

 测试文档分割器,使用的时基于内存的向量存储,

/**
     * 加载文档并存入向量数据库
     尚硅⾕
     6.3、测试文档分割
     */
    @Test
    public void testReadDocumentAndStore() {
        //使用FileSystemDocumentLoader读取指定目录下的知识库文档
        //并使用默认的文档解析器对文档进行解析(TextDocumentParser)
        Document document = FileSystemDocumentLoader.loadDocument("D:/BaiduNetdiskDownload/资料/knowledge/knowledge/人工智能.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);
    }

可以看到文档分割器按照默认的段落文档分割器将文档分成了17部分,每一块包含一部分文本

实现RAG 

首先在xiaozhiconfig中创建ContentRetriever,用于对外部资源库进行读取解析分割以及向量存储

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

即可在小智AI中实现RAG,比如外部资源库中包含了多个神经内科教授,我们就可以选择其中一个教授进行预约挂号

向量存储 

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

中国智能体开发者社区,聚焦智能体与大模型开发,提供前沿资讯、实用工具链、开源项目及行业案例。通过技术沙龙、开发者大赛等活动,促进经验交流与协作,助力开发者快速构建创新智能应用。

更多推荐