1. 为什么 Spring AI 项目一到多模型就乱
如果你正在用 Spring AI 或 LangChain4j 做 Agent,大概率遇到过这个场景:本地跑一个模型挺顺,一旦要接第二个模型、第三个模型,代码里就开始到处散落api-key、base-url、model-name。测试环境一套、生产环境一套,改一个 Key 要翻五个配置文件。更麻烦的是 MCP 协议接入后,工具调用链一长,你根本不知道是哪一层出了问题——是模型没选对工具,还是 MCP Server 没返回,还是 Key 通道被限流了。
这篇是 L2 高等篇,目标很明确:把 MCP 协议、Spring AI、Agent 编排这三件事,接到一条统一的 Key 通道上,从本地跑通到可商用部署。适合已经写过 Spring Boot、想认真做 Agent 工程化的 Java 开发者。核心检索词就三个:MCP 协议怎么落地、Spring AI 怎么接 MCP、Agent 编排怎么统一管 Key。
我会先讲清楚 MCP 协议里真正影响工程的那几个点,再给出config.toml和settings.json骨架,然后演示一次完整的 Agent 工具调用链验证。全程用 TaoToken 作为统一 Key/API 通道,这样你多模型切换时只改一个地方。
2. MCP 协议里真正影响工程的四个点
MCP 跑在 JSON-RPC 2.0 之上,消息格式本身不复杂。但工程落地时,有四个点你必须提前想清楚,否则后面排障会很痛苦。
第一是生命周期。initialize是强制的,必须先于其他调用,客户端和服务端在这里交换 capabilities。initialized是 notification,没有 id。所有tools/list、tools/call、resources/read都是 request/response。ping用于心跳。这意味着你的 MCP Server 启动后,第一件事是等握手,而不是直接暴露工具。
第二是 capability 协商。客户端不知道的 capability 不要调。比如 Server 没声明tools: {},客户端调tools/list会直接返回Method not found。这个错误码是-32601,排障时看到它,先回去检查 capability 声明。
第三是传输方式选型。本地工具、本地数据、开发环境用 Stdio,零网络配置、天然沙箱。远程 SaaS 包装、跨主机、Web 应用内嵌用 Streamable HTTP,这是 2025 的新标准,已经替代旧的 HTTP+SSE。旧的 SSE 方案新部署不要再用了。
第四是 Tool 设计原则。模型靠name+description选工具,所以 description 要写清楚「何时用、何时不用」。参数尽量少,超过 10 个参数模型基本选不对。返回内容要简洁,token 是钱。错误要明确,返回isError: true加错误信息。
注意:MCP 的 Sampling 特性允许 Server 反向调用 Client 的 LLM,这是很多高级 Agent 编排的基础。但前提是 Server 声明了
sampling: {}capability,Client 也要支持。
3. TaoToken 前置:统一 Key 通道怎么接
在动手写配置之前,先把 Key 通道这件事解决掉。多模型项目最痛的就是 Key 管理,TaoToken 在这里的作用是提供一个统一的 API 通道,你只需要维护一份 Key,模型切换、环境切换都在这一层完成。
先拿到你的 Key。访问控制台创建 API Key:
https://taotoken.net/api-keys?utm_source=taotoken_aicg_blog_end&utm_content=api_keys&utm_campaign=rewrite创建后你会得到一个形如sk-xxxx的 Key。这个 Key 就是后面所有配置里唯一需要填的凭证。API 基础地址统一用:
https://taotoken.net/api注意这个地址不带任何 UTM 参数,是纯 API 端点。官网入口在这里,需要看文档或模型列表时用:
https://taotoken.net/?utm_source=taotoken_aicg_blog_end&utm_medium=csdn&utm_campaign=rewrite&utm_content=home接入文档在:
https://taotoken.net/doc?utm_source=taotoken_aicg_blog_end&utm_content=doc&utm_campaign=rewrite如果你只是想先验证模型通不通,可以直接用模型对话页面测一条:
https://taotoken.net/chat?utm_source=taotoken_aicg_blog_end&utm_content=chat&utm_campaign=rewrite长期做编码和 Agent 的,建议直接上 Coding Plan,额度更划算:
https://taotoken.net/coding-plan?utm_source=taotoken_aicg_blog_end&utm_content=coding_plan&utm_campaign=rewrite拿到 Key 后,把它放进环境变量,不要硬编码进代码:
export TAOTOKEN_API_KEY="sk-你的key" export TAOTOKEN_BASE_URL="https://taotoken.net/api"这样 Spring AI、LangChain4j、MCP Server 三处都读同一份环境变量,切换环境只改这一处。
4. 可复制配置:config.toml 与 settings.json 骨架
MCP 生态里,不同客户端用不同配置文件格式。config.toml常见于 Rust/Python 系客户端,settings.json常见于 Node/编辑器系客户端。这里给出两份骨架,你按自己用的客户端选。
先看config.toml,重点是 MCP Server 的启动命令和环境变量注入:
# ~/.mcp/config.toml [mcp] transport = "stdio" [[mcp.servers]] name = "fs" command = "java" args = ["-jar", "/opt/mcp/fs-server.jar"] transport = "stdio" [mcp.servers.env] MCP_FS_ROOT = "/data/workspace" TAOTOKEN_API_KEY = "${TAOTOKEN_API_KEY}" TAOTOKEN_BASE_URL = "https://taotoken.net/api" [[mcp.servers]] name = "github" command = "npx" args = ["-y", "@modelcontextprotocol/server-github"] transport = "stdio" [mcp.servers.env] GITHUB_TOKEN = "${GITHUB_TOKEN}"再看settings.json,这是编辑器类客户端常用的格式,结构上把 servers 放在mcpServers下:
{ "mcpServers": { "fs": { "command": "java", "args": ["-jar", "/opt/mcp/fs-server.jar"], "env": { "MCP_FS_ROOT": "/data/workspace", "TAOTOKEN_API_KEY": "${TAOTOKEN_API_KEY}", "TAOTOKEN_BASE_URL": "https://taotoken.net/api" } }, "github": { "command": "npx", "args": ["-y", "@modelcontextprotocol/server-github"], "env": { "GITHUB_TOKEN": "${GITHUB_TOKEN}" } } } }两份配置的核心思路一致:MCP Server 作为子进程启动,通过环境变量拿到统一 Key 通道的地址和凭证。这样 Server 内部如果要调 LLM(比如 Sampling 场景),也走同一条通道。
Spring AI 侧的配置放在application.yml,把 OpenAI 兼容端点指向 TaoToken:
spring: ai: openai: api-key: ${TAOTOKEN_API_KEY} base-url: https://taotoken.net/api chat: options: model: deepseek-chat temperature: 0.7 mcp: client: enabled: true stdio: servers: - name: fs command: java args: ["-jar", "/opt/mcp/fs-server.jar"] env: MCP_FS_ROOT: "/data/workspace" server: enabled: true stdio: true name: my-spring-ai-server version: 1.0.0这里有个关键点:base-url指向 TaoToken 后,Spring AI 的 OpenAI starter 会把它当成标准 OpenAI 兼容端点。你换模型只改model字段,Key 和地址都不动。
5. 验证请求:一次完整的 Agent 工具调用链
配置写完了,怎么确认整条链路是通的?不要一上来就跑复杂 Agent,先做一次最小验证:让 Agent 调用一个 MCP 工具,看工具结果能不能回到模型。
先写一个最小的 MCP Server,暴露一个read_file工具。用 Java MCP SDK:
package com.example.mcpfs; import io.modelcontextprotocol.server.McpServer; import io.modelcontextprotocol.server.McpSyncServer; import io.modelcontextprotocol.server.transport.StdioServerTransportProvider; import io.modelcontextprotocol.spec.McpSchema.*; import java.nio.file.*; import java.util.*; public class McpFsServer { public static void main(String[] args) { String rootEnv = System.getenv().getOrDefault( "MCP_FS_ROOT", System.getProperty("user.home") + "/mcp-workspace" ); Path allowedRoot = Paths.get(rootEnv).toAbsolutePath().normalize(); var transport = new StdioServerTransportProvider(); McpSyncServer server = McpServer.sync(transport) .serverInfo("fs-server", "1.0.0") .capabilities(ServerCapabilities.builder() .tools(true) .resources(true) .build()) .build(); server.addTool(McpServerFeatures.SyncToolSpecification.builder() .tool(Tool.builder() .name("read_file") .description("读取指定路径的文件内容,路径必须在允许的根目录内") .inputSchema(Map.of( "type", "object", "properties", Map.of( "path", Map.of("type", "string", "description", "相对路径") ), "required", List.of("path") )) .build()) .callHandler((exchange, args) -> { try { String rel = (String) args.get("path"); Path abs = allowedRoot.resolve(rel).normalize(); if (!abs.startsWith(allowedRoot)) { return new CallToolResult("错误:路径越界", true); } return new CallToolResult(Files.readString(abs), false); } catch (Exception e) { return new CallToolResult("读取失败:" + e.getMessage(), true); } }) .build()); Thread.currentThread().join(); } }打包后,在 Spring AI 侧写一个 Agent 控制器,把 MCP 工具和本地工具合并:
@RestController public class AgentController { private final ChatClient chatClient; public AgentController( ChatClient.Builder builder, List<ToolCallback> localTools, ToolCallbackProvider mcpTools) { this.chatClient = builder .defaultTools(mcpTools) .defaultToolCallbacks(localTools) .defaultSystem("你是一个助手,可以读写文件、查询客户信息。") .build(); } @PostMapping("/chat") public String chat(@RequestBody ChatRequest req) { return chatClient.prompt() .user(req.message()) .call() .content(); } }启动应用后,发一条请求验证:
curl -X POST http://localhost:8080/chat \ -H "Content-Type: application/json" \ -d '{"message": "读取 /data/workspace/readme.txt 的内容并总结"}'预期结果:模型先决定调用read_file工具,MCP Server 返回文件内容,模型再基于内容生成总结。如果你在日志里看到tools/call的请求和响应,说明整条链路通了。
提示:第一次验证时,把
MCP_FS_ROOT指向一个只有测试文件的目录,避免误读敏感文件。路径越界检查是必须的,上面代码里已经做了startsWith校验。
6. 本篇常见错排查
报错一:Method not found,错误码 -32601。这是 capability 没声明。检查你的 MCP Server 是否在capabilities里声明了tools(true)。如果客户端调tools/list但 Server 没声明 tools,就会返回这个错。同理,调resources/read前要确认声明了resources。
报错二:Invalid params,错误码 -32602。参数不合法。常见原因是inputSchema里写了required,但模型传参时漏了字段,或者类型对不上。比如 schema 写integer,模型传了字符串。排查时把tools/call的原始请求打出来看。
报错三:Spring AI 启动时报base-url连接失败。先确认TAOTOKEN_BASE_URL环境变量有没有生效。Spring AI 读的是spring.ai.openai.base-url,如果你在application.yml里写死了地址但环境变量没覆盖,就会连到默认端点。用curl直接测一下:
curl https://taotoken.net/api/v1/models \ -H "Authorization: Bearer $TAOTOKEN_API_KEY"能返回模型列表,说明 Key 和地址没问题。
报错四:MCP Server 子进程启动后立即退出。多半是 Stdio 传输下,Server 主线程没阻塞。上面代码里的Thread.currentThread().join()就是干这个的。如果你用的是异步框架,确认主线程没有提前结束。
报错五:Agent 不调用工具,直接回答。这是 description 写得太模糊。模型靠 description 判断何时用工具。把「读取文件」改成「当用户要求读取、查看、打开某个文件内容时使用」,触发词写清楚。另外确认工具确实注册进了ChatClient,defaultTools和defaultToolCallbacks两个都要传。
报错六:多模型切换后 401。检查是不是某个模型单独配了 Key。统一走 TaoToken 后,所有模型共用一份 Key,不应该出现单个模型 401。如果出现,看是不是model字段写错了,或者该模型不在你的套餐范围内。
7. 从本地跑通到可商用部署
本地验证通过后,往商用走还有几件事要做。第一是把 MCP Server 容器化,用 Docker 跑 Stdio 传输,LangChain4j 侧可以这样接:
var transport = new StdioMcpTransport.Builder() .command(List.of("docker", "run", "-i", "--rm", "-v", "/data:/data", "-e", "TAOTOKEN_API_KEY", "my-mcp-fs:latest")) .build();第二是把 Agent 编排模式定下来。单步问答用简单 LLM 调用,简单工具调用用 ReAct,复杂多步任务用 Plan-and-Execute,写作类用 Reflection,角色分工明确的用 Multi-Agent。Spring AI 1.0 已经引入了 PlanningAgent 和实验性的@Agent注解,可以按需选用。
第三是可观测性。MCP 的notifications/progress可以用来上报工具执行进度,ping做健康检查。生产环境建议把每次tools/call的耗时、token 消耗、错误码都打点到日志,方便定位是模型选错工具还是工具执行失败。
第四是 Key 通道的稳定性。统一走 TaoToken 后,你只需要监控一个端点的可用性。如果要做多环境隔离,用不同的 Key 分别对应开发、测试、生产,配置里通过环境变量注入,代码零改动。
到这里,MCP 协议、Spring AI 集成、Agent 编排、统一 Key 通道这四块就串起来了。下一步 L3 会进入成本控制、性能优化、可观测性和安全选型,那是真正决定能不能上商用的部分。