☰
OpenClaw技能开发实战:用Python/TypeScript构建生产级业务逻辑
2026/10/7 5:19:03 网站建设 项目流程

刚接手 OpenClaw 那会,我跟大多数人的心态一样:先把它当成一个功能更全的工具箱,内置技能要聊天有聊天、要搜索有搜索、读写文件也方便,听着挺够用。但真正把业务逻辑往里塞的时候,问题立刻就来了——多步判断、多条规则组合、依上一个接口返回结果再决定下一步的动作,这类东西没法靠"配置一个动作"解决。折腾到后面我才意识到,OpenClaw 真正的玩法是它的脚本运行时:用 Python 或 TypeScript 写 Skill,把任何能写进代码的逻辑挂上去。这篇文章就是我踩完一遍坑之后的完整记录,适合那些已经跑通 OpenClaw 基础功能、开始嫌"工具不够用"的人。

1. 先想明白:OpenClaw 的技能不是"插件",是"脚本运行时"

1.1 内置工具解决高频简单操作,技能解决低频复杂操作

很多人把 OpenClaw 的内置能力理解成一个固定功能列表,这个理解没错,但容易让人走弯路。内置工具覆盖的是高频、通用、结果相对固定的操作,比如发一条消息、查一次天气、抓一个网页标题。它们的共同点是:输入简单、输出简单、不需要状态。

但真实业务不是这样的。举一个我这边最典型的例子,客服场景里的"退货审核"。一次退货请求要判断的维度包括订单是否在 7 天无理由期内、用户会员等级、商品是否拆封、当前库存是否需要回仓、历史退货率是否异常。这五六个条件单独拎出来都很简单,合在一起就需要循环、分支、中间变量、甚至要查两三个外部系统。内置工具不可能为这种场景单独开一个接口,它只提供一个通用的执行通道,具体怎么算,必须由你自己写代码决定。

所以我把 OpenClaw 的技能理解为"脚本运行时"而不是"插件":插件通常有固定的能力边界,而脚本运行时的边界是语言本身。能写进 Python 或 TypeScript 的逻辑,就能成为 OpenClaw 的一个技能。这个认知转变很关键,它会直接影响你后续怎么设计技能。

1.2 为什么是 Python 和 TypeScript,而不是某种私有 DSL

我第一次看到 OpenClaw 把 Python 和 TypeScript 设为主要技能语言时,其实松了一口气。如果它搞一套私有 DSL,那意味着每次写逻辑都要查文档、学语法,而且社区生态帮不上忙。Python 和 TypeScript 的好处在于,你需要的绝大多数库都是现成的。

Python 强在数据处理、算法表达、机器学习相关能力。比如你要写一个"根据历史订单聚类判断用户消费习惯"的技能,numpy、pandas 直接拿来用,不需要从零造轮子。TypeScript 强在异步编排和前端生态,尤其是跟无头浏览器、网页自动化、Node 生态的工具配合时,天然顺畅。

有人可能问:为什么不用 Bash 或者 Go?答案很简单——生态和心智负担。Bash 处理简单文本没问题,一旦遇到 JSON 嵌套和复杂流程就非常痛苦;Go 性能好,但开发效率和生态丰富度在"快速实现业务逻辑"这个目标下不如 Python/TS。在 OpenClaw 的场景里,我们更多的是要"快速、正确、可维护",而不是把单次执行压到极致性能。

1.3 判断"该不该写技能"的三个标准

技能虽然强大,但不等于所有东西都应该写成技能。我在团队里定过三条判断标准,命中任何一条就可以写,否则先用内置工具解决:

第一条,逻辑是否可以重复执行。一次性临时操作不值得变成技能,因为你维护它的成本大于收益。第二条,是否有多步条件和外部依赖。如果流程里包含"根据 A 的结果决定是否调用 B,B 失败要降级到 C"这类结构,内置配置通常表达不了。第三条,失败时是否需要明确的降级策略。内置工具往往是"成功了返回结果,失败了报错";但业务逻辑需要的是"失败后怎么补偿",这只有用自己的代码才能控制。

这套标准帮我避免了一个常见误区:把技能数量做成 KPI,结果写了一堆没人调用、Agent 也分不清楚的垃圾技能。

2. 环境搭建:Companion、WSL2、本地模型,一个都不能少

2.1 Windows 下推荐的技能开发环境

我在 Windows 上开发 OpenClaw 技能,用的是 Windows Companion + WSL2 的组合。Companion 负责把 Windows 桌面端和底层的 WSL2 运行时串起来,技能脚本实际跑在 Linux 环境里,这样依赖安装方式跟服务器一致,不会出现"本地能跑、部署到服务器报错"的经典问题。

环境清单我建议至少包含这几项:

  • Windows 10/11 的 WSL2 功能,内核保持在最新状态
  • 一个默认的 WSL 发行版,我用的是 Ubuntu 22.04/24.04
  • Node.js 和 Python3,版本分别建议 18+ 和 3.10+
  • OpenClaw 本体和 Windows Companion 保持同一版本线
  • Git,用来管理技能目录

这里面最容易忽略的是"默认发行版"。如果你的机器上装了多个 WSL 发行版,OpenClaw 会认默认那个,如果你压根没设置过默认发行版,后续会出现各种奇怪问题。所以环境搭建第一步,先把默认发行版定下来。

2.2 "无法安全验证 WSL2 环境"的排查链路

我在查资料时经常看到有人卡在"OpenClaw 无法安全验证 WSL2 环境"这个报错,包括我自己第一次也在这上面花了不少时间。这个提示看着很吓人,其实排查链路很清晰,按顺序走就行。

先打开 PowerShell,运行wsl --status和wsl --version。wsl --status输出里最关键的是"默认版本"这项,它应该是 2。如果显示默认版本是 1,或者根本看不到发行版信息,说明 WSL 配置不对。接着运行wsl --update更新内核,再运行wsl --set-default <你的发行版名>指定默认发行版。

如果状态正常但 OpenClaw 仍然校验失败,那大概率是权限不一致导致。Windows Companion 需要以管理员权限启动,但 OpenClaw 本体如果是以普通用户启动,两边会话权限不一致,校验会失败。反过来,Companion 普通启动,OpenClaw 管理员启动,也可能出问题。我的做法是两个都固定在管理员模式下启动试一次,能通再降回来。

还有一种情况是 WSL 里残留了旧版配置,比如手工改过.wslconfig或者从 WSL1 升级过来的老环境。这时候不要心疼,直接检查.wslconfig里有没有明显不兼容的配置项,没有把握就清空该文件重启 WSL。运行wsl --shutdown再重新进,很多幽灵问题就消失了。

2.3 把 Ollama + Qwen2.5 接到 OpenClaw

本地模型接入是很多人想绕开云端 API 的理由,我的方案是 Ollama 拉一个 Qwen2.5 3B 模型,然后在 OpenClaw 的模型配置里把 Provider 指向 Ollama 的本地接口。这样数据不出机器,调试技能时响应也快。

但这里有个坑:本地小模型的上下文窗口和推理能力都有限,它理解复杂技能描述和参数的能力比云端大模型弱。所以我在技能设计上做了妥协——技能的参数描述写得更机械、更明确,避免让模型去猜;返回的结果尽量精简,只回结构化摘要,不回一长串原文。

如果你在安卓上折腾过 Termux 版 OpenClaw,我的建议是:可以玩,但别在手机上做技能开发。手机上资源有限,跑小模型加脚本调试体验很差,手机端更适合做"只调用、不开发"的远端入口。

3. 第一个 Python 技能:把一条真实业务规则跑通

3.1 技能目录与入口文件约定

OpenClaw 的技能本质上就是一个目录,目录里放描述文件和脚本文件。我习惯的结构是这样:

skills/ return_review/ SKILL.md script.py requirements.txt tests/ sample_params.json

SKILL.md 是给大模型看的说明书,它决定了模型什么时候调用这个技能、传什么参数。这里我必须强调:描述文件写得越具体,调用准确率越高。不要只写一句"检查退货",要把触发条件、入参、出参都写清楚。

script.py 是真正的执行逻辑。不同版本文档对文件名的约定可能有差异,但核心思想一致:OpenClaw 会把参数传给脚本,脚本执行完把结果打印出来。我自己的技能里,入口函数只做三件事:解析参数、调用业务逻辑、格式化输出。

3.2 输入输出契约:少用环境变量,多用参数

我见过不少新手技能把外部状态塞在环境变量里,比如把订单号写进环境变量,然后从环境变量读。这不是不行,但会埋下隐患——技能是要被 Agent 动态调用的,它只知道该传什么参数,不可能替你先设置好环境变量。正确的做法是,所有业务输入都走参数。

我推荐在脚本里定义一个run(params)函数,外部通过--params传入 JSON 字符串或文件路径。这样写有一个直接好处:本地测试时我可以手工构造参数文件,不经过 OpenClaw 直接跑脚本,调试效率翻倍。

举个例子,退货审核脚本的入口大概这样:

import json import sys def run(params: dict) -> dict: # 业务逻辑写在这里 pass def main() -> None: if len(sys.argv) < 3 or sys.argv[1] != "--params": print(json.dumps({"ok": False, "error": "missing params"})) return params = json.loads(sys.argv[2]) result = run(params) print(json.dumps(result, ensure_ascii=False)) if __name__ == "__main__": main()

这个骨架看着简单,但它保证了脚本既能被 OpenClaw 调用,也能被python script.py --params sample.json直接执行。

3.3 复杂判断逻辑的代码组织

退货审核这类规则型业务,最容易写成一坨 if-else 嵌套。我在第二次重构这个技能时,把所有规则拆成了独立函数,每个函数只负责一条规则,返回通过/不通过以及原因。

def is_within_window(order_time, days=7): ... def is_vip_user(user_level): return user_level >= 3 def is_sealed(sealed): return sealed is not None

业务逻辑部分,规则之间是"并且"关系就顺序判断,只要有一个不通过就短路返回;是"或"关系就单独处理。实测下来,这种写法比把二十行 if 挤在一起好维护得多,而且每个函数都可以单独测试。

复杂逻辑还有一个隐藏问题:入参不规范。用户通过自然语言转过来的参数,很可能缺字段、字段类型错误、甚至时间格式不对。我通常在脚本开头做一层校验,缺参数直接返回错误信息,而不是让脚本在中间某个位置抛异常。

required = ["order_id", "user_level", "order_time", "sealed"] missing = [k for k in required if k not in params] if missing: return {"ok": False, "error": f"缺少参数: {missing}"}

这一步看似简单,实际能省掉大量排查时间。技能脚本一旦在运行到一半时崩掉,日志里只有 traceback,Agent 又看不懂,最后还是要靠人翻日志。

3.4 本地测试闭环:先跑脚本,再挂 Agent

我的工作流程分两步。第一步完全脱离 OpenClaw,写一个sample_params.json,里面放形态尽可能多样的参数,直接执行脚本跑出结果。第二步才把脚本挂到 OpenClaw 上,通过对话触发,看 Agent 是否正确理解技能描述、正确传参。

挂上去之后如果发现模型不调用技能或者传错参数,问题通常不在脚本,而在 SKILL.md 的描述。这时候要调整描述里的触发条件和参数说明,而不是去改代码。

这个"先脚本、后 Agent"的顺序,是我踩过几次坑之后总结出来的。最开始我总想着直接端到端测试,结果一个问题冒出三个嫌疑点:脚本 bug、描述不清、模型调用异常,全混在一起,排查成本极高。拆开之后,每一层都能单独验证。

4. TypeScript 技能:用类型和异步把复杂链路理顺

4.1 TS 技能最适合解决哪类问题

Python 技能解决的是"计算密集型"业务逻辑,TypeScript 技能在我这边主要解决"编排密集型"逻辑。什么叫编排密集型?就是一个流程里要连续调用多个外部服务,每次调用都是异步的,中途还要处理超时、并发、串行。这类逻辑用 Python 写也不是不行,但 Node/TS 生态里的工具更顺手。

还有一个现实原因:很多网页自动化、无头浏览器、数据抓取工具,Node 生态的封装比 Python 更好用。我后面集成 Playwright 时,就是直接用 TypeScript 写的技能,避开了 Python 版浏览器安装和依赖兼容的麻烦。

4.2 类型声明文件怎么用才不白写

搜 TypeScript 技能相关问题时,很多人卡在 d.ts 声明文件上。我的观点是:在 OpenClaw 技能里,类型声明不是为了取悦编译器,而是为了让"数据契约"显性化。

复杂业务逻辑最容易出错的地方不是算法,而是数据结构的隐性约定。比如某个技能返回的字段叫dataList,另一个技能接收的字段却叫items,两边靠文档对,迟早出问题。写一个类型声明文件,把这些结构固定下来,比写十行注释都管用。

我习惯在技能目录里放一个types.ts,把所有入参和出参的接口定义清楚。接口继承在真实场景里非常常用,比如:

interface BaseContext { requestId: string; userId: string; } interface OrderContext extends BaseContext { orderId: string; amount: number; } type ReviewResult = | { ok: true; allow: boolean; reason: string } | { ok: false; error: string };

这里用联合类型表达"成功和失败是两种不同结构",比用一个把所有字段都设为可选的接口严谨得多。定义好之后,业务代码里不用频繁猜字段,这也降低了后续维护成本。

4.3 多个技能组合:编排者模式

我一度把每个业务都写成一个聚合技能,后来发现代码大量重复,于是改成"原子技能 + 编排技能"的组合。原子技能负责单一能力,比如"查库存""查会员等级""查历史退货率";编排技能负责顺序调用这些原子技能,汇总结果。

编排技能用 TypeScript 写最舒服,因为异步操作可以用Promise.all做并发,也可以按依赖关系做串行。比如退货审核,查基础信息三个接口互相独立,用并发同时发出去;查完再进入规则判断阶段,就必须串行。

这里要特别注意并发控制,如果一个编排技能发出几十个并发请求,很容易打爆外部接口限流。我一般用p-limit这类库限制并发数,或者自己写一个简单的信号量,把并发控制在接口允许的范围。

4.4 集成 Playwright 做网页自动化的实战注意点

用 TypeScript 技能驱动 Playwright,我做过一个典型需求:每天早上打开内部运营后台,自动巡检一批配置是否生效,异常项目汇总成 JSON 返回。

整套流程代码结构不复杂,但有几个坑值得说。第一,不要在每次调用技能时都启动一个新的浏览器实例,启动成本非常高,还会让调用延迟从几百毫秒变成好几秒。我的做法是让浏览器实例常驻,技能只负责控制页面跳转和数据提取。第二,无头浏览器模式下,某些页面元素加载时机不稳定,不要用固定 sleep,尽量用waitForSelector这类显式等待。第三,脚本结束一定要把浏览器页面关闭,只保留浏览器实例,否则内存会被吃穿。

这些经验不是看文档得来的,是跑了几周自动化之后被内存告警逼出来的。

5. 迈向生产:超时、幂等、日志、错误返回一个都不能省

5.1 外部依赖的超时重试模板

技能一旦接进生产流程,外部接口就不可能永远稳定。我会给所有外部调用包一层超时和重试逻辑,模板大致长这样:

import time def call_with_retry(fn, max_retries=3, timeout=5): last_exc = None for attempt in range(max_retries): try: return fn(timeout=timeout) except Exception as exc: last_exc = exc time.sleep(1.5 ** attempt) raise last_exc

1.5 ** attempt是指数退避,第一次失败等 1.5 秒,第二次等 2.25 秒,避免重试风暴。另外,超时时间一定不能设成"感觉应该够",要实际测量接口 P95 延迟,然后在此基础上加一点余量。

5.2 幂等键:重复执行不等于重复下单

生产环境最怕的不是脚本报错,而是脚本因为超时被重试,结果外部系统收到了两次请求,产生了重复订单或者重复扣款。解决方案是幂等键。

调用外部写操作接口时,把requestId作为幂等键传过去。如果外部服务支持幂等,它会识别相同 requestId,直接返回第一次的结果。如果不支持,你在自己的状态存储里记录 requestId 和结果,重试时先查记录,命中就直接返回旧结果。

这个习惯只花十分钟写进代码,但能避免的事故可能是灾难级的。我自己曾因为漏了这个,让一个统计任务重跑了一遍,结果报表数据翻倍,排查了整整一个下午。

5.3 日志要能串成链路

技能内部日志不能只 print 一个"success"就完事。至少要有两层:关键节点日志和外部调用日志。关键节点包括"参数校验通过""规则 A 命中""调用了 XX 接口""返回最终结果";外部调用日志要记录目标地址、入参摘要、耗时、状态码。

我习惯把requestId从 OpenClaw 传入的参数里拿出来,放进每条日志开头,这样一次技能调用的所有日志可以被串起来。排查问题时,先按 requestId 过滤日志,再定位具体失败环节,比在海量日志里盲搜高效太多。

5.4 错误信息是给 Agent 看的,不是给人看的

这一点可能是整个技能开发里最反直觉的经验。OpenClaw 里技能返回的错误信息,第一读者是 Agent 大模型,它会根据错误信息决定下一步动作。所以错误信息不能写"第 34 行 KeyError: xxx"这种程序员语言,而要写成"缺少参数 order_time,格式应为毫秒时间戳"这种能指导 Agent 修正调用方式的信息。

我在脚本里把所有校验失败都返回结构化错误信息,并附上"你可以尝试补充 XX 参数后重试"的提示。实测下来,Agent 的自纠错能力会明显提升,很多错误不需要人工介入,模型自己就会修正参数重新调用。

5.5 更新与发布:别用"就地改文件"当部署流程

技能更新看起来简单,改完文件就生效。但生产环境里,就地改文件最大的问题是不可回滚。我跟团队定过一条纪律:技能目录用 Git 管理,每个技能目录里放一个版本号字段,更新走"改代码 -> 本地测试 -> 提交 -> 触发重新加载"。每次发布前在备份目录留一份上一个版本,万一新逻辑有问题,能秒级回退。

OpenClaw 对技能文件会有缓存,你改了文件但没触发重新加载,跑的还是旧代码,这种问题光靠肉眼很难发现。我的排查办法是,在脚本开头把当前版本号打印到日志,确认这次跑的是哪一版。

6. 我一直用的技能库组织方式,以及哪些情况别写技能

6.1 技能目录命名与职责分层

技能多了以后,最大的痛点不是写代码,而是让 Agent 能准确找到该用哪个技能。我的组织方式是把技能分成三层:原子技能、组合技能、入口技能。原子技能只做一件事,命名用"动词+对象",比如query_inventory、get_user_level;组合技能是原子技能的编排,命名用"业务目标",比如review_return_request;入口技能是暴露给 Agent 的最终能力,描述写得最详细。

分层之后,Agent 的调用逻辑会清晰很多。它需要的输入输出还是那些,但命中率明显提高,重复技能也少了。另外,我严格执行"一个技能目录只放一个技能"的原则,不搞目录里堆十几个函数然后靠参数区分,那会让描述文件无法写清楚。

6.2 什么时候坚持用内置工具

写了两三个月技能之后,我的结论是:能用内置工具解决的,绝对不写技能。原因很简单,技能是要长期维护的,每当 OpenClaw 升级、依赖变动、接口调整,所有技能都有可能需要跟着改。内置工具由平台维护,我不需要为它的生命周期负责。

所以我在设计需求时,先问自己一句:"平台内置能力真的表达不了这个需求吗?"很多时候答案是"能",只是不那么优雅。不优雅没关系,稳定和少维护更重要。

6.3 什么时候干脆另起服务

技能最适合的场景是"短平快"业务逻辑,输入参数,执行,返回结果,整个生命周期控制在秒级甚至亚秒级。但如果你发现一个业务需要持续跑很长时间、有大量后台调度、需要独立数据库,那它已经不适合做成技能了,应该另起一个独立服务,然后给 OpenClaw 留一个接口调用入口。

我踩过这个坑:把一个数据同步任务写成技能,每次调用要跑十几分钟,期间技能进程挂着,Agent 一直在等,超时之后又重复触发,整个任务并发了好几份,数据都被写乱了。后来改成独立服务加任务队列,技能只负责"提交任务+查询状态",这个问题才彻底解决。

所以,技能不是越重越好,它的定位永远是 OpenClaw 能力边界内的一块弹性垫片,用来补足那些"有逻辑但又不值得做成系统"的场景。我在实际使用中最深的体会是:一个稳定的技能库,核心不是代码写得多花哨,而是边界划得足够清楚——哪些交给模型、哪些交给脚本、哪些交给外部服务。把这个想明白了,OpenClaw 在你手里就不再是"工具不够用",而是真的有求必应。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询