
1. 技能Skill、插件Plugin与连接器Connector的边界以及你的 API 到底该走哪条路先从一个让我印象深刻的困惑说起。有段时间我在 OpenClaw 里接入一个内部知识库 API想在对话里直接问昨天上线的那批配置有没有异常Agent 需要实时拉取这个 API 的数据再回答。当时我面临一个典型的选型问题这个能力到底应该写成技能Skill还是做成插件Plugin又或者是配一个连接器Connector折腾了一个下午三种方式都试过之后我才真正把这三者的边界理清楚。先说结论性质的理解技能是告诉 Agent 怎么用某个能力它本质上是描述文件加少量脚本偏配置化、轻量化插件是给 Agent 装上新的能力本身它是正儿八经的代码扩展可以监听事件、拦截消息、跑定时任务偏工程化连接器则是把 Agent 接到某个外部平台上比如 Microsoft Teams、Discord、Obsidian 这种对话入口或知识载体。三者的关系如果用生活化类比大概是这样技能像是给一个人看了张操作说明书让他学会用某个工具插件是直接给这个人装了一双新手、一个新器官让他能做原本做不了的动作连接器是给他开了一扇门让他能走进不同的房间和不同的人说话。这个区分直接决定了你的自定义 API 集成方案怎么设计。我把三者的核心差异整理成一张表后面所有的实操都围绕这张表展开维度技能Skill插件Plugin连接器Connector本质指令模板 轻量脚本代码扩展 事件钩子平台通道适配与 API 的关系在脚本里调用 API返回结果给 Agent在事件回调里调用 API把外部数据/能力注入运行时一般不直接调业务 API主要负责平台消息收发适合场景让 Agent 学会一个可复用的动作如查天气、调翻译、做摘要需要监听消息事件、后台定时任务、多 API 编排、改写 Agent 行为接入 Teams、Discord、Obsidian 等外部平台开发成本低通常一个 markdown 加一个脚本中到高需要写代码和注册钩子取决于平台协议复杂度维护方式改描述文件或脚本即可生效需要重载/重启插件改连接配置我在最初做 API 集成时踩过的最大一个坑就是把所有需求都往插件里塞结果一个查配置是否有异常的小功能写了一百多行事件处理代码调试起来特别痛苦。后来我发现只要这个 API 的能力是用户主动问Agent 主动调用技能几乎总是更轻的选择只有当我要被动监听某类消息然后触发 API 调用或者定时拉取数据主动推送才值得动用插件。这个判断标准我建议你在动手之前先想清楚因为它决定了后续所有的文件结构、部署方式和排错路径。2. 技能侧 API 集成实操从 SKILL.md 到外部服务调用2.1 技能的标准目录结构与 API 配置入口如果你决定走技能路线第一步是理解 OpenClaw 的技能目录结构。这个我没见过有官方文档把它讲得特别细但根据社区里通用的约定一个技能通常放在skills/技能名/目录下里面有一个SKILL.md文件作为操作说明书再加一个scripts/子目录存放实际的可执行脚本。整个结构大概是这样的skills/ └── daily-check/ ├── SKILL.md └── scripts/ ├── check_config.py └── requirements.txtSKILL.md是最核心的部分。Agent 在决定是否调用这个技能时读的就是这个文件。它有点像给 Agent 看的产品说明书里面有技能的名字、它能解决什么问题、什么时候该用它、脚本怎么调用。我一般会这样写--- name: daily-check description: 当用户询问配置变更、上线检查、异常排查时使用拉取内部系统的配置核对 API 并返回结果。 --- # daily-check 用于从内部配置中心拉取最近更新记录检查是否存在异常变更。 ## 使用方式 1. 当用户提出配置是否异常上线检查等意图时运行以下脚本 bash python scripts/check_config.py --hours 24脚本输出为 JSON包含status与changes两个字段直接整理结果回答用户。这里的关键点是 description 字段写得越具体Agent 的命中率越高。我见过很多人写查询配置结果 Agent 在遇到昨天是不是有人动过路由这种话术时根本没触发技能。你需要在 description 里把各种说法都覆盖进去配置、变更、上线、检查、异常、回滚都列上这对大模型的意图识别帮助巨大。 ### 2.2 脚本调用 API 时的上下文传递与密钥读取 有了 SKILL.md接下来就是脚本本身。这里有一个容易忽略的设计问题技能脚本是一个独立进程它并不知道用户刚才在对话里说了什么。Agent 只会把技能描述里声明的参数比如上面的 --hours 24传给脚本。所以你在设计脚本时要把用户说话到脚本参数的映射提前设计好。比如用户问昨晚十一点之后的配置变更Agent 就会解析出 hours 大概等于多少或者你可能需要设计一个 --since 参数来传具体时间。核心原则是技能脚本只负责根据给定的参数调 API 并返回结果不负责理解复杂的自然语言。 再说密钥读取。我强烈建议不要在任何技能脚本里硬编码 API Key。OpenClaw 本身支持从环境变量和 .env 文件读取配置技能脚本直接读环境变量即可。以 Python 脚本为例 python import os import sys import json import requests API_BASE_URL os.environ.get(CONFIG_API_URL, https://api.example.internal) API_KEY os.environ.get(CONFIG_API_KEY) if not API_KEY: print(json.dumps({status: error, message: 未配置 CONFIG_API_KEY})) sys.exit(1) # 解析参数 hours 24 for i, arg in enumerate(sys.argv): if arg --hours and i 1 len(sys.argv): hours int(sys.argv[i 1]) resp requests.get( f{API_BASE_URL}/changes, params{hours: hours}, headers{Authorization: fBearer {API_KEY}}, timeout10, ) resp.raise_for_status() data resp.json() # 只输出精简后的结果避免把超长原始数据喂回给 Agent changes data.get(changes, []) summary { status: ok, count: len(changes), top_changes: [ { id: c.get(id), title: c.get(title), operator: c.get(operator), time: c.get(time), } for c in changes[:10] ], } print(json.dumps(summary, ensure_asciiFalse))注意到了吗我在脚本最后做了一个关键动作裁剪输出。这是技能脚本设计里特别重要的一环。Agent 回看这个脚本的输出时如果收到的是一个几百 KB 的原始 API 响应大概率会超出模型上下文限制或者被无关字段干扰判断。你只把 Agent 回答用户需要的那几个字段数量、变更标题、操作人、时间提取出来既省 token 又提升准确率。这也是我在多次实测后总结出的最重要经验之一。2.3 技能脚本的调试与生效机制技能写完之后怎么确认它真的生效我试过几种方法最直接的是在 OpenClaw 里触发一段包含技能描述关键词的话。比如我刚才那个例子直接输入检查一下最近的配置变更看 Agent 的输出里是否出现了脚本的执行结果。如果 Agent 完全没有调用脚本或者回答我没有相关能力大概率是SKILL.md的description写得不够明确或者技能目录放错了位置。如果脚本本身有问题我建议先在终端里手动跑一遍验证脚本逻辑和 API 连通性。注意OpenClaw 加载技能时通常是在 Agent 启动时扫描技能目录的如果你新增了技能文件而 Agent 没有感知可能需要重启会话或重载配置。这个我实测在部分版本里是必须的在部分版本里可以热加载具体看你的部署方式。一个相对稳妥的习惯是改完技能文件后重开一个会话再做验证避免干扰判断。3. 插件侧 API 集成事件钩子、定时任务与多 API 编排3.1 插件运行机制钩子到底钩的是什么当你的需求超出了用户提问—Agent 调用这种一问一答的模式比如你想实现每当有新消息进来时先经过一个外部敏感词 API 过滤或者每天早晨九点自动拉取外部系统的数据并总结推送到对话里这时候就该写插件了。插件的运行机制核心是事件钩子。OpenClaw 在运行时会在各种节点抛出事件插件可以注册对这些事件的监听在事件发生后执行自己的代码然后再把控制权交还给 Agent。常见的事件包括消息接收比如每次用户发言触发、Agent 回复前可以在回答前插入外部 API 的结果、定时任务触发按照 cron 表达式执行等等。我常用的一个模板是插件注册一个消息接收钩子在这个钩子里调用一个外部风险分析 API把分析结果注入给 Agent 让它决定怎么回答。插件的基础目录结构大概是plugins/ └── risk-filter/ ├── plugin.json ├── main.py └── requirements.txtplugin.json声明插件的元信息和入口文件main.py是插件主逻辑。一个最简的plugin.json长这样{ name: risk-filter, version: 1.0.0, entry: main.py, hooks: [message.received, agent.before_reply] }然后在main.py里实现对应的钩子函数import os import requests RISK_API_URL os.environ.get(RISK_API_URL) RISK_API_KEY os.environ.get(RISK_API_KEY) def on_message_received(ctx): 在消息传给 Agent 前先调用外部风险分析 API 做一次过滤。 text ctx.get(message, {}).get(content, ) if not text: return ctx try: resp requests.post( RISK_API_URL, json{text: text}, headers{Authorization: fBearer {RISK_API_KEY}}, timeout5, ) resp.raise_for_status() result resp.json() if result.get(blocked): ctx[message][content] 该消息未通过内容检查请直接忽略不要回复具体内容。 elif result.get(suggestion): # 把外部 API 的提示注入到上下文中Agent 最终回答时会参考 ctx.setdefault(injections, []).append(result[suggestion]) except requests.RequestException as e: print(frisk-filter: API 调用失败: {e}) return ctx写插件一定要注意钩子函数的返回值会被 OpenClaw 继续往下传递。你要是不小心把ctx里的关键字段改没了Agent 的行为会变得很诡异。我调试时曾经犯过把消息内容整个覆盖成空字符串的低级错误结果 Agent 在用户什么都没说的情况下自言自语排查了半天才发现是插件把ctx[message][content]置空了。所以插件代码里任何对上下文对象的修改都要有明确注释并在修改前打印原始值。3.2 定时拉取 API 的插件实践另一个特别常用的场景是定时任务。OpenClaw 的插件系统支持通过配置声明周期性的调度例如{ name: morning-brief, version: 1.0.0, entry: main.py, schedule: { cron: 0 9 * * * } }然后在main.py里实现一个定时执行函数拉取外部数据比如内部系统的昨日指标 API格式化之后推送到当前会话。注意定时任务里如果涉及向 Agent 发送消息要去了解 OpenClaw 的主动消息接口不同渠道Teams、Web 界面等的消息投递方式不太一样。我自己遇到过定时任务在本地运行正常但部署到远程服务器后没有把消息推送出去的情况后面排查才发现是远程环境里没有配置默认会话 ID消息不知道该往哪个会话发。这个坑在后面我会专门展开讲。3.3 插件的启用、重载与日志排查插件写完之后怎么启用一般是在 OpenClaw 的配置里把enabled_plugins加上插件名然后重启 Agent。有些版本也支持运行时动态注册但我建议不要依赖这个特性生产环境还是以配置声明为准。插件一旦挂载失败Agent 通常会忽略它而不是崩溃所以你会看到插件没生效的安静失败——这是最讨厌的情况因为系统不会报错只是行为不对。我的排查习惯是三步走第一步看启动日志里有没有加载插件的记录第二步在插件代码里加print观察这些日志是否出现在 agents 的日志输出流中第三步把外部 API 的请求和响应都打印出来确认 HTTP 层面是否通畅。很多插件问题其实就是外部 API 连通性问题而不是 OpenClaw 本身的问题先验证 API 能通再查插件逻辑效率会高很多。4. 密钥管理、模型供应商配置与多会话接入的细节4.1 API Key 的存放位置与 OpenRouter 这类聚合 API 的配置方式技能和插件都涉及密钥所以这个主题值得单独讲。OpenClaw 的配置体系里模型提供商的密钥通常配置在全局配置或.env文件里而技能/插件所需的业务 API 密钥则各取所需。我推荐的划分方式是模型提供商的密钥比如 DeepSeek、OpenRouter 等放全局配置业务系统 API 的密钥放.env通过环境变量注入到技能和插件里。两条路分开互不污染。这里特意说一下模型聚合 API 的问题。很多人会在 OpenClaw 里配置 OpenRouter 这类 API 聚合平台来统一接入多种模型。这类平台的好处是你只需要一个 API Key通过模型标识切换不同模型不用每个模型单独注册。配置方式通常就是在模型配置区填上 api_key 和 base_url然后在技能/插件的代码里调用时也走这个统一入口。但要注意这种统一入口指的是模型完成能力的入口比如让 Agent 用某个模型生成摘要。如果你要做的是调用一个业务系统查数据那就跟模型聚合 API 没关系不要绕道走模型入口直接调用业务 API 本身。4.2 多会话、多 Channel 下 API 集成的边界问题OpenClaw 里的 Agent 可以选择不同的 Channel也就是对话接入渠道比如终端、Microsoft Teams、Obsidian 等。初次接触的人容易把 Channel 和 API 集成搞混Channel 解决的是Agent 在哪个平台上跟你对话API 集成解决的是Agent 能调用什么外部数据和能力。两者是正交的。举个例子你配置 Agent 接入 Microsoft Teams那只是给 Agent 开了一个 Teams 的对话入口。Agent 内部的技能能不能调用内部系统的 API取决于技能和插件本身的配置跟 Channel 没关系。反过来说你在一个本地终端的 Channel 里照样可以调用任何外部 API。我在实践里见过有人在 Teams 接入时反复检查 API 配置结果发现 API 一直好着纯粹是 Teams Channel 自己的 token 配置过期了。所以在排查问题时要先分清层级是对话渠道的问题还是技能/插件的能力问题还是底层 API 服务的问题。把这个分清能少走一半弯路。4.3 密钥轮换与最小权限原则按最小权限原则给技能和插件设置的 API Key 只应该具备完成该任务所需的最小权限。我的内部系统原来给过一个管理员级的 token后来被一个技能脚本引用虽然功能上一切正常但心里总不踏实。后面我把那个 token 撤销换成了一个只读 token心里才踏实。另外密钥轮换一定要有节奏感建议每三到六个月轮换一次轮换时先确认没有正在运行的会话依赖旧密钥再改配置并重启 Agent。如果你用了.env注意把它加进.gitignore避免不小心提交到代码仓库里。这类事故我见过不止一次。5. 高频报错的完整排查链路从 session file locked 到上下文超限5.1 session file locked (timeout 60000ms) 的根因与修复这个报错在社区里被问得很多agent failed before reply: session file locked (timeout 60000ms)。我第一次碰到时完全摸不着头脑因为字面意思是会话文件被锁定等了 60 秒还没解开。后来分析了日志才明白这是并发的会话读写冲突。OpenClaw 会把每个会话的状态持久化到磁盘上的 session 文件里同一时间只有一个进程可以写这个文件。如果两个进程同时打开同一个会话或者同一个会话被反复拉起且没有正常退出就会产生锁竞争。最常见的触发场景是你开了一个终端窗口跟 Agent 对话然后又用脚本或定时任务触发同一个会话或者上次 Agent 进程没干净退出残留进程还占着锁。修复方式按顺序来检查是否有残留的 OpenClaw 进程把它停掉。Linux 上可以用ps aux | grep openclaw查进程Windows 上可以用任务管理器确认。确认定时任务和交互终端不要使用同一个会话 ID。如果你确实需要并行多个对话为每个上下文指定独立的 session id。如果锁文件确实异常残留可以手动移除对应的 session 文件前提是你能承担该会话历史丢失的代价或者用 OpenClaw 提供的清理命令重置会话状态。我自己的实践里最容易复现这个问题的是一边开着终端一边在 API 脚本里触发同一个会话的场景。所以后来我给自己定了个规矩凡是需要外部触发的自动任务单独分配一个专属 session绝不跟手工对话抢同一个文件。这样做了之后这个报错在我这边基本没有再出现过。5.2 模型上下文超限记住不是你代码的问题是 token 的问题另一个高频报错长这样api error: 400 this models maximum context length is 1048576 tokens。我第一次遇到时反复检查技能脚本以为是脚本传入的参数格式有误但后来想明白了这个报错的意思是请求里包含的 token 数量超过了模型的最大上下文长度跟你的 API 密钥、脚本逻辑没有直接关系。为什么一个看起来简单的查询会触发 104 万 token 的超限我遇到的情况是某个历史会话积累了极长的对话记录每次新请求都会把整段历史重新发给模型。对话一旦积累到几十万 token再叠加外部 API 返回的大段内容就撞到上限了。解决方案有几个方向限制单次请求携带的历史轮数。OpenClaw 层面一般有对话轮数或 token 预算的配置可以设置一个上限超出的部分自动裁剪或做摘要压缩。在技能脚本里控制返回内容的大小。这一点我在前面已经强调过脚本输出不要全量透传只给 Agent 需要的摘要信息。新建会话。如果历史记录本身已经不重要最简单直接的办法就是开一个新会话从干净状态开始。还有个思路是调整模型的上下文处理策略比如把超长文本做分段处理每个分片单独交给模型处理再汇总。这个比较适合拉取一个大文件做总结的场景在技能脚本里实现即可先按文件长度切块逐块调用模型 API最后把各块摘要合并。我实测这样既不会撞上下文上限总结效果也比一次性硬塞要好。5.3 认证与请求失败的排查顺序技能和插件接入 API 时最烦人的是那种能跑但时不时报错的情况。常见的认证报错信息是api key is required in authorization header这种通常直译就能看懂请求头里没有带 Authorization 字段或者带的格式不对。排查顺序我建议固定下来确认环境变量是否真被加载。在脚本里打印一下os.environ.get(XXX)看是不是空的。很多人漏了.env的加载结果脚本里读到的永远是 None。确认请求头格式。大部分 API 用Authorization: Bearer key也有部分用X-API-Key头。以目标 API 文档为准。用 curl 直接测目标 API排除是不是网络到目标服务的链路有问题。这一步看起来多余但能把OpenClaw 的问题和API 自身的问题快速切分开。看 OpenClaw 的日志目录。技能和插件的print输出一般都会进日志检查 API 调用是否真的发出去了有没有超时或 5xx 返回。这套顺序我沿着它排查了很多次几乎每次都能精确定位到某一层而不是东翻西找。6. 部署形态与长期运行从本地验证到持续集成的小建议6.1 Windows、Ubuntu 与 Docker 部署的差异OpenClaw 的部署方式在不同平台上有不少差异。Windows 上最常见的是通过 Docker Desktop 跑容器这一步本身就有一个著名报错failed to connect to the docker api at npipe:////./pipe/dockerdesktop-linux。这个报错的意思是 Docker 客户端连不上 Docker 引擎的命名管道通常是因为 Docker Desktop 没启动或者 Windows 容器/Linux 容器模式没切对。先启动 Docker Desktop等它显示引擎运行中如果还不行就检查容器模式是不是选成了 Windows 容器OpenClaw 镜像要求运行 Linux 容器模式。Ubuntu 上部署相对顺畅可以直接用 docker compose 也可以从源码运行。如果你是在服务器上长期跑我个人更推荐用 docker compose 托管因为日志、重启策略和环境变量管理都方便。有一个容易被忽略的点是时区配置定时任务依赖系统时区如果容器默认 UTC你配置的每天早晨九点就会变成北京时间下午五点。我建议在 compose 文件里显式设置TZAsia/Shanghai这样的时区环境变量避免定时任务行为出乎意料。6.2 日志可观测性怎么知道技能/插件到底在干什么技能和插件多了之后最需要解决的是可观测性问题。OpenClaw 自身的日志会记录 Agent 的思考和调用过程但技能脚本内部的请求失败、超时、参数异常不一定都体现在主日志里。我的做法是在每个技能/插件的入口处加统一的日志前缀比如[skill:daily-check]、[plugin:risk-filter]并且在关键节点打印开始调用 APIAPI 返回成功耗时 xxx ms结果裁剪后条数10这类信息。这样在查看日志时用grep一下前缀就能快速定位某个技能在某次对话里是否执行、执行到哪一步、外部 API 是否正常。配合持续的部署环境时可以把这些日志接到统一的日志采集管道里。我之前在一个部署中把 OpenClaw 的日志目录挂载到宿主机再用日志采集器思路类似 Logstash 这样的系统做过滤和告警。这个做法特别适合生产环境当外部 API 连续失败超过阈值时能第一时间收到提醒而不是等用户反馈说Agent 变笨了才发现。6.3 围绕 API 集成的自动化测试思路最后说说自动化。技能和插件本质上是代码代码就该有基础测试。我不要求每个脚本做多严格的单元测试但至少要做到两点第一脚本本身能在终端里独立运行并返回预期格式的 JSON这是可运行性测试第二用一个固定的模拟输入跑一遍确认输出字段的类型和数量稳定这是可解析性测试。这两点能保证 Agent 的调用链条不会断裂。为什么这很重要因为 OpenClaw 的技能调用是黑盒式的Agent 拿到你脚本的 stdout按字符串解析。如果某天外部 API 返回结构变了比如changes字段从数组变成了字符串你的脚本如果没有兜底逻辑Agent 就会把异常输出当成正常结果呈现给用户。我在脚本里会尽量做字段类型检查和异常捕获确保任何意外情况下脚本都能输出一个结构稳定的 JSON至少让 Agent 知道你遇到了错误而不是让它编造一个答案。我的一点个人总结这套自定义 API 集成流程我自己从最初的所有需求都写插件到后来能技能就技能不能技能才插件不涉及能力只涉及平台就用连接器花了不少时间。现在我的工作习惯是凡是用户主动询问、需要实时查数据的能力优先写成技能理由是它轻、好改、调试直观凡是需要监听事件、定时执行、或者在消息链路里做预处理的能力才写成插件理由是它能和 Agent 的生命周期深度绑定凡是系统接入类的需求比如从 Microsoft Teams 收消息、往 Obsidian 同步笔记我会明确告诉自己这是连接器的活儿别用插件去硬怼平台的协议细节。最后再分享一个我在技能脚本设计上最受益的小技巧每个技能脚本的 stdout 输出都必须严格遵循 JSON 格式哪怕出错也要输出{status: error, message: ...}这种结构。因为 Agent 对你的脚本永远是一个黑盒解析器给它稳定格式它才能稳定地帮你把事情办好给它的输出一个 JSON 结构它的回复质量会显著提升。这个习惯听起来不起眼但在我所有的 API 集成实践里它的价值可能比任何单点技术技巧都大。