【第20篇】Chat Example
本文介绍了Spring AI Alibaba Chat Example项目的技术架构与核心原理。该项目通过Spring AI统一抽象层实现多模型无缝接入,支持阿里云DashScope、Ollama等多种大语言模型服务。项目基于Spring Boot 3.x构建,采用分层设计:底层通过ChatModel接口对接不同供应商API,上层通过ChatClient提供流畅的编程接口,支持同步/异步调用和流式
第一章 概述
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-chat和qwq-chat模块。
第二章 核心原理:Spring AI 抽象层
2.1 Spring AI
在传统的 LLM 接入方式中,每接入一个新的模型供应商,开发者都需要:
- 学习该供应商的 SDK 和 API 规范
- 编写大量的 HTTP 调用和 JSON 解析代码
- 处理鉴权、重试、超时等横切关注点
- 当需要切换模型时,大量业务代码需要重写
Spring AI 的核心设计目标是消除这种"供应商锁定"。它借鉴了 Spring 框架在 JDBC、JMS、Cache 等领域的成功经验,为 AI 领域提供了一套统一的编程模型。
2.2 核心抽象:从 ChatModel 到 ChatClient
Spring AI 提供了两层核心抽象,原文仅介绍了 ChatModel,遗漏了更为重要的 ChatClient:
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 APIOpenAiChatModel→ 调用 OpenAI HTTP APIOllamaChatModel→ 调用本地 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 的自动配置是本项目"零代码切换模型"的魔法所在:
关键源码示意(以 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 的标准优先级(从高到低):
| 优先级 | 配置方式 | 示例 | 适用场景 |
|---|---|---|---|
| 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)
非流式调用是最简单的使用方式,适合短文本问答场景:
关键要点:
- 同步阻塞:调用线程从
call()开始阻塞,直到收到完整 HTTP 响应 - 统一封装:无论底层是 OpenAI 的 JSON 格式还是 DashScope 的格式,最终都转换为
ChatResponse - 内容提取链:
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 事件转换为响应式流:
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)机制
背压是响应式编程的核心概念:
背压的意义:当 LLM 生成速度远快于客户端渲染速度时,Reactor 的背压机制会自动调节流速,避免内存溢出。Spring AI 的 stream() 方法返回的 Flux 天然支持背压。
第四章 高级特性原理
4.1 Function Calling(工具调用)
Function Calling 是 LLM 与外部世界交互的桥梁,允许模型在生成回答的过程中"暂停",调用外部函数获取实时数据,然后继续生成。
4.1.1 完整工作流程
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 完整流程
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.* 配置前缀,这是最容易混淆的地方:
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 当前架构设计
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 模块 ==========
// 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 网关(长期演进目标)
@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 演进阶段规划
9.2 阶段二:公共抽象(1-2 周)
目标:消除代码重复,提升可维护性。
关键动作:
- 新建
common-chat模块,存放AbstractChatController、ApiResponse、GlobalExceptionHandler - 所有子模块继承抽象 Controller
- 在父 POM 中统一版本管理
- 制定端口规划表并落实到各模块
application.yml
9.3 阶段三:统一服务(1 个月)
目标:从"多进程"演进为"单进程多模型"。
架构设计:
优势:
- 一个 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 架构优点(保持)
- 模块化清晰:每个模型独立示例,学习成本低
- 统一抽象:基于 Spring AI 的
ChatModel/ChatClient,切换模型成本极低 - 响应式原生:流式输出基于标准 SSE + Reactor,无额外依赖
- Spring 生态融合:自动配置、依赖注入、配置文件管理,符合 Spring 开发者习惯
10.2 架构缺点(改进)
- 代码重复率高:70%+ 的 Controller 代码重复
- 部署复杂:9 个独立进程,运维成本高
- 缺少公共基础设施:无统一异常处理、响应封装、日志规范
- 安全薄弱:无鉴权、无限流、API Key 管理粗放
- 配置分散:版本号、端口、参数分散在 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 的多模型接入能力。但作为生产架构,它需要在代码复用、统一治理、安全加固和部署简化等方面进行系统性演进。希望本文的解析与改进方案能为你的项目提供有价值的参考。
更多推荐




所有评论(0)