☰
Spring AI实战初体验——用TaoToken统一Key实现可切换模型AI聊天助手
2026/10/7 7:47:18 网站建设 项目流程

1. 从本地 ollama 到云端模型:多模型切换到底卡在哪

做 Spring Boot + Spring AI + Vue2 的聊天助手,最容易踩的坑不是流式返回写不对,而是模型一多,配置就散架。我一开始只接了本地 ollama 的 deepseek-r1,application.yml 里写死一个 base-url,跑得挺顺。后来想加 qwen、再加一个云端模型做对比,问题立刻冒出来:每个模型一个地址、一个 Key、一套参数,前端下拉框切一下,后端就得重新 new 一个 ChatClient,稍不注意就把上一个模型的记忆和状态串了。

Spring AI 本身的设计是鼓励可移植的,ChatClient 抽象层把不同厂商的模型统一成 prompt().user().stream() 这套调用。但它的默认配置只认一个模型,多模型场景得自己动手。核心矛盾有三个:第一,模型地址和凭证怎么集中管理,而不是散落在各个 @Value 里;第二,前端切换模型后,后端如何在不重启服务的前提下拿到对应的 ChatClient;第三,每个用户的聊天记忆要跟模型解耦,切模型不能丢上下文,也不能把 A 模型的记忆喂给 B 模型。

这篇就围绕这三个矛盾展开。目标很明确:给你一份能直接复制的 application.yml 配置,通过 TaoToken 统一 Key 和 API 通道接入不同模型,再配合 Vue2 前端切换,做到复制配置就能完成模型热切换。适合已经会用 Spring Boot 写接口、但对 Spring AI 多模型管理还没头绪的开发者。你不需要提前把每个模型的 SDK 都摸一遍,只要理解 ChatClient 的构建链路,剩下的就是配置和映射的事。

我试过把每个模型的配置写成独立的 Bean,结果类越写越多,改一个参数要动三四个文件。后来改成「统一通道 + 模型注册表」的思路,配置文件只维护模型清单,代码里用 Map 动态构建,清爽很多。下面按这个思路一步步来。

2. TaoToken 前置:统一 Key 与 API 通道的接入准备

在写配置之前,先把 TaoToken 这一层理解清楚。它在这里扮演的角色是「统一入口」:你不需要为每个云端模型单独申请 Key、单独记 base-url,而是通过一个 API 通道和一份 Key,就能访问多个模型。对 Spring AI 来说,这意味着 application.yml 里可以少维护一堆凭证,模型切换只改 model 名,不改连接信息。

先拿到两样东西:API Key 和接入地址。Key 在控制台的 API Keys 页面创建,地址用 https://taotoken.net/api 作为 base-url。注意这个地址不带任何多余路径,Spring AI 的 OpenAI 兼容客户端会自动拼接 /v1/chat/completions 这类端点。如果你用的是 OpenAI 兼容模式接入,base-url 就填这个,不要自己加 /v1。

模型对话的调试入口在 https://taotoken.net/api?utm_source=taotoken_aicg_blog_end&utm_medium=csdn&utm_campaign=rewrite ,你可以先在网页里发一条消息,确认 Key 和模型名对得上,再去写代码。这一步能省掉后面很多「401 到底是 Key 错还是模型名错」的排查时间。

接入文档在 https://taotoken.net/doc?utm_source=taotoken_aicg_blog_end&utm_medium=csdn&utm_campaign=rewrite ,里面列了当前支持的模型 ID 和参数格式。重点看两处:模型 ID 的准确写法(大小写、连字符),以及是否支持流式。Spring AI 的 stream() 依赖服务端 SSE,如果某个模型不支持,前端会一直转圈。

如果你后面要做长期编码或 Agent 类任务,可以了解 Coding Plan:https://taotoken.net/coding-plan?utm_source=taotoken_aicg_blog_end&utm_medium=csdn&utm_campaign=rewrite 。它和单次对话的区别在于额度模型和并发策略,聊天助手这种交互式场景用按量就够,不用一上来就上套餐。

Key 管理页面在 https://taotoken.net/api-keys?utm_source=taotoken_aicg_blog_end&utm_medium=csdn&utm_campaign=rewrite ,建议给这个项目单独建一个 Key,方便后面按项目统计用量,也方便泄露时单独吊销。控制台入口是 https://taotoken.net/console?utm_source=taotoken_aicg_blog_end&utm_medium=csdn&utm_campaign=rewrite 。

这里有个容易忽略的点:TaoToken 的 Key 是给云端模型用的,本地 ollama 不需要 Key。所以你的配置里会同时存在「带 Key 的云端模型」和「不带 Key 的本地模型」,统一管理的难点就在这。我的做法是抽象一个 ModelProvider 概念,每个模型声明自己是 local 还是 remote,remote 的走统一通道,local 的走本地地址。这样前端切换时,后端根据 provider 类型决定用哪套客户端。

3. 可复制配置:application.yml 与模型注册表

这一节是全文的核心,配置写对了,后面基本就是验证。先看 application.yml 的完整片段。我把模型清单放在自定义的 ai.models 下,每个模型有 id、provider、base-url、api-key、model-name 五个字段。本地 ollama 的 api-key 留空,云端模型统一填 TaoToken 的 Key。

server: port: 8081 spring: application: name: spring-ai-chat ai: # TaoToken 统一通道配置 taotoken: base-url: https://taotoken.net/api api-key: sk-你的TaoTokenKey # 模型注册表:前端下拉框的数据源 models: - id: deepseek-r1 provider: local base-url: http://192.168.100.145:11434 api-key: "" model-name: deepseek-r1:latest - id: qwen-7b provider: local base-url: http://192.168.100.211:11434 api-key: "" model-name: qwen:7b - id: gpt-4o-mini provider: remote base-url: https://taotoken.net/api api-key: sk-你的TaoTokenKey model-name: gpt-4o-mini - id: claude-sonnet provider: remote base-url: https://taotoken.net/api api-key: sk-你的TaoTokenKey model-name: claude-3-5-sonnet

注意 remote 模型的 base-url 和 api-key 都指向 TaoToken,model-name 才是真正区分模型的字段。这样你加一个新云端模型,只需要在列表里加一项,不用改任何 Java 代码。

接下来是配置类,把 yml 里的列表绑定成对象。用 @ConfigurationProperties 比一堆 @Value 干净得多。

@Data @Component @ConfigurationProperties(prefix = "ai") public class AiProperties { private TaotokenConfig taotoken; private List<ModelConfig> models; @Data public static class TaotokenConfig { private String baseUrl; private String apiKey; } @Data public static class ModelConfig { private String id; private String provider; // local / remote private String baseUrl; private String apiKey; private String modelName; } }

然后是模型客户端工厂。核心逻辑:根据 provider 类型决定构建哪种 ChatModel。local 用 OllamaChatModel,remote 用 OpenAiChatModel(TaoToken 兼容 OpenAI 协议)。每个模型构建一次后缓存起来,避免每次请求都 new。

@Component public class ChatClientFactory { private final AiProperties aiProperties; private final Map<String, ChatClient> clientCache = new ConcurrentHashMap<>(); public ChatClientFactory(AiProperties aiProperties) { this.aiProperties = aiProperties; } public ChatClient getClient(String modelId) { return clientCache.computeIfAbsent(modelId, id -> { AiProperties.ModelConfig cfg = aiProperties.getModels().stream() .filter(m -> m.getId().equals(id)) .findFirst() .orElseThrow(() -> new IllegalArgumentException("未知模型: " + id)); if ("local".equals(cfg.getProvider())) { OllamaApi ollamaApi = new OllamaApi(cfg.getBaseUrl()); OllamaOptions options = OllamaOptions.builder() .model(cfg.getModelName()) .temperature(0.3) .topP(0.6) .build(); OllamaChatModel chatModel = new OllamaChatModel( ollamaApi, options, new NoOpToolCallingManager(), ObservationRegistry.create(), ModelManagementOptions.defaults()); return ChatClient.builder(chatModel) .defaultSystem("你是一个智能助手,回答简洁清晰,首选中文。") .build(); } else { OpenAiApi openAiApi = new OpenAiApi(cfg.getBaseUrl(), cfg.getApiKey()); OpenAiChatOptions options = OpenAiChatOptions.builder() .model(cfg.getModelName()) .temperature(0.3) .build(); OpenAiChatModel chatModel = new OpenAiChatModel( openAiApi, options, new NoOpToolCallingManager(), ObservationRegistry.create(), ModelManagementOptions.defaults()); return ChatClient.builder(chatModel) .defaultSystem("你是一个智能助手,回答简洁清晰,首选中文。") .build(); } }); } }

这里有个关键点:ChatClient 缓存的是「模型级」的实例,不带用户记忆。用户记忆通过 Advisor 在每次请求时动态注入,这样同一个模型可以被多个用户共享,记忆互不干扰。如果你把 MessageChatMemoryAdvisor 写进缓存的 ChatClient 里,所有用户就会共用一份记忆,这是新手最容易犯的错。

前端下拉框的数据源,直接暴露一个接口返回模型列表:

@GetMapping("/models") public List<Map<String, String>> listModels() { return aiProperties.getModels().stream() .map(m -> Map.of("id", m.getId(), "name", m.getModelName())) .collect(Collectors.toList()); }

Vue2 那边拿到这个列表渲染 select,切换时把 model id 传给后端即可。这样加模型只改 yml,前端自动多一个选项。

4. 验证请求:从 curl 到 Vue2 切换的完整链路

配置写完,先别急着开前端,用 curl 把后端链路跑通。启动 Spring Boot 后,先测模型列表接口:

curl http://localhost:8081/ai/models

预期返回一个 JSON 数组,包含 deepseek-r1、qwen-7b、gpt-4o-mini 等。如果这里报 500,多半是 yml 缩进或字段名对不上,检查 @ConfigurationProperties 的 prefix 和字段名是否一致。

接着测非流式对话,确认 TaoToken 通道能通:

curl "http://localhost:8081/ai/chat?message=你好&model=gpt-4o-mini"

如果返回 401,说明 Key 或 base-url 有问题。注意 base-url 必须是 https://taotoken.net/api,不要带尾部斜杠,也不要自己加 /v1。如果返回「未知模型」,检查 model 参数是否和 yml 里的 id 完全一致。

再测流式,这是聊天助手真正用的接口:

curl -N "http://localhost:8081/ai/chatStreamWithMemory?message=介绍一下Spring AI&userId=test1&model=deepseek-r1"

-N 关闭缓冲,你能看到 SSE 逐字返回。如果卡住不动,先确认本地 ollama 服务在跑,且模型名和 ollama list 里的一致。切到 gpt-4o-mini 再跑一次,对比响应速度。

后端通了,前端 Vue2 的切换逻辑就简单了。核心是 changeModel 时只改 currentModel 变量,下次 sendMessage 带上新的 model 参数。EventSource 每次请求重新创建,避免复用旧连接。

changeModel(event) { const modelName = event.target.options[event.target.selectedIndex].text; this.currentModel = event.target.value; this.messages.push({ text: `已切换模型为 ${modelName}`, type: 'ai-message' }); }, streamAIResponse(message) { if (this.eventSource) this.eventSource.close(); const url = `http://localhost:8081/ai/chatStreamWithMemory?userId=${this.userId}` + `&message=${encodeURIComponent(message)}&model=${this.currentModel}`; this.eventSource = new EventSource(url); // ... onmessage 追加到 aiMessage.text }

验证成功的结果是:你在下拉框选 deepseek-r1 问一句,再切到 gpt-4o-mini 问同一句,两个模型各自回答,且聊天记录连续保留。切回 deepseek-r1 时,它还记得你之前说过什么。这就说明记忆和模型解耦做对了。

如果切模型后记忆丢了,检查 MessageChatMemoryAdvisor 是不是绑在了缓存的 ChatClient 上。正确做法是在请求时动态加 Advisor:

ChatClient client = chatClientFactory.getClient(model); return client.prompt() .user(msg) .advisors(new MessageChatMemoryAdvisor(memory)) .stream() .chatResponse() .map(r -> r.getResult().getOutput().getText());

5. 本篇常见错排查:401、local proxy failed 与 reading choices

这一节把实际会撞到的报错列出来,对照着改。

401 Unauthorized:最常见。三种可能:Key 写错、base-url 写错、Key 没生效。先确认 yml 里 api-key 是 sk- 开头且没有多余空格。再确认 base-url 是 https://taotoken.net/api,不是 https://taotoken.net 也不是带 /v1 的地址。如果都对,去 API Keys 页面确认这个 Key 没被吊销、额度没耗尽。

local proxy failed / connection refused:本地 ollama 没启动,或者 base-url 的 IP 端口不对。先在浏览器或 curl 访问 http://你的ollama地址:11434,确认能返回 Ollama is running。如果 ollama 在另一台机器,确认防火墙放行了 11434 端口。注意本地模型不需要 TaoToken Key,别把云端 Key 填到 local 配置里。

Error reading choices / 解析响应失败:通常是模型名不对,或者该模型不支持 OpenAI 兼容格式。检查 model-name 是否和 TaoToken 文档里列的一致。有些模型 ID 带版本号,比如 claude-3-5-sonnet 后面可能还有日期后缀,以文档为准。如果模型名对但还报这个错,可能是流式和非流式端点混用,确认你的 ChatModel 构建时用的 options 和实际请求匹配。

OAuth / token 过期类报错:如果你用的是需要 OAuth 的接入方式,检查 token 刷新逻辑。TaoToken 的 Key 是长期有效的,一般不会遇到这个问题。如果出现,重新在控制台生成一个 Key 替换即可。

切换模型后前端一直转圈:EventSource 的 onerror 没处理,或者后端流式接口抛异常没返回。在 onerror 里 close 并恢复按钮状态。后端加日志,确认是模型调用失败还是序列化失败。

记忆串了:两个用户看到对方的聊天记录。检查 userChatMemory 的 key 是不是 userId,以及 MessageChatMemoryAdvisor 是不是每次请求新建。如果 Advisor 被缓存,所有用户共用一份记忆。

跨域报错:Vue2 开发服务器和后端端口不同,浏览器控制台报 CORS。后端加 CorsConfig,allowedOriginPatterns 设为,或者精确到前端地址。注意 allowedOrigins("") 和 allowCredentials(true) 不能同时用,用 allowedOriginPatterns 替代。

排查顺序建议:先 curl 后端,再开前端。后端不通就别调前端,否则你分不清是接口问题还是跨域问题。每次只改一个变量,改完立刻验证,别一次改三处再一起测。

6. 语义一致 CTA:把配置落到你的项目里

到这里,你应该已经有一份能跑的 application.yml 和一套模型切换逻辑了。接下来最值得做的一件事,是把你实际要用的模型 ID 补全。TaoToken 的模型列表会更新,接入文档里能查到最新的 ID 和参数:https://taotoken.net/doc?utm_source=taotoken_aicg_blog_end&utm_medium=csdn&utm_campaign=rewrite 。建议先把模型对话页面打开,逐个试一遍你要接的模型,确认返回正常再写进 yml:https://taotoken.net/api?utm_source=taotoken_aicg_blog_end&utm_medium=csdn&utm_campaign=rewrite 。

Key 的管理别偷懒,给这个聊天助手项目单独建一个:https://taotoken.net/api-keys?utm_source=taotoken_aicg_blog_end&utm_medium=csdn&utm_campaign=rewrite 。后面如果要做用量统计或者临时吊销,单独 Key 会省很多事。控制台里能看到每个 Key 的调用情况:https://taotoken.net/console?utm_source=taotoken_aicg_blog_end&utm_medium=csdn&utm_campaign=rewrite 。

如果你打算把这个聊天助手往 Agent 方向做,比如加工具调用、多轮任务规划,可以看看 Coding Plan 的额度模型是否更适合:https://taotoken.net/coding-plan?utm_source=taotoken_aicg_blog_end&utm_medium=csdn&utm_campaign=rewrite 。交互式聊天用按量就够,别提前上套餐。

最后提醒一个实操细节:yml 里的模型列表改完后,Spring Boot 需要重启才能生效,因为 @ConfigurationProperties 是启动时绑定的。如果你想要真正的「不重启热切换」,可以把模型列表放到数据库或配置中心,加一个刷新接口清空 clientCache。但对大多数项目来说,重启一次几秒钟的事,没必要过度设计。先把能跑的版本落地,再考虑优化。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询