最近我在折腾 OpenClaw最想做的事就是把它塞进钉钉让团队在群里随手 一下AI 就能跑起来。不管你是拿它在本地跑 Ollama 开源模型还是用它做电商客服、ROS 机器人这类偏垂直的场景最后基本都会卡在同一个问题怎么让钉钉里的人直接跟这套智能体对话。这篇文章就是我完整跑通一遍之后的记录内容包括对接架构怎么设计、两种对接模式怎么取舍、从零到一的配置过程以及最容易翻车的那几个坑。适合自己搭过 OpenClaw 但还没接 IM 渠道的人也适合刚接触钉钉机器人开发、想低成本把 AI 能力接进内部群的团队。1. 对接前先想清楚架构与选型1.1 这条链路上到底有哪几段先说结论OpenClaw 不认钉钉钉钉也不认 OpenClaw它们之间必须有一个“翻译层”这大概占了整个项目八成的工作量。完整链路大概是四段。第一段是钉钉 App 本身用户在群里发一条消息、机器人这一层只负责展示和输入。第二段是钉钉开放平台它负责把“有人 机器人了”这个事件以约定的格式推送到你的接收程序。第三段才是关键接收程序收到事件之后做三件事解密和校验消息、把消息转成 OpenClaw 能认的请求体、再把 OpenClaw 的回复拼成钉钉能展示的消息发回去。第四段是 OpenClaw 服务本身它负责调用大模型、执行技能、组织最终回复。很多人上来就想直接把 OpenClaw 做成钉钉插件其实没必要。OpenClaw 这一层完全可以当成一个黑盒它对外暴露一个 HTTP 接口你给它一句话它回你一句话。你只需要在它前面加一个适配层把钉钉的“方言”翻译成 HTTP 请求就行。这个思路一旦想通后面写代码就不容易被带偏。1.2 为什么我选择 Stream 模式而不是 webhook 回调钉钉机器人接收消息有两条路一条是 webhook 回调钉钉把消息事件 POST 到你提供的公网 URL 上另一条是 Stream 模式你的程序主动和钉钉服务器建立一条长连接事件通过这条连接推给你。这俩区别说白了就类似“你留了一个电话等别人打进来”和“你一直占着线听对面说”。webhook 等于留电话你得有一个公网固定号码——也就是一个从外网能访问到的 URL还得配 HTTPS 证书、加签名校验不然任何人都能往你的地址 POST 垃圾数据。Stream 模式等于占线程序启动后主动连到钉钉服务器钉钉有消息就往这个连接上推你根本不需要公网 IP也不需要处理回调地址和加密配置。我自己是在家里服务器上跑的没有固定公网 IP所以直接选了 Stream 模式。下面是两种模式的对比对比项Stream 模式webhook 回调是否需要公网 IP不需要必须要有可公网访问的 URLHTTPS 证书不需要通常需要配置消息加签校验SDK 内部处理需要自己实现 AES 解密和签名校验适用网络环境内网、家庭宽带、动态 IP 都行有云服务器、域名时比较顺手连接稳定性长连接断线需自动重连每次请求独立稳定性取决于服务上线率开发门槛低官方 SDK 一把梭略高回调地址调试麻烦一句话总结只要能跑起来用Stream 模式在个人自建和中小团队场景下体验好得多。如果你本来就有云服务器webhook 也没毛病代码逻辑差别不大本文后面讲到的转发和回复思路两种模式通用。还有个容易混的点我必须提醒钉钉里的“自定义群机器人 webhook”只能发消息不能收消息。很多新手在群里加了一个 Webhook 机器人发现无论如何都收不到 消息就开始怀疑人生。实际上要接收对话你得在钉钉开放平台创建一个“企业内部应用”在应用里添加“机器人”能力走 Stream 模式或配置回调 URL才能收到群里 机器人的消息。1.3 消息路由与会话隔离钉钉机器人跑起来之后一个机器人可能被拉进多个群也可能同时被几百个人 。这时候第一件要处理的事就是“这句消息是谁发给谁的”。最简单有效的办法是看消息事件里带的会话 ID 和发送者 ID。钉钉每一条消息事件里都有一个 conversationId它是群维度的唯一标识还有一个 senderId是用户维度的唯一标识。我建议接收程序用这两个字段组合成一个 key作为用户的会话缓存 key。这样用户在群里跟机器人聊了三句话上下文是连续的另一个人在另一个群里提问不会跟前面的人混淆。会话不能无限累积。大模型的上下文窗口就那么长如果一直把历史消息堆进去很快就把 token 冲爆了。我在项目里维护了一个很简单的字典缓存同一个 key 只保留最近 10 轮消息超过之后自动丢弃最老的记录。10 轮这个数是经验值你也可以根据模型能力调整但思路一定要有——会话必须带生命周期。触发规则也要想好。如果机器人进了大群群里两百人消息本来就多不可能每条消息都交给模型处理。前期先用“只响应被 的消息”就能挡住大部分噪音。如果你想把机器人做成一个纯命令入口配一个前缀拦截更省只有以!ai或者/ai开头的消息才走 OpenClaw其余一律不处理实测下来流量能省一大半。2. 核心细节解析与实操要点2.1 OpenClaw 本体要准备到什么程度在对接钉钉之前先把 OpenClaw 跑通这是所有工作的前提。它跑在哪不重要重要的是它对外提供一个稳定的 HTTP 接口。我推荐用 Docker 跑省心。目录结构大致这样一个 docker-compose 文件一个挂载出来的 skills 目录OpenClaw 容器连一个 Ollama 容器。下面是我本地用的配置version: 3.8 services: ollama: image: ollama/ollama:latest container_name: ollama volumes: - ./ollama_data:/root/.ollama ports: - 11434:11434 restart: unless-stopped openclaw: image: openclaw/openclaw:latest container_name: openclaw ports: - 8080:8080 environment: OPENCLAW_API_KEY: sk-123456 OPENCLAW_MODEL: llama3.1:8b OLLAMA_BASE_URL: http://ollama:11434 volumes: - ./skills:/app/skills depends_on: - ollama restart: unless-stopped这里面有几个参数值得解释一下。OPENCLAW_MODEL填的是模型名我本地先用 Ollama 拉了一个 llama3.1:8b 做测试OLLAMA_BASE_URL让 OpenClaw 把算力请求转发给旁边的 Ollama 容器OPENCLAW_API_KEY是 OpenClaw 自己暴露接口时要校验的密钥对接层调用它的时候在请求头带上即可。顺便回应一个很多人关心的问题OpenClaw 只能用接入 API 的方式使用算力吗不是的。OpenClaw 在设计上把“算力来源”解耦了你可以让 Ollama 在本地生成回复也可以接某个平台的 OpenAI 兼容 API关键是你得有一个能让它调用的推理服务。本地跑 Ollama 的好处是免费、数据不出内网缺点是没有高端云模型那么强。你完全可以在本地验证通链路之后再把模型源切成云端。跑起来之后先自测。在命令行里直接发一个请求到 OpenClaw 的接口确认它能正常回话再往后走curl -X POST http://localhost:8080/api/chat \ -H Authorization: Bearer sk-123456 \ -H Content-Type: application/json \ -d {message: 你好帮我简单介绍一下你自己}不同版本的 OpenClaw 接口路径可能不一样有的版本是/api/chat有的是/v1/chat以你安装的版本仓库文档为准。自测这一步务必完整跑通我后面遇到的对接问题里有将近三分之一是落在“模型根本没起来”这种低级问题上。2.2 钉钉机器人应用的创建与安全设置对接层的第二步是去钉钉开放平台创建一个内部应用。步骤不复杂关键是别走错门。登录钉钉开发者后台选择企业内部应用点创建。应用名称、图标这些随便填创建完成之后进入应用详情左侧菜单里找到“机器人”能力添加一个机器人。如果你用的钉钉版本界面新一点可能在“消息推送”或“AI 能力”里能找到对应入口逻辑是一样的添加机器人能力之后你会拿到两个关键凭证AppKey 和 AppSecret。这两个东西就是机器人接管消息的身份证不要泄露到公开仓库里和你的数据库密码同等对待。这里有个安全配置要留意。钉钉后台里有几个常见的防护选项加签、IP 白名单、关键词白名单。加签是对消息内容做签名校验防止第三方伪造请求IP 白名单是只允许指定来源 IP 调用你的应用关键词白名单是只有消息包含特定词才推送给你。自测阶段我建议先全部放开等链路通了再逐步收紧。Stream 模式本身不暴露公网端口这已经是很大的安全优势但 AppSecret 一旦泄露别人就可以冒充你的机器人收发消息所以它一定要放在环境变量里不要写死在代码中。最后强调一遍如果你只是想要一个能让大家在群里对话的机器人请选择“企业内部应用”加“机器人能力”不是“自定义群机器人 webhook”。后者的定位就是单向通知工具类似于一个发短信的邮箱接不了对话场景。2.3 消息类型与回复格式钉钉的消息体系比想象中丰富。你的接收程序能收到 text文本、picture图片、audio语音、file文件、link链接等等但在早期落地阶段你只需要关心 text其他类型可以先忽略。回复的时候就有讲究了。直接回纯文本当然最简单但如果 OpenClaw 返回的内容包含多行文字、表格或代码块纯文本在钉钉里展示起来会很丑而且换行可能被压掉。我建议从第一次上线就用 markdown 格式的消息OpenClaw 天然擅长生成 markdown钉钉客户端也能渲染出不错的效果。markdown 回复的格式就在消息类型里填写 markdown正文符合钉钉的 markdown 语法即可和你在群里发普通 markdown 消息的写法一致。比如回复的内容里加### 标题或**加粗**都是支持的。如果你有更复杂的交互需求比如想让用户点一个按钮把工单状态改成“已完成”那就用 actionCard 或者钉钉互动卡片。卡片消息本质上也是 markdown 能力加交互按钮按钮点击后的回传事件会通过同一个 Stream 连接推回给你的程序你可以在事件处理器里接着处理。这部分属于进阶玩法我建议先把文本对话跑通再碰卡片否则排起错来很烦。3. 实操过程与核心环节实现3.1 第一步先让 OpenClaw 在本地能“开口”这一步的目标只有一个OpenClaw 的 HTTP 接口能稳定返回结果。把上一节的 docker-compose 保存后执行docker compose up -d然后进 Ollama 容器把模型拉下来docker exec -it ollama ollama pull llama3.1:8b模型下载可能需要一点时间下载完成后等 OpenClaw 容器日志不再报错再用 curl 做一次完整自测。这一步建议你认真点因为后续你写接收程序的时候会反复怀疑“是不是我代码写错了”如果提前确认过 OpenClaw 本身是好的排查范围直接缩小一半。我在这一步踩过一个坑Docker 容器内 OpenClaw 就绪的时间比想象中慢第一次 curl 请求一直连不上我还以为是端口映射错了后来看日志发现是模型初始化没完成。所以 curl 之前先看几秒钟日志确认输出稳定再请求。3.2 第二步创建钉钉应用并开启机器人能力进入钉钉开放平台开发者后台选企业内部应用填写基本信息后进入应用管理页。在左侧菜单里找“机器人”点添加机器人填好机器人名称和头像后提交。此时应用详情页会展示 AppKey 和 AppSecret复制保存到本地环境变量。接下来是关键在机器人的消息接收方式设置里选择 Stream 模式。界面上可能显示“Stream 模式”或者“消息长连接”不同版本的钉钉后台叫法略有差别核心意思都是“不需要配置回调 URL通过长连接接收消息事件”。有的旧版本界面会让你填“消息接收地址”如果你选了 Stream 模式就不需要填让它留空即可。保存之后再打开应用的功能开关把“机器人消息”能力允许接收事件打开否则消息推不过来。这一步做完你的应用就有了接收群消息的许可证。以后如果要换 webhook 模式也是在这个页面填一个回调 URL再自己处理加密和签名但按本文的方案Stream 模式已经够用。3.3 第三步编写本地接收程序接收程序是整个对接链路的核心。我用 Python 写的主要用了钉钉官方提供的 Stream SDK 和 requests 库。代码如下import os import json import requests import dingtalk_stream from dingtalk_stream import AckMessage APP_KEY os.environ.get(DINGTALK_APP_KEY, ) APP_SECRET os.environ.get(DINGTALK_APP_SECRET, ) OPENCLAW_URL os.environ.get(OPENCLAW_URL, http://localhost:8080/api/chat) OPENCLAW_API_KEY os.environ.get(OPENCLAW_API_KEY, sk-123456) def call_openclaw(message_text: str) - str: headers { Authorization: fBearer {OPENCLAW_API_KEY}, Content-Type: application/json, } payload {message: message_text} resp requests.post(OPENCLAW_URL, jsonpayload, headersheaders, timeout120) resp.raise_for_status() data resp.json() return data.get(reply) or data.get(text) or def handle_chatbot_message(msg: dingtalk_stream.ChatbotMessage): # 这里先回复一个 ACK告诉钉钉“我收到了”避免触发超时 ack AckMessage() text msg.text or if not text.strip(): return ack # 用发送者 ID 会话 ID 做会话隔离 session_key f{msg.sender_id}:{msg.conversation_id} print(f[session] {session_key} - {text}) # 调用 OpenClaw拿回复 try: reply call_openclaw(text) except Exception as exc: reply fOpenClaw 调用失败: {exc} # 往钉钉群里回复这里只用了最简单的方式 # 不同 SDK 版本回复 API 名称可能不同以官方示例为准 msg.reply_markdown(OpenClaw, reply) return ack def main(): credential dingtalk_stream.Credential(APP_KEY, APP_SECRET) client dingtalk_stream.DingTalkStreamClient(credential) client.register_callback_handler( dingtalk_stream.ChatbotMessage.TOPIC, handle_chatbot_message, ) print(接收程序启动等待钉钉消息...) client.start_forever() if __name__ __main__: main()这段代码逻辑很直白收到消息立即回 ACK 防止超时然后同步调用 OpenClaw拿到回复后以 markdown 形式发回群里。实际项目里你要做的处理会多一点比如记录日志、对消息做前缀过滤、设置会话历史缓存——这些都可以在这个 handler 里扩展。注意一个细节msg.reply_markdown这个方法在不同版本的钉钉 Stream SDK 里写法可能不一样我在自己环境的 SDK 版本上验证过可用你如果遇到方法名报错去官方 SDK 示例里找一下对应的“回复消息”API 即可整体流程不受影响。3.4 第四步用 Skill 扩展真实能力对接钉钉之后OpenClaw 最值钱的地方不一定是模型本身而是它的 Skill 机制。Skill 相当于给智能体装上的“手”让它能查数据、调接口、做动作而不仅仅是回答问题。你可以在 OpenClaw 的 skills 目录下建一个子目录每个技能包含一个说明文件和实现代码。结构大致这样skills/ order_query/ SKILL.md skill.pySKILL.md 是给 OpenClaw 看的描述这个技能是干什么的、在什么场景下触发、需要什么参数。比如你做一个“查订单状态”的技能描述里写清楚“当用户询问订单进度时调用此技能”参数是订单号。skill.py 就是具体的实现逻辑可以去查数据库、调内部接口最终返回一个字符串给 OpenClaw再由它组织成自然语言回复。这里有一个设计上的建议不要把钉钉的适配逻辑塞进 Skill 里。Skill 的定位是“业务动作”而钉钉的 stream 接收、消息格式转换是“通道适配”。这两层分开以后你想再接一个飞书或者微信客服的时候只需要换通道适配层Skill 和 OpenClaw 完全不用动。我在项目里吃过把通道代码写进 Skill 的亏后来接第二个 IM 渠道时差点想重写一遍。4. 常见问题与排查技巧实录4.1 机器人收到了消息但 OpenClaw 没回复这是最高频的问题。遇到这种情况先把链路拆成二段排查。第一段用 curl 直接调 OpenClaw 接口看是否有正常回复。如果 curl 都超时或报错问题出在 OpenClaw 这一侧跟钉钉一点关系都没有——先看容器日志、模型进程、API Key 是否生效。第二段如果 curl 正常那就是接收程序转发环节的问题。最常见的原因是请求头里的Authorization没带对或者 OpenClaw 接口路径写错返回了 404。我自己的一个教训是刚开始测试时OpenClaw 容器还在加载模型接收程序这边同步请求直接卡了 120 秒才超时钉钉早就把那次请求判为失败。后来我给 OpenClaw 加了健康检查等模型加载完成再启动接收程序才解决。排查时先看接收程序有没有打印“调用失败”的异常信息一旦有把异常信息直接往 OpenClaw 日志里对着看。4.2 回复内容乱码、截断或者换行丢失这类问题多半不是钉钉的锅而是 JSON 编解码和消息格式没处理好。OpenClaw 返回的内容里如果带了奇怪的换行符或者特殊字符在拼装请求体时可能被转义吃掉导致回复内容错位。处理办法是统一用 JSON 库序列化不要手动拼字符串。钉钉对单条机器人消息有长度限制过长的回复会被截断。我的做法是OpenClaw 返回文本后如果超过一定长度先截断再加省略号或者拆成几条 markdown 消息连续发送。换行丢失通常出现在你用纯文本类型回复时改成 markdown 类型基本能解决markdown 对段落和代码块的兼容性好很多。4.3 Stream 连接不稳动不动断开Stream 模式长连接偶尔断开是正常的关键是必须有自动重连。官方 SDK 内置了重连机制一般不需要你自己实现但要在日志里能看到重连记录才算正常。如果日志显示反复断开、不断报错先查两样东西AppKey 和 AppSecret 是否正确。Secret 填错的时候连上之后会在鉴权阶段被服务端踢掉表现就是“连上-断开-重连”的死循环。还有一类情况是网络环境导致的。公司内网对长连接不友好连接会被中间设备掐断这时可以试试把建连的域名加入访问白名单或者换一个网络环境观察是否稳定。生产环境想省心可以再加一层 systemd 守护配合公开日志进程挂了自动拉起。4.4 多群并发消息积压和限流前期串行处理没问题但人一多模型推理时间又长用户消息排起队来体验很差。简单的优化方案是引入一个队列接收程序收到消息后立刻 ACK把消息塞进队列后台 worker 依次调用 OpenClaw再把结果发回对应会话。这样做最大的好处是隔离用户等待时间一个人卡住不会堵死所有人。钉钉对机器人的发送频率有限制如果某个群特别活跃一瞬间的大量回复可能触发限流。我建议在接收程序里对同一个会话的回复做“至少间隔 1 秒”的控制如果需要更复杂的流式输出再考虑把整个通道升级成异步框架。并发这块不用一步到位先把链路跑通再根据真实使用情况平滑优化。我个人在实际操作中的体会是对接过程最大的障碍往往不是某个单一环节特别难而是四个环节OpenClaw、钉钉后台、接收程序、Skill各自独立出了问题不好定位。所以建议你做的时候坚持一个原则——先黑盒验证每个环节再连成链路。我第一版跑通时只是让群里 一下机器人回一句“你好”整条链路通了之后后面的所有扩展都是在这个骨架上加血肉。最后再分享一个小技巧接收程序的日志一定要打印消息原文和最终回复这两行日志在排查“为什么机器人回答得不对”时能帮你省下一大半的问人时间。