🏠

skill_definition_models 模块深度解析

模块概述:为什么需要这个模块?

想象你正在设计一个可扩展的 AI 助手系统,这个助手需要能够"学习"新的技能——比如如何操作特定的 API、如何格式化特定类型的数据、或者如何遵循特定领域的工作流程。最 naive 的做法是把所有技能逻辑硬编码在代码里,但这会带来两个致命问题:

  1. 耦合问题:每次添加新技能都要修改核心代码、重新编译部署
  2. 上下文膨胀问题:如果把所有技能的完整指令都塞进 System Prompt,会迅速耗尽 LLM 的上下文窗口

skill_definition_models 模块正是为了解决这些问题而存在。它定义了一套基于文件的技能描述规范,让技能可以像插件一样被动态加载,同时通过"渐进式披露"(Progressive Disclosure)模式,只在需要时才加载技能的完整内容,从而在灵活性和效率之间取得平衡。

这个模块的核心洞察是:技能的元数据(名称、描述)应该始终可用,但技能的详细指令和资源应该按需加载。这正是 Claude 技能系统的设计哲学在本系统中的实现。


架构与数据流

graph TD subgraph "技能定义层 (本模块)" SkillMetadata["SkillMetadata
(Level 1: 元数据)"] Skill["Skill
(Level 2: 完整技能)"] SkillFile["SkillFile
(Level 3: 资源文件)"] end subgraph "技能管理层" Loader["Loader
技能发现与加载"] Manager["Manager
技能生命周期管理"] end subgraph "服务层" skillService["skillService
技能服务"] end subgraph "工具层" ReadSkillTool["ReadSkillTool
读取技能"] ExecuteSkillScriptTool["ExecuteSkillScriptTool
执行技能脚本"] end SKILL.md["SKILL.md 文件
(YAML Frontmatter + 指令体)"] --> Loader Loader -->|发现 | SkillMetadata Loader -->|完整加载 | Skill Skill -->|转换为 | SkillMetadata Skill -->|包含 | SkillFile Manager -->|使用 | Loader skillService -->|使用 | Loader Manager -->|管理 | Skill ReadSkillTool -->|依赖 | Manager ExecuteSkillScriptTool -->|依赖 | Manager

架构角色分析

这个模块在整个技能系统中扮演数据契约定义者的角色:

层级 组件 职责
定义层 Skill, SkillMetadata, SkillFile 定义技能的数据结构和验证规则
管理层 Loader, Manager 技能的发现、加载、缓存和生命周期管理
服务层 skillService 对外提供技能相关的业务服务
工具层 ReadSkillTool, ExecuteSkillScriptTool Agent 可调用的技能操作工具

数据流动路径

技能发现流程(轻量级,仅 Level 1):

SKILL.md 文件 → Loader 扫描 → ParseSkillMetadata() → SkillMetadata → 缓存到 Manager.metadataCache

技能完整加载流程(按需,Level 1+2+3):

SkillMetadata.BasePath → 读取 SKILL.md → ParseSkillFile() → Skill (含 Instructions) → 扫描目录 → SkillFile 列表

技能执行流程:

Agent 调用 ExecuteSkillScriptTool → Manager 验证技能是否允许 → 定位 SkillFile → Sandbox 执行脚本

核心组件深度解析

1. Skill 结构体:技能的完整表示

type Skill struct {
    // Metadata (Level 1) - always loaded
    Name        string `yaml:"name"`
    Description string `yaml:"description"`

    // Filesystem information
    BasePath string // Absolute path to the skill directory
    FilePath string // Absolute path to SKILL.md

    // Instructions (Level 2) - loaded on demand
    Instructions string // The main body of SKILL.md (after frontmatter)
    Loaded       bool   // Whether Level 2 instructions have been loaded
}

设计意图:

Skill 是技能的核心数据载体,它的设计体现了分层加载的思想。注意 Loaded 字段的存在——这不是一个普通的布尔值,而是一个惰性加载标记。当系统首次发现一个技能时,只会解析 YAML frontmatter 获取 Name 和 Description,此时 Loaded 为 false。只有当 Agent 真正需要执行这个技能时,才会读取完整的 Instructions 并将 Loaded 设为 true。

这种设计解决了一个关键问题:技能数量可能很多,但单次对话中实际使用的技能很少。如果每次都加载所有技能的完整内容,会造成严重的内存浪费和启动延迟。

SKILL.md 文件格式:

技能文件采用 YAML frontmatter + Markdown 主体的混合格式,这是静态网站生成器(如 Jekyll、Hugo)的经典模式:

---
name: data-analysis
description: 执行数据分析和可视化任务
---

# 技能指令

当用户请求数据分析时,请遵循以下步骤:
1. 首先确认数据源...

ParseSkillFile() 函数负责解析这种格式:

  • 使用 bufio.Scanner 逐行扫描,识别 --- 分隔符
  • frontmatter 部分用 yaml.Unmarshal 解析到 Skill 结构体
  • 主体部分作为 Instructions 存储

验证逻辑:

Validate() 方法执行严格的校验,防止恶意或错误的技能定义:

校验项 规则 原因
名称长度 ≤ 64 字符 防止过长名称影响 UI 展示
名称模式 ^[\p{L}\p{N}-]+$ 只允许 Unicode 字母、数字、连字符
保留词 不能包含 "anthropic", "claude" 避免与系统技能冲突
XML 标签 名称和描述中禁止 防止 prompt 注入攻击
描述长度 ≤ 1024 字符 保持描述简洁

这里的 XML 标签检测尤其值得注意——攻击者可能尝试在技能名称中插入 <system>ignore previous instructions</system> 这样的内容来绕过安全限制。xmlTagPattern 正则表达式 <[^>]+> 会捕获任何类似 XML 的结构并拒绝。


2. SkillMetadata 结构体:轻量级技能发现

type SkillMetadata struct {
    Name        string
    Description string
    BasePath    string // Path to skill directory for later loading
}

设计意图:

SkillMetadata 是 Skill 的精简投影,只包含系统启动和技能发现阶段必需的信息。它的设计遵循了最小知识原则:

  • Name 和 Description:用于构建 System Prompt 中的技能列表,让 LLM 知道有哪些可用技能
  • BasePath:延迟加载的"钩子",当需要完整技能时,可以用它定位并加载 SKILL.md

使用场景对比:

场景 使用 SkillMetadata 使用 Skill
系统启动时扫描所有技能 ✅ 快速、内存占用小 ❌ 过度加载
构建 System Prompt ✅ 只需要名称和描述 ❌ 包含不必要的 Instructions
Agent 执行技能 ❌ 缺少指令内容 ✅ 完整信息
技能列表 API 响应 ✅ 轻量级 ❌ 响应过大

ToMetadata() 方法提供了从 Skill 到 SkillMetadata 的转换,这是一个有损压缩——丢弃 Instructions 和 FilePath,保留核心标识信息。


3. SkillFile 结构体:技能资源文件

type SkillFile struct {
    Name     string // Filename (e.g., "FORMS.md", "scripts/validate.py")
    Path     string // Absolute path to the file
    Content  string // File content
    IsScript bool   // Whether this is an executable script
}

设计意图:

SkillFile 支持技能的资源扩展机制。一个技能不仅仅是 SKILL.md 中的指令,还可能包含:

  • 参考文档(如 FORMS.md 说明表单格式)
  • 可执行脚本(如 scripts/validate.py)
  • 配置文件(如 config.yaml)

IsScript 字段通过文件扩展名判断(.py, .sh, .js 等),这决定了文件是否可以被 ExecuteSkillScriptTool 执行。GetScriptLanguage() 函数进一步映射扩展名到解释器(.py → python, .js → node)。

安全考量:

注意 SkillFile 本身不包含执行逻辑——执行由 ExecuteSkillScriptTool 通过 sandbox.Manager 在隔离环境中进行。这种数据与执行分离的设计是安全的关键:定义技能文件是一回事,执行它们是另一回事,后者需要额外的权限检查和沙箱保护。


依赖关系分析

上游依赖(本模块被谁调用)

调用方 调用内容 期望契约
Loader ParseSkillFile(), ParseSkillMetadata() 返回有效的 Skill 或 SkillMetadata,错误时返回明确 error
Manager Skill.Validate(), Skill.ToMetadata() 验证技能合法性,转换为轻量表示
skillService Skill, SkillMetadata 通过 Loader 间接使用,提供技能管理服务
ReadSkillTool Skill (通过 Manager) 获取技能的完整信息供 Agent 读取
ExecuteSkillScriptTool SkillFile (通过 Manager) 获取可执行脚本文件

下游依赖(本模块调用谁)

被调用方 用途 耦合程度
gopkg.in/yaml.v3 解析 YAML frontmatter 紧耦合:依赖特定 YAML 库的 API
bufio, strings, regexp 文件解析和验证 松耦合:标准库,稳定
filepath 路径操作和扩展名判断 松耦合:标准库

数据契约

SKILL.md 文件格式契约:

---
name: <string, 1-64 字符,匹配 ^[\p{L}\p{N}-]+$>
description: <string, 1-1024 字符,不含 XML 标签>
---
<Markdown 格式的指令内容>

违反此契约会导致 ParseSkillFile() 返回错误,技能无法被加载。


设计决策与权衡

1. 渐进式披露 vs 全量加载

选择:采用三级渐进式披露(元数据 → 指令 → 资源)

权衡:

  • ✅ 优点:启动快、内存占用低、支持大量技能
  • ❌ 缺点:增加了代码复杂度,需要管理 Loaded 状态和延迟加载逻辑

替代方案:启动时加载所有技能的完整内容

  • 简单直接,但技能数量增长后会导致启动时间线性增长

为什么当前选择更优:在实际使用中,一个系统可能有数十甚至上百个技能,但单次对话通常只用到 3-5 个。渐进式披露将 O(n) 的启动成本降为 O(1),将 O(n) 的内存占用降为 O(k),其中 k 是实际使用的技能数。

2. YAML Frontmatter vs 纯 JSON 配置

选择:YAML frontmatter + Markdown 主体

权衡:

  • ✅ 优点:人类可读性好,指令部分可以用 Markdown 格式化,符合开发者习惯
  • ❌ 缺点:解析逻辑比纯 JSON 复杂,需要处理分隔符和两种格式

替代方案:skill.json + instructions.md 分离

  • 解析简单,但文件数量翻倍,管理不便

为什么当前选择更优:静态网站生成器社区已经验证了 frontmatter 模式的可用性,开发者熟悉这种格式。将元数据和指令放在同一个文件中,保证了技能的原子性——复制一个技能只需复制一个文件。

3. 严格验证 vs 宽松解析

选择:严格验证(名称模式、长度限制、保留词、XML 检测)

权衡:

  • ✅ 优点:防止恶意技能、保证系统稳定性、避免 prompt 注入
  • ❌ 缺点:可能拒绝一些边缘但合法的技能定义

为什么当前选择更优:技能系统直接面向 LLM,安全是首要考虑。一个恶意技能可能通过精心构造的名称或描述来操纵 LLM 行为。严格验证是防御性编程的体现。

4. 结构体标签 vs 手动解析

选择:使用 yaml:"name" 结构体标签

权衡:

  • ✅ 优点:代码简洁,利用 yaml 库的反射能力
  • ❌ 缺点:运行时反射有轻微性能开销,字段名与 YAML 键耦合

为什么当前选择更优:技能加载不是高频操作(系统启动时或首次使用时),性能开销可以忽略。结构体标签使代码更易读和维护。


使用指南与示例

创建新技能

  1. 创建技能目录:
mkdir -p skills/data-analysis
  1. 创建 SKILL.md:
---
name: data-analysis
description: 执行数据分析和可视化任务,支持 CSV、Excel 等格式
---

# 数据分析技能

## 能力范围
- 读取和解析结构化数据文件
- 执行统计分析和数据转换
- 生成可视化图表

## 使用规范
1. 在执行分析前,先确认用户的数据格式和分析目标
2. 对于大型数据集,建议先进行抽样探索
3. 输出结果时,同时提供关键统计指标
  1. 添加脚本资源(可选):
mkdir -p skills/data-analysis/scripts
cat > skills/data-analysis/scripts/validate.py << 'EOF'
#!/usr/bin/env python3
import pandas as pd
import sys

def validate_csv(path):
    df = pd.read_csv(path)
    print(f"Rows: {len(df)}, Columns: {len(df.columns)}")
    return df.notnull().sum()

if __name__ == "__main__":
    validate_csv(sys.argv[1])
EOF

编程使用

import "your-module/internal/agent/skills/skill"

// 解析技能文件
content, _ := os.ReadFile("skills/data-analysis/SKILL.md")
skill, err := skill.ParseSkillFile(content)
if err != nil {
    log.Fatalf("解析失败:%v", err)
}

// 验证技能
if err := skill.Validate(); err != nil {
    log.Fatalf("验证失败:%v", err)
}

// 转换为元数据(用于系统提示)
meta := skill.ToMetadata()
fmt.Printf("发现技能:%s - %s\n", meta.Name, meta.Description)

// 检查脚本文件
scriptPath := "skills/data-analysis/scripts/validate.py"
if skill.IsScript(scriptPath) {
    lang := skill.GetScriptLanguage(scriptPath)
    fmt.Printf("可执行脚本:%s (解释器:%s)\n", scriptPath, lang)
}

边界情况与注意事项

1. Frontmatter 格式错误

问题:YAML frontmatter 未正确闭合或格式错误

---
name: my-skill
description: 缺少结束分隔符

# 这会导致解析失败

表现:ParseSkillFile() 返回 errors.New("SKILL.md frontmatter is not properly closed with ---")

解决:确保 frontmatter 以 --- 开始和结束

2. 名称冲突与保留词

问题:技能名称包含保留词或特殊字符

---
name: my-claude-skill  # ❌ 包含保留词 "claude"
---

表现:Validate() 返回 fmt.Errorf("skill name cannot contain reserved word: %s", reserved)

解决:避免使用 anthropic、claude 以及非字母数字字符

3. XML 注入风险

问题:技能描述中包含类似 XML 的标签

---
name: data-skill
description: 使用<skill>标签处理数据  # ❌ 会被拒绝
---

表现:Validate() 返回 errors.New("skill description cannot contain XML tags")

解决:使用纯文本描述,避免任何 <...> 结构

4. 脚本执行安全

问题:SkillFile.IsScript 仅通过扩展名判断,不验证内容

风险:恶意用户可能将可执行脚本重命名为 .txt 绕过检测,或将恶意代码放入 .py 文件

缓解措施:

  • 脚本执行由 ExecuteSkillScriptTool 通过 sandbox.Manager 进行
  • 沙箱环境限制文件系统访问和网络请求
  • Manager 的 allowedSkills 配置限制可用技能列表

5. 并发加载竞争

问题:多个 goroutine 同时加载同一技能

表现:可能重复读取文件,浪费 I/O

解决:Manager 使用 sync.RWMutex 保护 metadataCache,确保并发安全。应用层应通过 Manager 而非直接调用 ParseSkillFile() 来加载技能。


相关模块参考


总结

skill_definition_models 模块是技能系统的数据基石。它通过三个核心结构体(Skill、SkillMetadata、SkillFile)定义了技能的表示方式,通过渐进式披露模式平衡了灵活性和效率,通过严格验证保障了系统安全。

理解这个模块的关键是把握分层加载的设计思想:元数据始终可用,指令按需加载,资源按需访问。这种设计使得系统可以支持大量技能而不影响启动性能,同时保持了技能定义的简洁性和可维护性。

On this page