Code Reader
首页
帮助
设计文档
首页
帮助
设计文档
  • nanobot 渐进式技能加载机制深度分析

nanobot 渐进式技能加载机制深度分析

概述

nanobot 的渐进式技能加载机制(Progressive Skill Loading)是一种智能的上下文管理策略,旨在解决 AI 助手在有限 token 预算下的技能加载问题。该机制将技能分为两类:

  • Always Skills:始终加载的核心技能,内容直接嵌入系统提示
  • Available Skills:按需加载的可用技能,仅展示元数据摘要

这种设计使得 nanobot 能够在保持低 token 成本的同时,支持大量技能的无缝扩展。当用户需要特定技能时,LLM 可以通过 read_file 工具动态加载完整技能内容,实现了真正的"按需加载"(Just-In-Time Loading)。


设计理念分析

为什么需要区分 Always Skills 和 Available Skills

维度Always SkillsAvailable Skills
加载时机每次对话始终加载仅展示摘要,按需加载
内容范围完整技能内容仅元数据(name + description + location)
Token 消耗较高(每次对话)极低(~100 words/技能)
使用场景核心工具、常用操作领域专长、偶发任务
响应速度即时可用需要额外调用 read_file
示例基础文件操作、shell 命令GitHub 集成、天气查询

两种加载方式对 Token 使用的影响

计算模型:

假设系统有 20 个技能:

  • Always Skills(2 个):平均 500 tokens/技能
  • Available Skills(18 个):平均 50 tokens/技能(元数据摘要)

Always Skills 模式(所有技能都完整加载):

20 × 500 tokens = 10,000 tokens

渐进式加载模式(Always + Available):

2 × 500 (always) + 18 × 50 (available) = 1,900 tokens

节省比例:81%

渐进式加载的性能优势

  1. 降低冷启动延迟:系统提示更小,LLM 首次响应更快
  2. 支持大规模技能库:理论上可以支持数百个技能而不会耗尽上下文
  3. 智能按需加载:只有真正需要的技能才占用 token 预算
  4. 灵活的技能热重载:技能文件变更时,摘要自动更新,无需重启

Always Skills 加载机制详解

get_always_skills() 的实现

位置:skills.py:193-201

def get_always_skills(self) -> list[str]:
    """Get skills marked as always=true that meet requirements."""
    result = []
    for s in self.list_skills(filter_unavailable=True):
        meta = self.get_skill_metadata(s["name"]) or {}
        skill_meta = self._parse_nanobot_metadata(meta.get("metadata", ""))
        if skill_meta.get("always") or meta.get("always"):
            result.append(s["name"])
    return result

关键逻辑:

  1. 遍历所有可用技能(已过滤掉依赖不满足的)
  2. 解析每个技能的 frontmatter metadata
  3. 检查 always 标志(支持两种格式):
    • metadata.nanobot.always(JSON 格式)
    • 顶层 always 字段(YAML 格式)
  4. 返回满足条件的技能名称列表

load_skills_for_context() 如何加载 Always Skills

位置:skills.py:82-99

def load_skills_for_context(self, skill_names: list[str]) -> str:
    """Load specific skills for inclusion in agent context."""
    parts = []
    for name in skill_names:
        content = self.load_skill(name)
        if content:
            content = self._strip_frontmatter(content)
            parts.append(f"### Skill: {name}\n\n{content}")
    return "\n\n---\n\n".join(parts) if parts else ""

流程:

  1. 遍历技能名称列表
  2. 使用 load_skill(name) 读取完整技能内容
  3. 使用 _strip_frontmatter(content) 移除 YAML frontmatter
  4. 格式化为 Markdown 标题 + 内容
  5. 用分隔符连接多个技能

Always Skills 在系统提示中的位置

位置:context.py:52-58

# Skills - progressive loading
# 1. Always-loaded skills: include full content
always_skills = self.skills.get_always_skills()
if always_skills:
    always_content = self.skills.load_skills_for_context(always_skills)
    if always_content:
        parts.append(f"# Active Skills\n\n{always_content}")

系统提示结构:

# nanobot 🐈 [Identity]
---

## AGENTS.md [Bootstrap]
---

## Memory [Memory context]
---

# Active Skills [Always Skills - Full Content]
---

# Skills [Available Skills - Summary Only]

Always Skills 的使用场景

Always Skills 适用于:

  1. 基础工具集成:文件读写、命令执行、web 搜索
  2. 高频操作模式:代码重构、测试运行、日志分析
  3. 系统核心能力:记忆管理、任务调度、子 agent 生成
  4. 安全关键功能:权限验证、敏感信息处理

Available Skills 摘要机制详解

build_skills_summary() 的实现

位置:skills.py:101-140

def build_skills_summary(self) -> str:
    """Build a summary of all skills (name, description, path, availability)."""
    all_skills = self.list_skills(filter_unavailable=False)
    if not all_skills:
        return ""

    def escape_xml(s: str) -> str:
        return s.replace("&", "&amp;").replace("<", "&lt;").replace(">", "&gt;")

    lines = ["<skills>"]
    for s in all_skills:
        name = escape_xml(s["name"])
        path = s["path"]
        desc = escape_xml(self._get_skill_description(s["name"]))
        skill_meta = self._get_skill_meta(s["name"])
        available = self._check_requirements(skill_meta)

        lines.append(f"  <skill available=\"{str(available).lower()}\">")
        lines.append(f"    <name>{name}</name>")
        lines.append(f"    <description>{desc}</description>")
        lines.append(f"    <location>{path}</location>")

        if not available:
            missing = self._get_missing_requirements(skill_meta)
            if missing:
                lines.append(f"    <requires>{escape_xml(missing)}</requires>")

        lines.append(f"  </skill>")
    lines.append("</skills>")

    return "\n".join(lines)

XML 格式的技能摘要结构

示例输出:

<skills>
  <skill available="true">
    <name>github</name>
    <description>Interact with GitHub using the `gh` CLI...</description>
    <location>/path/to/skills/github/SKILL.md</location>
  </skill>
  <skill available="false">
    <name>summarize</name>
    <description>Summarize URLs, files, and YouTube videos</description>
    <location>/path/to/skills/summarize/SKILL.md</location>
    <requires>CLI: summarize</requires>
  </skill>
</skills>

字段说明:

字段说明
available是否可用(依赖是否满足)
name技能名称
description技能描述(用于触发)
location技能文件路径
requires缺失的依赖(仅 unavailable 时)

Available Skills 的按需加载机制

触发流程:

关键代码:context.py:60-68

# 2. Available skills: only show summary (agent uses read_file to load)
skills_summary = self.skills.build_skills_summary()
if skills_summary:
    parts.append(f"""# Skills

The following skills extend your capabilities. To use a skill, read its SKILL.md file using the read_file tool.
Skills with available="false" need dependencies installed first - you can try installing them with apt/brew.

{skills_summary}""")

LLM 如何通过 read_file 工具加载 Available Skills

LLM 推理示例:

用户:帮我查看 GitHub PR 状态
LLM 思考:
1. 分析请求:"查看 PR 状态" → 需要 GitHub 相关功能
2. 查询技能摘要:找到 github 技能,available="true"
3. 决定:需要加载 github 技能的完整内容
4. 调用工具:read_file("/path/to/skills/github/SKILL.md")
5. 学习技能:阅读 SKILL.md 中的 gh 命令用法
6. 执行任务:使用 gh pr checks 命令

技能元数据解析详解

_get_skill_meta() 的实现

位置:skills.py:188-191

def _get_skill_meta(self, name: str) -> dict:
    """Get nanobot metadata for a skill (cached in frontmatter)."""
    meta = self.get_skill_metadata(name) or {}
    return self._parse_nanobot_metadata(meta.get("metadata", ""))

YAML Frontmatter 的解析逻辑

位置:skills.py:203-228

def get_skill_metadata(self, name: str) -> dict | None:
    """Get metadata from a skill's frontmatter."""
    content = self.load_skill(name)
    if not content:
        return None

    if content.startswith("---"):
        match = re.match(r"^---\n(.*?)\n---", content, re.DOTALL)
        if match:
            # Simple YAML parsing
            metadata = {}
            for line in match.group(1).split("\n"):
                if ":" in line:
                    key, value = line.split(":", 1)
                    metadata[key.strip()] = value.strip().strip('"\'')
            return metadata

    return None

解析示例:

输入(SKILL.md):

---
name: github
description: "Interact with GitHub using the `gh` CLI..."
homepage: https://github.com/cli/cli
metadata: {"nanobot":{"emoji":"🐙","requires":{"bins":["gh"]}}}
---

输出(metadata dict):

{
    "name": "github",
    "description": "Interact with GitHub using the `gh` CLI...",
    "homepage": "https://github.com/cli/cli",
    "metadata": '{"nanobot":{"emoji":"🐙","requires":{"bins":["gh"]}}}'
}

metadata 字段的用途

位置:skills.py:169-175

def _parse_nanobot_metadata(self, raw: str) -> dict:
    """Parse nanobot metadata JSON from frontmatter."""
    try:
        data = json.loads(raw)
        return data.get("nanobot", {}) if isinstance(data, dict) else {}
    except (json.JSONDecodeError, TypeError):
        return {}

nanobot.metadata 支持的字段:

字段类型说明示例
emojistring技能图标"🐙"
alwaysboolean是否始终加载true
requires.binsstring[]需要的 CLI 工具["gh", "git"]
requires.envstring[]需要的环境变量["OPENAI_API_KEY"]
installobject[]安装指令(JSON)[{"kind":"brew","formula":"gh"}]
osstring[]支持的操作系统["darwin","linux"]

依赖检查机制(_check_requirements())

位置:skills.py:177-186

def _check_requirements(self, skill_meta: dict) -> bool:
    """Check if skill requirements are met (bins, env vars)."""
    requires = skill_meta.get("requires", {})
    for b in requires.get("bins", []):
        if not shutil.which(b):
            return False
    for env in requires.get("env", []):
        if not os.environ.get(env):
            return False
    return True

检查流程:

  1. 检查 CLI 工具:使用 shutil.which() 检测是否在 PATH 中
  2. 检查环境变量:使用 os.environ.get() 检查是否设置
  3. 所有检查通过才返回 True

技能优先级和缓存分析

技能的优先级(workspace vs builtin)

位置:skills.py:26-57

def list_skills(self, filter_unavailable: bool = True) -> list[dict[str, str]]:
    skills = []

    # Workspace skills (highest priority)
    if self.workspace_skills.exists():
        for skill_dir in self.workspace_skills.iterdir():
            if skill_dir.is_dir():
                skill_file = skill_dir / "SKILL.md"
                if skill_file.exists():
                    skills.append({"name": skill_dir.name, "path": str(skill_file), "source": "workspace"})

    # Built-in skills
    if self.builtin_skills and self.builtin_skills.exists():
        for skill_dir in self.builtin_skills.iterdir():
            if skill_dir.is_dir():
                skill_file = skill_dir / "SKILL.md"
                if skill_file.exists() and not any(s["name"] == skill_dir.name for s in skills):
                    skills.append({"name": skill_dir.name, "path": str(skill_file), "source": "builtin"})
    ...

优先级规则:

  1. Workspace Skills 优先:workspace/skills/ 中的技能覆盖 builtin 技能
  2. 同名技能去重:Builtin 技能仅当 workspace 中不存在同名技能时才添加
  3. 加载优先级:load_skill(name) 先检查 workspace,再检查 builtin

位置:skills.py:69-80

def load_skill(self, name: str) -> str | None:
    # Check workspace first
    workspace_skill = self.workspace_skills / name / "SKILL.md"
    if workspace_skill.exists():
        return workspace_skill.read_text(encoding="utf-8")

    # Check built-in
    if self.builtin_skills:
        builtin_skill = self.builtin_skills / name / "SKILL.md"
        if builtin_skill.exists():
            return builtin_skill.read_text(encoding="utf-8")

    return None

是否需要缓存机制

当前实现分析:

当前 SkillsLoader 没有缓存机制,每次调用都重新读取文件。这种设计的理由:

考虑因素当前无缓存潜在缓存方案
代码复杂度✅ 简单(仅 229 行)❌ 需要缓存失效逻辑
文件变更检测✅ 自动(每次读取最新)❌ 需要文件监听或 TTL
性能开销⚠️ 磁盘 I/O(每次读取)✅ 内存缓存(更快)
技能热重载✅ 自动(无需重启)⚠️ 需要手动失效
技能规模✅ 当前 < 10 个技能⚠️ 大规模场景可能需要

结论:

  • 当前规模(< 10 技能):无需缓存,磁盘 I/O 开销可忽略
  • 未来扩展(> 50 技能):建议添加缓存 + 文件变更监听

技能变更时的热重载问题

当前机制:自动热重载(每次调用 load_skill 都读取最新文件)

优势:

  • 无需重启进程
  • 技能修改立即生效
  • 适合开发调试

劣势:

  • 每次系统提示构建都需要读取文件
  • 在高频调用场景下可能成为性能瓶颈

潜在优化方案:

class SkillsLoader:
    def __init__(self, workspace: Path):
        self.workspace = workspace
        self._cache = {}  # {skill_name: (content, mtime)}

    def load_skill(self, name: str) -> str | None:
        path = self._find_skill_path(name)
        if not path:
            return None

        mtime = path.stat().st_mtime
        cached = self._cache.get(name)

        if cached and cached[1] == mtime:
            return cached[0]

        content = path.read_text(encoding="utf-8")
        self._cache[name] = (content, mtime)
        return content

技能加载的性能优化建议

当前实现的性能瓶颈

操作当前实现性能问题
list_skills()遍历所有目录,读取每个 SKILL.md 的 frontmatter大规模技能(100+)时 I/O 开销大
get_always_skills()遍历所有技能 + 解析 metadata重复解析 frontmatter
build_skills_summary()对每个技能调用 _get_skill_description() 和 _get_skill_meta()多次读取同一文件
load_skill()直接读取文件无缓存,高频调用时重复 I/O

可能的优化方向

1. 技能索引(Skill Indexing)

问题:每次 list_skills() 都需要遍历文件系统

优化方案:构建技能索引文件

# workspace/skills/index.json
{
  "version": 1,
  "last_updated": "2026-02-03T12:00:00Z",
  "skills": {
    "github": {
      "name": "github",
      "description": "Interact with GitHub...",
      "metadata": {...},
      "available": true,
      "path": "workspace/skills/github/SKILL.md",
      "mtime": 1738584000
    }
  }
}

优化效果:

  • list_skills():O(n) → O(1)(读取 index.json)
  • get_always_skills():O(n) → O(n)(遍历索引,无需 I/O)

2. 并行加载(Parallel Loading)

问题:load_skills_for_context() 顺序加载技能

优化方案:使用 concurrent.futures 并行读取

from concurrent.futures import ThreadPoolExecutor

def load_skills_for_context(self, skill_names: list[str]) -> str:
    parts = []

    with ThreadPoolExecutor(max_workers=4) as executor:
        futures = {
            executor.submit(self._load_single_skill, name): name
            for name in skill_names
        }
        for future in as_completed(futures):
            name = futures[future]
            try:
                content = future.result()
                if content:
                    content = self._strip_frontmatter(content)
                    parts.append(f"### Skill: {name}\n\n{content}")
            except Exception as e:
                # Log error but continue
                pass

    return "\n\n---\n\n".join(parts) if parts else ""

优化效果:

  • 多个技能并行加载(I/O 密集型任务)
  • 理论加速比:接近线程数(受限于磁盘 I/O)

3. 懒加载元数据(Lazy Metadata Parsing)

问题:每次 build_skills_summary() 都重新解析 frontmatter

优化方案:缓存元数据

class SkillsLoader:
    def __init__(self, workspace: Path):
        self.workspace = workspace
        self._metadata_cache: dict[str, tuple[dict, float]] = {}  # {name: (metadata, mtime)}

    def _get_skill_meta_cached(self, name: str) -> dict:
        path = self._find_skill_path(name)
        if not path:
            return {}

        mtime = path.stat().st_mtime
        cached = self._metadata_cache.get(name)

        if cached and cached[1] == mtime:
            return cached[0]

        meta = self._get_skill_meta(name)
        self._metadata_cache[name] = (meta, mtime)
        return meta

大规模技能场景下的扩展性分析

假设场景:200 个技能

指标当前实现优化后(索引 + 缓存)
冷启动时间~200ms(读取 200 个文件)~10ms(读取 index.json)
系统提示大小~3,000 tokens(摘要)~3,000 tokens(摘要)
内存占用~10MB(无缓存)~20MB(缓存元数据)
热重载延迟即时延迟(TTL 或手动刷新)

扩展性建议:

  1. < 50 技能:当前实现足够
  2. 50-200 技能:添加元数据缓存
  3. > 200 技能:实现技能索引 + 分页加载

技能系统的扩展点分析

如何添加新技能

方式 1:创建 Workspace Skill

mkdir -p workspace/skills/my-skill
cat > workspace/skills/my-skill/SKILL.md << 'EOF'
---
name: my-skill
description: "Do something awesome"
metadata: {"nanobot":{"emoji":"🚀"}}
---

# My Skill

Instructions here...
EOF

方式 2:创建 Builtin Skill

mkdir -p nanobot/skills/my-skill
cat > nanobot/skills/my-skill/SKILL.md << 'EOF'
---
name: my-skill
description: "Do something awesome"
---

# My Skill

Instructions here...
EOF

优先级:workspace 技能覆盖 builtin 技能

如何实现技能依赖

当前机制:静态依赖检查(bins + env)

扩展方案 1:自动安装提示

---
metadata: {
  "nanobot": {
    "requires": {"bins": ["ffmpeg"]},
    "install": [
      {"id": "brew", "kind": "brew", "formula": "ffmpeg", "bins": ["ffmpeg"], "label": "Install ffmpeg (brew)"},
      {"id": "apt", "kind": "apt", "package": "ffmpeg", "bins": ["ffmpeg"], "label": "Install ffmpeg (apt)"}
    ]
  }
}
---

扩展方案 2:运行时依赖解析

def resolve_dependencies(self, skill_name: str) -> list[dict]:
    """Resolve and suggest installation commands for missing dependencies."""
    meta = self._get_skill_meta(skill_name)
    missing = []

    # Check CLI tools
    for bin_name in meta.get("requires", {}).get("bins", []):
        if not shutil.which(bin_name):
            missing.append({"type": "bin", "name": bin_name})

    # Suggest installation
    suggestions = []
    for install in meta.get("install", []):
        if any(m["name"] in install.get("bins", []) for m in missing):
            suggestions.append(install)

    return suggestions

如何实现技能版本管理

方案 1:Git-based Versioning

---
metadata: {
  "nanobot": {
    "version": "1.2.0",
    "min_version": "1.0.0",
    "url": "https://github.com/nanobot/skills/blob/main/github/SKILL.md"
  }
}
---

方案 2:Skill Marketplace Integration

def update_skill(self, name: str, force: bool = False) -> bool:
    """Update a skill from the marketplace."""
    current_version = self._get_skill_version(name)
    latest_version = self._marketplace.get_latest_version(name)

    if not force and current_version == latest_version:
        return False

    skill_content = self._marketplace.download_skill(name, latest_version)
    self._save_skill(name, skill_content)
    return True

如何实现技能市场

架构设计:

API 设计:

class SkillMarketplace:
    def search_skills(self, query: str, category: str | None = None) -> list[dict]:
        """Search for skills in the marketplace."""
        pass

    def get_skill_info(self, name: str) -> dict:
        """Get detailed information about a skill."""
        pass

    def download_skill(self, name: str, version: str | None = None) -> bytes:
        """Download a skill package (.skill file)."""
        pass

    def install_skill(self, name: str, version: str | None = None) -> str:
        """Install a skill to workspace."""
        pass

    def rate_skill(self, name: str, rating: int, comment: str) -> bool:
        """Rate and review a skill."""
        pass

关键代码位置索引

功能文件方法/行号说明
技能列表skills.pylist_skills() 26-57列出所有技能(workspace + builtin)
技能加载skills.pyload_skill() 59-80加载单个技能内容
技能摘要skills.pybuild_skills_summary() 101-140构建技能摘要 XML
Always Skillsskills.pyget_always_skills() 193-201获取始终加载的技能
元数据解析skills.pyget_skill_metadata() 203-228解析 YAML frontmatter
依赖检查skills.py_check_requirements() 177-186检查 bins/env 依赖
系统提示构建context.pybuild_system_prompt() 27-70构建完整系统提示
Identitycontext.py_get_identity() 72-101核心 identity 部分
Bootstrap 文件context.py_load_bootstrap_files() 103-113加载 AGENTS.md 等
消息构建context.pybuild_messages() 115-147构建完整消息列表

深挖价值点

1. 三级渐进式加载设计

nanobot 的技能系统采用了独特的三级加载机制:

设计优势:

  • Level 1:所有技能的元数据始终在上下文中,确保 LLM 知道所有可用能力
  • Level 2:只有被触发的技能才加载完整内容,按需使用 token 预算
  • Level 3:脚本和资源文件可以执行而不加载到上下文中,实现真正的"无限扩展"

2. XML 摘要 vs JSON 摘要

当前选择:XML 格式

理由分析:

格式优势劣势
XML✅ 易于在 Markdown 中嵌入
✅ 结构清晰
✅ LLM 训练数据中常见
❌ 稍显冗长
JSON✅ 紧凑
✅ 易于解析
❌ Markdown 中需要转义
❌ 可能被误认为代码块

示例对比:

<!-- XML - 在 Markdown 中自然嵌入 -->
<skills>
  <skill available="true">
    <name>github</name>
    <description>Interact with GitHub...</description>
  </skill>
</skills>
```json
{"skills":[{"available":true,"name":"github","description":"Interact with GitHub..."}]}

### 3. 简化版 YAML 解析器

**位置**:`skills.py:220-226`

```python
# Simple YAML parsing
metadata = {}
for line in match.group(1).split("\n"):
    if ":" in line:
        key, value = line.split(":", 1)
        metadata[key.strip()] = value.strip().strip('"\'')

设计权衡:

方案代码量功能依赖
简化解析~10 行✅ 基本键值对
✅ 去引号
✅ 去空格
✅ 无
PyYAML+1 依赖✅ 完整 YAML 支持
✅ 嵌套结构
✅ 列表
❌ PyYAML 包
ruamel.yaml+1 依赖✅ YAML 1.2
✅ 注释保留
❌ ruamel.yaml 包

决策依据:

  • nanobot 的 frontmatter 结构简单(单层键值对 + JSON 字符串)
  • 避免额外依赖,保持轻量级(~4,000 行核心代码)
  • 性能足够(每次启动仅解析一次)

4. 依赖缺失时的友好提示

位置:context.py:65-66

Skills with available="false" need dependencies installed first - you can try installing them with apt/brew.

XML 中的 missing requirements:

位置:skills.py:131-135

# Show missing requirements for unavailable skills
if not available:
    missing = self._get_missing_requirements(skill_meta)
    if missing:
        lines.append(f"    <requires>{escape_xml(missing)}</requires>")

用户体验:

  • LLM 可以直接看到缺失的依赖
  • LLM 可以自动建议安装命令
  • 用户不会被卡在"技能不可用"的困惑中

5. Workspace 技能覆盖机制

设计意图:

  • 允许用户自定义/覆盖 builtin 技能
  • 支持技能的本地化和定制化
  • 保持 builtin 技能的"参考实现"地位

应用场景:

# 用户想要自定义 github 技能的行为
cp -r nanobot/skills/github workspace/skills/github

# 编辑 workspace/skills/github/SKILL.md
# 添加特定组织的 GitHub 工作流

6. 技能摘要在上下文中的位置

位置:context.py:52-68

# nanobot 🐈
---

[Bootstrap Files]
---

[Memory]
---

# Active Skills (Always Skills - Full Content)
---

# Skills (Available Skills - Summary Only)

设计理由:

  • Always Skills 在前:核心能力优先展示
  • Available Skills 在后:扩展能力作为补充
  • 位置明确:LLM 可以快速定位技能摘要

总结

nanobot 的渐进式技能加载机制是一个设计精巧、实现简洁的上下文管理方案。核心特点:

  1. 三级加载:Metadata → SKILL.md → Bundled Resources
  2. 智能按需:Always Skills(常驻)+ Available Skills(按需)
  3. 低 Token 开销:~81% token 节省(20 技能场景)
  4. 零依赖:简化版 YAML 解析,无第三方库
  5. 可扩展:支持技能市场、版本管理、依赖安装等未来扩展

这种设计使得 nanobot 在保持超轻量级(~4,000 行核心代码)的同时,具备了强大的可扩展性,是轻量级 AI 助手框架的优秀参考实现。