【langchain4j】Springboot如何接入大模型以及实战开发-AI问答助手(一)

langchain4j介绍

官网地址:https://docs.langchain4j.dev/get-started

langchain4j可以说是java和spring的关系,spring让我们开发java应用非常简单,那么langchain4j对应的就是java开发ai的 “Spring”
他集成了AI应用的多种场景,并且抽象多种接口,让我们开发AI应用非常简单,下面介绍其常用功能,以及开发一个小的ai问答应用

AI应用的实现需求:支持对话、上下文对话、流式对话、对话数据隔离、对话数据持久化、Function Call实现特殊场景结合业务进行ai问答

为了降低模型的使用门槛,这里开发使用国内模型-阿里千问系列进行开发,登录去控制台获取key就行
https://bailian.console.aliyun.com/

项目依赖引入

依赖需要引入两个东西:langchain4j的依赖、对应的模型的依赖,但是如果通过starter的形式,只需要引入starter即可,具体可以看官方的文档
https://docs.langchain4j.dev/integrations/language-models/dashscope
注意:需要使用jdk17环境,SpringBoot3.x系列

<?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.xujie</groupId>
    <artifactId>langchain4j-demo</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>
        <spring.boot.version>3.2.4</spring.boot.version>
        <langchain4j.version>1.0.0-beta3</langchain4j.version>

    </properties>

    <dependencyManagement>
        <dependencies>
            <!-- Spring Boot Starter父依赖 -->
            <dependency>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-starter-parent</artifactId>
                <version>${spring.boot.version}</version>
                <type>pom</type>
                <scope>import</scope>
            </dependency>
            <dependency>
                <groupId>dev.langchain4j</groupId>
                <artifactId>langchain4j-community-bom</artifactId>
                <version>${langchain4j.version}</version>
                <type>pom</type>
                <scope>import</scope>
            </dependency>
        </dependencies>
    </dependencyManagement>

    <dependencies>

        <dependency>
            <groupId>dev.langchain4j</groupId>
            <artifactId>langchain4j-community-dashscope-spring-boot-starter</artifactId>
        </dependency>
        <!-- Langchain4j自己的核心库 -->
        <dependency>
            <groupId>dev.langchain4j</groupId>
            <artifactId>langchain4j</artifactId>
            <version>${langchain4j.version}</version>
        </dependency>
        <!-- Web依赖,以及Webflux依赖,实现流式响应 -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-webflux</artifactId>
        </dependency>

        <!-- Redis的依赖,用于消息持久化 -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-redis</artifactId>
        </dependency>
        <dependency>
            <groupId>org.apache.commons</groupId>
            <artifactId>commons-pool2</artifactId>
        </dependency>

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
        </dependency>
    </dependencies>
</project>

功能体验

首先我们先逐个体验其功能,最后再统一集成

AI对话

Langchain4j将AI的功能封装成一个Model,通过Model即可调用对应的AI功能,我们要使用千问的模型,new 一个千问的Model即可
这里有两种方式,一种是通过SpringBoot的配置文件,将模型注入到IOC容器中,另一种就是通过代码进行配置

ChatLanguageModel qwenModel = QwenChatModel.builder()
                    .apiKey("You API key here")
                    .modelName("qwen-max")
                    .build();

官方示例:

langchain4j.community.dashscope.api-key=<You API Key here>
langchain4j.community.dashscope.model-name=qwen-max
# The properties are the same as `QwenChatModel`
# e.g.
# langchain4j.community.dashscope.temperature=0.7
# langchain4j.community.dashscope.max-tokens=4096
langchain4j:
  community:
    dashscope:
      chat-model:
        api-key: ${Ali_AI_KEY} //通过环境参数
        model-name: qwen-max

这里我们就使用注入的方式

/**
 * @author Xujie
 * @since 2025/4/19 21:44
 **/
@Slf4j
@SpringBootTest
public class TestAi {
    @Resource
    private ChatLanguageModel qwenChatModel;

    @Test
    public void test() {
        String response = qwenChatModel.chat("你好呀");
        log.info(response);
    }
}

控制台输出:

2025-04-19T21:47:29.593+08:00  INFO 12660 --- [           main] com.xujie.TestAi                         : 你好!有什么可以帮助你的吗?

这里说明成功了

文生图

我们再来体验一下文生图的功能,这里用的阿里 wanx2.1-t2i-turbo 模型,大家也可以去阿里的模型广场看看有哪些支持图片生成的模型,调用即可,这里我们就通过手动构造的模式去构成图片模型

@Value("${langchain4j.community.dashscope.chat-model.api-key}")
    private String apiKey;

 @Test
    public void testGeneratePicture() {
        WanxImageModel.WanxImageModelBuilder builder = WanxImageModel.builder();
        WanxImageModel wanxImageModel = builder.apiKey(apiKey)
                .modelName("wanx2.1-t2i-turbo")
                .build();
        Response<Image> imageResponse = wanxImageModel.generate("生成一只剃了毛的黑色拉布拉多");
        log.info(imageResponse.toString());
    }

Java HotSpot(TM) 64-Bit Server VM warning: Sharing is only supported for boot loader classes because bootstrap classpath has been appended
2025-04-19T21:54:42.892+08:00  INFO 31912 --- [           main] com.xujie.TestAi                         : Response { content = Image { url = "https://dashscope-result-wlcb-acdr-1.oss-cn-wulanchabu-acdr-1.aliyuncs.com/1d/e3/20250419/b0fe3396/15a89e1c-f792-4156-bff2-5d2ccb80561e3378775189.png?Expires=1745157281&OSSAccessKeyId=LTAI5tKPD3TMqf2Lna1fASuh&Signature=m3QvlCGR4aGwVyI7Vj1IFnVY95Y%3D", base64Data = null, mimeType = null, revisedPrompt = "写实宠物摄影,一只黑色拉布拉多犬在户外草地上。它全身毛发被修剪得非常短,露出光滑的黑皮肤。狗狗四肢强健,肌肉线条明显,正抬头望向镜头,眼神机警灵动。背景是大片绿色草地和蓝天白云,阳光洒在狗身上形成自然光影效果。高清彩色摄影,近景侧面捕捉狗狗优雅姿态。" }, tokenUsage = null, finishReason = null, metadata = {} }

可以看见,这里也是成功的生成图片了,效果一般,也有可能是我Promot的问题

在这里插入图片描述

文生语音

同样,我们去模型广场看看哪些支持生成语音的模型,这里就使用cosyvoice-v1模型

@Test
    public void testGenerateVoice() {
        SpeechSynthesisParam param = SpeechSynthesisParam.builder()
                .apiKey(apiKey)
                .voice("longxiaochun")
                .model("cosyvoice-v1")
                .build();
        SpeechSynthesizer speechSynthesizer = new SpeechSynthesizer(param,null);
        ByteBuffer call = speechSynthesizer.call("你好,给我唱一首生日快乐歌");
        // Maven项目标准资源目录
        File file = new File("src/main/resources/response.mp3");
        // 确保目录存在
        file.getParentFile().mkdirs();
        try(FileOutputStream fileOutputStream = new FileOutputStream(file)) {
            fileOutputStream.write(call.array());
        } catch (Exception e) {
            throw new RuntimeException(e);
        }
    }

然语音文件的路径实在resources下面,也是成功了
在这里插入图片描述

对话上下文

首先,我们理解一下什么是对话上下文,我们要清楚,AI的服务是不会记录我们对话的记录,我们每一次请求都是独立的返回,不会关联我们之前的问题,那么我就要实现上下文,就只能将之前的对话记录一起加上这一次的提问,一起请求给AI,这才实现了上下文的功能
我们进行验证一下,AI是否会自动记录上下文:
我们简单改造一下第一个用例

  @Test
    public void test() {
        String response1 = qwenChatModel.chat("你好呀,我是XJ");
        String response2 = qwenChatModel.chat("我是什么名字");
        log.info(response1);
        log.info(response2);
    }
Java HotSpot(TM) 64-Bit Server VM warning: Sharing is only supported for boot loader classes because bootstrap classpath has been appended
2025-04-19T22:14:23.835+08:00  INFO 38908 --- [           main] com.xujie.TestAi                         : 你好,XJ!很高兴认识你。我是Qwen,由阿里云开发的超大规模语言模型。我在这里可以帮助你解答问题、提供信息或者进行各种话题的交流。有什么我可以帮到你的吗?
2025-04-19T22:14:23.835+08:00  INFO 38908 --- [           main] com.xujie.TestAi                         : 您好!您并没有告诉我您的名字,所以我无法直接知道您的名字是什么。如果您愿意分享的话,可以告诉我您的名字或者您想让我怎么称呼您。

可以看到,两次回复都是毫无关联的;
既然这样我们如何实现上下文呢,那最简单暴力的方式就是将前面的请求和响应一起发给AI,如下

    @Test
    public void test() {
        UserMessage userMessage1 = new UserMessage("你好呀,我是XJ");
        ChatResponse chatResponse1 = qwenChatModel.chat(userMessage1);
        AiMessage aiMessage = chatResponse1.aiMessage();// 拿到ai的响应

        UserMessage userMessage2 = new UserMessage("我是什么名字");
        ChatResponse chatResponse2 = qwenChatModel.chat(userMessage1,aiMessage,userMessage2);


        log.info(chatResponse1.aiMessage().text());
        log.info(chatResponse2.aiMessage().text());
    }

两次相应如下

Java HotSpot(TM) 64-Bit Server VM warning: Sharing is only supported for boot loader classes because bootstrap classpath has been appended
2025-04-19T22:18:23.274+08:00  INFO 40264 --- [           main] com.xujie.TestAi                         : 你好,XJ!很高兴认识你。有什么我可以帮助你的吗?或者你想聊些什么?
2025-04-19T22:18:23.274+08:00  INFO 40264 --- [           main] com.xujie.TestAi                         : 你刚才告诉我你的名字是XJ。如果你有其他的名字或者昵称,也可以告诉我哦!

可以发现,AI对话已经具有的上下文的功能,但是这样是不是太复杂?
没当发现一个东西复杂的时候,总会有对应的不复杂的情况,如果没有,那就自己造,如果要我们自己实现,那其实也是很简单,将请求AI的接口封装一下,并且内部维护对话记录,每次请求,将历史的记录携带即可。

但是框架Langchain4j的已经帮我们实现了,具体是采用动态代理的模式实现的
在这里插入图片描述


    public interface AiChat{
        String chat(String text);
        TokenStream tokenStream(String text);
    }

    @Test
    public void test() {
        AiChat aiChat = AiServices.builder(AiChat.class)
                .chatLanguageModel(qwenChatModel)
                .chatMemory(MessageWindowChatMemory.withMaxMessages(10))//限制10上下文
                .build();
        String response1 = aiChat.chat("我的名字是小徐");
        String response2 = aiChat.chat("我的名字是什么");
        log.info(response1);
        log.info(response2);
    }

Java HotSpot(TM) 64-Bit Server VM warning: Sharing is only supported for boot loader classes because bootstrap classpath has been appended
2025-04-19T22:30:34.652+08:00  INFO 32428 --- [           main] com.xujie.TestAi                         : 你好,小徐!很高兴认识你。有什么我可以帮助你的吗?
2025-04-19T22:30:34.652+08:00  INFO 32428 --- [           main] com.xujie.TestAi                         : 你的名字是小徐。如果你还有其他问题或需要帮助,随时告诉我哦!

通过返回可以看的,确实是具有的上下文的能力

回话隔离

我们与ai进行回话,通常是具有对此回话的,每一次的回话主题都不一样,需要进行隔离,那我们来看看Langchain4j如何实现的回话隔离,上面我们实现了回话的上下文,跟最初一样,我们理解一下如何实现回话隔离呢?
那肯定是通过什么ID或者其他的唯一标识来区分
而且,也确实是这样实现的
我们只需要每一次对话传入ID即可
相比上面加了一个注解以及id字段,这个注解是Langchain4j提供的,在代理中通过注解去拿到对应的值

   public interface AiChat{
        String chat(@MemoryId Long id, @UserMessage String text);
    }

    @Test
    public void test() {
        AiChat aiChat = AiServices.builder(AiChat.class)
                .chatLanguageModel(qwenChatModel)
                .chatMemoryProvider(memoryId -> MessageWindowChatMemory.builder()
                        .maxMessages(10)
                        .id(memoryId)
                        .build())
                .build();

        String response1 = aiChat.chat(1L,"我的名字是小徐");
        String response2 = aiChat.chat(1L,"我的名字是什么");
        log.info(response1);
        log.info(response2);

        log.info("======");
        response2 = aiChat.chat(2L,"我的名字是什么");

        log.info(response2);
    }

2025-04-19T22:39:18.264+08:00  INFO 40528 --- [           main] com.xujie.TestAi                         : 你好,小徐!很高兴认识你。有什么我可以帮助你的吗?
2025-04-19T22:39:18.264+08:00  INFO 40528 --- [           main] com.xujie.TestAi                         : 你的名字是小徐。如果有其他问题或需要帮助的地方,随时告诉我哦!
2025-04-19T22:39:18.264+08:00  INFO 40528 --- [           main] com.xujie.TestAi                         : ======
2025-04-19T22:39:19.739+08:00  INFO 40528 --- [           main] com.xujie.TestAi                         : 您好!您并没有告诉我您的名字,所以我无法知道您的名字是什么。如果您愿意,可以告诉我您的名字,我很乐意用您的名字来称呼您。

可以看的两个回话,确实是隔离了,第二个回话并不清楚我第一个回话的内容

Function Call

Function Call 就是我们提前预设一些场景,然后用户在进行AI问答时,如果ai觉得当前对话符合某一个场景,便会去调用预设的函数,获取函数的返回值,然后结合用户的提问,去回答用户
比如:我数据库中存储今天的香蕉价格是10元一斤
并且预设场景:水果的价格
那么用户提问:今天的香蕉多少钱一斤
那么就会提取到水果的名称:香蕉,去数据库查询香蕉的价格,为10元,然后结合用户的提问,进行回答

 @Test
    public void testFuncCall() {
        class FuncCallService{
            final Map<String,Integer> map = Map.of("香蕉",10,"苹果",12);
            @Tool("水果的价格")
            public Integer fruitsPrice(@P("水果名字") String fruitName) {
                return map.getOrDefault(fruitName,-1);
            }
        }
        AiChat aiChat = AiServices.builder(AiChat.class)
                .chatLanguageModel(qwenChatModel)
                .tools(new FuncCallService())
                .chatMemoryProvider(memoryId -> MessageWindowChatMemory.builder()
                        .maxMessages(10)
                        .id(memoryId)
                        .build())
                .build();
        String response1 = aiChat.chat(1L, "香蕉多少钱一斤");
        String response2 = aiChat.chat(2L, "苹果多少钱一斤");
        String response3 = aiChat.chat(3L, "栗子多少钱一斤");

        log.info(response1);
        log.info(response2);
        log.info(response3);
    }
2025-04-19T22:49:48.799+08:00  INFO 40704 --- [           main] com.xujie.TestAi                         : 香蕉的价格是10元一斤。
2025-04-19T22:49:48.799+08:00  INFO 40704 --- [           main] com.xujie.TestAi                         : 苹果的价格是12元一斤。
2025-04-19T22:49:48.799+08:00  INFO 40704 --- [           main] com.xujie.TestAi                         : 对不起,当前的查询工具中没有栗子的价格信息。我建议您可以去附近的市场或者在线购物网站上查看最新的价格。如果您需要查询其他水果的价格,也可以告诉我,我会尽力帮您查询。

可以看到,确实是结合我们预设的内容去执行了Func Call

综上,结合Langchain4j确实可以很方便引入AI的功能,让我们的应用具有AI的功能

项目实战放在下一篇文章中

### 集成LangChain4J与Spring Boot以提供AI服务 为了在Spring Boot项目中集成LangChain4J来提供AI服务,可以遵循以下方法: #### 1. 添加依赖项 首先,在`pom.xml`文件中加入LangChain4J库的相关依赖。这步骤确保了开发环境能够识别并利用LangChain4J所提供的功能。 ```xml <dependency> <groupId>com.langchain4j</groupId> <artifactId>langchain4j-core</artifactId> <version>${latest.version}</version> </dependency> ``` 此操作允许开发者通过Maven管理工具轻松引入所需的类库[^1]。 #### 2. 创建配置类 接着创建个新的Java配置类用于初始化LangChain4J组件和服务实例。此类通常会标注有`@Configuration`注解,并可能包含些Bean定义以便于后续调用。 ```java @Configuration public class LangChainConfig { @Bean public ChainService chainService() { return new DefaultChainService(); } } ``` 这段代码展示了如何声明个名为`chainService()`的方法返回类型为`ChainService`的对象作为Spring容器中的bean。 #### 3. 构建控制器接口 随后构建RESTful API端点供外部访问这些由LangChain4J支持的服务逻辑。这里展示了个简单的例子——处理密码找回请求的功能实现方式。 ```java @RestController @RequestMapping("/api/langchain") public class LangChainController { private final ChainService chainService; @Autowired public LangChainController(ChainService chainService) { this.chainService = chainService; } @PostMapping("/forget-password") public ResponseEntity<Map<String, Object>> forgetPassword(@RequestBody User form, HttpServletRequest request) { try { // 调用LangChain4J业务逻辑... boolean result = chainService.forgetPassword(form.getEmail()); if (result) { return ResponseEntity.ok().body(Collections.singletonMap("message", "Email sent")); } else { throw new RuntimeException("Failed to send email"); } } catch (Exception e) { return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).build(); } } } ``` 上述示例说明了如何接收来自客户端的数据并通过注入的`chainService`对象执行具体的操作[^4]。 #### 4. 测试API 最后不要忘记编写单元测试验证新添加的功能是否按预期工作。可以通过MockMvc或其他类似的框架来进行模拟HTTP请求和响应的过程检验。 ```java @SpringBootTest class LangChainApplicationTests { @Autowired private MockMvc mockMvc; @Test void testForgetPasswordApi() throws Exception { String jsonContent = "{\"email\":\"test@example.com\"}"; MvcResult mvcResult = mockMvc.perform(post("/api/langchain/forget-password") .contentType(MediaType.APPLICATION_JSON) .content(jsonContent)) .andExpect(status().isOk()) .andReturn(); assertEquals("{\"message\":\"Email sent\"}", mvcResult.getResponse().getContentAsString()); } } ``` 以上就是将LangChain4J成功嵌入到基于Spring Boot的应用程序内的基本流程介绍。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

小徐Chao努力

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

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

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

打赏作者

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

抵扣说明:

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

余额充值