本文总结了 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 改进。

Logo

欢迎加入 MCP 技术社区!与志同道合者携手前行,一同解锁 MCP 技术的无限可能!

更多推荐