一篇搞定Springboot整合AI- langchain4j

注:本方式借鉴著名博主(鱼皮)bilib教程https://www.bilibili.com/video/BV1X4GGziEyr/?spm_id_from=333.337.search-card.all.click&vd_source=e1ca9777ff5bb1eb549f04a3c1626d1chttps://www.bilibili.com/video/BV1X4GGziEyr/?spm_id_from=333.337.search-card.all.click&vd_source=e1ca9777ff5bb1eb549f04a3c1626d1c

喜欢看视频的小伙伴直接去看博主讲解的。

一、开始前

1、去阿里大模型开放平台注册并创建一个密钥

(老样子的别泄露咯!)https://bailian.console.aliyun.com/https://bailian.console.aliyun.com/

(1)这里的模型调用如果产生费用会直接扣你阿里云账号上面的钱钱!!所有调用模型要注意看有没有免费token。

(2)查看是否有免费token方式

2、langchain4j中文文档路径

https://docs.langchain4j.info/get-startedhttps://docs.langchain4j.info/get-started

二、简单使用

1、导入依赖

        <!-- 阿里社区langchain4j包 -->
        <dependency>
            <groupId>dev.langchain4j</groupId>
            <artifactId>langchain4j-community-dashscope-spring-boot-starter</artifactId>
            <version>1.1.0-beta7</version>
        </dependency>

2、配置application.yml

langchain4j:
  community:
    dashscope: # 阿里社区
      chat-model:
        model-name: deepseek-v3.1 # 有免费token数
        api-key: 你的密钥
      embedding-model: # RAG
        model-name: text-embedding-v4  # 有免费token数
        api-key: 你的密钥
      streaming-chat-model: # 流式输出
        model-name: qwen-max # qwen-max 这个模型要钱!慎用!!!
        api-key: 你的密钥

3、实现代码


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

@Slf4j
@Service
@RequiredArgsConstructor
public class AiCodeHelper {

    private final ChatModel qwenChatModel;

    public static final String SYSTEM_MESSAGE = "你叫攀小黑,是一名程序员";


    // 简单对话
    public String chat(String message) {
        UserMessage userMessage = UserMessage.from(message);
        ChatResponse chat = qwenChatModel.chat(userMessage);
        AiMessage aiMessage = chat.aiMessage();
        log.info("AI输出 : "+ aiMessage.toString());
        return aiMessage.text();
    }

    // 简单对话 - 用户自定义消息
    public String chatWithMessage(UserMessage userMessage) {
        ChatResponse chat = qwenChatModel.chat(userMessage);
        AiMessage aiMessage = chat.aiMessage();
        log.info("AI输出 : "+ aiMessage.toString());
        return aiMessage.text();
    }

    // 简单对话 - 系统提示词
    public String chatSystemMessage(String message) {
        SystemMessage systemMessage = SystemMessage.from(SYSTEM_MESSAGE);
        UserMessage userMessage = UserMessage.from(message);
        ChatResponse chat = qwenChatModel.chat(userMessage, systemMessage);
        AiMessage aiMessage = chat.aiMessage();
        log.info("AI输出 : "+ aiMessage.toString());
        return aiMessage.text();
    }


}

因为涉及到spring Bean注入,这里测试需要用@SpringBootTest 去测试

4、测试代码

@SpringBootTest
public class AiUnitTest {

    @Autowired
    private AiCodeHelper aiCodeHelper;

    @Test
    public void test() {
        String message = "你是谁";
        aiCodeHelper.chat(message);
    }

    @Test
    public void testWithMessage() {
        UserMessage userMessage = UserMessage.from(
            TextContent.from("描述图片"),
            ImageContent.from("https://admin.panxiaohei.cn/...")
        );
        aiCodeHelper.chatWithMessage(userMessage);
    }

    @Test
    public void testSystemMessage() {
        String message = "你是谁";
        aiCodeHelper.chatSystemMessage(message);
    }
}

三、整合spring boot

1、按需求导入依赖

        <!-- 阿里langchain4j包 -->
        <dependency>
            <groupId>dev.langchain4j</groupId>
            <artifactId>langchain4j-community-dashscope-spring-boot-starter</artifactId>
            <version>1.1.0-beta7</version>
        </dependency>
        <!--反射机制-->
        <dependency>
            <groupId>dev.langchain4j</groupId>
            <artifactId>langchain4j-spring-boot-starter</artifactId>
            <version>1.1.0-beta7</version>
        </dependency>
        <!--AiService-->
        <dependency>
            <groupId>dev.langchain4j</groupId>
            <artifactId>langchain4j</artifactId>
            <version>1.1.0</version>
        </dependency>
        <!-- RAG -->
        <dependency>
            <groupId>dev.langchain4j</groupId>
            <artifactId>langchain4j-easy-rag</artifactId>
            <version>1.1.0-beta7</version>
        </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>

2、AiCodeHelperService 服务类


import dev.langchain4j.service.MemoryId;
import dev.langchain4j.service.Result;
import dev.langchain4j.service.SystemMessage;
import dev.langchain4j.service.UserMessage;
import dev.langchain4j.service.guardrail.InputGuardrails;
import org.dromara.pxhai.guardrail.SafeInputGuardrail;
import reactor.core.publisher.Flux;

import java.util.List;

//@AiService # 封装的服务类不太灵活,不建议使用
@InputGuardrails(SafeInputGuardrail.class)
public interface AiCodeHelperService {

//    @SystemMessage("你叫攀小黑,是一名程序员")
    @SystemMessage(fromResource = "system-prompt.txt")
    String chat(String userMessage);

//    // 指定一个会话的记忆ID
//    @SystemMessage(fromResource = "system-prompt.txt")
//    String chat(@MemoryId int memoryId, String userMessage);

    // 指定对象返回
    @SystemMessage(fromResource = "system-prompt.txt")
    Report chatForReport(String userMessage);

    // 学习报告类(快速实现实体)
    record Report(String name, List<String> suggestionList){};

    // 返回引用文档
    @SystemMessage(fromResource = "system-prompt.txt")
    Result<String> chatWithReference(String userMessage);

    // 流式输出 - 需要修改配置文件
    @SystemMessage(fromResource = "system-prompt.txt")
    Flux<String> chatSSE(@MemoryId int memoryId, @UserMessage String userMessage);


}

3、工厂类(接口反射实现继承)


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 lombok.RequiredArgsConstructor;
import org.dromara.pxhai.apply.AiCodeHelperService;
import org.dromara.pxhai.domain.application.PXHAiConfig;
import org.dromara.pxhai.tools.RealTimeNewsTool;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@Configuration
@RequiredArgsConstructor
public class AicodeHelperServiceFactory {

    private final ChatModel myQwenChatModel;
    private final ContentRetriever contentRetriever; // RAG
    private final StreamingChatModel qwenStreamingChatModel; // 流式
    private final PXHAiConfig pxhAiConfig;


    /**
     * 工程模式-反射机制实现接口类
     * 可以导入 下面依赖 实现自动生成实现类
     *          <dependency>
     *             <groupId>dev.langchain4j</groupId>
     *             <artifactId>langchain4j-spring-boot-starter</artifactId>
     *             <version>1.1.0-beta7</version>
     *         </dependency>
     *  在service中使用@AiService
     *  注意,
     *  1、如果使用注解方式就必须把AicodeHelperServiceFactory类@Configuration注释,不然会引发重复bean
     *  2、不建议使用注解方式,不灵活
     * @return
     */
    @Bean
    public AiCodeHelperService aiCodeHelperService() {
        // 会话记忆
        ChatMemory chatMemory = MessageWindowChatMemory.withMaxMessages(10);
        // 持久化会话记忆
//        ChatMemory chatMemory = MessageWindowChatMemory.builder()
//            .id("12345")
//            .maxMessages(10)
//            .chatMemoryStore(new PersistentChatMemoryStore())
//            .build();


//        // 响应格式
//        ResponseFormat responseFormat = ResponseFormat.builder()
//            .type(JSON) // 类型可以是 TEXT(默认)或 JSON
//            .jsonSchema(JsonSchema.builder()
//                .name("Person") // OpenAI 要求为 schema 指定名称
//                .rootElement(JsonObjectSchema.builder() // 见下面的 [1]
//                    .addStringProperty("name")
//                    .addIntegerProperty("age")
//                    .addNumberProperty("height")
//                    .addBooleanProperty("married")
//                    .required("name", "age", "height", "married") // 见下面的 [2]
//                    .build())
//                .build())
//            .build();
//        ChatRequest chatRequest = ChatRequest.builder()
//            .responseFormat(responseFormat)
//            .messages(new UserMessage("你叫攀小黑,是一名程序员"))
//            .build();
//        ChatResponse chatResponse = qwenChatModel.chat(chatRequest);
//        String output = chatResponse.aiMessage().text();
//        System.out.println(output); // {"name":"John","age":42,"h


        AiServices<AiCodeHelperService> aiServices = AiServices.builder(AiCodeHelperService.class)
            .chatModel(myQwenChatModel) // 模型
            .streamingChatModel(qwenStreamingChatModel) // 流式输出
            .chatMemory(chatMemory) // 会话记忆
            .chatMemoryProvider(memoryId -> MessageWindowChatMemory.withMaxMessages(10)) // 会话记忆提供者
            .tools(new RealTimeNewsTool());// 工具调用 (需要定义工具方法,给定提示词和返回)
//            .chatMemoryProvider( memoryId -> MessageWindowChatMemory.withMaxMessages(10)) // 会话记忆提供者(需要在接口类 String chat(@MemoryId int memoryId, String userMessage);)

        if (pxhAiConfig.getOpenRag()) {
            aiServices.contentRetriever(contentRetriever); // RAG
        }

        AiCodeHelperService aiCodeHelperService = aiServices.build();


        // 构造 AI 服务
//        AiCodeHelperService aiCodeHelperService = AiServices.builder(AiCodeHelperService.class)
//            .chatModel(myQwenChatModel) // 模型
//            .streamingChatModel(qwenStreamingChatModel) // 流式输出
//            .chatMemory(chatMemory) // 会话记忆
//            .chatMemoryProvider(memoryId -> MessageWindowChatMemory.withMaxMessages(10)) // 会话记忆提供者
//            .contentRetriever(contentRetriever) // RAG
//            .tools(new RealTimeNewsTool()) // 工具调用 (需要定义工具方法,给定提示词和返回)
////            .chatMemoryProvider( memoryId -> MessageWindowChatMemory.withMaxMessages(10)) // 会话记忆提供者(需要在接口类 String chat(@MemoryId int memoryId, String userMessage);)
//            .build();
        return aiCodeHelperService;

//        return AiServices.create(AiCodeHelperService.class, qwenChatModel);
    }

这里引用的一些其他方法在后面。

4、我自定义的配置开关

import lombok.Data;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;

@Data
@Component("pxhAiConfig")
public class PXHAiConfig {

    @Value("${pxh.ai.open-rag}")
    private Boolean openRag;

}

.yml配置

pxh:
  ai:
    open-rag: false

5、RAG配置

调用ai前对本地文章进行检索,并将匹配值较高的内容一起发送给AI。


import dev.langchain4j.data.document.Document;
import dev.langchain4j.data.document.loader.FileSystemDocumentLoader;
import dev.langchain4j.data.document.splitter.DocumentByParagraphSplitter;
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 lombok.RequiredArgsConstructor;
import org.dromara.pxhai.domain.application.PXHAiConfig;
import org.springframework.boot.autoconfigure.condition.ConditionalOnExpression;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

import java.util.List;

/**
 * 加载 RAG
 */
@Configuration
@RequiredArgsConstructor
public class RagConfig {

    private final EmbeddingModel qwenEmbeddingModel;

    @Resource
    private EmbeddingStore<TextSegment> embeddingStore;

    @Bean
    @ConditionalOnExpression("@pxhAiConfig.openRag") // 动态注入
    public ContentRetriever contentRetriever() {
        // RAG
        // 1.加载文档(只会扫码当前目录,不会扫码子目录,需要自己在配置)
        List<Document> documents = FileSystemDocumentLoader.loadDocuments("E:\\");
        // 2.文档切割:每个文档按照段落进行切割,最大1000字符,每次最多重叠200字符()
        DocumentByParagraphSplitter documentByParagraphSplitter =
            new DocumentByParagraphSplitter(1000, 200);
        // 3。自定义文档加载器,把文档转换成向量并保存到向量数据库中
        EmbeddingStoreIngestor ingestor = EmbeddingStoreIngestor.builder()
            .documentSplitter(documentByParagraphSplitter)
            // 为了提高文档质量:为给每个切割后的文档碎片 TextSegment 添加文档名作为元信息
            .textSegmentTransformer(textSegment -> {
                return TextSegment.from(textSegment.metadata().getString("file_name")
                    + "\n" + textSegment.text());
            })
            // 使用指定的向量模型
            .embeddingModel(qwenEmbeddingModel)
            .embeddingStore(embeddingStore) // 保存向量
            .build();
        // 加载文档
        ingestor.ingest(documents);
        // 4.自定义内容加载器
        EmbeddingStoreContentRetriever contentRetriever = EmbeddingStoreContentRetriever.builder()
            .embeddingStore(embeddingStore)
            .embeddingModel(qwenEmbeddingModel)
            .maxResults(5) // 最多返回5个结果
            .minScore(0.75) // 过滤掉分数小于0.75的结果
            .build();
        return contentRetriever;
    }
}

6、工具调用(MCP)

(1)工具调用主要是用来给AI去调用的,当你提出的问题需要调用某些工具时,Ai会主动向我们的程序发出请求并调用这个工具并将结果返回给用户。如果你是要生成dpf等工具直接返回给前端,那么工具调用成功后就不用再去调用Ai了

(2)mcp广场,具体教程这里不多说,看官方文档

https://mcp.so/zh/servers?tag=featuredhttps://mcp.so/zh/servers?tag=featured


import dev.langchain4j.agent.tool.P;
import dev.langchain4j.agent.tool.Tool;
import lombok.extern.slf4j.Slf4j;
import org.jsoup.Jsoup;
import org.jsoup.nodes.Document;
import org.jsoup.nodes.Element;
import org.jsoup.select.Elements;

import java.io.IOException;
import java.net.URLEncoder;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.List;

/**
 * 工具调用
 */
@Slf4j
public class RealTimeNewsTool {


    /**
     * 一个简单的工具调用,主要实现网页爬取,并返回列表数据
     * P注解 是AI提示词传入的参数 required = true为必填
     * 给AI的提示词最后不用中文,使用英文
     * @param keyword 搜索关键词
     * @return 面试题列表,若失败返回失败信息。
     */
    @Tool(name = "realTimeNewsTool", value = """
        This is a tool that mainly returns the latest real-time news data.
        It can give you the latest news list.
    """)
    public String searchRealTimeNewsTool(@P(value = "the keyword to search" , required = true) String keyword) {
        List<String> questions =new ArrayList<>();
        //构建搜索URL(编码关键词以支持中文)
        String encodedKeyword = URLEncoder.encode(keyword, StandardCharsets.UTF_8);
        String url = "https://ww.mianshiya.com/search/all?searchText=" + encodedKeyword;//发送请求并解析页面
        Document doc;
        try {
            doc = Jsoup.connect(url)
                .userAgent("Mozilla/5.0")
                .timeout(5000)
                .get();
        }catch(IOException e) {
            log.error("get web error", e);
            return e.getMessage();
        }
        Elements questionElements = doc.select(".ant-table-cell > a");
        questionElements.forEach(element -> questions.add(element.text().trim()));
        return String.join("\n", questions);
    }



}

7、自定义日志

用于和Ai对话时打印日志,虽然官网上有,但是有一些不生效。毕竟还没有成熟。


import dev.langchain4j.community.model.dashscope.QwenChatModel;
import dev.langchain4j.model.chat.ChatModel;
import dev.langchain4j.model.chat.listener.ChatModelListener;
import jakarta.annotation.Resource;
import lombok.Data;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

import java.util.List;

/**
 * 自定义模型配置,用于日志打印
 */
@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()
            .modelName(modelName)
            .apiKey(apiKey)
             .listeners(List.of(chatModelListener))
            .build();
    }
}

import dev.langchain4j.model.chat.listener.ChatModelErrorContext;
import dev.langchain4j.model.chat.listener.ChatModelListener;
import dev.langchain4j.model.chat.listener.ChatModelRequestContext;
import dev.langchain4j.model.chat.listener.ChatModelResponseContext;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

/**
 * 聊天模型监听器(日志监听)
 */
@Configuration
public class ChatModelListenersConfiguration {

    @Bean
    ChatModelListener chatModelListener() {
        return new ChatModelListener() {

            private static final Logger log = LoggerFactory.getLogger(ChatModelListener.class);

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

8、安全过滤器

(1)访问Ai前过滤


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

import java.util.Set;

/**
 * 输入安全过滤
 * 使用:在接口类中添加 @InputGuardrails(SafeInputGuardrail.class)
 */
public class SafeInputGuardrail implements InputGuardrail {

    // 敏感词集合
    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();
    }
}

(2)访问Ai后过滤器

import dev.langchain4j.data.message.AiMessage;
import dev.langchain4j.guardrail.OutputGuardrail;
import dev.langchain4j.guardrail.OutputGuardrailResult;

/**
 * 输出安全过滤
 */
public class SafeOutputGuardrail implements OutputGuardrail {
    @Override
    public OutputGuardrailResult validate(AiMessage responseFromLLM) {
        return OutputGuardrail.super.validate(responseFromLLM);
    }
}

9、控制层


import cn.dev33.satoken.annotation.SaIgnore;
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.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.dromara.pxhai.apply.AiCodeHelperService;
import org.springframework.http.codec.ServerSentEvent;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import reactor.core.publisher.Flux;


/**
 * Ai测试接口
 */
@Slf4j
@Validated
@RequiredArgsConstructor
@RestController
@SaIgnore
@RequestMapping("/pxhAi/test")
public class TestChatController {

    @Resource
    private ChatModel qwenChatModel;

    @Resource
    private AiCodeHelperService aiCodeHelperService;

    /**
     * 等待全部回复后返回
     * @param msg
     * @return
     */
    @GetMapping("/one/{msg}")
    public String redirect(@PathVariable String msg){
        UserMessage userMessage = UserMessage.from(msg);
        ChatResponse chat = qwenChatModel.chat(userMessage);
        AiMessage aiMessage = chat.aiMessage();
        log.info("AI输出 : "+ aiMessage.toString());
        return aiMessage.text();
    }

    /**
     * 流式返回
     */
    @GetMapping("/stream")
    public Flux<ServerSentEvent<String>> redirectStream(int memoryId, String message){
        return aiCodeHelperService.chatSSE(memoryId, message)
            .map(chunk -> ServerSentEvent.<String>builder()
                .data(chunk)
                .build()
            );
    }


}

四、看下效果

五、目录结构

六、作者有话说

一直以来,对技术的渴望一直是作者的通病。

AI身为当前主流应用,几乎大一点的都会植入AI。

刚开始没接触以为AI对接是一个非常麻烦和反锁的事情,是要大佬才能去弄。

但是当你真正去了解了,才发现!哎哟,无非还是一堆AI问话,就是加了一些约束条件。

所谓的MCP协议也就是一个大家约定俗成的东西,只要按照一定的访问方式就可以了。

最后AI搞到手,就可以去实现自己心里的小想法啦!

冲冲冲!家人们!

评论 1
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

攀小黑

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值