1. SpringAI 项目搭建时多模型 Key 混乱的真实场景
做 SpringAI + AIAgent 项目最烦的不是写业务代码,而是配置。你打开一个开源仓库,application.yml 里躺着三四个 api-key:OpenAI 一个、DeepSeek 一个、Ollama 本地一个,MCP 服务端还要单独配 token。等你想把 ChatClient 从 deepseek-chat 切到 gpt-4.1 做对比测试,发现得改 yml、重启、再改回来,一个下午就没了。
这个场景在 SpringAI 里特别典型,因为它的自动装配机制会同时读取spring.ai.openai.*、spring.ai.ollama.*、spring.ai.mcp.client.*三套配置。你只要引入对应 starter,它就会尝试装配对应的 ChatModel、EmbeddingModel、ToolCallbackProvider。多模型源并存时,Bean 冲突、Embedding 模型选错、MCP 工具重复注册这三个坑几乎必踩。
我这次要解决的核心问题就一个:用 TaoToken 的统一 Key 和统一 API 通道,把 ChatClient 对话、MCP 工具调用、RAG 向量检索这三条链路的模型出口收敛到一个 base-url 上。这样你切换模型只改一个 model 名,不用动 Key,也不用维护多套凭证。
TaoToken 在这里扮演的角色是「模型网关」:它对外暴露 OpenAI 兼容的/v1/chat/completions和/v1/embeddings接口,对内帮你路由到不同模型。对 SpringAI 来说,它就是一个标准的 OpenAI 兼容服务,所以spring-ai-starter-model-openai这个 starter 可以直接用,不需要额外写适配层。
适合谁看:正在用 Spring Boot 3.x + SpringAI 1.0.0 搭 AIAgent、需要同时接 ChatClient 和 MCP、并且被多模型配置折磨过的后端开发。如果你还在用 1.0.0-M 系列,建议先升到正式版,后面会讲为什么。
先说结论性的配置思路,避免你走弯路:
- 所有对话模型(deepseek-chat、gpt-4.1、qwen 等)统一走
spring.ai.openai这一套配置,base-url 指向 TaoToken。 - Embedding 模型单独处理,因为不是所有模型都提供 embedding,这点后面 §5 会重点讲。
- MCP 的 SSE 连接和 stdio 连接分开配,工具注册用
SyncMcpToolCallbackProvider统一收口。 - 切换模型只改
spring.ai.openai.chat.options.model,Key 和 base-url 不动。
下面按「前置准备 → 可复制配置 → 验证请求 → 排错」的顺序展开,每一步都给完整代码。
2. TaoToken 前置准备:拿到统一 Key 与 API 通道
在写 SpringAI 配置之前,先把 TaoToken 这边的凭证和地址准备好。这一步很快,但地址别写错,否则后面 401 会查半天。
你需要准备三样东西:
第一是 API Key。登录 TaoToken 控制台,在 API Keys 页面创建一个新 Key。建议按项目命名,比如springai-agent-dev,方便后面区分环境。创建后立刻复制保存,页面刷新后就看不到完整 Key 了。
第二是 Base URL。TaoToken 的 API 入口是:
https://taotoken.net/api注意这里不要加 UTM 参数,API 调用地址保持干净。SpringAI 的 OpenAI starter 会自动在这个 base-url 后面拼/v1/chat/completions,所以你在 yml 里填的 base-url 就是上面这个,不要自己再加/v1,否则会变成/api/v1/v1/chat/completions,直接 404。
第三是确认你要用的 Model ID。TaoToken 支持多模型路由,Model ID 就是你在请求里传的model字段。比如deepseek-chat、gpt-4.1、claude-sonnet-4这类。具体可用列表在控制台的模型页面能看到,也可以直接调一次模型对话页面验证。
如果你只是想先跑通对话,不想马上写代码,可以打开模型对话页面直接测:
https://taotoken.net/models?utm_source=taotoken_aicg_blog_end&utm_medium=csdn&utm_campaign=rewrite&utm_content=model_chat在页面里选一个模型,输入一句话,能返回就说明 Key 和通道没问题。这一步相当于给你的 SpringAI 配置做了一次「人工预检」,比在 IDE 里 debug 快得多。
关于 Coding Plan:如果你这个 AIAgent 项目是长期开发、需要频繁调用模型做代码生成和 Agent 编排,可以看下 Coding Plan,它更适合高频编码场景:
https://taotoken.net/coding-plan?utm_source=taotoken_aicg_blog_end&utm_medium=csdn&utm_campaign=rewrite&utm_content=coding_plan接入文档在这里,遇到路径或参数问题优先查它:
https://taotoken.net/doc?utm_source=taotoken_aicg_blog_end&utm_medium=csdn&utm_campaign=rewrite&utm_content=docAPI Keys 管理页面:
https://taotoken.net/api-keys?utm_source=taotoken_aicg_blog_end&utm_medium=csdn&utm_campaign=rewrite&utm_content=api_keys控制台首页:
https://taotoken.net/console?utm_source=taotoken_aicg_blog_end&utm_medium=csdn&utm_campaign=rewrite&utm_content=console前置准备做完,你手上应该有:一个sk-开头的 Key、一个https://taotoken.net/api的 base-url、一个确定可用的 model 名。接下来进代码。
3. 可复制配置:application.yml 与 config.toml 骨架
这一节是全文的核心,给两份可直接复制的配置骨架。一份是 SpringAI 的application.yml,一份是 MCP 客户端的config.toml(对应 stdio 模式的 servers-configuration)。
先说 Maven 依赖。SpringAI 1.0.0 正式版用 BOM 统一管理版本,这是避免版本错乱的第一道防线:
<dependencyManagement> <dependencies> <dependency> <groupId>org.springframework.ai</groupId> <artifactId>spring-ai-bom</artifactId> <version>1.0.0</version> <type>pom</type> <scope>import</scope> </dependency> </dependencies> </dependencyManagement>然后按需引入 starter。对话和 embedding 走 openai starter,MCP 客户端走 webflux starter,向量库用 pgvector:
<dependency> <groupId>org.springframework.ai</groupId> <artifactId>spring-ai-starter-model-openai</artifactId> </dependency> <dependency> <groupId>org.springframework.ai</groupId> <artifactId>spring-ai-starter-vector-store-pgvector</artifactId> </dependency> <dependency> <groupId>org.springframework.ai</groupId> <artifactId>spring-ai-starter-mcp-client-webflux</artifactId> </dependency> <dependency> <groupId>org.springframework.ai</groupId> <artifactId>spring-ai-tika-document-reader</artifactId> </dependency>注意:1.0.0 正式版的 artifactId 是spring-ai-starter-model-openai,而 1.0.0-M 系列是spring-ai-openai-spring-boot-starter。这两个名字不一样,抄旧博客的配置会直接报找不到依赖。这是第一个高频坑。
下面是application.yml的完整骨架,Key 和 base-url 都指向 TaoToken:
spring: application: name: springai-agent-demo ai: openai: # TaoToken 统一 Key,所有对话模型共用 api-key: ${TAOTOKEN_API_KEY:sk-你的Key} # TaoToken 统一 API 通道 base-url: https://taotoken.net/api chat: options: # 切换模型只改这一行 model: deepseek-chat temperature: 0.7 embedding: options: # embedding 单独指定,见 §5 说明 model: text-embedding-3-small mcp: client: enabled: true name: springai-agent-client version: 1.0.0 request-timeout: 360s type: SYNC sse: connections: mcp-server-csdn: url: http://127.0.0.1:8101 mcp-server-weixin: url: http://127.0.0.1:8102几个关键点解释一下:
api-key用${TAOTOKEN_API_KEY:...}这种写法,是为了本地开发用默认值、生产环境用环境变量覆盖。别把真实 Key 提交到 Git。
base-url填https://taotoken.net/api,不要带/v1。SpringAI 的OpenAiApi默认 completionsPath 是/v1/chat/completions,它会自己拼。
model放在chat.options下,这是对话模型。切换模型时只改这里,Key 和 base-url 完全不动,这就是统一 Key 的价值。
mcp.client.sse.connections下每个 key 是一个 MCP 服务名,url 是 SSE 端点。如果你用 stdio 模式,把这段换成stdio.servers-configuration指向配置文件。
接下来是 stdio 模式的config.toml骨架。SpringAI 的 stdio 配置支持 JSON 和 TOML 两种格式,TOML 更清爽,推荐用 TOML:
[mcpServers.mcp-server-filesystem] command = "npx" args = ["-y", "@modelcontextprotocol/server-filesystem", "/Users/yourname/Desktop"] [mcpServers.mcp-server-csdn] command = "java" args = [ "-Dspring.ai.mcp.server.stdio=true", "-jar", "/opt/mcp/mcp-server-csdn-1.0.0.jar", "--csdn.api.categories=Java" ]然后在 yml 里把 SSE 那段换成:
spring: ai: mcp: client: stdio: servers-configuration: classpath:/config/mcp-servers-config.tomlclasspath:是打包进 jar 的路径,filepath:是服务器上的绝对路径。本地开发用 classpath,部署到服务器用 filepath,这样改配置不用重新打包。
配置骨架给完了,下面是 Java 侧的装配代码。ChatClient 的 Bean 定义:
@Slf4j @Configuration public class ChatClientConfig { @Bean public ChatClient chatClient(OpenAiChatModel model, ToolCallbackProvider toolCallbackProvider) { log.info("初始化 ChatClient,模型出口统一走 TaoToken"); return ChatClient.builder(model) .defaultSystem("你是一个 AI Agent,可以调用工具完成文章发布和通知。") .defaultTools(toolCallbackProvider) .build(); } }这里OpenAiChatModel是自动装配的,它读取的就是 yml 里spring.ai.openai那套配置。你不需要手动 newOpenAiApi,除非要做多模型源的高级玩法。
如果你确实需要手动装配(比如一个项目里同时接两个不同 base-url 的模型源),可以这样写:
@Bean public OpenAiChatModel customChatModel() { OpenAiApi api = OpenAiApi.builder() .apiKey(System.getenv("TAOTOKEN_API_KEY")) .baseUrl("https://taotoken.net/api") .completionsPath("/v1/chat/completions") .embeddingsPath("/v1/embeddings") .build(); return OpenAiChatModel.builder() .openAiApi(api) .defaultOptions(OpenAiChatOptions.builder() .model("gpt-4.1") .build()) .build(); }手动装配的好处是你可以给不同的 ChatClient 配不同的模型,但 Key 和 base-url 还是同一个 TaoToken 通道。这就是「统一 Key、多模型出口」的落地方式。
4. 验证请求:ChatClient 调用与 MCP 工具注册
配置写完不验证,等于没写。这一节给两个验证动作:一个验证 ChatClient 能正常对话,一个验证 MCP 工具能注册进 ChatClient。
先验证 ChatClient。写一个测试类:
@Slf4j @SpringBootTest class ChatClientVerifyTest { @Resource private ChatClient chatClient; @Test void testChatClientCall() { String content = chatClient.prompt() .user("用一句话说明 SpringAI 的 ChatClient 是什么") .call() .content(); log.info("ChatClient 返回: {}", content); assert content != null && !content.isEmpty(); } @Test void testChatClientStream() throws InterruptedException { CountDownLatch latch = new CountDownLatch(1); Flux<String> stream = chatClient.prompt() .user("数一下 1 到 5") .stream() .content(); stream.subscribe( chunk -> log.info("流式片段: {}", chunk), Throwable::printStackTrace, latch::countDown ); latch.await(30, TimeUnit.SECONDS); } }跑testChatClientCall,如果控制台打印出模型返回的内容,说明 TaoToken 通道、Key、model 三者都对。如果报 401,看 §5。
再验证 MCP 工具注册。MCP 工具注册的核心是ToolCallbackProvider,它会把所有 MCP 客户端暴露的工具收集起来,注入到 ChatClient。SpringAI 1.0.0 有个已知问题:多个 MCP 客户端如果 server name 重复,会导致工具重复注册,报multiname相关错误。解决办法是手动去重:
@Bean("syncMcpToolCallbackProvider") public SyncMcpToolCallbackProvider syncMcpToolCallbackProvider( List<McpSyncClient> mcpClients) { Map<String, Integer> nameToIndex = new HashMap<>(); Set<Integer> duplicates = new HashSet<>(); for (int i = 0; i < mcpClients.size(); i++) { String name = mcpClients.get(i).getServerInfo().name(); if (nameToIndex.containsKey(name)) { duplicates.add(i); } else { nameToIndex.put(name, i); } } List<Integer> sorted = new ArrayList<>(duplicates); sorted.sort(Collections.reverseOrder()); for (int index : sorted) { mcpClients.remove(index); } return new SyncMcpToolCallbackProvider(mcpClients); }然后写一个测试,问模型「有哪些工具可以使用」,看它能不能列出 MCP 注册的工具:
@Test void testMcpToolsRegistered() { String content = chatClient.prompt() .user("你现在有哪些工具可以使用?只列工具名") .call() .content(); log.info("可用工具: {}", content); }如果模型返回了工具列表,说明 MCP 注册成功。如果返回「我没有工具」,检查defaultTools(toolCallbackProvider)有没有加上,以及 MCP 服务端是否真的启动了。
手动装配 MCP 工具的写法也给你一份,适合按需给不同 ChatClient 配不同工具:
public McpSyncClient sseMcpClient(String url) { HttpClientSseClientTransport transport = HttpClientSseClientTransport.builder(url).build(); McpSyncClient client = McpClient.sync(transport) .requestTimeout(Duration.ofMinutes(3)) .build(); var init = client.initialize(); log.info("MCP 初始化: {}", init); return client; } @Bean public OpenAiChatModel modelWithTools() { OpenAiApi api = OpenAiApi.builder() .apiKey(System.getenv("TAOTOKEN_API_KEY")) .baseUrl("https://taotoken.net/api") .build(); return OpenAiChatModel.builder() .openAiApi(api) .defaultOptions(OpenAiChatOptions.builder() .model("deepseek-chat") .toolCallbacks(new SyncMcpToolCallbackProvider( sseMcpClient("http://127.0.0.1:8101"), sseMcpClient("http://127.0.0.1:8102") ).getToolCallbacks()) .build()) .build(); }这段代码里,Key 和 base-url 还是 TaoToken 那一套,只是工具来源不同。验证方式和上面一样,问模型有哪些工具即可。
5. 本篇常见错排查:401、local proxy failed、reading choices、OAuth
这一节按真实报错来,每个错误给现象、原因、解决。
错误一:401 Unauthorized
现象:ChatClient 调用返回 401,日志里能看到401 Unauthorized或invalid_api_key。
原因通常有三个:Key 写错、Key 没生效、base-url 拼错导致请求打到了错误端点。
排查顺序:先确认 yml 里api-key是不是完整的sk-开头字符串,有没有多余空格。再确认base-url是https://taotoken.net/api,没有多写/v1。最后去 TaoToken 控制台确认这个 Key 还在有效期内、额度没耗尽。
如果你用的是环境变量${TAOTOKEN_API_KEY},检查 IDE 的运行配置里有没有注入这个变量。IDEA 里在 Run Configuration 的 Environment variables 里加,别只在系统环境变量里加,因为 IDE 可能读不到。
错误二:local proxy failed / Connection refused
现象:启动时报local proxy failed或Connection refused,MCP 客户端连不上。
原因:MCP 服务端的 SSE 端点没启动,或者 url 写错。SSE 模式下,MCP 服务端必须先跑起来,客户端才能连。
排查:先用 curl 测一下 MCP 服务端是否活着:
curl -N http://127.0.0.1:8101/sse如果返回连接拒绝,说明服务端没起。检查 MCP 服务端的application.yml里server.port是不是 8101,以及有没有加spring-ai-mcp-server-webflux-spring-boot-starter依赖。SSE 模式必须用 webflux starter,用普通的 web starter 起不来 SSE 端点。
错误三:reading choices 相关报错
现象:日志里出现Error reading choices或Cannot deserialize value of type ... from Object value。
原因:模型返回的响应结构和 SpringAI 期望的不一致。常见于用了非 OpenAI 兼容的模型,或者 base-url 指向了一个返回格式不同的服务。
排查:先确认 TaoToken 的 base-url 是https://taotoken.net/api,它返回的是标准 OpenAI 格式。如果还是报错,把completionsPath显式写成/v1/chat/completions,有时候自动拼接会出问题。
还有一种情况是模型名写错,服务端返回了错误 JSON,SpringAI 解析失败。去 TaoToken 控制台确认 model 名拼写正确。
错误四:OAuth / 认证失败
现象:MCP 客户端连接时报 OAuth 相关错误,或者authentication failed。
原因:某些 MCP 服务端需要额外的认证头,而 SpringAI 的 SSE 配置默认不带自定义 header。
排查:如果 MCP 服务端需要 token,用HttpClientSseClientTransport手动加 header:
HttpClientSseClientTransport transport = HttpClientSseClientTransport .builder("http://127.0.0.1:8101") .build();然后在 MCP 服务端侧配置认证。注意:这里说的是 MCP 服务端自己的认证,不是 TaoToken 的认证。TaoToken 的认证就是 API Key,走的是Authorization: Bearer sk-xxx,SpringAI 的 OpenAI starter 会自动加。
错误五:Embedding 模型冲突
现象:启动时报NoUniqueBeanDefinitionException,说有两个 EmbeddingModel。
原因:你同时引入了 openai starter 和 ollama starter,两个都提供了 EmbeddingModel,Spring 不知道该注入哪个。
解决:手动装配 VectorStore,显式指定用哪个 EmbeddingModel:
@Bean public PgVectorStore pgVectorStore(JdbcTemplate jdbcTemplate, OpenAiEmbeddingModel embeddingModel) { return PgVectorStore.builder(jdbcTemplate, embeddingModel) .vectorTableName("vector_store") .dimensions(1536) .build(); }注意 dimensions 要和 embedding 模型的输出维度一致。text-embedding-3-small是 1536 维,nomic-embed-text是 768 维。写错了插入向量时会报维度不匹配。
这里补充一个关键点:不是所有对话模型都提供 embedding。比如 deepseek-chat 就没有 embedding 接口。所以你的 RAG 链路里,对话可以用 deepseek-chat,但 embedding 必须单独指定一个支持 embedding 的模型。在 TaoToken 里,你可以对话走 deepseek-chat,embedding 走 text-embedding-3-small,两者共用同一个 Key 和 base-url,这就是统一通道的好处。
错误六:MCP 工具重复注册 multiname
现象:启动时报multiname或工具名冲突。
原因:多个 MCP 客户端返回了同名工具,或者同一个 MCP 服务被注册了两次。
解决:用 §4 里的去重代码,在SyncMcpToolCallbackProvider构造前把重复的 McpSyncClient 移除。
排查时可以用这段代码打印所有 MCP 客户端的 server name:
mcpClients.forEach(c -> log.info("MCP server: {}", c.getServerInfo().name()));如果看到重复的 name,就是这个问题。
6. 把 ChatClient、MCP、RAG 收敛到一条通道
到这里,配置、验证、排错都走完了。回到最初的目标:一次配置跑通多模型调用。
你现在应该有一个能跑的 SpringAI 项目,它的结构是这样的:application.yml里spring.ai.openai指向 TaoToken,ChatClient 自动装配,MCP 工具通过ToolCallbackProvider注入,RAG 的 VectorStore 手动装配并指定 embedding 模型。切换对话模型只改spring.ai.openai.chat.options.model一行,Key 和 base-url 不动。
如果你要把这套配置用到 Claude Code 或 Cline 这类工具上,思路是一样的:Base URL 填https://taotoken.net/api,API Key 填 TaoToken 的 Key,Model ID 填你要用的模型名。三件套齐了就能通。
最后给一个实用技巧:在项目里加一个/actuator/health之外的自定义端点,启动时打印当前生效的模型配置,方便排查「我到底连的是哪个模型」:
@Bean public ApplicationRunner printModelConfig( @Value("${spring.ai.openai.base-url}") String baseUrl, @Value("${spring.ai.openai.chat.options.model}") String model) { return args -> log.info("当前模型出口: baseUrl={}, model={}", baseUrl, model); }这样每次启动,日志第一行就告诉你模型出口在哪,省得改了半天配置不知道生效没有。
接入文档和 API Keys 页面放在这里,配置过程中遇到路径问题优先查文档:
https://taotoken.net/doc?utm_source=taotoken_aicg_blog_end&utm_medium=csdn&utm_campaign=rewrite&utm_content=doc https://taotoken.net/api-keys?utm_source=taotoken_aicg_blog_end&utm_medium=csdn&utm_campaign=rewrite&utm_content=api_keys长期做 Agent 开发的,Coding Plan 更适合高频调用场景:
https://taotoken.net/coding-plan?utm_source=taotoken_aicg_blog_end&utm_medium=csdn&utm_campaign=rewrite&utm_content=coding_plan模型对话页面用来快速验证某个模型是否可用:
https://taotoken.net/models?utm_source=taotoken_aicg_blog_end&utm_medium=csdn&utm_campaign=rewrite&utm_content=model_chat