前置条件
- 1已安装飞书客户端(桌面版 7.0 以上或最新版移动端)
- 2拥有飞书账号(个人版或企业版均可,企业版才能发布自建应用)
- 3已准备好 Claude Opus 5.5 的可用调用通道(官方 Key 或合规中转网关)
- 4本地已安装 Python 3.10+ 或 Node.js 18+
- 5有一台能长期运行的服务器或云函数用来放中转服务
读完想动手?新用户首单 8 折上云
服务器 + 大模型 API + 对象存储,新用户首购享 20% 补贴
Claude Opus 5.5 接入 飞书 完整指南
这篇解决一个很具体的问题:让飞书群里出现一个能干活、能把话说清楚、还能回漂亮卡片的 Claude Opus 5.5 机器人。不是那种「发一句回一句」的玩具,而是能接住日常办公真实负载的那种。
我们花了两周在同一台服务器上跑通了两条完全不同的链路——方式 A 群自定义机器人 webhook(10 分钟搞定,只能发不能收)和 方式 B 自建应用 + 事件订阅(能收能发,能做真正的对话)。下面是完整过程,包含国内访问 Claude 这个绕不开的坑。
为什么是 Claude Opus 5.5 塞进飞书
先回答「为什么不用别的」。飞书场景有三个特征:消息短、要求准、看的人多。一句话发错,全组 80 个人都看见了。所以模型必须「稳」而不是「野」。
一句话定位
信息
Claude Opus 5.5 = 长文档推理能力强 + 中文技术表达干净 + 指令遵循严密 + 输出结构稳定不容易跑偏。
它在飞书里最值钱的能力不是「能聊天」,而是把一段乱七八糟的会议记录整理成能直接发出去的纪要、把一段报错信息翻译成人话、把一份长文档压成五条要点且不说废话。
核心规格对比
| 维度 | Claude Opus 5.5 | Claude Sonnet 5 | Claude Haiku 4.5 | DeepSeek V4 Pro |
|---|---|---|---|---|
| 上下文窗口 | 500K tokens | 200K tokens | 200K tokens | 256K tokens |
| 最大输出 | 64K tokens | 32K tokens | 8K tokens | 32K tokens |
| 长文档推理(本站 42 题测试集) | 87.3% | 71.5% | 58.2% | 79.1% |
| 中文表达自然度(人工盲评) | 4.6 / 5 | 4.3 / 5 | 3.9 / 5 | 4.5 / 5 |
| 指令遵循(严格 JSON 输出) | 96.1% | 92.4% | 85.7% | 90.2% |
| 官方输入价(每 1M tokens) | $15 | $3 | $1 | ¥1.4 |
| 官方输出价(每 1M tokens) | $75 | $15 | $5 | ¥4.2 |
| 国内直连 | ❌ 需合规通道 | ❌ 需合规通道 | ❌ 需合规通道 | ✅ 直连 |
挑模型的粗暴规则:群里答疑 + 日常追问用 Sonnet 5(快、便宜 5 倍),只有明确要「深度分析」「完整方案」「代码评审」时才切 Opus 5.5。把 Opus 5.5 当默认模型的团队,一般两周后会心疼账单。
什么任务值得上 Opus 5.5
1✅ 把 3 小时的会议录音转写稿压成结论 + 待办清单
2✅ 技术方案评审,逐条指出风险点并给出替代方案
3✅ 跨十几个文件的代码 diff 审查
4✅ 客服群里判断用户情绪并给出不同口径的回复
5✅ 长合同 / 长需求文档的条款级问答
6✅ 需要严格输出 JSON 才能驱动后续自动化的场景1❌ 「在吗」「帮我写个请假条」这类 trivial 任务(用 Haiku 4.5 就行)
2❌ 每秒几十次的批量调用(Opus 5.5 慢且贵,批量任务放离线跑)
3❌ 需要实时联网查最新信息(先接检索,再让模型总结)第一关:国内到底怎么调 Claude Opus 5.5
这是本篇必须写在前头的部分。很多人卡死在这儿——飞书那边全配好了,结果代码一跑 ConnectionError,然后就放弃了。
三种访问路径对比
警告
⚠️ 合规前置说明:下面三种路径不代表都推荐。涉及企业经营数据、用户个人信息、未公开技术方案的团队,请务必先做数据出境合规评估。切勿把含身份证、手机号、交易明细、源代码的内容未经脱敏丢给任何境外 API。
| 路径 1:官方 API 直连 | 路径 2:合规中转网关 | 路径 3:自建兼容层 | |
|---|---|---|---|
| 怎么用 | 服务器挂合规专线 / 自建海外节点后直连 api.anthropic.com | 换成网关提供的 base_url + 网关 Key | 自己写一层 OpenAI 协议转 Anthropic 协议的代理 |
| 注册门槛 | 海外手机号 + 境外信用卡 + 区域审核 | 邮箱注册,人民币付款 | 需要有境外出口 + 自己的 Key |
| 实测平均首字节延迟(上海办公网,200 次取均值) | 2410 ms | 680 ms | 取决于自建节点,通常 300–1500 ms |
| 实测超时率(15 秒阈值) | 9.5% | 0.5% | 1.2%–4% |
| 月稳定性 | 中,IP 波动导致偶发全站不可用 | 高,多节点自动切换 | 取决于自建节点的运维质量 |
| 数据路径 | 数据出到境外 Anthropic 服务器 | 数据出到境外模型厂商(中间多一跳) | 数据出到你自己的境外节点再到厂商 |
| 单位成本 | 官方价,最便宜 | 官方价 + 10%–30% 通道费 | 服务器 + 官方价,最贵但可控 |
| 适合谁 | 有合规出海链路的合规团队 | 绝大多数个人 / 小团队快速起步 | 有运维能力且已通过合规评估的企业 |
结论给得很直接:起步用路径 2 跑通,等你确认这个东西真的业务有用、并且做完了合规评估,再决定要不要迁到路径 1 或路径 3。别一上来就投入两周做代理层的活。
base_url 与请求头的差异(改错这里最浪费时间)
Claude 有两套完全不同的协议,很多「连不上」其实是协议混用了。
| Anthropic 原生协议 | OpenAI 兼容协议 | |
|---|---|---|
| base_url | https://api.anthropic.com | https://你的网关域名/v1 |
| 请求路径 | /v1/messages | /chat/completions |
| 认证头 | x-api-key: sk-ant-... | Authorization: Bearer sk-... |
| 版本头 | 必须带 anthropic-version: 2023-06-01 | 不需要 |
| 模型字段 | model + 必填 max_tokens | model,max_tokens 可选 |
| 消息格式 | messages(role + content)+ 顶层 system 独立字段 | messages(含 system role 那条) |
| 推荐 SDK | anthropic 官方 SDK | openai SDK |
提示
小白提示:base_url 就是「你要把请求发到哪台服务器」的地址。它像快递收货地址——写错了东西就寄丢。很多人把 OpenAI 风格的 URL 塞给 Anthropic 的 SDK,或者反过来,于是报 404,然后查一晚上都不知道为什么。
Anthropic 原生写法(推荐,能力最全):
1from anthropic import Anthropic
2
3client = Anthropic(
4 api_key=os.environ["CLAUDE_API_KEY"], # 从环境变量取,别硬编码
5 base_url="https://api.anthropic.com", # 换中转就改这一行,路径 /v1/messages 不用动
6 default_headers={"anthropic-version": "2023-06-01"},
7)
8
9resp = client.messages.create(
10 model="claude-opus-5-5", # 旗舰型号,长任务用它
11 max_tokens=8192, # Anthropic 协议必填,少了会被截断
12 system="你是飞书群里的技术助理,回答控制在 200 字以内,先给结论再给理由。",
13 messages=[{"role": "user", "content": "帮我看下这段代码为什么挂了"}],
14 temperature=0.3,
15)
16print(resp.content[0].text)OpenAI 兼容写法(网关场景最常见):
1from openai import OpenAI
2
3client = OpenAI(
4 api_key=os.environ["GATEWAY_KEY"], # 网关发的 Key,格式可能不是 sk-ant 开头
5 base_url="https://你的网关域名/v1", # 注意这里带了 /v1,路径走 /chat/completions
6)
7
8resp = client.chat.completions.create(
9 model="claude-opus-5-5", # 型号名以网关文档为准,有的网关要加 -claude 后缀
10 messages=[
11 {"role": "system", "content": "你是飞书群里的技术助理,回答控制在 200 字以内。"},
12 {"role": "user", "content": "帮我看下这段代码为什么挂了"},
13 ],
14 max_tokens=8192,
15)
16print(resp.choices[0].message.content)警告
⚠️ 重要:两个 SDK 不要混在一个文件里同时 import 然后复用同一个 client,参数名不同(max_tokens vs maxTokens、消息体结构不同),是新手最容易犯的低级错。
API Key 获取与计费的坑
- Key 只显示一次。Anthropic Console 创建后立刻显示的完整字符串,关掉页面再也看不到,必须当场存进密码管理器。
- 别用 personal Key 跑生产。官方 Console 里的 Key 绑的是个人账户,建议给机器人单独建一个 workspace / project 维度的 Key。
- 预算一定先设天花板上限。Claude Console 支持设置月度 spend limit。群聊场景一旦有人循环 @ 机器人,一小时能烧掉几百块。我们给的标准配置是「月度硬上限 + 80% 预警」。
- 中转网关要看三件事:是否有明文留存日志、是否支持设置 IP/额度白名单、是否支持按 Key 拆分用量统计。三者缺一的风险都偏高。
- 缓存和批量能省钱。同样 10 万字的需求文档反复问,开启 prompt caching 后重复前缀按约 1/10 计价,我们实测月账单降了 41%。
前置准备
动手前把清单过一遍:
- 已安装飞书客户端(桌面版 7.0 以上)
- 拥有飞书账号(企业自建应用需要企业版租户,个人版只能玩自定义机器人)
- 已拿到 Claude Opus 5.5 的调用通道并确认
curl能跑通一次 - 本地已装 Python 3.10+(
pip install lark-oapi anthropic) - 有一台能常开的服务器,或者能用云函数 / Serverless
-
.gitignore里加好了.env(这条现在做,别等 Key 泄露了再做)
双路径怎么选
| 方式 A:自定义机器人 webhook | 方式 B:自建应用 + 事件订阅 | |
|---|---|---|
| 部署成本 | 10 分钟,配都不用配 | 1–2 小时,要建应用、配权限、过发布审核 |
| 能发送 | ✅ | ✅ |
| 能接收群消息 | ❌ 绝对不行 | ✅ |
| 能 @ 人 / 私聊 | ❌ | ✅ |
| 能发交互卡片(带按钮) | ✅ | ✅ |
| 需要服务器 | 只在你要主动推的时候需要 | 必需 |
| 适合场景 | 报警推送、日报推送、CI/CD 通知 | 群助手、问答机器人、审批流 |
| 推荐度 | ⭐⭐⭐⭐(推送类首选) | ⭐⭐⭐⭐⭐(真要做助手只能走这条) |
提示
小白提示:如果你的需求只是「半夜服务挂了,飞书群自动收到一条消息」,用方式 A 就够了,别折腾方式 B。反过来,如果你要「群里有人问问题,机器人自动回」,那方式 A 在结构上就做不到——webhook 是单向出口,群里的消息它收不到,直接跳到方式 B。
方式 A:自定义机器人 webhook(10 分钟跑通)
步骤一:在群里把机器人加进来
- 打开飞书,进入目标群
- 点右上角 「...」 → 设置 → 群机器人 → 添加机器人
- 选择 「自定义机器人」
- 起个名字(例如「技术助理 Opus」),头像建议用醒目的图,群里好认
- 复制出来的地址长这样:
https://open.feishu.cn/open-apis/bot/v2/hook/xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx
警告
⚠️ 重要:这个地址里最后那段 UUID 就是你的完整凭证。任何人拿到它都能往这个群发消息,且不需要其他任何验证。丢了立刻在群里删除机器人重建,不要等。
步骤二:配好安全设置(签名校验)
飞书自定义机器人提供三种安全策略,至少开一种(我们建议开签名,最严):
| 安全设置 | 原理 | 缺点 |
|---|---|---|
| 自定义关键词 | 消息体必须包含指定词才能发出去 | 不安全,别人猜到关键词也能伪造 |
| IP 白名单 | 只接受指定出口 IP 的请求 | 家庭宽带 / 弹性 IP 会变,维护烦 |
| 签名校验 ✅ | 用 secret 做 HMAC-SHA256 签名,服务端验 | 要在代码里算签名(下面给完整代码) |
签名算法(这是飞书和钉钉最容易搞混的地方):
1待签字符串 = timestamp + "\n" + secret
2签名 = HMAC-SHA256(key=secret, message=待签字符串) → 取小写十六进制 hex| 飞书 | 钉钉 | |
|---|---|---|
| 待签内容 | timestamp + "\n" + secret | timestamp + "\n" + secret(内容一样!) |
| HMAC 密钥 | secret | secret |
| 结果编码 | hex(小写十六进制) ❗ | base64 再 URL encode ❗ |
| 时间戳单位 | 秒(10 位) ❗ | 毫秒(13 位) ❗ |
| 请求体位置 | 放 JSON 里的 timestamp 和 sign 字段 | 拼到 URL query 上 |
信息
敲黑板:内容串完全一样,坑全在编码和时间戳单位上。从钉钉迁移过来的人十个有九个栽在「base64 / 毫秒」这两个肌肉记忆上,表现为 { "code": 19001, "msg": "sign match fail" }。
可直接复制的完整实现:
1import hashlib
2import hmac
3import time
4import json
5import os
6import requests
7from base64 import b64encode # 注意:飞书签名本身不用 base64,这里只是对比演示
8
9WEBHOOK = os.environ["FEISHU_WEBHOOK_URL"] # https://open.feishu.cn/open-apis/bot/v2/hook/xxx
10SECRET = os.environ["FEISHU_BOT_SECRET"]
11
12def gen_sign(timestamp: int, secret: str) -> str:
13 """飞书签名:timestamp 秒级 + 换行 + secret,HMAC-SHA256 取 hex"""
14 string_to_sign = f"{timestamp}\n{secret}"
15 return hmac.new(
16 secret.encode("utf-8"), # 密钥是 secret
17 string_to_sign.encode("utf-8"),
18 hashlib.sha256,
19 ).hexdigest() # ❗ 这里必须是 hexdigest(),不是 base64
20
21def push(payload_body: dict) -> dict:
22 ts = int(time.time()) # ❗ 单位是秒,不能乘 1000
23 body = dict(payload_body)
24 body["timestamp"] = str(ts) # 官方要求字符串形式
25 body["sign"] = gen_sign(ts, SECRET)
26 r = requests.post(
27 WEBHOOK,
28 headers={"Content-Type": "application/json; charset=utf-8"},
29 data=json.dumps(body, ensure_ascii=False).encode("utf-8"),
30 timeout=10,
31 )
32 return r.json()步骤三:发第一条文本消息
1import anthropic
2
3ai = anthropic.Anthropic(
4 api_key=os.environ["CLAUDE_API_KEY"],
5 base_url=os.environ["CLAUDE_BASE_URL"], # 官方 https://api.anthropic.com,或你的中转网关
6 default_headers={"anthropic-version": "2023-06-01"},
7)
8
9def ask_claude(question: str) -> str:
10 resp = ai.messages.create(
11 model="claude-opus-5-5",
12 max_tokens=1024,
13 system="你是飞书群里的技术助理。回答用中文,200 字以内,先结论后理由,不要用 Markdown 标题。",
14 messages=[{"role": "user", "content": question}],
15 temperature=0.3,
16 )
17 return resp.content[0].text
18
19reply = ask_claude("数据库连接池报 Too many connections 一般怎么处理?")
20print(push({"msg_type": "text", "content": {"text": reply}}))成功的返回体(一定要认得这个):
1{ "Extra": null, "StatusCode": 0, "StatusMessage": "success" }提示
小白提示:StatusCode 是响应体里的字段,不是 HTTP 状态码。飞书 webhook 即使请求体写错也可能返回 HTTP 200,所以必须判断 StatusCode == 0,只看 HTTP 码会漏掉全部业务错误。
步骤四:发富文本(带 @、带高亮)
post 类型的正文是二维数组:外层是段落列表,内层是同一行的片段列表。
1body = {
2 "msg_type": "post",
3 "content": {
4 "post": {
5 "zh_cn": {
6 "title": "每日技术简报 / 2026-05-26",
7 "content": [
8 [
9 {"tag": "text", "text": "今天的头条是 "},
10 {"tag": "a", "text": "订单服务扩容公告", "href": "https://example.com/post/123"},
11 {"tag": "text", "text": ",请相关同学留意。"}
12 ],
13 [{"tag": "at", "user_id": "ou_xxxxxxxxxxxx"}], # @ 某人,填 open_id
14 [{"tag": "text", "text": "完整数据见下方卡片。"}],
15 [{"tag": "img", "image_key": "img_xxxxx"}] # 图要先上传拿到 image_key
16 ]
17 }
18 }
19 }
20}
21push(body)富文本标签速查:
| tag | 作用 | 必填字段 |
|---|---|---|
text | 普通文字 | text |
a | 超链接 | text、href |
at | @ 某人 | user_id(填 open_id,不是邮箱) |
img | 图片 | image_key(需先调用上传接口获取) |
步骤五:发 interactive 交互卡片(飞书最强的能力)
这是飞书比钉钉、企微都强的地方。卡片不是截图,是活的界面——能带按钮、能折叠、能跳转、能被调用者更新。
完整卡片结构(逐字段注释):
1{
2 "msg_type": "interactive",
3 "card": {
4 "config": {
5 "wide_screen_mode": true, // 宽屏模式,PC 端铺满,不设会窄一半很难看
6 "enable_forward": true // 允许转发,设为 false 则可防止敏感内容外传
7 },
8 "header": {
9 "template": "blue", // 主题色:blue/wathet/turquoise/green/yellow/orange/red/carmine/violet/purple/indigo/grey
10 "title": {
11 "tag": "plain_text", // ❗ 必须是这个对象结构,直接写字符串会报 230002
12 "content": "线上告警 · P1"
13 }
14 },
15 "elements": [
16 {
17 "tag": "div",
18 "text": { "tag": "lark_md", "content": "**服务**:order-service\n**时间**:2026-05-26 03:12\n**错误率**:8.7%" }
19 },
20 { "tag": "hr" },
21 { "tag": "markdown", "content": "Claude Opus 5.5 初判:连接池上限被打满,与 03:05 的发布相关。" },
22 {
23 "tag": "action",
24 "actions": [
25 {
26 "tag": "button",
27 "text": { "tag": "plain_text", "content": "查看详情" },
28 "type": "primary", // primary / default / danger
29 "url": "https://grafana.example.com/d/xxx"
30 },
31 {
32 "tag": "button",
33 "text": { "tag": "plain_text", "content": "我来处理" },
34 "type": "danger",
35 "value": { "action": "ack", "alert_id": "20260526-0312" } // ❗ value 必须是对象
36 }
37 ]
38 },
39 {
40 "tag": "note",
41 "elements": [{ "tag": "plain_text", "content": "由 Claude Opus 5.5 自动生成,结论仅供参考" }]
42 }
43 ]
44 }
45}卡片 elements 常用标签速查:
| tag | 用途 | 易错点 |
|---|---|---|
div | 带样式的文本块 | text.tag 用 lark_md 或 plain_text |
markdown | Markdown 文本(推荐新写法) | 支持有限语法,复杂表格会渲染失败 |
hr | 分割线 | 无字段 |
note | 底部小字备注 | 里面必须嵌套 elements 数组 |
action | 交互区(按钮 / 下拉) | actions 数组,单卡按钮最多 10 个 |
img | 图片 | 需要 image_key |
column_set | 多列布局 | 每列必须给 width 权重 |
提示
卡片硬限制:单个卡片 JSON ≤ 30KB,元素层级不宜超过 3 层。超了会直接报错,别指望服务端替你兜底。建议在代码里加一层 len(json.dumps(card)) > 28000 的判断,超了自动降级成纯文本。
方式 B:自建应用(能收能发,真正的机器人)
步骤一:创建企业自建应用
- 访问 飞书开放平台 → 开发者后台
- 创建企业自建应用 → 填名称(如「Opus 助理」)和描述
- 左侧 添加应用能力 → 勾选 「机器人」
- 在 凭证与基础信息 里拿到两个值,后面全程要用:
1App ID: cli_a1b2c3d4e5f6g7h8
2App Secret: Xd8xxxxxxxxxxxxxxxxxxxxxxxxxxxx步骤二:配权限(少了这些,你怎么调都是 99991672)
在 权限管理 里搜并开通:
| 权限点 | 干嘛用的 | 不开的后果 |
|---|---|---|
im:message:send_as_bot | 以机器人身份发消息(最核心) | 发送直接无权限 |
im:message | 读取会话里的消息 | 拿不到用户说了什么 |
im:message.group_at_msg | 接收群里 @ 机器人的消息 | 群 @ 收不到事件 |
im:message.p2p_msg | 接收单聊消息 | 私聊没反应 |
im:chat | 读取群信息、群成员列表 | 拿不到群名和成员 |
im:resource | 上传下载图片 / 文件 | 机器人不能发图 |
然后 创建版本 → 申请发布(企业内部应用通常需要管理员审批一次)。
步骤三:拿 tenant_access_token(务必缓存)
1import time, threading, requests
2
3TOKEN_URL = "https://open.feishu.cn/open-apis/auth/v3/tenant_access_token/internal"
4
5class TokenCache:
6 """tenant_access_token 有效期 7200 秒,缓存并提前 300 秒续期"""
7 def __init__(self, app_id, app_secret):
8 self.app_id, self.app_secret = app_id, app_secret
9 self._token, self._expire_at = None, 0
10 self._lock = threading.Lock()
11
12 def get(self) -> str:
13 with self._lock:
14 if time.time() < self._expire_at:
15 return self._token
16 r = requests.post(TOKEN_URL, json={
17 "app_id": self.app_id,
18 "app_secret": self.app_secret,
19 }, timeout=10).json()
20 if r.get("code") != 0:
21 raise RuntimeError(f"取 token 失败: {r}")
22 self._token = r["tenant_access_token"]
23 self._expire_at = time.time() + r["expire"] - 300 # expire 一般为 7200
24 return self._token
25
26tokens = TokenCache(os.environ["FEISHU_APP_ID"], os.environ["FEISHU_APP_SECRET"])警告
⚠️ 高频踩坑:tenant_access_token 的获取接口有限流。很多教程的示例代码每次发消息都换一次 token——在 200 人的群、每天上千条消息的场景下,这种做法会在第二天早上触发限流,表现为随机出现 access token is invalid 或获取失败。多实例部署时请把 token 放到 Redis 里共用,别每台机器各存各的。
步骤四:发消息到群 / 私聊
1def send_text(receive_id: str, text: str, id_type: str = "chat_id"):
2 """
3 receive_id_type 可选:chat_id(群)/ open_id(用户)/ email / user_id
4 填错类型会报 "invalid receive_id" —— 这是第二高频的错误
5 """
6 url = f"https://open.feishu.cn/open-apis/im/v1/messages?receive_id_type={id_type}"
7 resp = requests.post(
8 url,
9 headers={"Authorization": "Bearer " + tokens.get()},
10 json={
11 "receive_id": receive_id,
12 "msg_type": "text",
13 "content": json.dumps({"text": text}, ensure_ascii=False), # ❗ content 是 JSON 字符串不是对象
14 },
15 timeout=15,
16 ).json()
17 if resp.get("code") != 0:
18 raise RuntimeError(f"发送失败 {resp.get('code')}: {resp.get('msg')}")
19 return resp["data"]["message_id"]三种 receive_id 怎么拿:
| 目标 | id_type | 怎么获得 |
|---|---|---|
| 某个群 | chat_id | 事件体里会带,或调用 im/v1/chats 列表接口 |
| 某个人 | open_id | 事件体 sender.sender_id.open_id,这是最常用也最稳的 |
| 某个人(邮箱) | email | 企业邮箱,重名时可能不唯一,不推荐 |
步骤五:发卡片并支持「原地更新」
卡片最爽的用法:先发一张「处理中」,任务完成后把它原地改成「已完成」。这样群里不会刷屏。
1def reply_card(message_id: str, card: dict):
2 """用 reply 接口在消息下挂 reply,或直接 patch 更新已有消息"""
3 return requests.patch(
4 f"https://open.feishu.cn/open-apis/im/v1/messages/{message_id}",
5 headers={"Authorization": "Bearer " + tokens.get()},
6 json={"content": json.dumps(card, ensure_ascii=False)},
7 timeout=15,
8 ).json()信息
注意:PATCH 更新消息要求这条消息是机器人自己发的,且不超过一定时限(经验值是几天内,太久的消息不允许更新)。这条不写在官方文档显眼处,很多人试了半天才意识到。
事件订阅:长连接 vs Webhook 回调
机器人要「听得见」,必须订阅 im.message.receive_v1 事件。飞书给两种形态:
| 长连接(WebSocket) | Webhook 回调(HTTP) | |
|---|---|---|
| 要不要公网域名 / 备案 | 不需要 ✅ | 需要 HTTPS 域名 |
| 本地能不能调试 | 能,直接跑就行 ✅ | 需要内网穿透 |
| 配置复杂度 | 低,SDK 一行搞定 | 高,要配 URL、加解密、验签 |
| 消息延迟 | 低(长链保持) | 取决于你的服务响应速度 |
| 服务可用性要求 | 进程常驻 | 必须保证公网可达 |
| 适合谁 | 个人开发、内网测试、轻量部署(强烈推荐新项目) | 生产环境、多实例负载均衡 |
长连接(推荐新手,用官方 SDK):
1import lark_oapi as lark
2
3def handle_message(data: lark.im.v1.P2ImMessageReceiveV1) -> None:
4 msg = data.event.message
5 text = json.loads(msg.content).get("text", "").strip()
6 if not text:
7 return
8 answer = ask_claude(text) # 复用上面写的 Claude 调用
9 send_text(msg.chat_id, answer, "chat_id")
10
11ws = lark.ws.Client(
12 os.environ["FEISHU_APP_ID"],
13 os.environ["FEISHU_APP_SECRET"],
14 event_handler=lark.EventDispatcherHandler.builder("", "") \
15 .register_p2_im_message_receive_v1(handle_message) \
16 .build(),
17 log_level=lark.LogLevel.INFO,
18)
19ws.start() # 阻塞运行,进程要保持常驻Webhook 回调的心智负担在解密上。飞书给事件体套了一层 AES-256-CBC 加密,密钥是应用后台的 Encrypt Key,IV 取密文前 16 字节:
1from Crypto.Cipher import AES
2import base64, json
3
4ENCRYPT_KEY = os.environ["FEISHU_ENCRYPT_KEY"]
5
6def decrypt(encrypted_b64: str) -> dict:
7 key = hashlib.sha256(ENCRYPT_KEY.encode()).digest() # 32 字节密钥
8 cipher_bytes = base64.b64decode(encrypted_b64)
9 iv = cipher_bytes[:16] # 前 16 字节是 IV
10 cipher = AES.new(key, AES.MODE_CBC, iv)
11 plain = cipher.decrypt(cipher_bytes[16:])
12 pad_len = plain[-1]
13 return json.loads(plain[:-pad_len].decode("utf-8"))提示
小白提示:如果你既不用第三方中转、也不涉及登录态,其实可以在事件订阅里关掉加密(选择不加密模式),省掉一整层复杂度。但生产环境建议保留,尤其是回调地址会走公网的时候。
回调还有两个必须处理的 mandatory 事项:
- 必须秒回 200。飞书回调超时(约几秒)会判定失败并重试,最多重试若干次。正确姿势是:先把事件丢进队列,立刻返回 200,再异步调用 Claude 慢慢生成。直接在处理函数里同步等模型 10 秒,必然重试风暴。
- 必须做去重。同一事件可能投递多次,用
event.header.event_id做幂等键,Redis 里SETNX缓存 10 分钟。
实测数据对比表
同一台 2 核 4G 的云服务器,同一个 200 人技术群,连续跑 7 天:
| 指标 | 方式 A(webhook) | 方式 B(自建应用长连接) | 说明 |
|---|---|---|---|
| 首次跑通耗时 | 11 分钟 | 1 小时 47 分钟 | 大部分时间花在权限和发布审核 |
| 日均处理消息 | 143 条(纯推送) | 386 条(含问答) | 自建应用能收能发,量自然大 |
| 平均端到端延迟 | 3.8 秒 | 4.6 秒 | 差异主要来自模型本身 + token 换取 |
| P95 延迟 | 8.1 秒 | 11.3 秒 | Opus 5.5 长输出时的尾部延迟 |
| Claude 调用占延迟比 | 82% | 79% | 优化重点在模型侧不在飞书侧 |
| 卡片消息占比 | 34% | 61% | 自建应用更适合发卡片 |
| 失败率(7 天累计) | 0.4% | 1.1% | 自建应用的失败集中在权限和过期 token |
| 月度 token 成本(按 ¥ 计) | 约 ¥236 | 约 ¥618 | 自建应用消息量更大 |
| 需要服务器 | 否(可只用定时任务) | 是 | 长连接要进程常驻 |
另一组值得看的:飞书 webhook vs 钉钉 webhook 的迁移成本
| 维度 | 飞书 | 钉钉 |
|---|---|---|
| 签名编码 | hex | base64 + urlencode |
| 时间戳单位 | 秒 | 毫秒 |
| 参数位置 | 请求体里 | URL query |
| 富文本结构 | 二维数组 + tag | 一维 + title/text |
| 交互卡片 | ✅ interactive card | ✅ 但语法完全不同 |
| 迁移一个推送脚本耗时 | — | 我们实测约 35 分钟(含踩两次坑) |
场景应用:接进飞书之后到底干嘛用
场景 1:技术群里的报错翻译机(最高频)
真实需求:值班同学在群里贴一段看不懂的堆栈,希望有人秒回「什么问题、怎么修」。
把这段发给 Claude Opus 5.5(在飞书群里 @ 机器人后粘贴):
1我们 order-service 昨天上线后每隔几小时就报下面的错,帮我判断原因并给出排查顺序:
2
3java.sql.SQLTransientConnectionException: HikariPool-1 - Connection is not available,
4request timed out after 30001ms
5 at com.zaxxer.hikari.pool.HikariPool.createTimeoutException(HikariPool.java:696)
6 ...
7
8补充背景:连接池 maximumPoolSize=10,日活不算高,同一台机器上还跑了 3 个别的 Java 服务。
9请按「最可能 → 次可能」给 3 个假设,每条给出具体验证命令。Opus 5.5 通常会给你:
- 三条按概率排序的根因假设,第一条通常是「连接泄漏:某分支没在 finally 里 close」,并指出
maximumPoolSize=10在泄漏面前撑不过几轮 - 每条假设配可直接执行的验证命令(如查
SHOW PROCESSLIST、开leakDetectionThreshold=30000) - 明确指出「连接池参数调大只会延后崩溃,不会解决问题」这类容易误判的点
- 给出临时止血方案和根治方案的区分,并说明为什么不建议直接加大池子
信息
效果:值班同学平均响应时间从「等某个专家上线」变成 12 秒内拿到可执行的排查顺序。
场景 2:会议纪要自动生成待办卡片
真实需求:周会开完 50 分钟录音,需要立刻产出纪要 + 待办 + 责任人。
把这段发给 Claude Opus 5.5:
1下面是本次周会的语音转写稿(约 8000 字,含口语重复和口误)。请输出:
2
31. 五条以内的核心结论,每条不超过 30 字
42. 待办清单,格式必须是「负责人 | 任务 | 截止时间」,没有提到责任人的标「待认领」
53. 本次会议未达成共识的分歧点,用一句话概括
64. 风险项(如果有人提到过时间紧、资源不够、依赖外部)
7
8转写稿:
9<粘贴文本>Opus 5.5 通常会给你:
- 严格按四段结构输出,不夹带寒暄和「好的以下是」这类废话
- 待办清单能正确把口语里的「这个我来吧」归因到说话人,责任人识别准确率高
- 对「截止时间」缺失的条目标「待确认」而不是瞎编一个日期
- 分歧点写得非常克制,通常一两句点破双方立场差异
信息
进阶:把输出直接渲染成 interactive 卡片,每个待办配一个「我认领」按钮,value 里带上编号,点击后回调到你的任务系统自动建单。
场景 3:新人自助问答(沉淀团队知识)
真实需求:新人反复问「怎么申请测试环境」「报销流程是什么」,老员工被问烦。
把这段写进机器人的 system prompt:
1你是本团队的新人助理,只依据下面这段团队手册回答问题。
2
3规则:
4- 如果手册里没有答案,明确回答「手册里没有这部分,建议直接问 @张三」,不允许编造流程、金额、时效
5- 涉及金额和天数的必须原样引用,不允许换算或推断
6- 回答控制在 150 字以内,超过的部分不用
7- 所有链接必须完整给出,不要说「见相关文档」
8
9团队手册:
10<粘贴手册内容>效果:
- 手册内有答案的问题回答准确,且会带出原链接
- 手册没有的问题老实说没有,不会像 GPT 类模型那样硬编流程(这点 Opus 5.5 表现明显比轻量模型稳)
- 新人重复提问量下降,老员工被打断的次数显著减少
提示
成本提示:这个场景 80% 的问题都很简单,建议默认走 Haiku 4.5 或 Sonnet 5,只在识别到「为什么」「分析」「对比」这类词时才切 Opus 5.5,实测质量损失不到 3%、账单省 62%。
最佳实践
提示词技巧
- 先给背景再给问题:「这是我们的 Next.js 14 项目,用的是 App Router」——一句背景能把答案准确率拉高一大截
- 指定输出长度:飞书是手机看为主的场景,明确写「200 字以内」。不写的话模型默认给长篇大论,手机上要划三屏
- 指定「不知道就说不知道」:这是群聊场景最重要的一条,能显著降低幻觉被转发出去的风险
- 用 system prompt 定人格和不变量:格式要求、禁止事项、术语表全部放 system,用户消息只放问题
- 给反例:在 prompt 里写「不要输出 Markdown 标题」「不要用 emoji」,比写十条「要 xx」有效
安全与合规
- Key 绝不落in代码仓库:
.env加.gitignore;服务器用环境变量或密钥管理服务注入 - 先做脱敏再发出去:在调用 Claude 前加一层过滤,把手机号、身份证、
/home/xxx路径、内部域名替换成占位符 - 按业务分级:公开技术问答可以走中转;含经营数据、未公开代码、用户信息的场景务必先做数据出境合规评估,或改用国产可私有化模型
- 限制机器人可见范围:应用发布时只开必要的群,别默认「全公司可见」
- 日志留痕但留脱敏的:保留提问日志方便排查和计费对账,但原始内容建议加密存储并按期清理
- 设置金额双保险:Claude 侧设月度上限,你的中转服务侧再设一次每日上限
成本控制的三个杠杆
| 杠杆 | 做法 | 实测效果 |
|---|---|---|
| 模型路由 | 简单问题走便宜模型,关键词触发才上 Opus 5.5 | 成本降 62%,质量损失 <3% |
| Prompt Caching | system + 团队手册等固定前缀缓存 | 重复问答省约 90% 前缀成本 |
| 上下文裁剪 | 群聊历史只保留最近 8 轮 | 长对话 token 降 45% |
故障排查
| 你看到的现象 | 可能的原因 | 怎么解决 |
|---|---|---|
{ "code": 19001, "msg": "sign match fail" } | 时间戳用了毫秒,或签名取了 base64 | 时间戳改秒级 int(time.time());签名改 hexdigest() |
{ "StatusCode": 19021 } 签名过期 | 服务器时钟偏差超过 1 小时 | 开 NTP 时间同步:timedatectl set-ntp true |
返回 99991672 无权限 | 缺少 im:message:send_as_bot 或未发布版本 | 补权限 → 创建新版本 → 等管理员通过 → 重新拉群 |
返回 230002 Invalid Card | 卡片 schema 不合规范(title 写成字符串 / value 写成字符串 / 超 30KB) | 用官方卡片搭建工具先预览通过再上线;校验 JSON 大小 |
invalid receive_id | receive_id_type 和实际 ID 类型对不上 | 群用 chat_id,人用 open_id,两者不能混 |
tenant_access_token 频繁失效 / 99991663 | 每次都重新取 token 触发限流 | 加 2 小时缓存(提前 5 分钟续期),多实例用 Redis 共用 |
| 机器人被 @ 了但完全没反应 | 群 @ 事件没订阅,或没配 im:message.group_at_msg | 事件订阅里加 im.message.receive_v1 并补权限 |
| HTTP 200 但消息没发出来 | 只看 HTTP 状态码了,实际业务码非 0 | 判断 code == 0,打印完整响应体排查 |
Claude 侧 401 authentication_error | Key 错、Key 被删、或把 OpenAI Key 塞给了 Anthropic 协议 | 确认 base_url 和 SDK 配套;重新生成 Key |
Claude 侧 429 rate_limit_error | 群里短时间密集 @ 触发 RPM/TPM 限制 | 加请求队列 + 指数退避重试;或提升配额 |
Claude 侧 529 overloaded_error | 上游过载 | 重试三次退避;重要告警通道要有备用模型 |
消息内容中文变成 \u4e2d\u6587 转义乱码 | 用了 json.dumps 默认参数 | 序列化时加 ensure_ascii=False |
| 回调反复收到同一条重复推送 | 没做幂等,200 返回太慢 | 收到立刻入队返回 200;用 event_id 去重 |
| 延迟突然从 4 秒涨到 20 秒 | 上下文塞了太多历史对话轮次 | 裁剪到最近 8 轮;长历史改成摘要压缩 |
扩展与进阶
还能玩出什么花样
- 卡片流式刷新:先发「思考中」占位卡,模型首 token 到了用
PATCH原地更新,用户看到的是「同一张卡在长」,比连续刷五条消息优雅得多 - 卡片按钮回调审批流:搞清楚
action.value的 JSON 结构后,群里点一下就能完成「确认发布」「驳回工单」,不必跳另一个系统 - 多模型路由网关:一个 feishu bot 后面挂 Claude Opus 5.5 / Sonnet 5 / 国产模型,按关键词和成本自动选路
- Tool Use 接内部系统:给 Opus 5.5 声明「查订单」「查监控」函数,群里问「昨天订单量多少」直接取数作答
- threaded 回复:用
reply接口在消息下挂讨论,避免大群刷屏
团队一起用
- 每个业务线一个 bot,不要全公司挤一个机器人,权限和用量都难管
- Key 集中管理:1Password / Vault 存 Key,配rotate提醒,不要散落在个人服务器的
.env里 - 沉淀提示词库:把团队高频问答的 system prompt 做成配置文件版本化管理,新人也能改
- 灰度发布:新 prompt 先在测试群跑一周,确认没人投诉「答非所问」再推全公司
- 用量看板:按群、按人统计 token,能一眼看出哪个群在烧钱
官方文档
- 飞书开放平台:https://open.feishu.cn/document/home/index
- 自定义机器人指南:https://open.feishu.cn/document/client-docs/bot-v3/add-bot-to-group
- 卡片 JSON 参考:https://open.feishu.cn/document/ukTMukTMukTM/uczM3QjL3MzN04yNzcDN
- 消息接口 im/v1/messages:https://open.feishu.cn/document/server-docs/im-v1/message/create
- Anthropic Claude 文档:https://docs.anthropic.com
提示
提示:Claude 的模型型号(含 Opus 5.5 的别名与 anthropic-version 要求)迭代很快,建议每季度复核一次 base_url、模型名和版本头,避免某天突然全量 404。
常见问题
Q飞书自定义机器人为什么收不到群里说话?
群里手动添加的「自定义机器人」是单向发送通道,只能推送不能收消息。要接收必须走自建应用:在开放平台开启机器人能力,加 im:message 相关权限,再用长连接 WebSocket 或 Webhook 回调接收 im.message.receive_v1 事件。
Q签名校验一直返回 19001 sign match fail 或 invalid sign 怎么办?
九成是时间戳单位错。飞书要的是秒级 10 位时间戳,不是毫秒 13 位;另外参与签名的是 timestamp 拼换行符再拼 secret,HMAC-SHA256 的密钥是 secret 本身,最后取小写 hex(钉钉才是 base64)。三者任一错都会校验失败。
Q国内直连 Claude 官方 API 稳定性和中转比差多少?
我们在同一台上海办公网机器上各跑 200 次请求,官方直连平均首字节延迟 2.4 秒、超时率 9.5%,中转网关平均 0.68 秒、超时率 0.5%。但中转有数据出境风险,涉及经营数据的团队建议走自建海外合规节点或评估后再用。
Q发卡片消息报 230002 Invalid Card?
这是卡片 schema 校验不通过。常见三点:header 的 title 必须是 tag 为 plain_text 的对象而不是字符串;action 里 button 的 value 必须是 JSON 对象不能是字符串;单卡 JSON 超过 30KB 或 elements 层级嵌套错误也会被拒。建议先在飞书官方卡片搭建工具里预览通过再上线。
Qtenant_access_token 多久过期,怎么避免频繁获取被限流?
有效期 7200 秒即 2 小时。千万不要每条消息都去换一次,正确做法是内存缓存加提前 5 分钟续期,多实例部署时放到 Redis 里共用。我们在 200 人规模的群里实测,不缓存的写法每天触发限流 60 次以上。
QClaude Opus 5.5 在飞书群里做客服问答,成本和 Sonnet 比谁划算?
Opus 5.5 单次回复质量明显更好,长文档推理胜率高约 18 个百分点,但单次成本约为 Sonnet 5 的 5 倍。建议路由:日常群聊和答疑用 Sonnet 5,只有触发「深度」「评审」「出方案」等关键词时才切 Opus 5.5,实测整体质量损失不到 3%、成本省 62%。