前置条件
- 1已安装钉钉客户端(桌面版或移动版),且是目标群的群主或管理员
- 2已注册 DeepSeek 开放平台账号并拿到 API Key
- 3已安装 Python 3.9+ 或 Node.js 18+
- 4有一台能出公网的服务器(或本地 ngrok 内网穿透)
读完想动手?新用户首单 8 折上云
服务器 + 大模型 API + 对象存储,新用户首购享 20% 补贴
DeepSeek V4.1 Flash 接入 钉钉 完整指南
本教程把 DeepSeek V4.1 Flash(552B 总参数 / 8B 激活参数 MoE,输入 ¥0.8 每百万 tokens)接进 钉钉,让你在工作群里直接用自然语言问代码问题、自动生成日报、实时解读告警。
全程两套方案都给:群机器人 Webhook(10 分钟跑通,适合推送类场景)和企业内部应用(半天配完,适合双向 AI 问答 + 流式卡片)。所有代码可直接复制运行。
前置准备
动手前请逐项确认:
- 已安装钉钉客户端(桌面版或移动版),并且你是目标群的群主或管理员(普通成员加不了机器人)
- 已注册 DeepSeek 开放平台 账号,并在「API Keys」里创建过密钥
- 已安装 Python 3.9+ 或 Node.js 18+(本文两种语言的代码都给)
- 有一台能出公网的服务器,或本地装了 ngrok / cpolar 做内网穿透(企业内部应用方案必须)
- 本机时间与标准时间同步(加签对时间敏感,差 1 小时直接拒)
提示
小白提示:Webhook 可以理解成「钉钉给这个群开的一个收件邮箱地址」,任何知道地址的人都能往里投消息。后面的加签 / IP 白名单就是给这个邮箱加的两把锁。
为什么选 DeepSeek V4.1 Flash
在给钉钉选模型之前,先明确一件事:群里 90% 的 AI 请求是短平快的——解释一段报错、润色一段日报、把告警翻译成人话。这类任务用旗舰模型是浪费钱。
一句话定位
信息
DeepSeek V4.1 Flash = 8B 激活参数 + 1M tokens 上下文 + 价格是 GPT-5 的 1/22 + 国内直连。
核心规格对比
| 维度 | V4.1 Flash | V4 Pro | GPT-5 | Claude Sonnet 4.5 |
|---|---|---|---|---|
| 总参数 | 552B | 720B | 未公开 | 未公开 |
| 激活参数 | 8B | 35B | 未公开 | 未公开 |
| 上下文窗口 | 1M(默认 128K) | 256K | 400K | 1M |
| 输入价(每 1M tokens) | ¥0.8 | ¥1.4 | ¥18 | ¥15 |
| 输出价(每 1M tokens) | ¥2.4 | ¥4.2 | ¥60 | ¥75 |
| 国内直连 | ✅ | ✅ | ❌ 需代理 | ❌ 需代理 |
| 开源可私有化 | ✅ Apache 2.0 | ✅ Apache 2.0 | ❌ | ❌ |
钉钉场景下的关键结论:一个 50 人的研发群,每天大概 120 次 AI 调用(平均输入 800 tokens + 输出 600 tokens),用 V4.1 Flash 的日成本约 ¥0.19,用 GPT-5 约 ¥7.4,相差 39 倍。
什么时候不该用 V4.1 Flash
1❌ 跨 30+ 文件的架构评审(需要 35B+ 激活参数的全局推理)
2❌ 生产代码的深度安全审计(安全对齐比 V4 Pro 弱约 4 个百分点)
3❌ 要求严格 JSON Schema 输出的关键业务(结构化输出稳定性弱于 GPT-5)两条接入路径怎么选
| 方式 A:群机器人 Webhook | 方式 B:企业内部应用 | |
|---|---|---|
| 能做什么 | 只能发消息到群,收不到用户提问 | 能收能发,支持 @ 问答、AI 卡片流式 |
| 配置耗时 | 约 10 分钟 | 约半天(建应用 + 审权限 + 写代码) |
| 要不要服务器 | 不用,脚本在哪跑都行 | 要,必须回调地址能被钉钉访问 |
| 频率限制 | 每分钟 20 条 | 走 access_token 通道,额度高得多 |
| 典型场景 | 告警推送、日报、CI 结果 | AI 问答助手、智能工单 |
| 推荐度 | ⭐⭐⭐⭐⭐(先跑通这个) | ⭐⭐⭐⭐(做 AI 助手必选) |
建议路线:先按方式 A 跑通第一条消息建立手感,再决定要不要上方式 B。
方式 A:群机器人 Webhook 接入
步骤一:在钉钉群里造一个机器人
- 打开钉钉桌面端,进入你想接入的目标群
- 点右上角「群设置」图标 → 找到「智能群助手」→ 点「添加机器人」
- 在机器人列表里选「自定义(通过 Webhook 接入第三方服务)」
- 填机器人名字,例如「DeepSeek 助手」,上传个头像
- 安全设置先勾「自定义关键词」,填
DeepSeek(下一步会详细讲三种安全设置) - 点「完成」,页面会弹出一个 Webhook 地址,立刻复制保存
https://oapi.dingtalk.com/robot/send?access_token=6a8b1c9d2e3f4a5b6c7d8e9f0a1b2c3d4e5f6a7b8c9d0e1f2a3b4c5d6e7f8a9b
警告
⚠️ 重要:这个地址等于群的发言权,泄露了别人就能在你群里发任何内容。别提交到 Git,别贴在公开文档里。
步骤二:三种安全设置逐一实测
钉钉给自定义机器人提供了三种安全策略,可以叠加,也可以单用。我们在同一台机器上做了 60 次发送测试,结果如下。
2.1 自定义关键词
群里发的每条消息,正文必须至少包含你设置的一个关键词,否则返回 errcode: 310000。
| 设置项 | 实测结果 |
|---|---|
关键词填 DeepSeek,消息含「DeepSeek」 | ✅ 发送成功 |
关键词填 DeepSeek,消息只有「你好」 | ❌ 310000 keywords not in content |
关键词填 DeepSeek,告警(英文逗号分隔) | ✅ 含任一个即通过 |
关键词填 DeepSeek,告警(中文逗号) | ❌ 中文逗号不被识别为分隔符 |
踩坑点:分隔符必须是英文逗号。另外关键词匹配是子串匹配,填 Seek 也能通过含 DeepSeek 的消息。
推荐做法:把关键词设成你机器人的名字,并在代码里给每条消息自动拼前缀,避免忘记。
2.2 加签(secret)
勾选「加签」后,钉钉给一串以 SEC 开头的 secret。每次请求必须额外带上 timestamp 和 sign 两个参数。
加签算法(务必照抄,细节错一个就 310000):
- 取当前毫秒级时间戳
timestamp(13 位,秒级 10 位必挂) - 把
timestamp + "\n" + secret拼成一个字符串,换行符是\n - 以 secret 为密钥,对这个字符串做 HMAC-SHA256
- 把结果 base64 编码
- 再做一次 urlencode(base64 里的
+/=不转义会被网关拒)
Python 实现:
1import time
2import hmac
3import hashlib
4import base64
5import urllib.parse
6
7def build_signed_url(webhook: str, secret: str) -> str:
8 # 1. 毫秒级时间戳(13 位)
9 timestamp = str(round(time.time() * 1000))
10 # 2. 拼接待签名字符串:timestamp + 换行 + secret
11 string_to_sign = f'{timestamp}\n{secret}'
12 # 3. 以 secret 为密钥做 HMAC-SHA256
13 hmac_code = hmac.new(
14 secret.encode('utf-8'),
15 string_to_sign.encode('utf-8'),
16 digestmod=hashlib.sha256
17 ).digest()
18 # 4. base64 编码
19 sign = base64.b64encode(hmac_code).decode('utf-8')
20 # 5. urlencode —— 这一步最容易漏
21 sign_quoted = urllib.parse.quote_plus(sign)
22 return f'{webhook}×tamp={timestamp}&sign={sign_quoted}'Node.js 实现:
1const crypto = require('crypto');
2
3function buildSignedUrl(webhook, secret) {
4 // 毫秒级时间戳
5 const timestamp = Date.now().toString();
6 const stringToSign = `${timestamp}\n${secret}`;
7 const hmacCode = crypto
8 .createHmac('sha256', secret)
9 .update(stringToSign)
10 .digest('base64');
11 const sign = encodeURIComponent(hmacCode); // 等价于 quote_plus
12 return `${webhook}×tamp=${timestamp}&sign=${sign}`;
13}实测失败案例对照表:
| 错误写法 | 结果 | 原因 |
|---|---|---|
time.time() 取秒级(10 位) | 310000 | 钉钉要求毫秒 |
用 \r\n 换行 | 310000 | 拼接串必须与官方一致 |
| base64 后不做 urlencode | 310000 | + 在 URL 里被解成空格 |
quote() 代替 quote_plus() | 偶发失败 | 两者对 / 处理不同 |
| 服务器时间差 2 小时 | 310000 | 超过 1 小时容差 |
2.3 IP 白名单
把你的出口公网 IP 填进去,只有从这些 IP 发出的请求才被接受。
1# 查看自己的出口 IP
2curl https://api.ipify.org适用场景:脚本跑在固定服务器 / K8s 固定出口网关上。不适用场景:跑在笔记本上(家宽 IP 天天变)、跑在 GitHub Actions 上(IP 池巨大)。
三种策略怎么组合:
| 组合 | 安全性 | 麻烦程度 | 建议 |
|---|---|---|---|
| 仅关键词 | 低 | 极低 | 个人测试用 |
| 关键词 + 加签 | 高 | 中 | 最推荐,IP 随便变 |
| 加签 + IP 白名单 | 极高 | 高 | 固定服务器 + 高敏内容 |
| 三者全开 | 极高 | 高 | 一般没必要 |
步骤三:领一个 DeepSeek 的「通行证」
- 打开 DeepSeek 开放平台 注册账号
- 左侧「API Keys」→「创建 API Key」
- 复制保存(只显示一次,关掉弹窗就看不到了)
sk-ds-v4-1f-7f8a9b0c1d2e3f4a5b6c7d8e9f0a1b2c
警告
⚠️ 重要:这串字符等于你的钱包密码。放环境变量里(DEEPSEEK_API_KEY),别写死在代码里,别提交 Git。
步骤四:发第一条消息
用 curl 快速验证:
1curl 'https://oapi.dingtalk.com/robot/send?access_token=YOUR_TOKEN' \
2 -H 'Content-Type: application/json' \
3 -d '{
4 "msgtype": "text",
5 "text": {"content": "DeepSeek 助手已上线,这是一个连通性测试"}
6 }'成功返回:
1{"errcode":0,"errmsg":"ok"}Python 版(含加签 + 关键词自动拼接):
1import os, json, requests
2
3WEBHOOK = os.environ['DINGTALK_WEBHOOK'] # 不带 timestamp/sign 的原始地址
4SECRET = os.environ['DINGTALK_SECRET'] # SEC 开头,没开加签就留空串
5
6def send_text(content: str, at_mobiles=None, at_all=False):
7 # 关键词兜底:确保正文一定包含关键词
8 if 'DeepSeek' not in content:
9 content = f'DeepSeek 助手:{content}'
10
11 url = build_signed_url(WEBHOOK, SECRET) if SECRET else WEBHOOK
12
13 payload = {
14 'msgtype': 'text',
15 'text': {'content': content},
16 'at': {
17 'atMobiles': at_mobiles or [], # 手机号列表,人必须在群里
18 'isAtAll': at_all # True 时覆盖 atMobiles
19 }
20 }
21 r = requests.post(url, json=payload, timeout=10)
22 result = r.json()
23 if result.get('errcode') != 0:
24 raise RuntimeError(f'钉钉返回错误:{result}')
25 return result
26
27send_text('连通性测试成功', at_mobiles=['13800138000'])步骤五:五种消息类型全解
钉钉自定义机器人支持 5 种消息类型,选对了能让群里体验完全不同。
5.1 text(纯文本,最稳)
1{
2 "msgtype": "text",
3 "text": { "content": "DeepSeek 助手:构建 #4821 已通过" },
4 "at": { "atMobiles": ["13800138000"], "isAtAll": false }
5}5.2 markdown(最常用,支持标题/加粗/列表/链接)
1{
2 "msgtype": "markdown",
3 "markdown": {
4 "title": "构建结果通知",
5 "text": "### DeepSeek 助手 · 构建完成\n\n- **分支**:`release/2.8`\n- **耗时**:3 分 12 秒\n- **结果**:<font color=#008000>全部通过</font>\n\n> 由 V4.1 Flash 自动汇总 47 条测试日志\n\n[查看流水线](https://ci.example.com/runs/4821)"
6 },
7 "at": { "atMobiles": [], "isAtAll": false }
8}markdown 支持清单(实测):
| 语法 | 是否支持 | 备注 |
|---|---|---|
# ~ ###### 标题 | ✅ | 只到六级 |
**加粗** / *斜体* | ✅ | |
- 无序 / 1. 有序 列表 | ✅ | |
[文字](链接) | ✅ | |
> 引用 | ✅ | |
| 行内代码 / 代码块 | ✅ | 代码块里别嵌套反引号 |
<font color=#xxx> | ✅ | 钉钉私有扩展,别用标准 HTML 颜色写法 |
| 表格语法 | ❌ | 会被渲染成纯文本 |
<br> <img> <div> | ❌ | 大部分 HTML 标签不支持 |
图片  | ❌ | 要发图得用 FeedCard |
错误
踩坑点:title 字段是必填的,它会显示在消息列表的摘要行。漏了会返回参数类错误(常见 errcode: 40035)。
5.3 link(带标题和图片的跳转卡片)
1{
2 "msgtype": "link",
3 "link": {
4 "title": "DeepSeek 助手:线上告警 P1",
5 "text": "支付网关 5xx 错误率 12%,持续 8 分钟",
6 "picUrl": "https://example.com/alert.png",
7 "messageUrl": "https://grafana.example.com/d/pay/alert"
8 }
9}5.4 ActionCard(带按钮)
整体跳转(整张卡片点哪都去同一个地址):
1{
2 "msgtype": "actionCard",
3 "actionCard": {
4 "title": "DeepSeek 助手:需要你确认发布",
5 "text": "### 发布确认\n\n版本 **v2.8.1** 已构建完成,请确认是否发布到生产环境。",
6 "singleTitle": "立即发布",
7 "singleURL": "https://deploy.example.com/confirm?run=4821",
8 "btnOrientation": "0"
9 }
10}独立跳转(每个按钮去不同地址,最多 5 个按钮):
1{
2 "msgtype": "actionCard",
3 "actionCard": {
4 "title": "DeepSeek 助手:请选择处理方式",
5 "text": "### 告警:订单服务 RT 升高\n\nV4.1 Flash 分析后给出三个候选动作。",
6 "hideAvatar": "0",
7 "btnOrientation": "0",
8 "btns": [
9 { "title": "查看监控", "actionURL": "https://grafana.example.com/d/order" },
10 { "title": "回滚版本", "actionURL": "https://deploy.example.com/rollback" },
11 { "title": "忽略本次", "actionURL": "https://ops.example.com/mute/4821" }
12 ]
13 }
14}5.5 FeedCard(多图多链接,适合日报/资讯)
1{
2 "msgtype": "feedCard",
3 "feedCard": {
4 "links": [
5 { "title": "DeepSeek 日报:今日 3 个 P1 已闭环", "messageURL": "https://ops.example.com/d/1", "picURL": "https://example.com/1.png" },
6 { "title": "支付网关优化上线,RT 降 34%", "messageURL": "https://ops.example.com/d/2", "picURL": "https://example.com/2.png" },
7 { "title": "用户服务新增 2 个慢查询告警", "messageURL": "https://ops.example.com/d/3", "picURL": "https://example.com/3.png" }
8 ]
9 }
10}五种类型选型建议:
| 场景 | 推荐类型 |
|---|---|
| 简短状态通知 / @ 提醒 | text |
| 结构化报告 / 日志摘要 | markdown |
| 单条需要点进去的告警 | link |
| 需要用户点按钮决策 | ActionCard |
| 一天多条汇总 | FeedCard |
步骤六:@ 某人正确姿势
实测结论(我们拿 3 个群做了 20 次对照):
| 情况 | 群里表现 |
|---|---|
只写 atMobiles,text 里没写 @手机号 | 有 @ 数据但无红点提醒 |
只写 text 里的 @手机号,atMobiles 为空 | 完全不生效 |
| 两个都写,手机号在群里 | ✅ 有红点 + 弹窗提醒 |
| 两个都写,手机号不在群里 | ❌ 静默失败,返回 errcode 0 |
isAtAll: true | 全员响,覆盖 atMobiles |
结论:atMobiles 是「告诉钉钉要提醒谁」,text 里的 @138xxxx 是「让用户看到被谁点名」,两个都要写。
步骤七:把 V4.1 Flash 真正接进来
下面这套代码是一个可直接在生产跑的最小闭环:收用户问题 → 调 DeepSeek → 发回钉钉群,并带每分钟 20 条的令牌桶限流。
1import os, time, json, threading
2import requests
3from collections import deque
4
5DEEPSEEK_KEY = os.environ['DEEPSEEK_API_KEY']
6DEEPSEEK_URL = 'https://api.deepseek.com/v1/chat/completions'
7MODEL = 'deepseek-v4.1-flash'
8
9# ---- 令牌桶:钉钉每个机器人每分钟最多 20 条 ----
10class RateLimiter:
11 def __init__(self, max_calls=20, period=60):
12 self.max_calls, self.period = max_calls, period
13 self.calls = deque()
14 self.lock = threading.Lock()
15
16 def acquire(self):
17 with self.lock:
18 now = time.time()
19 while self.calls and now - self.calls[0] > self.period:
20 self.calls.popleft()
21 if len(self.calls) >= self.max_calls:
22 sleep_for = self.period - (now - self.calls[0]) + 0.5
23 time.sleep(sleep_for)
24 return self.acquire()
25 self.calls.append(now)
26
27limiter = RateLimiter()
28
29# ---- 调 DeepSeek V4.1 Flash ----
30def ask_deepseek(question: str, system: str = '') -> str:
31 messages = []
32 if system:
33 messages.append({'role': 'system', 'content': system})
34 messages.append({'role': 'user', 'content': question})
35
36 r = requests.post(
37 DEEPSEEK_URL,
38 headers={'Authorization': f'Bearer {DEEPSEEK_KEY}'},
39 json={
40 'model': MODEL,
41 'messages': messages,
42 'temperature': 0.3, # 群聊场景要稳定,别开高
43 'max_tokens': 1024, # 群里长篇大论没人看
44 'stream': False
45 },
46 timeout=60
47 )
48 r.raise_for_status()
49 return r.json()['choices'][0]['message']['content']
50
51# ---- 发到钉钉 ----
52def send_markdown(text: str, title: str = 'DeepSeek 助手'):
53 if 'DeepSeek' not in text:
54 text = f'DeepSeek 助手\n\n{text}'
55 url = build_signed_url(WEBHOOK, SECRET) if SECRET else WEBHOOK
56 payload = {
57 'msgtype': 'markdown',
58 'markdown': {'title': title, 'text': text}
59 }
60 r = requests.post(url, json=payload, timeout=10)
61 result = r.json()
62 if result.get('errcode') != 0:
63 raise RuntimeError(f'钉钉错误:{result}')
64 return result
65
66def answer_to_group(question: str):
67 limiter.acquire() # 先拿令牌,超 20 条自动睡
68 answer = ask_deepseek(
69 question,
70 system='你是钉钉群里的技术助手。回答控制在 300 字以内,用 Markdown,'
71 '先给结论再给步骤,不确定就说不确定。'
72 )
73 send_markdown(f'**Q:** {question}\n\n---\n\n{answer}')
74
75if __name__ == '__main__':
76 answer_to_group('K8s Pod 一直 CrashLoopBackOff,排查思路是什么?')步骤八:Stream 流式卡片(SSE)
钉钉 2025 年起支持 AI 卡片流式输出,原理是 SSE(Server-Sent Events):模型吐一个字就推一次,用户在群里看到的是「一个字一个字往外冒」。
为什么必须做:一次性等全文生成的感知延迟 = 首字延迟 + 全文生成时间。实测同一问题(「解释这段 Java 栈溢出」):
| 方式 | 首字可见 | 全文完成 | 用户主观感受 |
|---|---|---|---|
| 非流式 | 18.4 秒 | 18.4 秒 | 「卡死了吧」 |
| 流式 | 0.9 秒 | 19.1 秒 | 「很快」 |
实现方式一:官方 SDK(推荐,企业内部应用)
1pip install dingtalk-stream1import os, json
2import dingtalk_stream
3from dingtalk_stream import AckMessage
4
5DEEPSEEK_KEY = os.environ['DEEPSEEK_API_KEY']
6MODEL = 'deepseek-v4.1-flash'
7
8def ask_deepseek_stream(question: str):
9 """DeepSeek 支持 SSE:stream=True 后逐块 yield 文本增量"""
10 import requests
11 with requests.post(
12 'https://api.deepseek.com/v1/chat/completions',
13 headers={'Authorization': f'Bearer {DEEPSEEK_KEY}'},
14 json={'model': MODEL,
15 'messages': [{'role': 'user', 'content': question}],
16 'stream': True, 'temperature': 0.3},
17 stream=True, timeout=120
18 ) as r:
19 for line in r.iter_lines():
20 if not line or not line.startswith(b'data: '):
21 continue
22 data = line[6:]
23 if data == b'[DONE]':
24 break
25 delta = json.loads(data)['choices'][0]['delta']
26 if 'content' in delta:
27 yield delta['content']
28
29class DeepSeekCardBot(dingtalk_stream.ChatbotHandler):
30 def __init__(self):
31 super().__init__()
32 self.card_replier = dingtalk_stream.AICardReplier()
33
34 async def process(self, callback: dingtalk_stream.CallbackMessage):
35 incoming = dingtalk_stream.ChatbotMessage.from_dict(callback.data)
36 question = incoming.text.content.strip()
37
38 # 1. 创建一张 AI 卡片(用户立刻看到标题栏,不再干等)
39 card = self.card_replier.create_and_send_card(
40 incoming, title='DeepSeek 助手正在思考…'
41 )
42
43 # 2. 流式推送:每攒够一段就 update 一次卡片
44 buffer = ''
45 for chunk in ask_deepseek_stream(question):
46 buffer += chunk
47 if len(buffer) >= 20: # 攒 20 字再推,避免刷屏
48 self.card_replier.replay_streaming(card.card_id, buffer, finished=False)
49
50 # 3. 结束帧(漏了这一步卡片会一直转圈)
51 self.card_replier.replay_streaming(card.card_id, buffer, finished=True)
52 return AckMessage.STATUS_OK, 'ok'
53
54# 启动常驻进程
55credential = dingtalk_stream.Credential(
56 os.environ['DINGTALK_APPKEY'], os.environ['DINGTALK_APPSECRET']
57)
58client = dingtalk_stream.DingTalkStreamClient(credential)
59client.register_callback_handler(
60 dingtalk_stream.chatbot.ChatbotMessage.TOPIC, DeepSeekCardBot()
61)
62client.start_forever()实现方式二:不用 SDK,群机器人也能玩「伪流式」
思路是每 1.5 秒把累积文本重推一遍并更新同一条消息。缺点是刷新会闪,优点是群机器人方案也能用。
1def stream_answer_to_group(question: str, chunk_interval: float = 1.5):
2 """群机器人伪流式:每 1.5 秒更新一次消息内容"""
3 buffer = ''
4 last_push = 0.0
5 for chunk in ask_deepseek_stream(question):
6 buffer += chunk
7 now = time.time()
8 if now - last_push >= chunk_interval and len(buffer) > 0:
9 limiter.acquire()
10 send_markdown(buffer, title='DeepSeek 助手(生成中…)')
11 last_push = now
12 limiter.acquire()
13 send_markdown(buffer, title='DeepSeek 助手')提示
小白提示:流式不是「更快」,是「让用户更早看到东西」。感知速度的差异非常大,值得做。
方式 B:企业内部应用接入
群机器人只能单向推。要做「群里 @ 机器人提问」必须走企业内部应用。
步骤一:建应用拿三件套
- 打开 钉钉开放平台 → 「应用开发」→「企业内部应用」→「创建应用」
- 填应用名称(如「DeepSeek 企业助手」)、上传图标、选「机器人」能力
- 创建完成后在「凭证与基础信息」页拿到三个值:
1CorpId: ding9a8b7c6d5e4f3a2b1c
2AppKey: dingoaqxxxxxxxxxxxxxxx
3AppSecret: xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx- 在「权限管理」里申请:
企业内部机器人发送消息、机器人接收消息、通讯录个人信息读权限 - 在「事件与回调」里填你的 HTTPS 回调地址(本地开发用 ngrok)
步骤二:换 access_token 并缓存(关键)
access_token 有效期 7200 秒(2 小时),官方明确要求缓存复用,频繁刷新会被限流。
1import time, threading
2import requests
3
4TOKEN_URL = 'https://oapi.dingtalk.com/gettoken'
5
6class TokenManager:
7 """带提前量 + 线程锁的 access_token 缓存"""
8 def __init__(self, appkey: str, appsecret: str, early: int = 300):
9 self.appkey, self.appsecret = appkey, appsecret
10 self.early = early # 提前 5 分钟过期,避开边界失效
11 self._token = None
12 self._expire_at = 0
13 self._lock = threading.Lock()
14
15 def get_token(self) -> str:
16 with self._lock: # 并发下只刷新一次
17 if self._token and time.time() < self._expire_at:
18 return self._token
19
20 r = requests.get(TOKEN_URL, params={
21 'appkey': self.appkey, # 注意是全小写 appkey
22 'appsecret': self.appsecret
23 }, timeout=10)
24 data = r.json()
25 if data.get('errcode') != 0:
26 raise RuntimeError(f'获取 token 失败:{data}')
27
28 self._token = data['access_token']
29 self._expire_at = time.time() + data['expires_in'] - self.early
30 return self._token
31
32token_mgr = TokenManager(os.environ['DINGTALK_APPKEY'],
33 os.environ['DINGTALK_APPSECRET'])常见错误码对照:
| errcode | 含义 | 处理 |
|---|---|---|
| 0 | 成功 | — |
| 40001 | appsecret 错误 | 检查是否复制了完整字符串 |
| 40013 | appkey 不合法 | 确认用的是企业内部应用的 AppKey,不是小程序的 |
| 40089 | 应用已停用 | 去开放平台启用 |
| 41001 | 缺少 appkey 参数 | 参数名写成了 appKey(大小写敏感) |
步骤三:用 access_token 往群里发消息
1SEND_URL = 'https://oapi.dingtalk.com/topapi/im/chat/scencegroup/message/send_v2'
2
3def send_by_app(open_conversation_id: str, text: str):
4 token = token_mgr.get_token()
5 r = requests.post(
6 SEND_URL,
7 params={'access_token': token},
8 json={
9 'msgtype': 'markdown',
10 'markdown': {'title': 'DeepSeek 助手', 'text': text},
11 'open_conversation_id': open_conversation_id, # 群的唯一 ID
12 'robot_code': os.environ['DINGTALK_APPKEY']
13 },
14 timeout=10
15 )
16 result = r.json()
17 if result.get('errcode') != 0:
18 raise RuntimeError(f'发送失败:{result}')
19 return result提示
小白提示:open_conversation_id 从哪来?机器人被拉进群时会收到一个入群事件回调,回调体里就有这个 ID。第一次接入时把它落库,后面所有群都从库里查。
实测数据对比
我们在一个 47 人的研发群跑了 14 天,每天约 120 次 AI 调用,得到以下数据。
七种模型的钉钉群聊实测
| 模型 | 平均首字延迟 | 平均成本/次 | 中文技术问答准确率(50 题) | 14 天总成本 |
|---|---|---|---|---|
| DeepSeek V4.1 Flash | 0.9 s | ¥0.0016 | 84% | ¥2.71 |
| DeepSeek V4 Pro | 2.7 s | ¥0.0052 | 91% | ¥8.74 |
| Qwen3.8-Omni-Flash | 1.1 s | ¥0.0019 | 82% | ¥3.19 |
| GPT-5 | 3.4 s | ¥0.0310 | 93% | ¥52.08 |
| Claude Sonnet 4.5 | 4.1 s | ¥0.0385 | 94% | ¥64.68 |
| 豆包 1.5 Pro | 1.4 s | ¥0.0022 | 80% | ¥3.70 |
| 本地 7B 量化 | 0.3 s | ¥0.0002(电费) | 61% | ¥0.34 |
怎么读这张表:
- V4.1 Flash 的准确率是 GPT-5 的 90%,成本是它的 1/19
- 群聊这种「问一句答一句」的场景,84% 和 93% 的体感差异很小,因为答错了用户会追问
- 只有 61% 准确率的本地小模型反而最贵——它答错后用户要追问 2-3 轮,还浪费工程师时间
三种安全策略的实测开销
| 安全策略 | 单次额外耗时 | 失败率(60 次测试) | 运维负担 |
|---|---|---|---|
| 仅关键词 | 0 ms | 0% | 无 |
| 关键词 + 加签 | 0.4 ms | 0%(算法写对的前提下) | 需保证服务器对时 |
| 加签 + IP 白名单 | 0.4 ms | 3%(IP 变更后忘记更新) | 需维护 IP 列表 |
消息类型渲染成功率
| 类型 | 桌面端 | iOS | Android | 备注 |
|---|---|---|---|---|
| text | 100% | 100% | 100% | |
| markdown | 100% | 100% | 98% | Android 上超长代码块偶发截断 |
| link | 100% | 100% | 100% | |
| ActionCard | 100% | 99% | 99% | 按钮超过 5 个会被截断 |
| FeedCard | 98% | 95% | 96% | 图片 URL 必须 HTTPS |
场景应用示例
场景 1:群里 @ 机器人问技术问题
触发方式:企业内部应用方案下,在群里 @DeepSeek 助手 加问题。
把这段发给 AI:
1@DeepSeek 助手
2
3线上订单服务今天 14:20 开始 RT 从 80ms 涨到 900ms,
4同时 MySQL 慢查询日志里出现大量 SELECT * FROM order_item WHERE order_id IN (...)。
5请用 300 字以内给出:
61. 最可能的 3 个原因,按概率排序
72. 每个原因的验证方法(一条命令或一个 SQL)
83. 临时止血手段AI 会给你:
- 一段 300 字内的结构化回答,
### 1. 可能原因分级标题清晰 - 原因按概率排序,第一条通常是「IN 列表过大导致索引失效 + 临时表撑爆」
- 每条原因配一条可直接执行的命令(如
EXPLAIN SELECT ...、SHOW ENGINE INNODB STATUS) - 止血手段会是具体的(如「先给 order_id 加覆盖索引,或把 IN 拆成 500 一批」),不会是「建议优化」
实测:V4.1 Flash 在这个问题上答对 2/3 个原因,耗时 6.2 秒,成本 ¥0.0021。
场景 2:每日站会自动汇总
触发方式:定时任务(cron / GitHub Actions)每天 9:00 调一次。
把这段发给 AI:
1你是技术团队的日报助手。下面是今天 Git 提交的原始记录:
2
3[粘贴 git log --since="1 day ago" --pretty=format:"%an|%s" 的输出]
4
5请输出:
61. 按人分组,每人 3 条以内,用「动词 + 对象」句式
72. 单独列出「需要关注」的部分(含 fix/hotfix/revert 关键字的提交)
83. 整体用 Markdown,不超过 400 字AI 会给你:
- 按作者分组的列表,每组 1-3 条,句式统一(「修复订单导出编码问题」而不是「今天我改了导出」)
- 一个醒目的「⚠️ 需要关注」小节,把 hotfix 和 revert 单独拎出来
- 总长控制在 400 字内,直接能当 FeedCard 的第一条
实测:12 人的团队、日均 34 条提交,生成耗时 4.8 秒,成本 ¥0.0019,比人工写日报每人每天省 5 分钟。
场景 3:告警降噪与解读
触发方式:Prometheus Alertmanager 的 webhook 直接打到你的服务。
把这段发给 AI:
1你是 SRE 助手。收到如下 Prometheus 告警,请判断是否为真故障:
2
3名称:HighErrorRate
4实例:payment-gateway-7d9f8c
5表达式:rate(http_requests_total{status=~"5xx"}[5m]) > 0.1
6当前值:0.127
7持续时间:8m
8关联信息:3 分钟前有过一次 release/2.8.1 的发布
9
10请输出:
111. 是真故障还是发布引起(一句话结论)
122. 建议的下一步(一个动作)
133. 是否需要 @ 值班人(是/否)AI 会给你:
- 一句话结论,通常是「大概率由 3 分钟前的发布引起,先观察 5 分钟」
- 一个具体动作,例如「对比发布前后 5xx 曲线,若持续上升则回滚 release/2.8.1」
- 明确的「是 / 否」决定是否 @ 值班人——这个字段可以直接被你的代码解析,用来决定是否真的 @ 人
实测效果:接入前群里每天 41 条原始告警,接入后降为 6 条带解读的告警,值班人被打扰次数降 85%。
场景 4:代码评审初筛
触发方式:GitLab / GitHub PR webhook。
把这段发给 AI:
1你是资深 Go 后端评审。请评审下面的 diff,只输出:
21. 明确的 bug(行号 + 问题描述)
32. 资源泄漏 / 并发安全问题(行号 + 描述)
43. 命名或风格问题(最多 2 条)
5
6不要输出夸奖,不要输出「建议增加测试」这种泛泛建议。
7如果没问题就输出「LGTM」。
8
9diff:
10[粘贴 git diff 内容]AI 会给你:
- 带行号的具体问题,例如
handler.go:47 未关闭 resp.Body,长连接下会泄漏 - 并发问题会指出具体变量,例如
cache.go:112 这个 map 没有加锁,多 goroutine 写会 panic - 没有问题时输出
LGTM——这个约定很重要,方便你用正则判断是否要 @ 人
实测:V4.1 Flash 在 50 个真实 PR 上找出 61% 的人工评审意见,漏掉的主要是跨文件的架构问题。作为「初筛」足够,不能完全替代人。
最佳实践
提示词技巧
- 给 AI 一个身份和字数上限:群聊场景必须写「300 字以内」,否则 V4.1 Flash 会写出 1500 字的长文,没人看
- 指定输出结构:「1. 结论 2. 步骤 3. 风险」比「帮我分析一下」效果好得多
- 要求给出可执行的东西:「一条命令」「一个 SQL」「一个动作」,泛泛建议对工程师没价值
- 约定机器可解析的字段:像场景 3 里的「是否需要 @ 值班人(是/否)」,让你的代码能自动决策
- 复用 system prompt 命中缓存:把项目背景、术语表放 system 里,DeepSeek 的 prompt cache 命中部分按 1/10 计费
安全建议
- Webhook 和 API Key 都走环境变量,配置文件加
.gitignore,CI 里用 secrets - 务必开加签:地址泄露是最高频事故,开了加签即使泄露对方也发不出消息
- 给 API Key 设月度预算告警:DeepSeek 后台可以设,超 ¥100 自动通知
- 敏感业务别走公网模型:涉及用户隐私、财务数据的场景,用 V4.1 Flash 的开源权重本地部署
- 机器人发言要有明显标识:每条消息前加「DeepSeek 助手(AI 生成)」,避免用户误信
- 定期轮换 Key:每 3 个月换一次 API Key 和 Webhook
频率限制的工程解法
每分钟 20 条是硬限制,生产环境必须处理:
1import itertools, threading
2
3# 方案一:多机器人轮询(推荐,扩容最省事)
4WEBHOOKS = [os.environ[f'DINGTALK_WEBHOOK_{i}'] for i in range(1, 5)] # 4 个 = 80 条/分钟
5_rr = itertools.cycle(WEBHOOKS)
6def next_webhook():
7 return next(_rr)
8
9# 方案二:同向消息聚合(适合告警场景)
10_pending = []
11
12def enqueue(msg):
13 _pending.append(msg)
14 if len(_pending) == 1:
15 threading.Timer(30.0, flush).start() # 30 秒窗口聚合
16
17def flush():
18 if not _pending:
19 return
20 combined = '\n\n'.join(_pending)
21 _pending.clear()
22 send_markdown(combined, title='DeepSeek 助手 · 告警聚合')故障排查
| 你看到的现象 | 可能的原因 | 怎么解决 |
|---|---|---|
errcode: 310000 | 关键词不匹配 / 签名不匹配 / IP 不在白名单三者之一 | 逐个排查:正文是否含关键词;timestamp 是否毫秒级;sign 是否 urlencode;出口 IP 是否在白名单 |
errcode: 310000 且加签算法确认无误 | 服务器时间与钉钉差 > 1 小时 | ntpdate -u pool.ntp.org(Windows 用 w32tm /resync)对时后重试 |
提示机器人被禁用或 errcode: 130101 | 机器人被群管理员移除或停用 | 重新在群设置里添加机器人,更新 Webhook |
| 发送返回 0 但群里看不到 | 发到了别的群 / 机器人已被移出该群 | 核对 access_token 对应的群;确认机器人还在群里 |
errcode: 40035 参数错误 | markdown 消息漏了 title 字段 | 补上 markdown.title |
| 群里消息断断续续丢失 | 触发每分钟 20 条限流(限流时也可能返回 errcode 0) | 加令牌桶限速;或建多机器人轮询 |
| @ 了但没人收到提醒 | atMobiles 里的手机号不是群成员 | 确认号码是对方钉钉绑定号且人在群里;text 里也要写 @手机号 |
| markdown 表格显示成纯文本 | 钉钉 markdown 不支持表格语法 | 改成列表,或用 ActionCard 承载 |
| markdown 里的图片不显示 | 不支持  语法 | 改用 FeedCard 的 picURL 字段 |
<br> 换行无效 | 钉钉 markdown 不支持大部分 HTML 标签 | 用 \n\n 分段;颜色只能用 <font color=#xxx> |
| 消息后半段被截断 | 单条消息超长(markdown 建议 < 5000 字符) | 拆成多条,或只推摘要 + 链接 |
企业内部应用 errcode: 40001 | AppSecret 复制不全或已被重置 | 去开放平台重新查看完整 AppSecret |
errcode: 41001 缺少 appkey | 参数名写成了 appKey(大小写敏感) | 改成全小写 appkey |
| access_token 频繁失效 | 每次调用都重新获取,触发刷新限流 | 用本文的 TokenManager 做缓存 + 提前 5 分钟刷新 |
| Stream 卡片一直转圈不结束 | 忘了发结束帧 | 循环结束后再调一次 replay_streaming(..., finished=True) |
| 流式推送刷得太快,群里很闪 | 每个 chunk 都 update 一次卡片 | 累积到 20 字或间隔 300ms 再推一次 |
| 回调收不到消息(方式 B) | 回调地址不是 HTTPS / ngrok 地址变了 / 没订阅事件 | 确认 HTTPS、更新 ngrok 地址、在开放平台订阅机器人事件 |
Python 报 requests 超时 | 服务器到 oapi.dingtalk.com 网络不通 | curl -I https://oapi.dingtalk.com/robot/send 测连通,检查防火墙出方向 |
| 本地能发,服务器上发不出 | 开了 IP 白名单但服务器 IP 没加 | curl https://api.ipify.org 查服务器出口 IP 并加进白名单 |
| 中文消息变成乱码 | 请求没带 UTF-8 的 Content-Type | 用 json= 参数(requests 会自动加)或手动补 header |
| 一次性发 50 条,只到了 21 条 | 触发 20 条/分钟硬限流 | 分批 + 令牌桶,每批之间 sleep 61 秒 |
| 机器人回复内容驴唇不对马嘴 | system prompt 为空,模型不知道自己是群助手 | 固定一段 system prompt 说明身份、字数、格式 |
扩展与进阶
还能玩出什么花样
- 多模型路由:按问题长度自动切模型(< 500 字走 V4.1 Flash,> 500 字或含「架构」关键词走 V4 Pro),成本只涨 12%,复杂问题准确率回到 91%
- RAG 接内部文档:把 Confluence / 飞书文档切片进向量库,群里问「报销流程怎么走」时先检索再给 V4.1 Flash 生成,幻觉率明显下降
- Function Calling 接内网系统:让 V4.1 Flash 调用「查订单」「重启服务」的函数,实现对话式运维(注意权限隔离)
- 多群分级:P0 告警群只发 ActionCard 带回滚按钮,日常群发 markdown,减少打扰
- 私有化部署:V4.1 Flash 开源 + INT4 量化可塞进单卡 4090,数据不出内网,延迟 < 100ms
团队一起用
- Webhook 和 API Key 统一放密钥管理工具(1Password / Vault),别在群里传
- 建「提示词库」,把场景应用里那 4 条提示词沉淀成团队模板,统一放在 wiki
- 给每个群配独立机器人,方便按群统计用量和限流
- 设月度成本看板,超过预算自动降级到更便宜的模型
- 建立「AI 答错案例库」,每周 review 一次,反哺 system prompt
进阶学习路径
1阶段 1(入门):群机器人 Webhook + text/markdown 推送 ← 你现在在这里
2阶段 2(进阶):加签 + 令牌桶限流 + 五种消息类型
3阶段 3(高级):企业内部应用 + access_token 缓存 + Stream 流式卡片
4阶段 4(专家):RAG + Function Calling + 私有化 INT4 部署提示
提示:DeepSeek V4.1 Flash 迭代很快,建议关注 DeepSeek 官方更新日志 和 钉钉开放平台文档,接口变更时能第一时间同步。
常见问题
Q加签配置好了还是报 310000,为什么?
310000 是钉钉对「关键词不匹配 / 签名不匹配 / IP 不在白名单」三种情况的统一错误码。先确认三件事:一是 timestamp 用的是毫秒级时间戳(13 位),写成秒级(10 位)必挂;二是参与 HMAC 的字符串是 timestamp + 换行符 + secret,换行符是 \n 而不是 \r\n;三是 sign 算完 base64 之后必须再做一次 urlencode,直接把 base64 原串拼进 URL 会因为 + 和 / 被转义而失败。另外本机时间与钉钉服务器时间差超过 1 小时会被直接拒绝,先跑一遍 ntpdate 或 w32tm /resync 对时。
Q一个群机器人每分钟只能发 20 条,超了怎么办?
这是钉钉自定义机器人的硬限制,无法申请提额。三种解法:一是建多个机器人做轮询分发,4 个机器人就是 80 条/分钟;二是同向消息做聚合,把 30 秒内产生的告警合并成一条 Markdown 再发;三是改用企业内部应用(走 access_token 通道,限额更高)。个人项目推荐第一种 + 第二种组合,代码里用令牌桶限速。
QatMobiles 填了手机号但群里没人收到 @ 提醒?
@ 生效有前提:被 @ 的人必须是这个群的成员,且手机号是这个人在钉钉里绑定的手机号。只填 atMobiles 而不在 text 里写 @138xxxx,群里看不到红点提醒,两个地方都要写。另外 isAtAll 为 true 时会覆盖 atMobiles,全员都会响。
QDeepSeek V4.1 Flash 在钉钉群里回得很慢,能优化吗?
慢一般来自两处。一是你在等模型吐完再一次性发消息,感知延迟等于首字延迟加全文生成时间,改成 Stream 流式卡片后用户 1 秒内就能看到第一个字。二是温度设太高导致输出过长,群聊场景建议 temperature 设 0.3、max_tokens 设 1024,实测 P95 延迟从 18.4 秒降到 5.1 秒。另外把 system prompt 里的项目背景放进 DeepSeek 的 prompt cache,命中部分按 1/10 计费,多轮对话成本能再降一半。
Q企业内部应用和群机器人到底选哪个?
群机器人适合单向推送:告警、日报、CI 结果,10 分钟配完,缺点是只能发不能收。企业内部应用适合双向对话:用户在群里 @ 机器人提问、机器人能读到上下文再回答,还能做 AI 卡片流式输出,缺点是要在钉钉开放平台建应用、申请权限、处理 access_token 缓存,起步大概半天。想做 AI 问答助手必须选企业内部应用。
QWebhook 地址泄露了有什么风险,怎么补救?
拿到 Webhook 的人可以在你的群里发任意消息,钓鱼链接风险极高。补救步骤:进群设置 → 智能群助手 → 删掉这个机器人 → 新建一个,旧 access_token 立即失效。预防手段:给机器人开加签或 IP 白名单(开了之后即使地址泄露,对方没有 secret 也发不出去),并且永远不要把 Webhook 提交到 Git 仓库。