1. Java 后端接 Agent 的真实卡点在哪Spring AI 是 Spring 官方对 AI 能力的抽象层MCP 是让模型安全调用外部工具的开放协议两者组合能让纯 Java 后端把多工具编排跑起来适合已有 Spring Boot 微服务、不想为 Agent 单独维护一套 Python 服务的团队。我所在的团队就是典型情况订单、库存、权限、审计全在 Java 微服务里模型调用如果另起 Python 服务等于把鉴权和事务边界重新做一遍。真正卡住我们的不是“能不能调模型”而是三件事工具怎么注册、模型怎么知道该调哪个工具、多个工具源怎么在一个请求里串起来。先说工具注册。Java 里已有的 Service 方法天然就是工具但直接暴露给模型有两个问题一是参数校验和权限判断不能丢二是返回体太大容易把上下文撑爆。Spring AI 的Tool注解解决的是“暴露”这一步但治理逻辑还得自己加。再说编排。单工具调用只是 demo真实场景是“先查订单状态再查库存最后生成回复”。模型需要在一轮对话里连续触发多个工具并且能拿到中间结果继续推理。MCP 的 value 在于它把工具层标准化了你用 Java 写的 MCP Server 和用别的语言写的 MCP Server对客户端来说都是同一套协议挂载方式一致。最后是通道问题。很多团队卡在“模型 endpoint 怎么统一管理”——每个环境一套 Key、每个模型一个地址切换成本高。把 endpoint 收敛到一个统一 Key 通道配合 Spring AI 的 OpenAI 兼容配置是成本最低的落地方式。下面按“环境准备 → 工具注册 → 编排链路 → 验证 → 排障”走一遍代码可直接复制。2. TaoToken 前置统一 Key 通道怎么接进 Spring AITaoToken 在这里扮演的是统一模型通道你不需要在每个微服务里散落不同厂商的 Key而是把 Base URL 指向同一个入口用一把 Key 管理模型调用。对 Spring AI 来说它走的是 OpenAI 兼容协议所以配置方式和接 OpenAI 完全一致只是base-url和api-key换成 TaoToken 的。先看依赖。Spring AI 1.1.x 稳定线三个 starter 分别对应模型、MCP 客户端、MCP 服务端dependency groupIdorg.springframework.ai/groupId artifactIdspring-ai-starter-model-openai/artifactId version1.1.1/version /dependency dependency groupIdorg.springframework.ai/groupId artifactIdspring-ai-starter-mcp-client/artifactId version1.1.1/version /dependency dependency groupIdorg.springframework.ai/groupId artifactIdspring-ai-starter-mcp-server/artifactId version1.1.1/version /dependency版本以你本地mvn dependency:tree实际解析到的为准别照抄网上 0.9 或 2.x 的写法方法名差异很大。接着是application.yml里的模型通道配置。这里就是“把 endpoint 改到统一 Key 通道”的关键片段spring: ai: openai: base-url: https://taotoken.net/api api-key: ${TAOTOKEN_API_KEY} chat: options: model: claude-sonnet-4-5 temperature: 0.3base-url指向https://taotoken.net/apiapi-key从环境变量注入不要硬编码进仓库。模型 ID 按你实际要用的填这里用claude-sonnet-4-5举例。Key 的获取入口在控制台的 API Keys 页面生成后写进环境变量即可export TAOTOKEN_API_KEYsk-你的key如果你更习惯用 properties 或 Java Config等价写法是Bean public OpenAiChatModel chatModel() { OpenAiApi api OpenAiApi.builder() .baseUrl(https://taotoken.net/api) .apiKey(System.getenv(TAOTOKEN_API_KEY)) .build(); return OpenAiChatModel.builder().openAiApi(api).build(); }这一步做完模型通道就通了。注意base-url不要带多余路径Spring AI 会自己拼/v1/chat/completions。如果你在网关层做了转发确保转发后路径不被二次改写否则会出现 404。统一 Key 通道的好处在这里体现得很直接测试、预发、生产三套环境只需要换环境变量不用改代码。3. 可复制配置MCP 工具注册与多工具编排链路这一节是核心分三步定义工具、注册 MCP Server、把工具挂到 ChatClient 上做编排。3.1 定义工具并加治理逻辑工具方法用Tool注解暴露描述要写清楚入参含义模型靠描述决定调不调Component public class OrderTools { private final OrderService orderService; public OrderTools(OrderService orderService) { this.orderService orderService; } Tool(description 查询订单状态入参为订单号如 ORD20250101) public String getOrderStatus(String orderId) { Order order orderService.findByNo(orderId); if (order null) { return 订单不存在; } return 订单号 order.getNo() , 状态 order.getStatus(); } Tool(description 查询商品库存入参为商品编码如 SKU1001) public String getStock(String sku) { int qty orderService.stockOf(sku); return 商品 sku , 可用库存 qty; } }注意返回体我做了裁剪只回关键字段。工具结果会塞回上下文返回一大坨 JSON 会让模型变“笨”这是踩过的坑。3.2 注册 MCP Server把本服务的工具发布成 MCP 工具列表供客户端拉取Configuration public class McpServerConfig { Bean public McpServer.Sync mcpServer(ListObject toolInstances) { return McpServer.sync() .serverInfo(java-order-mcp, 1.0.0) .tools(toolInstances) .build(); } }toolInstances里就是上面那些带Tool的 BeanSpring 会自动注入。3.3 挂载工具并编排客户端侧把 MCP 工具挂到 ChatClient模型就能在对话里自动调用Configuration public class ChatClientConfig { Bean public ChatClient chatClient(OpenAiChatModel model, McpSyncClient mcpClient) { ToolCallbackProvider tools SyncMcpToolCallbackProvider.builder() .mcpClients(mcpClient) .build(); return ChatClient.builder(model) .defaultToolCallbacks(tools) .build(); } }多工具源就是多挂几个McpSyncClientSyncMcpToolCallbackProvider支持传入列表。编排链路的关键在于模型拿到用户问题后自己决定先调getOrderStatus还是getStock拿到结果后继续推理直到能回答。你不需要写 if-else 路由路由由模型完成但你要在工具描述里把边界写清楚否则模型会乱调。3.4 结构化输出如果下游要入库或做幂等用entity()直接映射成对象public record OrderView(String orderId, String status, int stock) {} OrderView view chatClient.prompt(查一下 ORD20250101 的状态和对应库存) .call() .entity(OrderView.class);这样返回就是强类型对象方便直接进业务逻辑。4. 验证请求一次多工具串联调用与日志检查点配置写完跑一次真实串联调用。写个测试接口RestController public class AgentController { private final ChatClient chatClient; public AgentController(ChatClient chatClient) { this.chatClient chatClient; } GetMapping(/agent/ask) public String ask(RequestParam String q) { return chatClient.prompt(q).call().content(); } }启动服务后请求curl http://localhost:8080/agent/ask?q订单ORD20250101状态如何对应商品SKU1001还有货吗预期结果模型先调getOrderStatus再调getStock最后组织成一句话回答类似“订单 ORD20250101 状态为已支付商品 SKU1001 可用库存 42 件”。日志检查点有三个。第一看 MCP 工具是否注册成功启动日志里会有工具列表确认getOrderStatus和getStock都在。第二看模型请求是否打到统一通道Spring AI 的 debug 日志会打印请求 URL确认是https://taotoken.net/api而不是默认地址。第三看工具调用链开启logging.level.org.springframework.aiDEBUG能看到ToolCall的入参和返回确认两个工具都被触发且顺序合理。如果只触发了一个工具通常是工具描述不够明确模型没意识到需要第二个。把描述改得更具体比如“查询商品库存用于判断是否有货”命中率会明显提升。5. 本篇常见错排查401、local proxy failed、reading choices排障按报错对照这几个是高频的。401 UnauthorizedKey 没注入或写错。先确认环境变量TAOTOKEN_API_KEY在当前 shell 可见echo $TAOTOKEN_API_KEY有输出。如果用了 IDE 启动检查 Run Configuration 里有没有配环境变量。还有一种情况是 Key 带了多余空格复制时容易带上。local proxy failed / connection refusedbase-url写错或网络不通。确认是https://taotoken.net/api不要写成https://taotoken.net/api/v1Spring AI 会自己拼路径多写一层会 404。如果公司网络有出口限制确认能访问该域名。Error reading choices / 返回体解析失败多半是模型 ID 写错或者通道返回了非预期格式。先确认model字段是你账号下可用的模型 ID。如果返回体里没有choices字段通常是请求打到了错误路径检查base-url是否被网关二次改写。OAuth / 鉴权头冲突如果你在网关层加了统一鉴权可能和 Spring AI 自带的Authorization头冲突。确认网关对/api路径放行或者把 Key 透传下去不要覆盖。工具没被调用检查Tool的 Bean 是否被 Spring 扫描到McpServer的tools()是否传入了实例列表。如果工具注册了但模型不调把temperature调低到 0.1 试试高温会让模型更“随意”。上下文爆炸工具返回太大导致后续请求超长。把大结果落库或落文件只把摘要或 ID 给模型这是生产环境必须做的。6. 把 Agent 接进 Java 微服务的下一步到这里一个纯 Java 的多工具编排链路就跑通了统一 Key 通道解决模型接入MCP 解决工具标准化Spring AI 解决编排和结构化输出。落地时优先做两件事一是给工具加超时和限流外部接口不能裸调二是把工具调用日志接进现有审计体系谁在什么时候调了什么工具、传了什么参数都要可追溯。如果你还在选型阶段建议先用模型对话页面验证模型 ID 和通道是否可用再进代码。长期做编码和 Agent 场景的可以看 Coding Plan 的额度方案比按次调用更划算。接入文档里有各语言的完整示例Java 部分和本文配置一致遇到路径或参数问题可以直接对照。