大部分人写 Skill 的方式,是把一堆指令塞进一个 Markdown 文件,然后祈祷模型能理解。
这种方式在简单场景下能跑。但当你有二十个 Skill,它们开始互相干扰、边界模糊、改动一个就弄坏另一个时,你会发现:你缺的不是更好的 Prompt,而是软件工程。
Skill 的本质是函数
这是我最想传达的一个认知转换:
把 Skill 当成一个函数来写,而不是一段说明书。
函数有什么?明确的输入、明确的输出、单一的职责、可测试的行为。Skill 也应该有。
一个糟糕的 Skill 长这样:
---
name: code-helper
description: 帮助处理代码相关任务
---
你是一个资深的软件工程师。
请帮助用户处理代码相关的问题,包括编写、审查、重构、调试。
要注意代码质量、性能、安全性。
请用中文回复。问题在哪?它没有边界。 "代码相关任务"包含了一切,模型无法确定什么时候该用它,也无法确定做到什么程度算完成。
同一件事,工程化的写法:
---
name: api-endpoint-review
description: 审查新增或改动的 HTTP 接口定义,检查命名规范、错误处理与向后兼容性。当用户提到"接口评审""API review"或给出路由代码时使用。
---
# 接口评审
## 触发条件
用户提供了 HTTP 路由处理函数或 OpenAPI 定义,并明确要求评审。
## 检查清单
按顺序检查,每项给出 [通过 / 不通过 / 不适用] 与理由:
1. **命名**:路径是否使用名词复数、是否符合既有路由风格
2. **错误处理**:是否覆盖 4xx 与 5xx、错误响应结构是否与全局一致
3. **向后兼容**:是否改变既有字段语义、废弃字段是否有过渡期
4. **幂等性**:写操作是否可安全重试
5. **鉴权**:是否显式声明所需权限,而非依赖默认放行
## 输出格式
先给结论(通过 / 需修改),再按清单逐项列出。
不通过项必须给出具体的修改建议,不要只说"建议优化"。
## 边界
- 不评审实现代码的内部逻辑,只评审接口契约
- 不修改文件,只输出评审意见
- 若信息不足以判断某项,标注"信息不足"而非猜测差别是结构上而非文采上的:
| 维度 | 说明书式 | 函数式 |
|---|---|---|
| 触发条件 | 模糊("代码相关") | 明确(路由定义 + 明确要求) |
| 职责范围 | 无限 | 限定为接口契约 |
| 输出格式 | 未定义 | 结论优先 + 逐项清单 |
| 边界 | 无 | 显式声明「不做什么」 |
| 可测试性 | 无 | 可用固定输入验证输出结构 |
description 是唯一的基础设施
Skill 的 description 字段决定了模型会不会选对它。这是整个 Skill 体系里 ROI 最高的一行字。
但它也是最常被敷衍的字段。对比一下:
# ❌ 无效:太宽泛,模型无法与任务匹配
description: 处理数据相关任务
# ❌ 无效:只重复了 name,没有增加判断信息
description: CSV 处理器,处理 CSV
# ✅ 有效:说清了做什么 + 什么时候用
description: >
将 CSV / Excel 文件转换为带类型的 JSON,自动推断日期与数值列,
输出转换报告。当用户上传表格文件并要求结构化、导入、转换时使用。判断标准很简单:把 description 单独拿出来,一个不了解上下文的人能否判断出该不该用这个 Skill? 如果不能,就重写。
渐进式加载:别一次性喂完
Skill 支持把详细内容拆到附带文件里,只在需要时读取。这个机制被严重低估。
api-endpoint-review/
├── SKILL.md # 常驻:触发条件 + 主流程(< 500 行)
├── checklist.md # 按需:完整的检查项细则
└── examples/
├── good.md # 按需:优秀接口示例
└── bad.md # 按需:典型反例SKILL.md 里这样引用:
## 检查清单
按 `checklist.md` 中的条目逐项检查。
遇到不确定的判定,参考 `examples/` 下的对应案例。这样做的好处不是省 token(虽然确实省),而是控制模型的注意力。上下文里塞太多细节,模型反而会忽略关键指令。
我的经验阈值:常驻内容控制在 500 行以内,超出部分一律拆分。
给 Skill 写回归测试
这是最反直觉但最有价值的一步:Skill 也需要测试。
我把测试用例组织成输入/期望输出对,每次改动 Skill 后跑一遍:
# tests/api-endpoint-review.yaml
- name: 检出缺失的错误处理
input: |
@POST /api/orders
async def create_order(req: Request):
order = await db.orders.insert(req.json())
return {"id": order.id}
expect:
conclusion: 需修改
failed_items:
- 错误处理
- 鉴权
- name: 不应评审内部实现
input: |
def calculate_discount(price, level):
if level > 3: return price * 0.8
return price
expect:
conclusion: 不适用
reason_contains: 非接口定义第二条用例尤其重要——它测的是边界,确保 Skill 不会越界去做不该做的事。
没有测试的 Skill,每次修改都是在赌。而 Prompt 的改动效果是非线性的:你以为只优化了一个措辞,实际可能改变了整个行为模式。
常见的三个坑
坑一:Skill 之间职责重叠。 当你有两个 Skill 的 description 都能匹配同一个任务时,模型的选择会变得不稳定。解法是合并,而不是增加区分度描述——靠文字区分两个相似职责,本质上是在和概率对抗。
坑二:在 Skill 里写死绝对路径。 Skill 应该描述"做什么",路径和具体文件名由运行时决定。写死路径的 Skill 换个项目就废了。
坑三:忽略失败路径。 大部分 Skill 只描述"成功了怎么做",没说"信息不足时怎么做"。结果是模型在信息不足时倾向于编造。显式声明「信息不足时如何反馈」,能显著降低幻觉。
生命周期
把上面这些串起来,是一个可持续迭代的闭环:
重点在右下那个回路:线上失败案例 → 补充测试用例 → 修改 Skill。这个回路转得越快,Skill 质量提升越快。
小结
如果你只想记住一句话:
写 Skill 的时候,先问"它不做什么",再问"它做什么"。
边界比能力更重要。一个能力平庸但边界清晰的 Skill,远比一个能力强大但边界模糊的 Skill 有用。