第一章 概述

1.1 项目定位与价值

Spring AI Alibaba Chat Example 是 Spring AI Alibaba 生态下的官方示例项目,核心目标是演示如何通过 Spring AI 的统一抽象层,以最小代码差异接入多种大语言模型(LLM)服务。它不仅是学习 Spring AI 框架的绝佳入口,也是企业级多模型接入架构的参考实现。

核心价值体现

  • 供应商无关性:通过 ChatModel / ChatClient 抽象,切换底层模型只需改配置,无需改动业务代码
  • Spring 原生体验:完全遵循 Spring Boot 的自动配置(AutoConfiguration)和依赖注入(DI)范式
  • 响应式支持:原生支持同步阻塞调用与异步流式(Streaming)输出
  • 功能完备:覆盖简单对话、流式对话、Function Calling、RAG 等典型 AI 应用场景

1.2 技术栈全景

层级 技术选型 说明
基础框架 Spring Boot 3.x 基于 Jakarta EE namespace,要求 Java 17+
AI 抽象层 Spring AI + Spring AI Alibaba Spring AI 提供跨供应商抽象;Spring AI Alibaba 提供阿里云 DashScope、通义千问等适配
构建工具 Maven 多模块 父 POM 统一管理依赖版本,子模块独立打包
响应式编程 Project Reactor (Flux/Mono) 流式输出基于 Reactor 的背压感知流
通信协议 HTTP/REST + SSE 非流式用标准 REST;流式用 Server-Sent Events
本地推理 Ollama / vLLM 支持私有化部署,数据不出域

1.3 项目结构

spring-ai-alibaba-chat-example/
├── pom.xml                          # 父 POM:定义依赖版本、子模块聚合
├── README.md
├── common/                          # 【建议新增】公共模块,存放统一响应、异常处理、工具类
├── dashscope-chat/                  # 阿里云 DashScope(通义千问 qwen 系列)
├── ollama-chat/                     # Ollama 本地模型(Llama、Qwen 等)
├── openai-chat/                     # OpenAI GPT 系列
├── zhipuai-chat/                    # 智谱 AI(GLM 系列)⚠️ 非通义千问
├── deepseek-chat/                   # DeepSeek V3 / R1
├── vllm-chat/                       # vLLM 推理引擎(OpenAI 兼容 API)
├── azure-openai-chat/               # Azure OpenAI Service
├── minimax-chat/                    # MiniMax 大模型
└── qwq-chat/                        # 通义千问 QwQ 推理模型(阿里)

重要澄清zhipuai-chat 对应的是**智谱 AI(Zhipu AI)**的 GLM 系列模型,而非通义千问。通义千问(Qwen)是阿里云的产品,对应 dashscope-chatqwq-chat 模块。


第二章 核心原理:Spring AI 抽象层

2.1 Spring AI

在传统的 LLM 接入方式中,每接入一个新的模型供应商,开发者都需要:

  1. 学习该供应商的 SDK 和 API 规范
  2. 编写大量的 HTTP 调用和 JSON 解析代码
  3. 处理鉴权、重试、超时等横切关注点
  4. 当需要切换模型时,大量业务代码需要重写

Spring AI 的核心设计目标是消除这种"供应商锁定"。它借鉴了 Spring 框架在 JDBC、JMS、Cache 等领域的成功经验,为 AI 领域提供了一套统一的编程模型

2.2 核心抽象:从 ChatModel 到 ChatClient

Spring AI 提供了两层核心抽象,原文仅介绍了 ChatModel,遗漏了更为重要的 ChatClient

调用方式

具体实现

Spring AI 核心抽象层

封装

构建

包含

可选包含

同步

流式

ChatModel
底层通信接口

ChatClient
高层 Fluent API

Prompt
提示词对象

UserMessage / SystemMessage

FunctionCallback
工具定义

DashScopeChatModel

OpenAiChatModel

OllamaChatModel

ZhiPuAiChatModel

DeepSeekChatModel

call&(&) → ChatResponse

stream&(&) → Flux<ChatResponse>

2.2.1 ChatModel:底层通信接口

ChatModel 是 Spring AI 最底层的抽象,直接面向 LLM 的 HTTP API:

public interface ChatModel extends Model<Prompt, ChatResponse> {
    // 继承自 Model 接口
    // ChatResponse call(Prompt prompt);
    // Flux<ChatResponse> stream(Prompt prompt);
}

每个模型供应商提供自己的实现类:

  • DashScopeChatModel → 调用阿里云 DashScope HTTP API
  • OpenAiChatModel → 调用 OpenAI HTTP API
  • OllamaChatModel → 调用本地 Ollama HTTP API(默认 localhost:11434)
2.2.2 ChatClient:高层 Fluent API(原文缺失)

ChatClient 是 Spring AI 推荐的主流使用方式,它基于 ChatModel 提供了更流畅的 API:

// 构建 ChatClient(通常在 @Configuration 中配置一次)
@Bean
ChatClient chatClient(ChatModel chatModel) {
    return ChatClient.builder(chatModel)
        .defaultSystem("你是一个专业的技术助手,用中文回答。")  // 默认系统提示
        .defaultOptions(DashScopeChatOptions.builder()
            .withModel("qwen-max")
            .withTemperature(0.7)
            .build())
        .build();
}

// 在 Controller 中使用
@RestController
public class ChatController {
    private final ChatClient chatClient;

    @GetMapping("/chat")
    public String chat(@RequestParam String message) {
        return chatClient.prompt()
            .user(message)           // 用户消息
            .call()                  // 执行调用
            .content();              // 提取文本内容
    }

    @GetMapping(value = "/chat/stream", produces = MediaType.TEXT_EVENT_STREAM_VALUE)
    public Flux<String> streamChat(@RequestParam String message) {
        return chatClient.prompt()
            .user(message)
            .stream()                // 流式调用
            .content();              // Flux<String>
    }
}

ChatClient 的优势

  • 链式调用:代码可读性极高
  • 默认配置:在构建时设置系统提示、默认参数,避免每次调用重复设置
  • 功能注册:可统一注册 Function Calling 工具(见第四章)
  • Advisor 机制:支持日志、重试、RAG 等横切逻辑的 AOP 式插入

2.3 AutoConfiguration 自动装配机制

Spring Boot 的自动配置是本项目"零代码切换模型"的魔法所在:

Controller Bean 装配 @Conditional 评估 Classpath 扫描 Spring Boot 启动 Controller Bean 装配 @Conditional 评估 Classpath 扫描 Spring Boot 启动 alt [dashscope-spring-ai-starter 存在] [ollama-spring-ai-starter 存在] [openai-spring-ai-starter 存在] 扫描 classpath 中的 starter 检查 @ConditionalOnProperty spring.ai.dashscope.api-key 条件满足 → 创建 DashScopeChatModel 检查 @ConditionalOnProperty spring.ai.ollama.base-url 条件满足 → 创建 OllamaChatModel 检查 @ConditionalOnProperty spring.ai.openai.api-key 条件满足 → 创建 OpenAiChatModel 通过构造函数注入 ChatModel/ChatClient HTTP 服务就绪

关键源码示意(以 DashScope 为例):

@AutoConfiguration
@ConditionalOnClass(DashScopeApi.class)
@EnableConfigurationProperties(DashScopeConnectionProperties.class)
public class DashScopeAutoConfiguration {

    @Bean
    @ConditionalOnMissingBean
    @ConditionalOnProperty(prefix = "spring.ai.dashscope", name = "api-key")
    public DashScopeChatModel dashScopeChatModel(
            DashScopeConnectionProperties connectionProperties,
            ObjectProvider<DashScopeChatOptions> chatOptions) {

        var dashScopeApi = new DashScopeApi(connectionProperties.getApiKey());
        return new DashScopeChatModel(dashScopeApi, chatOptions.getIfAvailable());
    }
}

这就是为什么每个子模块只需引入对应的 starter 依赖,无需编写任何配置类,就能自动获得可用的 ChatModel Bean。

2.4 配置优先级体系

Spring AI 的配置遵循 Spring Boot 的标准优先级(从高到低):

运行时编程配置
ChatOptions.builder&(&)

ChatClient 默认配置
.defaultOptions&(&)

application.yml 配置
spring.ai.xxx.chat.options

框架硬编码默认值

优先级 配置方式 示例 适用场景
1(最高) 运行时编程配置 DashScopeChatOptions.builder().withTemperature(0.9).build() 单次调用的特殊参数
2 ChatClient 默认配置 .defaultOptions(...) 该 Client 实例的全局默认参数
3 application.yml spring.ai.dashscope.chat.options.temperature=0.7 应用级别的默认参数
4(最低) 框架默认值 temperature=0.8 兜底配置

第三章 调用流程深度解析

3.1 非流式调用(Synchronous)

非流式调用是最简单的使用方式,适合短文本问答场景:

LLM 服务端 (DashScope/OpenAI等) HTTP 客户端库 (RestClient/WebClient) ChatModel (如 DashScopeChatModel) ChatClient ChatController HTTP 客户端 LLM 服务端 (DashScope/OpenAI等) HTTP 客户端库 (RestClient/WebClient) ChatModel (如 DashScopeChatModel) ChatClient ChatController HTTP 客户端 POST /chat {message: "你好"} 1 chatClient.prompt().user(message).call() 2 chatModel.call(new Prompt(messages, options)) 3 构建供应商特定请求体 (如 DashScopeRequest) 4 发送同步 HTTP POST 5 HTTP Request 6 HTTP Response (完整 JSON) 7 原始响应字节流 8 解析 JSON → 封装为 ChatResponse 9 ChatResponse 对象 10 提取 content 字符串 11 JSON 响应 {data: "你好!很高兴为你服务。"} 12

关键要点

  1. 同步阻塞:调用线程从 call() 开始阻塞,直到收到完整 HTTP 响应
  2. 统一封装:无论底层是 OpenAI 的 JSON 格式还是 DashScope 的格式,最终都转换为 ChatResponse
  3. 内容提取链ChatResponse → Generation → AssistantMessage → String
// 底层的内容提取路径(了解即可,实际使用 ChatClient 无需这么繁琐)
ChatResponse response = chatModel.call(new Prompt("你好"));
String text = response.getResult().getOutput().getContent();

3.2 流式调用(Streaming)原理

流式调用是 LLM 应用的核心体验优化手段,它基于 Server-Sent Events (SSE) 协议实现:

3.2.1 SSE 协议详解

SSE 是 HTML5 标准的一部分,基于 HTTP 长连接,允许服务器向客户端单向推送文本流:

HTTP/1.1 200 OK
Content-Type: text/event-stream
Cache-Control: no-cache
Connection: keep-alive

data: {"choices":[{"delta":{"content":"你"}}]}

data: {"choices":[{"delta":{"content":"好"}}]}

data: {"choices":[{"delta":{"content":"!"}}]}

data: [DONE]

SSE 的核心特征

  • 基于标准 HTTP,无需 WebSocket 握手,穿透性更好
  • 文本格式简单,以 data: 开头,以两个换行符分隔事件
  • 天然支持断线重连(通过 Last-Event-ID 头)
  • 单向流:服务器 → 客户端,适合 LLM 文本生成场景
3.2.2 Reactor Flux 流式处理

Spring AI 使用 Project Reactor 的 Flux 将 SSE 事件转换为响应式流:

☁️ LLM服务端 🤖 ChatModel ⚡ Flux流处理器 🎯 ChatController 🖥️ 客户端 (浏览器/App) ☁️ LLM服务端 🤖 ChatModel ⚡ Flux流处理器 🎯 ChatController 🖥️ 客户端 (浏览器/App) 1. 客户端发起流式请求 2. 流式响应处理 loop [流式数据传输] 3. 流结束处理 GET /stream/chat?message=你好 1 chatModel.stream(prompt) 2 🔗 建立HTTP长连接 📤 发送流式请求 3 📡 SSE: data: "你" 4 onNext(ChatResponse) 5 emit(ChatResponse) 6 data: "你" 7 📡 SSE: data: "好" 8 onNext(ChatResponse) 9 emit(ChatResponse) 10 data: "好" 11 🛑 SSE: [DONE] 12 onComplete() 13 流完成信号 14 关闭连接 15 流式聊天API调用序列图

Controller 实现

@GetMapping(value = "/stream/chat", produces = MediaType.TEXT_EVENT_STREAM_VALUE)
public Flux<String> streamChat(@RequestParam String message, 
                                HttpServletResponse response) {
    // 强制 UTF-8 编码,避免中文乱码
    response.setCharacterEncoding("UTF-8");

    return chatClient.prompt()
        .user(message)
        .stream()
        .content()
        .doOnNext(token -> System.out.print(token))  // 日志观察
        .doOnError(e -> log.error("流式调用异常", e))
        .onErrorResume(e -> Flux.just("[服务异常,请稍后重试]"));
}
3.2.3 流式 vs 非流式对比
维度 非流式 (call) 流式 (stream)
响应方式 一次性返回完整结果 逐 Token 推送
首字节延迟 (TTFB) 高(等待全部生成) 极低(毫秒级可见首字)
用户体验 等待 Loading,体验差 实时打字机效果,体验佳
连接占用 短连接,快速释放 长连接,持续占用
错误处理 全有或全无 可能部分成功,需处理断流
适用场景 后台任务、短文本 对话 UI、长文本生成
实现复杂度 中(需处理 SSE、Flux、背压)

3.3 响应式背压(Backpressure)机制

背压是响应式编程的核心概念:

对比场景

✅ 有背压控制
Reactive Stream 模式

📥 request(n)
请求n个元素

📤 onNext()
发送1个元素

📊 动态调节
request(1)或request(n)

🔄 Flux 生产者
响应式流

📱 Reactor 消费者
主动请求

❌ 无背压控制
传统发布-订阅模式

⚡ 过量数据

💥 OOM/数据丢失

🧠 LLM
高速生成

💻 客户端
慢速消费

背压的意义:当 LLM 生成速度远快于客户端渲染速度时,Reactor 的背压机制会自动调节流速,避免内存溢出。Spring AI 的 stream() 方法返回的 Flux 天然支持背压。


第四章 高级特性原理

4.1 Function Calling(工具调用)

Function Calling 是 LLM 与外部世界交互的桥梁,允许模型在生成回答的过程中"暂停",调用外部函数获取实时数据,然后继续生成。

4.1.1 完整工作流程
天气 API 大语言模型 Spring AI 应用 用户 天气 API 大语言模型 Spring AI 应用 用户 "北京今天天气怎么样?" 1 构建 Prompt,附加可用函数定义 [getCurrentWeather: 参数{city: string}] 2 发送 Prompt + Functions 3 分析:需要天气数据 → 生成函数调用请求 4 FunctionCall 对象 {name: "getCurrentWeather", arguments: {city: "北京"}} 5 调用 getCurrentWeather("北京") 6 {temperature: 22, condition: "晴"} 7 将函数结果追加到对话历史 8 再次发送(含函数结果) 9 基于函数结果生成自然语言回答 10 "北京今天天气晴朗,气温 22°C。" 11 返回最终回答 12
4.1.2 Spring AI 实现方式

Spring AI 提供了声明式的 Function Calling 支持:

// Step 1: 定义函数(普通的 Spring Bean)
@Component
@Description("获取指定城市的当前天气信息")  // 描述会被转换为 LLM 的 function schema
public class WeatherService implements Function<WeatherService.Request, WeatherService.Response> {

    public record Request(@Description("城市名称,如:北京、上海") String city) {}
    public record Response(String temperature, String condition, String humidity) {}

    @Override
    public Response apply(Request request) {
        // 实际调用天气 API
        return new Response("22°C", "晴", "45%");
    }
}

// Step 2: 在 ChatClient 中注册(推荐方式)
@Bean
ChatClient chatClient(ChatModel chatModel, WeatherService weatherService) {
    return ChatClient.builder(chatModel)
        .defaultFunctions(weatherService)  // 注册函数
        .build();
}

// Step 3: 使用(完全无感知,ChatClient 自动处理函数调用循环)
@GetMapping("/chat")
public String chat(@RequestParam String message) {
    return chatClient.prompt()
        .user(message)
        .call()
        .content();  // 如果 LLM 决定调用函数,ChatClient 会自动执行并重新调用 LLM
}

关键原理

  • @Description 注解的内容会被转换为 OpenAI 格式的 function 定义,作为 Prompt 的一部分发送给 LLM
  • LLM 不会直接执行函数,而是返回一个"调用意图"(Function Call)
  • Spring AI 的 ChatClient 会自动解析意图、执行函数、将结果回填、再次调用 LLM——这个循环对开发者完全透明

4.2 RAG(检索增强生成)

RAG 解决的是 LLM "知识截止"和"幻觉"问题,通过将外部知识库作为上下文注入 Prompt。

4.2.1 RAG 完整流程

在线阶段:检索生成

离线阶段:知识库构建

原始文档
PDF/Word/TXT

文档加载
DocumentReader

文本分割
TextSplitter

文本块
Document Chunks

向量化
EmbeddingModel

向量存储
VectorStore
Redis/Milvus/PGVector

用户提问

问题向量化
EmbeddingModel

相似度搜索
VectorStore.similaritySearch

Top-K 相关文档块

Prompt 组装
System: 你是助手
Context: 文档块
User: 问题

LLM 生成回答

4.2.2 Spring AI RAG 实现
@Service
public class RagService {
    private final ChatClient chatClient;
    private final VectorStore vectorStore;

    // 1. 初始化:加载文档到向量库(通常只在应用启动时执行一次)
    @PostConstruct
    public void init() {
        // 读取文档
        Resource resource = new ClassPathResource("docs/terms-of-service.txt");

        // 分割文本(按 Token 数或字符数)
        TextSplitter splitter = new TokenTextSplitter(500, 100);  // chunkSize=500, chunkOverlap=100
        List<Document> documents = splitter.split(new Document(resource));

        // 嵌入并存储
        vectorStore.add(documents);
    }

    // 2. 检索并回答
    public String ask(String question) {
        // 检索最相关的 3 个文档块
        List<Document> relevantDocs = vectorStore.similaritySearch(
            SearchRequest.query(question).withTopK(3));

        // 组装上下文
        String context = relevantDocs.stream()
            .map(Document::getContent)
            .collect(Collectors.joining("\n\n"));

        // 调用 LLM
        return chatClient.prompt()
            .system("基于以下上下文回答问题,如果上下文中没有相关信息,请明确告知。\n\n上下文:\n" + context)
            .user(question)
            .call()
            .content();
    }
}
4.2.3 RAG vs Function Calling 对比
特性 RAG Function Calling
数据来源 静态知识库(文档、历史数据) 实时 API / 数据库 / 外部服务
数据更新 低频(重新索引文档) 高频(每次调用实时查询)
技术依赖 向量数据库 + Embedding 模型 普通 Java 函数
延迟 低(本地向量检索) 中(依赖外部 API 延迟)
幻觉控制 强(有文档约束) 强(有函数返回结果约束)
典型场景 知识库问答、文档摘要、客服 天气查询、订单查询、实时计算

4.3 多模态能力

Spring AI Alibaba 支持多模态输入(文本 + 图片),以 DashScope 的 Qwen-VL 为例:

@GetMapping("/chat/image")
public String chatWithImage(@RequestParam String message,
                             @RequestParam String imageUrl) {
    return chatClient.prompt()
        .user(userSpec -> userSpec
            .text(message)
            .media(MimeTypeUtils.IMAGE_JPEG, new UrlResource(imageUrl)))
        .call()
        .content();
}

第五章 各模型差异化配置原理

5.1 配置命名空间总览

不同模型使用不同的 spring.ai.* 配置前缀,这是最容易混淆的地方:

🎯 Spring AI 配置前缀映射表

🔗 配置前缀

📦 自托管模型

☁️ 云服务商模型

兼容API

兼容API

📋 spring.ai.dashscope.*

🧩 通义千问
(DashScope)

🤖 OpenAI

📋 spring.ai.openai.*

☁️ Azure OpenAI

📋 spring.ai.azure.openai.*

🧠 智谱 AI

📋 spring.ai.zhipuai.*

💎 MiniMax

📋 spring.ai.minimax.*

🐳 Ollama

📋 spring.ai.ollama.*

🔍 DeepSeek

🚀 vLLM

5.2 各模型详细配置

5.2.1 DashScope / 通义千问(阿里云)
spring:
  ai:
    dashscope:
      api-key: ${DASHSCOPE_API_KEY}  # 从环境变量读取,禁止硬编码
      chat:
        options:
          model: qwen-max           # 可选: qwen-max, qwen-plus, qwen-turbo, qwen-coder
          temperature: 0.7          # 创造性: 0-2,越低越确定
          max-tokens: 2000          # 最大生成 Token 数

特点

  • 商业 API,按 Token 计费,稳定性高
  • 支持 Function Calling、RAG、多模态
  • qwq-chat 模块使用 qwen-qwq 模型,专为推理优化(类似 DeepSeek-R1 的推理链)
5.2.2 Ollama(本地私有化)
spring:
  ai:
    ollama:
      base-url: http://localhost:11434  # Ollama 服务地址
      chat:
        options:
          model: qwen2.5:7b              # 需先执行 ollama pull qwen2.5:7b
          temperature: 0.8

特点

  • 数据完全不出本机,隐私性最强
  • 需要本地 GPU/CPU 资源,性能取决于硬件
  • 适合涉密场景或网络受限环境
5.2.3 OpenAI
spring:
  ai:
    openai:
      api-key: ${OPENAI_API_KEY}
      base-url: https://api.openai.com/v1  # 默认,可替换为代理地址
      chat:
        options:
          model: gpt-4o
          temperature: 0.7
5.2.4 智谱 AI(GLM 系列)⚠️ 纠正原文错误
spring:
  ai:
    zhipuai:
      api-key: ${ZHIPUAI_API_KEY}
      chat:
        options:
          model: glm-4                   # GLM-4、GLM-4-Flash、GLM-4V 等
          temperature: 0.7

:智谱 AI(Zhipu AI)的模型是 GLM 系列(General Language Model),由清华大学和智谱 AI 联合研发。

5.2.5 DeepSeek
spring:
  ai:
    openai:  # ⚠️ DeepSeek 兼容 OpenAI API 格式
      api-key: ${DEEPSEEK_API_KEY}
      base-url: https://api.deepseek.com/v1
      chat:
        options:
          model: deepseek-chat          # 或 deepseek-reasoner(R1)
5.2.6 vLLM(自托管推理引擎)
spring:
  ai:
    openai:  # ⚠️ vLLM 提供 OpenAI 兼容接口
      api-key: unused                   # vLLM 通常无需鉴权
      base-url: http://localhost:8000/v1
      chat:
        options:
          model: Qwen/Qwen2.5-7B-Instruct

重要说明:vLLM 本身是一个高性能推理引擎(PagedAttention 技术),不是模型。它通过启动时加载的模型来提供服务。Spring AI 没有专门的 vllm 配置前缀,因为它使用 OpenAI 兼容协议。


第六章 架构设计分析

6.1 多模块架构的权衡

6.1.1 当前架构设计

当前架构:多独立模块

父 POM
spring-ai-alibaba-chat-example

dashscope-chat
port: 10000

ollama-chat
port: 10005

openai-chat
port: 10010

zhipuai-chat
port: 10015

deepseek-chat
port: 10020

vllm-chat
port: 10025

minimax-chat
port: 10030

qwq-chat
port: 10035

azure-openai-chat
port: 10040

6.1.2 架构优劣分析
维度 优点 缺点
耦合度 模块间零耦合,互不影响 公共逻辑无法复用,代码大量重复
部署 可独立部署和扩容 每个模块都包含完整的 Spring Boot 容器,资源浪费
依赖 依赖完全隔离,无版本冲突 升级 Spring Boot / Spring AI 版本需修改 9 个 pom.xml
学习 每个模块独立可运行,适合学习 生产环境管理 9 个进程/容器,运维复杂
扩展 新增模型只需新增模块 新增模型需复制粘贴大量样板代码

核心矛盾:当前架构是示例项目的最优解(清晰、独立、易于学习),但不是生产环境的最优解。

6.2 代码重复问题深度分析

几乎所有模块的 Controller 都是如下模式的复制粘贴:

// dashscope-chat / ollama-chat / openai-chat ... 几乎一模一样
@RestController
@RequestMapping("/model")
public class XxxChatController {
    private final ChatModel xxxChatModel;  // 或 ChatClient

    @GetMapping("/simple/chat")
    public String simpleChat() {
        return xxxChatModel.call(new Prompt(DEFAULT_PROMPT))
            .getResult().getOutput().getContent();
    }

    @GetMapping("/stream/chat")
    public Flux<String> streamChat(HttpServletResponse response) {
        response.setCharacterEncoding("UTF-8");
        return xxxChatModel.stream(new Prompt(DEFAULT_PROMPT))
            .map(r -> r.getResult().getOutput().getContent());
    }
}

重复代码统计(估算):

  • Controller 层:9 个模块 × 80% 相似代码 ≈ 70% 的 Controller 代码是重复的
  • application.yml:端口和名称不同,结构完全相同
  • pom.xml:依赖结构高度相似

第七章 部署操作指南

7.1 环境准备

7.1.1 系统要求
项目 最低配置 推荐配置
操作系统 Linux (Ubuntu 20.04+) / macOS / Windows WSL2 Linux (Ubuntu 22.04 LTS)
JDK OpenJDK 17 OpenJDK 17/21 LTS
Maven 3.8.x 3.9.x
内存 4GB(仅运行单个模块) 8GB+(多模块并行)
磁盘 10GB 20GB+(含 Ollama 模型)
7.1.2 环境变量配置

强烈建议将所有 API Key 配置到环境变量,禁止写入代码仓库:

# ~/.bashrc 或 ~/.zshrc
export AI_DASHSCOPE_API_KEY="sk-xxxxxxxx"
export OPENAI_API_KEY="sk-xxxxxxxx"
export ZHIPUAI_API_KEY="xxxxxxxx.xxxxxxxx"  # 智谱 AI
export DEEPSEEK_API_KEY="sk-xxxxxxxx"
export MINIMAX_API_KEY="xxxxxxxx"
export AZURE_OPENAI_API_KEY="xxxxxxxx"
export AZURE_OPENAI_ENDPOINT="https://xxx.openai.azure.com/"

source ~/.bashrc

7.2 构建与启动

7.2.1 全量构建
cd spring-ai-alibaba-chat-example

# 编译所有模块
mvn clean compile -DskipTests

# 打包(生成可执行 JAR)
mvn clean package -DskipTests
7.2.2 单模块构建与启动
# 构建指定模块
cd dashscope-chat
mvn clean package -DskipTests

# 启动(通过命令行参数覆盖端口和 API Key)
java -jar target/dashscope-chat-*.jar   --server.port=10000   --spring.ai.dashscope.api-key=${AI_DASHSCOPE_API_KEY}
7.2.3 多模块并行启动脚本
#!/bin/bash
# start-all.sh
BASE_DIR="$(cd "$(dirname "$0")" && pwd)"
LOG_DIR="$BASE_DIR/logs"
mkdir -p "$LOG_DIR"

declare -A MODULES=(
    [dashscope-chat]=10000
    [ollama-chat]=10005
    [openai-chat]=10010
    [zhipuai-chat]=10015
    [deepseek-chat]=10020
    [vllm-chat]=10025
    [minimax-chat]=10030
    [qwq-chat]=10035
    [azure-openai-chat]=10040
)

for MODULE in "${!MODULES[@]}"; do
    PORT=${MODULES[$MODULE]}
    JAR=$(ls "$BASE_DIR/$MODULE/target/"*.jar 2>/dev/null | head -1)

    if [ -z "$JAR" ]; then
        echo "⚠️  $MODULE JAR 未找到,跳过"
        continue
    fi

    echo "🚀 启动 $MODULE → http://localhost:$PORT"
    nohup java -jar "$JAR" --server.port=$PORT > "$LOG_DIR/$MODULE.log" 2>&1 &
    sleep 2
done

echo "✅ 所有模块已启动!日志目录: $LOG_DIR"
echo "📋 查看状态: ps aux | grep java"
echo "🛑 停止全部: pkill -f 'spring-ai-alibaba-chat'"
chmod +x start-all.sh
./start-all.sh

7.3 验证部署

# 测试非流式接口
curl "http://localhost:10000/model/simple/chat"

# 测试流式接口(SSE)
curl -N "http://localhost:10000/model/stream/chat"

# 测试带参数的接口
curl "http://localhost:10000/model/simple/chat?prompt=你好"

7.4 Ollama 本地模型部署

# 1. 安装 Ollama
curl -fsSL https://ollama.com/install.sh | sh

# 2. 拉取模型
ollama pull qwen2.5:7b      # 通义千问 7B,中文表现优秀
ollama pull llama3.2:3b     # Meta Llama 3.2,轻量快速
ollama pull deepseek-r1:7b  # DeepSeek R1 推理模型

# 3. 验证
ollama run qwen2.5:7b "请用中文自我介绍"

# 4. 配置 application.yml
# spring.ai.ollama.base-url=http://localhost:11434
# spring.ai.ollama.chat.options.model=qwen2.5:7b

7.5 Docker 部署

7.5.1 单模块 Dockerfile
# 多阶段构建,减小镜像体积
FROM maven:3.9-eclipse-temurin-17-alpine AS builder
WORKDIR /build
COPY pom.xml .
COPY src ./src
RUN mvn clean package -DskipTests

FROM eclipse-temurin:17-jre-alpine
WORKDIR /app
COPY --from=builder /build/target/*.jar app.jar
EXPOSE 8080
ENTRYPOINT ["java", "-jar", "app.jar"]
7.5.2 Docker Compose 编排(含 Ollama)
version: "3.8"

services:
  dashscope-chat:
    build: ./dashscope-chat
    ports:
      - "10000:8080"
    environment:
      - SPRING_AI_DASHSCOPE_API_KEY=${AI_DASHSCOPE_API_KEY}
    restart: unless-stopped

  ollama-chat:
    build: ./ollama-chat
    ports:
      - "10005:8080"
    environment:
      - SPRING_AI_OLLAMA_BASE_URL=http://ollama:11434
    depends_on:
      - ollama
    restart: unless-stopped

  ollama:
    image: ollama/ollama:latest
    ports:
      - "11434:11434"
    volumes:
      - ollama_data:/root/.ollama
    restart: unless-stopped

volumes:
  ollama_data:
docker compose up -d --build

第八章 现存问题诊断与改进方案

8.1 问题一:代码高度重复

诊断

9 个模块的 Controller 代码重复率超过 70%,违反 DRY 原则。任何接口变更(如增加参数校验、修改响应格式)都需要修改 9 个文件。

优化方案:提取公共模块 + 策略模式

优化后架构

common-chat 模块
公共代码

AbstractChatController
统一端点定义

统一响应封装
ApiResponse

全局异常处理
GlobalExceptionHandler

ChatModelFactory
模型路由

dashscope-chat

ollama-chat

openai-chat

...其他模块

实现代码

// ========== common-chat 模块 ==========

// 1. 统一响应封装
public record ApiResponse<T>(int code, String message, T data) {
    public static <T> ApiResponse<T> success(T data) {
        return new ApiResponse<>(200, "success", data);
    }
    public static <T> ApiResponse<T> error(int code, String message) {
        return new ApiResponse<>(code, message, null);
    }
}

// 2. 全局异常处理
@RestControllerAdvice
public class GlobalExceptionHandler {
    @ExceptionHandler(IllegalArgumentException.class)
    public ApiResponse<Void> handleIllegalArg(IllegalArgumentException e) {
        return ApiResponse.error(400, e.getMessage());
    }

    @ExceptionHandler(ChatModelException.class)
    public ApiResponse<Void> handleChatModel(ChatModelException e) {
        return ApiResponse.error(500, "模型调用失败: " + e.getMessage());
    }

    @ExceptionHandler(RateLimitException.class)
    public ResponseEntity<ApiResponse<Void>> handleRateLimit(RateLimitException e) {
        return ResponseEntity.status(HttpStatus.TOO_MANY_REQUESTS)
            .header("Retry-After", "60")
            .body(ApiResponse.error(429, "请求过于频繁,请稍后重试"));
    }
}

// 3. 抽象 Controller
public abstract class AbstractChatController {
    protected abstract ChatClient getChatClient();

    @GetMapping("/simple/chat")
    public ApiResponse<String> simpleChat(@RequestParam String prompt) {
        if (StringUtils.isBlank(prompt)) {
            throw new IllegalArgumentException("prompt 不能为空");
        }
        String response = getChatClient().prompt().user(prompt).call().content();
        return ApiResponse.success(response);
    }

    @GetMapping(value = "/stream/chat", produces = MediaType.TEXT_EVENT_STREAM_VALUE)
    public Flux<String> streamChat(@RequestParam String prompt) {
        if (StringUtils.isBlank(prompt)) {
            return Flux.error(new IllegalArgumentException("prompt 不能为空"));
        }
        return getChatClient().prompt().user(prompt).stream().content();
    }
}

// 4. 模型工厂(可选:用于统一服务架构)
@Component
public class ChatModelFactory {
    private final Map<String, ChatClient> clients = new ConcurrentHashMap<>();

    public ChatClient getClient(String model) {
        return clients.get(model);
    }

    public void register(String name, ChatClient client) {
        clients.put(name, client);
    }
}

// ========== dashscope-chat 模块(精简后)==========
@RestController
@RequestMapping("/dashscope")
public class DashScopeChatController extends AbstractChatController {
    private final ChatClient dashScopeChatClient;

    @Override
    protected ChatClient getChatClient() {
        return dashScopeChatClient;
    }
}

8.2 问题二:端口冲突与管理混乱

诊断

原文前后矛盾:前面说各模块使用不同端口,后面又说"所有模块默认都使用 8080"。实际情况通常是每个模块的 application.yml 中硬编码了不同端口,但缺乏统一规划。

优化方案

方案 A:固定端口规划表(当前阶段的务实选择)

在父 POM 或文档中明确定义端口矩阵,并在各模块 application.yml 中显式声明:

# dashscope-chat/src/main/resources/application.yml
server:
  port: 10000

方案 B:动态端口(微服务化过渡)

server:
  port: ${SERVER_PORT:0}  # 0 表示随机端口,配合服务发现使用

方案 C:统一服务 + API 网关(长期演进目标)

/api/dashscope/*

/api/ollama/*

/api/openai/*

路由

路由

路由

客户端

API Gateway
Nginx/Spring Cloud Gateway

统一 Chat Service

DashScopeChatModel

OllamaChatModel

OpenAiChatModel

@RestController
@RequestMapping("/api/v1")
public class UnifiedChatController {
    private final ChatModelFactory factory;

    @GetMapping("/{model}/chat")
    public ApiResponse<String> chat(@PathVariable String model,
                                     @RequestParam String prompt) {
        ChatClient client = factory.getClient(model);
        if (client == null) {
            throw new IllegalArgumentException("不支持的模型: " + model);
        }
        return ApiResponse.success(client.prompt().user(prompt).call().content());
    }
}

8.3 问题三:配置安全风险

诊断
  • API Key 可能通过 application.yml 误提交到 Git
  • 缺少请求频率限制,存在被刷接口的风险
  • 没有认证鉴权,任何人都可以调用
优化方案
# application.yml(安全强化版)
spring:
  ai:
    dashscope:
      api-key: ${AI_DASHSCOPE_API_KEY}  # 必须从环境变量读取

# 接口限流(使用 Bucket4j 或 Sentinel)
ai:
  rate-limit:
    enabled: true
    capacity: 100        # 令牌桶容量
    refill-rate: 10      # 每秒补充令牌数
// API Key 鉴权过滤器(简易版)
@Component
public class ApiKeyAuthFilter implements Filter {
    @Value("${app.api-key:}")
    private String expectedApiKey;

    @Override
    public void doFilter(ServletRequest request, ServletResponse response, 
                         FilterChain chain) throws IOException, ServletException {
        HttpServletRequest req = (HttpServletRequest) request;
        String apiKey = req.getHeader("X-API-Key");

        if (!expectedApiKey.equals(apiKey)) {
            HttpServletResponse resp = (HttpServletResponse) response;
            resp.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
            resp.getWriter().write("{"error":"Unauthorized"}");
            return;
        }
        chain.doFilter(request, response);
    }
}

8.4 问题四:错误处理缺失

诊断
  • 空 prompt 没有校验
  • 模型调用超时没有处理
  • 异常直接抛出,前端收到 500 和堆栈信息
优化方案

已在 8.1 节的 GlobalExceptionHandler 中展示。补充超时控制:

@GetMapping("/stream/chat")
public Flux<String> streamChat(@RequestParam String prompt) {
    return chatClient.prompt()
        .user(prompt)
        .stream()
        .content()
        .timeout(Duration.ofSeconds(30))      // 30 秒超时
        .onErrorResume(TimeoutException.class, 
            e -> Flux.just("[请求超时,请稍后重试]"))
        .onErrorResume(e -> Flux.just("[服务异常: " + e.getMessage() + "]"));
}

8.5 问题五:依赖版本分散

诊断

每个子模块可能独立声明 Spring Boot 和 Spring AI 版本,升级时容易遗漏。

优化方案

在父 POM 中统一管理所有版本:

<!-- 父 pom.xml -->
<properties>
    <spring-boot.version>3.3.0</spring-boot.version>
    <spring-ai.version>1.0.0-M2</spring-ai.version>
    <spring-ai-alibaba.version>1.0.0-M2</spring-ai-alibaba.version>
</properties>

<dependencyManagement>
    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-dependencies</artifactId>
            <version>${spring-boot.version}</version>
            <type>pom</type>
            <scope>import</scope>
        </dependency>
        <dependency>
            <groupId>org.springframework.ai</groupId>
            <artifactId>spring-ai-bom</artifactId>
            <version>${spring-ai.version}</version>
            <type>pom</type>
            <scope>import</scope>
        </dependency>
    </dependencies>
</dependencyManagement>

子模块的 pom.xml 只需声明依赖,无需指定版本:

<dependencies>
    <dependency>
        <groupId>com.alibaba.cloud.ai</groupId>
        <artifactId>spring-ai-alibaba-starter-dashscope</artifactId>
        <!-- 版本从父 POM 继承 -->
    </dependency>
</dependencies>

第九章 架构演进路线图

9.1 演进阶段规划

问题

改进

改进

改进

阶段一:示例项目
当前状态

阶段二:公共抽象
短期目标

阶段三:统一服务
中期目标

阶段四:云原生平台
长期目标

代码重复
端口混乱
无公共模块

提取 common-chat
统一响应/异常/日志
固定端口规划

单服务多模型
API Gateway 统一入口
配置中心管理

K8s 部署
自动扩缩容
多租户隔离
插件化模型加载

9.2 阶段二:公共抽象(1-2 周)

目标:消除代码重复,提升可维护性。

关键动作

  1. 新建 common-chat 模块,存放 AbstractChatControllerApiResponseGlobalExceptionHandler
  2. 所有子模块继承抽象 Controller
  3. 在父 POM 中统一版本管理
  4. 制定端口规划表并落实到各模块 application.yml

9.3 阶段三:统一服务(1 个月)

目标:从"多进程"演进为"单进程多模型"。

架构设计

统一服务架构

dashscope

ollama

openai

...

API Gateway
端口: 8080

认证鉴权

限流熔断

模型路由层

DashScopeChatModel

OllamaChatModel

OpenAiChatModel

其他模型

配置中心
Nacos/Apollo

优势

  • 一个 JAR 包运行,部署简单
  • 统一入口,便于监控和日志收集
  • 通过路由参数动态切换模型:POST /api/v1/chat?model=dashscope

9.4 阶段四:云原生平台(3 个月+)

目标:企业级 AI 中台。

关键特性

  • 插件化模型加载:模型以 SPI 插件形式动态加载,新增模型无需改代码、无需重启
  • 多租户隔离:不同租户使用不同的 API Key 配额和模型权限
  • 智能路由:根据模型负载、成本、延迟自动选择最优模型
  • 可观测性:集成 Prometheus + Grafana,监控 Token 消耗、延迟、错误率
// 插件化模型接口(长期演进)
public interface ModelPlugin {
    String getName();
    boolean isAvailable();
    ChatClient createClient(ModelConfig config);
}

@Component
public class PluginManager {
    private final Map<String, ModelPlugin> plugins = new ConcurrentHashMap<>();

    public void register(ModelPlugin plugin) {
        plugins.put(plugin.getName(), plugin);
    }

    public ChatClient getClient(String name) {
        return plugins.get(name).createClient(loadConfig(name));
    }
}

第十章 总结与最佳实践

10.1 架构优点(保持)

  1. 模块化清晰:每个模型独立示例,学习成本低
  2. 统一抽象:基于 Spring AI 的 ChatModel / ChatClient,切换模型成本极低
  3. 响应式原生:流式输出基于标准 SSE + Reactor,无额外依赖
  4. Spring 生态融合:自动配置、依赖注入、配置文件管理,符合 Spring 开发者习惯

10.2 架构缺点(改进)

  1. 代码重复率高:70%+ 的 Controller 代码重复
  2. 部署复杂:9 个独立进程,运维成本高
  3. 缺少公共基础设施:无统一异常处理、响应封装、日志规范
  4. 安全薄弱:无鉴权、无限流、API Key 管理粗放
  5. 配置分散:版本号、端口、参数分散在 9 个模块中

10.3 核心最佳实践

实践项 建议
API Key 管理 必须使用环境变量或配置中心,禁止硬编码
模型切换 优先使用 ChatClient 而非直接使用 ChatModel
流式输出 必须设置 produces = TEXT_EVENT_STREAM_VALUE 和 UTF-8 编码
异常处理 使用 @ControllerAdvice 统一处理,禁止暴露堆栈
Prompt 校验 所有入口参数必须校验非空和长度限制
超时控制 流式调用必须设置 .timeout(),防止长连接挂死
日志规范 记录模型名称、Token 消耗、响应时间,便于成本核算

10.4 关键概念纠正汇总

原文错误 纠正
ZhiPuAiChatModel 是"通义千问" 智谱 AI 是 GLM 系列,通义千问是阿里云的 Qwen 系列,两者完全不同
vLLM 有独立配置前缀 vLLM 使用 OpenAI 兼容 API,配置前缀为 spring.ai.openai
DeepSeek 有独立配置前缀 DeepSeek 使用 OpenAI 兼容 API,配置前缀为 spring.ai.openai
所有模块默认端口 8080 实际示例中各模块通常已分配不同端口,但缺乏统一规划
仅介绍了 ChatModel 实际应优先使用 ChatClient,它是更高级的抽象

结语:Spring AI Alibaba Chat Example 是一个优秀的学习项目,它清晰地展示了 Spring AI 的多模型接入能力。但作为生产架构,它需要在代码复用、统一治理、安全加固和部署简化等方面进行系统性演进。希望本文的解析与改进方案能为你的项目提供有价值的参考。

Logo

一站式 AI 云服务平台

更多推荐