本文总结了 Faasil Mman(来自 embarX)关于使用 Spring AI 模块将 AI 无缝集成到 Spring Boot 应用程序中的全面、实践课程。本课程超越了理论,专注于构建利用机器学习和自然语言处理 (NLP) 以及 OpenAI API 的真实应用程序。
为什么选择 AI,为什么选择 Spring AI?
近年来,AI 的快速发展使得开发人员了解如何将 AI 功能整合到他们的应用程序中变得越来越重要。许多组织,包括谷歌等科技巨头,都在其产品中利用 AI。Spring AI 是 Spring 生态系统中的一个项目,它简化了这种集成,允许开发人员在没有不必要复杂性的情况下添加 AI 功能。
课程概述和项目
本课程完全是实践性的,通过项目构建强调实际应用。涵盖两个主要项目:
-
多功能应用程序: 这个全栈项目(Spring Boot 后端,React 前端)包括:
- 图库生成器: 用户可以输入文本描述,应用程序使用 AI 生成图库照片。
- 问答机器人: 一个简单的聊天机器人界面,用于提问并接收 AI 生成的答案。
- 食谱生成器: 用户输入食材、菜系偏好和饮食限制,应用程序会生成食谱。
-
音频转文本转录器: 一个单独的应用程序,允许用户上传音频文件并接收文本转录。
关键概念和技术
- 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 构建。关键步骤包括:
- 项目设置: 使用
start.spring.io
(Spring Initializr) 创建一个新的 Spring Boot 项目。包括spring-boot-starter-web
和spring-ai-openai-spring-boot-starter
依赖项。 - API 密钥配置: 从 OpenAI (platform.openai.com) 获取 API 密钥。将此密钥安全地存储在
application.properties
文件中:
您还可以在此文件中配置其他选项,例如 OpenAI API 的基本 URL。spring.ai.openai.api-key=YOUR_API_KEY
- 创建服务: 创建服务类(例如,
ChatService
、ImageService
、RecipeService
)来封装与 OpenAI API 交互的逻辑。这些服务将使用 Spring AI 提供的ChatClient
和ImageClient
接口。 - 创建控制器: 创建控制器类(例如,
GenAIController
)来处理来自前端的传入 HTTP 请求。这些控制器将公开 REST 端点(例如,/askAI
、/generateImage
、/recipeCreator
)。他们将使用服务类与 OpenAI API 交互。 - CORS 配置: 创建一个实现
WebMvcConfigurer
的WebConfig
类来启用 CORS。这允许 React 前端(在不同的端口上运行)向 Spring Boot 后端发出请求。 - 使用提示模板来提高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 构建。关键步骤:
- 项目设置: 使用
npx create-react-app my-app
(或类似的工具,如 Vite)创建一个新的 React 项目。 - 安装 Axios(可选):
npm install axios
(如果您选择使用 Axios 进行 HTTP 请求)。 - 创建组件: 为每个功能创建单独的组件(例如,
ImageGenerator.js
、ChatComponent.js
、RecipeGenerator.js
)。 - 管理状态: 使用
useState
钩子来管理每个组件的状态(例如,用户输入、生成的图像 URL、转录的文本)。 - 处理用户输入: 使用事件处理程序(例如,输入字段的
onChange
、按钮的onClick
)来更新状态并触发 API 调用。 - 进行 API 调用: 使用
fetch
API 或 Axios 向 Spring Boot 后端端点发出 HTTP 请求。 - 渲染 UI: 在适当的组件中显示来自 API 调用的结果。
- 条件渲染: 使用条件渲染根据所选选项卡显示不同的组件。
示例: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 改进。