LangChain4J教程

官方文档为:开始使用 | LangChain4j 中文文档

1.入门案例

先创建一个maven工程,导入相关的pom的GAV坐标,pom如下:

1
2
3
4
5
6
7
8
9
10
 <dependency>
<groupId>dev.langchain4j</groupId>
<artifactId>langchain4j-open-ai</artifactId>
<version>1.0.0-beta3</version>
</dependency>
<dependency>
<groupId>dev.langchain4j</groupId>
<artifactId>langchain4j</artifactId>
<version>1.0.0-beta3</version>
</dependency>

然后,导入你的 OpenAI API 密钥。 建议将 API 密钥存储在环境变量中,以降低公开暴露的风险。(注意,你没有LLM密钥的话,可以使用LangChain4J官方的进行测试

使用这个之前请确保密钥已经设置在系统Path中:

1
String apiKey = System.getenv("OPENAI_API_KEY");

设置好密钥后,让我们创建一个 OpenAiChatModel 实例:

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

你没有密钥,可以使用官方的演示Demo,如下:

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

官方提示:

如果您没有自己的 OpenAI API 密钥,不用担心。 您可以临时使用我们免费提供的 demo 密钥,用于演示目的。 请注意,当使用 demo 密钥时,所有对 OpenAI API 的请求都需要通过我们的代理, 该代理会在转发请求到 OpenAI API 之前注入真实的密钥。 我们不会以任何方式收集或使用您的数据。 demo 密钥有配额限制,仅限于使用 gpt-4o-mini 模型,并且应该仅用于演示目的。

接下来就可以聊天了:

1
2
String answer = model.chat(”问题“);
System.out.println(answer是一个”回答“);

输出结果如下:

image-20251003140644059

2.LangChain4J介绍

LangChain4j 的目标是简化将 LLM 集成到 Java 应用程序中的过程。

具体方式如下:

  1. 统一 API: LLM 提供商(如 OpenAI 或 Google Vertex AI)和嵌入(向量)存储(如 Pinecone 或 Milvus) 使用专有 API。LangChain4j 提供统一的 API,避免了学习和实现每个特定 API 的需求。 要尝试不同的 LLM 或嵌入存储,您可以在它们之间轻松切换,无需重写代码。 LangChain4j 目前支持 15+ 个流行的 LLM 提供商20+ 个嵌入存储
  2. 全面的工具箱: 自 2023 年初以来,社区一直在构建众多 LLM 驱动的应用程序, 识别常见的抽象、模式和技术。LangChain4j 将这些提炼成一个即用型包。 我们的工具箱包含从低级提示模板、聊天记忆管理和函数调用 到高级模式如代理和 RAG 的工具。 对于每个抽象,我们提供一个接口以及基于常见技术的多个即用型实现。 无论您是在构建聊天机器人还是开发包含从数据摄取到检索完整管道的 RAG, LangChain4j 都提供多种选择。
  3. 丰富的示例: 这些示例展示了如何开始创建各种 LLM 驱动的应用程序, 提供灵感并使您能够快速开始构建。

LangChain4j 始于 2023 年初 ChatGPT 热潮期间。 我们注意到与众多 Python 和 JavaScript LLM 库和框架相比,缺少 Java 对应物, 我们必须解决这个问题! 虽然我们的名字中有”LangChain”,但该项目是 LangChain、Haystack、 LlamaIndex 和更广泛社区的想法和概念的融合,并加入了我们自己的创新。

我们积极关注社区发展,旨在快速整合新技术和集成, 确保您保持最新状态。 该库正在积极开发中。虽然一些功能仍在开发中, 但核心功能已经就位,让您现在就可以开始构建 LLM 驱动的应用程序!

为了更容易集成,LangChain4j 还包括与 QuarkusSpring Boot 的集成。

3.LangChain4J模块介绍

Models

Models是LangChain中用于生成文本或执行其他任务的AI模型,通常是预训练的语言模型(如GPT-3、GPT-4等),用于生成文本、回答问题、翻译、总结等任务。Langchain允许你通过API调用这些模型,并将它们集成到
更复杂的应用中。

Prompts

Prompts是用户提供给模型的输入文本,用于引导模型生成特定的输出。Prompts可以是简单的文本,也可以是结构化的模板。Prompts用于控制模型的输出,使其生成符合预期的结果。通过设计好的Prompts,可以引导模型执行特定的任务,如问答、翻译、生成代码等。

Vector Store

Vector store是用于存储和检索文本向量的数据库。文本向量是通过将文本嵌入到高维空间中生成的数值表示,Vector store用于快速检索与査询文本相似的文本片段。这在文档检索、推荐系统、语义搜索等任务中非常有用。

Document Loaders

Document Loaders是用于从不同来源(如文件、数据库、API等)加载文档的工具。Document Loaders用于将外部数据加载到LangChain中,以便进一步处理。例如,可以从PDF、Word文档、网页等加载文本数据。

Text Splitters

Text Splitters用于将长文本分割成较小的片段或块。Text Splitters用于处理长文本,使其适合模型的输入长度限制。这在处理大型文档或长篇文章时非常有用,可以确保模型能够处理整个文本。

Output Parsers

Output Parsers用于将模型的输出解析为结构化数据或特定格式。0utput Parsers用于处理模型的原始输出,将其转换为更易用的格式,如JSON、字典、列表等。这对于后续的数据处理和分析非常有用。

Tools

Tools是Langchain中用于执行特定任务的函数或接口。它们可以是内置的工具,也可以是自定义的工具。Tools用于扩展模型的能力,例如调用外部API、执行计算、查询数据库等。通过Tools,模型可以与环境进行交互,执行更复杂的任务。

LangChain4J的工程结构

image-20251004160641209

SpringBoot集成LangChainJ

注意,我这里用的是硅基流动的免费千问模型,本来我是用deepseek模型,后来发现deepseek不支持function calling。硅基流动是适配openAI的API,所以只要使用对应的pom依赖就可以

这里我还是推荐,学习的小伙伴可以对照官方的文档学习(我就是这样,哈哈哈哈)

工具及版本

JDK17

Maven>=3.9

Langchain4J是1.0.0-alpha1

SpringBoot是3.2.0

准备工作

  • 导入pom依赖

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    <!-- Spring Boot 核心依赖(Web、测试、基础) -->
    <dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-web</artifactId>
    </dependency>
    <dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-test</artifactId>
    <scope>test</scope>
    </dependency>

    <!-- OpenAI starter(自动包含 core) -->
    <dependency>
    <groupId>dev.langchain4j</groupId>
    <artifactId>langchain4j-open-ai-spring-boot-starter</artifactId>
    <version>1.0.0-alpha1</version>
    </dependency>
    <!-- 若需直接依赖 core,也显式指定版本 -->

    <dependency>
    <groupId>dev.langchain4j</groupId>
    <artifactId>langchain4j-core</artifactId>
    <version>1.0.0-alpha1</version>
    </dependency>
    <dependency>
    <groupId>dev.langchain4j</groupId>
    <artifactId>langchain4j</artifactId>
    <version>1.0.0-alpha1</version>
    <scope>compile</scope>
    </dependency>
    <dependency>
    <groupId>org.projectlombok</groupId>
    <artifactId>lombok</artifactId>
    <version>1.18.38</version>
    </dependency>
  • 申请LLM(大模型)的api及对应的key密钥

    这里需要自己去硅基流动平台申请,官网地址:https://cloud.siliconflow.cn

    选择API密钥

    image-20251007193432351.png

    新建密钥:

    image-20251007193501268.png

    就可以复制使用了(注意,不要给别人(注意隐私))

  • 配置application.yml文件

    yml配置文件如下(model-name可以根据自己的需求选择):

    1
    2
    3
    4
    5
    6
    7
    langchain4j:
    open-ai:
    chat-model:
    api-key: *****
    base-url: https://api.siliconflow.cn/v1
    model-name: Qwen/Qwen3-8B

  • 封装配置类

    这里不需要,Springboot已经对其进行自动配置

1.Lowlevel调用

“low-level 调用” 通常指直接操作框架底层组件和 API,而非使用高层封装好的便捷工具或模板。这种方式更接近原始实现,灵活性更高,但需要手动处理更多细节。

具体来说,low-level 调用的特点包括:

  1. 直接实例化核心组件:例如手动创建 ChatLanguageModelMessagePromptTemplate 等基础对象,而非使用 ChatMemoryAgent 等高级抽象。
  2. 手动管理交互流程:需要自己处理输入格式化、模型调用、响应解析等完整流程,而不是依赖框架的自动串联能力。
  3. 细粒度控制参数:可以直接设置模型的温度(temperature)、最大令牌数(max tokens)等底层参数。

创建Controller类

1
2
3
4
5
6
7
8
9
10
11
12
13
@RestController
@RequestMapping("/ai")
public class LowerAPIController {
@Resource
private ChatLanguageModel model;

//这个是低api的方式调用model,通过注入就可以直接使用了
@GetMapping("/low/chat")
public String chatWithLangchainLow(@RequestParam("message") String message){
return model.chat(message);
}
}

因为SpringBoot已经自动实现ChatLanguageModel实例(通过yml文件的配置),所以可以直接调用。

通过浏览器就可以进行访问及提问了:http://localhost:8080/ai/low/chat?message=你要问的问题

2.Highlevel调用

“high-level 调用” 是相对于 “low-level 调用” 而言的概念,指的是使用框架封装的高层级 API 和抽象组件,无需关注底层细节,通过简洁的方式实现复杂功能。这种方式更注重快速开发和易用性,适合大多数常见场景。

high-level 调用的核心特点包括:

  1. 基于高级抽象组件:直接使用封装好的 AgentChatAssistantRunnableChain 等高级对象,这些组件内部已整合了模型调用、内存管理、工具调用等能力。
  2. 自动处理流程细节:框架会自动处理输入格式化、上下文维护、响应解析、多步骤交互等中间环节,开发者无需手动串联各个步骤。
  3. 简化的 API 接口:通过更少的代码实现复杂功能,例如一行代码添加记忆功能、工具调用能力等。

你需要创建一个AI助手接口

接口代码如下:

1
2
3
public interface Assistant {
String chat(String message);
}

然后对这个接口进行实例化

代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
@Configuration
public class AssistantInit {
@Resource
private ChatLanguageModel model;

//完成assistan实例化
@Bean
public Assistant assistant(){
//这里是极简配置模式
return AiServices.create(Assistant.class, model);
}
}

这样就可以直接使用这个接口进行对话了

创建一个Controller进行实验

1
2
3
4
5
6
7
8
9
10
11
12
@RequestMapping("/ai/high")
@RestController
public class HighAPIController {

@Resource
private Assistant assistant;

@GetMapping("/chat")
public String HighAPIchat(@RequestParam("message") String message){
return assistant.chat(message);
}
}

同样,你就可以通过浏览器进行访问及提问了:http://localhost:8080/ai/high/chat?message=你要问的问题

3.System系统提示词

**”System 系统提示词” **是指提供给 AI 模型的初始指令或背景信息,用于定义 AI 的角色、行为准则、回答风格等。它通常在对话开始前传递给模型,作为整个交互的 “上下文基准”。

系统提示词的作用类似于给 AI 设定 “操作手册”,例如:

  • 定义 AI 的身份(如 “你是一名编程助手”)
  • 规定回答格式(如 “请用 Markdown 列表形式回答”)
  • 设定行为边界(如 “拒绝回答敏感问题”)
  • 提供领域知识(如 “基于 Java 8 语法解释问题”)

这里同样是有HighLevel和LowLevel两种方式设置,前者简单而已。

LowLevel实现系统提示词

这里需要传入SystemMessage用来告诉AI,它扮演的角色或者需要回答的语气,代码如下

1
2
3
4
5
6
7
8
9
10
11
//系统提示词(这个是采用lowAPI的方式实现的)
@GetMapping("/low/chat/system")
public String chatWithLangchainLowSystem(@RequestParam("message") String message){
return model.chat(ChatRequest.builder()
.messages(List.of(
new SystemMessage("你是一个猫娘,用猫娘的语气回答"),
new UserMessage(message)
))
.build()).aiMessage().text();
//build是23种设计模式中的建造者模式,有兴趣可以看看简述或者看看源码
}

这里不懂代码含义的可以让ai分析,本质就是调用chat方法时传入了用户信息(UserMessage)和系统提示词(SystemMessage),AI模型会自动根据SystemMessage的指示进行回答UserMessage。

HighLevel实现系统提示词

这里就非常简单了,只需要在AI助手接口中自己需要的方法上面加一个@SystemMessage注解就可以实现系统提示词功能了。

接口代码如下:

1
2
3
4
5
public interface Assistant {
@SystemMessage("你是一个高冷御姐,请用御姐的方法回答")
String chat(String message);
}

这样,你通过注入接口调用这个方法,就会自动根据你设置的提示词进行回答

Controller类的代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
@RequestMapping("/ai/high")
@RestController
public class HighAPIController {

@Resource
private Assistant assistant;

@GetMapping("/chat")
public String HighAPIchat(@RequestParam("message") String message){
return assistant.chat(message);
}
}

通过浏览器近可以测试这两种方式了:http://localhost:8080/ai/high/chat?message=你要问的问题

4.会话记忆,会话隔离和会话持久化

会话记忆就是AI可以记住以前的回答,如我先提问了一个问题:“小明进今天考了98分”,你再提问:“小明今天考了多少分”,这时Ai就会回答98分(这是设置了会话记忆)。如果没有设置会话记忆,Ai就会说不知道。

这里同样也有两个方式(LowLevel和HighLevel)进行实现会话记忆,下面我来逐一说明。

MessageWindowChatMemory

MessageWindowChatMemory 是用于管理对话历史的核心组件,专门用于存储和维护一定数量的对话消息(用户消息和 AI 回复),让 AI 能够在多轮对话中 “记住” 上下文。

它的核心特点和作用:

  1. 窗口式记忆

    • 如同一个 “消息窗口”,只保留最近的 N 条消息(而非全部历史)
    • 例如 MessageWindowChatMemory.withMaxMessages(10) 表示最多保留 10 条消息
  2. 自动维护对话上下文

    • 自动添加新消息到记忆中
    • 当消息数量超过上限时,自动移除最早的消息(类似队列的 FIFO 机制)
    • 确保传给模型的始终是最新的对话上下文
  3. 与模型调用结合

    • 在每次调用 AI 模型时,会自动将记忆中的消息作为上下文传入
    • 使 AI 能够基于历史对话做出连贯回应,而非孤立处理单条消息
  4. MessageWindowChatMemory本身是单个对话的上下文容器

LowLevel实现

只要注入MessageWindowChatMemory,就可以进行会话记忆了,具体代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
//这里初始化MessageWindowChatMemory
final ChatMemory chatMemory = MessageWindowChatMemory.withMaxMessages(10);

@Resource
private ChatLanguageModel chatLanguageModel;
@Resource
private Assistant assistant;


//这是low实现会话记忆
@GetMapping("/low/chat")
public String chatLow(@RequestParam("message") String message){
//要自己拼装对话
//历史对话和当前对话进行封装返回
chatMemory.add(UserMessage.from(message));
ChatResponse response = chatLanguageModel.chat(ChatRequest.builder().messages(chatMemory.messages()).build());
chatMemory.add(response.aiMessage());
return response.aiMessage().text();
}

这个代码的意思是:先初始化MessageWindowChatMemory(这里初始化为10,表示这个会话窗口只能存储10条消息,当你要求Ai回答第前11条消息,Ai就会报不知道),同时MessageWindowChatMemory存储的消息和当前用户的消息一同传递到Ai,之后再将Ai回答的消息再次存储到MessageWindowChatMemory。

HighLevel实现

使用HighLevel就比较简单了,只需要在Ai助手接口实例化时进行设置就可以实现会话记忆(不需要手动传递及手动存储),Ai助手接口的实例化代码如下

1
2
3
4
5
6
7
@Bean
public Assistant assistant(EmbeddingStore<TextSegment> embeddingStore,EmbeddingModel embeddingModel){
return AiServices.builder(Assistant.class)
.chatMemoryProvider(memory-> MessageWindowChatMemory.withMaxMessages(10))//这个是配置会话记忆
.chatLanguageModel(model)//这个是配置模型
.build();
}

chatMemoryProvider需要提供一个存储提供器,这里采用的是MessageWindowChatMemory,这样就通过HighLevel设置好了会话记忆。

通过Controller就可以测试了,代码如下:

1
2
3
4
5
6
 //这是通过high实现会话记忆
@GetMapping("/high/chat")
public String chatHigh(@RequestParam("memoryId") String memoryId,@RequestParam("message") String message){
//实现会话记忆
return assistant.chat(message);
}

ChatMemoryStore

ChatMemoryStore是用于持久化和管理多个对话记忆的组件,它相当于一个 “记忆仓库”,可以存储、检索和管理不同对话会话的 ChatMemory 实例。

核心作用
  • 当你需要同时处理多个独立对话(例如多用户场景)时,ChatMemoryStore 可以为每个对话分配独立的 ChatMemory(如 MessageWindowChatMemory),并通过唯一的 sessionId 进行区分和管理。
  • 解决了单 ChatMemory 无法隔离多会话上下文的问题。
ChatMemory 的关系
  • ChatMemory:管理单个对话的消息历史(如最近 10 条消息)。

  • ChatMemoryStore:管理多个 ChatMemory 实例,通过 sessionId 区分不同对话,实现多会话记忆的持久化和隔离。

下面就是通过HighLevel和ChatMemoryStore进行会话隔离。

所谓的会话隔离就是值两个人向同一个模型提问时,AI会将两个人的提问分开,不会彼此干扰。就好比:你和小明同时向同一个Ai模型提问,小明告诉Ai自己考了98分,之后你向Ai提问小明考了多少分,Ai会会回答不知道,这就是会话隔离。

其实上面的HighLevel实现会话记忆时,默认就启动了会话隔离,你需要做的是将接口的Chat方法进行修改就可以了,Ai助手的修改方法如下:

1
String chat(@MemoryId String memoryId, @UserMessage String message);

注意,一定要标注哪个是ID,哪个是提问消息(用户消息)

Controller类中的方法测试方法为:

1
2
3
4
5
6
//这是通过high实现会话记忆和会话隔离
@GetMapping("/high/chat")
public String chatHigh(@RequestParam("memoryId") String memoryId,@RequestParam("message") String message){
//只要传入memoryId就可以实现会话隔离
return assistant.chat(memoryId,message);
}

这时就可以在浏览器中测试了:http://localhost:8080/ai/high/chat?memoryId=这里是会话id&message=你要问的问题

你可以通过不同的memoryId进行测试

通过ChatMemoryStore进行会话持久化

我在上面写的都是基于MessageWindowChatMemory进行存储的,MessageWindowChatMemory这个是将会话存储到内存,也就是说在程序退出,这些存储的会话就会丢失。那为了解决这个问题,可以将消息持久化默认为内存改为我们自定义的数据库,下面我来细细的说一下。

其实Ai实现会话记忆是基于ChatMemoryStore中默认的查,增,删方法进行的,要实现会话的持久化,就需要自己重写ChatMemoryStore

下面我来对ChatMemoryStore进行重写:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
class MyChatMemoryStore implements ChatMemoryStore {

@Override
public List<ChatMessage> getMessages(Object memoryId) {
// TODO: 实现通过内存ID从持久化存储中获取所有消息。
// 可以使用ChatMessageDeserializer.messageFromJson(String)和
// ChatMessageDeserializer.messagesFromJson(String)辅助方法
// 轻松地从JSON反序列化聊天消息。
}

@Override
public void updateMessages(Object memoryId, List<ChatMessage> messages) {
// TODO: 实现通过内存ID更新持久化存储中的所有消息。
// 可以使用ChatMessageSerializer.messageToJson(ChatMessage)和
// ChatMessageSerializer.messagesToJson(List<ChatMessage>)辅助方法
// 轻松地将聊天消息序列化为JSON。
}

@Override
public void deleteMessages(Object memoryId) {
// TODO: 实现通过内存ID删除持久化存储中的所有消息。
}
}

同时,在Ai助手实例化的同时设置ChatMemoryStore就可以,原始的ChatMemoryStore是基于内存的,你自己可以在上面的代码将消息存储到数据库(如Mysql数据库)

在Ai助手实例化的同时设置ChatMemoryStore

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
@Bean
public Assistant assistant(
EmbeddingStore<TextSegment> embeddingStore,
EmbeddingModel embeddingModel,
ChatLanguageModel model) { // 注意:确保model参数被注入

// 1. 实例化你的数据库持久化存储
ChatMemoryStore memoryStore = new MyChatMemoryStore(); // 你的自定义实现

return AiServices.builder(Assistant.class)
.chatLanguageModel(model)
.tools(new HighCalculator())
.contentRetriever(EmbeddingStoreContentRetriever.builder()
.embeddingStore(embeddingStore)
.embeddingModel(embeddingModel)
.build())
.chatMemoryStore(memoryStore) // 指定持久化存储
.chatMemoryProvider(memoryId -> // 为每个会话创建带窗口限制的记忆
MessageWindowChatMemory.builder()
.id(memoryId) // 绑定会话ID
.maxMessages(10) // 单会话最大消息数
.build()
)
.build();
}

接下来每次会话就将消息存储到数据库中了。

补充一点

使用LowLevel也可以设置为消息隔离,只需要通过Map集合进行对Controller的修改就可以,也就是将MemoryId设置为Key,MessageWindowChatMemory设置为Value就可以,这里就不做具体说明,还是挺基础的,有兴趣的小伙伴可以自己写写。

5.RAG向量存储

它是 RAG(Retrieval-Augmented Generation,检索增强生成)技术栈中的 “数据仓库”,负责以向量(Embedding)形式高效存储、管理和检索非结构化数据(如文档、PDF、对话记录等),为大模型(LLM)提供 “实时、精准、私有” 的外部知识支持,解决大模型 “知识过时、缺乏私有数据、易产生幻觉” 的核心痛点。

通俗的讲就是:将预先的数据进行向量化,存储到向量数据库中,用户提问Ai时,Ai会结合向量数据库中的内容进行按需回答(如果提问的问题向量数据库中没有,同时Ai也不知道,这时Ai就会回答不知道)。

所以要进行这个步骤,你需要一个向量化模型(这里硅基流动有免费的向量化模型:beg-m3),这个模型是里面的免费模型中最强的。

同样,你也需要准备工作。

初始化Ai助手接口

这里需要设置向量存储(embeddingStore)和向量转换(embeddingModel

1
2
3
4
5
6
7
8
9
10
11
12
13
@Bean
public Assistant assistant(EmbeddingStore<TextSegment> embeddingStore,EmbeddingModel embeddingModel){
return AiServices.builder(Assistant.class)
.chatMemoryProvider(memory-> MessageWindowChatMemory.withMaxMessages(10))//这个是配置会话记忆
.chatLanguageModel(model)//这个是配置模型
.tools(new HighCalculator())//工具Tool
//这里就是配置向量数据库的地方,包含了向量数据库的类型和向量转换器模型
.contentRetriever(EmbeddingStoreContentRetriever.builder()
.embeddingStore(embeddingStore)
.embeddingModel(embeddingModel)
.build())
.build();
}

注意:这里使用的向量存储还是基于内存的数据库(是LangChain4J自带的),同时传入的参数就是这两个(由Spring管理,我们不需要管)

如下:

1
2
3
4
@Bean
public EmbeddingStore<TextSegment> InitEmbeddingStore(){
return new InMemoryEmbeddingStore<>();//这个是内置的向量数据库(langchain提供)
}

同时需要自己定义嵌入式模型

如下:

1
2
3
4
5
6
7
8
9
@Bean
public EmbeddingModel embeddingModel() {
System.out.println("💡 正在创建 OpenAI Embedding Model for SiliconFlow...");
return OpenAiEmbeddingModel.builder()
.baseUrl("https://api.siliconflow.cn/v1") //
.apiKey("*****") // 这里是你的密钥
.modelName("baai/bge-m3") // 指定模型
.build();
}

好的,接下来就可以测试了

我们写一个Controller类,同时,我会简述代码的含义

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
package com.cn.org.langchain4jdemo.rag;

import com.cn.org.langchain4jdemo.mapper.Assistant;
import dev.langchain4j.data.document.Document;
import dev.langchain4j.data.document.loader.FileSystemDocumentLoader;
import dev.langchain4j.data.document.splitter.DocumentByLineSplitter;
import dev.langchain4j.data.segment.TextSegment;
import dev.langchain4j.model.embedding.EmbeddingModel;
import dev.langchain4j.store.embedding.EmbeddingStore;
import dev.langchain4j.store.embedding.EmbeddingStoreIngestor;
import dev.langchain4j.store.embedding.IngestionResult;
import jakarta.annotation.Resource;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;

import java.util.List;

@RequestMapping("/ai/rag")
@RestController
public class RAGController {

@Resource
private EmbeddingStore<TextSegment> embeddingStore;
@Resource
private Assistant assistant;
@Resource
private EmbeddingModel embeddingModel;

@GetMapping("/high/chat")
public String highChat(@RequestParam("message") String message){
return assistant.chat(message);
}

@GetMapping("/load")
public String load(){
//通过tika进行文件读取
//文档分割
List<Document> documents = FileSystemDocumentLoader.loadDocuments("E:\\javaclass\\JavaMail\\LangChain4Jdemo\\src\\main\\resources\\documents");//绝对路径
EmbeddingStoreIngestor.ingest(documents, embeddingStore);
return "sucess";
}

}

要测试RAG,主要是通过HighLevel进行测试(有兴趣可以自己写一下LowLevel的代码)

定义的**highChat**方法就是用户提问的入口测试方法,就是一个典型的web方法。

定义的**load方法是将需要进行向量化同时存储的数据存储入向量数据库而准备的。这里有一个Tika** 工具,这个工具是Apache 基金会旗下的一个开源工具库,主要用于文档内容提取和类型检测。它能够处理几乎百种不同格式的文件(如文本、PDF、Word、Excel、图片、音频、视频等),并从中提取结构化或非结构化数据(文本内容、元数据等),是处理多格式文档的重要工具。

Tika 的核心功能

  1. 内容提取从各种文件中提取文本内容,无论文件格式如何(如从 PDF 中提取文字、从图片中提取 OCR 文本、从压缩包中提取内部文件内容等)。例:对于一个 PDF 文档,Tika 可以提取其中的所有文字内容,忽略格式信息(如字体、颜色),得到纯文本。
  2. 元数据提取提取文件的元数据(描述文件属性的数据),如作者、创建时间、文件大小、格式版本等。例:从一张图片中提取拍摄设备型号、拍摄时间、分辨率等 EXIF 信息。
  3. 文件类型检测自动识别文件的真实类型(即使文件扩展名被篡改),基于文件的二进制签名而非扩展名判断。例:将一个 .txt 扩展名的 PDF 文件正确识别为 PDF 格式。
  4. 统一接口对所有文件格式提供一致的 API,开发者无需针对不同格式编写单独的处理逻辑,只需调用 Tika 的统一方法即可。

而Langchain4j是集成了Tika模型的,可以直接使用。对于**load**方法中的代码,就是两点:文档读取,文档写入。文档读取就是根据设置按行,按字等(默认是按段落读取)。文档写入就是通过调用我们预先设置的Embedding模型将文档(分割好的)向量化存入向量数据库中。

这里有一个名词**向量化**,可以自己问Ai了解一下。你可以理解为数据唯一数字化。

测试

这里测试,你可以先提问:清明是谁?===>http://localhost:8080/ai/rag/high/chat?message=清梦是谁?

这里Ai应该会告诉你不知道。

再测试

这里先进行数据的载入,就是:http://localhost:8080/ai/rag/load

文档数据:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
# 清梦与枕上书
清梦在巷口的旧书铺里发现那只青釉瓷枕时,梅雨季刚过,檐角还滴着水,混着书页的霉味,像浸了时光的茶。瓷枕通身是淡青的釉色,侧面刻着细如蚊足的字:“枕此者,入旧梦”。书铺老板是个留着山羊胡的老人,眯眼打量她半晌,说:“这枕是前儿收的老物件,旁人枕着只觉硌,许是跟你有缘。”

清梦把瓷枕抱回家时,出租屋的窗台正摆着一盆蔫了的薄荷。她是个插画师,总在深夜对着空白画布发呆,客户要的“烟火气”,她画了三稿都不满意——城市的霓虹太亮,遮住了她记忆里的星子,她忘了小时候外婆家院角的牵牛花,是几点钟绽开的。

第一晚枕着瓷枕入眠,清梦竟真的走进了梦里。不是寻常的混沌梦境,是清晰的旧时光:青石板路湿漉漉的,巷口的糖画摊冒着热气,穿蓝布衫的外婆正站在院门口喊她:“清梦,来吃槐花糕咯!”她奔过去,指尖触到外婆袖口的补丁,粗布的纹理蹭得指腹发痒,槐花糕的甜香裹着风,飘进鼻腔里。

醒来时,清梦的眼角还沾着泪。她摸出速写本,凭着梦里的记忆,一笔笔画下青石板路、糖画摊,还有外婆手里的槐花糕——笔尖落下时,她忽然懂了客户要的“烟火气”,不是车水马龙的热闹,是藏在细节里的温度。

往后的日子,清梦总在深夜枕着瓷枕入梦。有时是外婆在灯下缝补她的小裙子,顶针在布面上轻轻叩击;有时是夏夜的院坝,外婆摇着蒲扇,指给她看银河里的牛郎织女星,说“星星眨一次眼,就是有人在想你”;还有一次,她梦见自己趴在外婆的膝头,看她用毛笔在宣纸上写“清梦”两个字,墨汁晕开时,像撒了一把碎星。

她把梦里的场景一一画进画里,画布上渐渐有了温度:沾着露水的牵牛花、冒着热气的糖画、灯下的顶针、夜空中的银河……客户看到新画稿时,当即拍板:“就是这个!像能让人闻到槐花的香味。”

可渐渐的,清梦发现梦里的场景在变。起初是外婆的身影变得模糊,后来是院门口的牵牛花谢了,再后来,青石板路的尽头蒙着一层白雾,她喊外婆,却没人应答。她慌了,夜里抱着瓷枕不肯睡,怕下一次入梦,连外婆的轮廓都记不清。

书铺老板再见到她时,见她眼底有红血丝,便知缘由。老人从抽屉里取出一本泛黄的线装书,封面上写着“枕上书”:“这枕的前主人,是个念旧的老太太,她把想记住的事都写在了书里。你呀,不是在做梦,是在替她温故。”

清梦翻开“枕上书”,里面的字迹和瓷枕上的一样细巧:“今日清梦吃了三块槐花糕,笑出了两个小梨涡”“给清梦缝的裙子,要绣上她最爱的牵牛花”“清梦问银河有多远,我说,心近了,就不远”……每一页都记着“清梦”的事,最后一页落款是“外婆”。

原来那不是别人的旧梦,是外婆当年怕她忘了,偷偷写下的念想,藏进了瓷枕里。清梦抱着书,眼泪落在泛黄的纸页上,晕开了墨迹。她忽然明白,外婆从未离开,那些藏在细节里的爱,早被她记在了心里,画进了画里。

后来,清梦把“枕上书”里的故事,画成了一本绘本。绘本的最后一页,是长大的清梦,在院角种满了牵牛花,风一吹,花瓣落在摊开的书上,书里的字,正映着天上的星星。

有人问她,为什么绘本里的烟火气这样动人。清梦笑着说:“因为每一笔,都藏着有人惦记的温暖。”而那只青釉瓷枕,她依旧放在床头,只是不再依赖它入梦——她知道,外婆的爱,早成了她心里的光,照亮了每一个需要温暖的夜晚。

这个文档放在resources目录下的documents目录下(命名为001.txt)

再进行提问:http://localhost:8080/ai/rag/high/chat?message=清梦是谁?

这时,Ai就会根据这个文档回答了。

Ai的回答顺序是:向量数据库==>官方API

6.向量数据库持久化

上面那个是基于内存的持久化,也就是说程序退出,内存中的向量数据库就会消失。

那如何进行向量数据库的持久化呢?

很简单,只要重写**EmbeddingStore就可以,下面我就以Redis**为持久化向量数据库为例子。

(不单单只能用Redis,你也可以使用ES等,这里你可以查看官方文档看看向量数据库支持哪些:嵌入(向量)存储 | LangChain4j 中文文档

好的,也是需要准备工作

  • 开启一个Redis,这里推荐使用Docker进行部署,具体的步骤可以参考Redis官方教程

  • 导入langchain4j对应的redis坐标依赖

    1
    2
    3
    4
    5
    <dependency>
    <groupId>dev.langchain4j</groupId>
    <artifactId>langchain4j-redis</artifactId>
    <version>1.0.0-alpha1</version>
    </dependency>
  • 配置yml文件

    1
    2
    3
    4
    5
    6
    7
    8
    spring:
    data:
    redis:
    host: 192.168.178.132
    port: 6379
    password: *****
    database: 0
    user: default
  • 创建RedisConfig类

1
2
3
4
5
6
7
8
9
10
@ConfigurationProperties(prefix = "spring.data.redis")
@Configuration
@Data
public class RedisConfig {
private String host;
private Integer port;
private String password;
private String database;
private String user;
}

好了,准备工作好了,下面进行向量数据库持久化的具体操作

重写EmbeddingStore,这里推荐用一个类重写

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
@Configuration
public class EmbeddingInitConfig {

@Resource
private RedisConfig redisConfig;

//配置向量数据库初始化为redis
//@Bean
public EmbeddingStore<TextSegment> embeddingStoreInit() {
System.out.println("打印:"+redisConfig);
return RedisEmbeddingStore.builder()
.host(redisConfig.getHost())
.port(redisConfig.getPort())
.password(redisConfig.getPassword())
.dimension(1024)
.user(redisConfig.getUser())
.indexName("embedding-index")
.prefix("langchain4j:embedding:")
.build();
}
}

我简述一下这个代码:这个代码是将原来的InMemoryEmbeddingStore重写为RedisEmbeddingStore,同时传入redis的主机好,密码等。注意,**dimension**是根据你所用模型来配置维度,这里我使用的是beg-m3,它的维度是1024(这里硅基流动可以查看)如果你使用的别的Embedding模型可以去对应官网查看。

注意indexNameprefix是必备的,前者是用于向量查询时所用的索引下标,后者时向量化数据存储时所用的前缀名。这里可以自己定义。

修改Ai助手接口的初始化

这里就是将原来的**InMemoryEmbeddingStore去掉,(这里Spring会自动配置我们重写的RedisEmbeddingStore,前提是InMemoryEmbeddingStore**初始化已经去掉),具体的接口初始化代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
@Configuration
public class AssistantInit {

@Resource
private ChatLanguageModel model;

//初始化embeddingStore
//@Bean
//public EmbeddingStore<TextSegment> InitEmbeddingStore(){
// return new InMemoryEmbeddingStore<>();//这个是内置的向量数据库(langchain提供)
//}


@Bean
public EmbeddingModel embeddingModel() {
System.out.println("💡 正在创建 OpenAI Embedding Model for SiliconFlow...");
return OpenAiEmbeddingModel.builder()
.baseUrl("https://api.siliconflow.cn/v1") //
.apiKey("sk-eejpytshnxihqbeedqsgfuixodazjxjxjetbounsqbzzygtr") // SiliconFlow 的 token
.modelName("baai/bge-m3") // 指定模型
.build();
}

//完成assistan实例化
@Bean
public Assistant assistant(EmbeddingStore<TextSegment> embeddingStore,EmbeddingModel embeddingModel){
return AiServices.builder(Assistant.class)
.chatMemoryProvider(memory-> MessageWindowChatMemory.withMaxMessages(10))//这个是配置会话记忆
.chatLanguageModel(model)//这个是配置模型
.tools(new HighCalculator())//工具Tool
.contentRetriever(EmbeddingStoreContentRetriever.builder()
.embeddingStore(embeddingStore)
.embeddingModel(embeddingModel)
.build())
.build();
}
}

我来简述一下这个代码,我注释了**InitEmbeddingStore**这个方法,那embeddingStore(embeddingStore)这里就会自动加载重写的那个方法了(这个是Spring的特性)

下面就可以进行测试了

同样,你直接加载文件数据进入向量数据库就可以:http://localhost:8080/ai/rag/load

你查看你的redis数据库,就会发现多了好多数据。(redis查询命令:keys *

这就是向量数据库的自定义持久化了,是不是很简单?

7.文档分割

所谓的文档分割就是将数据存储到向量数据库时可以选择分割的数据的样式,如果不指定,默认是按段落进行分割。

你只需要在**load**方法中配置DocumentByLineSplitter 分割器就可以,下面来具体说明一下

代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
@GetMapping("/load")
public String load() {
try {
// 1. 加载文档
List<Document> documents = FileSystemDocumentLoader.loadDocuments(
"E:\\javaclass\\JavaMail\\LangChain4Jdemo\\src\\main\\resources\\documents"
);
// 2. 构建 Ingestor(必须传入 embeddingModel)
EmbeddingStoreIngestor ingestor = EmbeddingStoreIngestor.builder()
.embeddingStore(embeddingStore)
.embeddingModel(embeddingModel) // ✅ 必须传
//切分文本(第一个参数是每行的char字数,第二个是重叠char字段)
.documentSplitter(new DocumentByLineSplitter(100,40))
.build();

// 3. 执行 ingest
IngestionResult result = ingestor.ingest(documents);

return "Success! Ingested " ;
} catch (Exception e) {
e.printStackTrace();
return "Failed to ingest documents: " + e.getMessage();
}
}

.documentSplitter(new DocumentByLineSplitter(100,40))就是一个文档分割,DocumentByLineSplitter这个是可以自己定义的,默认是按行进行分割,里面的第一个参数是每一行最多容纳的字数(如果多于这个字数,会将多的字数放到下一行),第二个参数是重叠的字数,也就是当前行和下一行重叠的字数。

你再进行测试====>http://localhost:8080/ai/rag/load

就会发现redis中存储的key多了好多。

8.Function-calling(结构化输入)

这个就是你提问(输入),Ai大模型会根据你的提问自动选择你定义好的函数,这就是结构化输入。

**注意:**使用这个之前,你一定要选择支持结构化输入的模型(这就是为什么我选择阿里的模型,deepseek不支持function-calling)

同时,这里也可以使用LowLevelHighLevel进行实现。

同样,也是需要准备工作

创建一个工具类(我定义了一个计算功能)

1
2
3
4
5
6
7
8
9
10
11
12
13
@Data
@Component
public class Calculator implements Runnable{
//注意名字要意义
private int num1; // 匹配 JSON 中的 "num1"
private int num2; // 匹配 JSON 中的 "num2"

@Override
public void run() {
System.out.println("结果是:" + (num1 + num2)); // 输出:结果是:11
}
}

Runnable你可以看作是一个由JAVA提供的模板工厂类,你自己也可以用自己定义的模板。

下面就两个进行演示。

LowLevel实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
@RestController
@RequestMapping("/func/api")
public class FuncAPI {

@Resource
private ChatLanguageModel chatLanguageModel;
@Resource
private ApplicationContext applicationContext;

@Resource
private ObjectMapper objectMapper;

@GetMapping("/low/chat")
public String LowChat(@RequestParam("message") String message){
//构造自己的function
/***
* functin calling:大模型不仅仅可以用来对话,还可以自觉调用我们自定义的接口
*/
ToolSpecification specification = ToolSpecification.builder()
.name("calculator")//这个是function的名字
.description("输入两个数,对这两个数进行计算求和")//功能描述
.parameters(JsonObjectSchema.builder()
.addIntegerProperty("num1")//参数类型,参数个数
.addIntegerProperty("num2")
.required("num1","num2")//设置必填(表示调用这个函数时一定要这两个参数)
.build()
).build();
ChatResponse chatResponse = chatLanguageModel.chat(ChatRequest.builder()
.messages(List.of(UserMessage.from(message)))
.parameters(ChatRequestParameters.builder()
.toolSpecifications(specification)
.build())
.build());
chatResponse.aiMessage().toolExecutionRequests().forEach(toolExecutionRequest -> {
//工具名字
System.out.println(toolExecutionRequest.name());
//返回结结果
System.out.println(toolExecutionRequest.arguments());
try {
//通过反射
Class.forName("com.cn.org.langchain4jdemo.func."+toolExecutionRequest.name());

//通过工厂模式加Spring动态获取bean
Runnable bean = applicationContext.getBean(toolExecutionRequest.name(), Runnable.class); objectMapper.updateValue(bean,objectMapper.readValue(toolExecutionRequest.arguments(),Map.class));
System.out.print("结果是:");
bean.run();
System.out.println();
} catch (Exception e) {
throw new RuntimeException(e);
}
});
return chatResponse.aiMessage().text();
}
}

我简单描述一下这个代码:

先用ToolSpecification封装一下我们自己定义的function函数(注意,工具类的熟属性名字要和ToolSpecification配置的参数名字一样)。

然后就是通过Spring来动态获取反射类(用于调用我们需要的函数)

你可以将上面的代码让Ai阐述,具体不做太多描述(打字太累)

然后你就可以测试了====>http://localhost:8080/func/api/high/chat?message=我的两个数字是3和8

它就会自动计算了。

HighLevel实现

用HighLevel就比较简单了,首先需要定义自己的函数类:

1
2
3
4
5
6
public class HighCalculator {
@Tool("计算两数之和")
public int sum(int a,int b){
return a + b;
}
}

使用Tool注解标注,就是告诉Ai这个是一个functioncalling。

测试方法(Controller类):

1
2
3
4
@GetMapping("/high/chat")
public String HighChat(@RequestParam("message") String message){
return assistant.chat(message);
}

然后你就可以测试了====>http://localhost:8080/func/api/high/chat?message=我的两个数字是3和8

它就会自动计算了。

其实,就是通过description或者Tool描述语意匹配功能函数。举例,我定义的函数会加上描述,同时将方法函数和描述提供给Ai,Ai根据你的提问,自动选择匹配哪个方法。(语意匹配)

9.结构化输出

结构化输出,它是在数据处理、系统交互或 API 通信的底层环节中,将原始、非结构化 / 半结构化数据(如二进制流、日志文本、数据库行数据等)转化为格式固定、字段明确、机器可直接解析的结构化数据(如 JSON、Protocol Buffers、CSV 等)的过程。其核心目标是 “让底层数据从‘无规则’变为‘可被程序精准读取和处理’”,是上层应用(如数据分析、业务系统)可靠运行的基础。

我举个例子:

假设我们将上述配置传入大语言模型,并发送提示词:“请解析用户信息:姓名是李四,年龄30岁,身高1.8米,已婚。按照指定格式返回。”

由于配置了强制 JSON 输出和 Schema,模型会严格按照定义返回:

1
2
3
4
5
6
{
"name": "李四",
"age": 30,
"height": 1.8,
"married": true
}

同样,它也是有LowLevel实现和HighLevel实现

LowLevel实现

只需要封装一下Response就可以,代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
@GetMapping("/low/chat")
public String LowChat(@RequestParam("message") String message){
ResponseFormat responseFormat = ResponseFormat.builder()
.type(ResponseFormat.Type.JSON)
.jsonSchema(JsonSchema.builder()
.name("Person")
.rootElement(JsonObjectSchema.builder()
// 为 name 定义嵌套对象,包含 firstName 和 lastName
.addObjectProperty("name",
JsonObjectSchema.builder() // name 字段的子结构
.addStringProperty("firstName") // 名
.addStringProperty("lastName") // 姓
.required("firstName", "lastName") // 子字段必填
.build()
)
.addIntegerProperty("age")
.addNumberProperty("height")
.addBooleanProperty("married")
// 根对象的必填字段(包含嵌套的 name)
.required("name", "age", "height", "married")
.build())
.build())
.build();
return Chat.chat(ChatRequest.builder()
.responseFormat(responseFormat)
.messages(userMessage)
.build()).aiMessage().text();
}

就是设置一下回复格式(这里是使用JSON回复的,如有需要你可以采用别的格式,官方也是使用JSON格式进行演示的)

再将封装的回复格式同提问消息一同交给AI,这样回复的格式就是我们自定义的了。

HighLevel实现

使用HighLevel实现还是比较简单的,只需要封装一个返回类,封装一个接口,将接口交给AIService,AiService就会自动将回复的进行序列化返回。

返回类:

1
2
3
4
5
6
7
@Data
public class Person(){
private string name;
private int age;
private double high;
private double weigh;
}

接口

1
2
3
interface PersonService {
Person extractPersonFrom(String text);
}

测试类

1
2
3
4
5
6
7
8
@Resource
private ChatLanguageModel chatLanguageModel;

@GetMapping("/high/json")
public String highJson(@RequestParam(value = "message") String message) {
PersonService personService = AiServices.create(PersonService.class, chatLanguageModel);
return personService.extractPerson(message).toString();
}

你可以测试了:GET http://localhost:8080/high/json?message=李四今年28岁,身高1.75米,未婚

输出:

1
{"name":"李四","age":28,"height":1.75,"married":false}

对于联网搜索和多模态

联网搜索需要一个WebSearch模型,本质就是将Ai加上一个类似浏览器的搜索功能,当回答不了时,就会进行调用这个Search模型。

多模态就是可以传递给Ai图片,视频等,这些看官方文档就可以,就是封装而已。

联网搜索:Web Search Engines | LangChain4j 中文文档

多模态:图像模型 | LangChain4j 中文文档

@by 初榆