
如果你跟我一样第一次打开 openclaw没错就是经常被大家喊“小龙虾”的那个项目的文档时第一反应大概是我装好了、聊天也通了但自定义 skill 到底怎么加文档翻来覆去就一句“创建目录、写个描述文件模型会自动调用”剩下的全靠猜。更麻烦的是我平时写代码习惯在 PyCharm 里完成遇到新技能没生效、参数没传进去、断点打不上这种问题时只能去翻日志。这篇文章就是我折腾 pycharm 开发 openclaw skill 的完整记录包含我最终采用的目录组织思路、三版技能迭代的真实过程以及几个我确认过根因的高频坑。不是官方文档复述是拿“做一个笔记整理技能”当例子一步步从零跑到能稳定复用。1. skill 在 openclaw 里到底是个什么先把模型“用技能”的过程看懂在给 PyCharm 配环境之前我花了很多时间搞明白一件事openclaw 不是一个普通的脚本调度器它内部有一个“读你的说明—帮你填参数—执行你的代码—把结果带回对话”的闭环。如果你不理解这个闭环后面加技能就会陷入“写完了但永远不被调用”的玄学状态。1.1 技能不是插件而是“会说话的脚本”我第一次参考别人的 skill 写法时以为它类似 IDE 插件写个入口、注册一下就行。实际用过之后才发现openclaw 里的 skill 更像是一份“带简历的脚本”——脚本负责干活简历负责让大模型知道“你什么时候该用它、参数该怎么填”。这里的关键点在于openclaw 本身不硬编码技能的调用条件它把选择权交给了模型。用户说了一句“帮我把这段网页内容整理成笔记”模型看到你定义过一个叫 note_tidy 的技能描述写的是“把剪贴板或网页正文整理成结构化 Markdown”并且参数列表很清晰地说明了 content 和 tags 的含义它才会决定调用。所以一个 skill 好不好用代码占一半那几十行描述文字占了另一半。我用了一个很笨但很有效的办法验证这件事把技能描述里的关键词从“网页正文”改成“旅游攻略”再去问它“帮我整理这段开发文档”模型立刻就不会再调用这个技能了。别小看这个实验它直接告诉我——你在 skill.md 里写的每一个字都会影响模型在真实请求里的判断。1.2 一条 skill 的“完整简历”目录结构长什么样我在自己的工作目录里建了一个专门放技能的文件夹约定如下~/openclaw-workspace/ skills/ note_tidy/ skill.md run.py requirements.txt notes/ openclaw.yamlskill.md 是这个技能的“对外简历”run.py 是真正的实现。我把 skill.md 分成两大部分上面是 YAML front-matter下面是给模型看的自然语言补充说明。--- name: note_tidy description: 整理剪贴板或网页正文片段去掉多余空行和噪音文字输出结构化 Markdown 笔记。 arguments: - name: content type: string required: true description: 待整理的原始文本内容通常来自剪贴板粘贴 - name: tags type: array required: false description: 可选的标签列表会写入笔记的标签行 output: type: string description: 返回整理后的 Markdown 文本 --- 当你收到一段带有大量空白、重复标题、或者明显属于网页导航噪音的文本时应该优先使用这个技能。 处理原则保留标题结构压缩连续空行去掉 URL 和脚注把最核心的正文段落按二级标题重新组织。最开始我懒得写补充说明以为 front-matter 就够了。后来发现模型在复杂任务里经常拿不准“这算不算网页噪音”补充说明其实就是它的判断依据。用大白话把场景边界写出来比堆砌一大堆形式字段有用得多。1.3 从“一句话请求”到“脚本执行完毕”的调用链路我在 PyCharm 终端里跑 openclaw 时开着 debug 日志观察过一次完整链路大体分四步用户请求进入对话模型先判断是否需要技能如果需要就从技能清单里找描述最匹配的那一个。模型根据 arguments 定义把用户原话里的信息填进参数。比如“帮我把这段整理一下加个标签python”会被填成 content原文、tags[python]。openclaw 把参数序列化后交给技能进程运行 run.py捕获标准输出。标准输出内容作为技能结果返回给模型模型再基于这个结果生成最终回答。看明白这个链路后我意识到了两个之前一直没想通的问题为什么技能脚本的 print 输出莫名其妙消失因为如果你在脚本里把日志也 print 出来了模型会把日志当成最终结果看起来就像“技能没干活”。为什么技能一直没被调用很可能不是脚本有问题而是描述里缺少触发词。这些理解也直接决定了我后面在 PyCharm 里的调试方式不是先写完整代码再试而是先把技能壳子跑通再逐步加内容。2. PyCharm 环境准备的顺序与三处必改配置openclaw 本身是命令行优先的工具PyCharm 只是我用来写和调试的壳。用 PyCharm 最大的好处是有变量面板、能打断点但如果你直接拿默认配置去跑大概率会遇到“终端好好的Run 配置一跑就挂”的怪事。2.1 用专用虚拟环境隔离运行时别偷懒我第一次直接把 openclaw 装进了某个大项目同一个环境里结果那个项目升级依赖时把 openclaw 的某几个包顶掉技能脚本突然就跑不起来了。排查了半天才发现是依赖冲突。现在我的固定做法是给 openclaw 单独建一个虚拟环境python -m venv ~/openclaw-workspace/.venv source ~/openclaw-workspace/.venv/bin/activate pip install -U openclaw注意我提到的 openclaw-workspace这个目录既是代码工程也是技能仓库。PyCharm 里新建项目时直接选 Existing environment解释器指到.venv/bin/python这样依赖就锁在项目内部不跟其他环境交叉。还有一个细节如果技能脚本需要额外的第三方库不要直接 pip install 到全局而是写进对应技能目录的 requirements.txt然后再安装。好处是以后换机器时skills 目录整个拷走就能恢复环境不会漏依赖。2.2 配置文件里的三处必改项模型端点、技能路径、日志级别openclaw 初始化之后会生成一个 openclaw.yaml我几乎每次新环境都要改三个地方。第一是模型端点。openclaw 默认配置指向的是本地推理服务地址如果你像我一样使用一个兼容 OpenAI 协议的远端模型服务需要把 model 配置里的 base_url 和 api_key 替换成你自己的。我就是在这里踩过坑只改了模型名没改 base_url结果框架一直尝试连接本地端口连接超时后才报错。第二是技能目录路径。把 skills_dir 指到工作区里的 skills 文件夹skills_dir: ~/openclaw-workspace/skills这个改完以后最好重启一次 openclaw 的交互进程因为技能索引是在启动时扫描的。第三是日志级别。排查阶段我直接把日志开到 DEBUGlog_level: DEBUGDEBUG 日志会显示模型到底有没有选中某个技能、参数填充成什么样了、脚本以什么退出码结束。看日志虽然啰嗦但遇到玄学问题时它是唯一可靠的信息源。2.3 先别接模型用一条“最小技能”跑通全链路在正式写笔记整理技能之前我建议你先做一个 hello_world 级别的技能目的不是验证功能而是验证“PyCharm 终端 → openclaw → 技能脚本”这条路通不通。我当时的做法是写了一个 10 行脚本接收 content 参数然后原样返回前面加一行前缀。skill.md 里的描述写得非常窄“无论收到什么文本都返回固定前缀 原文本”。这样模型在对话里必然选中它。跑通以后再让 PyCharm 的 Run Configuration 用虚拟环境里的 python 执行-m openclaw chat确认终端和 IDE 两种启动方式都正常。这一步值得耐心做。很多朋友一上来就写复杂技能结果链路本身没通后面所有调试都变成猜谜。3. 从零写一个“笔记整理”技能三版迭代的完整过程现在进入正题。我拿“笔记整理”这个 skill 当例子展示它从“能跑”到“真正好用”的三次迭代。每个版本我都会说清楚我当时为什么改以及改了之后解决了什么问题。3.1 第一版能读输入并输出稳定结果第一版我追求的是“链路最短”只做三件事读 JSON 参数、提取 content、把空行压缩后原样输出。import json import re import sys def main(): payload json.loads(sys.stdin.read()) content payload.get(content, ) cleaned re.sub(r\n\s*\n, \n\n, content) result { output: cleaned.strip() } print(json.dumps(result, ensure_asciiFalse)) if __name__ __main__: main()这里有一个我在第一版就定下的约定脚本统一从标准输入读 JSON往标准输出写 JSON。这样不管 openclaw 内部怎么调度我的脚本对外接口稳定调试也容易。日志一律写 stderr。你可能注意到我还没做任何 Markdown 结构化因为第一版的目标只是确认一件事模型能选中技能openclaw 能把 JSON 正确传进来脚本能跑完并返回结果。跑通之后我马上进入第二版。3.2 第二版输入校验和输出结构让模型更容易“配合”第一版跑通后我发现了新问题模型传过来的 tags 有时是字符串有时是数组content 偶尔是 None。如果我不做校验脚本就会报错openclaw 会把异常堆栈返回给模型模型又开始胡猜。第二版我加了比较完整的参数清洗逻辑。def normalize_tags(raw): if raw is None: return [] if isinstance(raw, str): return [raw] if isinstance(raw, list): return [str(t).strip() for t in raw if str(t).strip()] return []同时我规定了输出结构里的固定字段output 是正文extra_meta 里放附加信息。这个结构不复杂但让模型在后续对话里有东西可引用。它说“我已经把笔记整理好了”的时候可以顺便引用 extra_meta 里的标题或者字数信息交互体感自然很多。输出结构我在 skill.md 里也同步写明了--- output: type: object description: 返回 JSON 对象包含 output 正文字段和 extra_meta 元信息字段 ---这一版改完技能在普通对话里已经不容易出错了。但是我很快就遇到一个更实际的问题光把整理结果返回给对话我最后还得手动复制出来存文件非常难受。3.3 第三版把结果安全地写入本地 Markdown 文件第三版我做了一个关键改动如果调用时传了 save 参数技能会把整理结果写到 notes 目录下并且返回文件路径而不是把全文都塞回对话。这对大段笔记来说体验好很多聊天窗口不会刷屏文件也真正落盘了。def save_note(content: str, tags: list[str], notes_dir: Path) - Path: notes_dir.mkdir(parentsTrue, exist_okTrue) safe_title re.sub(r[^\w\u4e00-\u9fff-], _, content[:30]) filename f{datetime.now():%Y%m%d_%H%M%S}_{safe_title}.md filepath notes_dir / filename header tags: , .join(tags) \n\n filepath.write_text(header content, encodingutf-8) return filepath写入这一步带出一个很容易被忽视的问题文件名的安全清洗。如果直接用内容前 30 个字做文件名很可能出现斜杠、冒号之类的字符在 Windows 上直接报错。我用了正则把非单词字符替换成下划线同时保留中文。第三版完成后这个技能的使用体验才算真正闭环我复制一段网页文本让模型调用技能几秒钟后 notes 目录里出现一个带标签的 Markdown 文件我只需要打开编辑器微调即可。我在这里忍不住多说一句一个技能值不值得做取决于它能不能缩短你重复操作的路径。如果整理完还要手动复制、手动命名、手动归档那一开始就用普通脚本处理好了没必要做成 openclaw skill。4. PyCharm 调试时的线程模型与三个高频坑这一章写给已经能在 PyCharm 里跑 openclaw 但遇到“断点不生效”“技能不刷新”“环境变量对不上”等问题的朋友。这三个问题我都实际遇到过也确认了根因排查过程可以复现。4.1 断点为什么“停不住”理解 openclaw 的执行线程我在技能脚本的 main 函数第一行打了断点点 Debug 以后终端正常输出结果但断点就是灰着不进来。我一度以为是 PyCharm 坏了后来看了调用栈才明白——openclaw 在独立线程里调用技能脚本而 PyCharm 默认的挂起策略是只挂起当前线程。在 Debug 模式里点击运行配置右侧的“View Breakpoints”找到 Python 的断点设置把挂起策略改成挂起所有线程或者干脆在断点属性里勾选“Suspend all threads”。这样技能在新线程里执行时断点才会真正把进程停住。另一个更省心的替代方案我在技能脚本底部留了一个独立入口用于绕过 openclaw 直接模拟调用。写完技能后我先用一小段假 JSON 喂给脚本确认逻辑正确再跑进 openclaw 里做集成验证。两件事分开调试效率反而更高。echo {content: 测试文本, tags: [python]} | python run.py4.2 高频坑一新技能写了但一直不生效多半是索引缓存症状非常典型我在 skills 目录里新增了一个 skill 文件夹打开 openclaw 对话问它“现在有哪些技能”它报到的还是旧清单。重启终端也不行旧技能依然在那儿。查了源码日志后确认openclaw 在启动时会扫描技能目录并生成一份索引缓存。如果你用的是服务模式它不会每次对话都重新扫磁盘。解决方式很简单跑一下索引重建命令openclaw skills index --rebuild如果命令不确定直接重启 openclaw 的服务进程也能达到目的。我的习惯是新增或修改 skill.md 后先重建索引再打开对话省得白等。这里要特别提醒的是只改 run.py 不需要重建索引因为索引只记录描述和参数不读脚本内容。搞清楚这条边界你就不会每次改代码都去傻傻重建一遍了。4.3 高频坑二PyCharm 的 Run 配置里环境变量不生效这个坑困扰了我最久。在 PyCharm 的终端里openclaw 命令能正常找到配置文件也能访问模型端点但用 Run/Debug Configurations 直接跑同样的命令它就像失忆了一样连模型 API key 都找不到。根因是PyCharm 的 Run 配置默认不会完整继承 shell 里的环境变量尤其是我在.bashrc里 export 的那些 OPENCLAW_HOME、API_KEY 之类。终端能跑是因为 shell 启动时加载了这些变量Run 配置不是通过 shell 启动的。解决方案有两条。简单粗暴的做法在 Run Configuration 里的 Environment variables 一栏手动把需要的变量补上。我更推荐的做法是在 openclaw.yaml 里直接把配置写全代码里不要依赖环境变量。因为配置文件是 openclaw 自己读取的跟 PyCharm 的环境变量继承机制无关。把模型端点、技能目录、默认输出目录都放进配置文件后两边启动方式终于完全一致了。4.4 高频坑三技能输出内容被模型“篡改”其实是 I/O 协议问题有一次技能明明返回了一段结构很完整的 Markdown结果对话里展示出来的却少了后半截还多了一句模型自己加的“根据整理结果我认为……”。我一开始以为是模型输出不稳定后来给技能脚本加了校验才发现问题出在我把日志也 print 到了 stdout。openclaw 会把技能的标准输出整体当作结果传给模型我脚本里顺手 print 的那条“完成时间 0.3s”也被当成了结果的一部分。模型看到这段奇怪的文字就自作主张解释了一句于是最终回复变成了原结果模型注释的混合体。从那以后我的脚本统一遵守一个纪律stdout 只输出给模型看的最终 JSON所有调试信息一律走 stderr。如果你想确认技能脚本本身有没有问题不要靠对话里的回复判断直接看 PyCharm 的 Run 窗口里 stderr 的输出。5. 技能粒度怎么定一个让我少改很多次的经验这个章节算是我个人折腾 openclaw 这么久之后最想写给后来者的一段话。前面讲的都是具体操作但真正决定一个技能好不好维护的是技能的粒度。最开始我写技能时喜欢“大而全”比如这个笔记技能我当时想把抓取、清洗、去重、格式化、归档全塞进一个脚本里。结果每次需求变一点我就得改一大段代码而且模型在判断“该不该调用”时也容易糊涂因为描述里面塞满了各种功能词反而抓不住核心场景。后来我一个做智能体开发的朋友点醒了我openclaw 的 skill 应该像函数一样每个只做一件明确的事而不是像服务一样包罗万象。我重新拆了一下把笔记整理拆成“清洗文本”和“写入文件”两个技能模型根据用户意图选其中一个或者串起来用。技能描述也变得非常集中命中率反而高了。这里还涉及一个判断技巧如果某个技能连续几次被模型误调用不要急着改代码先看看它的 description 是不是包含了太多场景边界。我在 note_tidy 的描述里曾经写了“也可以用于英文笔记”结果英文内容统统被送进来而我脚本里的正则清洗规则只针对中文网页做得比较完善。把描述改回单一职责后误调用就消失了。如果你想在这条路上继续深入还可以考虑给技能增加一个“dry_run”参数让模型先返回处理结果但不落盘确认之后再加 save 参数实际写文件。这样既保留了智能体的便利性又不会因为模型理解偏差产生一堆垃圾笔记。我最终的技能虽然只简化成“清洗并输出”“清洗并保存”两种形态但它在我的日常工作里每天都会用。工具的复杂度不是目的能把一个高频动作稳定地缩短成一句话就已经值回折腾的时间了。