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 连接池
相关推荐
long3163 小时前
PostgreSQL 学习资料 · 入门 / 练习 / 精通 / 扩展
java·数据结构·数据库·spring boot·sql·postgresql·数据库开发
天丁o3 小时前
我把一套 Flowable + Spring Boot + Vue 的 BPM 流程引擎整理开源了:从流程设计器到待办闭环
spring boot·vue·工作流引擎·flowable·bpm流程引擎·流程审批
立心者016 小时前
SpringBoot中使用TOTP实现MFA(多因素认证)
java·spring boot·后端
QQ_216962909620 小时前
Spring Boot 养老院管理系统:从入住、护理到费用结算的全流程实现(源码可领)
java·spring boot·后端
xxwl5851 天前
数据库后端接口测试报告
spring boot·mysql·tomcat
zzzzzz3101 天前
别让大模型直接碰业务:我在 Spring Boot 里给 AI 操作加了一道“可拒绝的闸门”
人工智能·spring boot·spring
知彼解己1 天前
Java 版本演进
java·开发语言·spring boot
夜郎king1 天前
SpringBoot+PostgreSQL + 硅基流动大模型从零搭建 Text-to-SQL 智能问答系统
spring boot·postgresql·text-to-sql·llm大模型
就改了1 天前
MyBatis核心类用法详解
java·spring boot·后端·mybatis