Spring AI + Spring Boot:从 0 到 1 进化式搭建 LLM 多模型接入
面向 AI 应用开发新手 。目标不是一次画出完美架构,而是跟着做:
先跑通一个模型 → 能换模型 → 能抽象多厂商 → 能路由/降级 → 再谈智能选模 。
每一阶段都能独立验证,写完就能用。
手搓建立心智模型;学完进化路线后,可直接用文末 [十四、Vibe Coding](#十四、Vibe Coding "#%E5%8D%81%E5%9B%9Bvibe-coding%E7%94%A8-ai-ide-%E6%8A%8A%E4%B8%8A%E6%96%87%E8%BF%9B%E5%8C%96%E5%BC%8F%E6%90%AD%E5%BB%BA%E8%B7%91%E5%87%BA%E6%9D%A5") 的分阶段提示词,在 Cursor 等 AI IDE 里落地。
你将学到什么
读完并跟练后,你能回答这些问题:
- 为什么业务里要接「多个」大模型,而不只接一个?
- Spring AI 如何用最少代码先跑通对话?
- 为什么「改 YAML 换模型」不够,还要 Provider?
- 路由层解决什么问题?重试、降级、轮询分别何时用?
- 正确的进化顺序是什么,避免一上来就过度设计?
- 如何用 AI IDE(Vibe Coding)按同一进化路线分阶段实现,而不是一次性甩锅给 Agent?
一、为什么要实现 LLM 多模型接入
单模型够用吗?很多 Demo 够用。真实业务通常不够:
| 诉求 | 说明 |
|---|---|
| 方便切换 | 今天用通义,明天试 DeepSeek / GPT,不应改遍业务代码 |
| 按任务选模型 | 复杂推理用强模型;简单摘要/分类用小模型或本地模型 → 省 token、降成本、降延迟 |
| 可用性 | 某厂商限流/故障时,自动降级到备用模型 |
| AB / 对比 | 同一批请求分流到不同模型,对比效果与成本 |
多模型接入 = 把「选哪个模型」从业务代码里抽出来,变成可配置、可策略化的能力。
二、先建立心智模型:进化路线图(比最终架构更重要)
新手最容易犯的错:一上来抄完整分层(Router + 一堆 Strategy + AutoConfiguration),结果两天还调不通第一个 chat。
正确姿势是 进化式搭建:
arduino
阶段 1 跑通 1 个模型 ChatClient / ChatModel 直接调用
│
▼
阶段 2 用配置切换模型 仍是 Spring AI 自动装配,改 YAML 换 model
│
▼
阶段 3 Provider 抽象 多厂商统一接口,业务只认 ChatRequest/ChatResponse
│
▼
阶段 4 单一路由策略 例如 Priority:主模型失败 → 降级备用
│
▼
阶段 5 多策略可插拔 priority / round-robin / weighted-random
│
▼
阶段 6(进阶)智能选模 按任务类型、长度、成本预算动态选模型
每一阶段都对应一个「你多赚到的能力」:
| 阶段 | 你多赚到的能力 | 还没解决的问题 |
|---|---|---|
| 1 | 能对话 | 换厂商要改代码 |
| 2 | 同厂商换型号很方便 | 跨厂商 API 仍不一致 |
| 3 | 业务与厂商解耦,可同时挂多个 | 失败了不会自动换;选谁全靠手写 if |
| 4 | 有降级/重试,可用性上来 | 只有一种选模方式 |
| 5 | 策略可配置切换 | 选模仍偏静态 |
| 6 | 按场景智能选 | 需要评测与观测配套 |
下文按这个顺序写。文末附「目标架构总览」,那是阶段 5 做完后的样子,不要一开始就照着全量建目录。
三、阶段 1:接入 1 个大模型并验证
目标:项目能启动,发一个 HTTP 请求,拿到模型回复。
1.0 环境准备
- JDK 17+
- Spring Boot 3.x
- 一个可用的 API Key(本文以阿里云通义 DashScope 为例)
- (可选)IDEA / Cursor
新建或沿用已有 Spring Boot 工程即可。
1.1 引入依赖
以 Maven 为例(版本号按你选用的 Spring AI / Spring AI Alibaba 发行版调整):
xml
<!-- Spring AI Alibaba:通义千问 -->
<dependency>
<groupId>com.alibaba.cloud.ai</groupId>
<artifactId>spring-ai-alibaba-starter-dashscope</artifactId>
</dependency>
若你用 OpenAI 官方或其它兼容接口,换成对应的 Spring AI starter 即可。阶段 1 的重点是 先跑通一条链路,厂商可替换。
1.2 配置 YAML
yaml
spring:
ai:
dashscope:
api-key: ${DASHSCOPE_API_KEY}
chat:
options:
model: qwen-plus
把 DASHSCOPE_API_KEY 放到环境变量或本地 application-local.yml(勿提交仓库)。
1.3 最简调用:ChatClient
java
@RestController
@RequestMapping("/api/v1/ai")
public class AiChatController {
private final ChatClient chatClient;
public AiChatController(ChatClient.Builder builder) {
this.chatClient = builder.build();
}
@PostMapping("/chat")
public Map<String, String> chat(@RequestBody Map<String, String> body) {
String reply = chatClient.prompt()
.user(body.get("message"))
.call()
.content();
return Map.of("content", reply);
}
}
1.4 验证
bash
curl -X POST http://localhost:8080/api/v1/ai/chat \
-H "Content-Type: application/json" \
-d '{"message":"用一句话介绍你自己"}'
能返回文本,阶段 1 就完成了。
1.5 阶段 1 你该建立的认知
- Spring AI 把「HTTP 调厂商 API」封装成了
ChatModel/ChatClient。 - 业务代码此时 直接依赖 Spring AI,这很正常,也是正确起点。
- 还不要急着写 Router、SPI、策略模式。
四、阶段 2:改 YAML 切换「同生态」模型
目标 :不改 Java,只改配置,从 qwen-plus 换成 qwen-turbo / qwen-max。
2.1 怎么做
改配置即可:
yaml
spring:
ai:
dashscope:
api-key: ${DASHSCOPE_API_KEY}
chat:
options:
model: qwen-max # 只改这一行
重启(或支持动态刷新则热更),再打一次 curl,观察回复风格/延迟/成本差异。
2.2 阶段 2 的边界(重要)
| 能做 | 不能做(或很痛苦) |
|---|---|
| 同厂商换型号 | 同时挂 OpenAI + 通义 + Ollama |
| 快速对比两个通义模型 | 主模型挂了自动切备用 |
| 配置驱动选型 | 业务里按「任务类型」选不同厂商 |
当你开始想「再接一个 DeepSeek / 本地 Ollama」时,就会碰到:
- 不同 starter、不同 Bean 名、不同 options
- Controller 里出现
if (provider == ...) { ... } - 测试和线上切换都靠改代码
→ 这时候进入阶段 3,引入 Provider 抽象。
五、阶段 3:引入 Provider,自由切换多厂商
目标 :业务只认统一的 ChatRequest / ChatResponse;新增厂商 = 新实现类 + 一条配置。
3.1 为什么需要 Provider(用痛点说话)
阶段 1/2 的代码本质是:
text
Controller → ChatClient(DashScope) → 通义
接第二个厂商后,如果没有抽象,会变成:
text
Controller → if/else → ChatClientA / RestClientB / OllamaC
if/else 会随着厂商数量线性膨胀,且 路由、重试、降级 无处安放。
Provider 做的事只有一件:把「某个具体模型的调用」收成同一个接口。
text
业务 → ModelProvider.chat(request) → 具体厂商实现
3.2 本阶段建议新建的最小目录
先别建 router/strategy,只加这些:
text
src/main/java/com/demo/www/ai/
├── model/
│ ├── AiModelProvider.java # 枚举:DASHSCOPE / OPENAI_COMPATIBLE / OLLAMA
│ ├── ModelInfo.java # id、type、model、endpoint、apiKey、enabled...
│ ├── ChatRequest.java
│ └── ChatResponse.java
├── provider/
│ ├── ModelProvider.java # SPI
│ ├── AbstractModelProvider.java
│ ├── dashscope/DashScopeModelProvider.java
│ ├── openai/OpenAiModelProvider.java # 也覆盖 DeepSeek 等兼容接口
│ └── ollama/OllamaModelProvider.java
├── service/
│ ├── AiChatService.java
│ └── impl/AiChatServiceImpl.java
└── controller/
└── AiChatController.java
配置仍可先写死「当前用哪个 providerId」,例如:
yaml
app:
ai:
active-provider: qwen-plus
providers:
- id: qwen-plus
type: DASHSCOPE
model: qwen-plus
api-key: ${DASHSCOPE_API_KEY:}
- id: deepseek-v3
type: OPENAI_COMPATIBLE
endpoint: https://api.deepseek.com
api-key: ${DEEPSEEK_API_KEY:}
model: deepseek-chat
- id: ollama-local
type: OLLAMA
endpoint: http://127.0.0.1:11434
model: qwen2.5:7b
enabled: false
3.3 核心接口
java
public interface ModelProvider {
AiModelProvider getProviderType();
String getModelId();
boolean isAvailable();
ChatResponse chat(ChatRequest request);
ModelInfo getModelInfo();
}
统一请求/响应(字段可按需增减):
java
public class ChatRequest {
private List<Message> messages;
private String systemPrompt;
private Double temperature;
private Integer maxTokens;
/** 可选:调用方指定模型;为空则用 active-provider */
private String modelId;
// getters/setters...
}
public class ChatResponse {
private String content;
private Integer promptTokens;
private Integer completionTokens;
private String modelId;
private AiModelProvider provider;
private long latencyMs;
// getters/setters...
}
3.4 基类:把脏活留在一处
java
public abstract class AbstractModelProvider implements ModelProvider {
protected ModelInfo modelInfo;
@Override
public ChatResponse chat(ChatRequest request) {
long start = System.currentTimeMillis();
try {
ChatResponse response = doChat(request);
response.setLatencyMs(System.currentTimeMillis() - start);
response.setModelId(getModelId());
response.setProvider(getProviderType());
return response;
} catch (Exception e) {
// 新手:统一包装即可,Router「失败就重试/降级」能跑通。
// 生产建议:抛出自定义异常(如 ModelTemporaryException / ModelPermanentException),
// 让 Router 按类型选择「同模型重试」或「直接降级」------分类细则见 4.7。
throw new IllegalStateException(
"模型调用失败: " + getModelId() + ", cause=" + e.getMessage(), e);
}
}
protected abstract ChatResponse doChat(ChatRequest request);
}
3.5 具体 Provider 怎么写(原则)
| Provider | 推荐实现方式 |
|---|---|
DashScopeModelProvider |
注入已有 DashScopeChatModel / ChatModel,复用 Spring AI Alibaba 自动配置,不要重复配 API Key |
OpenAiModelProvider |
OpenAI 兼容协议(含 DeepSeek 等):RestClient 调 /v1/chat/completions |
OllamaModelProvider |
调本地 http://host:11434/api/chat |
示意(OpenAI 兼容,省略 DTO):
java
@Component
public class OpenAiModelProvider extends AbstractModelProvider {
// 【教学】一行创建即可跑通 Demo
private final RestClient restClient = RestClient.create();
// 【生产】注入 Spring 管理的 Builder,复用连接池 / 超时 / SSL,避免每 Provider 各建一套:
// private final RestClient restClient;
// public OpenAiModelProvider(RestClient.Builder builder, ModelInfo info) {
// this.restClient = builder.baseUrl(info.getEndpoint())
// // .requestFactory(...) 配置连接超时、读超时等
// .build();
// this.modelInfo = info;
// }
@Override
protected ChatResponse doChat(ChatRequest request) {
// POST {endpoint}/v1/chat/completions
// Authorization: Bearer {apiKey}
// body: model, messages, temperature, max_tokens
// 解析 choices[0].message.content → ChatResponse
return new ChatResponse(/* ... */);
}
}
3.6 Service:按 modelId / active-provider 选取
阶段 3 还没有 Router,Service 里做最简单的选择即可:
java
@Service
public class AiChatServiceImpl implements AiChatService {
private final Map<String, ModelProvider> providers;
private final String activeProviderId;
public AiChatServiceImpl(List<ModelProvider> list,
@Value("${app.ai.active-provider}") String activeProviderId) {
this.providers = list.stream()
.collect(Collectors.toMap(ModelProvider::getModelId, Function.identity()));
this.activeProviderId = activeProviderId;
}
@Override
public ChatResponse chat(ChatRequest request) {
String id = request.getModelId() != null ? request.getModelId() : activeProviderId;
ModelProvider provider = providers.get(id);
if (provider == null || !provider.isAvailable()) {
throw new IllegalArgumentException("模型不可用: " + id);
}
return provider.chat(request);
}
}
3.7 阶段 3 验证清单
text
☐ 指定 modelId=qwen-plus 能通
☐ 指定 modelId=deepseek-v3 能通
☐ 改 active-provider,不传 modelId 也能切
☐ 业务代码(Service 以上)没有出现 DashScope/OpenAI 专用 API
3.8 阶段 3 结束时的能力边界
你已经可以「自由切换」。但仍缺:
- 主模型超时/5xx 时不会自动换备用
- 没有轮询、加权分流
- 选模逻辑还在 Service 里,后续会越来越胖
→ 进入阶段 4,把「选谁 + 失败怎么办」抽成 Router。
六、阶段 4:引入「一种」路由策略(建议先做 Priority)
目标 :主模型失败时自动降级;同模型可有限次重试。先只实现 Priority(优先级) 一种策略。
4.1 为什么先做 Priority,而不是一次做四种策略
- 生产里最刚需的是 可用性:挂了能切走。
- Priority 语义简单:
priority越小越优先,失败则试下一个。 - 先跑通「选模 → 调用 → 失败重试/降级」这条主循环,再复制出其它策略。
4.2 本阶段新增目录
text
router/
├── ModelRouter.java
├── DefaultModelRouter.java
├── RouterContext.java
└── strategy/
├── RouterStrategy.java
└── PriorityStrategy.java
配置升级为路由段(示例):
yaml
app:
ai:
router:
strategy: priority
retry:
max-attempts: 3
backoff-ms: 1000
fallback:
enabled: true
providers:
- id: qwen-plus
type: DASHSCOPE
model: qwen-plus
api-key: ${DASHSCOPE_API_KEY:}
priority: 1
timeout: 30s
- id: deepseek-v3
type: OPENAI_COMPATIBLE
endpoint: https://api.deepseek.com
api-key: ${DEEPSEEK_API_KEY:}
model: deepseek-chat
priority: 2
timeout: 30s
- id: ollama-local
type: OLLAMA
endpoint: http://127.0.0.1:11434
model: qwen2.5:7b
priority: 99
enabled: false
4.3 核心接口
java
public interface ModelRouter {
ChatResponse route(ChatRequest request);
void register(ModelProvider provider);
}
public interface RouterStrategy {
ModelProvider select(List<ModelProvider> candidates, RouterContext context);
String getName();
}
RouterContext 至少记录:
- 原始
ChatRequest - 已尝试过的
modelId列表 - 当前重试次数
4.4 DefaultModelRouter 主循环(心智图)
text
route(request)
1. 过滤:enabled && isAvailable()
2. strategy.select(candidates, context) → 选出一个 Provider
3. provider.chat(request)
成功 → 返回
失败 → 同模型重试未超限? → 回到 3
→ 否则标记已尝试,若允许 fallback → 回到 2
4. 全部失败 → 抛异常(带上尝试轨迹,方便排查)
4.5 PriorityStrategy 规则
- 按
priority升序排列 - 跳过
context里已尝试的 id - 取第一个
4.6 Service 变薄
java
// AiChatServiceImpl
return modelRouter.route(request);
选模、重试、降级都不再写在业务里。
4.7 阶段 4 怎么验证(建议做故障演练)
- 正常:只开
qwen-plus,请求成功。 - 人为把主模型 api-key 配错,确认能降级到
deepseek-v3。 - 日志里应看到:
try qwen-plus → fail → try deepseek-v3 → ok。 - 两个都失败时,错误信息包含尝试过的模型列表。
进阶提醒:别把「所有异常」当成同一种失败
AbstractModelProvider 把 doChat 的异常统一包成 IllegalStateException,新手这样写没问题------Router 只要「失败就重试 / 降级」就能跑通。上线后你会发现失败并不都一样:
| 类型 | 典型例子 | 建议策略 |
|---|---|---|
| 瞬时故障 | 超时、网络抖动、429 / 5xx | 同模型可重试;仍失败再降级 |
| 模型侧不可用 | 模型名不存在、配额耗尽、区域不可用 | 一般 不要死磕重试,尽快降级到下一候选 |
| 配置/鉴权类 | API Key 错误、权限不足 | 应降级(保证可用性),但要打 ERROR + 告警:换模型掩不住根因,Key 配错不会靠重试好 |
可选演进(阶段 4 验收通过后再做,不必阻塞主线):
- 抛出
ModelTemporaryException(可重试)/ModelPermanentException(直接降级),或统一带retryable的异常 /Result,供DefaultModelRouter决策; - HTTP 状态码与厂商 error code 在 Provider 内翻译成上述三类,避免 Router 解析各家报文。
故障演练时可多加两笔:① 人为超时(看会不会先重试再降级);② 主模型 Key 错误(应降级,且日志能区分「鉴权失败」与「偶发超时」)。
七、阶段 5:多种路由策略可插拔
目标:通过配置切换策略,而不是改 Java。
5.1 策略对照表
| 策略 | 适用场景 | 选择逻辑 |
|---|---|---|
| priority | 主备、成本优先、质量优先 | 按 priority 依次尝试 |
| round-robin | 多账号/多实例均摊 | 轮流选下一个可用模型 |
| weighted-random | AB 实验、按比例分流 | 按 weight 加权随机 |
接口不变,新增实现类即可:
text
strategy/
├── RouterStrategy.java
├── PriorityStrategy.java
├── RoundRobinStrategy.java
└── WeightedRandomStrategy.java
并发提醒 :策略类默认是 Spring 单例,多请求会并发进入
select()。共享计数器或Random必须线程安全;教学阶段先跑通语义,上线前务必检查。
java
// RoundRobinStrategy:用 AtomicInteger,不要用普通 int++
private final AtomicInteger counter = new AtomicInteger(0);
public ModelProvider select(List<ModelProvider> candidates, RouterContext context) {
List<ModelProvider> list = /* 过滤已尝试后的候选 */;
int idx = Math.floorMod(counter.getAndIncrement(), list.size());
return list.get(idx);
}
// WeightedRandomStrategy:用 ThreadLocalRandom.current(),不要共享一个 java.util.Random
int r = ThreadLocalRandom.current().nextInt(totalWeight);
5.2 配置切换
yaml
app:
ai:
router:
strategy: weighted-random # 改这里
providers:
- id: qwen-plus
weight: 50
priority: 1
- id: deepseek-v3
weight: 30
priority: 2
- id: qwen-max
weight: 20
priority: 3
装配时按名称注入对应 RouterStrategy Bean(可用 Map<String, RouterStrategy> + strategy 配置项)。
5.3 重试 vs 降级(别混)
| 概念 | 含义 | 典型触发 |
|---|---|---|
| 同模型重试 | 还是这个模型,再打一次 | 瞬时超时、429、网络抖动 |
| 降级(换模型) | 换下一个候选 | 连续失败、鉴权错误、模型不可用 |
建议:max-attempts 控制「整体尝试次数」或「每模型重试次数」------在文档和代码注释里写清楚一种语义,团队统一即可。新手推荐:
- 每个模型最多重试
N次 - 仍失败则换下一个 priority / 策略给出的下一个候选
5.4 阶段 5 完成标志
- 改
strategy即可换行为 - 新增策略 = 新类 + 注册 Bean,不动 Router 主循环
- Controller / Service 零感知
八、阶段 6(进阶展望):智能切换模型
阶段 5 之前的选模都是 静态规则(配置写死 priority/weight)。智能选模是下一层:
常见思路(实现可后置):
- 规则引擎 :
taskType=code→ 强模型;taskType=classify→ 小模型 - 启发式:按 prompt 长度、是否需要工具调用、用户等级选模
- 学习式:根据历史成功率、延迟、费用做在线决策(需埋点与评测)
建议等你有了:
- 统一的
ChatResponse元数据(modelId、latency、token) - 基础路由与降级
再做智能层,把它做成 又一个 RouterStrategy (例如 SmartStrategy),而不是打散进业务代码。
九、目标架构总览(阶段 5 完成后的样子)
下面这张图是「进化完成后」的全景,用来对照,不是第 1 天的施工图。
text
┌─────────────────────────────────────────────────────────────┐
│ controller/ │
│ AiChatController (对外 API) │
├─────────────────────────────────────────────────────────────┤
│ service/ │
│ AiChatService / AiChatServiceImpl │
│ (业务编排:预处理、持久化、后处理) │
├─────────────────────────────────────────────────────────────┤
│ router/ ← 路由层 │
│ ModelRouter → DefaultModelRouter │
│ ├── RoundRobinStrategy (轮询) │
│ ├── PriorityStrategy (优先级降级) │
│ ├── Retry(可内嵌在 Router 或独立 Strategy) │
│ └── WeightedRandomStrategy(加权随机) │
├─────────────────────────────────────────────────────────────┤
│ provider/ ← 模型接入层 │
│ ModelProvider (SPI) │
│ ├── DashScopeModelProvider (阿里通义) │
│ ├── OpenAiModelProvider (OpenAI / DeepSeek 兼容) │
│ └── OllamaModelProvider (本地部署) │
├─────────────────────────────────────────────────────────────┤
│ config/ ← 配置层 │
│ AiModelAutoConfiguration │
│ AiRouterProperties / AiModelProviderProperties │
├─────────────────────────────────────────────────────────────┤
│ model/ ← 通用模型 │
│ ChatRequest / ChatResponse / ModelInfo / AiModelProvider │
└─────────────────────────────────────────────────────────────┘
9.1 目标目录结构
text
src/main/java/com/demo/www/ai/
├── config/
│ ├── AiModelAutoConfiguration.java
│ └── properties/
│ ├── AiRouterProperties.java
│ └── AiModelProviderProperties.java
├── model/
│ ├── AiModelProvider.java
│ ├── ChatRequest.java
│ ├── ChatResponse.java
│ └── ModelInfo.java
├── provider/
│ ├── ModelProvider.java
│ ├── AbstractModelProvider.java
│ ├── dashscope/DashScopeModelProvider.java
│ ├── openai/OpenAiModelProvider.java
│ └── ollama/OllamaModelProvider.java
├── router/
│ ├── ModelRouter.java
│ ├── DefaultModelRouter.java
│ ├── RouterContext.java
│ └── strategy/
│ ├── RouterStrategy.java
│ ├── RoundRobinStrategy.java
│ ├── PriorityStrategy.java
│ └── WeightedRandomStrategy.java
├── service/
│ ├── AiChatService.java
│ └── impl/AiChatServiceImpl.java
└── controller/
└── AiChatController.java
9.2 分层职责
| 层次 | 职责 | 关键模式 |
|---|---|---|
| provider | 屏蔽厂商 API 差异 | SPI 插件化 |
| router | 选择、重试、降级、负载均衡 | 策略模式 |
| service | 业务编排、会话/持久化 | Facade |
| config | YAML 驱动装配 | 配置与代码分离 |
接入新模型只需:① 实现
ModelProvider② YAML 加一条 ③ 不改路由与业务代码。
9.3 依赖关系
text
YAML
→ AiRouterProperties / providers[]
→ ModelInfo
→ ModelProvider 实现们
→ RouterStrategy
→ DefaultModelRouter
→ AiChatService
→ AiChatController
9.4 编码顺序(仅在「你已决定一次建到阶段 5」时使用)
若你已经走完阶段 1/2,准备一次性补齐阶段 3--5 的代码,推荐依赖从底向上:
text
1. model/ 零依赖
2. config/properties/ 依赖枚举
3. provider/ 依赖 model
4. router/ 依赖 provider
5. service/ 依赖 router
6. controller/ 依赖 service
7. AiModelAutoConfiguration 串联 Bean
8. 补全 application-*.yml
这和「产品进化阶段」不矛盾:进化阶段决定 什么时候引入哪一层 ;编码顺序决定 某一阶段内部先写谁。
十、AutoConfiguration 在做什么(阶段 4/5 再看也不迟)
text
AiModelAutoConfiguration
├── 启用 @ConfigurationProperties → AiRouterProperties
├── 按 YAML providers[].type / id 绑定到对应 ModelProvider,写入 ModelInfo
├── 按 router.strategy 选出 RouterStrategy
└── 注册 DefaultModelRouter(List<ModelProvider>, Strategy, Properties)
与现有 DashScope 共存的注意点 :若项目已通过 spring.ai.dashscope 自动配置了 DashScopeChatModel,则 DashScopeModelProvider 应 注入并复用 它,避免两套 API Key / 两套客户端。
十一、跟练 TODO(按进化阶段勾选)
text
阶段 1 跑通单模型
☐ 建 Spring Boot 工程(JDK 17+)
☐ 引入 DashScope(或其它)starter
☐ 配置 api-key + model
☐ ChatClient 写通 /api/v1/ai/chat
☐ curl 验证
阶段 2 配置切换同厂商模型
☐ 只改 YAML 的 model,验证换模成功
☐ 记录:延迟 / 效果差异(建立对比习惯)
阶段 3 Provider 抽象
☐ model/:ChatRequest、ChatResponse、ModelInfo、枚举
☐ ModelProvider + AbstractModelProvider
☐ 至少 2 个具体 Provider(建议 DashScope + OpenAI 兼容)
☐ Service 按 modelId / active-provider 调用
☐ 业务层不再依赖具体厂商 SDK
阶段 4 单一路由(Priority)
☐ RouterContext / ModelRouter / PriorityStrategy
☐ DefaultModelRouter:重试 + 降级主循环
☐ 故障演练:主模型失败能切备用
阶段 5 多策略
☐ RoundRobin / WeightedRandom
☐ YAML 切换 strategy
☐ AB:看流量是否按 weight 大致分布
阶段 6(可选)
☐ 按 taskType / 长度等做 SmartStrategy
☐ 补齐 token、延迟、成功率观测
十二、常见坑(新手向)
- 密钥进仓库:一律环境变量或本地 profile。
- 阶段跨太大:没有单模型验证就写 Router,出问题不知道哪层坏了。
- Provider 里塞业务:会话存储、敏感词、计费应放在 Service,不要写进厂商适配器。
- 降级静默失败:降级成功也要打日志(从哪个 model 切到哪个),否则线上很难查。
- OpenAI 兼容 endpoint 尾巴 :有的要
/v1,有的文档已含,拼接时统一处理,避免//v1或漏路径。 - 本地 Ollama 默认别开 :
enabled: false,避免没装 Ollama 时拖垮健康检查。 - 所有异常一视同仁 :超时与 Key 错误若不加区分,重试会空转、根因被降级掩盖------见 4.7 进阶提醒;生产可拆
ModelTemporaryException/ModelPermanentException。 - 策略 Bean 非线程安全 :
RoundRobinStrategy/WeightedRandomStrategy是单例,普通int++或共享Random在并发下会乱序甚至越界------用AtomicInteger/ThreadLocalRandom(见 5.1 并发提醒)。这是上线极易踩的坑。 RestClient.create()当生产客户端 :教学可以;生产不会复用连接池,高并发下易导致端口耗尽。应注入RestClient.Builder(或WebClient.Builder)统一超时与连接管理。
十三、小结
多模型接入不是「一开始就上完整路由框架」,而是:
- 先能聊(Spring AI 单模型)
- 再能换(配置切换)
- 再能插(Provider SPI)
- 再能托底(Router + Priority 降级)
- 再能玩法多样(多策略)
- 最后才智能(按场景选模)
每一阶段都留下可运行的系统;下一阶段只解决上一阶段露出来的痛点。这样你既学得会,也建得稳。
原理清楚之后,不必全程手敲:下一章把同一条进化路线翻译成 可复制的分阶段提示词,用 AI IDE 加速落地。
十四、Vibe Coding:用 AI IDE 把上文「进化式搭建」跑出来
上文用手搓帮助你理解原理。现实里你也可以用 Cursor / Trae / Windsurf 等 AI IDE,用自然语言让 Agent 按阶段写代码------第十四章不是附录彩蛋,而是跟练的第二条路径(手搓 / Vibe Coding 二选一或交替均可)。
推荐用法:
- 先读懂进化路线(第二节),再让 AI 写------你知道它该停在哪一阶段。
- 优先分阶段提示:每阶段结束用 curl / 故障演练验收,再进下一阶段。
- 一步到位提示只适合:你已理解架构、只想快速出脚手架时用;出问题再拆回分阶段修。
- 把 API Key 放环境变量,提示词里写
${DASHSCOPE_API_KEY}即可,不要把真实密钥贴进对话。 - 包名、JDK、Spring Boot / Spring AI 版本按你工程实际改;下面示例用
com.demo.www.ai。
14.1 分阶段提示词(推荐)
每个提示词默认前提:当前仓库已是 Spring Boot 3.x + JDK 17+ 工程。若从空目录开始,在阶段 1 提示词前加一句:「用 Maven 新建 Spring Boot 3 工程」。
阶段 1:跑通单模型
复制到 Agent:
text
请在当前 Spring Boot 3 工程中,用 Spring AI Alibaba 接入阿里云通义 DashScope,完成「最小可运行」对话接口。
要求:
1. 引入 spring-ai-alibaba-starter-dashscope(或当前推荐的等价依赖),版本与 Spring Boot 兼容。
2. application.yml 配置:
- spring.ai.dashscope.api-key: ${DASHSCOPE_API_KEY:}
- spring.ai.dashscope.chat.options.model: qwen-plus
3. 写一个 AiChatController:POST /api/v1/ai/chat,请求体 JSON 含 message 字段,返回模型文本。
4. 用 ChatClient(或 ChatModel)调用,代码尽量短,不要引入 Router / Provider。
5. 给出 curl 验证示例。
6. 不要把真实 API Key 写进仓库;用环境变量。
验收:服务启动后,curl 能拿到通义回复。
阶段 1 → 2:配置切换同厂商模型
text
在现有「DashScope 单模型对话」工程上做阶段 2,不要大改架构。
要求:
1. 只通过修改 application.yml 的 spring.ai.dashscope.chat.options.model,支持在 qwen-plus / qwen-turbo(或同类)之间切换。
2. Java 代码尽量不动;若必须动,说明为什么。
3. 在 README 或注释里写清:阶段 2 的边界是「同生态换型号」,还不能优雅接 OpenAI / Ollama。
4. 给出切换模型后的验证步骤,以及建议记录的对比项:延迟、效果差异。
验收:改 YAML + 重启(或热加载)后,响应里能看出用了新模型(或日志打印 model)。
阶段 2 → 3:引入 Provider 抽象
text
在现有可运行的 Spring AI 对话工程上,进入阶段 3:引入 Provider,支持多厂商切换。严格按「进化式」做,本阶段不要做 Router / 重试 / 降级。
包根路径:com.demo.www.ai
请新建:
- model/:AiModelProvider 枚举(DASHSCOPE / OPENAI_COMPATIBLE / OLLAMA)、ModelInfo、ChatRequest、ChatResponse
- provider/:ModelProvider 接口、AbstractModelProvider 基类
- dashscope/DashScopeModelProvider:复用已有 DashScopeChatModel / ChatModel,不要重复配一套 Key
- openai/OpenAiModelProvider:OpenAI 兼容协议,RestClient 调 {endpoint}/v1/chat/completions(覆盖 DeepSeek 等)
- ollama/OllamaModelProvider:调本地 /api/chat;默认 enabled=false
- service/:AiChatService + Impl,按 request.modelId,否则用 app.ai.active-provider 选 Provider
- controller/:对外仍暴露 POST /api/v1/ai/chat,请求体改为统一 ChatRequest(可兼容旧的 message 字段)
配置示例(写入 application.yml):
app:
ai:
active-provider: qwen-plus
providers:
- id: qwen-plus
type: DASHSCOPE
model: qwen-plus
api-key: ${DASHSCOPE_API_KEY:}
- id: deepseek-v3
type: OPENAI_COMPATIBLE
endpoint: https://api.deepseek.com
api-key: ${DEEPSEEK_API_KEY:}
model: deepseek-chat
- id: ollama-local
type: OLLAMA
endpoint: http://127.0.0.1:11434
model: qwen2.5:7b
enabled: false
约束:
1. 业务层(Service 以上)不得直接依赖 DashScope/OpenAI SDK 专用 API。
2. AbstractModelProvider 统一记 latencyMs、填充 modelId/provider、包装异常信息。
3. 本阶段不做路由策略、不做 AutoConfiguration 复杂装配,能用 @Component + @ConfigurationProperties 即可。
4. 给出验证清单:指定 modelId、改 active-provider、业务无厂商泄漏。
验收:至少 DashScope + 一个 OpenAI 兼容模型能通。
阶段 3 → 4:单一路由 Priority + 重试降级
text
在已有 Provider 多模型工程上进入阶段 4:引入 ModelRouter,先只实现 Priority 一种策略。
新增目录 com.demo.www.ai.router:
- ModelRouter、DefaultModelRouter、RouterContext
- strategy/RouterStrategy、PriorityStrategy
行为(DefaultModelRouter.route):
1. 过滤 enabled && isAvailable()
2. strategy.select(candidates, context) 选出 Provider
3. provider.chat;失败则同模型重试(max-attempts / backoff-ms)
4. 仍失败则标记已尝试,fallback.enabled 时选下一个;全部失败抛异常并带上尝试轨迹
5. PriorityStrategy:priority 升序,跳过已尝试 id,取第一个
配置改为:
app.ai.router.strategy: priority
app.ai.router.retry.max-attempts / backoff-ms
app.ai.router.fallback.enabled
providers 增加 priority、timeout;去掉仅靠 active-provider 硬选的逻辑(可保留 modelId 指定时优先该模型)
Service 变薄:AiChatServiceImpl 只调用 modelRouter.route(request)。
约束:
1. 不要一次实现 round-robin / weighted-random。
2. 降级成功必须打日志:from → to。
3. 给出故障演练步骤:主模型 Key 配错 → 应落到备用模型。
4. 异常分类进阶(可选):瞬时故障可重试,鉴权/模型不存在宜直接降级并告警(见文档 4.7)。
验收:正常请求成功;主模型失败能降级;双失败错误信息含尝试列表。
阶段 4 → 5:多策略可插拔
text
在已有 Priority 路由工程上进入阶段 5:策略可配置切换,不改业务代码。
新增:
- RoundRobinStrategy:在可用候选上轮询;计数器用 AtomicInteger,保证线程安全
- WeightedRandomStrategy:按 weight 加权随机;用 ThreadLocalRandom,勿共享 Random
保持 RouterStrategy 接口不变;DefaultModelRouter 主循环复用。
通过 app.ai.router.strategy: priority | round-robin | weighted-random 切换。
providers 增加 weight 字段。
可选:补一个简单的 AiModelAutoConfiguration,用 @ConfigurationProperties 绑定 AiRouterProperties,按 strategy 名称注入对应 RouterStrategy Bean。
约束:
1. 区分「重试」(同模型)与「降级」(换模型),配置与日志语义清晰。
2. 给出 AB 验证思路:weighted-random 下多次请求,流量大致按 weight 分布。
3. 不要做阶段 6 的智能选模。
验收:只改 YAML 的 strategy 即可切换行为;三种策略都能跑通。
(可选)阶段 5 → 6:智能选模展望
text
在阶段 5 多策略工程上,做一个「最小可用」的智能选模原型(可标为实验特性)。
要求:
1. 新增 SmartStrategy(或独立 SmartModelSelector):根据 ChatRequest 的简单信号选模型,例如:
- messages 总长度 / maxTokens 超过阈值 → 选强模型
- 短文本摘要/分类 → 选小模型或本地 Ollama
- 可先用规则 if/else,不要上复杂 ML
2. 配置里增加 taskType 或 rules 段(保持 YAML 驱动)。
3. 必须保留手动指定 modelId 的覆盖能力。
4. 补充最基本观测:每次响应带 modelId、latencyMs;日志打印选型原因。
5. 明确标注:这是进阶展望,规则需结合业务评测再固化。
验收:构造「短请求」与「长/复杂请求」各一次,走不同模型,且日志能说明为何如此选择。
14.2 一步到位的提示词(理解架构后再用)
适合:你已经读完本文阶段 1--5,想让 AI 一次性生成到「多策略可插拔」脚手架。
不适合:完全没跑通过单模型对话------出 bug 时很难分层排查。
text
请基于 Spring Boot 3 + JDK 17,按「进化完成后的目标架构」实现 LLM 多模型接入脚手架(做到阶段 5 即可,阶段 6 只留扩展点注释)。
包根:com.demo.www.ai
目录与职责:
- config/:AiModelAutoConfiguration;properties/AiRouterProperties、AiModelProviderProperties
- model/:AiModelProvider、ChatRequest、ChatResponse、ModelInfo
- provider/:ModelProvider、AbstractModelProvider
- dashscope/DashScopeModelProvider(复用 Spring AI Alibaba DashScopeChatModel)
- openai/OpenAiModelProvider(OpenAI 兼容 /v1/chat/completions,含 DeepSeek)
- ollama/OllamaModelProvider(本地,默认 enabled=false)
- router/:ModelRouter、DefaultModelRouter、RouterContext
- strategy/:RouterStrategy + PriorityStrategy + RoundRobinStrategy + WeightedRandomStrategy
- service/:AiChatService + Impl(只调 router)
- controller/:AiChatController,POST /api/v1/ai/chat
核心行为:
1. YAML 驱动注册多个 providers;strategy 可配置切换。
2. DefaultModelRouter:过滤可用 → 策略选模 → 同模型重试 → 失败降级 → 全失败抛带轨迹的异常。
3. Priority:priority 越小越优先;RoundRobin 均摊(AtomicInteger);WeightedRandom 按 weight(ThreadLocalRandom)。
4. 密钥全部环境变量;Ollama 默认关闭。
5. 若项目已有 spring.ai.dashscope 自动配置,DashScope Provider 必须复用,避免两套客户端。
6. OpenAI 兼容 Provider 生产环境注入 RestClient.Builder,勿只用 RestClient.create()。
7. 提供 example application.yml、curl 示例、简短 README(如何切 strategy、如何做主备故障演练)。
明确不要做:
- 不要在 Provider 里写会话存储/敏感词/计费
- 不要一上来做复杂智能选模(阶段 6 仅注释扩展点)
- 不要把真实 API Key 写入文件
验收标准:
- 能启动;至少一种云模型 chat 成功
- 改 strategy 无需改 Java
- 主模型 Key 错误时可降级到备用(priority 模式)
14.3 和 AI 协作时的小技巧
| 技巧 | 说明 |
|---|---|
| 一次只升一阶段 | 提示词结尾写清「不要提前实现下一阶段」 |
| 贴上文约束 | 把本文对应阶段的目录树 / 接口签名贴进对话,减少 AI 自由发挥 |
| 先验收再继续 | 每阶段用第十一节 TODO 勾选;红了先修,再发下一阶段提示词 |
| 让 AI 写验证 | 追加:「请补充集成测试或 shell 脚本做故障演练」 |
| 出问题就降级提问 | 「不要重构,只修:主模型失败没有降级」比「帮我优化架构」更有效 |
| 对照第十二节坑 | 密钥、阶段跨太大、Provider 塞业务、降级无日志、endpoint /v1 拼接、策略非线程安全、RestClient 连接池 |