
OpenClaw 最近在代理自动化圈子里热度很高但我发现很多人还是把它当成一个“装完就能自动干活的工具箱”实际用起来却在自定义能力上卡壳。这篇内容从一个真实需求出发带你从零写一个自定义 Agent 工具再一路把接上 requireApproval 审批钩子把 OpenClaw 插件开发的完整链路走一遍。文章不预设你已经很熟悉这个框架但也不会把时间浪费在安装向导上适合已经跑通基本部署、想给自己代理增加私有能力的开发者。1. 动手前必须搞清楚的三个底层概念1.1 Agent Harness 是编排者, 不是工具先记住一个最关键的心智模型OpenClaw 这样的 agent harness它的职责是发起工具调用而不是自己成为工具。换句话说它是台前的调度中心而不是每个具体任务的执行者。你可以把它理解成一个项目经历项目经历不会亲自去写代码、画图、发消息但它知道在什么时候把什么任务派给哪个人。OpenClaw 里的工具就是那些被派活的工程师每个工具只负责一件具体的事。这个理解直接决定了你写插件的方式。很多人第一次接触时会问能不能写一个插件让 OpenClaw 自己完成一个完整的视频剪辑流程可以但你写的不应该是一个巨大的单体脚本而是一组被代理编排的工具提取音频、剪辑片段、渲染导出每一个都是独立的能力块。代理根据用户的指令在正确的时机调用正确的那一个。如果你把所有逻辑都塞进一个工具代理就失去了中途插手、调整参数、跳过步骤的空间整个流程会变得极其僵硬。所以在动手之前先接受一个事实你写的工具不是一个“给代理用的程序”而是一个“被代理调度的函数”。这意味着接口约定比实现逻辑更重要输入要严格按参数 schema 来输出要是干净的 JSON返回的信息要让模型能直接理解。后面所有步骤都围绕这个原则展开。1.2 Skill、Tool、Hook 到底有什么区别OpenClaw 的文档里会反复出现三个词Skill、Tool、Hook。很多人把混在一起讲实际上它们的层次是不一样的。Skill 是一个打包好的能力集合通常以目录形式存在里面可以包含多个工具的实现、一个描述文件、依赖和资源。Tool 是最小的可执行单元有固定的名字、描述、参数定义代理通过 function calling 决定是否调用它。Hook 则是事件钩子它不在工具列表里暴露给模型而是由 OpenClaw 在特定生命周期主动调用。用一张表来对比会更清楚概念层次谁调用典型例子Skill打包单位开发者在配置中启用server_health 技能目录Tool执行单元模型通过 function calling 调用server_health 检查 URLHook事件回调OpenClaw 框架在生命周期调用requireApproval 审批钩子再换一个生活类比Skill 是工具箱Tool 是箱子里的一把扳手Hook 是借工具时需要过的审批流程。三者的关系不是替代关系而是不同层面的设计机制。理解这一点你在配置文件里看到skills、hooks、requireApproval就不会觉得混乱了。1.3 为什么自定义工具必须考虑审批很多人一上来就急着写工具忽略了一个关键问题权限边界才是代理自动化真正的风险点。一个助手如果只能聊天出问题也就是话说错一旦它能写文件、执行命令、发消息、调用支付接口一次 prompt injection 或者模型误判就可能产生实质损失。举个直接的例子你给代理加了一个“发送邮件”工具结果某次对话里用户输入的内容包含恶意指令让代理向通讯录里所有人发送垃圾邮件。单纯依赖模型的安全对齐是不够的因为模型可能被绕过也可能在复杂上下文中判断失误。requireApproval 审批钩子的存在就是在框架层面强制加一道人工确认闸门让敏感操作在执行前必须经过你同意。所以你在设计自定义工具时要先想清楚这个工具如果被误调用后果是什么然后决定它属于“直接放行”还是“需要审批”的类别。这不是可选的加分项而是一个成熟插件的基本素养。2. 搭好开发环境安装、目录与最小 Skill2.1 安装方式怎么选脚本、源码还是容器动手写代码之前先把环境准备好。OpenClaw 的安装方式常见的有几种官方安装脚本是最省事的也可以通过脚本指定 git 安装方式从 GitHub 的 main 分支直接检出源码来跑。这种方式适合想跟踪最新特性的场景但代价是配置字段可能随时变化今天能用的写法明天可能就废弃了需要有心理准备。如果你在服务器上使用我建议用 Docker 方式部署隔离性和可复现性都更好。社区也有一些 Windows 离线整合包适合本地快速体验但我不建议在生成环境中依赖这类包因为你不知道它内置了什么版本、什么依赖出了问题很难排查。安装完之后先确认版本运行openclaw --version和openclaw --help看看这份版本的命令和字段长什么样。还有一个容易踩的坑是语言运行时版本。OpenClaw 的插件脚本可以用多种语言写但不同语言运行时版本会影响依赖安装。比如你用 Python 写 skill本地环境是 3.8而技能里用到了 3.10 才有的语法加载时就会报错。建议在一个干净的虚拟环境或容器里做开发避免全局依赖污染。2.2 配置文件与插件目录怎么组织OpenClaw 的配置文件通常是一个openclaw.json位置可能在项目根目录也可能在用户目录下的.openclaw文件夹里。配置里会有模型提供商、启用的 skill、hooks、审批规则、日志级别等信息。插件Skill的默认目录一般是~/.openclaw/skills/每个 Skill 都放在一个独立子目录里。你可以在配置里指定额外的 skill 目录比如把团队公用的 skill 放在一个共享路径下或者把当前项目的自定义 skill 指向代码仓库这样改完代码直接重启服务就能加载。我的习惯是开发阶段一定把 skill 目录指向仓库而不是复制到默认目录这样版本管理、代码 review、回滚都方便。配置文件本身是 JSON 格式不支持注释。我经常看到有人为了方便在里面加了//注释结果服务启动直接报解析错误。如果你需要注释可以把不同环境的配置拆成多个文件或者用配置管理工具来维护。下面是一个简化配置示例{ model: { provider: anthropic, name: claude-sonnet-4-20250514 }, skills: { dirs: [~/.openclaw/skills, ./my-skills] }, hooks: { requireApproval: python ~/.openclaw/hooks/require_approval.py }, logLevel: debug }hooks.requireApproval这里指向的是一个外部脚本后面第 4 章会详细说。2.3 先跑通一个最小 SkillHello World在写复杂工具之前先做一个最小可用的 Skill 验证整个链路。创建一个目录~/.openclaw/skills/hello_skill/里面放两个文件。第一个是SKILL.md负责描述这个 Skill 的用途和工具定义--- name: hello_skill description: 一个用于测试的最简技能。当用户和你打招呼、或者要求你说Hello时使用。 input_schema: type: object properties: name: type: string description: 打招呼时使用的名字可省略。 required: [] ---第二个是main.py负责真正执行逻辑#!/usr/bin/env python3 import sys import json def main(): payload json.loads(sys.stdin.read()) name payload.get(name, world) print(json.dumps({message: fHello, {name}!})) if __name__ __main__: main()保存后重启 OpenClaw让重新扫描 skill 目录。之后你在对话里问一句“打个招呼”或“say hello”如果代理正确调用了这个工具日志里会看到工具执行记录对话里也会出现 “Hello, world!” 这样的内容。这里有个非常容易忽略的点SKILL.md里的description是模型决定是否调用工具的主要依据。它写给模型看不是写给人看。要写触发条件和使用场景比如“当用户询问服务器状态、检查接口是否在线时使用”而不是写“这是一个健康检查模块”。3. 从零实现一个自定义 Agent 工具服务健康检查实战3.1 需求案例让代理检查服务是否在线下面进入正题写一个真正有实际价值的工具。我选的案例是服务健康检查代理接收一个或多个 URL返回每个地址的 HTTP 状态码和可用性。选这个案例有几个原因。第一代码简单我故意用 Python 标准库的urllib而不是requests这样你的技能目录里不需要额外装依赖放到任何环境都能跑。第二它典型地体现了“给代理一个最小权限工具”的思想你不需要给代理 shell 权限让它自己去 curl因为那会带来命令注入等风险。第三这个工具的参数里可能包含内网地址非常适合后面演示 requireApproval 动态审批。在没有这个工具的时候代理面对“检查一下某个网站是否活着”这类问题只能说自己无法访问外部网络或者干脆瞎猜。有了这个工具它就能给出真实的状态数据后续还可以基于状态码做告警、重试等更复杂的操作。3.2 编写 SKILL.md 和 main.py在~/.openclaw/skills/server_health/目录下先创建SKILL.md--- name: server_health description: 检查一个或多个URL的HTTP健康状态。当用户询问网站、服务或接口是否在线、是否可以访问、状态码是什么时使用。参数urls为逗号分隔的URL列表。 input_schema: type: object properties: urls: type: string description: 需要检查的URL列表多个URL用英文逗号分隔例如 https://example.com,https://api.example.com/health required: - urls ---然后是main.py#!/usr/bin/env python3 import json import sys import urllib.request def check_url(url: str) - dict: req urllib.request.Request(url, methodGET, headers{User-Agent: openclaw-server-health}) try: with urllib.request.urlopen(req, timeout5) as resp: return {url: url, status: resp.status, ok: resp.status 400} except Exception as exc: return {url: url, status: 0, ok: False, error: str(exc)} def main(): raw sys.stdin.read() if not raw.strip(): print(json.dumps({error: empty input})) return payload json.loads(raw) urls_str payload.get(urls, ) urls [u.strip() for u in urls_str.split(,) if u.strip()] if not urls: print(json.dumps({error: no valid urls})) return result [check_url(url) for url in urls] print(json.dumps(result)) if __name__ __main__: main()有几个细节需要提醒。第一脚本从标准输入读入 JSON 参数从标准输出输出 JSON 结果这是 OpenClaw 插件的通用约定。第二输出必须是一个有效的 JSON不能有多余的 print 日志否则解析会失败。第三错误处理要在工具内部完成而不是抛异常让框架处理因为模型需要得到一个可读的错误信息来决定下一步动作而不是收到一串堆栈。超时时间我设的是 5 秒。如果你检查的是大量 URL可以考虑用线程并发但为了示例简单这里用的串行。实际使用中如果 URL 很多代理一次调用会等很久所以最好在 description 里提示“一次不要超过 10 个 URL”。另外返回值尽量精简只保留模型需要的关键字段不要堆砌无关数据。3.3 用调试日志验证工具加载与调用写完文件后重启 OpenClaw 服务用调试模式启动比如openclaw --debug或者在配置里把logLevel设为debug。正常启动日志里会列出当前加载的 skills 和 tools。如果没有看到server_health先检查目录路径是不是在配置的skills.dirs里再检查 SKILL.md 的 frontmatter 格式是否完整。加载成功后直接在对话里输入“检查一下 https://example.com 是否在线”。如果一切正常代理会调用server_health工具并在回复中总结结果。这时候打开调试日志你就能看到工具收到的原始参数和返回的原始结果这是排查问题最有用的信息。有时候你会发现代理明明看到了工具但就是不调用。这时不要急着改代码先看日志里模型返回的内容。有些模型会把工具调用写成文本而没有走标准的 function calling这种情况和工具本身无关而是模型能力或提示词的问题后面第 5 章会展开。3.4 提高工具调用命中率的三个技巧第一个技巧是认真写 description。我见过太多人把 description 写成功能说明书比如“本工具用于检查服务器在线状态通过 HTTP 请求获取响应”。这种写法模型很难判断什么时候该调用它。更好的写法是直接写触发场景“当用户询问网站、服务或接口是否在线时使用”。模型看到这句话遇到相关提问就会自然想起这个工具。第二个技巧是控制工具粒度。如果一个 Skill 暴露 10 个工具模型选择准确率会下降。更好的做法是把相关工具拆成几个小 Skill或者把调用频率低的工具描述写得更保守让模型优先使用高频工具。这不是框架限制而是模型决策的现实规律。第三个技巧是给参数名和工具名保持一致的风格。比如工具名server_health是下划线风格参数名urls也是下划线风格别混用驼峰。模型在生成调用时很多时候直接照抄你给的参数名不一致会导致参数传错。4. requireApproval 审批钩子静态配置与动态脚本4.1 requireApproval 在工具调用链路中的位置requireApproval 位于工具调用的生命周期中间。大致流程是模型决定调用某个工具 → harness 检查这个工具是否命中审批规则 → 如果命中暂停执行向用户发起审批请求 → 用户同意后才真正执行工具 → 执行结果返回给模型。这个机制必须是强制的不能依赖模型自觉。你可以把它想成一道物理门禁代理想进机房门禁不会问“你是不是有权限”而是直接拦住请求等保安来确认。审批钩子就是那个保安的执法依据。在 OpenClaw 中requireApproval 可以通过两种方式配置。一种是静态的工具名单适合固定的、简单的场景另一种是注册一个外部钩子脚本根据工具名和参数动态判断适合规则复杂的场景。下面分别说。4.2 静态名单哪些工具必须审批如果你的需求很简单比如“凡是执行 shell 命令、写文件、发消息的工具都需要人工确认”那直接在openclaw.json里配一个名单就好{ requireApproval: { tools: [shell, file_write, server_health] } }这段配置的含义是当代理尝试调用这些工具时先暂停等用户确认。注意工具名必须和 SKILL.md 里定义的 name 保持一致。如果你写成server-health而实际是server_health审批不会触发工具会直接执行这种问题特别隐蔽排查起来也特别费劲。还有一种常见变体是配置一个布尔开关比如requireApproval: true会让所有工具都进入审批流程。我不建议在生成环境中这样做因为用户会被审批请求烦死最终变成机械式点允许完全失去安全意义。审批要用在关键的少数操作上而不是用在高频的日常操作上。4.3 动态钩子按参数规则决定审批静态名单只能按工具名判断做不到更细粒度控制。比如我们的server_health工具检查公网网站没什么风险但参数里如果出现内网地址就可能泄露内网信息或者被用来做内网探测。这种情况需要根据参数动态决定是否审批。这时就需要注册一个动态钩子脚本。在配置里{ hooks: { requireApproval: python ~/.openclaw/hooks/require_approval.py } }然后创建~/.openclaw/hooks/require_approval.py#!/usr/bin/env python3 import json import sys def is_internal(url: str) - bool: internal_markers [localhost, 127.0.0.1, 192.168., 10., 172.16., 172.17., 172.18., 172.19., 172.30., 172.31.] return any(marker in url for marker in internal_markers) def main(): payload json.loads(sys.stdin.read()) tool_name payload.get(tool_name) or payload.get(name) arguments payload.get(arguments, {}) if tool_name server_health: urls arguments.get(urls, ) if any(is_internal(url.strip()) for url in urls.split(,)): print(json.dumps({approved: False, reason: 检测到内网地址为防止内网探测风险需要人工确认后执行。})) return print(json.dumps({approved: True, reason: 无风险操作直接放行。})) if __name__ __main__: main()这个脚本的逻辑很简单只有在参数中检测到内网特征时才要求审批其他情况直接放行。它演示了一个重要思路审批钩子不仅是一个开关可以是一段完整的业务逻辑。但这里有几个坑你必须注意。第一钩子脚本的输入结构在不同版本可能不同有的版本用tool_name有的用name有的会把参数放在args而不是arguments。我写这个脚本时是先打印一次原始输入确认字段结构后再写判断逻辑。第二脚本必须在很短时间内返回结果不要在钩子里做网络请求或慢查询否则会阻塞整个工具调用流程。第三脚本一旦抛异常框架的处理方式可能是直接拒绝或直接放行如果是放行就会有安全风险。所以最好在脚本里加一个全局的 try-except出错时按拒绝处理。4.4 审批被拒后代理该怎么办当审批钩子返回拒绝后用户端会收到一个提示代理也会拿到一个拒绝反馈。这个反馈的质量直接决定后续对话走向。如果 reason 写得太模糊比如只写 “not approved”模型会不知道该怎么办反复尝试或者干脆放弃。所以钩子里给出的 reason 要给可操作的建议。比如上面例子写了“检测到内网地址需要人工确认”用户看到之后可以批准也可以要求代理换一个公网地址。如果你设计的工具涉及敏感数据还可以在 reason 里说明当前即将执行的参数是什么方便用户复核。从产品角度看审批流程的目标是让用户在最少干扰下获得最大安全。如果一个工具 10 次有 9 次都会被批准那这个工具也许不需要放在审批名单里如果一个工具 10 次有 9 次被拒绝那说明这个工具的行为预期和用户意图不匹配应该回头检查 description 是否写得太宽泛导致模型在不该调用的时候调用了。5. 常见问题与排查技巧工具加载、审批失效与模型适配5.1 工具加载不上按这个顺序排查工具加载不上是新手最常遇到的问题。按下面顺序排查基本能解决第一确认目录位置在配置的skills.dirs列表里第二确认SKILL.md的 frontmatter 有开始和结束的---字段名没有拼写错误第三看启动日志有没有关于这个 skill 的报错第四确认脚本文件有执行权限尤其是在 Linux 上。有一次我被一个问题卡了半天SKILL.md 的 description 里有个未转义的冒号导致 YAML 解析失败整个 skill 被框架静默跳过。日志里只有一行 “skip skill xxx”不仔细看根本发现不了。所以调试时一定要开 debug 日志改完配置后养成检查日志的习惯。另外有些版本的 OpenClaw 在 skill 文件变更后不会热加载需要重启服务。如果你改了代码但测试时发现行为没变先确认是否重启过不要急着怀疑代码逻辑。5.2 审批钩子不生效五个常见原因审批钩子不生效通常有几个原因。第一配置里的钩子路径用了相对路径而 OpenClaw 的工作目录和你想象的不一样导致找不到脚本。建议用绝对路径。第二脚本本身有执行权限但输出的字段名和版本期望的不一致。比如有的版本要求返回{allow: true}而不是{approved: true}字段对不上时框架可能按默认行为放行这很危险。写完钩子后一定要在一个敏感工具上试一次确认审批提示真的弹出来了。第三静态名单里的工具名和实际注册的工具名不一致。排查时直接把日志里的工具名复制过来用不要手动敲。第四钩子报错但被框架吞掉了。建议在钩子脚本里加一点日志比如把每次判断的原始输入和输出写到文件这样可以快速定位是脚本逻辑问题还是框架调用问题。第五个原因比较隐蔽钩子脚本里用了相对路径读取其他模块或配置文件导致运行环境不对。所有这些问题的通用排查思路都是先确认能拿到钩子的原始输入再逐步检查输出和返回值。5.3 本地模型调用工具效果差怎么破很多人为了隐私或成本会用本地 Ollama 或类似方案部署模型。但本地模型的 function calling 能力普遍比商业模型弱很多。典型表现是代理看得到工具但就是不调用或者偶尔调用一次参数还是错的。这时候有几个应对思路。一个思路是换一个对 function calling 支持更好的模型。社区里有人用 ccswitch 这类工具在模型之间快速切换可以用来对比测试。如果你只是在开发插件建议先用一个成熟的云模型验证工具逻辑再切到本地模型调效果否则你很难判断是工具问题还是模型问题。另一个思路是改造工具的交互方式。有些本地模型不支持标准的 function calling但你可以让代理通过一个统一入口工具来调用比如一个call_tool工具参数里带上目标工具名和参数。这种做法虽然绕但确实能在老模型上跑通一些自动化流程代价是提示词复杂度和模型理解成本都会上升。还有一个容易被忽略的点本地模型的上下文长度有限工具描述如果太长会占大量上下文影响模型其他能力。所以本地模型场景下工具描述尽量精简一个工具三行以内最好。5.4 平台对接、容器部署与版本升级的坑插件开发里还有一类问题和工具本身无关而是出在对接平台上。比如你把 OpenClaw 接到微信或其他聊天平台会话可能因为平台风控或会话残留而卡住导致审批请求发不出去或者回复位置错乱。我遇到过一种情况代理已经发起了审批请求但用户在手机上看不到任何提示过了一会儿整个会话超时。排查下来是平台侧对连续消息有频率限制不是 OpenClaw 的锅。处理方式是给插件加节流或者把审批请求合并到同一条消息里。容器部署也有一些经典的坑。比如用容器控制 Chrome 做浏览器自动化时容器里的/dev/shm太小会导致浏览器崩溃需要启动时加挂载参数把/dev/shm扩大。还有容器内网络权限如果你的工具需要访问内网资源容器网络模式要提前规划好。版本升级同样是个风险点。OpenClaw 迭代很快配置字段和 hook API 都可能变化。升级前一定备份配置和自定义 skill升级后先跑一遍你的最小验证用例。我个人习惯是把自定义 skill 单独放在 git 仓库升级前提交一次出问题可以快速回滚。最后把这几个高频问题整理成一个速查表方便你以后直接对照问题可能原因处理方式工具加载不上目录不在 skills.dirs 列表里检查配置并重启工具加载不上SKILL.md frontmatter 格式错误查看 debug 日志审批钩子不生效钩子路径是非绝对路径改用绝对路径审批钩子不生效返回字段与版本不匹配打印原始输入确认字段代理不调用工具description 写得太模糊重写触发条件本地模型调用差模型 function calling 弱切换模型或改文本协议审批请求发不出去平台消息频率限制加节流或合并消息我在实际开发中最深的体会是插件开发的核心不在于代码写得多花哨而在于你多了解代理的决策逻辑。工具的 description 写得好审批规则设计得清楚比任何复杂的技术都管用。最后再分享一个小技巧每次改完插件我都会在日志里确认工具注册和审批触发两个节点确认无误再进对话测试这能省掉大量反复试错的时间。如果你正准备动手建议从一个小而清晰的工具开始先跑通加载、调用、审批这条链路再逐步扩展更多能力。