跳至内容

Skill 开发实践:skill 配置化以适配更多的环境

一、背景与动机

最近我一直在用 AI Agent(CodeBuddy)辅助写博客。之前在 OpenClaw 平台上搭建了一个 blog-writer Skill,可以自动完成从文章生成、Banner 制作到 PR 提交的完整流程。但当我把这个 Skill 迁移到本地 CodeBuddy IDE 时,立刻遇到了一个棘手的问题——所有 Python 脚本里都写死了绝对路径

其实这里我有一个思考就是现在所谓的skill到底是一个什么?他和大模型、和程序、和具体的业务场景之间的关系是什么?这个是我在这次实践当中得到的一点思考。我认为首先他应该是解决一个具体场景一系列操作的一个个操作的集合,他能够解决一个具体的问题,这有点些像我们以前在写代码的时候,我们要把多个操作能够有序的串联起来去解决拒绝具体的问题,比如说我们可能会写一个crontab 定时任务,然后去解决这个一个具体的问题。

那么这种情况下我们就是要把 skill 当作一个软件体看,但是这个软件他有他的特点,就是除了他有对于问题的描述过程,还有解决的这个程序,另外他还可以调用大模型去对解决问题过程中的情况进行反思会,更加的自动或智能,而对于解决具体的场景的问题是没有问题的,但是很多时候我们就会想到那同样的场景,这个问题能解决,能不能解决其他的问题?那问题和问题是不同的,但是解决多而步骤和过程是一样的,这种情况下是不是可以抽象成为一种配置,让这个 skill 更加通用化。

比如:

BLOG_ROOT = Path("/projects/hxblog/content/blog")
SSH_KEY   = "/root/.ssh/id_ed25519"
BLOG_ROOT = "/projects/hxblog"

这些路径在 OpenClaw 的容器环境里能跑,换到我的 macOS 本地环境就全部失效了。这让我意识到:一个好的 Agent Skill 必须像一个好的软件库一样,消除对特定运行环境的硬依赖

于是我花了一个下午,把整个 blog-writer Skill 从"能跑就行"重构成了"哪都能跑"。这篇文章记录完整的重构过程和思路。

二、问题分析:硬编码依赖清单

在动手之前,我先做了一次全面的依赖审计。用 grep 扫描所有脚本,梳理出以下硬编码问题:

脚本文件 硬编码内容 影响范围
gen_article.py BLOG_ROOT = Path("/projects/hxblog/content/blog") 文章生成路径错误
submit_pr.py BLOG_ROOT = "/projects/hxblog"git config user.name helight Git 操作失败
deploy.py SSH 主机/端口/用户/密钥 4 个常量 部署完全不可用
init_post.py BLOG_ROOT = "/projects/hxblog/content/blog" 初始化失败
gen_banner.py Linux-only 字体路径 macOS 上字体加载失败
sync_to_wechat.py --blog-root 默认值硬编码 微信同步失败
SKILL.md 所有命令用 /projects/.openclaw/skills/... 绝对路径 Agent 无法执行

一共 7 个脚本 + 1 个 SKILL.md,涉及 十几处硬编码。问题的本质是:配置散落在代码各处,没有统一的配置管理层

三、实战步骤

1. 第一步:SKILL.md 路径通用化

SKILL.md 是 Agent 的"操作手册",里面的命令路径必须先改对。

我引入了 SCRIPTS_DIR 变量约定,所有脚本调用统一使用 ${SCRIPTS_DIR}/xx.py

## 环境约定

以下用 `SCRIPTS_DIR` 代表脚本目录的实际路径:

SCRIPTS_DIR=".codebuddy/skills/blog-writer/scripts"

> Agent 在执行时应根据实际 Skill 安装位置替换 `SCRIPTS_DIR`

这里我考虑过直接用 scripts/xx.py 短路径,但分析后发现 Agent 默认在项目根目录执行命令,如果用短路径需要先 cd 到 Skill 目录,反而增加出错风险。最终选择了从项目根目录出发的相对路径。

2. 第二步:脚本内路径改为自动探测 + 参数注入

核心改造思路是三级优先级:命令行参数 > 环境变量 > 自动探测。

gen_article.py 为例,原来的硬编码:

BLOG_ROOT = Path("/projects/hxblog/content/blog")

改为自动探测函数 + --blog-root 参数:

def _detect_blog_root() -> Path:
    """自动探测博客内容根目录"""
    cwd = Path.cwd()
    candidate = cwd / "content" / "blog"
    if candidate.is_dir():
        return candidate
    return cwd

自动探测的逻辑很简单:如果当前目录下有 content/blog 子目录,就认为是 Hugo 博客仓库。这个启发式规则在绝大多数情况下都能正确工作。

3. 第三步:设计统一配置文件

脚本改完后发现一个新问题:每个脚本都有自己的参数,用户需要在多个地方重复配置同样的信息(比如微信凭证、SSH 信息)。于是我设计了一个 TOML 格式的统一配置文件

# config.toml

[blog]
site_url = "https://helight.cn"
repo = "helight/hxblog"

[author]
name = "helight"
link = "http://helight.cn"

[git]
user_name = "helight"
user_email = "helight@qq.com"
github_token = ""

[deploy]
host = ""
port = 22
user = ""
key = "~/.ssh/id_ed25519"
blog_dir = ""

[wechat]
app_id = ""
app_secret = ""

选择 TOML 而不是 YAML/JSON 的原因:

  • Python 3.11+ 内置 tomllib零依赖
  • 语法比 YAML 更严格,不容易犯缩进错误
  • 注释友好,适合配置文件场景

4. 第四步:实现配置解析模块

config.py 是整个配置系统的核心,它实现了:

# 三级优先级覆盖
def load_config(**overrides) -> Config:
    # 1. 加载 config.toml
    config_path = _find_config_file()
    data = _parse_toml(config_path) if config_path else {}

    # 2. 环境变量覆盖
    data = _apply_env_overrides(data)

    # 3. 命令行参数覆盖
    for key, val in overrides.items():
        if val:
            section, field = key.split("__")
            data[section][field] = val

    return _build_config(data)

关键设计决策:

  • 配置文件查找顺序:环境变量 BLOG_WRITER_CONFIG > Skill 目录 config.toml > 项目根目录 .blog-writer.toml
  • 环境变量映射DEPLOY_SSH_HOSTdeploy.host,兼容已有的环境变量使用方式
  • 类型安全:用 dataclass 定义配置结构,IDE 有自动补全
  • 向后兼容:没有 config.toml 也能运行,回退到环境变量和默认值

我还为 Python 3.10 及以下版本写了一个简易 TOML 解析器(不依赖第三方库 tomli),确保在各种环境下都能使用。

5. 第五步:跨平台字体探测

gen_banner.py 原来只有 Linux 字体路径:

candidates = [
    "/usr/share/fonts/google-noto-cjk/NotoSansCJK-Regular.ttc",
    "/usr/share/fonts/wqy-microhei/wqy-microhei.ttc",
    ...
]

改为全平台覆盖:

candidates = [
    cfg_font,  # 用户配置优先
    # Linux
    "/usr/share/fonts/google-noto-cjk/NotoSansCJK-Regular.ttc",
    # macOS
    "/System/Library/Fonts/PingFang.ttc",
    "/System/Library/Fonts/STHeiti Medium.ttc",
    # Windows
    "C:/Windows/Fonts/msyh.ttc",
]

6. 第六步:敏感信息保护

config.toml 包含 SSH 密钥路径、微信 AppSecret、GitHub Token 等敏感信息,必须确保不会被意外提交:

# .gitignore
config.toml
__pycache__/
*.pyc

同时提供 config.example.toml 作为模板,用户复制后填写自己的值。

四、踩坑记录

踩坑 1:相对路径 vs 绝对路径的抉择

最初我想把 SKILL.md 中的脚本路径写成 scripts/xx.py,看起来更简洁。但分析后发现:

  • Agent 默认 cwd 是项目根目录,不是 Skill 目录
  • 脚本内部用 Path(__file__).parent.parent 定位 Skill 资源,不依赖 cwd
  • 如果要用短路径,就得先 cd 到 Skill 目录,增加了一步容易遗漏的操作

结论:从项目根目录出发的完整相对路径是最稳妥的方案

踩坑 2:环境变量和配置文件的优先级

一开始我把环境变量的优先级设为最高,后来发现问题:用户在 shell 里 export 过一个临时变量,忘了 unset,导致配置文件里的值永远不生效。

最终确定的优先级:命令行参数 > 环境变量 > config.toml > 默认值。命令行参数优先级最高,因为它是最"显式"的操作。

踩坑 3:TOML 解析的兼容性

Python 3.11 才内置 tomllib,很多环境还在用 3.10 甚至 3.9。我的方案是三级回退

try:
    import tomllib          # Python 3.11+
except ModuleNotFoundError:
    try:
        import tomli as tomllib  # pip install tomli
    except ModuleNotFoundError:
        tomllib = None       # 使用内置简易解析器

简易解析器只支持 [section] + key = "value" 基本语法,但对配置文件已经完全够用了。

五、总结

这次重构的核心收获:

  1. Skill 就是软件库:Agent Skill 虽然是给 AI 用的,但本质上和写一个开源库没区别——要考虑可移植性、配置管理、文档完整性。

  2. 配置集中管理是关键:从散落在 7 个文件里的十几处硬编码,收敛到一个 config.toml,极大降低了使用门槛和出错概率。

  3. 三级优先级模式值得推广:命令行 > 环境变量 > 配置文件 > 默认值 这个模式在 CLI 工具中非常常见(Docker、kubectl 都是这么做的),用在 Agent Skill 上同样合适。

  4. 自动探测 + 显式覆盖的组合:大多数情况下自动探测就能工作,少数特殊场景用户可以显式指定,兼顾了易用性和灵活性。

重构后的 blog-writer Skill 已经可以在 OpenClaw 容器、本地 macOS、甚至 Windows 上无缝运行——这篇文章本身就是用重构后的 Skill 写的。