
最近在项目里折腾了一轮豆包大模型接入飞书把原本只会“对话聊天”的 Agent 真正接到了日常办公场景中。接入之后发现豆包不再只是网页里的问答工具而是能收到飞书消息、跨应用读取数据、调用接口干活甚至被团队成员当成“新同事”来 。这篇文章就记录我这次接入的整体思路、环境配置、核心代码、常见报错和工程化建议希望给正在做 Agent 开发、飞书机器人接入的同学提供一份可复用的参考。1. 背景为什么要把豆包接进飞书1.1 豆包和飞书的组合解决了什么问题豆包是字节跳动旗下的 AI 大模型产品除了网页版、App 之外还提供了大模型 API 服务开发者可以基于它构建自己的智能体应用。而飞书是很多团队日常使用的协同办公平台承载了即时消息、文档、多维表格、审批等大量工作流。把两者打通之后价值非常直接随时随地触发 Agent不用打开豆包网页直接在飞书群里 机器人就能让 Agent 干活。让 Agent 拥有办公数据通过飞书开放平台Agent 可以读取多维表格、文档内容甚至把处理结果写回飞书。把 AI 能力嵌入现有协作流程比如 Jenkins 构建失败时通知飞书群群成员艾特机器人查看原因或者机器人定时汇总多维表格数据并生成报告。更接近“同事”的体验Agent 有独立的飞书身份可以接收任务、返回结果、发起交互而不是一个孤立的网页工具。简单说豆包负责“思考”飞书负责“协作”两者通过开放接口连接后Agent 才真正进入了团队的工作流。1.2 Agent 在飞书中的形态飞书中的 Agent 通常以“企业自建应用 机器人”的形式存在。管理员在飞书开放平台创建一个应用启用机器人能力然后配置事件订阅。当用户在群里 机器人或给机器人发私聊消息时飞书会通过 HTTP 回调把消息内容推送给你的后端服务。后端服务拿到消息后调用豆包大模型生成回复再调用飞书消息发送接口把回复内容发回对应的会话。整体看起来就像是一个真实同事在群里回答问题、执行任务。1.3 这篇文章适合谁想给团队做一个飞书 AI 机器人的开发者。正在学习 Agent 开发想知道大模型如何对接企业 IM 的同学。已经在用 Dify、n8n 等工具但想了解手写接入原理的读者。遇到“事件订阅验证失败”“回调超时”等问题需要排查的人。学完之后你会掌握豆包大模型 API 的调用方式、飞书自建应用的配置步骤、事件回调加解密流程、消息回复实现以及多维表格等业务数据的接入思路。2. 整体架构设计在动手写代码之前先把整体链路理清楚。2.1 消息流转链路当用户在飞书群里 机器人时实际发生的事件流如下用户发送消息到飞书服务器。飞书根据应用配置的事件订阅把事件请求推送到你的回调服务。回调服务验证请求合法性。回调服务从事件中解析出消息内容和用户信息。回调服务调用豆包大模型 API构造 Prompt 并获取回复。回调服务调用飞书 API把回复发送到对应会话。用户在飞书里看到机器人的回复。整个链路里最关键的是第 2 步到第 6 步。飞书的事件推送是异步的并且要求你的回调服务在短时间内返回 HTTPS 响应否则飞书会认为回调失败并可能重试。2.2 技术选型后端服务我选择 Java Spring Boot原因是团队现有技术栈是 Java而且 Spring Boot 处理 HTTP 回调比较方便。如果你更熟悉 Python也可以使用 FastAPI 或 Flask原理完全一样只是语言不同。需要准备的材料一个已认证的飞书企业或团队用于创建自建应用。一个公网可访问的 HTTPS 回调地址。本地调试时可以用内网穿透工具如 frp将本机地址暴露到公网或者直接部署到云服务器。一个豆包大模型 API Key。在火山方舟控制台开通模型服务获取 API Key 和接入点 ID。2.3 涉及的关键概念概念说明企业自建应用在飞书开放平台创建的应用拥有自己的 App ID 和 App Secret机器人自建应用开启机器人能力后可以出现在群聊或私聊中事件订阅飞书将用户操作事件推送给开发者的机制Encrypt Key事件订阅加密密钥用于对回调消息体做 AES 加解密Verification Token事件订阅校验令牌用于验证请求来源tenant_access_token应用的访问凭证调用飞书 OpenAPI 时需要携带Agent基于大模型构建的智能体可以对话、调用工具、处理任务3. 环境准备与版本说明本文示例以 Java 17 Spring Boot 3.x 为基础依赖管理使用 Maven。版本需要根据你的项目实际情况调整本文重点是演示配置思路。3.1 开发环境环境版本建议JDK17 或更高Spring Boot3.xMaven3.8豆包大模型火山方舟平台已开通的模型服务飞书开放平台新版后台3.2 创建飞书自建应用登录飞书开放平台进入“开发者后台”。点击“创建企业自建应用”填写应用名称和描述。创建完成后在“凭证与基础信息”页面获取 App ID 和 App Secret。在“添加应用能力”中启用机器人。在“权限管理”中申请以下常用权限im:message读取用户发给机器人的消息。im:message:send_as_bot以机器人身份发送消息。如果需要读取用户信息可以申请contact:user.base:readonly。如果需要操作多维表格申请bitable:app相关权限。这里特别提醒权限申请遵循最小够用原则不要一次性申请大量不相关的权限避免审核麻烦和安全风险。3.3 配置事件订阅在飞书开放平台找到“事件与回调”页面订阅“接收消息”事件事件名为im.message.receive_v1。填写请求地址例如https://yourdomain.com/api/feishu/callback。配置 Encrypt Key 和 Verification Token并把这两个值记录下来。如果你设置了 Encrypt Key飞书推送过来的请求体会被加密。下面会给出加解密代码。4. 核心代码实现下面用 Spring Boot 实现一个最小可运行的飞书事件回调服务并把豆包大模型接入进来。4.1 项目结构feishu-doubao-agent/ ├── pom.xml ├── src/main/java/com/example/feishuagent/ │ ├── FeishuAgentApplication.java │ ├── config/ │ │ └── FeishuProperties.java │ ├── controller/ │ │ └── FeishuCallbackController.java │ ├── crypto/ │ │ └── FeishuCryptoUtil.java │ ├── service/ │ │ ├── DoubaoService.java │ │ ├── FeishuService.java │ │ └── MessageHandler.java │ └── dto/ │ ├── CallbackRequest.java │ ├── EventMessage.java │ └── ReplyRequest.java └── src/main/resources/ └── application.yml4.2 Maven 依赖dependencies dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-web/artifactId /dependency dependency groupIdcom.squareup.okhttp3/groupId artifactIdokhttp/artifactId version4.12.0/version /dependency dependency groupIdcom.fasterxml.jackson.core/groupId artifactIdjackson-databind/artifactId /dependency /dependenciesOkHttp 用于调用飞书 API 和豆包大模型 API。Jackson 用于 JSON 解析。4.3 配置文件# 文件路径src/main/resources/application.yml server: port: 8080 feishu: app-id: cli_xxxxxxxx app-secret: xxxxxxxxxxxxxxxxxx encrypt-key: xxxxxxxxxxxxxxxxxx verification-token: xxxxxxxxxx doubao: api-key: xxxxxxxxxxxxxxxxxx endpoint-id: ep-xxxxxxxxx api-url: https://ark.cn-beijing.volces.com/api/v3/chat/completions注意这里只是示范实际项目中不要把密钥写死在 application.yml 中建议通过环境变量或配置中心管理。4.4 配置属性类// 文件路径src/main/java/com/example/feishuagent/config/FeishuProperties.java Component ConfigurationProperties(prefix feishu) public class FeishuProperties { private String appId; private String appSecret; private String encryptKey; private String verificationToken; // getter 和 setter 省略使用 Lombok Data 也可 }// 文件路径src/main/java/com/example/feishuagent/config/DoubaoProperties.java Component ConfigurationProperties(prefix doubao) public class DoubaoProperties { private String apiKey; private String endpointId; private String apiUrl; // getter 和 setter 省略 }4.5 加解密工具类飞书事件订阅的加解密逻辑是将 Encrypt Key 做 SHA-256 哈希取前 16 字节作为 AES 密钥加密算法为 AES-256-CBCIV 取密钥前 16 字节。Java 中的AES/CBC/PKCS5Padding可以兼容常见的 PKCS7 填充场景。// 文件路径src/main/java/com/example/feishuagent/crypto/FeishuCryptoUtil.java public class FeishuCryptoUtil { public static String decrypt(String encryptContent, String encryptKey) throws Exception { byte[] keyBytes sha256First16(encryptKey); byte[] ivBytes Arrays.copyOf(keyBytes, 16); Cipher cipher Cipher.getInstance(AES/CBC/PKCS5Padding); SecretKeySpec keySpec new SecretKeySpec(keyBytes, AES); IvParameterSpec ivSpec new IvParameterSpec(ivBytes); cipher.init(Cipher.DECRYPT_MODE, keySpec, ivSpec); byte[] decrypted cipher.doFinal(Base64.getDecoder().decode(encryptContent)); return new String(decrypted, StandardCharsets.UTF_8); } public static String encrypt(String content, String encryptKey) throws Exception { byte[] keyBytes sha256First16(encryptKey); byte[] ivBytes Arrays.copyOf(keyBytes, 16); Cipher cipher Cipher.getInstance(AES/CBC/PKCS5Padding); SecretKeySpec keySpec new SecretKeySpec(keyBytes, AES); IvParameterSpec ivSpec new IvParameterSpec(ivBytes); cipher.init(Cipher.ENCRYPT_MODE, keySpec, ivSpec); byte[] encrypted cipher.doFinal(content.getBytes(StandardCharsets.UTF_8)); return Base64.getEncoder().encodeToString(encrypted); } private static byte[] sha256First16(String input) throws Exception { MessageDigest md MessageDigest.getInstance(SHA-256); byte[] hash md.digest(input.getBytes(StandardCharsets.UTF_8)); return Arrays.copyOf(hash, 16); } }这里需要注意飞书文档中描述的是 AES-256-CBC 加 PKCS7但 Java 标准库没有直接提供PKCS7Padding实际上PKCS5Padding与之兼容。如果你用 Python可以使用pycryptodome库实现同样的逻辑。4.6 事件回调控制器回调接口需要处理两种情况应用配置事件订阅时飞书发来的 URL 验证请求以及后续的正式事件推送。// 文件路径src/main/java/com/example/feishuagent/controller/FeishuCallbackController.java RestController RequestMapping(/api/feishu) public class FeishuCallbackController { Autowired private FeishuProperties feishuProperties; Autowired private MessageHandler messageHandler; PostMapping(/callback) public MapString, String callback(RequestBody MapString, Object requestBody) throws Exception { String encrypt (String) requestBody.get(encrypt); String body; if (encrypt ! null !encrypt.isEmpty()) { body FeishuCryptoUtil.decrypt(encrypt, feishuProperties.getEncryptKey()); } else { body new ObjectMapper().writeValueAsString(requestBody); } JsonNode json new ObjectMapper().readTree(body); // 校验 verification token String token json.path(token).asText(); if (!feishuProperties.getVerificationToken().equals(token)) { throw new IllegalArgumentException(invalid token); } // URL 验证请求 if (json.has(type) url_verification.equals(json.path(type).asText())) { String challenge json.path(challenge).asText(); String responseJson {\challenge\:\ challenge \}; if (encrypt ! null !encrypt.isEmpty()) { String encryptedResponse FeishuCryptoUtil.encrypt(responseJson, feishuProperties.getEncryptKey()); MapString, String result new HashMap(); result.put(encrypt, encryptedResponse); return result; } MapString, String result new HashMap(); result.put(challenge, challenge); return result; } // 处理事件消息 messageHandler.handleEvent(body); MapString, String ok new HashMap(); ok.put(msg, success); return ok; } }需要注意回调接口必须快速返回。如果处理事件前需要调用模型接口不建议在回调线程里同步等待可以先返回成功再用异步线程处理消息。不过在最小实现中为了演示方便我们可以简化处理方式但生产环境一定要考虑异步化。4.7 消息处理服务// 文件路径src/main/java/com/example/feishuagent/service/MessageHandler.java Service public class MessageHandler { Autowired private DoubaoService doubaoService; Autowired private FeishuService feishuService; Async public void handleEvent(String body) { try { JsonNode json new ObjectMapper().readTree(body); String eventType json.path(header).path(event_type).asText(); if (!im.message.receive_v1.equals(eventType)) { return; } JsonNode event json.path(event); JsonNode message event.path(message); String messageId message.path(message_id).asText(); String messageType message.path(message_type).asText(); String content message.path(content).asText(); // 只处理文本消息 if (!text.equals(messageType)) { return; } // content 是 JSON 字符串需要解析 String text new ObjectMapper().readTree(content).path(text).asText(); // 过滤机器人自己发的消息避免死循环 JsonNode sender event.path(sender); String senderId sender.path(sender_id).path(open_id).asText(); String appId event.path(app_id).asText(); if (feishuProperties.getAppId().equals(senderId)) { return; } // 调用豆包生成回复 String reply doubaoService.chat(text); // 发送回复到飞书 feishuService.replyMessage(messageId, reply); } catch (Exception e) { e.printStackTrace(); } } }这里有个细节飞书推送事件时带上app_id如果你的开发环境里机器人自己也收到了消息要避免机器人回复自己造成循环。4.8 豆包大模型服务豆包大模型提供了 OpenAI 兼容的 API 格式。你可以直接使用 OpenAI 的请求结构只需要把 endpoint 和 api-key 换成自己的。// 文件路径src/main/java/com/example/feishuagent/service/DoubaoService.java Service public class DoubaoService { Autowired private DoubaoProperties doubaoProperties; private final OkHttpClient client new OkHttpClient.Builder() .connectTimeout(10, TimeUnit.SECONDS) .readTimeout(30, TimeUnit.SECONDS) .build(); private final ObjectMapper objectMapper new ObjectMapper(); public String chat(String userMessage) throws IOException { MapString, Object message new HashMap(); message.put(role, user); message.put(content, userMessage); MapString, Object requestBody new HashMap(); requestBody.put(model, doubaoProperties.getEndpointId()); requestBody.put(messages, List.of(message)); requestBody.put(stream, false); String json objectMapper.writeValueAsString(requestBody); Request request new Request.Builder() .url(doubaoProperties.getApiUrl()) .addHeader(Authorization, Bearer doubaoProperties.getApiKey()) .addHeader(Content-Type, application/json) .post(RequestBody.create(json, MediaType.parse(application/json; charsetutf-8))) .build(); try (Response response client.newCall(request).execute()) { if (!response.isSuccessful()) { throw new IOException(豆包 API 调用失败: response.code()); } String responseBody response.body().string(); JsonNode jsonNode objectMapper.readTree(responseBody); return jsonNode.path(choices).path(0).path(message).path(content).asText(); } } }这里说明一下endpoint-id的概念。在火山方舟平台你可以把接入点 ID 当作模型名称传入model字段也可以直接传入模型名称。不同账号、不同权限下的写法略有差异以你开通服务时控制台展示的信息为准。4.9 飞书消息发送服务发送消息前需要先通过 App ID 和 App Secret 获取tenant_access_token。// 文件路径src/main/java/com/example/feishuagent/service/FeishuService.java Service public class FeishuService { Autowired private FeishuProperties feishuProperties; private final OkHttpClient client new OkHttpClient.Builder() .connectTimeout(10, TimeUnit.SECONDS) .readTimeout(10, TimeUnit.SECONDS) .build(); private final ObjectMapper objectMapper new ObjectMapper(); private String tenantAccessToken; public String getTenantAccessToken() throws IOException { MapString, String body new HashMap(); body.put(app_id, feishuProperties.getAppId()); body.put(app_secret, feishuProperties.getAppSecret()); Request request new Request.Builder() .url(https://open.feishu.cn/open-apis/auth/v3/tenant_access_token/internal) .post(RequestBody.create(objectMapper.writeValueAsString(body), MediaType.parse(application/json; charsetutf-8))) .build(); try (Response response client.newCall(request).execute()) { String responseBody response.body().string(); JsonNode json objectMapper.readTree(responseBody); if (json.path(code).asInt() ! 0) { throw new IOException(获取 tenant_access_token 失败: responseBody); } return json.path(tenant_access_token).asText(); } } public void replyMessage(String messageId, String text) throws IOException { MapString, String content new HashMap(); content.put(text, text); MapString, String body new HashMap(); body.put(msg_type, text); body.put(content, objectMapper.writeValueAsString(content)); Request request new Request.Builder() .url(https://open.feishu.cn/open-apis/im/v1/messages/ messageId /reply) .addHeader(Authorization, Bearer getTenantAccessToken()) .addHeader(Content-Type, application/json; charsetutf-8) .post(RequestBody.create(objectMapper.writeValueAsString(body), MediaType.parse(application/json; charsetutf-8))) .build(); try (Response response client.newCall(request).execute()) { if (!response.isSuccessful()) { throw new IOException(发送消息失败: response.code()); } } } }tenant_access_token建议缓存起来不要每次请求都重新获取。飞书接口对获取 token 的调用频率有限制频繁调用容易触发限流。实际开发中可以将 token 和过期时间保存在内存或 Redis 中。5. 运行与验证5.1 启动服务确保回调地址可以被公网访问然后启动 Spring Boot 服务。mvn spring-boot:run5.2 配置飞书事件订阅回到飞书开放平台填写回调地址https://yourdomain.com/api/feishu/callback点击“保存”后飞书会发送一个 URL 验证请求。如果一切正常事件订阅状态会变成“已启用”。5.3 给机器人发消息在飞书群里添加应用机器人然后 机器人发送一条文本消息。正常情况下机器人会先返回事件订阅的回调确认HTTP 200随后异步收到豆包生成的回复。预期流程用户发送“帮我整理一下今天的工作计划”。回调服务收到事件。豆包 API 返回一段文本。飞书群里出现机器人的回复。如果出现消息没有回复的情况可以先查看后端日志确认事件是否到达、豆包 API 是否返回、飞书 API 是否报错。6. 进阶让 Agent 操作飞书多维表格上面的实现只完成了“文本对话”。要让 Agent 真的“像个同事”还需要给它接入飞书业务数据其中最典型的场景就是飞书多维表格。6.1 为什么需要分页飞书多维表格的 API 返回数据有数量限制默认一次最多返回 100 条记录。当表格里的记录很多时一次性全量拉取会非常慢甚至超时失败。很多同学第一次接入时报错就是因为没有处理分页。6.2 分页获取多维表格记录飞书开放平台bitable/v1/apps/{app_token}/tables/{table_id}/records接口支持page_size和page_token参数。第一次请求不传page_token响应中如果has_more为true则把page_token取出来继续请求下一页。public ListJsonNode getAllRecords(String appToken, String tableId) throws IOException { ListJsonNode allRecords new ArrayList(); String pageToken null; do { HttpUrl url HttpUrl.get(https://open.feishu.cn/open-apis/bitable/v1/apps/ appToken /tables/ tableId /records) .newBuilder() .addQueryParameter(page_size, 100) .build(); if (pageToken ! null) { url url.newBuilder().addQueryParameter(page_token, pageToken).build(); } Request request new Request.Builder() .url(url) .addHeader(Authorization, Bearer getTenantAccessToken()) .build(); try (Response response client.newCall(request).execute()) { JsonNode json objectMapper.readTree(response.body().string()); if (json.path(code).asInt() ! 0) { throw new IOException(获取多维表格数据失败: json); } JsonNode items json.path(data).path(items); for (JsonNode item : items) { allRecords.add(item); } pageToken json.path(data).path(page_token).asText(null); boolean hasMore json.path(data).path(has_more).asBoolean(false); if (!hasMore) { pageToken null; } } } while (pageToken ! null !pageToken.isEmpty()); return allRecords; }这个思路同样适用于 n8n 工作流中分页拉取多维表格记录。n8n 里的 “Execute by page” 或循环节点就是通过page_token一页页取完的原理和上面的代码一致。6.3 让豆包 Agent 调用工具想让 Agent 自己决定“什么时候查多维表格”可以使用大模型 Function Calling 能力。豆包大模型支持tools参数你可以声明一个工具比如{ type: function, function: { name: query_bitable_records, description: 查询飞书多维表格中指定数据表的记录, parameters: { type: object, properties: { app_token: { type: string, description: 多维表格 app_token }, table_id: { type: string, description: 数据表 ID } }, required: [app_token, table_id] } } }调用豆包接口时把tools放在请求体里。模型会根据用户意图决定是否调用工具并返回tool_calls。你的服务拿到tool_calls后执行对应的工具函数再把结果作为新的消息回传给模型最终得到给用户的回答。这是一个相对完整的 Agent 工作模式模型负责规划代码负责执行。如果想要更成熟的 Agent 框架目前社区也有很多选择比如 Dify、n8n 等但底层逻辑与本方案一致。7. 常见问题与排查思路7.1 事件订阅 URL 验证失败问题现象常见原因解决思路保存回调地址时提示验证失败回调地址公网无法访问确认域名解析、HTTPS 证书、端口是否开放验证失败日志中没有请求防火墙拦截或回调地址错误用 curl 模拟请求测试回调接口连通性解密报错Encrypt Key 填错检查飞书后台 Encrypt Key 与应用配置是否一致返回 challenge 格式不对加密开启后仍返回明文如配置了 Encrypt Key需对 challenge 返回做加密7.2 机器人收到消息但不回复问题现象常见原因解决思路日志显示事件已到达但没有回复缺少发送消息权限检查应用权限中是否包含im:message:send_as_bot日志报tenant_access_token获取失败App Secret 错误核对 App ID 和 App Secret日志报模型调用超时豆包 API 响应时间过长增加超时时间或使用异步处理回复内容没有出现在群里消息 ID 使用错误确认回复使用的是message_id而不是 Chat ID7.3 回调超时飞书事件推送要求回调接口在短时间内返回 2xx。如果事件处理和模型调用同步执行模型响应稍慢就容易触发超时。此时可以在回调接口中先返回成功把事件处理放到异步线程中。对应提示the agent execution provider did not respond in time这类问题本质上也是某个环节响应超时需要从网络、超时配置、模型调用耗时三个方向排查。7.4 消息重复处理飞书事件推送为了可靠性会做重试所以同一事件可能被推送多次。如果 Agent 每次都执行同样的操作可能会造成重复回复或重复写入数据。建议在处理事件时结合message_id做幂等已经处理过的消息直接跳过。8. 最佳实践与工程建议8.1 权限最小化飞书应用权限非常细化只申请当前功能需要的权限。比如只做消息回复就申请消息相关权限需要读取多维表格再加bitable:app权限。不要一个应用申请所有权限权限越大风险越大。8.2 密钥安全App Secret、Encrypt Key、豆包 API Key 都是敏感信息。不要把密钥提交到 Git更不要写死在前端代码中。推荐使用环境变量、配置中心或密钥管理服务。日志输出时也要对密钥做脱敏避免排查问题时把密钥带到日志平台。8.3 控制超时与限流豆包 API 调用设置合理的连接超时和读取超时。飞书消息发送接口也要设置超时避免网络异常导致线程阻塞。对用户请求可以做频控例如单用户每分钟最多请求 10 次防止有人恶意刷接口产生高额费用。8.4 上下文管理大模型对话不是无状态记忆的每次请求只能基于传入的 messages 生成回复。如果要做多轮对话可以把历史消息保存到 Redis 中每次组装最近的 N 条消息传给模型。历史消息过多会增加 token 消耗和响应延迟建议只保留最近 10 到 20 条。8.5 灰度发布不要在开发完成后直接对全员开放。可以先创建一个测试群把机器人加进测试群验证稳定再逐渐扩大到其他群。生产环境如果发现了问题可以通过飞书后台临时停用机器人不会影响其他业务。8.6 日志与监控建议为回调接口、豆包调用、飞书消息发送分别记录日志回调是否到达。模型返回耗时和 token 消耗。消息发送是否成功。异常堆栈。这样即使出问题也能快速定位是哪个环节出的问题。9. 总结与后续方向把豆包接进飞书之后Agent 从“聊天玩具”变成了能接收任务、跨应用读写数据的办公助手。本文核心部分已经跑通了“飞书消息 → 事件回调 → 豆包模型 → 飞书回复”的闭环并且给出了多维表格分页和 Function Calling 的思路。下一步你可以尝试的方向把豆包 Agent 接入飞书审批流实现自动审批提示。用飞书多维表格存储 Agent 的问答记录方便后续分析和优化 Prompt。结合 Jenkins 或 n8n让 Agent 在构建失败时自动分析日志并给出修复建议。在现有 Agent 框架例如 Dify中接入豆包模型减少重复开发。每个团队的使用场景不同但底层的接入思路是通用的。如果这篇文章对你有帮助可以收藏备用也欢迎在评论区交流你接入时遇到的报错和解决方案。