
Spring AI Alibaba实战:通义千问与Java的完美融合
前言
在国产大模型快速发展的今天,通义千问(Qwen)已经成为国内开发者首选的AI模型之一。它有着优秀的中文理解能力、合理的定价,以及与国内合规要求的天然契合。
本文基于我在实际项目中使用Spring AI Alibaba的经验,分享如何在Spring Boot中集成通义千问,以及踩过的那些坑。
一、为什么选择Spring AI Alibaba
1.1 与OpenAI方式的对比
| 对比项 | OpenAI | 通义千问(Spring AI Alibaba) |
|---|---|---|
| 合规要求 | 数据出境限制 | 国内合规,数据不出境 |
| 中文理解 | 一般 | 优秀 |
| API定价 | 较高 | 约1/3价格 |
| 网络要求 | 需要代理 | 国内直连 |
| 定制能力 | 有限 | 支持企业专属模型 |
1.2 Spring AI Alibaba的优势
ini
Spring AI Alibaba = Spring生态 + 通义千问 + 国内基础设施
核心优势:
- 无缝集成:与Spring Boot自动配置完美融合
- 类型安全:相比Python,Java的编译期检查能避免很多运行时错误
- 企业级特性:支持连接池、熔断、监控等生产级特性
- Prompt模板:提供结构化的提示词管理
二、快速开始:10分钟接入通义千问
2.1 获取API密钥
- 访问阿里云百炼平台:dashscope.aliyun.com/
- 开通通义千问服务
- 获取API-KEY(格式:
sk-xxxxxxxx)
2.2 创建Spring Boot项目
使用Spring Initializr创建项目,选择:
- Spring Boot 3.3+
- Java 17+
- 依赖:Spring Web, Lombok
2.3 添加依赖
xml
<!-- pom.xml -->
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>3.3.0</version>
</parent>
<groupId>com.example</groupId>
<artifactId>spring-ai-alibaba-demo</artifactId>
<version>1.0.0</version>
<properties>
<java.version>17</java.version>
<spring-ai-alibaba.version>1.0.0-M3</spring-ai-alibaba.version>
</properties>
<dependencies>
<!-- Spring Web -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!-- Spring AI Alibaba -->
<dependency>
<groupId>com.alibaba.cloud.ai</groupId>
<artifactId>spring-ai-alibaba-spring-boot-starter</artifactId>
<version>${spring-ai-alibaba.version}</version>
</dependency>
<!-- Lombok -->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
<repositories>
<repository>
<id>spring-milestones</id>
<name>Spring Milestones</name>
<url>https://repo.spring.io/milestone</url>
</repository>
</repositories>
</project>
2.4 配置application.yml
yaml
# application.yml
server:
port: 8080
spring:
application:
name: spring-ai-alibaba-demo
ai:
alibaba:
# 从环境变量或配置中心获取,不要硬编码
api-key: ${ALI_API_KEY:sk-your-key-here}
base-url: https://dashscope.aliyuncs.com/compatible-mode/v1
chat:
options:
# 可选模型:qwen-turbo(快)、qwen-plus(强)、qwen-max(最强)
model: qwen-plus
# 温度参数:0=确定性输出,1=创造性输出
temperature: 0.7
# 最大生成token数
max-tokens: 2000
embedding:
options:
# 嵌入模型
model: text-embedding-v4
logging:
level:
com.alibaba.cloud.ai: DEBUG
org.springframework.ai: DEBUG
三、基础功能:ChatClient实战
3.1 最简单的对话
java
package com.example.demo.service;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.ai.chat.client.ChatClient;
import org.springframework.stereotype.Service;
@Slf4j
@Service
@RequiredArgsConstructor
public class ChatService {
private final ChatClient chatClient;
/**
* 构造函数注入ChatClient.Builder
* Spring会自动配置ChatClient.Builder
*/
public ChatService(ChatClient.Builder chatClientBuilder) {
this.chatClient = chatClientBuilder.build();
log.info("ChatService initialized with ChatClient");
}
/**
* 基础对话
*/
public String chat(String message) {
log.info("Received message: {}", message);
String response = chatClient.prompt()
.user(message)
.call()
.content();
log.info("AI response length: {}", response.length());
return response;
}
}
3.2 带系统提示的对话
java
/**
* 带系统提示的对话
* 系统提示用于设定AI的角色和行为规范
*/
public String chatWithSystemPrompt(String message) {
return chatClient.prompt()
.system("你是一个专业的Java技术专家,擅长Spring Boot、微服务架构和性能优化。" +
"回答要简洁、准确,尽量给出代码示例。如果涉及代码,请使用Java语言。")
.user(message)
.call()
.content();
}
3.3 流式输出
对于长文本回复,流式输出能显著改善用户体验:
java
import reactor.core.publisher.Flux;
/**
* 流式输出
* 适用于长回复场景,用户可以实时看到生成过程
*/
public Flux<String> chatStream(String message) {
return chatClient.prompt()
.user(message)
.stream()
.content();
}
Controller层实现SSE(Server-Sent Events):
java
package com.example.demo.controller;
import com.example.demo.service.ChatService;
import lombok.RequiredArgsConstructor;
import org.springframework.http.MediaType;
import org.springframework.web.bind.annotation.*;
import reactor.core.publisher.Flux;
@RestController
@RequestMapping("/api/chat")
@RequiredArgsConstructor
public class ChatController {
private final ChatService chatService;
/**
* 普通对话接口
* POST /api/chat
*/
@PostMapping
public String chat(@RequestBody ChatRequest request) {
if (request.message() == null || request.message().isBlank()) {
throw new IllegalArgumentException("消息内容不能为空");
}
return chatService.chat(request.message());
}
/**
* 流式对话接口(SSE)
* POST /api/chat/stream
*/
@PostMapping(value = "/stream", produces = MediaType.TEXT_EVENT_STREAM_VALUE)
public Flux<String> chatStream(@RequestBody ChatRequest request) {
return chatService.chatStream(request.message());
}
}
/**
* 请求参数记录
*/
public record ChatRequest(String message) {}
四、进阶功能:结构化输出与Prompt模板
4.1 结构化输出
在很多场景下,我们需要AI返回结构化的数据(JSON、Java对象等)。
定义输出结构
java
package com.example.demo.model;
/**
* 代码审查结果
*/
public record CodeReviewResult(
int score, // 代码质量评分(1-100)
List<String> issues, // 发现的问题
List<String> suggestions, // 改进建议
String summary // 总体评价
) {}
/**
* SQL优化建议
*/
public record SqlOptimizationResult(
String originalSql,
String optimizedSql,
List<String> improvements,
String explanation
) {}
实现结构化输出
java
/**
* 代码审查:返回结构化结果
*/
public CodeReviewResult reviewCode(String code) {
String prompt = """
请作为资深Java代码审查专家,对以下代码进行审查。
代码内容:
```java
%s
```
请按JSON格式返回审查结果,包含以下字段:
- score: 代码质量评分(1-100的整数)
- issues: 发现的问题列表(字符串数组)
- suggestions: 改进建议列表(字符串数组)
- summary: 总体评价(字符串)
返回格式必须是合法的JSON,不要包含其他内容。
""".formatted(code);
String jsonResponse = chatClient.prompt()
.user(prompt)
.call()
.content();
// 提取JSON(AI可能返回markdown代码块)
String json = extractJsonFromResponse(jsonResponse);
try {
ObjectMapper mapper = new ObjectMapper();
return mapper.readValue(json, CodeReviewResult.class);
} catch (JsonProcessingException e) {
log.error("Failed to parse JSON response: {}", jsonResponse, e);
throw new RuntimeException("AI返回格式错误", e);
}
}
/**
* 从AI响应中提取JSON
*/
private String extractJsonFromResponse(String response) {
// 去除markdown代码块标记
response = response.trim();
if (response.startsWith("```json")) {
response = response.substring(7);
}
if (response.startsWith("```")) {
response = response.substring(3);
}
if (response.endsWith("```")) {
response = response.substring(0, response.length() - 3);
}
return response.trim();
}
4.2 Prompt模板
Spring AI提供了Prompt模板功能,可以管理复杂的提示词。
java
import org.springframework.ai.chat.prompt.PromptTemplate;
import org.springframework.ai.chat.messages.Message;
/**
* 使用Prompt模板
*/
public String chatWithTemplate(String topic, String audience, int length) {
String template = """
请写一篇关于{topic}的技术博客,要求:
1. 面向{audience}
2. 文章长度约{length}字
3. 包含代码示例
4. 语言简洁易懂
5. 包含实际应用案例
请直接开始写文章正文,不要有任何开场白。
""";
PromptTemplate promptTemplate = new PromptTemplate(template);
Message message = promptTemplate.createMessage(Map.of(
"topic", topic,
"audience", audience,
"length", String.valueOf(length)
));
return chatClient.prompt()
.messages(message)
.call()
.content();
}
五、RAG实战:让AI读懂你的业务文档
5.1 添加向量存储依赖
xml
<!-- 使用Milvus作为向量数据库 -->
<dependency>
<groupId>com.alibaba.cloud.ai</groupId>
<artifactId>spring-ai-alibaba-vector-store-milvus</artifactId>
<version>1.0.0-M3</version>
</dependency>
5.2 配置Milvus
yaml
# application.yml(追加)
spring:
ai:
vectorstore:
milvus:
client:
host: localhost
port: 19530
username: ""
password: ""
collection-name: company_docs
embedding-dimension: 1536 # text-embedding-v4的输出维度
5.3 文档处理服务
java
package com.example.demo.service;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.ai.document.Document;
import org.springframework.ai.vectorstore.VectorStore;
import org.springframework.stereotype.Service;
import java.util.List;
import java.util.Map;
import java.util.UUID;
@Slf4j
@Service
@RequiredArgsConstructor
public class DocumentIngestionService {
private final VectorStore vectorStore;
private final EmbeddingModel embeddingModel;
/**
* 将文本添加到知识库
*/
public int addDocument(String content, String source) {
Document doc = new Document(
UUID.randomUUID().toString(),
content,
Map.of(
"source", source,
"timestamp", String.valueOf(System.currentTimeMillis())
)
);
vectorStore.add(List.of(doc));
log.info("Document added to knowledge base: {}", source);
return 1;
}
/**
* 批量添加文档
*/
public int addDocuments(List<String> contents, String sourcePrefix) {
List<Document> documents = new ArrayList<>();
for (int i = 0; i < contents.size(); i++) {
Document doc = new Document(
UUID.randomUUID().toString(),
contents.get(i),
Map.of(
"source", sourcePrefix + "_chunk_" + i,
"chunk_index", String.valueOf(i)
)
);
documents.add(doc);
}
vectorStore.add(documents);
log.info("Added {} document chunks to knowledge base", documents.size());
return documents.size();
}
}
5.4 RAG问答服务
java
package com.example.demo.service;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.ai.chat.client.ChatClient;
import org.springframework.ai.document.Document;
import org.springframework.ai.vectorstore.VectorStore;
import org.springframework.ai.chat.messages.UserMessage;
import org.springframework.ai.chat.prompt.Prompt;
import org.springframework.stereotype.Service;
import java.util.List;
import java.util.stream.Collectors;
@Slf4j
@Service
@RequiredArgsConstructor
public class RagService {
private final ChatClient chatClient;
private final VectorStore vectorStore;
/**
* RAG问答
* @param question 用户问题
* @return AI回答
*/
public String answerWithRag(String question) {
// Step 1: 从向量库检索相关文档
List<Document> relevantDocs = vectorStore.similaritySearch(
org.springframework.ai.chat.client.SearchRequest.builder()
.query(question)
.topK(5) // 返回最相关的5个文档
.similarityThreshold(0.5) // 相似度阈值
.build()
);
if (relevantDocs.isEmpty()) {
return "知识库中未找到相关信息。";
}
log.info("Retrieved {} relevant documents", relevantDocs.size());
// Step 2: 构建上下文
String context = relevantDocs.stream()
.map(Document::getText)
.collect(Collectors.joining("\n\n---\n\n"));
// Step 3: 构建RAG Prompt
String prompt = """
你是一个专业的企业知识助手。请严格根据以下上下文信息回答问题。
【重要规则】:
1. 如果上下文中有答案,请基于上下文回答
2. 如果上下文中没有相关信息,请明确告知用户
3. 回答要准确、简洁、有条理
4. 可以适当引用上下文中的内容
上下文信息:
%s
用户问题:%s
请基于上下文回答:
""".formatted(context, question);
// Step 4: 调用AI生成答案
String answer = chatClient.prompt()
.user(prompt)
.call()
.content();
log.info("RAG answer generated, length: {}", answer.length());
return answer;
}
/**
* 流式RAG问答
*/
public reactor.core.publisher.Flux<String> answerWithRagStream(String question) {
List<Document> relevantDocs = vectorStore.similaritySearch(
org.springframework.ai.chat.client.SearchRequest.builder()
.query(question)
.topK(5)
.build()
);
if (relevantDocs.isEmpty()) {
return reactor.core.publisher.Flux.just("知识库中未找到相关信息。");
}
String context = relevantDocs.stream()
.map(Document::getText)
.collect(Collectors.joining("\n\n"));
String prompt = "根据以下上下文回答问题:\n%s\n\n问题:%s".formatted(context, question);
return chatClient.prompt()
.user(prompt)
.stream()
.content();
}
}
六、踩坑记录与解决方案
6.1 API调用超时
问题现象:
csharp
java.net.SocketTimeoutException: Read timed out
原因分析: 大模型生成时间较长,默认的10秒超时不够。
解决方案:
java
// 自定义超时配置
@Configuration
public class AiConfig {
@Bean
public ChatClient chatClient(ChatClient.Builder builder) {
return builder
.defaultClientRequest(request -> request
.timeout(java.time.Duration.ofSeconds(60)) // 延长到60秒
)
.build();
}
}
6.2 中文乱码
问题现象: 返回的中文显示为乱码。
原因分析: HTTP响应编码未正确设置。
解决方案:
yaml
spring:
ai:
alibaba:
base-url: https://dashscope.aliyuncs.com/compatible-mode/v1
# 确保使用兼容模式URL,该URL默认支持UTF-8
6.3 Token超限
问题现象:
vbnet
RuntimeException: This model's maximum context length is 128000 tokens
解决方案:
java
/**
* 智能截断,避免超出Token限制
*/
private String truncateByTokenEstimate(String text, int maxTokens) {
// 粗略估算:中文1字≈1.5 token,英文1词≈1.3 token
int estimatedTokens = estimateTokens(text);
if (estimatedTokens <= maxTokens) {
return text;
}
// 按字符比例截断
int maxChars = (int) (maxTokens / 1.5);
log.warn("Text truncated from {} to {} chars to fit token limit",
text.length(), maxChars);
return text.substring(0, Math.min(maxChars, text.length()));
}
private int estimateTokens(String text) {
long chineseCount = text.chars()
.filter(c -> c >= 0x4E00 && c <= 0x9FFF)
.count();
long englishWords = text.split("\\s+").length;
return (int) (chineseCount * 1.5 + englishWords * 1.3);
}
6.4 嵌入模型维度不匹配
问题现象:
yaml
IllegalArgumentException: Dimension mismatch: expected 1536, got 1024
原因分析: 不同嵌入模型的输出维度不同,需要与向量库配置一致。
解决方案:
yaml
spring:
ai:
alibaba:
embedding:
options:
model: text-embedding-v4 # 输出维度1536
vectorstore:
milvus:
embedding-dimension: 1536 # 与嵌入模型输出维度一致
6.5 流式输出被拦截器处理
问题现象: SSE流式输出被Spring的某些拦截器处理,导致格式错误。
解决方案:
java
@RestController
@RequestMapping("/api/chat")
public class ChatController {
// 为流式接口禁用某些拦截器
@PostMapping(value = "/stream", produces = MediaType.TEXT_EVENT_STREAM_VALUE)
public Flux<String> chatStream(@RequestBody ChatRequest request) {
return chatService.chatStream(request.message())
.doOnNext(token -> log.debug("Streaming token: {}", token));
}
}
七、生产级最佳实践
7.1 配置管理
java
@Configuration
@ConfigurationProperties(prefix = "app.ai")
@Data
public class AiProperties {
/**
* 是否启用AI功能(用于灰度发布)
*/
private boolean enabled = true;
/**
* 最大重试次数
*/
private int maxRetries = 3;
/**
* 超时时间(秒)
*/
private int timeoutSeconds = 60;
/**
* Token预算(每次请求最大token数)
*/
private int maxTokensPerRequest = 2000;
}
7.2 错误处理与重试
java
import org.springframework.retry.annotation.Retryable;
import org.springframework.retry.annotation.Backoff;
@Service
@Slf4j
public class RobustChatService {
private final ChatClient chatClient;
@Retryable(
value = {Exception.class},
maxAttempts = 3,
backoff = @Backoff(delay = 1000, multiplier = 2)
)
public String chatWithRetry(String message) {
try {
return chatClient.prompt()
.user(message)
.call()
.content();
} catch (Exception e) {
log.error("AI调用失败,将重试", e);
throw e; // 触发重试
}
}
}
7.3 成本监控
java
@Component
@Slf4j
public class TokenUsageMonitor {
private final MeterRegistry meterRegistry;
public TokenUsageMonitor(MeterRegistry meterRegistry) {
this.meterRegistry = meterRegistry;
}
/**
* 记录Token使用情况
*/
public void recordUsage(int inputTokens, int outputTokens) {
meterRegistry.counter("ai.token.usage.input").increment(inputTokens);
meterRegistry.counter("ai.token.usage.output").increment(outputTokens);
// 估算成本(通义千问qwen-plus价格:0.004元/千tokens)
double costYuan = (inputTokens + outputTokens) * 0.004 / 1000;
meterRegistry.counter("ai.cost.yuan").increment(costYuan);
log.info("Token usage - Input: {}, Output: {}, Estimated cost: {} yuan",
inputTokens, outputTokens, costYuan);
}
/**
* 获取今日总成本
*/
public double getTodaysCost() {
// 实际实现需要对接监控系统
return meterRegistry.counter("ai.cost.yuan").count();
}
}
八、完整项目示例
8.1 项目结构
css
spring-ai-alibaba-demo/
├── src/main/java/com/example/demo/
│ ├── DemoApplication.java
│ ├── config/
│ │ ├── AiConfig.java
│ │ └── SwaggerConfig.java
│ ├── controller/
│ │ ├── ChatController.java
│ │ └── RagController.java
│ ├── service/
│ │ ├── ChatService.java
│ │ ├── RagService.java
│ │ └── DocumentService.java
│ └── model/
│ ├── ChatRequest.java
│ └── CodeReviewResult.java
├── src/main/resources/
│ ├── application.yml
│ └── prompt-templates/
│ ├── code-review.st
│ └── sql-optimization.st
└── docker-compose.yml
8.2 Docker Compose配置
yaml
version: '3.8'
services:
milvus:
image: milvusdb/milvus:v2.4.0
container_name: milvus-standalone
ports:
- "19530:19530"
environment:
ETCD_ENDPOINTS: milvus-etcd:2379
MINIO_ADDRESS: milvus-minio:9000
volumes:
- ./volumes/milvus:/var/lib/milvus
restart: always
app:
build: .
container_name: spring-ai-app
ports:
- "8080:8080"
environment:
- ALI_API_KEY=${ALI_API_KEY}
depends_on:
- milvus
restart: always
8.3 启动类
java
package com.example.demo;
import lombok.extern.slf4j.Slf4j;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
@Slf4j
@SpringBootApplication
public class DemoApplication {
public static void main(String[] args) {
SpringApplication.run(DemoApplication.class, args);
log.info("Spring AI Alibaba Demo started successfully!");
log.info("API endpoint: http://localhost:8080/api/chat");
}
}
九、性能测试与优化
9.1 性能基准测试
java
@Test
void benchmarkChatPerformance() {
int iterations = 100;
long totalTime = 0;
for (int i = 0; i < iterations; i++) {
long start = System.currentTimeMillis();
String response = chatService.chat("用一句话介绍Spring Boot");
long end = System.currentTimeMillis();
totalTime += (end - start);
}
double avgTime = totalTime / (double) iterations;
log.info("Average response time: {} ms", avgTime);
// 性能目标:平均响应时间 < 3秒
assertTrue(avgTime < 3000);
}
9.2 优化建议
- 使用连接池:
yaml
spring:
ai:
alibaba:
chat:
options:
# 启用请求合并
stream: false
- 启用响应缓存:
java
@Cacheable(value = "ai-responses", key = "#message.hashCode()")
public String chatWithCache(String message) {
return chatClient.prompt().user(message).call().content();
}
- 异步处理:
java
@Async
public CompletableFuture<String> chatAsync(String message) {
return CompletableFuture.supplyAsync(() -> chatService.chat(message));
}
十、总结与资源
10.1 关键要点
- Spring AI Alibaba让Java开发者能够以Spring的方式使用通义千问
- ChatClient API简单易用,支持链式调用和流式输出
- RAG是生产环境最常用的模式,需要配合向量数据库
- 注意超时、Token限制、成本控制等生产级问题
- 通义千问的中文理解能力非常适合国内项目
10.2 适用场景
| 场景 | 推荐模型 | 理由 |
|---|---|---|
| 实时对话 | qwen-turbo | 速度快,成本低 |
| 复杂推理 | qwen-plus | 能力强,性价比高 |
| 代码生成 | qwen-coder-plus | 专门针对代码优化 |
| 向量嵌入 | text-embedding-v4 | 中文优化,维度合理 |
10.3 参考资源
- Spring AI官方文档:docs.spring.io/spring-ai/r...
- 阿里云百炼平台:dashscope.aliyun.com/
- Spring AI Alibaba GitHub:github.com/alibaba/spr...
- 通义千问模型文档:help.aliyun.com/zh/model-st...
如果有帮助,欢迎点赞、收藏、关注! 如有问题,欢迎在评论区交流。