第1971篇:Spring AI Alibaba深度实践——通义千问系列模型的特殊配置
第1971篇:Spring AI Alibaba深度实践——通义千问系列模型的特殊配置
说实话,第一次把 Spring AI Alibaba 接入生产的时候,我踩了不少坑。文档写得挺全,但有几个细节藏得很深,一旦漏掉,要么模型调不起来,要么效果差一大截。今天这篇就把我的实战经验系统梳理一遍,从依赖配置、模型选型到多模态、函数调用,一层一层讲清楚。
为什么选 Spring AI Alibaba 而不是直接用阿里云 SDK
很多同学上来就说"直接用阿里的 Java SDK 不就行了"。我以前也这么想。但用过之后发现,如果你的系统要同时接入多个模型(比如通义千问 + DeepSeek + 本地 Ollama),各家 SDK 风格差异极大,维护起来很痛苦。
Spring AI 的价值在于它提供了一层统一抽象:ChatClient、EmbeddingModel、VectorStore……不管底层换哪个模型,上层业务代码基本不动。Spring AI Alibaba 就是在这套体系里,把通义千问系列接进来的官方适配层。
选它的另一个理由:Spring AI Alibaba 由阿里云官方维护,跟 DashScope API 的版本同步比第三方快,新出的模型特性(比如 qwen-max-latest 的长文本能力)能第一时间用上。
不过坑也有,下面慢慢说。
依赖配置:版本对齐是第一道关
很多人一开始就栽在这里。Spring AI Alibaba 对 Spring AI 的版本有严格依赖关系,不对齐直接报运行时错误,而且错误信息不够直观。
当前推荐组合(2024年底到2025年初稳定):
<dependencyManagement>
<dependencies>
<!-- Spring AI BOM 统一管理版本 -->
<dependency>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-bom</artifactId>
<version>1.0.0-M4</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
<dependencies>
<!-- Spring AI Alibaba 核心 -->
<dependency>
<groupId>com.alibaba.cloud.ai</groupId>
<artifactId>spring-ai-alibaba-starter</artifactId>
<version>1.0.0-M3.1</version>
</dependency>
<!-- Spring Boot Web -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!-- Spring Boot WebFlux(流式输出必需) -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-webflux</artifactId>
</dependency>
</dependencies>注意:Spring AI Alibaba 1.0.0-M3.x 对应 Spring AI 1.0.0-M4,这两个版本号不一样,手动填错了就会出现 ClassNotFoundException 或者接口不兼容的问题。别问我为什么知道,血泪经验。
还有一个仓库配置要加,Spring AI 的 milestone 版本不在中央仓库:
<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>基础配置:application.yml 的那些细节
spring:
ai:
dashscope:
api-key: ${DASHSCOPE_API_KEY}
# 默认模型,不写就用 qwen-plus
chat:
options:
model: qwen-max
temperature: 0.7
max-tokens: 4096
# 开启流式增量输出,减少首字延迟感知
incremental-output: trueapi-key 建议通过环境变量注入,千万不要硬编码进配置文件提交到 Git,这是安全红线。
incremental-output: true 这个参数很多人不知道,它决定了流式输出时每个 chunk 是"累积全文"还是"只返回新增部分"。设为 true 是返回增量,前端接收处理起来更自然;设为 false 是每次返回从头到现在的全部内容,流量消耗翻倍。
通义千问模型系列的选型逻辑
通义千问不是一个模型,是一个系列。选错了,要么浪费钱,要么效果不达预期。
我在项目里总结的经验:
- 客服对话:用 qwen-plus,性价比最高,响应也快
- 代码生成:用 qwen-coder-plus 或 qwen-max,通用模型在代码任务上表现不如专用版
- 文档摘要/RAG 检索增强:qwen-plus 足够,别用 qwen-max 浪费 token
- 复杂推理/分析报告:qwen-max 或 qwen-max-latest
- 高并发场景:qwen-turbo,延迟低,成本低一个数量级
实战代码:ChatClient 的标准用法
@Service
public class QwenChatService {
private final ChatClient chatClient;
public QwenChatService(ChatClient.Builder chatClientBuilder) {
this.chatClient = chatClientBuilder
.defaultSystem("你是一位专业的Java工程师助手,回答要简洁准确,代码示例要可以直接运行。")
.build();
}
/**
* 同步调用——简单场景
*/
public String chat(String userMessage) {
return chatClient.prompt()
.user(userMessage)
.call()
.content();
}
/**
* 动态切换模型——根据任务复杂度选型
*/
public String chatWithModel(String userMessage, String modelName) {
DashScopeChatOptions options = DashScopeChatOptions.builder()
.withModel(modelName)
.withTemperature(0.7f)
.withMaxToken(4096)
.build();
return chatClient.prompt()
.user(userMessage)
.options(options)
.call()
.content();
}
/**
* 流式输出——用户体验关键
*/
public Flux<String> streamChat(String userMessage) {
return chatClient.prompt()
.user(userMessage)
.stream()
.content();
}
/**
* 带对话历史的多轮对话
*/
public String multiTurnChat(List<Message> history, String newMessage) {
return chatClient.prompt()
.messages(history)
.user(newMessage)
.call()
.content();
}
}流式输出配套的 Controller:
@RestController
@RequestMapping("/api/chat")
public class ChatController {
@Autowired
private QwenChatService chatService;
@GetMapping(value = "/stream", produces = MediaType.TEXT_EVENT_STREAM_VALUE)
public Flux<ServerSentEvent<String>> streamChat(@RequestParam String message) {
return chatService.streamChat(message)
.map(content -> ServerSentEvent.<String>builder()
.data(content)
.build())
.concatWith(Flux.just(
ServerSentEvent.<String>builder()
.event("done")
.data("[DONE]")
.build()
));
}
}特殊配置一:多轮对话的会话管理
Spring AI 默认每次请求都是无状态的,多轮对话需要自己管理历史。Spring AI Alibaba 提供了 ChatMemory 支持:
@Configuration
public class ChatConfig {
@Bean
public ChatMemory chatMemory() {
// 内存存储,生产环境建议换成 Redis 实现
return new InMemoryChatMemory();
}
@Bean
public ChatClient chatClient(ChatClient.Builder builder, ChatMemory chatMemory) {
return builder
.defaultSystem("你是一位AI助手")
.defaultAdvisors(
// MessageChatMemoryAdvisor 自动把历史消息塞进请求
new MessageChatMemoryAdvisor(chatMemory),
// 日志 Advisor,开发调试用
new SimpleLoggerAdvisor()
)
.build();
}
}
@Service
public class SessionChatService {
@Autowired
private ChatClient chatClient;
public String chat(String sessionId, String message) {
return chatClient.prompt()
.user(message)
// 通过 conversationId 区分不同会话
.advisors(advisor -> advisor
.param(AbstractChatMemoryAdvisor.CHAT_MEMORY_CONVERSATION_ID_KEY, sessionId)
.param(AbstractChatMemoryAdvisor.CHAT_MEMORY_RETRIEVE_SIZE_KEY, 10)
)
.call()
.content();
}
}踩坑提示:CHAT_MEMORY_RETRIEVE_SIZE_KEY 控制每次取多少条历史记录,默认是全部取出。如果会话很长,会把 context window 撑爆,建议根据模型限制设置合理上限。qwen-max 支持 8k token,保守设 10 条消息比较安全。
特殊配置二:Function Calling 的工程化使用
Function Calling 是通义千问能力很强的一块,但配置方式跟 OpenAI 有些差异。Spring AI Alibaba 做了统一封装,但细节要注意。
// 定义工具函数
@Component
public class WeatherTools {
@Tool(description = "根据城市名查询当前天气,返回温度、湿度和天气描述")
public WeatherInfo getWeather(
@ToolParam(description = "城市名称,例如:北京、上海、广州") String city
) {
// 实际业务中调用天气 API
// 这里用 mock 数据演示
return WeatherInfo.builder()
.city(city)
.temperature(25.0)
.humidity(60)
.description("晴天,微风")
.build();
}
@Tool(description = "查询未来N天的天气预报")
public List<WeatherForecast> getForecast(
@ToolParam(description = "城市名称") String city,
@ToolParam(description = "预报天数,1到7之间") int days
) {
// 实现略
return List.of();
}
}
// 在 ChatClient 中注册工具
@Service
public class ToolEnabledChatService {
@Autowired
private ChatClient chatClient;
@Autowired
private WeatherTools weatherTools;
public String chatWithTools(String message) {
return chatClient.prompt()
.user(message)
.tools(weatherTools) // 注册工具
.call()
.content();
}
}有个坑值得注意:通义千问的 Function Calling 在工具描述质量上很敏感。我测试发现,描述写得模糊(比如"查天气"三个字),模型调用工具的成功率比详细描述低了将近 30%。所以 @Tool 的 description 要写清楚,尽量包含"什么情况下用"和"返回什么"。
特殊配置三:多模态——图文理解实战
@Service
public class MultimodalChatService {
@Autowired
private ChatClient chatClient;
/**
* 图片理解——URL 方式
*/
public String analyzeImageByUrl(String imageUrl, String question) {
UserMessage userMessage = new UserMessage(
question,
List.of(new Media(MimeTypeUtils.IMAGE_PNG, new URL(imageUrl)))
);
return chatClient.prompt()
.options(DashScopeChatOptions.builder()
.withModel("qwen-vl-max")
.build())
.messages(userMessage)
.call()
.content();
}
/**
* 图片理解——Base64 方式(适合本地图片)
*/
public String analyzeImageByBase64(byte[] imageBytes, String question) {
String base64Image = Base64.getEncoder().encodeToString(imageBytes);
UserMessage userMessage = new UserMessage(
question,
List.of(new Media(
MimeTypeUtils.IMAGE_JPEG,
"data:image/jpeg;base64," + base64Image
))
);
return chatClient.prompt()
.options(DashScopeChatOptions.builder()
.withModel("qwen-vl-max")
.build())
.messages(userMessage)
.call()
.content();
}
}多模态这块有个常见误区:用了 qwen-vl-max 之后发现图片识别有时很准、有时很差。后来排查发现是图片分辨率问题——太低(低于 200px)或太高(超过 10MB)都影响效果。我在业务层加了图片预处理,压缩到 1024x1024 以内,识别准确率明显提升。
特殊配置四:Embedding 和向量检索
通义千问的 Embedding 模型是 text-embedding-v3,在中文语义理解上比 OpenAI 的 ada-002 要好一些(至少在我们的中文业务场景里如此)。
@Configuration
public class EmbeddingConfig {
@Bean
public EmbeddingModel embeddingModel(DashScopeConnectionProperties properties) {
DashScopeEmbeddingOptions options = DashScopeEmbeddingOptions.builder()
.withModel("text-embedding-v3")
.build();
return new DashScopeEmbeddingModel(
new DashScopeApi(properties.getApiKey()),
MetadataMode.EMBED,
options
);
}
}
@Service
public class EmbeddingService {
@Autowired
private EmbeddingModel embeddingModel;
@Autowired
private VectorStore vectorStore;
/**
* 文档入库
*/
public void indexDocuments(List<String> texts) {
List<Document> docs = texts.stream()
.map(text -> new Document(text, Map.of("source", "manual_input")))
.toList();
// VectorStore 内部会调用 EmbeddingModel 计算向量
vectorStore.add(docs);
}
/**
* 语义检索
*/
public List<Document> search(String query, int topK) {
return vectorStore.similaritySearch(
SearchRequest.query(query)
.withTopK(topK)
.withSimilarityThreshold(0.75)
);
}
}RAG 完整流程:把所有配置串起来
@Service
public class RAGService {
@Autowired
private ChatClient chatClient;
@Autowired
private VectorStore vectorStore;
private static final String RAG_PROMPT_TEMPLATE = """
你是一位专业助手。请根据以下参考资料回答用户问题。
如果参考资料中没有相关信息,请如实告知,不要编造内容。
参考资料:
{context}
用户问题:{question}
""";
public String ragQuery(String question) {
// 1. 检索相关文档
List<Document> docs = vectorStore.similaritySearch(
SearchRequest.query(question).withTopK(5)
);
// 2. 拼接上下文
String context = docs.stream()
.map(Document::getContent)
.collect(Collectors.joining("\n---\n"));
// 3. 构建 Prompt 并调用模型
return chatClient.prompt()
.system(RAG_PROMPT_TEMPLATE
.replace("{context}", context)
.replace("{question}", question))
.user(question)
.call()
.content();
}
}当然,上面是手动实现 RAG 的方式。Spring AI 提供了更优雅的 QuestionAnswerAdvisor:
@Bean
public ChatClient ragChatClient(ChatClient.Builder builder, VectorStore vectorStore) {
return builder
.defaultAdvisors(
new QuestionAnswerAdvisor(
vectorStore,
SearchRequest.defaults().withTopK(5)
)
)
.build();
}这样 Advisor 会自动在每次请求前做检索并注入上下文,业务代码干净很多。
重试与限流:生产必备配置
通义千问 API 有 QPS 限制,高并发场景下必须做好重试和限流。
@Configuration
public class RetryConfig {
@Bean
public RetryTemplate dashscopeRetryTemplate() {
return RetryTemplate.builder()
.maxAttempts(3)
.exponentialBackoff(1000, 2, 10000)
// 只对服务端限流和临时错误重试,不对认证错误重试
.retryOn(List.of(
ResourceAccessException.class,
HttpServerErrorException.class
))
.build();
}
}
// 配套限流(使用 Resilience4j)
@Service
public class RateLimitedChatService {
@Autowired
private ChatClient chatClient;
@RateLimiter(name = "dashscope", fallbackMethod = "fallbackChat")
@Retry(name = "dashscope")
public String chat(String message) {
return chatClient.prompt()
.user(message)
.call()
.content();
}
public String fallbackChat(String message, Exception e) {
log.warn("DashScope API 限流,触发降级: {}", e.getMessage());
return "系统繁忙,请稍后重试";
}
}# application.yml 里的 Resilience4j 配置
resilience4j:
rate-limiter:
instances:
dashscope:
limit-for-period: 10 # 每个周期允许的请求数
limit-refresh-period: 1s # 周期时长
timeout-duration: 3s # 等待令牌的超时时间
retry:
instances:
dashscope:
max-attempts: 3
wait-duration: 1s
exponential-backoff-multiplier: 2监控:你必须知道模型调用在消耗什么
没有监控的 AI 服务是定时炸弹,费用和延迟随时可能失控。
@Aspect
@Component
@Slf4j
public class DashScopeMonitorAspect {
@Autowired
private MeterRegistry meterRegistry;
@Around("@annotation(org.springframework.ai.chat.client.ChatClient)")
public Object monitorChatCall(ProceedingJoinPoint pjp) throws Throwable {
long start = System.currentTimeMillis();
String model = extractModel(pjp);
try {
Object result = pjp.proceed();
long duration = System.currentTimeMillis() - start;
// 记录调用延迟
meterRegistry.timer("dashscope.chat.duration",
"model", model,
"status", "success"
).record(duration, TimeUnit.MILLISECONDS);
// 记录 Token 消耗(从响应中提取)
if (result instanceof ChatResponse response) {
Usage usage = response.getMetadata().getUsage();
meterRegistry.counter("dashscope.tokens.prompt",
"model", model).increment(usage.getPromptTokens());
meterRegistry.counter("dashscope.tokens.completion",
"model", model).increment(usage.getGenerationTokens());
}
return result;
} catch (Exception e) {
meterRegistry.counter("dashscope.chat.errors",
"model", model,
"error", e.getClass().getSimpleName()
).increment();
throw e;
}
}
}最后说几句
Spring AI Alibaba 整体上很好用,把通义千问系列接进 Spring 生态的工作量比直接用原生 SDK 低很多。但有几点要记住:
第一,版本对齐很重要,Spring AI 还在快速迭代,BOM 版本管理必须做。
第二,通义千问不同型号的能力差异很大,别用 qwen-max 干 qwen-turbo 能干的活,成本差十倍以上。
第三,Function Calling 的工具描述质量直接影响调用成功率,值得花时间打磨。
第四,生产环境必须加重试、限流和监控,不然哪天 API 抖一下,你的服务就跟着抖了。
下一篇聊 DeepSeek 的接入,那个模型有意思,推理能力很强但接入方式有些特别。
