Java 开发 AI 应用利器:Spring AI

1,255 阅读4分钟

💬 概述

随着 AI 技术的快速发展,大语言模型应用在各个行业领域相继应运而生。尽管 Python 依然是 AI 应用开发的主流语言,但如今用其他编程语言开发 AI 应用也能成为新的选项。从 2023 年的 Langchain4j 到 2024 年的 Spring AI 的出现,让开发者在 Java 应用程序中利用大型语言模型构建复杂的 AI 应用也成为可能。

本文将基于 Spring Boot + Spring AI + doubao API 开发一个简易的 AI 聊天机器人,探索 Java 开发 AI 应用的可能性。

🪴 Spring AI 介绍

Spring AI 是 Spring 官方发布的 AI 应用开发框架。通过 Spring AI 框架可以简化 AI 功能相关应用程序的开发,而降低不必要的复杂配置、架构设计等起步成本。Spring AI 从一些著名的 Python 项目,例如 LangChain 和 LlamaIndex 中汲取灵感,但并非这些项目的直接移植,该项目的成立基于这样的信念:下一波生成式 AI 应用将不仅面向 Python 开发人员,还将遍及多种编程语言。从本质上讲,Spring AI 解决了 AI 集成的基本挑战:将企业数据和应用程序编程接口(APIs)与人工智能模型相连接。

⛳ 技术选型

目前 Spring AI 最新版本为1.0.0-M,支持 Spring Boot 3.2.x and 3.3.x

  • Spring Boot 3.3.5
  • Spring AI 1.0.0-M3
  • JDK 21
  • 大语言模型:doubao-lite-128k
  • OpenAI 接口管理 & 分发系统:One API

🔮 环境准备

获取 LLM API Key

开发一个 AI 应用,首先需要选定一个接入的大型语言模型。虽然 OpenAI 是首选,不过由于在国内有魔法限制所以最好还是选择国内的,这里我用的是豆包大模型。

在豆包大模型官网(www.volcengine.com/product/dou… 注册,免费额度有50万tokens,足够个人使用了。

进入控制台,在 “在线推理” 中创建推理接入点,模型任意选择,这里我选择Doubao-lite-128k/240828。创建好之后复制保存接入点 ID

之后在 “API Key 管理” 中创建 API Key,然后复制保存API Key

部署 One API

One API 是一个 OpenAI 接口管理 & 分发系统,它通过标准的 OpenAI API 格式访问所有的大模型,开箱即用。这样我们就只需要按照 OpenAI API 格式就可以调用豆包大模型的 API 了,可以减少差异化编码和配置。

One API 可以通过 Docker Compose 本地快速部署,然后在浏览器访问:http://localhost:3000/

version: "3"
services:
  one-api:
    image: justsong/one-api
    container_name: one-api
    volumes:
      - ./data:/data
    ports:
      - "3000:3000"
    privileged: true
    environment:
      TZ: Asia/Shanghai

进入 One API 主页,在 “渠道” 中 “添加新的渠道”,或者选择一个渠道点击 “测试”,保证渠道连通性。

以下是我的渠道配置,主要关注以下配置:

  • 类型:类型选择字节跳动豆包
  • 模型:这里的模型不是doubao-lite-128k,对于豆包需要使用接入点 ID来指示所使用的模型。
  • 密钥:也就是前面获取的 LLM API Key

然后在 One API 主页的 “令牌” 中 “添加新的令牌”,模型范围选择刚才 “渠道” 中使用的模型。然后复制生成的令牌,这个令牌可以看作是API Key的代理,之后在开发中是用这个令牌作为api-key配置的。

🤖 聊天机器人

准备工作完毕,开始进入 AI 应用开发,本次我们将实现一个聊天机器人。

开发依赖

首先我们的 AI 应用基于 RESTful API 调用,其次我们会通过标准的 OpenAI API 格式访问豆包大模型,这是由 One API 实现的。

创建一个新项目,使用 Spring AI 需要 Spring Boot 3.2.x 及以上版本,然后在依赖中添加 Spring Web,Open AI。

查看所引入的依赖,如果使用阿里云镜像可能无法拉取,需要先临时注释掉阿里云镜像配置。

<parent>
  <groupId>org.springframework.boot</groupId>
  <artifactId>spring-boot-starter-parent</artifactId>
  <version>3.3.5</version>
  <relativePath/> <!-- lookup parent from repository -->
</parent>

<properties>
  <spring-ai.version>1.0.0-M3</spring-ai.version>
</properties>
<dependencies>
  <dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-web</artifactId>
  </dependency>
  <dependency>
    <groupId>org.springframework.ai</groupId>
    <artifactId>spring-ai-openai-spring-boot-starter</artifactId>
  </dependency>
</dependencies>

依赖版本管理中引入了 Spring AI BOM,用于声明给定版本 Spring AI 所使用依赖项的推荐版本。

<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>

属性配置

打开application.yml,这里关注以下配置:

  • spring.ai.openai.api-key:这里填写在 One API 中所创建的令牌
  • spring.ai.openai.base-url:One API 访问地址
  • spring.ai.openai.chat.options.model:填写接入点 ID,默认为gpt-4o
server:
  port: 8080

spring:
  application:
    name: aichat

  ai:
    openai:
      api-key: API_KEY
      base-url: http://localhost:3000
      chat:
        options:
          model: ep-2024******-z5csm

简单对话

一切准备就绪,开始编码。

编写aichat/controller/AIChatController.java

我们直接注入使用 Spring AI 帮我们自动装配的OpenAiChatModel,然后写一个简单的 API,实现对话。

@RestController
public class AIChatController {

    /* spring-ai 自动装配的,直接注入使用 */
    @Resource
    private OpenAiChatModel openAiChatModel;

    /**
     * 调用 OpenAI 的接口
     * @param message 用户提问
     * @return
     */
    @RequestMapping("/ai/chat")
    public String chat(@RequestParam(value = "message", defaultValue = "来个开场白吧?") String message) {
        String call = openAiChatModel.call(message);
        return call;
    }
}

调用http://localhost:8080/ai/chat,可以看到 AI 成功响应了✨。

使用 Prompt

接着我们把代码改造一下,在模型调用方法中传入Prompt,这样我们就可以得到 JSON 结构的响应数据。其中result.output.content就是 AI 的回应内容。此外result.metadata.finishReason是我们程序判断是否回应完成的依据。

@RequestMapping("/ai/chat")
public ChatResponse chat(@RequestParam(value = "message", defaultValue = "来个开场白吧?") String message) {
    ChatResponse chatResponse = openAiChatModel.call(new Prompt(message));
    return chatResponse;
}

流式调用

前面的对话示例中,是要等待所有文本完成后才一次性返回。如果文本很长就要等很久,这对用户体验来说并不好,所以我们接着改造代码用流式输出实时响应。

@RequestMapping(value = "/ai/chat", produces = MediaType.TEXT_EVENT_STREAM_VALUE)
public Flux<String> chat(@RequestParam(value = "message", defaultValue = "来个开场白吧?") String message) {
    Flux<ChatResponse> responseFlux = openAiChatModel.stream(new Prompt(message));
    return responseFlux.map(e -> e.getResult().getOutput().getContent());
}

在浏览器中发起调用,然后可以查看到请求的事件流数据,这样就完成了流式调用。

image.png

Web AI 聊天

最后我们完善一下我们的 AI 程序实现一个 Web AI 聊天,让它看起来更像平时用的 AI 聊天应用。

Web AI 聊天界面中只需要对话消息列表区域和一个发送消息区域即可。

<template>
  <div class="chat-container">
    <div class="chat-messages" ref="chatMessages">
      <!-- 这里将显示对话消息 -->
      <div class="message" v-for="(message, index) in messages" :key="index" :class="['message', message.sender === 'You'? 'user-message' : 'ai-message']">
        <p>{{ message.text }}</p>
        <small>{{ message.sender }}</small>
      </div>
    </div>
    <div class="chat-input">
      <input type="text" v-model="newMessage" @keyup.enter="sendMessage" placeholder="Type your message...">
      <button @click="sendMessage">Send</button>
    </div>
  </div>
</template>

逻辑实现中,主要是使用EventSource处理 SSE 流。每当接收到一段消息后触发onmessage,首先判断响应数据是否完成,也就是前面提到的result.metadata.finishReason,然后把消息渲染到消息列表区域,同时为了让消息看起来像打字机一样一点一点输出的,可以将利用响应式数据拼接分段消息实现。

<script setup lang="ts">
import { ref } from 'vue';

const messages = ref([
  { text: '你好,我是AI助手。有什么我可以帮助你的吗?', sender: 'AI' },
  // 可以在这里添加更多的初始消息
]);

const newMessage = ref('');

const sendMessage = () => {
  if (newMessage.value.trim()!== '') {
    messages.value.push({ text: newMessage.value, sender: 'You' });

    // 使用 EventSource 处理 SSE 流
    const eventSource = new EventSource(`http://localhost:8080/ai/chat/stream?message=${newMessage.value}`);
    newMessage.value = '';

    let currentMessage = '';
    eventSource.onmessage = (event) => {
      const data = JSON.parse(event.data);
      if (data.result.metadata.finishReason === 'STOP') {
        eventSource.close();
        return;
      }

      if (currentMessage === '') {
        currentMessage += data.result.output.content;
        messages.value.push({ text: currentMessage, sender: 'AI' });
      } else {
        currentMessage += data.result.output.content;
        messages.value[messages.value.length - 1].text = currentMessage
      }
    };

    eventSource.onerror = (error) => {
      console.error('Error:', error);
    };
  }
};
</script>

Java 代码如下,在之前流式调用的接口中修改响应类型为Flux<ChatResponse>

@RequestMapping(value = "/ai/chat/stream", produces = MediaType.TEXT_EVENT_STREAM_VALUE)
public Flux<ChatResponse> chatStream(@RequestParam(value = "message", defaultValue = "来个开场白吧?") String message) {
    Flux<ChatResponse> responseFlux = openAiChatModel.stream(new Prompt(message));
    return responseFlux;
}

启动应用,下面是实际的效果演示。至此,一个简易的 Web AI 聊天机器就完成了。

🏆 总结

通过前面一步步地对 Spring AI 的了解,开发前的对接准备以及开发配置、实现,最终完成了一个 AI 聊天机器人。这其实也让我们看到在 Java 应用程序中利用大型语言模型构建 AI 应用也并非难事,随着 AI 技术的快速发展,我想编程语言的应用范围也会越来越广泛,甚至不了解编程语言的群体也有朝一日也能够开发出属于自己的 AI 应用!