飞书多层级交互必须使用应用机器人模式,而非Webhook;因Webhook单向无状态,无法获取message_id/chat_id来调用message/reply API,强行使用将触发16001错误或静默失败。

飞书机器人做多层级交互回复,本质不是靠单次 webhook 推送,而是必须切换到「应用机器人」模式,用 message/reply API + 事件订阅机制来实现。硬用 webhook 模拟“回复”会失败或被限流。
为什么 webhook 无法支撑多层级交互
Webhook 是单向、无状态的推送通道,它只接受你发过去的消息体,不返回上下文、不保存会话、也不支持“针对某条消息回复”。当你想实现“用户问A → 机器人答B → 用户基于B追问C → 机器人再答D”这种链路时,webhook 根本拿不到原始消息的 message_id 和 chat_id,更没法调用 message/reply 接口。
- 常见错误现象:
{"code": 16001, "msg": "Invalid message_id"}或静默失败,消息根本不出现在对话中 - 使用场景:只有在「收到用户@消息」或「群内关键词触发」后需展开多轮问答(如工单查询→选状态→导出详情)时,才必须走应用机器人路径
- 性能影响:webhook 调用频率限制宽松(约 50 QPS),但
message/reply受 tenant_access_token 有效期和 per-chat 限流约束,需做好 token 刷新和重试
必须启用的飞书开放平台配置项
跳过这一步,后续所有代码都只是空转。重点不是“怎么写”,而是“有没有权限”。
- 在飞书开放平台创建「企业自建应用」,不是「群机器人」——后者连
message/reply权限都没有 - 权限管理中至少勾选:
im:message:send(发送消息)、im:message:receive(接收消息)、contact:user:readonly(读用户信息,用于识别提问人) - 发布应用后,在「事件订阅」里开启
im.message.receive_v1事件,并配置你自己的 HTTPS 回调地址(需备案、有合法证书) - Webhook 地址此时完全弃用;所有消息入口统一走回调解析
event.message.message_id和event.message.chat_id
Python 中处理多级回复的关键逻辑链
核心是把用户消息当事件触发器,而不是待响应的 HTTP 请求。每一轮交互都要显式记录上下文,不能依赖 session 或内存变量(服务重启就丢)。
开箱即用的采购订单自动审核 Skill:读取本地订单或飞书邮箱附件,调用 Laiye ADP 识别跨国采购订单,初始化/写入飞书多维表格订单工作台,上传订单源文件,并给审核负责人发送结构化私信,让业务人员只盯未匹配到内部商品、价格异常、新客户、币种异常等异常。
立即学习“Python免费学习笔记(深入)”;
- 收到
im.message.receive_v1事件后,先校验event.header.token和event.header.sign,防止伪造请求 - 提取
event.event.message.chat_id、event.event.message.message_id、event.event.message.text,存入本地数据库或 Redis,键为f"session:{chat_id}" - 根据当前
session状态机决定下一步动作:比如上一轮发了「请选择:1. 查进度 2. 改备注」,这次收到「1」就查订单,收到「2」就进编辑流程 - 调用
https://open.feishu.cn/open-apis/im/v1/messages/{message_id}/reply时,message_id必须来自事件,content字段要 JSON 序列化,且msg_type支持text、interactive(卡片)等,不能填post
容易忽略的兼容性与安全细节
这些点不报错,但会导致交互中断或被封禁。
-
tenant_access_token有效期 2 小时,必须缓存并自动刷新;每次message/reply前检查是否过期,否则返回999917错误 - 飞书对同一
chat_id的回复频率有限制(约 1 条/秒),高频交互需加time.sleep(1.1),不能靠重试硬扛 - 用户撤回消息时,飞书会发
im.message.delete_v1事件,若你的 session 还在,得同步清理,否则下次回复会指向已删除消息 - 卡片消息中的按钮
action如果设为open_url,URL 必须是白名单域名;如果是callback,则回调地址必须和事件订阅地址一致
真正的难点不在代码长度,而在于把「事件驱动」思维从 HTTP request/response 里剥离出来。很多人卡在第一步:以为复制粘贴几行 requests.post 就能做交互,结果发现永远只能发一次,再也接不上第二轮。

















