ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

SpringAI 与飞书机器人:用 Function Calling 精简智能助手开发

SpringAI 与飞书机器人:用 Function Calling 精简智能助手开发 1. 为什么我又把 MCP 那套拆了先说结论SpringAI 的 Function Calling 能让你在 Spring Boot 里用几十行代码把飞书机器人接上大模型工具方法就是普通 Java 方法不需要 MCP Client、不需要 MCP Server、不需要 JSON-RPC 序列化。我上一版方案是飞书 Bot → Spring BootMCP Client→ MCP 协议 → MCP Server → 工具方法链路长、调试烦、两个进程要同时起。这次换成 Function Calling 后链路变成飞书 Bot → Spring BootChatClient Tool→ 本地方法调用少了一层网络往返断点能直接打到工具方法里。这篇写给谁正在用 Spring Boot 3.x 做飞书机器人、想让机器人根据用户一句话自动查天气/查订单/查文档但不想引入 MCP 复杂度的同学。你需要会基本的 Spring Boot 注解、Maven、能拿到飞书自建应用凭证。全文给的是可复制的最小闭环application.yml、Tool 工具类、ChatClient 配置、飞书事件监听、本地启动后发消息验证函数回调。跑通之后你会看到后台日志里出现「查询城市天气: 北京」这种工具被真实调用的记录而不是模型瞎编。我试过把工具描述写得很模糊结果模型该调工具的时候不调、不该调的时候乱调后面第 5 节专门讲这个坑。先把环境搭起来。2. TaoToken 前置把模型 Key 和接入地址准备好Function Calling 要能跑前提是模型侧支持 tools 参数并且能正确返回 tool_calls。我这边统一用 TaoToken 做模型接入它的 API 地址是https://taotoken.net/api兼容 OpenAI 的 chat/completions 协议SpringAI 的 OpenAI starter 直接改 base-url 就能用不用改代码结构。你需要先拿到一个 API Key。打开控制台创建密钥https://taotoken.net/console?utm_sourcetaotoken_aicg_blog_endutm_mediumcsdnutm_campaignrewriteutm_contentconsole 在 API Keys 页面新建一个复制出来形如sk-xxxx的字符串。这个 Key 只显示一次建议直接写进环境变量别硬编码进 yml。关于模型选择Function Calling 对模型的指令遵循能力有要求选支持工具调用的对话模型即可。你可以在模型对话页先手动验证一下模型能不能识别工具意图https://taotoken.net/models?utm_sourcetaotoken_aicg_blog_endutm_mediumcsdnutm_campaignrewriteutm_contentmodels 如果后面要长期跑编码类 Agent 场景可以看 Coding Planhttps://taotoken.net/coding-plan?utm_sourcetaotoken_aicg_blog_endutm_mediumcsdnutm_campaignrewriteutm_contentcoding-plan 。接入文档在这里SpringAI 的 base-url 和 model 名称以文档为准https://taotoken.net/doc?utm_sourcetaotoken_aicg_blog_endutm_mediumcsdnutm_campaignrewriteutm_contentdoc 。API Keys 管理页https://taotoken.net/api-keys?utm_sourcetaotoken_aicg_blog_endutm_mediumcsdnutm_campaignrewriteutm_contentapi-keys 。注意Key 走环境变量注入yml 里用${TAOTOKEN_API_KEY}占位别把明文提交到 Git。3. 可复制配置pom、application.yml、工具类、ChatClient3.1 Maven 依赖Spring Boot 3.5.x SpringAI 1.1.x核心是spring-ai-starter-model-openai走 OpenAI 兼容协议和飞书官方 SDK。project xmlnshttp://maven.apache.org/POM/4.0.0 xmlns:xsihttp://www.w3.org/2001/XMLSchema-instance xsi:schemaLocationhttp://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd modelVersion4.0.0/modelVersion parent groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-parent/artifactId version3.5.5/version /parent groupIdcom.example/groupId artifactIdfeishu-spring-ai-agent/artifactId version1.0.0/version properties java.version17/java.version spring-ai.version1.1.5/spring-ai.version /properties dependencyManagement dependencies dependency groupIdorg.springframework.ai/groupId artifactIdspring-ai-bom/artifactId version${spring-ai.version}/version typepom/type scopeimport/scope /dependency /dependencies /dependencyManagement dependencies dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-web/artifactId /dependency dependency groupIdorg.springframework.ai/groupId artifactIdspring-ai-starter-model-openai/artifactId /dependency dependency groupIdcom.larksuite.oapi/groupId artifactIdoapi-sdk/artifactId version2.4.19/version /dependency dependency groupIdorg.projectlombok/groupId artifactIdlombok/artifactId optionaltrue/optional /dependency /dependencies /project3.2 application.yml关键点base-url指向 TaoToken 的 API 地址model填文档里支持工具调用的模型名。飞书部分开发环境用 websocket 长连接省掉公网回调地址。server: port: 8080 spring: ai: openai: base-url: https://taotoken.net/api api-key: ${TAOTOKEN_API_KEY} chat: options: model: your-tool-capable-model temperature: 0.7 feishu: app-id: ${FEISHU_APP_ID} app-secret: ${FEISHU_APP_SECRET} event-mode: websocket verification-token: ${FEISHU_VERIFICATION_TOKEN:} encrypt-key: ${FEISHU_ENCRYPT_KEY:} callback-path: /feishu/event/callback logging: level: com.example.feishuagent: debug3.3 工具类Tool 就是普通 Java 方法和 MCP 版本最大的区别注解从McpTool换成Tool参数从McpToolParam换成ToolParam方法里不再需要McpSyncServerExchange这种协议对象。package com.example.feishuagent.tool; import lombok.extern.slf4j.Slf4j; import org.springframework.ai.tool.annotation.Tool; import org.springframework.ai.tool.annotation.ToolParam; import org.springframework.stereotype.Service; import java.util.Map; import java.util.Random; Service Slf4j public class WeatherTool { private static final String[] CONDITIONS {晴朗, 多云, 阴天, 小雨, 大雨, 雷雨}; private final Random random new Random(); Tool(description 根据城市名称获取当前天气信息包括温度、湿度、天气状况等) public MapString, Object getWeather( ToolParam(description 城市名称例如北京、上海、广州) String cityName) { log.info(查询城市天气: {}, cityName); int temperature random.nextInt(35) - 5; int humidity random.nextInt(80) 20; String condition CONDITIONS[random.nextInt(CONDITIONS.length)]; return Map.of( city, cityName, temperature, temperature °C, humidity, humidity %, condition, condition, advice, getAdvice(condition, temperature) ); } private String getAdvice(String condition, int temperature) { if (temperature 0) return 天气寒冷注意保暖; if (大雨.equals(condition) || 雷雨.equals(condition)) return 有雨建议携带雨伞; return 天气不错适合外出; } }订单工具同理重点是description要写清楚「做什么 参数格式 示例值」模型靠这个判断调不调、传什么。package com.example.feishuagent.tool; import lombok.extern.slf4j.Slf4j; import org.springframework.ai.tool.annotation.Tool; import org.springframework.ai.tool.annotation.ToolParam; import org.springframework.stereotype.Service; import java.util.HashMap; import java.util.Map; Service Slf4j public class OrderTool { private static final MapString, String[] ORDERS new HashMap(); static { ORDERS.put(ORD-1001, new String[]{iPhone 16 Pro, 已发货, 8999.00}); ORDERS.put(ORD-1002, new String[]{MacBook Air M3, 处理中, 9499.00}); ORDERS.put(ORD-1003, new String[]{AirPods Pro 2, 已完成, 1899.00}); } Tool(description 根据订单 ID 查询订单状态和详细信息) public MapString, Object getOrderStatus( ToolParam(description 订单编号例如ORD-1001) String orderId) { log.info(查询订单状态: {}, orderId); String[] order ORDERS.get(orderId); if (order null) { return Map.of(error, 订单不存在: orderId); } return Map.of( orderId, orderId, productName, order[0], status, order[1], price, order[2] ); } }3.4 ChatClient 配置注册工具MethodToolCallbackProvider扫描工具对象上的Tool注解自动生成 JSON Schema 工具定义ChatClient通过defaultToolCallbacks注册。package com.example.feishuagent.config; import com.example.feishuagent.tool.OrderTool; import com.example.feishuagent.tool.WeatherTool; import lombok.RequiredArgsConstructor; import org.springframework.ai.chat.client.ChatClient; import org.springframework.ai.tool.ToolCallbackProvider; import org.springframework.ai.tool.method.MethodToolCallbackProvider; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; Configuration RequiredArgsConstructor public class ChatConfig { Bean public ToolCallbackProvider toolCallbackProvider(WeatherTool weatherTool, OrderTool orderTool) { return MethodToolCallbackProvider.builder() .toolObjects(weatherTool, orderTool) .build(); } Bean public ChatClient chatClient(ChatClient.Builder builder, ToolCallbackProvider provider) { return builder .defaultToolCallbacks(provider) .defaultSystem(你是一个智能助手可以查询天气和订单信息。 请根据用户问题自动调用合适的工具回答简洁友好使用中文。) .build(); } }3.5 飞书事件监听拿到消息直接喂给 ChatClientpackage com.example.feishuagent.feishu.listener; import com.example.feishuagent.feishu.sender.FeishuMessageSender; import com.lark.oapi.event.EventDispatcher; import com.lark.oapi.service.im.ImService; import com.lark.oapi.service.im.v1.model.P2MessageReceiveV1; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springframework.ai.chat.client.ChatClient; import org.springframework.beans.factory.annotation.Value; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import java.util.concurrent.CompletableFuture; Configuration RequiredArgsConstructor Slf4j public class FeishuEventListener { private final ChatClient chatClient; private final FeishuMessageSender messageSender; Value(${feishu.verification-token:}) private String verificationToken; Value(${feishu.encrypt-key:}) private String encryptKey; Bean public EventDispatcher eventDispatcher() { return EventDispatcher.newBuilder(verificationToken, encryptKey) .onP2MessageReceiveV1(new ImService.P2MessageReceiveV1Handler() { Override public void handle(P2MessageReceiveV1 event) { CompletableFuture.runAsync(() - handleMessage(event)); } }) .build(); } private void handleMessage(P2MessageReceiveV1 event) { try { String chatId event.getEvent().getMessage().getChatId(); String content event.getEvent().getMessage().getContent(); String userText content.replaceAll(\\S\\s*, ).trim(); log.info(收到飞书消息 - chatId: {}, text: {}, chatId, userText); String result chatClient.prompt().user(userText).call().content(); if (result null || result.isBlank()) { result 抱歉暂时无法处理您的请求请稍后再试。; } messageSender.sendTextMessage(chatId, result); } catch (Exception e) { log.error(处理飞书消息异常, e); } } }消息发送器负责 JSON 转义和长度截断飞书单条文本约 8KB 上限超了会报错。package com.example.feishuagent.feishu.sender; import com.lark.oapi.Client; import com.lark.oapi.service.im.v1.enums.CreateMessageReceiveIdTypeEnum; import com.lark.oapi.service.im.v1.enums.MsgTypeEnum; import com.lark.oapi.service.im.v1.model.*; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springframework.stereotype.Component; Component RequiredArgsConstructor Slf4j public class FeishuMessageSender { private final Client feishuClient; public void sendTextMessage(String chatId, String text) { try { String safe escapeJson(text); String truncated safe.length() 7900 ? safe : safe.substring(0, 7890) \n...内容已截断; String content String.format({\text\:\%s\}, truncated); CreateMessageReq req CreateMessageReq.newBuilder() .receiveIdType(CreateMessageReceiveIdTypeEnum.CHAT_ID) .createMessageReqBody(CreateMessageReqBody.newBuilder() .receiveId(chatId) .msgType(MsgTypeEnum.MSG_TYPE_TEXT.getValue()) .content(content) .build()) .build(); var resp feishuClient.im().message().create(req); if (resp.success()) { log.info(消息发送成功 - messageId: {}, resp.getData().getMessageId()); } else { log.error(消息发送失败 - code: {}, msg: {}, resp.getCode(), resp.getMsg()); } } catch (Exception e) { log.error(发送飞书消息异常, e); } } private String escapeJson(String text) { return text.replace(\\, \\\\).replace(\, \\\) .replace(\n, \\n).replace(\r, \\r).replace(\t, \\t); } }飞书客户端和 WebSocket 长连接配置package com.example.feishuagent.feishu.config; import com.lark.oapi.Client; import com.lark.oapi.event.EventDispatcher; import com.lark.oapi.ws.Client as WsClient; import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Value; import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; Configuration Slf4j public class FeishuClientConfig { Value(${feishu.app-id}) private String appId; Value(${feishu.app-secret}) private String appSecret; Bean public Client feishuClient() { return Client.newBuilder(appId, appSecret).build(); } Bean(initMethod start) ConditionalOnProperty(name feishu.event-mode, havingValue websocket) public FeishuWebSocketClient feishuWebSocketClient(EventDispatcher dispatcher) { log.info(初始化飞书 WebSocket 客户端); return new FeishuWebSocketClient(appId, appSecret, dispatcher); } }注意上面import com.lark.oapi.ws.Client as WsClient;是伪代码示意实际 Java 里飞书 SDK 的 WebSocket 客户端类名以你引入的 SDK 版本为准别照抄这一行。4. 验证请求本地启动后发消息看函数回调4.1 设置环境变量并启动export TAOTOKEN_API_KEYsk-你的key export FEISHU_APP_IDcli_xxxxxxxxxx export FEISHU_APP_SECRETxxxxxxxxxxxxxxxx export FEISHU_VERIFICATION_TOKENxxxxxxxx mvn spring-boot:run启动日志里应该能看到 WebSocket 客户端发起连接INFO --- FeishuClientConfig : 初始化飞书 WebSocket 客户端 INFO --- FeishuWebSocketClient : 飞书 WebSocket 客户端启动发起 WSS 连接... INFO --- FeishuAgentApplication : Started FeishuAgentApplication in 3.2 seconds4.2 飞书侧配置在飞书开放平台创建企业自建应用开启机器人能力权限勾选im:message发消息和im:message:readonly收消息。事件订阅选「使用长连接接收事件」添加事件im.message.receive_v1勾选「读取用户发给机器人的单聊消息」和「获取 当前机器人的消息」。开发阶段用长连接就不用配公网回调地址。4.3 发消息验证在飞书里 机器人发「北京今天天气怎么样」预期机器人回复天气信息同时后台日志出现工具调用记录INFO --- FeishuEventListener : 收到飞书消息 - chatId: oc_xxx, text: 北京今天天气怎么样 INFO --- WeatherTool : 查询城市天气: 北京 INFO --- FeishuMessageSender : 消息发送成功 - messageId: om_xxx再发「查一下订单 ORD-1003」日志出现查询订单状态: ORD-1003。发「你是」这种闲聊日志里不会出现任何工具调用说明模型正确判断了不需要工具。发「查订单 ORD-9999」工具返回 error模型会转述成「订单不存在」。判断 Function Calling 是否真的生效就看后台有没有查询城市天气这行日志。如果只有「收到飞书消息」没有工具日志说明模型没触发工具调用去第 5 节排查。5. 本篇常见错排查5.1 模型不调用工具直接瞎编答案最常见。原因通常是Tool的 description 太模糊。写「查询信息」模型不知道啥时候用写「根据订单 ID 查询订单状态和详细信息」模型就能对上。参数 description 也要给示例值比如「订单编号例如ORD-1001」。另外确认你选的模型本身支持 tools 参数不支持工具调用的模型会忽略工具列表。5.2 报 401 或 base-url 相关错误检查spring.ai.openai.base-url是不是https://taotoken.net/api注意结尾不要多加/v1或斜杠具体以接入文档为准。Key 是否通过环境变量正确注入可以在启动日志里确认配置加载。如果报模型不存在核对model名称和文档里支持工具调用的模型列表。5.3 Async 不生效导致消息处理阻塞我一开始想用Async异步处理飞书事件结果发现 private 方法上加了没用同类内部调用也不走代理。后来改成CompletableFuture.runAsync()简单直接不依赖 Spring 代理。如果你也遇到事件回调线程被阻塞、机器人响应慢先看这里。5.4 飞书发消息报 JSON 解析错误模型返回的文本里带换行、引号直接拼进{text:...}会破坏 JSON。必须做转义escapeJson里把\、、\n、\r、\t都处理掉。另外文本超 8KB 要截断否则飞书 API 直接拒绝。5.5 群聊里机器人不响应群聊消息默认需要 机器人才触发。事件里mentions数组为空说明没 直接忽略。处理文本时记得把_user_1这类提及占位符去掉再喂给模型否则模型会困惑。6. 接下来怎么走跑通这个最小闭环后工具方法就是你的主战场。加新能力只需要写一个带Tool的 Spring Bean然后在ChatConfig的toolObjects里加进去不用动飞书那层代码。工具多了之后注意 description 之间的区分度两个工具描述太像模型会选错。如果你后面要把工具拆成独立服务、跨语言复用再考虑 MCP单体应用内、工具就在同一个 JVMFunction Calling 是更省事的选择。模型接入这块Key 在控制台建https://taotoken.net/console?utm_sourcetaotoken_aicg_blog_endutm_mediumcsdnutm_campaignrewriteutm_contentconsole 接入参数看文档https://taotoken.net/doc?utm_sourcetaotoken_aicg_blog_endutm_mediumcsdnutm_campaignrewriteutm_contentdoc 密钥管理在 API Keyshttps://taotoken.net/api-keys?utm_sourcetaotoken_aicg_blog_endutm_mediumcsdnutm_campaignrewriteutm_contentapi-keys 。先把天气和订单两个工具跑通再往上叠你自己的业务方法比一上来堆十个工具好调得多。
返回列表