一、引言
如果你渴望全方位掌握 Spring AI,从最初的基础搭建,到复杂场景下的精妙运用,直至成长为高手应对各类难题,这本教程将是你的不二之选。它会像一位贴心导师,手把手带你走过每一个关键阶段,开启 Spring AI 实战之旅。
Spring AI介绍:
在当今数字化时代,人工智能(AI)技术的蓬勃发展正深刻地改变着各个领域的运作模式。Spring AI 作为一款极具创新性的开发工具,应运而生,为 Java 开发者们搭建起了通往 AI 应用开发的便捷桥梁。它融合了一系列先进的技术和理念,助力开发者轻松驾驭复杂的 AI 开发场景。
Spring AI 的核心在于对多种 AI 模型的强大支持。这些模型如同智慧的引擎,能够处理和生成丰富多样的信息,从文本到图像,再到音频,无所不能。以 ChatGPT 为代表的语言模型,通过强大的预训练机制,能够实现流畅的人机对话交互;而 Midjourney 和 Stable Diffusion 等图像生成模型,则可以根据文本描述创造出令人惊叹的艺术作品。Spring AI 将这些不同类型的模型整合在一起,让开发者可以根据具体的业务需求灵活选择和运用。
在与 AI 模型交互的过程中,提示(Prompts)和提示模板(Prompt Templates)扮演着至关重要的角色。提示就像是与模型沟通的 “密码”,精心设计的提示能够引导模型产生符合预期的输出。Spring AI 借助像 StringTemplate 这样的库,实现了提示模板的灵活运用。开发者可以通过设定模板,轻松替换其中的参数,从而快速生成个性化的提示内容。这种方式不仅提高了开发效率,还使得提示的管理和维护更加便捷。
嵌入(Embeddings)技术是 Spring AI 的又一亮点。它将文本、图像等数据转化为数值向量,通过计算向量之间的距离,能够精准地衡量数据之间的相似性。在实际应用中,这一特性被广泛用于检索增强生成(RAG)场景。例如,在智能客服系统中,RAG 技术可以从大量的知识库中快速检索出与用户问题相似的内容,并将其融入到提示中,让模型给出更加准确和丰富的回答。
标记(Tokens)作为 AI 模型处理数据的基本单元,在 Spring AI 中也得到了充分的考虑。开发者需要了解模型对标记数量的限制,也就是 “上下文窗口” 的概念。Spring AI 提供了相应的工具和策略,帮助开发者合理地处理数据,确保在模型的限制范围内充分发挥其性能。同时,由于在实际使用中,标记数量与成本密切相关,Spring AI 的优化措施也有助于开发者降低使用成本。
对于 AI 模型的输出,Spring AI 致力于实现结构化输出(Structured Output)。传统的 AI 模型输出往往是简单的字符串形式,即使要求以 JSON 格式输出,也并非真正的 JSON 数据结构。Spring AI 通过精心设计的提示和转换机制,能够将模型输出转换为便于应用集成的结构化数据,大大提高了数据的可用性。
在将外部数据和 API 引入 AI 模型方面,Spring AI 提供了多种灵活的解决方案。微调(Fine Tuning)技术可以根据特定的数据集对模型进行优化,但该方法对技术要求较高且资源消耗大。提示填充(Prompt Stuffing)则是一种更为实用的方式,它将相关数据嵌入到提示中,Spring AI 为这一技术的实现提供了有力支持。此外,函数调用(Function Calling)机制允许开发者连接大语言模型与外部系统的 API,获取实时数据并进行处理,极大地拓展了模型的功能边界。
为了确保 AI 模型输出的准确性和实用性,Spring AI 还提供了完善的评估机制。通过评估 AI 响应(Evaluating AI responses),开发者可以从相关性、连贯性和事实正确性等多个维度对模型的输出进行衡量。利用预训练模型自身以及向量数据库中的信息,能够更全面地评估模型的表现,为进一步优化提供有力依据。
Spring AI 凭借其丰富的功能和强大的性能,为 Java 开发者提供了一个全面、高效的 AI 应用开发平台。无论是构建智能聊天机器人、图像生成应用,还是进行数据分析和预测,Spring AI 都能成为开发者的得力助手,助力开发者在人工智能领域创造出更加精彩的应用。
二、创建项目
我们一起来体验一下Spring AI的魅力,首先来创建项目:
- 创建项目:使用IDEA创建spring项目,注意官方要求版本号为3.2.x 和 3.3.x。本教程中我们使用maven来作为项目管理工具。
勾选spring-web选项
- 添加依赖: 首先添加snapshot需要的依赖库
<repositories>
<repository>
<id>spring-milestones</id>
<name>Spring Milestones</name>
<url>https://repo.spring.io/milestone</url>
<snapshots>
<enabled>false</enabled>
</snapshots>
</repository>
<repository>
<id>spring-snapshots</id>
<name>Spring Snapshots</name>
<url>https://repo.spring.io/snapshot</url>
<releases>
<enabled>false</enabled>
</releases>
</repository>
</repositories>
接下来添加spring-ai所有的bom,用来锁定依赖版本:
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-bom</artifactId>
<version>1.0.0-SNAPSHOT</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
最后添加对应大模型的依赖:
Spring AI支持的大模型有很多,在官网上有详细的对比,有兴趣的同学们可以详细去看下。 docs.spring.io/spring-ai/r…
今天我们选择的是智谱大模型,不需要科学上网就可以使用。首先去智谱的官网进行注册,然后申请一个API KEY。
接下来我们引入对应的依赖:
<dependency>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-zhipuai-spring-boot-starter</artifactId>
</dependency>
配置刚才申请的api key:
spring.ai.zhipuai.api-key=your_api_key
三、代码编写
package com.brianxiadong.spring_ai_demo.controller;
import org.springframework.ai.chat.messages.UserMessage;
import org.springframework.ai.chat.model.ChatResponse;
import org.springframework.ai.chat.prompt.Prompt;
import org.springframework.ai.zhipuai.ZhiPuAiChatModel;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import reactor.core.publisher.Flux;
import java.util.Map;
@RestController
public class ChatController {
private final ZhiPuAiChatModel chatModel;
@Autowired
public ChatController(ZhiPuAiChatModel chatModel) {
this.chatModel = chatModel;
}
@GetMapping("/ai/generate")
public Map generate(@RequestParam(value = "message", defaultValue = "Tell me a joke") String message) {
return Map.of("generation", this.chatModel.call(message));
}
@GetMapping("/ai/generateStream")
public Flux<ChatResponse> generateStream(@RequestParam(value = "message", defaultValue = "Tell me a joke") String message) {
var prompt = new Prompt(new UserMessage(message));
return this.chatModel.stream(prompt);
}
}
运行spring boot服务之后,调用接口进行测试:
curl http://localhost:8080/ai/generate
这个时候我们会遇到一个错误:
{"timestamp":"2025-02-05T07:25:47.090+00:00","status":500,"error":"Internal Server Error","path":"/ai/generate"}
回头去控制台看看
这说明我们已经将请求发送给了智谱大模型,但是由于Spring AI配置的默认大模型是收费的,同时api-key绑定的账户没有充值,所以无法成功调用。我们可以选择使用免费的模型进行测试:
修改一下配置
spring.ai.zhipuai.chat.options.model=GLM-4-Flash
使用GLM-4-Flash这个免费模型,再次进行测试:
成功返回了结果,然后我们换一个问题:
四、流式响应测试
让我们编写一个页面,测试流式相应的效果,同时实现打字机效果:
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>AI Chat Assistant</title>
<style>
/* 全局样式 */
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
line-height: 1.6;
color: #333;
background-color: #f7f7f8;
}
/* 聊天容器 */
.chat-container {
max-width: 800px;
margin: 0 auto;
padding: 20px;
height: 100vh;
display: flex;
flex-direction: column;
}
/* 消息区域 */
.messages-container {
flex: 1;
overflow-y: auto;
margin-bottom: 20px;
padding: 20px;
}
/* 消息样式 */
.message {
margin-bottom: 20px;
padding: 15px;
border-radius: 8px;
}
.user-message {
background-color: #fff;
margin-left: 20%;
}
.assistant-message {
background-color: #f0f0f0;
margin-right: 20%;
}
/* 输入区域 */
.input-container {
position: relative;
padding: 20px;
background-color: #fff;
border-radius: 8px;
box-shadow: 0 0 10px rgba(0,0,0,0.1);
}
#message-input {
width: 100%;
padding: 12px;
border: 1px solid #ddd;
border-radius: 4px;
resize: none;
height: 50px;
font-size: 16px;
}
#send-button {
position: absolute;
right: 30px;
bottom: 30px;
padding: 8px 16px;
background-color: #007bff;
color: white;
border: none;
border-radius: 4px;
cursor: pointer;
transition: background-color 0.3s;
}
#send-button:hover {
background-color: #0056b3;
}
#send-button:disabled {
background-color: #cccccc;
cursor: not-allowed;
}
/* 打字动画 */
.typing {
display: inline-block;
margin-left: 4px;
}
.typing span {
display: inline-block;
width: 6px;
height: 6px;
background-color: #666;
border-radius: 50%;
margin: 0 2px;
animation: typing 1s infinite;
}
.typing span:nth-child(2) { animation-delay: 0.2s; }
.typing span:nth-child(3) { animation-delay: 0.4s; }
@keyframes typing {
0%, 100% { transform: translateY(0); }
50% { transform: translateY(-4px); }
}
</style>
</head>
<body>
<div class="chat-container">
<div class="messages-container" id="messages">
<!-- 消息将在这里动态添加 -->
</div>
<div class="input-container">
<textarea
id="message-input"
placeholder="输入您的问题..."
rows="1"
onkeydown="if(event.keyCode === 13 && !event.shiftKey) { event.preventDefault(); sendMessage(); }">
</textarea>
<button id="send-button" onclick="sendMessage()">发送</button>
</div>
</div>
<script>
// DOM 元素
const messagesContainer = document.getElementById('messages');
const messageInput = document.getElementById('message-input');
const sendButton = document.getElementById('send-button');
// 工具函数:创建消息元素
function createMessageElement(content, isUser) {
const messageDiv = document.createElement('div');
messageDiv.className = `message ${isUser ? 'user-message' : 'assistant-message'}`;
messageDiv.textContent = content;
return messageDiv;
}
// 创建打字动画元素
function createTypingIndicator() {
const typingDiv = document.createElement('div');
typingDiv.className = 'message assistant-message';
typingDiv.innerHTML = '正在思考中<div class="typing"><span></span><span></span><span></span></div>';
return typingDiv;
}
// 发送消息
async function sendMessage() {
const message = messageInput.value.trim();
if (!message) return;
// 禁用输入和发送按钮
messageInput.disabled = true;
sendButton.disabled = true;
// 显示用户消息
messagesContainer.appendChild(createMessageElement(message, true));
messageInput.value = '';
// 显示打字动画
const typingIndicator = createTypingIndicator();
messagesContainer.appendChild(typingIndicator);
messagesContainer.scrollTop = messagesContainer.scrollHeight;
try {
// 调用流式API
const response = await fetch(`/ai/generateStream?message=${encodeURIComponent(message)}`);
const reader = response.body.getReader();
const decoder = new TextDecoder();
// 创建新的助手消息容器
const assistantMessage = document.createElement('div');
assistantMessage.className = 'message assistant-message';
// 移除打字动画
typingIndicator.remove();
// 添加新的消息容器
messagesContainer.appendChild(assistantMessage);
// 读取流式响应
let buffer = ''; // 用于存储未完成的JSON字符串
while (true) {
const {done, value} = await reader.read();
if (done) break;
// 解码响应数据
const chunk = decoder.decode(value);
// 去掉数组的开始和结束标志
let processedChunk = chunk.replace(/^[/, '').replace(/]$/, '');
// 如果chunk中间有数组标志,也去掉
processedChunk = processedChunk.replace(/],[/g, ',');
buffer += processedChunk;
// 查找完整的JSON对象
while (true) {
// 查找第一个完整的JSON对象
const startIndex = buffer.indexOf('{"result"');
if (startIndex === -1) break;
// 从开始位置查找匹配的结束括号
let bracketCount = 0;
let endIndex = -1;
for (let i = startIndex; i < buffer.length; i++) {
if (buffer[i] === '{') bracketCount++;
if (buffer[i] === '}') bracketCount--;
if (bracketCount === 0) {
endIndex = i;
break;
}
}
// 如果没有找到匹配的结束括号,说明JSON对象不完整
if (endIndex === -1) break;
// 提取完整的JSON对象
const jsonStr = buffer.substring(startIndex, endIndex + 1);
// 更新buffer,移除已处理的部分(包括可能的逗号)
buffer = buffer.substring(endIndex + 1).replace(/^,/, '');
try {
const jsonData = JSON.parse(jsonStr);
if (jsonData.result?.output?.text) {
// 将文本分成字符数组,逐个显示
const text = jsonData.result.output.text;
for (let i = 0; i < text.length; i++) {
await new Promise(resolve => setTimeout(resolve, 50)); // 每个字符间隔50ms
assistantMessage.textContent += text[i];
messagesContainer.scrollTop = messagesContainer.scrollHeight;
}
}
} catch (e) {
console.error('解析JSON数据失败:', e, jsonStr);
}
}
}
} catch (error) {
console.error('请求失败:', error);
const errorMessage = document.createElement('div');
errorMessage.className = 'message assistant-message';
errorMessage.textContent = '抱歉,发生了一些错误,请稍后重试。';
messagesContainer.appendChild(errorMessage);
} finally {
// 重新启用输入和发送按钮
messageInput.disabled = false;
sendButton.disabled = false;
messageInput.focus();
}
}
// 页面加载完成后聚焦输入框
window.onload = () => {
messageInput.focus();
};
</script>
</body>
</html>
访问页面,查看效果:
成功实现了类似于ChatGPT的效果。
通过本章系统学习,你将拥有一个完备的 Spring AI 开发环境,为后续深入探索知识、实践创新项目筑牢根基。