Spring AI + Spring Boot:从 0 到 1 进化式搭建 LLM 多模型接入

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 里落地。


你将学到什么

读完并跟练后,你能回答这些问题:

  1. 为什么业务里要接「多个」大模型,而不只接一个?
  2. Spring AI 如何用最少代码先跑通对话?
  3. 为什么「改 YAML 换模型」不够,还要 Provider?
  4. 路由层解决什么问题?重试、降级、轮询分别何时用?
  5. 正确的进化顺序是什么,避免一上来就过度设计?
  6. 如何用 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 规则

  1. priority 升序排列
  2. 跳过 context 里已尝试的 id
  3. 取第一个

4.6 Service 变薄

java 复制代码
// AiChatServiceImpl
return modelRouter.route(request);

选模、重试、降级都不再写在业务里。

4.7 阶段 4 怎么验证(建议做故障演练)

  1. 正常:只开 qwen-plus,请求成功。
  2. 人为把主模型 api-key 配错,确认能降级到 deepseek-v3
  3. 日志里应看到:try qwen-plus → fail → try deepseek-v3 → ok
  4. 两个都失败时,错误信息包含尝试过的模型列表。
进阶提醒:别把「所有异常」当成同一种失败

AbstractModelProviderdoChat 的异常统一包成 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)。智能选模是下一层:

常见思路(实现可后置):

  1. 规则引擎taskType=code → 强模型;taskType=classify → 小模型
  2. 启发式:按 prompt 长度、是否需要工具调用、用户等级选模
  3. 学习式:根据历史成功率、延迟、费用做在线决策(需埋点与评测)

建议等你有了:

  • 统一的 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、延迟、成功率观测

十二、常见坑(新手向)

  1. 密钥进仓库:一律环境变量或本地 profile。
  2. 阶段跨太大:没有单模型验证就写 Router,出问题不知道哪层坏了。
  3. Provider 里塞业务:会话存储、敏感词、计费应放在 Service,不要写进厂商适配器。
  4. 降级静默失败:降级成功也要打日志(从哪个 model 切到哪个),否则线上很难查。
  5. OpenAI 兼容 endpoint 尾巴 :有的要 /v1,有的文档已含,拼接时统一处理,避免 //v1 或漏路径。
  6. 本地 Ollama 默认别开enabled: false,避免没装 Ollama 时拖垮健康检查。
  7. 所有异常一视同仁 :超时与 Key 错误若不加区分,重试会空转、根因被降级掩盖------见 4.7 进阶提醒;生产可拆 ModelTemporaryException / ModelPermanentException
  8. 策略 Bean 非线程安全RoundRobinStrategy / WeightedRandomStrategy 是单例,普通 int++ 或共享 Random 在并发下会乱序甚至越界------用 AtomicInteger / ThreadLocalRandom(见 5.1 并发提醒)。这是上线极易踩的坑。
  9. RestClient.create() 当生产客户端 :教学可以;生产不会复用连接池,高并发下易导致端口耗尽。应注入 RestClient.Builder(或 WebClient.Builder)统一超时与连接管理。

十三、小结

多模型接入不是「一开始就上完整路由框架」,而是:

  1. 先能聊(Spring AI 单模型)
  2. 再能换(配置切换)
  3. 再能插(Provider SPI)
  4. 再能托底(Router + Priority 降级)
  5. 再能玩法多样(多策略)
  6. 最后才智能(按场景选模)

每一阶段都留下可运行的系统;下一阶段只解决上一阶段露出来的痛点。这样你既学得会,也建得稳。

原理清楚之后,不必全程手敲:下一章把同一条进化路线翻译成 可复制的分阶段提示词,用 AI IDE 加速落地。


十四、Vibe Coding:用 AI IDE 把上文「进化式搭建」跑出来

上文用手搓帮助你理解原理。现实里你也可以用 Cursor / Trae / Windsurf 等 AI IDE,用自然语言让 Agent 按阶段写代码------第十四章不是附录彩蛋,而是跟练的第二条路径(手搓 / Vibe Coding 二选一或交替均可)。

推荐用法

  1. 先读懂进化路线(第二节),再让 AI 写------你知道它该停在哪一阶段。
  2. 优先分阶段提示:每阶段结束用 curl / 故障演练验收,再进下一阶段。
  3. 一步到位提示只适合:你已理解架构、只想快速出脚手架时用;出问题再拆回分阶段修。
  4. 把 API Key 放环境变量,提示词里写 ${DASHSCOPE_API_KEY} 即可,不要把真实密钥贴进对话
  5. 包名、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 连接池
相关推荐
脉动数据行情120 小时前
Java SpringBoot 国际期货批量采集实践 美原油 / 黄金 / 指数期货定时落库
java·开发语言·spring boot
Zzzzmo_1 天前
SpringBoot 日志
java·spring boot·spring
计算机毕设定制辅导-无忧学长1 天前
《基于SpringBoot的家政服务管理平台的设计与实现》
java·vue.js·spring boot·计算机毕业设计选题推荐·家政服务管理平台
xiaoqiMikko1 天前
pom 里没有、代码里没调过,但 WebClient 默认用的就是 netty-resolver-dns
java·spring boot
xixingzhe21 天前
spring boot对接Dify
java·spring boot·dify
步行cgn1 天前
Spring Boot 指定数据来源详解
spring boot·后端·python
一个有温度的技术博主1 天前
深入理解 Spring Boot 自动装配
java·spring boot·后端
步行cgn1 天前
Spring Boot 将配置绑定到第三方对象详解
spring boot·后端·python
凤山老林1 天前
Spring Boot 集成 Apache Kafka Streams 构建流处理应用:状态存储、窗口聚合与
spring boot·kafka·apache
Flynt2 天前
把公司项目迁到 Spring Boot 4.0:编译通过只是开始
java·spring boot·后端