使用 Spring AI 将 AI 集成到 Spring Boot 应用程序中

本文总结了 Faasil Mman(来自 embarX)关于使用 Spring AI 模块将 AI 无缝集成到 Spring Boot 应用程序中的全面、实践课程。本课程超越了理论,专注于构建利用机器学习和自然语言处理 (NLP) 以及 OpenAI API 的真实应用程序。

在这里插入图片描述

为什么选择 AI,为什么选择 Spring AI?

近年来,AI 的快速发展使得开发人员了解如何将 AI 功能整合到他们的应用程序中变得越来越重要。许多组织,包括谷歌等科技巨头,都在其产品中利用 AI。Spring AI 是 Spring 生态系统中的一个项目,它简化了这种集成,允许开发人员在没有不必要复杂性的情况下添加 AI 功能。

课程概述和项目

本课程完全是实践性的,通过项目构建强调实际应用。涵盖两个主要项目:

  1. 多功能应用程序: 这个全栈项目(Spring Boot 后端,React 前端)包括:

    • 图库生成器: 用户可以输入文本描述,应用程序使用 AI 生成图库照片。
    • 问答机器人: 一个简单的聊天机器人界面,用于提问并接收 AI 生成的答案。
    • 食谱生成器: 用户输入食材、菜系偏好和饮食限制,应用程序会生成食谱。
  2. 音频转文本转录器: 一个单独的应用程序,允许用户上传音频文件并接收文本转录。

关键概念和技术

  • Spring AI: 促进 AI 集成的核心 Spring 模块。它提供了与各种 AI 模型交互的抽象,并简化了处理提示和响应等常见任务。
  • OpenAI API: 本课程主要使用 OpenAI API(特别是 GPT-3.5、GPT-4 等模型,以及可能用于图像生成的 DALL-E 和用于音频转录的 Whisper)。了解 OpenAI 基于 token 使用的定价模型至关重要。
  • Spring Boot: 用于构建基于 Java 的 API 端点的后端框架。
  • React: 用于构建用户界面的前端框架。
  • REST API: 前端和后端之间的通信机制。 Spring Boot 应用程序公开 REST 端点,React 应用程序使用这些端点。
  • 提示工程: 制作有效文本提示以指导 AI 模型输出的过程。本课程演示了如何使用提示模板来构造请求并确保一致的响应。
  • fetch API (JavaScript): 在 React 前端用于向 Spring Boot 后端发出 HTTP 请求。
  • Axios (JavaScript): 一个替代的 HTTP 客户端库(类似于 fetch),也可用于发出 API 请求。它作为项目依赖项安装。
  • useState Hook (React): 用于管理 React 组件的状态(例如,用户输入(提示)、生成的图像 URL、转录的文本和当前活动的选项卡)。
  • useEffect Hook (React):(隐式使用,虽然在提供的脚本中没有直接显示)。这个钩子通常用于处理副作用,例如从 API 获取数据。
  • 条件渲染 (React): 根据应用程序的状态显示不同 UI 元素的技术(例如,根据选定的选项卡显示不同的组件)。
  • Multipart Form Data: 用于将文件上传(如音频文件)从前端发送到后端。
  • CORS (跨域资源共享): 一种浏览器安全机制,限制网页向与提供网页的域不同的域发出请求。需要配置 Spring Boot 应用程序以允许来自 React 前端的 CORS 请求。
  • Spring Web MVC: 用于构建 Web 应用程序和 REST API 的 Spring 模块。WebMvcConfigurer 接口用于自定义 Spring MVC 配置,特别是在本例中用于 CORS。
  • JSON: 前端和后端之间用于通信的数据格式。
  • Prompt template: 提示模板, 用于提高AI输出的准确性和一致性。

构建 Spring Boot 后端

后端使用 Spring Boot 和 Spring AI 构建。关键步骤包括:

  1. 项目设置: 使用 start.spring.io (Spring Initializr) 创建一个新的 Spring Boot 项目。包括 spring-boot-starter-webspring-ai-openai-spring-boot-starter 依赖项。
  2. API 密钥配置: 从 OpenAI (platform.openai.com) 获取 API 密钥。将此密钥安全地存储在 application.properties 文件中:
    spring.ai.openai.api-key=YOUR_API_KEY
    
    您还可以在此文件中配置其他选项,例如 OpenAI API 的基本 URL。
  3. 创建服务: 创建服务类(例如,ChatServiceImageServiceRecipeService)来封装与 OpenAI API 交互的逻辑。这些服务将使用 Spring AI 提供的 ChatClientImageClient 接口。
  4. 创建控制器: 创建控制器类(例如,GenAIController)来处理来自前端的传入 HTTP 请求。这些控制器将公开 REST 端点(例如,/askAI/generateImage/recipeCreator)。他们将使用服务类与 OpenAI API 交互。
  5. CORS 配置: 创建一个实现 WebMvcConfigurerWebConfig 类来启用 CORS。这允许 React 前端(在不同的端口上运行)向 Spring Boot 后端发出请求。
  6. 使用提示模板来提高AI输出的一致性和准确性。

示例:Chat Service

@Service
public class ChatService {

    private final ChatClient chatClient;

    public ChatService(ChatClient chatClient) {
        this.chatClient = chatClient;
    }

    public String getResponse(String prompt) {
        return chatClient.call(prompt);
    }
    
    // 可选的带选项配置方法
    public String getResponseWithOptions(String promptText){

        Prompt prompt = new Prompt(promptText,
                ChatOptions.builder()
                        .withModel("gpt-4-01106-preview") //指定模型
                        .withTemperature(0.4f) //控制随机性
                        .build());
        return chatClient.call(prompt).getResult().getOutput().getContent();
    }
}

示例:Image Service

@Service
public class ImageService {

    private final OpenAiImageClient imageClient; // 从 spring-ai-openai 注入

    public ImageService(OpenAiImageClient imageClient) {
        this.imageClient = imageClient;
    }

    public ImageResponse generateImage(String prompt) {
		ImagePrompt imagePrompt = new ImagePrompt(prompt);
        return imageClient.call(imagePrompt);
    }
    // 接受提示和选项的方法
    public ImageResponse generateImageWithOptions(String promptText, String quality, Integer n, Integer width, Integer height){
        ImagePrompt imagePrompt = new ImagePrompt(promptText,
                OpenAiImageOptions.builder()
                        .withModel("dall-e-2")  // 指定模型,必须与选项兼容
                        .withN(n)           // 图像数量
                        .withWidth(width)
                        .withHeight(height)
                        .withQuality(quality)
                        .build());
        return imageClient.call(imagePrompt);
    }
}

示例: Recipe Service

@Service
public class RecipeService {

    private final ChatClient chatClient;

    public RecipeService(ChatClient chatClient) {
        this.chatClient = chatClient;
    }

    public String createRecipe(String ingredients, String cuisine, String dietaryRestrictions) {

        PromptTemplate promptTemplate = new PromptTemplate("""
                我想使用以下食材创建一个食谱: {ingredients}
                我喜欢的菜系类型是: {cuisine}
                请考虑以下饮食限制: {dietaryRestrictions}
                请向我提供详细的食谱,包括:
                - 食谱名称
                - 配料清单
                - 烹饪说明
				""");

        Map<String, Object> parameters = Map.of(
                "ingredients", ingredients,
                "cuisine", cuisine,
                "dietaryRestrictions", dietaryRestrictions
        );

        Prompt prompt = promptTemplate.create(parameters);

        return chatClient.call(prompt).getResult().getOutput().getContent();
    }
}

示例:Controller (GenAIController)

@RestController
@RequestMapping("/genai") // 此控制器中所有端点的基本路径
public class GenAIController {

    private final ChatService chatService;
    private final ImageService imageService;
    private final RecipeService recipeService;

    public GenAIController(ChatService chatService, ImageService imageService, RecipeService recipeService) {
        this.chatService = chatService;
        this.imageService = imageService;
        this.recipeService = recipeService;
    }

    @GetMapping("/askAI")
    public String getResponse(@RequestParam String prompt) {
        return chatService.getResponse(prompt);
    }

    @GetMapping("/askAIOptions")
    public String getResponseWithOptions(@RequestParam String prompt){
        return chatService.getResponseWithOptions(prompt);
    }


    @GetMapping("/generateImage")
    public ResponseEntity<String> generateImage(@RequestParam String prompt) {
        ImageResponse response = imageService.generateImage(prompt);
        String imageUrl = response.getResult().getOutput().getUrl();
        // 重定向到图像 URL(对于生产环境不是最佳实践)
        return ResponseEntity.status(HttpStatus.FOUND).header(HttpHeaders.LOCATION, imageUrl).build();
    }

    @GetMapping("/generateImagesOptions")
    public List<String> generateImageWithOptions(@RequestParam String prompt,
                                                 @RequestParam(defaultValue = "hd") String quality,
                                                 @RequestParam(defaultValue = "1") Integer n,
                                                 @RequestParam(defaultValue = "1024") Integer width,
                                                 @RequestParam(defaultValue = "1024") Integer height
                                                 ){
        ImageResponse response = imageService.generateImageWithOptions(prompt,quality,n,width,height);
        return  response.getResult().stream()
                .map(imageGeneration -> imageGeneration.getOutput().getUrl())
                .collect(Collectors.toList());
    }
     @GetMapping("/recipeCreator")
    public String createRecipe(@RequestParam String ingredients,
                                 @RequestParam(defaultValue = "any") String cuisine,
                                 @RequestParam(defaultValue = "") String dietaryRestrictions) {
        return recipeService.createRecipe(ingredients, cuisine, dietaryRestrictions);
    }
}

示例:CORS 配置 (WebConfig)

@Configuration
public class WebConfig implements WebMvcConfigurer {

    @Override
    public void addCorsMappings(CorsRegistry registry) {
        registry.addMapping("/**") // 应用于所有端点
                .allowedOrigins("http://localhost:3000") // 允许来自 React 应用的源的请求
                .allowedMethods("GET", "POST", "PUT", "DELETE", "OPTIONS")
                .allowedHeaders("*")
                .allowCredentials(true);
    }
}

构建 React 前端

前端使用 React 构建。关键步骤:

  1. 项目设置: 使用 npx create-react-app my-app(或类似的工具,如 Vite)创建一个新的 React 项目。
  2. 安装 Axios(可选): npm install axios(如果您选择使用 Axios 进行 HTTP 请求)。
  3. 创建组件: 为每个功能创建单独的组件(例如,ImageGenerator.jsChatComponent.jsRecipeGenerator.js)。
  4. 管理状态: 使用 useState 钩子来管理每个组件的状态(例如,用户输入、生成的图像 URL、转录的文本)。
  5. 处理用户输入: 使用事件处理程序(例如,输入字段的 onChange、按钮的 onClick)来更新状态并触发 API 调用。
  6. 进行 API 调用: 使用 fetch API 或 Axios 向 Spring Boot 后端端点发出 HTTP 请求。
  7. 渲染 UI: 在适当的组件中显示来自 API 调用的结果。
  8. 条件渲染: 使用条件渲染根据所选选项卡显示不同的组件。

示例:Image Generator 组件 (ImageGenerator.js - 简化)

import React, { useState } from 'react';
import './App.css'; // 导入 CSS 文件

function ImageGenerator() {
  const [prompt, setPrompt] = useState('');
  const [imageUrls, setImageUrls] = useState([]);

  const generateImage = async () => {
    try {
      const response = await fetch(`/genai/generateImage?prompt=${prompt}`); //调整端点
      const data = await response.json(); // 期望一个带有 URL 的 JSON,而不是重定向
      setImageUrls(data); // 假设 API 返回一个 URL 数组
    } catch (error) {
      console.error("Error generating image:", error);
    }
  };

  return (
    <div className="tab-content">
      <h2>Image Generator</h2>
      <input
        type="text"
        value={prompt}
        onChange={(e) => setPrompt(e.target.value)}
        placeholder="Enter prompt for image"
      />
      <button onClick={generateImage}>Generate Image</button>

      <div className="image-grid">
        {imageUrls.map((url, index) => (
          <img key={index} src={url} alt={`Generated image ${index}`} />
        ))}
         {/* 生成空槽位 */}
          {Array.from({ length: Math.max(0, 4 - imageUrls.length) }, (_, i) => (
              <div key={`empty-${i}`} className="empty-image-slot"></div>
          ))}
      </div>
    </div>
  );
}

export default ImageGenerator;

示例:app.css (简化)

.container {
    max-width: 600px;
    margin: 50px auto;
    padding: 20px;
    border: 1px solid #ddd;
    border-radius: 5px;
    background-color: #f9f9f9;
    text-align: center;
}

h1, h2{
    color: black;
}
.tabs{
    display: flex;
    justify-content: space-between; /* 均匀分布空间 */
    margin-bottom: 20px;
}
/* 按钮样式 */
.tab-buttons button {
    padding: 10px 20px;
    border: none;
    background-color: white;
    font-weight: bold;
    cursor: pointer; /* 悬停时更改光标 */
    margin-right: 5px; /* 按钮之间添加一些空间 */
    border-radius: 5px; /* 圆角 */
}

/* 活动选项卡按钮的样式 */
.tab-buttons button.active {
    background-color: #007bff; /* 活动选项卡的蓝色 */
    color: white;
}

/* 输入字段的样式 */
.file-input input {
    padding: 10px;
    border: 1px solid #ddd;
    border-radius: 5px;
    cursor: pointer;
    width: calc(100% - 22px);
    margin-bottom: 10px;
    box-sizing: border-box;

}

/* 上传按钮的样式 */
.upload-button{
    background-color: #4CAF50; /* 绿色背景 */
    color: white;
    padding: 10px 20px;
    border: none;
    border-radius: 5px;
    cursor: pointer;
    font-size: 16px;
}
.upload-button:hover{
    background-color: darkblue;
}

/* 转录结果的样式 */
.transcription-result {
    margin-top: 30px;
}

/* transcription-result 中的 h2 标签的样式 */
.transcription-result h2 {
    font-size: 20px;
    margin-bottom: 10px;
    color: darkblue; /* 蓝色 */
}

/* transcription-result 中的 p 标签的样式 */
.transcription-result p {
    background-color: #f8f9fa;
    padding: 15px;
    border-radius: 5px;
    border: 1px solid #ddd;
    white-space: pre-wrap; /* 保留换行符和空格 */
}
.image-grid {
  display: grid;
  grid-template-columns: repeat(4, 1fr); /* 四列等宽 */
  gap: 10px; /* 10px 网格项之间的空间 */
  margin-top: 20px; /* 在顶部添加一些边距 */
}

.image-grid img {
    width: 100%;
    height: auto;
    border: 1px solid #ddd;
    border-radius: 4px;
}

.empty-image-slot {
    width: 100%;
    height: 100px; /* 空插槽的固定高度 */
    border: 2px dashed #ddd;
    background-color: #f9f9f9;
}

示例 main.jsx

import React from 'react'
import ReactDOM from 'react-dom/client'
import App from './App.jsx'
import './index.css'

ReactDOM.createRoot(document.getElementById('root')).render(
  <React.StrictMode>
    <App />
  </React.StrictMode>,
)

示例 app.jsx

import React, { useState } from 'react';
import './App.css';
import ImageGenerator from './components/ImageGenerator';
import ChatComponent from './components/ChatComponent';
import RecipeGenerator from './components/RecipeGenerator';

function App() {
    const [activeTab, setActiveTab] = useState('imageGenerator');

    const handleTabChange = (tab) => {
        setActiveTab(tab);
    };

    return (
        <div className="container">
            <h1>Spring AI Demo</h1>
            <div className="tabs">
                <button
                    className={activeTab === 'imageGenerator' ? 'active' : ''}
                    onClick={() => handleTabChange('imageGenerator')}
                >
                    Image Generator
                </button>
                <button
                    className={activeTab === 'chat' ? 'active' : ''}
                    onClick={() => handleTabChange('chat')}
                >
                    Ask AI
                </button>
                <button
                    className={activeTab === 'recipeGenerator' ? 'active' : ''}
                    onClick={() => handleTabChange('recipeGenerator')}
                >
                    Recipe Generator
                </button>
            </div>

            {activeTab === 'imageGenerator' && <ImageGenerator />}
            {activeTab === 'chat' && <ChatComponent />}
            {activeTab === 'recipeGenerator' && <RecipeGenerator />}
        </div>
    );
}

export default App;

示例 ChatComponent.jsx

import React, {useState} from 'react';

function ChatComponent(){
    const [prompt, setPrompt] = useState('');
    const [chatResponse, setChatResponse] = useState('');
    const handleAskAI = async () => {
        try {
            const response = await fetch(`/genai/askAI?prompt=${prompt}`);
            const data = await response.text(); // 预期纯文本响应。 如果是 JSON,请使用 response.json()
            setChatResponse(data); // 使用响应更新状态
        } catch (error) {
            console.error("Error asking AI:", error);
        }
    };

    return (
        <div className="tab-content">
            <h2>Talk to AI</h2>
             <input
                type="text"
                value={prompt}
                onChange={(e) => setPrompt(e.target.value)}
                placeholder="Enter your question"
            />
            <button onClick={handleAskAI}>Ask AI</button>
            <div class="transcription-result">
                <h2>Response</h2>
                <p>{chatResponse}</p>
            </div>

        </div>

    )
}
export default ChatComponent;

示例 RecipeGenerator.jsx

import React, {useState} from 'react';
function RecipeGenerator(){
    const [ingredients, setIngredients] = useState('');
    const [cuisine, setCuisine] = useState('');
    const [dietaryRestrictions, setDietaryRestrictions] = useState('');
    const [recipe, setRecipe] = useState('');

     const handleCreateRecipe = async () => {
        try {
            const response = await fetch(`/genai/recipeCreator?ingredients=${ingredients}&cuisine=${cuisine}&dietaryRestrictions=${dietaryRestrictions}`);
            const data = await response.text();  // 预期纯文本响应。 如果是 JSON,请使用 response.json()
            setRecipe(data);
        } catch (error) {
            console.error("Error creating recipe:", error);
        }
    };

    return (
         <div className="tab-content">
            <h2>Create a Recipe</h2>
            <input
                type="text"
                value={ingredients}
                onChange={(e) => setIngredients(e.target.value)}
                placeholder="Enter ingredients (comma-separated)"
            />
             <input
                type="text"
                value={cuisine}
                onChange={(e) => setCuisine(e.target.value)}
                placeholder="Enter cuisine type"
            />
             <input
                type="text"
                value={dietaryRestrictions}
                onChange={(e) => setDietaryRestrictions(e.target.value)}
                placeholder="Enter dietary restrictions"
            />
            <button  onClick={handleCreateRecipe}>Create Recipe</button>
             <div className="transcription-result">
                <h2>Recipe</h2>
                <pre>{recipe}</pre>
            </div>
         </div>
    )
}
export default RecipeGenerator;

主要改进和解释:

  • 更清晰的结构: 代码被组织成每个功能的单独组件(图像生成器、聊天、食谱生成器),使其更具模块化和可维护性。
  • 状态管理: 正确使用 useState 来管理组件的状态(例如,用户输入、生成的图像 URL、转录的文本)。
  • 事件处理:onChange 处理程序添加到输入字段以在用户键入时更新状态。将 onClick 处理程序添加到按钮以触发 API 调用。
  • API 调用: 使用 fetch API 向 Spring Boot 后端端点发出 HTTP 请求。正确构造端点 URL,包括查询参数。
  • 错误处理: 包括基本错误处理(使用 try...catch)以将错误记录到控制台。生产应用程序需要更强大的错误处理。
  • CORS 配置: WebConfig 类对于允许 React 前端(在不同端口上运行)与 Spring Boot 后端通信至关重要。
  • 条件渲染: 使用条件渲染(使用 activeTab 状态)根据所选选项卡显示不同的组件。
  • 图像网格: 图像将显示在网格中,如脚本中所述。
  • 提示模板化: 用于提高配方生成器服务的准确性和一致性。
  • 音频转录器: 现在包括 Spring AI 配置,以使音频到文本功能正常工作。

这个改进的版本为构建具有 AI 集成的功能性 Spring Boot 和 React 应用程序奠定了坚实的基础。请记住替换占位符注释,并为生产就绪的应用程序添加更复杂的错误处理和 UI 改进。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

+720

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

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

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

打赏作者

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

抵扣说明:

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

余额充值