Spring AI 通过ChatClient + OpenRouter 对接多种模型,每个用户可动态切换模型

一、核心思路

OpenRouter 兼容 OpenAI API 格式,所以用 spring-ai-starter-model-openai OpenAiChatModelbase-url 指到 OpenRouter。模型切换放在每次调用的 options 里覆盖,无需为每个模型建 Bean。

二、依赖(pom.xml)

<parent>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-parent</artifactId>
    <version>3.3.4</version>
</parent>

<properties>
    <java.version>17</java.version>
    <spring-ai.version>1.0.0-M5</spring-ai.version>
</properties>

<dependencies>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-web</artifactId>
    </dependency>
    <!-- Spring AI OpenAI 起步依赖(OpenRouter 走 OpenAI 协议) -->
    <dependency>
        <groupId>org.springframework.ai</groupId>
        <artifactId>spring-ai-starter-model-openai</artifactId>
    </dependency>
</dependencies>

<dependencyManagement>
    <dependencies>
        <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>

<repositories>
    <repository>
        <id>spring-milestones</id>
        <url>https://repo.spring.io/milestone</url>
    </repository>
</repositories>

三、application.yml

spring:
  ai:
    openai:
      api-key: ${OPENROUTER_API_KEY:sk-or-xxxxxxxx}
      base-url: https://openrouter.ai/api/v1
      chat:
        options:
          # 默认模型,调用时可被覆盖
          model: openai/gpt-4o-mini
          temperature: 0.7

OpenRouter 的模型名格式是 供应商/模型名,例如 openai/gpt-4oanthropic/claude-3.5-sonnetgoogle/gemini-pro-1.5meta-llama/llama-3.1-70b-instruct

四、模型枚举(统一管理可用模型)

package com.example.openrouter.model;

import lombok.Getter;

@Getter
public enum ChatModelEnum {

    GPT4O_MINI("openai/gpt-4o-mini", "GPT-4o Mini"),
    GPT4O("openai/gpt-4o", "GPT-4o"),
    CLAUDE_35_SONNET("anthropic/claude-3.5-sonnet", "Claude 3.5 Sonnet"),
    GEMINI_PRO_1_5("google/gemini-pro-1.5", "Gemini Pro 1.5"),
    LLAMA_31_70B("meta-llama/llama-3.1-70b-instruct", "Llama 3.1 70B");

    private final String code;
    private final String displayName;

    ChatModelEnum(String code, String displayName) {
        this.code = code;
        this.displayName = displayName;
    }

    public static ChatModelEnum fromCode(String code) {
        for (ChatModelEnum m : values()) {
            if (m.code.equalsIgnoreCase(code)) return m;
        }
        throw new IllegalArgumentException("未知模型: " + code);
    }
}

五、ChatClient 配置

package com.example.openrouter.config;

import org.springframework.ai.chat.client.ChatClient;
import org.springframework.ai.openai.OpenAiChatModel;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@Configuration
public class ChatClientConfig {

    /**
     * 全局共享一个 ChatClient,模型在调用时通过 options 动态覆盖。
     */
    @Bean
    public ChatClient chatClient(OpenAiChatModel chatModel) {
        return ChatClient.builder(chatModel).build();
    }
}

六、核心服务:按用户动态切换模型

package com.example.openrouter.service;

import com.example.openrouter.model.ChatModelEnum;
import org.springframework.ai.chat.client.ChatClient;
import org.springframework.ai.chat.messages.UserMessage;
import org.springframework.ai.chat.model.ChatResponse;
import org.springframework.ai.openai.OpenAiChatOptions;
import org.springframework.stereotype.Service;

import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;

@Service
public class ChatService {

    private final ChatClient chatClient;

    /**
     * 记录每个用户当前选择的模型(生产环境可换成 Redis / 数据库)。
     * key: userId,value: 模型 code
     */
    private final Map<String, String> userModelMap = new ConcurrentHashMap<>();

    public ChatService(ChatClient chatClient) {
        this.chatClient = chatClient;
    }

    /** 用户切换模型 */
    public void switchModel(String userId, String modelCode) {
        // 校验模型是否合法
        ChatModelEnum.fromCode(modelCode);
        userModelMap.put(userId, modelCode);
    }

    /** 获取用户当前模型,未设置则用默认 */
    public String currentModel(String userId) {
        return userModelMap.getOrDefault(userId, ChatModelEnum.GPT4O_MINI.getCode());
    }

    /** 同步聊天 */
    public String chat(String userId, String userInput) {
        String model = currentModel(userId);

        return chatClient.prompt()
                .user(userInput)
                // 关键:在调用级别覆盖模型,实现动态切换
                .options(OpenAiChatOptions.builder()
                        .model(model)
                        .temperature(0.7)
                        .build())
                .call()
                .content();
    }

    /** 带完整响应(可拿 usage / 元数据) */
    public ChatResponse chatWithMeta(String userId, String userInput) {
        return chatClient.prompt()
                .messages(new UserMessage(userInput))
                .options(OpenAiChatOptions.builder()
                        .model(currentModel(userId))
                        .build())
                .call()
                .chatResponse();
    }
}

七、Controller 接口

package com.example.openrouter.controller;

import com.example.openrouter.model.ChatModelEnum;
import com.example.openrouter.service.ChatService;
import org.springframework.web.bind.annotation.*;

import java.util.Arrays;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;

@RestController
@RequestMapping("/api/chat")
public class ChatController {

    private final ChatService chatService;

    public ChatController(ChatService chatService) {
        this.chatService = chatService;
    }

    /** 列出所有可用模型 */
    @GetMapping("/models")
    public List<Map<String, String>> models() {
        return Arrays.stream(ChatModelEnum.values())
                .map(m -> Map.of("code", m.getCode(), "name", m.getDisplayName()))
                .collect(Collectors.toList());
    }

    /** 用户切换模型 */
    @PostMapping("/switch")
    public Map<String, String> switchModel(@RequestHeader("X-User-Id") String userId,
                                            @RequestParam String model) {
        chatService.switchModel(userId, model);
        return Map.of("userId", userId, "currentModel", chatService.currentModel(userId));
    }

    /** 聊天 */
    @PostMapping
    public Map<String, String> chat(@RequestHeader("X-User-Id") String userId,
                                     @RequestBody Map<String, String> body) {
        String reply = chatService.chat(userId, body.get("message"));
        return Map.of(
                "userId", userId,
                "model", chatService.currentModel(userId),
                "reply", reply
        );
    }
}

八、流式输出(SSE)可选

public Flux<String> chatStream(String userId, String userInput) {
    return chatClient.prompt()
            .user(userInput)
            .options(OpenAiChatOptions.builder()
                    .model(currentModel(userId))
                    .build())
            .stream()
            .content();
}

Controller 用 text/event-stream 返回即可。

©著作权归作者所有,转载或内容合作请联系作者
【社区内容提示】社区部分内容疑似由AI辅助生成,浏览时请结合常识与多方信息审慎甄别。
平台声明:文章内容(如有图片或视频亦包括在内)由作者上传并发布,文章内容仅代表作者本人观点,简书系信息发布平台,仅提供信息存储服务。

友情链接更多精彩内容