把 SkillOpt 包装成一个 Claude Code Skill
从「一个自动优化提示词的算法」到「用一句自然语言就能跑起来的技能」------本文以本项目里的
prompt-optimizerskill 为样本,拆解怎样把 SkillOpt 的 6 步优化循环包装成一个可复用的 Claude Code Skill。
一、先说清楚:SkillOpt 是什么,Skill 又是什么
这两个词长得像,但指的是完全不同的两层东西,先分开。
SkillOpt 是一个算法思想:借鉴强化学习里 rollout → evaluate → reflect 的训练循环,把「优化一段提示词」变成一个可以自动跑、有明确接受/拒绝判据的迭代过程。它的核心信念只有一句:
不猜测一次改动是否有效,而是用真实重跑出来的分数去对比新旧提示词。
Skill 则是 Claude Code 的一种封装形式 :一个目录,里面放一份 SKILL.md(告诉 Claude 什么时候用、怎么用)、若干脚本、若干参考文档。用户不需要记命令,只要说「跑 3 轮优化」,Claude 就会自己找到这个 skill、执行脚本、读回结果、汇报给你。
这篇博客讲的就是中间那一层桥:怎么把前者(一个 Python 算法)包装进后者(一个 Claude 能自主调度的技能)。
二、被包装的内核:6 步优化循环
包装之前,得先有个值得包装的内核。prompt-optimizer 的内核是 optimize_prompt.py 里的一轮循环,六步环环相扣:
vbnet
Step 1: ROLLOUT --- 用当前提示词跑任务 → 得到标注结果
Step 2: EVALUATE --- LLM Judge 评分 → 得到 old_hard_acc, old_soft_acc
Step 3: REFLECT --- 分析低分样本 → 提出结构化 edits(修改建议)
Step 4: UPDATE --- 把 edits 应用到提示词 → 生成新提示词
Step 5: VERIFY --- 用新提示词重跑任务 + 评分 → 得到 new_hard_acc
Step 6: GATE --- new > old ? 接受新提示词 : 回滚到旧提示词
这里面最值钱的设计是 GATE。它不是「让模型自我感觉良好地说『我改进了』」,而是拿两组真实测量的分数硬碰硬:
python
if new_hard_acc > old_hard_acc:
→ ACCEPT,更新 current_prompt.md、best_prompt.md
elif new_hard_acc == old_hard_acc and new_soft_acc > old_soft_acc:
→ ACCEPT(软分提升,硬分不变)
else:
→ REJECT,回滚旧提示词,converge_count += 1
连续 N 轮被 GATE 拒绝,就判定「收敛」,自动停机。整个内核对外只暴露几个动作:跑 N 轮 / 只评分 / 查状态 / 最终验证 / 重置。
记住这份「动作清单」------它就是我们要在 Skill 层暴露的接口。
三、包装的四个部件
一个 Skill 目录长这样:
bash
.claude/skills/prompt-optimizer/
├── SKILL.md # 契约:何时触发 + 怎么执行 + 怎么汇报
├── references/
│ └── optimization_guide.md # 深层参考:参数、配置、机制细节
└── scripts/
├── optimize_prompt.py # 6 步循环
└── llm_judge.py # 评分脚本
再加一个「入口糖」:
bash
.claude/commands/prompt-opt.md # 斜杠命令,/prompt-opt 3
下面逐一说这四个部件各自扛什么责任。
部件 1:SKILL.md ------ 触发契约 + 执行剧本
这是整个包装的灵魂。它做两件事。
第一,写好 description,让 Claude 知道「什么时候该想到我」。 frontmatter 里的描述不是给人看的文案,是给模型做语义匹配的触发器:
yaml
---
name: prompt-optimizer
description: |
Claude Code 驱动的提示词自动优化 --- 用 SkillOpt 6步循环(打标→评分→反思→修改→验证→决策)
自动改进总结提示词直到收敛。Claude 执行脚本、读取结果、汇报进展,用户只需说意图。
Use when: 用户想优化提示词、评估标注质量、跑优化循环。
Triggered by: "优化提示词", "调优prompt", "跑优化", "评估质量", "prompt-opt",
"继续优化", "评分", "evaluate", 或给一个数字表示优化轮数。
---
关键技巧:把用户会说的原话列进 Triggered by。用户不会说「请调用 prompt-optimizer skill」,他们会说「跑 3 轮」「评个分」「继续优化」。把这些口语直接写进去,命中率立刻上一个台阶。
第二,写一份「执行剧本」,让 Claude 知道拿到意图后一步步做什么。 SkillOpt 内核对外暴露 5 个动作,SKILL.md 就用一张表把「用户口语 → 模式 → 该跑哪个脚本」对齐:
| 用户说 | 模式 | Claude 执行 |
|---|---|---|
| "跑3轮优化"/数字 | N 轮优化 | optimize_prompt.py --rounds N |
| "评分"/"evaluate" | 只评分 | llm_judge.py |
| "优化状态"/"status" | 查状态 | optimize_prompt.py --status |
| "重置优化" | 重置 | 删除 optimize/ 目录 |
剧本里还有一条铁律,值得单独强调:
执行脚本后,Claude 必须 Read 结果文件(
prompt_history.json、best_prompt.md),再汇报给用户。
这是把「脚本」变成「技能」的分水岭。脚本只管在终端打印;而 Skill 要求 Claude 主动读回结构化产物、翻译成人能看懂的进展表格:
yaml
🔄 优化进展
轮 | 旧hard | 新hard | gate | edits | 收敛计数
1 | 0.50 | 0.67 | accept_new_best | 3 | 0
2 | 0.67 | 0.67 | reject | 2 | 1
best_hard: 0.73 | best_soft: 0.80
✅ 尚未收敛,可以继续: /prompt-opt 3
执行---读取---汇报,这个三拍子就是 Skill 相较裸脚本的全部增量价值。
部件 2:scripts/ ------ 自包含、能自己找到依赖
内核脚本要跟 SKILL.md 放在一起,让 skill 自包含。但真实项目里脚本之间有依赖------optimize_prompt.py 需要调 vlm-labeler skill 的 run_task.py 和 task_config.py。包装时踩的第一个坑就是路径:skill 被安装到别处后,硬编码的相对路径会全断。
解法是让脚本自己向上找项目根 ,认准 .claude/skills/ 这个锚点:
python
def _find_project_dir():
"""向上遍历,找到包含 .claude/skills/ 的目录当作项目根。"""
current = os.path.dirname(os.path.abspath(__file__))
for _ in range(10):
parent = os.path.dirname(current)
if os.path.isdir(os.path.join(parent, ".claude", "skills")):
return parent
current = parent
...
然后跨 skill 找依赖脚本时,按「优先 skill 源头 → 再项目根 scripts/ → 最后当前目录」的优先级兜底:
python
run_task_script = _find_script("run_task.py", "vlm-labeler")
judge_script = _find_script("llm_judge.py", "prompt-optimizer")
包装启示:脚本一旦要进 skill,就不能再假设「当前工作目录」。把「我在哪」「依赖在哪」的问题在脚本内部解决掉,skill 才能被搬到任意项目里即插即用。
部件 3:references/ ------ 分层,别把细节塞进 SKILL.md
SKILL.md 要短、要像剧本,让 Claude 一眼扫完就知道怎么动手。而参数全集、配置格式、GATE 的显著性阈值、Judge 多次采样聚合、过拟合诊断......这些深层细节一股脑塞进去会淹没主线。
于是它们被拆到 references/optimization_guide.md,并在 SKILL.md 里留一句指路:
所有任务特定内容由
task_config.yaml定义,详见references/optimization_guide.md。
Claude 平时只读 SKILL.md;只有当用户追问「怎么调采样次数」「显著性阈值怎么设」时,才按需去翻 reference。这是一种渐进式披露------上下文预算花在刀刃上。
部件 4:commands/ ------ 给个斜杠入口
Skill 靠语义触发,已经够用。但再包一层斜杠命令,能给用户一个确定性的、可打字的入口:
markdown
---
description: 通用提示词自动优化 --- Claude Code 驱动,评分或迭代优化提示词直到收敛
---
用户意图: $ARGUMENTS
根据 `$ARGUMENTS` 判断模式,然后由 Claude 执行脚本并汇报结果。
详细流程见 .claude/skills/prompt-optimizer/SKILL.md。
命令文件本身几乎不装逻辑,只把 $ARGUMENTS 接住,再把 Claude 指回 SKILL.md。于是 /prompt-opt 3、/prompt-opt evaluate、/loop 10m /prompt-opt 都能用------命令是壳,SKILL.md 是芯。
四、把配置从内核里抽出去
包装成 skill 的一个隐藏收益:通用化 。原本 SkillOpt 只会优化「VLM 图片打标」的提示词,但只要把所有任务特定的东西------输入类型、评分维度、角度提示词------全部外置到 task_config.yaml,同一个 skill 就能优化图片打标、QA 评估、文本分类:
yaml
judge:
pass_threshold: 0.8
dimensions: # 评分维度完全由配置决定
completeness: { name: "信息完整性", weight: 0.3, ... }
accuracy: { name: "准确性", weight: 0.3, ... }
optimize:
converge_patience: 3 # 连续几轮无提升算收敛
max_edits_per_round: 5 # 每轮最多几条修改
内核代码里没有一处写死「图片」或某个具体维度;references/ 里还附了三套模板(image_labeling / qa_evaluation / text_classification)。算法归脚本,任务归配置------这条边界划清楚,一个 skill 才能服务一类问题,而不是一个问题。
五、可复用的包装配方
抽掉 SkillOpt 这个具体案例,把「把一个算法包装成 Skill」的通用步骤留下:
- 锁定内核的动作清单。先让算法脚本能独立跑通,并想清楚它对外暴露哪几个动作(这里是:优化 N 轮 / 评分 / 查状态 / 验证 / 重置)。动作清单 = Skill 的接口。
- 写触发型
description。把用户会说的原话塞进Use when和Triggered by,让语义匹配命中口语,而不是命中术语。 - 写执行剧本。用一张「用户口语 → 模式 → 脚本命令」的表把意图映射到动作;并强制「执行 → Read 产物 → 汇报」三拍子。
- 让脚本自包含、自寻路 。以
.claude/skills/为锚点向上找项目根,跨 skill 依赖按优先级兜底,不依赖当前工作目录。 - 分层放细节 。
SKILL.md只留主线剧本,参数/机制/FAQ 下沉到references/,按需披露。 - 外置任务配置 。算法归脚本、任务归
*.yaml,让一个 skill 覆盖一类任务。 - (可选)加斜杠命令壳 。给一个确定性入口
/xxx $ARGUMENTS,逻辑仍指回SKILL.md。
尾声
SkillOpt 的算法值钱,但把它包装成 Skill 之后,它才真正好用:不用记参数、不用手动读 JSON、不用自己判断收敛。用户说「跑 3 轮」,Claude 就替他跑循环、读分数、画进展表、给下一步建议。
包装做的事,一句话概括------把「一个人要会用的脚本」变成「一句话就能指挥的技能」 。而这层包装的全部秘密,就藏在那份写得像剧本的 SKILL.md 里。