Spring AI 的 MCP 多服务器编排,卡点常常不在 ToolCallbackProvider,而在模型通道。TaoToken(https://taotoken.net/?utm_source=taotoken_aicg_blog_end)要解决的就是这一步:application.yml 里那行spring.ai.chat.openai.api-key一换、base-url一指,订单、CRM、知识库、文件系统四个 MCP Server 暴露出来的工具照样被汇总进同一次对话,模型请求统一走一个入口。原文那个 Spring AI 2.0 项目就是典型场景——用 GPT-4o 当 ChatModel,四个 MCP Server 各管一摊,ChatClient 通过 ToolCallbackProvider 把工具清单递给模型,模型决定调哪个工具、传什么参数,工具结果再回到上下文里生成回答。
问题出在模型这一侧的通道经常不由自己掌控:额度用完、模型名下架、接口偶尔抽风,表现却很像「MCP 坏了」——工具列表是空的、对话直接 500、或者模型只回一句「我无法访问订单系统」。真去翻日志才发现 MCP Server 好好的,是模型调用那一步先挂了。这篇就按原文的路径往下走一遍:不动编排代码,只把模型供应商换掉,然后验证工具调用是否照旧。
1. 从 application.yml 里那行 spring.ai.chat.openai.api-key 说起
1.1 四个 MCP Server 共用同一个 ChatModel
原文的架构里,MCP 是「工具供给方」,ChatModel 是「决策方」,两者靠 ToolCallbackProvider 这条线连接。订单 Server 提供查询订单状态、列出订单明细;CRM Server 提供客户档案、最近工单;知识库 Server 提供文档检索;文件系统 Server 提供目录列举和文本读取。四个 Server 是四套独立进程(或四个独立 SSE 端点),但它们在应用侧被汇总成一份工具清单,交给同一个 ChatModel 去做 function calling 决策。
这样的结构带来一个很实际的后果:模型通道是整个链路的单点。四个 MCP Server 随便哪个挂掉,模型还能用剩下的工具继续回答;可模型通道一挂,四个 Server 再健康也没意义,因为没有人来「选工具」了。原文把编排做得很整齐,恰恰让这个单点更明显——所有工具调用都要经过spring.ai.chat.openai这组配置指向的那个上游。
1.2 要改的是两行配置,不是四个 Server
切换模型供应商这件事,在 Spring AI 里落点非常小。原文配置里spring.ai.chat.openai.api-key填的是原供应商的 Key,spring.ai.chat.openai.base-url填的是原供应商的地址(或者留空走默认)。现在只需要:
api-key换成 TaoToken 的 Key,占位符写作YOUR_API_KEYbase-url换成https://taotoken.net/api
注意这里的区别:给人点的官网是 https://taotoken.net/?utm_source=taotoken_aicg_blog_end ,用来注册、创建 Key、看模型广场和用量;填进配置文件的接口地址是https://taotoken.net/api,末尾不要带/v1,也不要带任何 utm 参数。两者混用是后面 404 报错最常见的来源,第 5 节会专门拆开讲。
1.3 Key 从哪儿来:先去控制台建一把
打开 https://taotoken.net/?utm_source=taotoken_aicg_blog_end 注册登录,在控制台创建一把 API Key。创建时建议按环境分开命名,比如spring-ai-local、spring-ai-staging,这样第 6 节对用量时能一眼看出是哪套环境在跑。Key 只在创建时完整显示一次,复制完直接塞进环境变量或本地未提交的配置文件里,不要写进 Git 里的application.yml。
拿 Key 的这一步和原文「申请密钥」那一步是同一件事,只是动作换了个地方做。同一个落地页上还能看到模型广场,里面有当前可用的模型 ID 列表——后面model这一项就从那里抄,别凭记忆编。
2. 多服务器编排这条链为什么不用改
2.1 ToolCallbackProvider 汇总工具的动作与模型无关
应用启动时,Spring AI 的 MCP 客户端会去连配置里那四个 SSE 端点,把每个 Server 的 tools 拉回来,转成ToolCallback。这些 callback 被塞进ToolCallbackProvider,再交给ChatClient的defaultTools(...)(或 auto-configuration 自动挂载)。整条链路里没有任何一处依赖「模型是谁」,它依赖的是「工具描述长什么样」。
所以换base-url之后,工具清单的长度、名字、JSON Schema 都不会变。变的是每次对话时把这份清单发给谁、由谁返回 tool_calls。这也是为什么原文里那段构造 ChatClient 的代码可以一行不动:
@Bean ChatClient assistantChatClient(ChatClient.Builder builder, ToolCallbackProvider toolCallbackProvider) { return builder .defaultSystem("你是订单助手,需要数据时优先调用已注册的 MCP 工具,不要凭空编造订单号。") .defaultTools(toolCallbackProvider) .build(); }ToolCallbackProvider还是那个 Bean,defaultTools还是那把工具,模型侧换了供应商,编排侧完全无感。
2.2 McpSyncServerCustomizer 与 McpToolFilter 的职责边界
原文里用McpSyncServerCustomizer做了超时、日志、初始化请求这类统一设置,用McpToolFilter做了工具白名单和权限裁剪——比如给知识库 Server 只放开检索工具,不放写入类工具。这两个东西的作用对象都是 MCP Server 侧,跟模型调用没有耦合:
McpSyncServerCustomizer调的是 MCP 会话的超时和日志,管的是「本地应用 ↔ MCP Server」这段McpToolFilter管的是「哪些工具允许被暴露出去」,管的是工具清单本身
换模型通道不会改变这两处的行为,也不需要重新实现。真正可能受影响的是握手的第一次调用时间——如果模型首 token 慢,某些自定义超时设得过紧会误伤,这个在第 5.4 节讲。
@Bean McpSyncServerCustomizer mcpSyncServerCustomizer() { return (serverName, spec) -> spec .requestTimeout(Duration.ofSeconds(30)) .initializationTimeout(Duration.ofSeconds(20)); }这段配置保持在原来的位置即可。切换模型供应商不涉及它的任何字段。
2.3 工具真正执行的边界不在模型通道
有一点需要提前说清:TaoToken 承载的是模型请求,不是工具执行。订单、CRM、知识库、文件系统这四个 MCP Server 是你自己起的服务,工具调用最终落在哪个库、哪个接口、执行什么动作,由这些 Server 自己的实现和权限控制决定。模型只负责「决定调哪个工具、传什么参数」,它不直连你的业务库,也不替你执行 SQL。
所以涉及生产环境的写操作(改订单状态、删客户记录),确认逻辑必须放在 MCP Server 侧,而不是寄希望于模型「想清楚再动手」。诊断类 SQL 也一样:让模型生成 SQL、解释执行计划,然后由你在本地或测试库手动跑一遍,把报错贴回对话里继续分析——这条链路和换不换模型通道无关,但换通道时容易被忽略。
3. 把 spring.ai.chat.openai 指向 TaoToken 的两种写法
3.1 直接改 application.yml
最直接的写法是在原有配置上改两处、加一处模型 ID。原文那份application.yml结构基本是这样:
spring: ai: chat: openai: api-key: ${TAOTOKEN_API_KEY} base-url: https://taotoken.net/api options: model: ${TAOTOKEN_MODEL_ID} temperature: 0.2 mcp: client: enabled: true toolcallback: enabled: true sse: connections: order-server: url: http://localhost:8081 sse-endpoint: /sse crm-server: url: http://localhost:8082 sse-endpoint: /sse kb-server: url: http://localhost:8083 sse-endpoint: /sse fs-server: url: http://localhost:8084 sse-endpoint: /ssebase-url写https://taotoken.net/api,结尾没有斜杠、没有/v1、没有任何查询参数。options.model的值以 https://taotoken.net/?utm_source=taotoken_aicg_blog_end 上的模型广场为准,抄错一个字符就是 404 或模型不存在。MCP 那一段一个字都没动,四个 SSE 连接照旧。
3.2 用环境变量覆盖,Key 不进仓库
application.yml里用${}占位,真实值走环境变量。这样本地、CI、容器三套环境共用一份配置文件,Key 也不会被提交:
export TAOTOKEN_API_KEY=YOUR_API_KEY export TAOTOKEN_MODEL_ID=YOUR_MODEL_ID export SPRING_AI_CHAT_OPENAI_BASE_URL=https://taotoken.net/api如果你更习惯用 Spring 的 relaxed binding 直接覆盖属性名,也可以写成SPRING_AI_CHAT_OPENAI_API_KEY和SPRING_AI_CHAT_OPENAI_BASE_URL。两种方式等价,选一种团队里统一的就行。Key 一律从 https://taotoken.net/?utm_source=taotoken_aicg_blog_end 的控制台创建,用完后在同一个页面吊销,不要靠改代码来「停用」。
提示:base-url这类地址不要带 utm 参数。utm 是给落地页做来源统计用的,写进接口地址会被当成路径的一部分,直接吃 404。
3.3 模型 ID 跟着模型广场走
原文默认的 ChatModel 是一个具体型号,这里换成什么要看你手上的业务场景。工具调用密集的编排链路,优先选 instruction following 稳、function calling 支持完整的型号,因为工具描述和参数 schema 都挺长,模型稍微跑偏就会把orderId写成order_id,或者把两个工具的参数混在一起。
具体有哪些型号、各自的上下文长度是多少,以 https://taotoken.net/?utm_source=taotoken_aicg_blog_end 模型广场的实时列表为准,别照抄别人博客里的字符串。选好之后先在一套环境里跑通,再推到其他环境。
4. 跑 ToolRegistryInspector 和 /api/assistant/chat 验证一遍
4.1 先看工具登记表有没有齐
原文里那个ToolRegistryInspector是验证 MCP 挂载情况最省事的办法:它把ToolCallbackProvider里的工具全部打印出来。切换模型通道后第一件事就是跑它,确认工具数量和名字跟切换前一致。
@Component public class ToolRegistryInspector implements ApplicationRunner { private static final Logger log = LoggerFactory.getLogger(ToolRegistryInspector.class); private final ToolCallbackProvider toolCallbackProvider; public ToolRegistryInspector(ToolCallbackProvider toolCallbackProvider) { this.toolCallbackProvider = toolCallbackProvider; } @Override public void run(ApplicationArguments args) { ToolCallback[] callbacks = toolCallbackProvider.getToolCallbacks(); Arrays.stream(callbacks).forEach(cb -> log.info( "MCP tool registered: name={}, desc={}", cb.getToolDefinition().name(), cb.getToolDefinition().description())); log.info("MCP tool total = {}", callbacks.length); } }启动日志里如果四个 Server 的工具都在(比如订单两个、CRM 两个、知识库一个、文件系统两个),说明工具装载这段没受任何影响。如果数量变少了,别急着怀疑 Key,先看对应 MCP Server 的 SSE 连接日志——工具装载和模型通道是两条独立的线。
4.2 再打一次对话接口,看工具结果有没有进上下文
工具清单齐了,下一步验证「模型会不会正确调用工具」。直接打你自己的接口:
curl -s -X POST http://localhost:8080/api/assistant/chat \ -H "Content-Type: application/json" \ -d '{"sessionId":"s-001","message":"查一下订单 SO-20241101 现在的状态,并带上这位客户最近一次工单的标题"}'一次成功的响应应该包含两段信息:订单状态、以及带客户上下文的工单标题。回答里出现真实数据,而不是「我无法访问订单系统」「请提供订单号」这种话,说明模型确实调了 MCP 工具并且读到了返回值。
反过来说,如果回答看着像通的、但内容是编的,那多半是工具没被调用——模型自己硬答了。这种情况先看应用日志里有没有工具调用记录,再对照 5.3 节排查。
4.3 健康检查和熔断指标顺手对一眼
原文给每个 MCP Server 都做了健康检查和熔断。切换之后这两块逻辑不用改,但值得看一眼指标:如果某个 Server 的失败率在切换后突然上升,通常是它自己的下游慢了,跟模型通道无关。反过来,如果四个 Server 指标都正常,只有对话变慢或报错,那基本可以锁定在模型侧。
到了这一步,建议回到 https://taotoken.net/?utm_source=taotoken_aicg_blog_end 看一眼这次的调用记录,确认请求确实打到了预期的模型上:模型 ID、请求时间、消耗量能对上,才算真的切换完成,而不是「碰巧本地还留着旧通道的缓存」。
5. 切换后常见的四类报错
5.1 404,十个里有八个是 base-url 写错了
base-url最常见的两种写错方式:末尾多了/v1,或者把带 utm 的落地页地址粘了进去。正确值只有一个:https://taotoken.net/api。落地页地址(带?utm_source=...)只用于浏览器打开,不要出现在任何配置项、环境变量、curl 命令里。
Spring AI 的 OpenAI 兼容客户端会按自己的规则拼出完整的 chat completions 路径,你在base-url里再加上一段前缀,拼出来的地址自然对不上。排查方法很朴素:把base-url和一个已知可用的模型 ID 写死,用最小请求先打通,再往配置里加东西。
5.2 401,先分清是 Key 的问题还是归属的问题
401 基本就两种:Key 打错字符,或者这把 Key 已经被吊销。复制时把首尾空格带进去是很常见的手滑,application.yml里看不出异常,环境变量里却能看出来。另一类是团队里多人共用一把 Key,某个人在控制台做了轮换,其他人的服务就一起 401。
处理办法:从 https://taotoken.net/?utm_source=taotoken_aicg_blog_end 的控制台重新创建一把,按服务或按人分,命名能看出归属。别在四个 MCP Server 之间共享 Key,更别把 Key 硬编码到镜像里。
5.3 模型不调工具,或者把参数名写歪了
这类问题的表现是「对话能通,但答案像编的」。原因通常有三个:模型本身对 function calling 支持不完整;系统提示词里没有强制优先调用工具;工具描述写得太含糊,模型看不出该选哪个。
原文里defaultSystem那句话(优先调用已注册工具、不要编造订单号)就是为这种情况写的,换模型后值得重新读一遍措辞。工具描述也别偷懒,orderId的格式、允许的取值范围写清楚,比事后加十句提示词管用。如果只是参数名被改写(orderId变order_id),先检查 JSON Schema 里的命名风格是否统一,工具内部再做一层容错解析会稳很多。
5.4 超时、流式截断和熔断误触发
切换后如果出现偶发的握手超时或回答半截断掉,先区分是网络抖动还是超时阈值太紧。原文里给 MCP 会话设的超时是针对工具调用的,模型首 token 慢的时候容易连带受影响。可以把模型侧的超时和 MCP 侧的会话超时分开设,别用一个数字包打天下。
流式输出被截断还有一种可能:某些模型在长工具链下会先输出一段说明再发 tool_calls,日志里看到的内容会显得断断续续,实际链路是完整的。判断标准不是「输出顺不顺」,而是最终回答里有没有工具返回的真实数据。
6. 把这次调用对到控制台上,再决定下一步
6.1 用量、套餐和 Key 的对应关系
本地验证通过之后,最该做的一件事是把这次调用对上账:到控制台看这次请求消耗了多少、哪把 Key 在跑、模型 ID 是不是你预期的那个。多人协作时这一步尤其重要,否则月底对用量会发现一堆来源不明的 Key。想长期拿这套编排写业务代码,可以顺手看一下套餐是否够跑你现在的调用频率,别等跑批任务把额度打空才发现。
6.2 接下来去哪儿
想先用同一把 Key 单独试模型,可以在 TaoToken 模型对话 里发一条消息,对照模型广场确认模型 ID 和响应是否符合预期;如果这套 MCP 编排后面要接更多编码类工作,可以看 Coding Plan 是否匹配你的用量节奏;需要给新环境开 Key 就在 控制台 API Keys 里建,命名带上环境前缀;如果你还打算让 Claude Code 之类的工具也走同一条通道,环境变量对照表在 Claude Code 接入文档 里。
回到这套 Spring AI 项目本身,接下来要留意的其实只有两件事:一是把base-url和模型 ID 固化到配置模板里,别让每个新同事自己手抄;二是给关键工具(尤其是写操作类)在 MCP Server 侧补上确认环节。通道换了,编排逻辑没变,但工具能干什么、不能干什么,始终是你自己说了算。