
1. 飞书机器人接大模型为什么卡在 endpoint 这一层飞书智能机器人这个场景真正难的不是把消息收进来而是让机器人“会思考”。飞书开放平台的长连接WebSocket负责把用户消息推给你的服务你的服务再把消息交给大模型拿到回复后用 messageId 回帖。链路本身不复杂但一旦落到 Spring-AI 上很多人会卡在两个地方一是自定义 mcp server 怎么注册进 ChatClient二是大模型的 endpoint 到底写在哪、怎么改。我见过太多项目把 api-key 和 base-url 硬编码在 Java 代码里换一个模型供应商就要重新打包。更麻烦的是Spring-AI 默认的模型 starter 往往只认官方地址你想走统一 Key/API 通道就得知道它到底读哪个配置项。这篇就围绕“把 endpoint 改到 TaoToken”这件事把 Spring-AI 自定义 mcp server 飞书机器人这条链路完整跑一遍。适合谁看已经会用 Spring Boot 写接口、想给飞书机器人加“工具调用”能力的后端同学或者手上有个 mcp server想把它接进 Spring-AI 生态的开发者。核心检索词就三个Spring-AI、mcp server、飞书智能机器人。读完你能拿到可复制的 application.yml、mcp server 注册代码以及飞书侧验证机器人是否真的在回复的动作。先说清楚整体结构。工程分两个模块mcp-server 负责暴露工具比如查天气、查订单mcp-client 负责跑 Spring-AI、连飞书长连接、调大模型。mcp-client 通过java -jar或 stdio 方式拉起 mcp-serverSpring-AI 用SyncMcpToolCallbackProvider把工具注册进 ChatClient。大模型这一层我们把 endpoint 指向 TaoToken 的统一通道这样 Key 和地址只维护一份。为什么值得这么做因为 mcp 的价值在于“工具和模型解耦”。工具定义写在 mcp-server 里模型换供应商不影响工具而 endpoint 统一之后模型换供应商也不影响业务代码。飞书机器人只是最外层的入口它不关心你背后用的是哪家模型只关心有没有拿到回复。2. TaoToken 前置统一 Key 与 endpoint 的接入准备在动手改配置之前先把 TaoToken 这一层准备好。你可以把它理解成一个“模型调用的统一入口”不管底层接的是哪家模型你的 Spring-AI 只需要认一个 Base URL 和一把 Key。官网入口是 https://taotoken.net/?utm_sourcetaotoken_aicg_blog_endutm_mediumcsdnutm_campaignrewriteutm_content API 地址是 https://taotoken.net/api 注意 API 地址后面不加 UTM 参数。第一步是拿 Key。登录后进控制台在 API Keys 页面创建一个新 Key。建议按项目命名比如feishu-bot-dev方便后面排查是哪个应用在调用。创建完立刻复制保存页面刷新后通常不再完整显示。控制台地址走这个 deep linkhttps://taotoken.net/console?utm_sourcetaotoken_aicg_blog_endutm_contentconsoleutm_campaignrewrite 。第二步是确认模型 ID。不同模型在 TaoToken 上的 Model ID 写法可能不一样别凭记忆写。可以在模型对话页面先手动发一条消息验证页面地址https://taotoken.net/chat?utm_sourcetaotoken_aicg_blog_endutm_contentmodel_chatutm_campaignrewrite 。在模型列表里选一个你打算用的比如通用的对话模型记下它的准确 ID。这个 ID 后面要写进 application.yml 的model字段。第三步是理解 Spring-AI 的配置读取逻辑。Spring-AI 的 OpenAI starter 默认读spring.ai.openai.base-url和spring.ai.openai.api-key。如果你用的是别的 starter比如 zhipuai配置前缀会不同。关键点是base-url 要指向 TaoToken 的 API 地址而不是模型厂商的官方地址。这样你的请求先到 TaoToken再由它转发到具体模型。这里有个容易踩的坑base-url 结尾要不要带/v1取决于 starter 的实现。OpenAI 兼容的 starter 通常会在 base-url 后面自动拼/v1/chat/completions所以 base-url 写到https://taotoken.net/api即可不要自己再加/v1否则会变成/api/v1/v1/...。如果你不确定先用 curl 测一下后面第 4 节会给验证命令。另外提醒一句Key 不要提交到 Git。用环境变量或者本地application-local.yml覆盖生产环境走配置中心。这是基本安全习惯和用哪家服务无关。3. 可复制配置application.yml 与 mcp server 注册代码这一节是全文的核心直接给可复制的片段。先看 mcp-client 的application.yml。注意路径和字段名要和你的工程一致我按 OpenAI 兼容 starter 来写server: port: 8080 spring: ai: openai: # 指向 TaoToken 统一 API 地址不要带 /v1 base-url: https://taotoken.net/api api-key: ${TAOTOKEN_API_KEY} chat: options: model: your-model-id temperature: 0.7 mcp: client: enabled: true name: feishu-mcp-client version: 1.0.0 type: SYNC request-timeout: 30s stdio: connections: weather-server: command: java args: - -jar - ./mcp-server/target/mcp-server-0.0.1-SNAPSHOT.jar lark: app-id: ${LARK_APP_ID} app-secret: ${LARK_APP_SECRET}几个关键点解释一下。base-url写https://taotoken.net/apiapi-key用环境变量注入避免明文。model填你在模型对话页面确认过的 ID。spring.ai.mcp.client.stdio.connections下面注册了一个叫weather-server的 stdio 连接command 是javaargs 是-jar加 jar 包路径。Spring-AI 启动时会自动拉起这个子进程并通过 stdio 和它通信。然后是 mcp server 侧的工具定义。用Tool注解暴露方法Spring-AI 会自动扫描import org.springframework.ai.tool.annotation.Tool; import org.springframework.ai.tool.annotation.ToolParam; import org.springframework.stereotype.Service; Service public class WeatherService { Tool(name getWeather, description 查询指定城市的天气) public WeatherResult getWeather(ToolParam(description 请求参数) WeatherRequest req) { // 真实场景这里调内部天气接口 return new WeatherResult(req.city(), SUNNY, 25C, mcp:getWeather); } }接着是 mcp-client 里把工具注册进 ChatClient 的配置类。这一步决定了模型能不能“看到”工具import org.springframework.ai.chat.client.ChatClient; import org.springframework.ai.mcp.SyncMcpToolCallbackProvider; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import io.modelcontextprotocol.client.McpSyncClient; import java.util.List; Configuration public class AiConfig { Bean public ChatClient chatClient(ChatClient.Builder builder, ListMcpSyncClient mcpSyncClients) { return builder .defaultSystem(你是一个AI助手必须调用工具 kings-spring-ai-mcp-tools 下的方法如果工具不可用就明确说明无法调用工具不要编造。) .defaultToolCallbacks( SyncMcpToolCallbackProvider.builder() .mcpClients(mcpSyncClients) .build() ) .build(); } }注意defaultToolCallbacks接收的是SyncMcpToolCallbackProvider它会把所有McpSyncClient里的工具都注册进去。defaultSystem里明确要求模型调用工具否则有些模型会偷懒直接编答案。这段配置和前面的 yml 是配套的yml 负责建立 stdio 连接配置类负责把连接里的工具挂到 ChatClient 上。最后是飞书长连接监听器负责收消息、异步调 botServiceimport com.lark.oapi.event.EventDispatcher; import com.lark.oapi.service.im.ImService; import com.lark.oapi.service.im.v1.model.P2MessageReceiveV1; import com.lark.oapi.ws.Client; import jakarta.annotation.Resource; import org.springframework.boot.CommandLineRunner; import org.springframework.stereotype.Component; Component public class LarkWsListener implements CommandLineRunner { Resource private LarkBotService botService; Resource private Client.Builder larkWsBuilder; Override public void run(String... args) { EventDispatcher handler EventDispatcher.newBuilder(, ) .onP2MessageReceiveV1(new ImService.P2MessageReceiveV1Handler() { Override public void handle(P2MessageReceiveV1 event) { String messageId event.getEvent().getMessage().getMessageId(); String contentJson event.getEvent().getMessage().getContent(); String userText LarkMsgParser.extractText(contentJson); ThreadUtil.execAsync(() - botService.onUserMessage(messageId, userText)); } }) .build(); Client wsClient larkWsBuilder.eventHandler(handler).build(); wsClient.start(); } }botService.onUserMessage里就是调chatClient.prompt().user(userText).call().content()拿到结果后用 messageId 回帖。整条链路到这里就闭环了飞书推消息 → 监听器解析 → ChatClient 带工具调模型 → 模型决定是否调 mcp 工具 → 回复回帖。4. 验证请求从 curl 到飞书实测配置写完别急着启动整个工程先分层验证。第一层验证 TaoToken 的 endpoint 通不通。用 curl 直接打 chat completionscurl -X POST https://taotoken.net/api/v1/chat/completions \ -H Authorization: Bearer $TAOTOKEN_API_KEY \ -H Content-Type: application/json \ -d { model: your-model-id, messages: [{role: user, content: 你好}] }如果返回里有choices[0].message.content说明 Key 和 endpoint 没问题。如果返回 401检查 Key 是否复制完整、有没有多余空格。如果返回 404大概率是 base-url 拼错了注意/api后面由 starter 自动补/v1curl 里要手动写全。第二层验证 mcp server 能不能被拉起。单独跑一下 jar 包看它是否正常启动、是否在 stdio 上等待输入。如果 jar 包启动就报错先解决 mcp-server 自己的问题别急着调 client。第三层验证 Spring-AI 启动日志。启动 mcp-client观察控制台有没有类似Connected to MCP server或者工具注册的日志。如果看到工具数量为 0说明 stdio 连接没建立成功回去检查 yml 里的 command 和 args 路径。第四层才是飞书实测。启动成功后飞书应用里应该能看到机器人上线。在飞书里给机器人发一条消息比如“北京天气怎么样”。预期行为是机器人先回一个“正在查询”或者直接回结果日志里能看到MCP Tool getWeather called。如果机器人没反应先看长连接有没有连上日志里通常会有connected to wss://msg-frontier.feishu.cn/这样的字样。实测下来最容易出问题的是飞书应用的事件订阅配置。你需要在飞书开放平台把长连接模式打开并且订阅im.message.receive_v1事件。如果事件没订阅消息根本不会推给你的服务。另外机器人要能被拉进群或者私聊权限里要开im:message相关权限。验证成功的标志很明确飞书里发消息机器人回复内容里包含工具返回的数据比如mcp:getWeather这个标记同时后端日志有工具调用记录。两个都对上说明 Spring-AI mcp server 飞书这条链路通了。5. 本篇常见错排查401、local proxy failed、reading choices排障这块我按真实报错来写都是这条链路上高频出现的。401 Unauthorized。这个最直接Key 不对或者没带上。检查三处环境变量TAOTOKEN_API_KEY有没有真正注入到进程yml 里api-key的${}占位符有没有被正确解析curl 测试时 Header 是不是Bearer加空格加 Key。如果 Key 是对的还报 401确认一下是不是把 Key 用在了错误的 endpoint 上。local proxy failed / connection refused。这个通常出现在 mcp client 拉 mcp server 的时候。stdio 模式下Spring-AI 会 fork 一个子进程跑java -jar如果 jar 路径不对、Java 不在 PATH 里就会报连接失败。排查方法把 yml 里的 command 和 args 复制出来在终端手动执行一遍看能不能跑起来。另外注意相对路径是相对于 mcp-client 的工作目录不是相对于 yml 文件。reading choices 相关报错。比如Cannot read field choices because response is null或者解析响应时 NPE。这多半是模型返回了非预期结构常见原因是 base-url 写错导致返回了 HTML 错误页或者 model ID 不存在导致返回错误 JSON。先用 curl 确认返回体结构再对照 starter 期望的格式。如果 curl 正常但代码报错检查是不是 starter 版本和 API 格式不匹配。OAuth / token 相关报错。如果你用的是需要 OAuth 的模型通道可能会遇到 token 过期。TaoToken 的 Key 是长期有效的 API Key不涉及 OAuth 刷新所以这类报错一般出现在飞书侧的应用凭证上。检查lark.app-id和lark.app-secret是否正确以及飞书应用是否开启了对应的权限。工具不生效模型直接编答案。这个不是报错但很常见。原因是defaultSystem没写清楚或者工具没注册进去。先确认启动日志里工具数量大于 0再确认 system prompt 里明确要求调用工具。有些模型对工具调用的触发比较保守可以在 prompt 里加一句“涉及天气、订单等实时信息时必须调用工具”。飞书消息重复回复。长连接模式下如果服务重启可能会重复消费事件。飞书事件本身有去重机制但你的业务侧最好也按 messageId 做幂等。简单做法是用一个本地缓存记录已处理的 messageId处理前先查一下。排查顺序建议先 curl 验 endpoint再单独跑 mcp server再看 Spring-AI 启动日志最后才看飞书。从内到外别一上来就怀疑飞书配置。6. 把 Key 和 endpoint 收口后续换模型不再改代码走到这里整条链路已经能跑通了。回头看真正让这个方案可维护的是把 endpoint 和 Key 收口到了配置层。mcp server 负责工具Spring-AI 负责编排飞书负责入口TaoToken 负责模型通道。四层各司其职换任何一层都不需要动其他层的代码。如果你打算长期跑这个机器人建议把 Coding Plan 用起来地址是 https://taotoken.net/coding-plan?utm_sourcetaotoken_aicg_blog_endutm_contentcoding_planutm_campaignrewrite 适合需要持续调用、做 Agent 类应用的场景。接入文档在 https://taotoken.net/doc?utm_sourcetaotoken_aicg_blog_endutm_contentdocutm_campaignrewrite 里面有各语言 starter 的配置示例。API Keys 管理页面还是 https://taotoken.net/api-keys?utm_sourcetaotoken_aicg_blog_endutm_contentapi_keysutm_campaignrewrite 建议按环境建不同的 Key。最后留一个实用技巧在botService.onUserMessage里把用户原始消息、模型回复、工具调用记录打一条结构化日志。飞书机器人出问题时这条日志能帮你快速定位是消息没收到、模型没回、还是工具没调。比在飞书里反复发消息试要高效得多。