01-LangChain4j
学习代码产出 —> 点击跳转
1. 介绍
LangChain4j 的目标是简化与 Java 应用程序 集成大模型,点击跳转官网

- 特征
langchain4jvsspringAI
| 维度 | Spring AI | LangChain4j |
|---|---|---|
| 技术栈绑定 | 强依赖 Spring 生态 | 无框架依赖,可独立使用 |
| 适用场景 | SpringBoot应用快速接入单模型 | 多模型(动态模型)平台 |
点击查看代码产出
2. 初识(纯java)
- 导入maven依赖
pom.xml
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>com.x</groupId>
<artifactId>LangChain4j_01</artifactId>
<version>1.0-SNAPSHOT</version>
<properties>
<maven.compiler.source>17</maven.compiler.source>
<maven.compiler.target>17</maven.compiler.target>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<langchain4j.version>1.11.0</langchain4j.version>
<dashscope.version>1.11.0-beta19</dashscope.version>
</properties>
<dependencies>
<dependency>
<groupId>dev.langchain4j</groupId>
<artifactId>langchain4j</artifactId>
<version>${langchain4j.version}</version>
</dependency>
<!--OPEN AI-->
<dependency>
<groupId>dev.langchain4j</groupId>
<artifactId>langchain4j-open-ai</artifactId>
<version>${langchain4j.version}</version>
</dependency>
<!--ALI-->
<dependency>
<groupId>dev.langchain4j</groupId>
<artifactId>langchain4j-community-dashscope</artifactId>
<version>${dashscope.version}</version>
</dependency>
<!--ollama-->
<dependency>
<groupId>dev.langchain4j</groupId>
<artifactId>langchain4j-ollama</artifactId>
<version>1.11.0</version>
</dependency>
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>4.13</version>
<scope>test</scope>
</dependency>
</dependencies>
</project>
- 测试代码
public class demo_01 {
/**
* TODO open ai (deepseek差不多)
* */
@Test
public void test01(){
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);
}
/**
* TODO ali的qwen-plus-latest
* */
@Test
public void test02(){
ChatModel qwenModel = QwenChatModel.builder()
.apiKey(System.getenv("API_KEY"))
.modelName("qwen-plus-latest")
.build();
String answer = qwenModel.chat("你好,你是谁?");
System.out.println(answer);
}
/**
* TODO 图像模型
* */
@Test
public void test03() {
WanxImageModel wanxImageModel = WanxImageModel.builder()
.modelName("qwen-image-plus")
.apiKey(System.getenv("API_KEY"))
.build();
Response<Image> response = wanxImageModel.generate("生成一张游戏三角洲里边女英雄麦晓雯的图片");
System.out.println(response.content().url());
}
/**
* TODO 本地ollama的deepseek-r1:1.5b
* */
@Test
public void test04() {
ChatModel model = OllamaChatModel.builder()
.baseUrl("http://192.168.0.115:11434")
.modelName("deepseek-r1:1.5b")
.build();
String answer = model.chat("请帮我介绍一下明朝开国皇帝");
System.out.println(answer);
}
}
点击 ollama官网
3. 集合spring boot
- 依赖
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>3.4.3</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<groupId>com.x</groupId>
<artifactId>demo_02</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>demo_02</name>
<description>demo_02</description>
<properties>
<java.version>17</java.version>
<langchain4j.version>1.0.0-beta1</langchain4j.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</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>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>
3.1 百炼平台
- 依赖
<dependency>
<groupId>dev.langchain4j</groupId>
<artifactId>langchain4j-community-dashscope-spring-boot-starter</artifactId>
</dependency>
- 配置文件
application.properties
#接入别的模型要选择相应的模型的key和名称
langchain4j.community.dashscope.chatModel.apiKey=${API_KEY}
langchain4j.community.dashscope.chatModel.modelName=qwen-plus
- 代码实现
@Autowired
@Qualifier("qwenChatModel")
private ChatLanguageModel chatLanguageModel;
/**
* TODO 接入百炼
* */
@GetMapping("test01")
public String test01(@RequestParam(defaultValue = "你是谁") String message){
String chat = chatLanguageModel.chat(message);
return chat;
}
3.2 接入ollama
前提
- Ollama的本地部署
- 直击官网
- 依赖
<!--本地ollama-->
<dependency>
<groupId>dev.langchain4j</groupId>
<artifactId>langchain4j-ollama-spring-boot-starter</artifactId>
<version>${langchain4j.version}</version>
</dependency>
- 配置文件
application.properties
#ollama配置
langchain4j.ollama.chat-model.base-url=http://192.168.0.115:11434
langchain4j.ollama.chat-model.model-name=deepseek-r1:1.5b
langchain4j.ollama.chat-model.temperature=0.8
langchain4j.ollama.chat-model.timeout=PT60S
- 代码实现
@Autowired
@Qualifier("ollamaChatModel")
private OllamaChatModel ollamaChatModel;
/**
* TODO 本地ollama
* */
@GetMapping("test02")
public String test02(@RequestParam(defaultValue = "你是谁") String message){
String chat = ollamaChatModel.chat(message);
return chat;
}
3.3 流式输出
- 因为langchain4j不是spring家族, 所以我们在wen应用中需要引入webflux
<!--流式输出-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-webflux</artifactId>
</dependency>
- 配置文件
application.properties
# 流式输出
langchain4j.community.dashscope.streaming-chat-model.apiKey=${API_KEY}
langchain4j.community.dashscope.streaming-chat-model.modelName=qwen-plus
- 代码实现
@Autowired
private QwenStreamingChatModel qwenStreamingChatModel;
/**
* TODO 流式返回
* */
@GetMapping(value = "test03",produces ="text/stream;charset=UTF-8")
public Flux<String> test03(@RequestParam(defaultValue = "你是谁") String message){
return Flux.create(sink ->
qwenStreamingChatModel.chat(message, new StreamingChatResponseHandler() {
@Override
public void onPartialResponse(String s) {
System.out.println(s);
sink.next(s);// 逐次返回部分响应
}
@Override
public void onCompleteResponse(ChatResponse chatResponse) {
sink.complete(); // 完成整个响应流
}
@Override
public void onError(Throwable throwable) {
sink.error(throwable); // 异常处理
}
})
);
}
3.4 记忆对话
3.4.1 原生方式
原生方式(不推荐),大模型并不会把我们每次的对话存在服务端, 所以他记不住我们说的话
所以每次对话都需要将之前的对话记录,都发给大模型, 这样才能知道我们之前说了什么
- 这里使用
OpenAiChatModel,所以需要引入相关的依赖
<!--open ai-->
<dependency>
<groupId>dev.langchain4j</groupId>
<artifactId>langchain4j-open-ai-spring-boot-starter</artifactId>
<version>${langchain4j.version}</version>
</dependency>
- 原生方式
@Test
void test01() {
ChatLanguageModel model = OpenAiChatModel
.builder()
.baseUrl("http://langchain4j.dev/demo/openai/v1")
.apiKey("demo")
.modelName("gpt-4o-mini")
.build();
UserMessage userMessage1 = UserMessage.userMessage("你好,我是小明");
ChatResponse response1 = model.chat(userMessage1);
AiMessage aiMessage1 = response1.aiMessage(); // 大模型的第一次响应
System.out.println(aiMessage1.text());
System.out.println("-------------------------------");
// 下面一行代码是重点 (上次提的问题 + 上次回答 + 这次问题)都要进行提供
ChatResponse response2 = model.chat(userMessage1, aiMessage1, UserMessage.userMessage("我叫什么"));
AiMessage aiMessage2 = response2.aiMessage(); // 大模型的第二次响应
System.out.println(aiMessage2.text());
}
3.4.2 ChatMemory
使用
ChatMemory
原理
- 通过AiService创建的代理对象(),调用chat方法
- 代理对象会去ChatMemory中获取之前的对话记录(获取记忆)
- 将获取到的对话记录合并到当前对话中(此时大模型根据之前的聊天记录肯定就拥有了“记忆”)
- 将当前的对话内容存入ChatMemory(保存记忆)

- 引入依赖
<!--langchain4j-->
<dependency>
<groupId>dev.langchain4j</groupId>
<artifactId>langchain4j</artifactId>
<version>${langchain4j.version}</version>
</dependency>
- 创建
AiConfig
@Configuration
public class AiConfig2 {
public interface Assistant {
String chat(String message);
// 流式响应
TokenStream stream(String message);
}
@Bean
public Assistant assistant(ChatLanguageModel qwenChatModel,
StreamingChatLanguageModel qwenStreamingChatModel) {
// 最多十条
ChatMemory chatMemory = MessageWindowChatMemory.withMaxMessages(10); // 需要引入langchain4j包
Assistant assistant = AiServices.builder(Assistant.class)
.chatLanguageModel(qwenChatModel)
.streamingChatLanguageModel(qwenStreamingChatModel)
.chatMemory(chatMemory)
.build();
return assistant;
}
}
- 进行代码测试
@Autowired
private AiConfig.Assistant assistant;
/**
* TODO 记忆存储 进行记忆储存
* */
@GetMapping("memory_chat")
public String memoryChat(@RequestParam(defaultValue = "我是小明") String message){
return assistant.chat(message);
}
/**
* TODO 记忆证明
* */
@GetMapping(value = "/memory_stream_chat",produces ="text/stream;charset=UTF-8")
public Flux<String> memoryStreamChat(@RequestParam(defaultValue="我是谁") String message, HttpServletResponse response) {
TokenStream stream = assistant.stream(message);
return Flux.create(sink -> {
stream.onPartialResponse(s -> sink.next(s))
.onCompleteResponse(c -> sink.complete())
.onError(sink::error)
.start();
});
}
3.4.3 记忆分离
记忆分离:不同的用户或者不同的对话肯定不能用同一个记忆,要不然对话肯定会混淆,此时就需要进行区分
原理:
- 通过AiService创建的代理对象()调用chat方法传入id
- 代理对象会去ChatMemory中根据id获取之前的对话记录(获取记忆)
- 将获取到的对话记录合并到当前对话中(此时大模型根据之前的聊天记录肯定就拥有了“记忆”)
- 将当前的对话内容根据id存入ChatMemory(保存记忆)

- 配置config
@Configuration
public class AiConfig {
public interface AssistantUnique {
String chat(@MemoryId int memoryId, @UserMessage String userMessage);
// 流式响应
TokenStream stream(@MemoryId int memoryId, @UserMessage String userMessage);
}
@Bean
public AssistantUnique assistantUnique(ChatLanguageModel qwenChatModel,
StreamingChatLanguageModel qwenStreamingChatModel) {
AssistantUnique assistant = AiServices.builder(AssistantUnique.class)
.chatLanguageModel(qwenChatModel)
.streamingChatLanguageModel(qwenStreamingChatModel)
.chatMemoryProvider(memoryId ->
MessageWindowChatMemory.builder().maxMessages(10)
.id(memoryId).build()
)
.build();
return assistant;
}
}
- 进行测试
@Autowired
private AiConfig.AssistantUnique assistantUnique;
/**
* TODO 记忆分离
* */
@GetMapping("memory_chat")
public String memoryChat(@RequestParam(defaultValue = "我是小明") String message,@RequestParam Integer userId){
return assistantUnique.chat(userId,message);
}
/**
* TODO 记忆证明
* */
@GetMapping(value = "/memory_stream_chat",produces ="text/stream;charset=UTF-8")
public Flux<String> memoryStreamChat(@RequestParam(defaultValue="我是谁") String message,@RequestParam Integer userId) {
TokenStream stream = assistantUnique.stream(userId,message);
return Flux.create(sink -> {
stream.onPartialResponse(s -> sink.next(s))
.onCompleteResponse(c -> sink.complete())
.onError(sink::error)
.start();
});
}
3.4.4 持久化对话
持久化对话:可以配置一个
ChatMemoryStore,
- 默认是InMemoryChatMemoryStore——通过一个map进行存储
- 所以如果需要持久化到第三方存储, 可以重新配置ChatMemoryStore
- 自定义持久化存储
@Slf4j
@Component
public class PersistentChatMemoryStore implements ChatMemoryStore {
@Override
public List<ChatMessage> getMessages(Object o) {
// todo 根据memoryId从数据库获取
log.info("getMessages:{}", o);
return List.of();
}
@Override
public void updateMessages(Object o, List<ChatMessage> list) {
// todo 根据memoryId修改、新增记录
log.info("updateMessages:{},list:{}", o,list);
}
@Override
public void deleteMessages(Object o) {
// todo 根据memoryId删除
log.info("deleteMessages:{}", o);
}
}
- 进行配置
@Configuration
public class AiConfig {
public interface AssistantUnique {
String chat(@MemoryId int memoryId, @UserMessage String userMessage);
// 流式响应
TokenStream stream(@MemoryId int memoryId, @UserMessage String userMessage);
}
@Bean
public AssistantUnique assistantUniqueStore(ChatLanguageModel qwenChatModel,
StreamingChatLanguageModel qwenStreamingChatModel) {
PersistentChatMemoryStore store = new PersistentChatMemoryStore();
ChatMemoryProvider chatMemoryProvider = memoryId -> MessageWindowChatMemory.builder()
.id(memoryId)
.maxMessages(10)
.chatMemoryStore(store)
.build();
AssistantUnique assistant = AiServices.builder(AssistantUnique.class)
.chatLanguageModel(qwenChatModel)
.streamingChatLanguageModel(qwenStreamingChatModel)
.chatMemoryProvider(memoryId ->
MessageWindowChatMemory.builder().maxMessages(10)
.id(memoryId).build()
)
.chatMemoryProvider(chatMemoryProvider)
.build();
return assistant;
}
}
3.4.5 Function-call (Tools)
- 对于基础大模型来说, 只具备通用信息,他的参数都是拿公网进行训练,并且有一定的时间延迟, 无法得知一些具体业务数据和实时数据, 这些数据往往被各软件系统存储在自己数据库中:
- 比如我问大模型:“中国有多少叫xxx的人” 他肯定不知道, 我们就需要去调用政务系统的接口。
- 比如我现在开发一个智能票务助手, 我现在跟AI说需要退票, AI怎么做到呢? 就需要让AI调用我们自己系统的退票业务方法,进行操作数据库。
- 那这些都可以通过
function-call进行完成,更多的用于实现类似智能客服场景,因为客服需要帮用户解决业务问题(就需要调用业务方法)。
function-call的流程
- 我现在需要当对话中用户问的是“中国有多少叫xxx的人”的对话, 我需要去我程序中获取
- 问大模型 中国有多少叫xxx的人
- 大模型在识别到你的问题是: “中国有多少叫xxx的人”
- 大模型提取“xxx”
- 调用相关方法得到结果
- 通过返回的结果再结合上下文再次请求大模型
- 响应“中国有多少叫xxx的人"

- 加入回调方法
- ToolsService配置为了一个bean
- @Tool 用于告诉AI什么对话调用这个方法
- @P(“姓名”) 用于告诉AI ,调用方法的时候需要提取对话中的什么信息, 这里提取的是姓名
所以, 你如果需要加更多的tool. 只需要在TollsService中加
@Service
@Slf4j
public class ToolsService {
/**
* TODO 进行工具调用的简单大模型配置
* */
public interface AssistantUniqueByTools {
String chat(@MemoryId int memoryId, @UserMessage String userMessage);
// 流式响应
TokenStream stream(@MemoryId int memoryId, @UserMessage String userMessage);
}
@Bean
public AssistantUniqueByTools assistantUniqueByTools(ChatLanguageModel qwenChatModel,
StreamingChatLanguageModel qwenStreamingChatModel,
ToolsService toolsService
) {
AssistantUniqueByTools assistant = AiServices.builder(AssistantUniqueByTools.class)
.chatLanguageModel(qwenChatModel)
.streamingChatLanguageModel(qwenStreamingChatModel)
.tools(toolsService)
.chatMemoryProvider(memoryId ->
MessageWindowChatMemory.builder().maxMessages(10)
.id(memoryId).build()
)
.build();
return assistant;
}
}
- 接口api
/**
* TODO 大模型大处理的时候调用工具
* */
@Autowired
private AiConfig.AssistantUniqueByTools assistantUniqueByTools;
@GetMapping(value = "/assistantUniqueByTools",produces ="text/stream;charset=UTF-8")
public Flux<String> assistantUniqueByTools(@RequestParam(defaultValue="河北有多少个小明") String message,@RequestParam Integer userId) {
TokenStream stream = assistantUniqueByTools.stream(userId,message);
return Flux.create(sink -> {
stream.onPartialResponse(s -> sink.next(s))
.onCompleteResponse(c -> sink.complete())
.onError(sink::error)
.start();
});
}
3.4.6 预设角色(系统消息SystemMessage)
- 基础大模型是没有目的性的, 你聊什么给什么, 但是如果我们开发的事一个智能票务助手, 我需要他以一个票务助手的角色跟我对话, 并且在我跟他说"退票"的时候, 让大模型一定要告诉我“车次”和"姓名" ,这样我才能去调用业务方法(假设有一个业务方法,需要根据车子和姓名才能查询具体车票),进行退票。

在langchain4j中实现也非常简单
- @SystemMessage 系统消息, 一般做一些预设角色的提示词,设置大模型的基本职责
- 可以通过{{current_date}} 传入参数, 因为预设词中的文本可能需要实时变化
- @V(“current_date”), 通过@V传入{{}}中的参数
- 一旦参数不止一个, 就需要通过@UserMessage设置用户信息
另外:假设大模型不支持系统消息(一般都支持),可以用@UserMessage代替@SystemMessag
- 进行配置
@Configuration
public class AiConfig {
/**
* TODO 进行工具调用的简单大模型配置
* */
public interface AssistantUniqueByTools {
String chat(@MemoryId int memoryId, @UserMessage String userMessage);
// 流式响应
TokenStream stream(@MemoryId int memoryId, @UserMessage String userMessage);
@SystemMessage("""
您是“xxx”航空公司的客户聊天支持代理。请以友好、乐于助人且愉快的方式来回复。
您正在通过在线聊天系统与客户互动。
在提供有关预订或取消预订的信息之前,您必须始终从用户处获取以下信息:预订号、客户姓名。
请讲中文。
今天的日期是 {{current_date}}.
""")
TokenStream stream(@MemoryId int memoryId, @UserMessage String userMessage,@V("current_date") String currentDate);
}
@Bean
public AssistantUniqueByTools assistantUniqueByTools(ChatLanguageModel qwenChatModel,
StreamingChatLanguageModel qwenStreamingChatModel,
ToolsService toolsService
) {
AssistantUniqueByTools assistant = AiServices.builder(AssistantUniqueByTools.class)
.chatLanguageModel(qwenChatModel)
.streamingChatLanguageModel(qwenStreamingChatModel)
.tools(toolsService)
.chatMemoryProvider(memoryId ->
MessageWindowChatMemory.builder().maxMessages(10)
.id(memoryId).build()
)
.build();
return assistant;
}
}
- 工具准备调用
@Service
@Slf4j
public class ToolsService {
@Tool("描述某个地区有多少叫什么姓名的人")
public String callToolsName(@P("姓名") String name, @P("地区") String area) {
// 调用工具
log.info("在{}地区有多少叫{}姓名的人", area, name);
return "在"+ area +"地区有10000叫"+ name +"姓名的人";
}
@Tool("描述航空公司要求预定或者取消票,要求的姓名和预定号")
public String callToolsName1(@P("姓名") String name, @P("预定号") String num) {
// 调用工具
log.info("姓名为:{}的班机号{}要求更改", name,num);
return "姓名为:"+ name +"的班机号"+ num +"要求更改";
}
}
- api接口进行调用
/**
* TODO 大模型在使用的时候
* */
@GetMapping(value = "/assistantUniqueByToolsUse",produces ="text/stream;charset=UTF-8")
public Flux<String> assistantUniqueByToolsUse(@RequestParam(defaultValue="G18航班的小明要求退票") String message,@RequestParam Integer userId) {
TokenStream stream = assistantUniqueByTools.stream(userId,message, String.valueOf(new Date()));
return Flux.create(sink -> {
stream.onPartialResponse(s -> sink.next(s))
.onCompleteResponse(c -> sink.complete())
.onError(sink::error)
.start();
});
}
4. RAG
- 检索增强生成(Retrieval-augmented Generation)
- 对于基础大模型来说, 他只具备通用信息,他的参数都是拿公网进行训练,并且有一定的时间延迟, 无法得知一些具体业务数据和实时数据, 这些数据往往在各种文件中(比如txt、word、html、数据库…)
- 虽然function-call、SystemMessage可以用来解决一部分问题,但是它只能少量, 如果你要提供大量的业务领域信息, 就需要给他外接一个知识库
比如
- 我问他退订要多少费用
- 这些资料可能都由产品或者需求编写在了文档中:
- 所以需要现在需求信息存到向量数据库(这个过程叫Embedding, 涉及到文档读取、分词、向量化存入)
- 去向量数据库中查询“退订费用相关信息”
- 将查询到的数据和对话信息再请求大模型
- 此时会响应退订需要多少费用

4.1 相关概念
4.1.1 向量
- 向量通常用来做相似性搜索,比如语义的一维向量,可以表示词语或短语的语义相似性。例如,“你好”、“hello”和“见到你很高兴”可以通过一维向量来表示它们的语义接近程度。

- 然而,对于更复杂的对象,比如小狗,无法仅通过一个维度来进行相似性搜索。这时,我们需要提取多个特征,如颜色、大小、品种等,将每个特征表示为向量的一个维度,从而形成一个多维向量。例如,一只棕色的小型泰迪犬可以表示为一个多维向量 [棕色, 小型, 泰迪犬]。

- 如果需要检索见过更加精准, 我们肯定还需要更多维度的向量, 组成更多维度的空间,在多维向量空间中,相似性检索变得更加复杂。我们需要使用一些算法,如余弦相似度或欧几里得距离,来计算向量之间的相似性。向量数据库会帮我实现。
4.1.2 文本向量化
- LangChain4j中来调用向量模型来对一句话进行向量化体验
@Test
void test02(){
// 使用向量模型
/**
* TODO QwenEmbeddingModel 类中的 默认对象模型代码
* this.modelName = Utils.isNullOrBlank(modelName) ? "text-embedding-v2" : modelName;
* */
QwenEmbeddingModel model = QwenEmbeddingModel.builder()
.apiKey(System.getenv("API_KEY"))
.build();
// 进行向量存储
Response<Embedding> embed = model.embed("你好,我是小明");
log.info(embed.content().toString());// float数组 转换成向量
log.info(String.valueOf(embed.content().vector().length));// 数组长度
}
Embedding { vector = [-0.020235416, 0.005272401, ..., 0.007930692] }
1536
- 从结果可以知道"你好,我是小明"这句话经过OpenAiEmbeddingModel向量化之后得到的一个长度为1536的float数组。注意,1536是固定的,不会随着句子长度而变化。
- 那么,我们通过这种向量模型得到一句话对应的向量有什么作用呢?非常有用,因为我们可以基于向量来判断两句话之间的相似度,举个例子:
- 查询跟秋田犬类似的狗, 在向量数据库中根据每个狗的特点进行多维向量, 你会发现秋田犬的向量数值和柴犬的向量数值最接近, 就可以查到类似的狗。

4.1.3 向量数据库
- 对于向量模型生成出来的向量,我们可以持久化到向量数据库,并且能利用向量数据库来计算两个向量之间的相似度,或者根据一个向量查找跟这个向量最相似的向量。
- 在LangChain4j中,EmbeddingStore表示向量数据库,它有支持20+ 嵌入模型
| Embedding Store | Storing Metadata | Filtering by Metadata | Removing Embeddings |
|---|---|---|---|
| In-memory | ✅ | ✅ | ✅ |
| Astra DB | ✅ | ||
| Azure CosmosDB Mongo vCore | ✅ | ||
| Azure CosmosDB NoSQL | ✅ | ||
| Cassandra | ✅ | ||
| Chroma | ✅ | ✅ | ✅ |
| ClickHouse | ✅ | ✅ | ✅ |
| Coherence | ✅ | ✅ | ✅ |
| Couchbase | ✅ | ||
| DuckDB | ✅ | ✅ | ✅ |
| Elasticsearch | ✅ | ✅ | ✅ |
| Infinispan | ✅ | ||
| Milvus | ✅ | ✅ | ✅ |
| MongoDB Atlas | ✅ | ✅ | ✅ |
| Neo4j | ✅ | ||
| OpenSearch | ✅ | ||
| Oracle | ✅ | ✅ | ✅ |
| PGVector | ✅ | ✅ | ✅ |
| Pinecone | ✅ | ✅ | ✅ |
| Qdrant | ✅ | ✅ | ✅ |
| Redis | ✅ | ||
| Tablestore | ✅ | ✅ | ✅ |
| Vearch | ✅ | ||
| Vespa | |||
| Weaviate | ✅ | ✅ |
- 其中有我们熟悉的几个数据库都可以用来存储向量,比如Elasticsearch、MongoDb、Neo4j、Pg、Redis。下边主要通过In-memory方式演示使用流程。
演示redis(到那时这里使用的是内存存储,内存比较宝贵推荐使用磁盘存储)
- Redis也很简单, 你需要先安装redis7.0+的版本:其他的向量数据库不做介绍
<dependency>
<groupId>dev.langchain4j</groupId>
<artifactId>langchain4j-redis</artifactId>
<version>${langchain4j.version}</version>
</dependency>
- 然后需要注意的是,普通的Redis是不支持向量存储和查询的,需要额外的redisearch模块,我这边是直接使用docker来运行一个带有redisearch模块的redis容器的,命令为:
docker run -p 6379:6379 redis/redis-stack-server:latest
注意端口6379不要和你现有的Redis冲突了。
- 然后就可以使用以下代码把向量存到redis中了:
RedisEmbeddingStore embeddingStore = RedisEmbeddingStore.builder()
.host("127.0.0.1")
.port(6379)
.dimension(1536)
.build();
// 生成向量
Response<Embedding> embed = embeddingModel.embed("我是小明");
// 存储向量
embeddingStore.add(embed.content());
- dimension表示要存储的向量的维度,所以为1536,如果你不是使用OpenAiEmbeddingModel得到的向量,那么维度可能会不一样。
- 可以使用以下命令来清空:
redis-cli FT.DROPINDEX embedding-index DD
4.1.4 匹配向量
- 分别存储了预订航班和取消预订2段说明到向量数据库中,然后通过"退票要多少钱" 进行查询

- 代码实现
/**
* TODO 匹配向量
* */
@Test
void test03(){
InMemoryEmbeddingStore<TextSegment> embeddingStore = new InMemoryEmbeddingStore<>(); // 使用 in-memory
QwenEmbeddingModel model = QwenEmbeddingModel.builder()
.apiKey(System.getenv("API_KEY"))
.build();
// 利用向量模型进行向量化,然后存储到向量数据库中
TextSegment textSegment1 = TextSegment.from("""
预订航班:
- 通过我们的网站或移动应用程序预订。
- 预订时需要全额付款。
- 确保个人信息(姓名、ID 等)的准确性,因为更正可能会产生 25 的费用。
""");
Embedding content1 = model.embed(textSegment1).content();
embeddingStore.add(content1,textSegment1); // 进行向量存储
// 利用向量模型进行向量化,然后存储到向量数据库中
TextSegment textSegment2 = TextSegment.from("""
取消预订:
- 最晚在航班起飞前 48 小时取消。
- 取消费用:经济舱 75 美元,豪华经济舱 50 美元,商务舱 25 美元。
- 退款将在 7 个工作日内处理。
""");
Embedding content2 = model.embed(textSegment2).content();
embeddingStore.add(content2,textSegment2); // 进行向量存储
// 需要查询的内容,向量化
Embedding content = model.embed("退票需要多少钱").content();
// 去向量库进行查询
// 构建查询条件 还可以设置匹配率为多少以上才会被查询出来
EmbeddingSearchRequest build = EmbeddingSearchRequest.builder()
.queryEmbedding(content) // 查询内容的向量化
.maxResults(2) // 最大查询结果数量
.minScore(0.7) // 设置相似度阈值
.build();
EmbeddingSearchResult<TextSegment> search = embeddingStore.search(build);
search.matches().forEach(item -> {
log.info(String.valueOf(item.score())); //匹配率
log.info(item.embedded().text()); // 匹配的内容
});
// ----向量模型已经匹配,下边是叫大模型进行回答输出----
StringBuilder contextBuilder = new StringBuilder();
search.matches().forEach(match -> {
contextBuilder.append(match.embedded().text()).append("\n\n");
});
String context = contextBuilder.toString().trim();
// 大语言模型(用于生成回答)
QwenChatModel chatModel = QwenChatModel.builder()
.apiKey(System.getenv("API_KEY"))
.modelName("qwen-max") // 或其他模型如 qwen-plus, qwen-max
.build();
String systemPrompt = """
你是一个专业的客服助手。请根据以下提供的参考信息回答用户的问题。
如果参考信息不足以回答问题,请说明无法回答。
参考信息:
%s
""".formatted(context);
UserMessage userMessage = UserMessage.from("退票需要多少钱");
SystemMessage from = SystemMessage.from(systemPrompt);
List<ChatMessage> messages = Arrays.asList(from, userMessage);
Response<AiMessage> response = chatModel.generate(messages);
log.info(response.content().text());
}
- 结果
0.7146489962563928
取消预订:
- 最晚在航班起飞前 48 小时取消。
- 取消费用:经济舱 75 美元,豪华经济舱 50 美元,商务舱 25 美元。
- 退款将在 7 个工作日内处理。
退票费用根据您预订的舱位不同而有所差异:
- 经济舱的取消费用为 75 美元。
- 豪华经济舱的取消费用为 50 美元。
- 商务舱的取消费用为 25 美元。
请注意,这些费用适用于在航班起飞前至少48小时进行取消的情况。如果在这个时间之后取消,可能有不同的政策或无法获得退款。此外,退款将在7个工作日内处理。如果您有其他特殊情况或者疑问,建议直接联系航空公司确认。
4.2 知识库RAG演练

4.2.1 Document Loaders 文档读取器
分片 -> 向量 -> 存入向量库

- 读取为文档
// 读取为文档 (读取文件)
Document document = ClassPathDocumentLoader.loadDocument("./terms-of-service.txt", new TextDocumentParser());
- Document Parser 文档解析器
- 如果要开发一个知识库系统, 这些资料可能在各种文件中, 比如word、txt、pdf、image、html等等, 所以langchain4j也提供了不同的文档解析器:
- TextDocumentParser来自 langchain4j 模块的 TextDocumentParser,它可以解析纯文本格式(e.g. TXT、HTML、MD 等)的文件。
- ApachePdfBoxDocumentParser来自langchain4j-document-parser-apache-pdfbox ,它可以解析 PDF 文件
- ApachePoiDocumentParser来自langchain4j-document-parser-apache-poi ,可以解析 MS Office 文件格式(e.g. DOC、DOCX、PPT、PPTX、XLS、XLSX 等)
- ApacheTikaDocumentParser来自 langchain4j-document-parser-apache-tika 模块中,可以自动检测和解析几乎所有现有的文件格式
- 在这里我来解析一份这个txt文件, 所以我们用TextDocumentParser随便放在
reources/rag/TextDocumentParser
#TextDocumentParser
本服务条款适用于您对 xxxx 的体验。预订航班,即表示您同意这些条款。
1. 预订航班
- 通过我们的网站或移动应用程序预订。
- 预订时需要全额付款。
- 确保个人信息(姓名、ID 等)的准确性,因为更正可能会产生 25 的费用。
2. 更改预订
- 允许在航班起飞前 24 小时更改。
- 通过在线更改或联系我们的支持人员。
- 改签费:经济舱 50,豪华经济舱 30,商务舱免费。
3. 取消预订
- 最晚在航班起飞前 48 小时取消。
- 取消费用:经济舱 75 美元,豪华经济舱 50 美元,商务舱 25 美元。
- 退款将在 7 个工作日内处理。
// 获取resource里边的文件
Path path = Paths.get(this.getClass().getClassLoader().getResource("rag/terms-of-service.txt").toURI());
TextDocumentParser textDocumentParser = new TextDocumentParser();
// 读取为文档 (读取文件)
Document document1 = FileSystemDocumentLoader.loadDocument(path, textDocumentParser);
4.2.2 DocumentSplitter 文档拆分器

- 由于文本读取过来后, 还需要分成一段一段的片段(分块chunk), 分块是为了更好地拆分语义单元,这样在后面可以更精确地进行语义相似性检索,也可以避免LLM的Token限制。
- langchain4j也提供了不同的文档拆分器:
| 分词器类型 | 匹配能力 | 适用场景 |
|---|---|---|
| DocumentByCharacterSplitter | 无符号分割 | 就是严格根据字数分隔(不推荐,会出现断句) |
| DocumentByRegexSplitter | 正则表达式分隔 | 根据自定义正则分隔 |
| DocumentByParagraphSplitter | 删除大段空白内容 | 处理连续换行符(如段落分隔)(\\s*(?>\\R)\\s*(?>\\R)\\s* |
| DocumentByLineSplitte | 删除单个换行符周围的空白, 替换一个换行 | (\\s*\\R\\s*)● 示例:○ 输入文本:"This is line one.\n\tThis is line two."○ 使用 \s*\R\s* 替换为单个换行符:“This is line one.\nThis is line two.” |
| DocumentByWordSplitter | 删除连续的空白字符。 | \\s+● 示例:○ 输入文本:“Hello World” ○ 使用 \s+ 替换为单个空格:“Hello World” |
| DocumentBySentenceSplitter | 按句子分割 | 该分割器使用Apache OpenNLP 库中的一个类,用于检测文本中的句子边界。它能够识别标点符号(如句号、问号、感叹号等)是否标记着句子的末尾,从而将一个较长的文本字符串分割成多个句子。 |
这里选DocumentByLineSplitter, 因为内容不多, 所以其实没有特别大的关系, 后面如果大家有兴趣我详细讲解每一种的应用场景。
- 代码,将第一步读取到的文档进行分割
// 将读取到的文档进行分割
DocumentByCharacterSplitter splitter = new DocumentByCharacterSplitter(
20,// 每段最大长度
10 // 自然语言最大重叠字数
);
List<TextSegment> split = splitter.split(document1);
- chunk_size(块大小)指的就是我们分割的字符块的大小;chunk_overlap(块间重叠大小)就是下图中加深的部分,上一个字符块和下一个字符块重叠的部分,即上一个字符块的末尾是下一个字符块的开始。

- 在使用按字符切分时,需要指定分割符,另外需要指定块的大小以及块之间重叠的大小(允许重叠是为了尽可能地避免按照字符进行分割造成的语义损失)
#比如
- 最晚在航班起飞前 48 小时取消。取消费用:经济舱 75 美元,豪华经济舱 50 美元,
商务舱 25 美元。退款将在 7 个工作日内处理。
按照chunksize可能会分隔成:
最晚在航班起飞前 48 小时取消。取消费用:经济舱 7
如果设置了重叠可能会:
-最晚在航班起飞前 48 小时取消。取消费用:经济舱 75 美元,豪华经济舱 50 美元,商务舱 25 美元。
-取消费用:经济舱 75 美元,豪华经济舱 50 美元,商务舱 25 美元。退款将在 7 个工作日内处理。
- 分块流程

首先按照指定的分割符进行切分,切分过之后,如果块的长度小于 chunk_size 的大小,则进行块之间的合并。在进行合并时,遵循下面的规则:
- 如果相邻块加在一起的长度小于或等于chunk_size,则进行合并;否则看你有没有子分割器,如果没有报错。
- 在进行合并时,如果块的大小小于或等于chunk_overlap,并且和前后两个相邻块合并后,两个合并后的块均不超过chunk_size,则两个合并后的块允许有重叠
在RAG系统中,文本分块的粒度需要平衡语义完整性与计算效率,并非越细越好。以下是关键考量点:参考分割经验
分隔经验
- 过细分块的潜在问题
- 语义割裂: 破坏上下文连贯性,影响模型理解
- 计算成本增加:分块过细会导致向量嵌入和检索次数增多,增加时间和算力开销。
- 信息冗余与干扰:碎片化的文本块可能引入无关内容,干扰检索结果的质量,降低生成答案的准确性。
- 分块过大的弊端
- 信息丢失风险:过大的文本块可能超出嵌入模型的输入限制,导致关键信息未被有效编码。
- 检索精度下降:大块内容可能包含多主题混合,与用户查询的相关性降低,影响模型反馈效果。
| 场景 | 分块策略 | 参数参考 |
|---|---|---|
| 微博/短文本 | 句子级分块,保留完整语义 | 每块100-200字符 |
| 学术论文 | 段落级分块,叠加10%重叠 | 每块300-500字符 |
| 法律合同 | 条款级分块,严格按条款分隔 | 每块200-400字符 |
| 长篇小说 | 章节级分块,过长段落递归拆分为段落 | 每块500-1000字符 |
- 固定长度分块
- 字符数范围:通常建议每块控制在 100-500字符(约20-100词),以平衡上下文完整性与检索效率
- 重叠比例:相邻块间保留 10-20%的重叠内容(如块长500字符时重叠50-100字符),减少语义断层
- 语义分块
- 段落或章节:优先按自然段落、章节标题划分,保持逻辑单元完整
- 动态调整:对于长段落,可递归分割为更小单元(如先按段落分块,过长时再按句子拆分)
- 专业领域调整
- 高信息密度文本(如科研论文、法律文件):采用更细粒度分块(100-200字符),保留专业术语细节
- 通用文本(如新闻、社交媒体):适当放宽分块大小(300-500字符)
4.2.3 文本向量化

向量化存储之前在”文本向量化“介绍了,就是通过向量模型进行向量化,下边仍然通过qwen向量模型进行向量化,将之前分割的进行向量化
// 通过向量模型进行向量化
QwenEmbeddingModel embeddingModel = QwenEmbeddingModel.builder()
.apiKey(System.getenv("API_KEY"))
.build();
List<Embedding> content = embeddingModel.embedAll(split).content();
4.2.4 存储向量

- 选择向量库进行存储
// 使用向量库进行存储
InMemoryEmbeddingStore<TextSegment> embeddingStore = new InMemoryEmbeddingStore<>();
embeddingStore.addAll(content,split);
4.2.5 向量数据库检索
- 先将文本向量化,然后去向量库查询匹配
// 将查询内容向量化,之后去向量数据库进行查询
Response<Embedding> embed = embeddingModel.embed("退费费用");
EmbeddingSearchRequest build = EmbeddingSearchRequest.builder().queryEmbedding(embed.content()).build();
// 查询
EmbeddingSearchResult<TextSegment> search = embeddingStore.search(build);
search.matches().forEach(item -> {
log.info("匹配的内容:{},分数为:{}",item.embedded().text(),item.score()); // 匹配的内容
});
4.2.6 完整代码
/**
* TODO 分片
* */
@SneakyThrows
@Test
void test04(){
// 获取resource里边的文件
Path path = Paths.get(this.getClass().getClassLoader().getResource("rag/terms-of-service.txt").toURI());
TextDocumentParser textDocumentParser = new TextDocumentParser();
// 读取为文档 (读取文件)
Document document1 = FileSystemDocumentLoader.loadDocument(path, textDocumentParser);
// 将读取到的文档进行分割
DocumentByCharacterSplitter splitter = new DocumentByCharacterSplitter(
20,// 每段最大长度
10 // 自然语言最大重叠字数
);
List<TextSegment> split = splitter.split(document1);
// 通过向量模型进行向量化
QwenEmbeddingModel embeddingModel = QwenEmbeddingModel.builder()
.apiKey(System.getenv("API_KEY"))
.build();
List<Embedding> content = embeddingModel.embedAll(split).content();
// 使用向量库进行存储
InMemoryEmbeddingStore<TextSegment> embeddingStore = new InMemoryEmbeddingStore<>();
embeddingStore.addAll(content,split);
// 将查询内容向量化,之后去向量数据库进行查询
Response<Embedding> embed = embeddingModel.embed("退费费用");
EmbeddingSearchRequest build = EmbeddingSearchRequest.builder().queryEmbedding(embed.content()).build();
// 查询
EmbeddingSearchResult<TextSegment> search = embeddingStore.search(build);
search.matches().forEach(item -> {
log.info("匹配的内容:{},分数为:{}",item.embedded().text(),item.score()); // 匹配的内容
});
}
4.2.7 对话阶段

在4.2.7完整代码基础上进行编写
// 对话阶段
QwenChatModel qwenChatModel = QwenChatModel.builder().apiKey(System.getenv("API_KEY")).modelName("qwen-max").build();
EmbeddingStoreContentRetriever retriever = EmbeddingStoreContentRetriever.builder()
.embeddingStore(embeddingStore)
.embeddingModel(embeddingModel)
.maxResults(5)// 最相似的五个结果
.minScore(0.6)// 只找相似度0.6以上的
.build();
// 为Assistant动态代理对象 chat ----> 对话内容存储chatMemory ----> 放入当前对话中
Assistant build1 = AiServices.builder(Assistant.class)
.chatLanguageModel(qwenChatModel)
.contentRetriever(retriever)
.build();
log.info(build1.chat("退费费用"));
Assistant
public interface Assistant {
String chat(String message);
}
- AiService向量检索原理

4.2.8 整合SpringBoot
最终其实还会将查询到的内容, 和对话上下文组合起来, 发给LLM为我们组织语言进行回答。
这一步我们直接整合进SpringBoot进行实战:
- 配置一个Content Retriever 内容检索器
a. 提供向量数据库和向量模型及其他参数- 将内容检索器绑定到AiServices
- 当我们进行LLM对话时, 底层会自动为我们检索向量数据库进行回答。
- 向量模型配置
#langchain4j设置
langchain4j:
community:
dashscope:
chatModel:
apiKey: ${API_KEY}
modelName: qwen-plus
# 流式
streamingChatModel:
apiKey: ${API_KEY}
modelName: qwen-plus
# 向量模型配置
embeddingModel:
apiKey: ${API_KEY}
# modelName: qwen-plus
- Assistant配置
/**
* TODO RAG文档内容检索
* */
@Bean
public EmbeddingStore embeddingStore(){
return new InMemoryEmbeddingStore();
}
public interface AssistantRAG {
String chat(@MemoryId int memoryId, @UserMessage String userMessage);
// 流式响应
TokenStream stream(@MemoryId int memoryId, @UserMessage String userMessage);
}
@Bean
public AssistantRAG assistantARG(ChatLanguageModel qwenChatModel,
StreamingChatLanguageModel qwenStreamingChatModel,
ToolsService toolsService,
EmbeddingStore embeddingStore,
QwenEmbeddingModel qwenEmbeddingModel
) {
EmbeddingStoreContentRetriever retriever = EmbeddingStoreContentRetriever.builder()
.embeddingStore(embeddingStore)
.embeddingModel(qwenEmbeddingModel)
.maxResults(5)// 最相似的五个结果
.minScore(0.6)// 只要相似大于0.6
.build();
AssistantRAG assistant = AiServices.builder(AssistantRAG.class)
.chatLanguageModel(qwenChatModel)
.streamingChatLanguageModel(qwenStreamingChatModel)
.tools(toolsService)
.chatMemoryProvider(memoryId ->
MessageWindowChatMemory.builder().maxMessages(10)
.id(memoryId).build()
)
.contentRetriever(retriever)
.build();
return assistant;
}
- 当然我们还需要提前存储向量数据到向量数据库,
embeddingStore在上边已经放进bean
/**
* TODO 模拟将内容转为向量存储到向量数据库中
* */
@SneakyThrows
@Bean
CommandLineRunner ingestTermOfServiceToVectorStore(QwenEmbeddingModel qwenEmbeddingModel,
EmbeddingStore embeddingStore){
// 获取resource里边的文件
Path path = Paths.get(this.getClass().getClassLoader().getResource("rag/terms-of-service.txt").toURI());
return args -> {
// 文档读取器
TextDocumentParser textDocumentParser = new TextDocumentParser();
// 读取为文档 (读取文件)
Document document = FileSystemDocumentLoader.loadDocument(path, textDocumentParser);
// 读取文档进行分割
DocumentByLineSplitter documentByLineSplitter = new DocumentByLineSplitter(500, 200);
List<TextSegment> split = documentByLineSplitter.split(document);
// 向量化
List<Embedding> content = qwenEmbeddingModel.embedAll(split).content();
// 存入
embeddingStore.addAll(content,split);
};
}
- controller层进行接口编写
/**
* TODO RAG测试
* */
@Autowired
private AiConfig.AssistantRAG assistantRAG;
@GetMapping(value = "/assistantRAG",produces ="text/stream;charset=UTF-8")
public Flux<String> assistantRAG(@RequestParam(defaultValue="退费费用") String message,@RequestParam Integer userId) {
TokenStream stream = assistantRAG.stream(userId,message);
return Flux.create(sink -> {
stream.onPartialResponse(s -> sink.next(s))
.onCompleteResponse(c -> sink.complete())
.onError(sink::error)
.start();
});
}
5. Chain多个ServiceAI
在一个应用中, 可能需要多个模型共同一起协作完成一个任务。
为什么要这样:
- 您的LLM可能不需要始终了解您拥有的每个tools。例如,当用户只是向LLM打招呼或说再见时,让 LLM 访问数十或数百个tools的成本很高,有时甚至很危险(LLM 调用中包含的每个tools都会消耗大量token),并且可能会导致意想不到的结果(LLM 可能会产生幻觉或被操纵以使用非预期的输入来调用tools)。
- 关于 RAG:同样,有时需要为 LLM 提供一些上下文,但并非总是如此,因为它会产生额外的成本(更多上下文 = 更多token)并增加响应时间(更多上下文 = 更高的延迟)。
关于模型参数:在某些情况下,您可能想不通的对话使用不同的 LLM ,以利用不同LLM的最佳特性。
- 您可以一个接一个地调用 AI 服务(又称链接-chain)。
- 您可以使用确定性和 LLM 支持的if/else语句(AI 服务可以返回boolean)。
- 您可以使用确定性和 LLM 支持的switch语句(AI 服务可以返回enum)。
- 您可以使用确定性和 LLM 驱动的for/while循环(AI 服务可以返回int和其他数字类型)。
- 您可以在单元测试中模拟 AI 服务(因为它是一个接口)。
- 您可以单独地对每个 AI 服务进行集成测试。
并且我们可以自由的进行任务编排:
大家平常应该见过一些AI智能体, 由多个(LLM)任务组合编排为一个智能体,

其实利用langchain4j的chain特性, 也可以完成这种类似的效果, 不过你需要自己完成前端编排以及不同任务的初始化和具体实现。
- 演示2个模型协调合作,但是实际非常灵活
/**
* TODO 多个大模型进行调用
* */
ChatLanguageModel qwen;
OllamaChatModel deepseek;
@BeforeEach
public void init(){
deepseek = OllamaChatModel.builder()
.baseUrl("http://192.168.xxx.xxx:11434")
.modelName("deepseek-r1:1.5b")
.build();
qwen = QwenChatModel
.builder()
.apiKey(System.getenv("API_KEY"))
.modelName("qwen-max")
.build();
}
interface GreetingExpert {
@dev.langchain4j.service.SystemMessage("""
你是任务分类专家。将用户输入分类到以下类别之一:
- MODIFY_TICKET(修改机票)
- QUERY_TICKET(查询机票)
- CANCEL_TICKET(退票)
- OTHER(其他)
重要:必须只返回枚举值本身(如 MODIFY_TICKET),不要添加任何解释、格式或特殊符号。
错误示例:\boxed{MODIFY_TICKET} 或 "修改机票"
正确示例:MODIFY_TICKET
""")
// {{it}}:LangChain4j 的占位符,表示方法参数 text
@dev.langchain4j.service.UserMessage("以下文本是什么任务: {{it}}")
TaskType isTask(String text);
}
interface ChatBot {
@dev.langchain4j.service.SystemMessage("你是一名航空公司客服代理,请为客户服务:")
String reply(String userMessage);
}
class MilesOfSmiles {
private GreetingExpert greetingExpert;
private ChatBot chatBot;
public MilesOfSmiles(GreetingExpert greetingExpert, ChatBot chatBot) {
this.greetingExpert = greetingExpert;
this.chatBot = chatBot;
}
public String handle(String userMessage) {
TaskType task = greetingExpert.isTask(userMessage);
switch (task) {
case MODIFY_TICKET:
case QUERY_TICKET:
case CANCEL_TICKET:
return task.getName() + "调用service方法处理";
case OTHER:
return chatBot.reply(userMessage);
}
return null;
}
}
@Test
void test05(){
GreetingExpert greetingExpert = AiServices.create(GreetingExpert.class, deepseek);
ChatBot chatBot = AiServices.create(ChatBot.class, qwen);
MilesOfSmiles milesOfSmiles = new MilesOfSmiles(greetingExpert, chatBot);
log.info(milesOfSmiles.handle("你好,请问你是谁"));
}
- 枚举
public enum TaskType {
MODIFY_TICKET("修改机票"),
QUERY_TICKET("查询机票"),
CANCEL_TICKET("退票"),
OTHER("其他");
private String name;
TaskType(String name) {
this.name = name;
}
public String getName() {
return name;
}
}
6. MCP
mcp其实很简单, 就是tools的一种外部调用的方式(既然要外部调用,肯定就需要遵循一种通信协议, 这里的协议就MCP,利用一种json-rpc2.0的json格式告知用有哪些tools什么参数, 调用哪个tool, 返回什么数据)。之前我们在自己程序中实现了tools, 但是这种tools无法提供给其他应用调用, 形成了应用孤岛, 无法提供外部共享
点击查询mcp服务

langchain4j 没有提供mcp server的实现, 但是提供的mcp client的实现:
当然mcpserver哪怕纯java也可以单独实现,下次有时间单独给大家讲解(如果有兴趣的话)
- 依赖包
<!--mcp-->
<dependency>
<groupId>dev.langchain4j</groupId>
<artifactId>langchain4j-mcp</artifactId>
<version>${langchain4j.version}</version>
</dependency>
- 代码实现
/**
* TODO 调用mcp
* */
@Test
void test06() {
// 1. 构建模型
QwenChatModel qwenChatModel = QwenChatModel.builder()
.apiKey(System.getenv("API_KEY"))
.modelName("qwen-max")
.build();
// 2. 构建 MCP 传输层(stdio 方式)
StdioMcpTransport transport = new StdioMcpTransport.Builder()
.command(List.of("cmd", "/c", "npx", "-y", "howtocook-mcp"))
.logEvents(true)
.build();
// 3. 构建 MCP 客户端
DefaultMcpClient mcpClient = new DefaultMcpClient.Builder()
.transport(transport)
.build();
try {
// 4. 获取 MCP 提供的工具
List<ToolSpecification> tools = mcpClient.listTools();
log.info("🤖 Agent 已加载 {} 个工具:", tools.size());
tools.forEach(tool -> log.info(" 📦 {} - {}",
tool.name(),
tool.description()));
// 5. 构建工具提供者
McpToolProvider toolProvider = McpToolProvider.builder()
.mcpClients(List.of(mcpClient))
.build();
// 6. 构建 AI 服务(关键:启用工具支持)
Bot bot = AiServices.builder(Bot.class)
.chatLanguageModel(qwenChatModel)
.toolProvider(toolProvider)
.chatMemory(MessageWindowChatMemory.withMaxMessages(10)) // 添加记忆
.build();
// 7. 执行对话
String response = bot.chat("我想吃西红柿炒鸡蛋");
log.info("AI 回复:{}", response);
} catch (Exception e) {
log.error("MCP 调用失败", e);
} finally {
// 8. 清理资源
mcpClient.close();
}
}
interface Bot {
// 简单对话接口,工具由 MCP 自动提供
String chat(String userMessage);
}
更多推荐



所有评论(0)