跳转到内容

06 - Skills 系统:从 SKILL.md 到动态 Prompt 注入

学习目标

理解 CowAgent 的 Skills 系统如何管理双层目录(builtin + custom)、启用/禁用配置、环境要求过滤与动态 Prompt 注入。

前置知识

技能设计的通用原理见 技能设计。本章聚焦 CowAgent 的具体实现。

项目实践

双层技能目录

CowAgent 的技能加载支持两个来源:

来源路径说明
内置技能项目 skills/ 目录随项目发布的默认技能(skill-creator, image-generation, knowledge-wiki)
自定义技能workspace skills/ 目录(默认 ~/cow/skills/用户安装或创建的技能

启动时 _sync_builtin_skills() 将内置技能从项目目录同步到 workspace,确保用户可在不修改项目代码的情况下拥有全部默认技能。

技能生命周期

技能过滤

技能的启用状态由 skills_config.json 管理,支持:

  1. 启用/禁用:用户通过 Web 控制台或 CLI 切换
  2. 环境变量要求:部分技能依赖特定 API Key(如 BOCHA_API_KEY for web_search)
  3. 知识系统开关conf().get("knowledge", True) 控制 knowledge-wiki 技能是否启用
# 伪代码 — 技能过滤核心逻辑
def filter_skills(self, skill_filter=None, include_disabled=False):
entries = list(self.skills.values())
# 1. 环境变量要求检查
entries = [e for e in entries if should_include_skill(e, self.config)]
# 2. 名称过滤(用户指定的 skill_filter)
if skill_filter:
entries = [e for e in entries if e.skill.name in skill_filter]
# 3. 启用状态过滤
if not include_disabled:
entries = [e for e in entries if self.is_skill_enabled(e.skill.name)]
# 4. 知识系统开关
if not conf().get("knowledge", True):
entries = [e for e in entries if e.skill.name != "knowledge-wiki"]
return entries

Prompt 注入策略

PromptBuilder 中的技能注入策略:

  1. 扫描可用技能:对每个技能检查 description 是否与用户需求匹配
  2. 匹配优先:有匹配技能时,提示 LLM 使用 read 工具读取 SKILL.md
  3. 不可用技能提示:显示安装指引,而非静默跳过
## 🧩 技能系统(mandatory)
在回复之前:扫描下方 <available_skills> 中每个技能的 <description>
- 如果有技能匹配:使用 `read` 工具读取 SKILL.md,然后遵循其中指令
- 如果没有技能适用:不要读取任何 SKILL.md,直接使用通用工具
**重要**: 技能不是工具,不能直接调用。使用技能的唯一方式是用 `read` 读取 SKILL.md 文件。

技能热刷新

Agent.get_full_system_prompt() 每次构建时调用 skill_manager.refresh_skills(),确保磁盘上的技能变更立即生效,无需重启 Agent。

问题与规避

问题CowAgent 的规避策略
技能目录被修改但未刷新refresh_skills() 每次构建 prompt 时调用
技能环境变量缺失导致报错should_include_skill() 检查环境变量,不满足时不注入 prompt
内置技能更新覆盖用户自定义同步时先 rmtree(dst)copytree(src, dst),用户自定义技能应在 workspace 而非项目目录编辑
LLM 一次读取多个技能prompt 约束”永远不要一次性读取多个技能”

设计取舍

为什么技能不是工具,而是通过 read 文件来使用?

这种设计(“Skills as files” 模式)使得技能定义完全独立于代码——一个 SKILL.md 文件就是完整的技能定义,可以用自然语言编写,无需编程。相比”注册制”技能(需要代码注册到框架),“文件制”技能更灵活、更易扩展。代价是每次使用需要一次额外的 read 工具调用。

参考来源