Spring Boot与Spring AI的结合为开发者提供了一条快速构建智能应用的捷径。作为一名长期使用Spring生态的开发者,我亲身体验了这套技术组合带来的效率提升。Spring Boot的自动配置和约定优于配置理念,与Spring AI提供的开箱即用AI能力相得益彰,让开发者可以专注于业务逻辑而非基础设施搭建。
这套技术栈特别适合以下场景:
在开始之前,确保你的开发环境满足以下要求:
提示:虽然Spring AI支持JDK 11,但为了获得最佳性能和完整功能,强烈建议使用JDK 17+
使用Spring Initializr创建项目是最便捷的方式:
对于已有项目,可以手动添加Spring AI依赖到pom.xml:
xml复制<dependency>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-core</artifactId>
<version>0.8.1</version>
</dependency>
大多数AI服务需要API密钥才能使用。在application.properties中添加你的密钥:
properties复制spring.ai.openai.api-key=你的API密钥
spring.ai.openai.chat.options.model=gpt-3.5-turbo
重要:永远不要将API密钥直接提交到版本控制系统。考虑使用环境变量或密钥管理服务。
创建一个简单的文本生成服务:
java复制@RestController
@RequestMapping("/ai")
public class AIController {
private final ChatClient chatClient;
public AIController(ChatClient chatClient) {
this.chatClient = chatClient;
}
@GetMapping("/generate")
public String generateText(@RequestParam String prompt) {
return chatClient.call(prompt);
}
}
启动应用后,访问/ai/generate?prompt=写一篇关于Spring Boot的文章即可获得AI生成的文本。
Spring AI提供了便捷的对话上下文管理功能:
java复制@GetMapping("/chat")
public String chat(@RequestParam String message) {
ChatResponse response = chatClient.call(
new Prompt(message,
OpenAiChatOptions.builder()
.withTemperature(0.7f)
.build()
)
);
return response.getResult().getOutput().getContent();
}
这里的temperature参数控制生成文本的创造性(0-1之间),值越高结果越随机。
Spring AI也支持图像生成:
java复制@GetMapping("/generate-image")
public String generateImage(@RequestParam String prompt) {
ImageResponse response = imageClient.call(
new ImagePrompt(prompt,
OpenAiImageOptions.builder()
.withQuality("hd")
.withN(1)
.build()
)
);
return response.getResult().getOutput().getUrl();
}
创建可重用的提示模板:
java复制@Bean
public PromptTemplate poemPromptTemplate() {
return new PromptTemplate("""
请用{style}风格写一首关于{topic}的诗。
诗的长度应为{length}行。
""");
}
@GetMapping("/poem")
public String generatePoem(
@RequestParam String style,
@RequestParam String topic,
@RequestParam int length
) {
Map<String, Object> params = Map.of(
"style", style,
"topic", topic,
"length", length
);
Prompt prompt = poemPromptTemplate().create(params);
return chatClient.call(prompt).getResult().getOutput().getContent();
}
对于长文本生成,使用流式响应可以提升用户体验:
java复制@GetMapping("/stream")
public SseEmitter streamResponse(@RequestParam String prompt) {
SseEmitter emitter = new SseEmitter();
chatClient.stream(new Prompt(prompt))
.subscribe(
chunk -> {
try {
emitter.send(chunk.getResult().getOutput().getContent());
} catch (IOException e) {
emitter.completeWithError(e);
}
},
emitter::completeWithError,
emitter::complete
);
return emitter;
}
Spring AI支持轻松切换不同AI模型:
properties复制# 切换为Azure OpenAI
spring.ai.azure.openai.api-key=你的Azure API密钥
spring.ai.azure.openai.endpoint=你的Azure端点
spring.ai.azure.openai.chat.options.model=gpt-35-turbo
在代码中无需任何修改,Spring AI会自动适配不同的提供商。
为减少API调用和降低成本,实现简单的缓存:
java复制@GetMapping("/cached-generate")
public String cachedGenerate(@RequestParam String prompt) {
return cacheManager.getCache("aiResponses").get(
prompt,
() -> chatClient.call(prompt)
);
}
防止超出API调用限制:
java复制@Bean
public RateLimiter aiRateLimiter() {
return RateLimiter.create(5); // 每秒5个请求
}
@GetMapping("/rate-limited")
public String rateLimitedGenerate(@RequestParam String prompt) {
if (!aiRateLimiter.tryAcquire()) {
throw new ResponseStatusException(
HttpStatus.TOO_MANY_REQUESTS,
"请求过于频繁,请稍后再试"
);
}
return chatClient.call(prompt);
}
统一的AI服务异常处理:
java复制@ControllerAdvice
public class AIExceptionHandler {
@ExceptionHandler(AIException.class)
public ResponseEntity<String> handleAIException(AIException ex) {
return ResponseEntity.status(HttpStatus.SERVICE_UNAVAILABLE)
.body("AI服务暂时不可用: " + ex.getMessage());
}
@ExceptionHandler(RateLimitException.class)
public ResponseEntity<String> handleRateLimit(RateLimitException ex) {
return ResponseEntity.status(HttpStatus.TOO_MANY_REQUESTS)
.body("请求过于频繁,请稍后再试");
}
}
将Spring AI集成到现有客服系统:
java复制@RestController
@RequestMapping("/support")
public class SupportController {
private final ChatClient chatClient;
private final KnowledgeBaseService knowledgeBase;
@PostMapping("/query")
public SupportResponse handleQuery(@RequestBody UserQuery query) {
String context = knowledgeBase.getRelevantArticles(query.getTopic());
String prompt = """
你是一个客服助手。基于以下知识库内容回答问题:
{context}
用户问题:{question}
""";
PromptTemplate template = new PromptTemplate(prompt);
Map<String, Object> params = Map.of(
"context", context,
"question", query.getText()
);
String response = chatClient.call(template.create(params));
return new SupportResponse(response, List.copyOf(context));
}
}
批量生成营销内容:
java复制@Scheduled(fixedRate = 3600000) // 每小时生成一次
public void generateMarketingContent() {
List<String> products = productService.getFeaturedProducts();
for (String product : products) {
String description = chatClient.call(
"为我们的产品'" + product + "'写一段吸引人的营销文案,约100字"
);
contentService.saveGeneratedContent(
product,
description,
"marketing"
);
}
}
创建代码生成助手:
java复制@GetMapping("/generate-code")
public String generateCode(
@RequestParam String language,
@RequestParam String task
) {
String prompt = String.format(
"用%s语言实现一个%s。只返回代码,不要解释。",
language, task
);
String code = chatClient.call(prompt);
return syntaxHighlighter.highlight(code, language);
}
防止注入攻击和不当内容:
java复制public String sanitizeInput(String input) {
// 移除HTML标签
String sanitized = Jsoup.clean(input, Safelist.none());
// 过滤敏感词汇
return sensitiveWordFilter.filter(sanitized);
}
@PostMapping("/safe-generate")
public String safeGenerate(@RequestBody UserInput input) {
String cleanPrompt = sanitizeInput(input.getPrompt());
return chatClient.call(cleanPrompt);
}
确保不泄露敏感信息:
java复制@Bean
public PromptSanitizer promptSanitizer() {
return prompt -> {
String sanitized = prompt;
// 移除身份证号、信用卡号等
sanitized = sanitized.replaceAll("\\d{15,18}", "[ID]");
sanitized = sanitized.replaceAll("\\d{4}-\\d{4}-\\d{4}-\\d{4}", "[CARD]");
return sanitized;
};
}
记录AI使用情况用于审计:
java复制@Aspect
@Component
public class AILoggingAspect {
@AfterReturning(
pointcut = "execution(* com.example.ai..*.*(..))",
returning = "result"
)
public void logAICall(JoinPoint jp, Object result) {
AILog log = new AILog();
log.setTimestamp(Instant.now());
log.setMethod(jp.getSignature().getName());
log.setInput(Arrays.toString(jp.getArgs()));
log.setOutput(result.toString());
logRepository.save(log);
}
}
测试AI服务组件:
java复制@SpringBootTest
class AIServiceTest {
@Autowired
private ChatClient chatClient;
@Test
void testTextGeneration() {
String result = chatClient.call("写一个简短的问候");
assertNotNull(result);
assertFalse(result.isEmpty());
}
@Test
void testLongInput() {
String longInput = "a".repeat(10000);
assertThrows(InputLengthException.class,
() -> chatClient.call(longInput));
}
}
测试完整API流程:
java复制@SpringBootTest(webEnvironment = WebEnvironment.RANDOM_PORT)
class AIControllerIT {
@LocalServerPort
private int port;
@Test
void testGenerateEndpoint() {
String url = "http://localhost:" + port + "/ai/generate?prompt=你好";
ResponseEntity<String> response = restTemplate.getForEntity(
url, String.class);
assertEquals(HttpStatus.OK, response.getStatusCode());
assertNotNull(response.getBody());
}
}
评估响应时间和吞吐量:
java复制@SpringBootTest
class AIPerformanceTest {
@Autowired
private AIService aiService;
@Test
void testResponseTime() {
int requests = 100;
long totalTime = 0;
for (int i = 0; i < requests; i++) {
long start = System.currentTimeMillis();
aiService.generateResponse("测试请求 " + i);
totalTime += System.currentTimeMillis() - start;
}
double avgTime = totalTime / (double) requests;
assertTrue(avgTime < 1000, "平均响应时间应小于1秒");
}
}
创建Dockerfile:
dockerfile复制FROM eclipse-temurin:17-jdk-jammy
WORKDIR /app
COPY target/spring-ai-demo.jar app.jar
ENTRYPOINT ["java", "-jar", "app.jar"]
构建并运行:
bash复制docker build -t spring-ai-app .
docker run -p 8080:8080 -e SPRING_AI_OPENAI_API_KEY=$API_KEY spring-ai-app
添加AI服务健康检查:
java复制@Bean
public HealthIndicator aiHealthIndicator() {
return () -> {
try {
chatClient.call("健康检查");
return Health.up().build();
} catch (Exception e) {
return Health.down(e).build();
}
};
}
通过Actuator监控AI使用情况:
java复制@Bean
public MeterBinder aiMetrics(AIService aiService) {
return registry -> {
Gauge.builder("ai.requests.count", aiService::getRequestCount)
.register(registry);
Timer.builder("ai.response.time")
.publishPercentiles(0.5, 0.95)
.register(registry);
};
}
跟踪API调用情况:
java复制@Aspect
@Component
public class AICostAspect {
@Autowired
private CostTrackingService costService;
@AfterReturning(
pointcut = "execution(* org.springframework.ai..*.*(..))",
returning = "response"
)
public void trackCost(JoinPoint jp, Object response) {
if (response instanceof ChatResponse) {
ChatResponse chatResponse = (ChatResponse) response;
costService.recordUsage(
chatResponse.getResult().getUsage().getPromptTokens(),
chatResponse.getResult().getUsage().getCompletionTokens()
);
}
}
}
实现多级缓存:
java复制@Bean
public CacheManager aiCacheManager() {
CaffeineCache memoryCache = new CaffeineCache("aiMemoryCache",
Caffeine.newBuilder()
.maximumSize(1000)
.expireAfterWrite(1, TimeUnit.HOURS)
.build());
RedisCacheManager redisCache = RedisCacheManager.builder(redisConnectionFactory)
.cacheDefaults(RedisCacheConfiguration.defaultCacheConfig()
.entryTtl(Duration.ofDays(1)))
.build();
return new CompositeCacheManager(memoryCache, redisCache);
}
根据场景选择合适模型:
java复制public String generateWithModelSelection(String prompt, boolean highQuality) {
String model = highQuality ? "gpt-4" : "gpt-3.5-turbo";
return chatClient.call(
new Prompt(prompt,
OpenAiChatOptions.builder()
.withModel(model)
.build()
)
);
}
在实际项目中,我发现合理设置temperature参数对生成结果的质量影响很大。对于创意性内容,0.7-0.9的效果较好;而对于技术性内容,0.3-0.5的参数值能产生更准确的结果。另一个实用技巧是在提示中明确指定输出格式,比如"请用Markdown格式返回,包含标题和列表",这样能显著减少后续处理的工作量。