背景:

mac os 15.7

IDEA最新版

其他组件均为最新稳定版本
 

嗨嗨嗨!!最近ai很火,ai流行的框架也越来越多了。目前流行的有Spring团队原生支持的SpringAI和阿里旗下的Langchain4j框架

SpringAI适用于SpringBoot项目,而LangChain4j则是一种兼容的框架。它们的核心组件都是差不多的,可以根据个人的需要自由选择

前置条件需要本地部署大模型,如果还不是很了解的可以看看我的这篇文章:

https://bytedance.larkoffice.com/share/base/form/shrcn9BK1yGzHT4OIEvIK6R7ONb

补充:本次项目使用的deepseek-r1:14b是不支持tools工具调用的!!!这也可能是我配置失败的原因之一,如果大家对mcp开发有兴趣,一定要先确认自己选择的大模型是否支持工具调用!

——————————————————————项目搭建——————————————————————

最新版的IDEA是自带了SpringBoot的初始创建的,如果是老版本的IDEA社区版则需要手动创建项目结构引入POM文件,或者使用Spring插件也可以完成。如果是商业版则完全没有问题是一样的操作的

至于配置文件格式则是根据个人的习惯来就行,我个人比较习惯yaml的配置文件格式

这里的javaSDK版本选择21,因为LangChain4j框架的最低要求是java17,在这个基础上选一个高一点的稳定版本21就够了

完成以后选择下一步,开始选择依赖:

这里先只用选择基础的web框架和Flux流式响应的web框架

进阶一点的可以选上lombok

这个可以简化代码的书写,有的时候还是比较方便的,推荐选上

最重要的是Spring版本!!!建议选择Spring3+的稳定版本!!!!选择4版本不稳定,容易有各种bug,反正个人目前来说用过最好用的是2加版本哈哈哈啊哈💦

然后创建项目即可

创建完项目首先来到pom文件,将langchain4j的一些依赖补上去即可:

<?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.5.11</version>
        <relativePath/> <!-- lookup parent from repository -->
    </parent>
    <groupId>com.wc</groupId>
    <artifactId>LangChain4j</artifactId>
    <version>0.0.1-SNAPSHOT</version>
    <name>LangChain4j</name>
    <description>LangChain4j</description>
    <url/>
    <licenses>
        <license/>
    </licenses>
    <developers>
        <developer/>
    </developers>
    <scm>
        <connection/>
        <developerConnection/>
        <tag/>
        <url/>
    </scm>
    <properties>
        <java.version>21</java.version>
        <spring-ai.version>1.1.2</spring-ai.version>
    </properties>
    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>

        <dependency>
            <groupId>dev.langchain4j</groupId>
            <artifactId>langchain4j-ollama</artifactId>
            <version>1.11.0</version>
        </dependency>
        <dependency>
            <groupId>dev.langchain4j</groupId>
            <artifactId>langchain4j-spring-boot-starter</artifactId>
            <version>1.11.0-beta19</version>
        </dependency>
<!--这是自动配置yaml依赖-->
        <dependency>
            <groupId>dev.langchain4j</groupId>
            <artifactId>langchain4j-ollama-spring-boot-starter</artifactId>
            <version>1.11.0-beta19</version>
        </dependency>
<!--        LangChain本地集成ollama-->

        <dependency>
            <groupId>dev.langchain4j</groupId>
            <artifactId>langchain4j</artifactId>
            <version>1.11.0</version>
        </dependency>

        <dependency>
            <groupId>dev.langchain4j</groupId>
            <artifactId>langchain4j-mcp</artifactId>
            <version>1.1.0-beta7</version>
        </dependency>

        <dependency>
            <groupId>junit</groupId>
            <artifactId>junit</artifactId>
            <version>4.13.2</version>
            <scope>test</scope>
        </dependency>
        <dependency>
            <groupId>dev.langchain4j</groupId>
            <artifactId>langchain4j-reactor</artifactId>
            <version>1.1.0-beta7</version>
        </dependency>

        <dependency>
            <groupId>org.jsoup</groupId>
            <artifactId>jsoup</artifactId>
            <version>1.20.1</version>
        </dependency>


        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <optional>true</optional>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>
    </dependencies>
    <dependencyManagement>
        <dependencies>
            <dependency>
                <groupId>org.springframework.ai</groupId>
                <artifactId>spring-ai-bom</artifactId>
                <version>${spring-ai.version}</version>
                <type>pom</type>
                <scope>import</scope>
            </dependency>
        </dependencies>
    </dependencyManagement>

    <build>
        <plugins>
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-compiler-plugin</artifactId>
                <configuration>
                    <annotationProcessorPaths>
                        <path>
                            <groupId>org.projectlombok</groupId>
                            <artifactId>lombok</artifactId>
                        </path>
                    </annotationProcessorPaths>
                </configuration>
            </plugin>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
                <configuration>
                    <excludes>
                        <exclude>
                            <groupId>org.projectlombok</groupId>
                            <artifactId>lombok</artifactId>
                        </exclude>
                    </excludes>
                </configuration>
            </plugin>
        </plugins>
    </build>

</project>

一次性将本项目所有需要用的依赖都放上来了 ,可以直接复制。也可以根据个人项目进度选择依赖放入:

这是langchain4j适配ollama接口的依赖:

        <dependency>
            <groupId>dev.langchain4j</groupId>
            <artifactId>langchain4j-ollama</artifactId>
            <version>1.11.0</version>
        </dependency>

langchain4j基础依赖:

        <dependency>
            <groupId>dev.langchain4j</groupId>
            <artifactId>langchain4j-spring-boot-starter</artifactId>
            <version>1.11.0-beta19</version>
        </dependency>

langchain4j和Ollama集成Springboot:

       <dependency>
            <groupId>dev.langchain4j</groupId>
            <artifactId>langchain4j-ollama-spring-boot-starter</artifactId>
            <version>1.11.0-beta19</version>
        </dependency>

langchain4j基础依赖:

        <dependency>
            <groupId>dev.langchain4j</groupId>
            <artifactId>langchain4j</artifactId>
            <version>1.11.0</version>
        </dependency>

langchain4j的实现mcp服务接口的依赖:

        <dependency>
            <groupId>dev.langchain4j</groupId>
            <artifactId>langchain4j-mcp</artifactId>
            <version>1.1.0-beta7</version>
        </dependency>

测试单元:

        <dependency>
            <groupId>junit</groupId>
            <artifactId>junit</artifactId>
            <version>4.13.2</version>
            <scope>test</scope>
        </dependency>

langchain4j流式响应:

        <dependency>
            <groupId>dev.langchain4j</groupId>
            <artifactId>langchain4j-reactor</artifactId>
            <version>1.1.0-beta7</version>
        </dependency>

langchian with tools工具依赖:

       <dependency>
            <groupId>org.jsoup</groupId>
            <artifactId>jsoup</artifactId>
            <version>1.20.1</version>
        </dependency>

之后就差不多了

然后是yaml文件配置:

spring:
  application:
    name: LangChain4j

langchain4j:
  ollama:
    chat-model:
      base-url: http://localhost:11434
      model-name: deepseek-r1:14b
      log-requests: true        # 注意这里是 log-requests 不是 log-request
      log-responses: true       # 注意这里是 log-responses 不是 log-response
      temperature: 0.7
      timeout: 60s
      max-retries: 3
    embedding-model:
      base-url: http://localhost:11434
      model-name: Qwen3-Embedding:4b
      timeout: 60s
    streaming-chat-model:
      base-url: http://localhost:11434
      model-name: deepseek-r1:14b
      temperature: 0.7
      log-requests: true
      log-responses: true

server:
  port: 8081
  servlet:
    context-path: /api

# MCP 配置
mcp:
  enabled: true
  log-events: true

  search:
    engine: baidu
    allowed-engines: baidu,bing,csdn,juejin

  proxy:
    enabled: false

这里主要是来配置各种大模型的,如果没有本地配置也可以使用ai厂商提供的api服务进行调用,只是需要购买token再进行配置

langchain4j:
community:
  dashscope:
    chat-model:
      model-name: qwen-max
      api-key: <You API Key here>

至于模型的名称是你使用的模型的名字

当然更加灵活一点还可以使用手动@Configuration + @Bean手动配置chatModel

ChatModel qwenModel = QwenChatModel.builder()
                  .apiKey("You API key here")
                  .modelName("qwen-max")
                  .enableSearch(true)
                  .temperature(0.7)
                  .maxTokens(4096)
                  .stops(List.of("Hello"))
                  .build();

————————————————前置工作完成,小试牛刀!——————————————————

可以先不管项目的层级结构

先创建一个AiCodeHelper:

package com.wc.langchain4j.service;


import dev.langchain4j.data.message.AiMessage;
import dev.langchain4j.data.message.UserMessage;
import dev.langchain4j.model.chat.ChatModel;
import dev.langchain4j.model.chat.response.ChatResponse;
import jakarta.annotation.Resource;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;

@Service
@Slf4j
public class AiCodeHelper {

    @Resource
    private ChatModel deepseekChatModel;

    public String chat(String message){

        UserMessage userMessage = UserMessage.from(message);

        ChatResponse chatResponse = deepseekChatModel.chat(userMessage);

        AiMessage aiMessage = chatResponse.aiMessage();

        log.info("AI输出" + aiMessage.toString());

        return aiMessage.toString();
    }


}

在这个服务中注入我们的ChatModel,然后调用这个Model的各种方法

然后再写一个测试类:来看看我们的模型基本的回答能力

这里选择Debug运行

像这样测试通过就算是项目已经成功了一半了!!!哈哈哈哈哈

java也可以集成AI了,难道不是非常兴奋的一件事吗!!!

只是跑本地ai非常吃性能,比如我这里使用了本地配置的Deepseek-r1:14b,当时mac直接功耗直接拉满了 💦

直接是70多w的功耗💦,而且等出结果还需要等一会

不过当时我跑出来结果还是感觉很高兴的,嗨嗨嗨!!

好了闲话少叙,接下来可以开始Ai接口编写了

————————————————————AI Service-——————————————————————

像刚刚的AiCodeHelper的简单的小调用,可以看到内部的实现还是比较麻烦的

需要手动自定义UserMessage然后调用方法

langchain4j给出了一个更加方便的使用形式——AI Service,高度抽象的接口实现,使得业务代码的实现更加简单了 

和传统后端业务实现差不多,也是通过Service接口 + 一个实现类完成的,只是这里使用一个工厂代替了实现类的完成

Service定义业务能力,工厂负责内部的细节实现:

从这里就可以开始规范项目的结构了,比如经典的Service层,controller层之类:

这里就是项目的整个结构

清晰的项目结构可以让编程的思路更加清晰,通常service接口类都是在service目录下,然后之类的实现类则是在工厂目录下保存的

Service接口:

package com.wc.langchain4j.service;

import com.wc.langchain4j.model.Report;

import com.wc.langchain4j.model.SafeInputGuardrail;
import dev.langchain4j.service.MemoryId;
import dev.langchain4j.service.SystemMessage;
import dev.langchain4j.service.UserMessage;
import dev.langchain4j.service.guardrail.InputGuardrails;
import dev.langchain4j.service.spring.AiService;
import reactor.core.publisher.Flux;

//这是AI Service的业务开发模式,可以使用工厂类来实现这个业务接口

//或者直接使用@AiService注解来自动创建实例,注意这种方式就需要注释掉工厂类的@Configuration的注解了,否则会有依赖冲突的风险

//@AiService
@InputGuardrails({SafeInputGuardrail.class})
public interface AiCodeHelperService {
    

    @SystemMessage("你是一位编程小助手")
    String chat(@UserMessage String userMessage);

    @SystemMessage(fromResource = "system-prompt.txt")
    Report chatForReport(@UserMessage String userMessage);

    @SystemMessage("你是一位编程小助手")
    String chatWithMcp(@UserMessage String userMessage);

    @SystemMessage("你是一位编程小助手")
    dev.langchain4j.service.Result<String> chatWithRag(@UserMessage String userMessage);

    Flux<String> chatStream(@UserMessage String userMessage);





}

这里的@System注解是给ai调用加入系统提示词,可以专业化ai的人格,让回答更加专业

短的提示词可以直接写在注解里,如果提示词长了写在注解里非常不优雅,所以可以在资源文件目录中专门保存一个系统提示词,然后注解再调用它们

system-prompt.txt

只返回纯粹的 JSON 格式,不要添加任何解释,不要包含任何Markdown代码块标记比如(```json或者```)
你是一个专业的信息提取助手。请从给定文本中提取人员信息,
并严格按照以下 JSON 格式返回结果:

{
   "name": "人员姓名",
   "age": 年龄数字,
   "height": 身高(米),
   "married": true/false,
   "occupation": "职业"
}

重要规则:
1. 只返回纯粹的 JSON 格式,不要添加任何解释,不要包含任何Markdown代码块标记比如(```json或者```)
2. 如果信息不明确,使用 null
3. age 必须是数字,不是字符串
4. married 必须是布尔值

我们先来实现接口中的chat方法,其他方法稍后会介绍的,至于@InputGuardrails则是后面的拦截器的注解,这里可以先忽略


    @SystemMessage("你是一位编程小助手")
    String chat(@UserMessage String userMessage);

然后创建一个工厂类来实现这个接口:也就是填充一个bean

package com.wc.langchain4j.factory;

import com.wc.langchain4j.service.AiCodeHelperService;
import dev.langchain4j.mcp.McpToolProvider;
import dev.langchain4j.memory.ChatMemory;
import dev.langchain4j.memory.chat.MessageWindowChatMemory;
import dev.langchain4j.model.chat.ChatModel;
import dev.langchain4j.model.chat.StreamingChatModel;
import dev.langchain4j.rag.content.retriever.ContentRetriever;
import dev.langchain4j.service.AiServices;
import jakarta.annotation.Resource;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@Configuration
public class AiCodeHelperServiceFactory {

    @Resource
    private ChatModel deepseekChatModel;

    @Resource
    private ContentRetriever contentRetriever;

    @Autowired(required = false)
    private McpToolProvider mcpToolProvider;

    @Resource
    private StreamingChatModel deepseekStreamingChatModel;

    @Bean
    public AiCodeHelperService aiCodeHelperService() {

        ChatMemory chatMemory = MessageWindowChatMemory.withMaxMessages(10);

//        return AiServices.create(AiCodeHelperService.class, deepseekChatModel);
        //这个工厂类用于实现service接口,原理是反射出一个代理对象进行结果转换,这里使用create或者builder方法都可以

        return AiServices.builder(AiCodeHelperService.class)
                .chatModel(deepseekChatModel)
                .streamingChatModel(deepseekStreamingChatModel)
                .chatMemory(chatMemory)
                .contentRetriever(contentRetriever) //在构建AI Service时绑定内容检索器,RAG检索增强生成
                .toolProvider(mcpToolProvider)//调用mcp工具
                .build();
    }
}

目前来说@Resource的bean引入只需要一个ChatModel即可,除此之外的都是额外的配置:

我们只需要关心这些就行了:

        return AiServices.builder(AiCodeHelperService.class)
                .chatModel(deepseekChatModel)
                .build();

这里也可以使用crate方法进行装配,但是build会更加灵活,方便以后将其他额外配置进行构建

create:

@Configuration
public class AiCodeHelperServiceFactory {
​
   @Resource
   private ChatModel qwenChatModel;
​
   @Bean
   public AiCodeHelperService aiCodeHelperService() {
       return AiServices.create(AiCodeHelperService.class, qwenChatModel);
  }
}

比如这样引入千问的一个大模型

注意这里的@Configuration,这是手动装配bean 的核心注解,没有的话会出现经典的invoke null错误哈哈哈哈哈啊哈💦,当年学Spring 的时候没少吃过这个亏

这里create的底层原理是java的代理对象机制,我们开发的时候不用关系类型转换的种种细节,只需要安心编写接口和工厂类就行了,嗨嗨嗨!!

这是手动装配,比较麻烦一点,不过也比较灵活。当然还是可以使用自动装配的注解@AiService

注意这个时候需要用到之前的基础langchain4j集成springboot的依赖:

<dependency>
   <groupId>dev.langchain4j</groupId>
   <artifactId>langchain4j-spring-boot-starter</artifactId>
   <version>1.1.0-beta7</version>
</dependency>

然后直接给接口类加上注解即可自动装配好接口service的实现类:

@AiService
public interface AiCodeHelperService {
​
   @SystemMessage(fromResource = "system-prompt.txt")
   String chat(String userMessage);
}

注意两种方式只能选取一种方式进行,也就是要么使用@Configuration注解,要么使用@AiService注解。如果都使用的话会出现bean注入冲突的问题

然后进行单元测试:

@SpringBootTest

public class AiCodeHelperServiceTest {

    @Autowired
    private AiCodeHelperService aiCodeHelperService;

    @Test
    void chat(){

        if (aiCodeHelperService == null) {
            System.err.println("AiCodeHelperService is null!");
            return;
        }

        String result = aiCodeHelperService.chat("你好我是小明");

        System.out.println(result);

    }

看到这个提前bean的空值判断了吗,这个就是之前真的在这了卡了很久,从接口定义到工厂类全部检查了几遍的,项目重新来了两次的那种,都没有检查出问题,然后又怀疑是不是测试类的问题,结果全都没有问题哈哈哈💦

于是本来我说要不先做后面的功能算了,但是最后又试了一下,换了一个依赖版本,居然可以了!!!!!!!!!!!!!!!!!牛逼11!!!!

       详情可以看我另一个博客:https://blog.csdn.net/2402_87326256/article/details/158741694?fromshare=blogdetail&sharetype=blogdetail&sharerId=158741694&sharerefer=PC&sharesource=2402_87326256&sharefrom=from_link

总之还真是依赖的问题!!!当时反复怀疑create和builder方法到底有没有把接口实现出来哈哈哈,又换了@AiService注解还是有问题。总之还是长经验了

还是一样的debug运行

这样就是测试通过了,(我当时测试通过的时候真的非常惊喜!!!nb!!!!!)

—————————————————————Ai对话记忆————————————————————

维持ai的对话记忆,最传统的方式是使用一个自己维护的消息队列,但是非常麻烦

获取用户历史记录 -》添加用户新消息 -〉 构建完整历史上下文 -》 调用AI API -〉 保存AI回复到历史

现在langchain4j框架直接提供了现成的API,直接调用即可。

回到工厂类中将这个额外配置添加到build的构建过程即可:

        ChatMemory chatMemory = MessageWindowChatMemory.withMaxMessages(10);

非常简单,定义记住的最大消息数量为10。

添加到build构建:

        return AiServices.builder(AiCodeHelperService.class)
                .chatModel(deepseekChatModel)
                .streamingChatModel(deepseekStreamingChatModel)
                .chatMemory(chatMemory)

                .build();

像这里就算是配置好了

这里我们也可以看出手动写一个工厂类进行配置是比@AiService注解来得更加灵活的

@Configuration
public class AiCodeHelperServiceFactory {
​
   @Resource
   private ChatModel qwenChatModel;
​
   @Bean
   public AiCodeHelperService aiCodeHelperService() {
       // 会话记忆
       ChatMemory chatMemory = MessageWindowChatMemory.withMaxMessages(10);
       AiCodeHelperService aiCodeHelperService = AiServices.builder(AiCodeHelperService.class)
              .chatModel(qwenChatModel)
              .chatMemory(chatMemory)
              .build();
       return aiCodeHelperService;
  }
}

然后写一个测试类进行测试即可:

    @Test
    void chatMemory() {

        String result = aiCodeHelperService.chat("你好我是小明");

        System.out.println(result);

        result = aiCodeHelperService.chat("你好我是谁来着");

        System.out.println(result);
    }

debug运行:

像这样就是测试成功了

不过默认这种方式是将会话内容存储在内存中的,进阶用法是定义ChatMemoryStore接口来将数据存储在数据库中

不过本次项目是一个本地部署的项目,因此没有使用这种进阶的用法了,感兴趣的小伙伴可以看看鱼皮大佬的ai项目,里面详细讲述了这种进阶用法(不像我这种菜鸡哈哈哈哈💦)

————————————————————结构化输出———————————————————————

结构化输出,意思就是ai可以按照用户自定义的内容结构输出结构化的内容

比如从一段文本中提取出信息,并按照json格式输出

{
   "name": "人员姓名",
   "age": 年龄数字,
   "height": 身高(米),
   "married": true/false,
   "occupation": "职业"
}

因此还需要实现定义专门的系统提示词:

你是一个专业的信息提取助手。请从给定文本中提取人员信息,
并严格按照以下 JSON 格式返回结果:
​
{
   "name": "人员姓名",
   "age": 年龄数字,
   "height": 身高(米),
   "married": true/false,
   "occupation": "职业"
}
​
重要规则:
1. 只返回 JSON 格式,不要添加任何解释
2. 如果信息不明确,使用 null
3. age 必须是数字,不是字符串
4. married 必须是布尔值

我们再写一个结构化输出的单元测试:

    @Test
    void chatForReport() {

        String userMessage = "你好我是小坤,我是练习时长两年半的个人练习生,请帮我定制学习报告";

        Report report = aiCodeHelperService.chatForReport(userMessage);

        System.out.println(report);


    }

当然我们还要修改对话的返回值,让框架帮我们自实现结构化输出

添加一个model,用于定义一个输出结构:

package com.wc.langchain4j.model;

import java.util.List;

public record Report(String name, List<String> suggestions) {
    
}

编写测试单元:

    @Test
    void chatForReport() {

        String userMessage = "你好我是小坤,我是练习时长两年半的个人练习生,请帮我定制学习报告";

        Report report = aiCodeHelperService.chatForReport(userMessage);

        System.out.println(report);


    }

debug:

哈哈哈,信息的准确提取还是得看模型本身的实力才行,我这里部署的是deepseek-r1的孙子版本哈哈哈哈哈哈💦,因此这个功能不是很完美实现得。这里使用通用模型比专门的推理模型要更好

——————————————————RAG检索增强————————————————————————

RAG!!!这是最近很流行的一种说法

人工投喂信息给embedding模型,embdding模型会将信息转换为llm模型的知识再给ai,也就是增加了ai的信息来源,相当于ai通过RAG可以学习更多知识了,增加了知识储备

RAG使得ai回答信息的时候会查一查我们投喂的特定知识库,减少出现幻觉的情况

这里我们使用标准版的LangChain4j的RAG配置,虽然也有开箱即用的快速配置,但是实际开发还是使用标准版更加灵活:

  • 加载 Markdown 文档并按需切割

  • Markdown 文档补充文件名信息

  • 自定义 Embedding 模型

  • 自定义内容检索器

RAG需要两个模型,一个是embedding模型,将投喂的文章拆解,然后建立检索,转换为ai可以识别的向量模型

然后再是我们熟悉的llm大语言模型直接调用这些向量模型,从而达到扩充知识的目的

在yaml中新增我们的embedding模型:

langchain4j:
  ollama:
    chat-model:
      base-url: http://localhost:11434
      model-name: deepseek-r1:14b
      log-requests: true        # 注意这里是 log-requests 不是 log-request
      log-responses: true       # 注意这里是 log-responses 不是 log-response
      temperature: 0.7
      timeout: 60s
      max-retries: 3
    embedding-model:
      base-url: http://localhost:11434
      model-name: Qwen3-Embedding:4b
      timeout: 60s

这里需要重新在ollama中部署:我一般喜欢从终端中输入命令:

ollama pull Qwen3-Embedding:4b

embedding模型就没有llm模型那么吃性能,这里可以选择较好的模型进行部署

使用标准模式的RAG,需要我们手动书写一个config来执行RAG流程和自定义内容检索器

config.RagConfig.class:

package com.wc.langchain4j.config;

import dev.langchain4j.data.document.Document;
import dev.langchain4j.data.document.loader.FileSystemDocumentLoader;
import dev.langchain4j.data.document.splitter.DocumentByParagraphSplitter;
import dev.langchain4j.data.embedding.Embedding;
import dev.langchain4j.data.segment.TextSegment;
import dev.langchain4j.model.embedding.EmbeddingModel;
import dev.langchain4j.rag.content.retriever.ContentRetriever;
import dev.langchain4j.rag.content.retriever.EmbeddingStoreContentRetriever;
import dev.langchain4j.store.embedding.EmbeddingStore;
import dev.langchain4j.store.embedding.EmbeddingStoreIngestor;
import jakarta.annotation.Resource;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

import java.util.ArrayList;
import java.util.List;

@Configuration

/*
    检索增强,信息检索和和AI内容生成的混合架构,也就是给AI额外的信息来源,防止AI出现幻觉和知识空缺,确保回答基于真实的资料而不是想象
 */
public class RagConfig {

    @Resource
    private EmbeddingModel qwenEmbeddingModel;

    @Resource
    private EmbeddingStore<TextSegment> embeddingStore;

    @Bean
    public ContentRetriever contentRetriever() {

        //1.从资源文件目录中加载文档
        List<Document> documents = FileSystemDocumentLoader.loadDocuments("src/main/resources/docs");

        //2.定义一个文档切割:将文档按照段落进行切割,最大1000字符,每次最多重叠200个字符
        DocumentByParagraphSplitter paragraphSplitter = new DocumentByParagraphSplitter(1000,200);

        //3.自定义文档加载器
        EmbeddingStoreIngestor ingestor = EmbeddingStoreIngestor.builder().documentSplitter(paragraphSplitter)
                //给每个textsegment添加文档名称
                .textSegmentTransformer(textsegment -> TextSegment.from(
                        textsegment.metadata().getString("file_name") + "\n" + textsegment.text(),
                        textsegment.metadata()
                        //使用指定的向量模型
                )).embeddingModel(qwenEmbeddingModel).embeddingStore(embeddingStore).build();

        ingestor.ingest(documents);


        //自定义内容查询器
        return EmbeddingStoreContentRetriever.builder()
                .embeddingStore(embeddingStore)
                .embeddingModel(qwenEmbeddingModel)
                .maxResults(5)
                .minScore(0.75)
                .build();

    }

}

注意这里的文档加载路径:

在resources中创建docs,当然也可以自定义文档的加载路径,只要确保文档确实在这个路径即可。

然后在里面放一些markdown的文档,实际上直接从word文档中复制到markdown也没有什么问题

有了这个配置,再到工厂类中添加到build的构建过程就可以正常使用了:

    @Bean
    public AiCodeHelperService aiCodeHelperService() {

        ChatMemory chatMemory = MessageWindowChatMemory.withMaxMessages(10);

//        return AiServices.create(AiCodeHelperService.class, deepseekChatModel);
        //这个工厂类用于实现service接口,原理是反射出一个代理对象进行结果转换,这里使用create或者builder方法都可以

        return AiServices.builder(AiCodeHelperService.class)
                .chatModel(deepseekChatModel)
                .chatMemory(chatMemory)
                .contentRetriever(contentRetriever) //在构建AI Service时绑定内容检索器,RAG检索增强生成
                .build();

非常灵活,不是吗哈哈哈

一般这样就可以了,如果想增加可信度,比如展示回答的时候点名知识来源,可以在Ai Service接口的返回类型String上封装一个Result,本次项目采取这样的方式:

    @SystemMessage("你是一位编程小助手")
    dev.langchain4j.service.Result<String> chatWithRag(@UserMessage String userMessage);

然后再来一个测试类,测试ai是否消化了刚刚投喂的文档:

    @Test
    void chatWithRag(){
        //我靠,真的牛逼,可以根据你给出的markdown文档进行知识库的积累!!!相当于AI也会学习总结东西嘛!RAG!!!
        //因此不管什么内容多么冗杂,都可以丢给AI让它帮你总结!!!nb!!!!!!!!

        Result<String> result = aiCodeHelperService.chatWithRag(" 线程崩溃了,进程也会崩溃吗?");

        System.out.println(result.content());

        System.out.println(result.sources());
    }

看看我们的测试结果:

可以看到,ai输出了回答和引用来源(

System.out.println(result.sources());

还是非常有意思的,像是ai也会学习了哈哈哈,可以根据你投喂的文档总结内容

————————————————MCP模型上下文——————————————————————

MCP就是扩展Ai的接口,Ai可以通过mcp完成联网搜索等各种功能

但是目前Langchain4j框架对mcp的开发适配程度还不是很好,而且使用java开发也有点麻烦,💦

我自己尝试了过mcp的开发的联网搜索功能,想用本地化不需要额外购买WebSearch服务的方式实现

我是用的npx本地运行open-websearch服务,但是一直连接超时

更换搜索引擎,各种排查错误也不知怎么回事

感兴趣的小伙伴可以自己尝试一下:

首先是yaml配置mcp服务:

server:
  port: 8081
  servlet:
    context-path: /api

# MCP 配置
mcp:
  enabled: true
  log-events: true

  search:
    engine: baidu
    allowed-engines: baidu,bing,csdn,juejin

  proxy:
    enabled: false

用不用代理我都试了一下,但是都连接超时💦

然后新建一个配置类:

package com.wc.langchain4j.config;

import dev.langchain4j.mcp.McpToolProvider;
import dev.langchain4j.mcp.client.DefaultMcpClient;
import dev.langchain4j.mcp.client.McpClient;
import dev.langchain4j.mcp.client.transport.stdio.StdioMcpTransport;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

import java.time.Duration;
import java.util.List;
import java.util.Map;

@Configuration
public class McpConfig {

    @Value("${open-websearch.search-engine:baidu}")
    private String searchEngine;

    @Value("${open-websearch.use-proxy:false}")
    private boolean useProxy;

    @Value("${open-websearch.proxy-url:}")
    private String proxyUrl;

    @Value("${mcp.log-events:true}")
    private boolean logEvents;

    @Bean
    public McpToolProvider mcpToolProvider() {
        // 构建环境变量
        Map<String, String> env = new java.util.HashMap<>();
        env.put("DEFAULT_SEARCH_ENGINE", searchEngine);
        env.put("MODE", "stdio");  // 强制使用stdio模式

        if (useProxy && proxyUrl != null && !proxyUrl.isEmpty()) {
            env.put("USE_PROXY", "true");
            env.put("PROXY_URL", proxyUrl);
        }

        // 配置传输层
        StdioMcpTransport transport = new StdioMcpTransport.Builder()
                .command(List.of("bing-cn-mcp"))
                .environment(env)
                .logEvents(logEvents)
                .build();

        McpClient mcpClient = new DefaultMcpClient.Builder()
                .transport(transport)
                .pingTimeout(Duration.ofSeconds(60))
                .build();

        return McpToolProvider.builder()
                .mcpClients(List.of(mcpClient))
                .build();
    }
}

注意如果后面测试失败的话还是最好注释掉这个config,不然项目已开始就会因为nullBean报错,从而根本没办法跑通业务

然后是修改工厂类,新添加一个配置对象:
 

    @Autowired(required = false)
    private McpToolProvider mcpToolProvider;//mcp服务适配器

但是由于是配置失败的,因此这里使用自动装配的时候加上一个false注解值,允许注入一个空的null,不然测试直接不同过的 

然后将mcp添加到AIservice的构建过程:

        return AiServices.builder(AiCodeHelperService.class)
                .chatModel(deepseekChatModel)
                .streamingChatModel(deepseekStreamingChatModel)
                .chatMemory(chatMemory)
                .contentRetriever(contentRetriever) //在构建AI Service时绑定内容检索器,RAG检索增强生成
                
                .toolProvider(mcpToolProvider)//调用mcp工具
                .build();

但是实际上如果是null的话没有什么作用的

然后是新增接口方法:

    @SystemMessage("你是一位编程小助手")
    String chatWithMcp(@UserMessage String userMessage);

再进行测试:

    /*
        目前mcp开发在java上的生态还不成熟,因此这里为了先把业务跑通,只能先允许注入一个空的null的mcp服务提供
     */
    @Test
    void chatWithMcp(){

        String result = aiCodeHelperService.chatWithMcp("什么是十万个为什么?");

        System.out.println(result);
    }

测试是可以正常通过,但是不会联网进行搜索了,只会利用已知的知识进行搜索

————————————————————拦截器————————————————————————

使用拦截器可以在用户的请求到达ai之前就检查一遍信息,防止有敏感内容

首先实现一个拦截器的接口:

拦截器:

package com.wc.langchain4j.model;

import dev.langchain4j.data.message.UserMessage;
import dev.langchain4j.guardrail.InputGuardrail;
import dev.langchain4j.guardrail.InputGuardrailResult;

import java.io.Serializable;
import java.util.HashSet;
import java.util.Set;

public class SafeInputGuardrail implements InputGuardrail {

    /*
        自定义的一个拦截器:在用户的请求到达ai服务之前先筛选掉含有敏感词的请求
     */

    private static final Set<String> sensitiveWords = Set.of("kill","evil");//敏感词

    @Override
    public InputGuardrailResult validate(UserMessage userMessage) {

        String inputText = userMessage.singleText().toLowerCase();//将用户的输入全部转换为小写

        String[] words = inputText.split("\\W+");//将输入的文本切割为单词

        for(String word : words){//遍历所有的单词,检查是否存在敏感词

            if(sensitiveWords.contains(word)){

                return fatal("Sensitive word detected :" + word);
            }
        }

        return success();
    }
}

然后再回到我们的Aiservice接口了,这个时候就可以接入我们的拦截器了:

@InputGuardrails({SafeInputGuardrail.class})

AiService:

package com.wc.langchain4j.service;

import com.wc.langchain4j.model.Report;

import com.wc.langchain4j.model.SafeInputGuardrail;
import dev.langchain4j.service.MemoryId;
import dev.langchain4j.service.SystemMessage;
import dev.langchain4j.service.UserMessage;
import dev.langchain4j.service.guardrail.InputGuardrails;
import dev.langchain4j.service.spring.AiService;
import reactor.core.publisher.Flux;

//这是AI Service的业务开发模式,可以使用工厂类来实现这个业务接口

//或者直接使用@AiService注解来自动创建实例,注意这种方式就需要注释掉工厂类的@Configuration的注解了,否则会有依赖冲突的风险

//@AiService
@InputGuardrails({SafeInputGuardrail.class})
public interface AiCodeHelperService {
    

    @SystemMessage("你是一位编程小助手")
    String chat(@UserMessage String userMessage);

    @SystemMessage(fromResource = "system-prompt.txt")
    Report chatForReport(@UserMessage String userMessage);

    @SystemMessage("你是一位编程小助手")
    String chatWithMcp(@UserMessage String userMessage);

    @SystemMessage("你是一位编程小助手")
    dev.langchain4j.service.Result<String> chatWithRag(@UserMessage String userMessage);

    Flux<String> chatStream(@UserMessage String userMessage);





}

然后进行测试:

    @Test
    void chatWithGuardrail(){

        String result = aiCodeHelperService.chat("kill the game");

        System.out.println(result);
    }

测试结果不通过才是对的,说明拦截器生效了:

————————————————————添加日志功能————————————————————

有些ChatModel是支持日志实现的,直接在yaml配置中配置一下即可:
 

      log-requests: true        # 注意这里是 log-requests 不是 log-request
      log-responses: true       # 注意这里是 log-responses 不是 log-response

如果不支持日志的话得手动进行配置listener了:具体参考鱼皮大佬的code:

新建 listener.ChatModelListenerConfig,输出请求、响应、错误信息:

@Configuration
@Slf4j
public class ChatModelListenerConfig {
   
   @Bean
   ChatModelListener chatModelListener() {
       return new ChatModelListener() {
           @Override
           public void onRequest(ChatModelRequestContext requestContext) {
               log.info("onRequest(): {}", requestContext.chatRequest());
          }
​
           @Override
           public void onResponse(ChatModelResponseContext responseContext) {
               log.info("onResponse(): {}", responseContext.chatResponse());
          }
​
           @Override
           public void onError(ChatModelErrorContext errorContext) {
               log.info("onError(): {}", errorContext.error().getMessage());
          }
      };
  }
}

然后构造的时候添加给build:
 

新建 model.QwenChatModelConfig,构造 ChatModel 对象并绑定 Listener:

@Configuration
@ConfigurationProperties(prefix = "langchain4j.community.dashscope.chat-model")
@Data
public class QwenChatModelConfig {
​
   private String modelName;
​
   private String apiKey;
​
   @Resource
   private ChatModelListener chatModelListener;
​
   @Bean
   public ChatModel myQwenChatModel() {
       return QwenChatModel.builder()
              .apiKey(apiKey)
              .modelName(modelName)
              .listeners(List.of(chatModelListener))
              .build();
  }
}

————————————————SSE流式接口的开发————————————————————

到这里差不多AI的功能已经开发的差不多,完整的项目要加上前端,那么得写一个接口给前端使用:

对于AI应用来说使用流式响应比同步响应体验更好,这里使用flux进行接口的编写:

首先引入流式接口的依赖:

<dependency>
   <groupId>dev.langchain4j</groupId>
   <artifactId>langchain4j-reactor</artifactId>
   <version>1.1.0-beta7</version>
</dependency>

然后给AI接口新增流式对话的方法:
 

    Flux<String> chatStream(@UserMessage String userMessage);

再在yaml配置中新增流式模型:

    streaming-chat-model:
      base-url: http://localhost:11434
      model-name: deepseek-r1:14b
      temperature: 0.7
      log-requests: true
      log-responses: true

然后直接在AIService的工厂类中添加到build的过程:


    @Resource
    private StreamingChatModel deepseekStreamingChatModel;//流式对话模型,也是Chat模型

        return AiServices.builder(AiCodeHelperService.class)
                .chatModel(deepseekChatModel)
                .streamingChatModel(deepseekStreamingChatModel)
                .chatMemory(chatMemory)
                .contentRetriever(contentRetriever) //在构建AI Service时绑定内容检索器,RAG检索增强生成
                .toolProvider(mcpToolProvider)//调用mcp工具
                .build();

然后新增一个服务器yaml配置,准备实地测试了:

server:
port: 8081
servlet:
  context-path: /api

再启动项目进行测试:

curl -G 'http://localhost:8081/api/ai/chat' \
 --data-urlencode 'message=你好' 

测试结果:

这样看到类似于打字机一样实时输出结果,就说明流式接口是ok的

————————————————跨域支持——————————————————————————

最后让后端的代码可以成功和前端沟通上,需要自定义一个跨域支持的配置类:

package com.wc.langchain4j.config;

import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.CorsRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;

@Configuration
public class CorsConfig implements WebMvcConfigurer {

    @Override
    public void addCorsMappings(CorsRegistry registry){

        registry.addMapping("/**")
                .allowCredentials(true)
                .allowedOriginPatterns("*")
                .allowedMethods("GET", "POST", "PUT", "DELETE", "OPTIONS")
                .allowedHeaders("*")
                .exposedHeaders("*");
    }
}

至此后端部分就大功告成了!!!!!!!哈哈哈哈💦

(实际上我真是修了各种bug才走到这里的哈哈哈💦)

——————————————————AI前端————————————————————————

对于后端的同学,前端部分就使用AI来实现就好了:这里使用字节的Trae进行前端页面生成

对话的时候加入提示词,让AI运行更加专业:这里使用国内版的Trae

然后前端框架选的是vue

你是一位专业的前端开发,请帮我根据下列信息来生成对应的前端项目代码。

## 需求

应用为《AI 编程小助手》,帮助用户解答编程学习和求职面试相关的问题,并给出建议。

只有一个页面,就是主页:页面风格为聊天室,上方是聊天记录(用户信息在右边,AI 信息在左边),下方是输入框,进入页面后自动生成一个聊天室 id,用于区分不同的会话。通过 SSE 的方式调用 chat 接口,实时显示对话内容。

## 技术选型

1. Vue3 项目
2. Axios 请求库

## 后端接口信息

接口地址前缀:http://localhost:8081/api

## SpringBoot 后端接口代码

@RestController
@RequestMapping("/ai")
public class AiController {

   @GetMapping("/chat")
   public Flux<ServerSentEvent<String>> chat(int memoryId, String message) {
       return aiCodeHelperService.chatStream(memoryId, message)
               .map(chunk -> ServerSentEvent.<String>builder()
                       .data(chunk)
                       .build());
   }
}

然后让AI生成就行了!!

如果中途有bug再去让AI自查一下就行(真是nb现在的AI)

生成完以后大概是这样

页面展示:

然后我们来试试对话怎么样:

哈哈哈哈哈,还是很好玩的

项目完全在本地运行!!!!做出来还是比较有成就感的!!!!嗨嗨嗨!!!

但是目前AI生成的前端只有最基础的记忆会话方法,其他的RAG以及流式响应页面还有待后续进一步开发~

那么本次项目到这里也差不多告一段落咯!!!

这次使用的是Langchain4j的框架,当然使用SPring AI也是可以的。

如果有任何的问题欢迎留言或私信,本人菜鸡,文章有错误还请轻喷💦哈哈哈

感谢看到这里的小伙伴,那么我们下次再见!!!

                                ——————————————————2026年3月9日

Logo

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

更多推荐