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 Skills | Available 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%
渐进式加载的性能优势
- 降低冷启动延迟:系统提示更小,LLM 首次响应更快
- 支持大规模技能库:理论上可以支持数百个技能而不会耗尽上下文
- 智能按需加载:只有真正需要的技能才占用 token 预算
- 灵活的技能热重载:技能文件变更时,摘要自动更新,无需重启
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
关键逻辑:
- 遍历所有可用技能(已过滤掉依赖不满足的)
- 解析每个技能的 frontmatter metadata
- 检查
always标志(支持两种格式):metadata.nanobot.always(JSON 格式)- 顶层
always字段(YAML 格式)
- 返回满足条件的技能名称列表
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 ""
流程:
- 遍历技能名称列表
- 使用
load_skill(name)读取完整技能内容 - 使用
_strip_frontmatter(content)移除 YAML frontmatter - 格式化为 Markdown 标题 + 内容
- 用分隔符连接多个技能
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 适用于:
- 基础工具集成:文件读写、命令执行、web 搜索
- 高频操作模式:代码重构、测试运行、日志分析
- 系统核心能力:记忆管理、任务调度、子 agent 生成
- 安全关键功能:权限验证、敏感信息处理
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("&", "&").replace("<", "<").replace(">", ">")
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 支持的字段:
| 字段 | 类型 | 说明 | 示例 |
|---|---|---|---|
emoji | string | 技能图标 | "🐙" |
always | boolean | 是否始终加载 | true |
requires.bins | string[] | 需要的 CLI 工具 | ["gh", "git"] |
requires.env | string[] | 需要的环境变量 | ["OPENAI_API_KEY"] |
install | object[] | 安装指令(JSON) | [{"kind":"brew","formula":"gh"}] |
os | string[] | 支持的操作系统 | ["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
检查流程:
- 检查 CLI 工具:使用
shutil.which()检测是否在 PATH 中 - 检查环境变量:使用
os.environ.get()检查是否设置 - 所有检查通过才返回
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"})
...
优先级规则:
- Workspace Skills 优先:
workspace/skills/中的技能覆盖 builtin 技能 - 同名技能去重:Builtin 技能仅当 workspace 中不存在同名技能时才添加
- 加载优先级:
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 或手动刷新) |
扩展性建议:
- < 50 技能:当前实现足够
- 50-200 技能:添加元数据缓存
- > 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.py | list_skills() 26-57 | 列出所有技能(workspace + builtin) |
| 技能加载 | skills.py | load_skill() 59-80 | 加载单个技能内容 |
| 技能摘要 | skills.py | build_skills_summary() 101-140 | 构建技能摘要 XML |
| Always Skills | skills.py | get_always_skills() 193-201 | 获取始终加载的技能 |
| 元数据解析 | skills.py | get_skill_metadata() 203-228 | 解析 YAML frontmatter |
| 依赖检查 | skills.py | _check_requirements() 177-186 | 检查 bins/env 依赖 |
| 系统提示构建 | context.py | build_system_prompt() 27-70 | 构建完整系统提示 |
| Identity | context.py | _get_identity() 72-101 | 核心 identity 部分 |
| Bootstrap 文件 | context.py | _load_bootstrap_files() 103-113 | 加载 AGENTS.md 等 |
| 消息构建 | context.py | build_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 的渐进式技能加载机制是一个设计精巧、实现简洁的上下文管理方案。核心特点:
- 三级加载:Metadata → SKILL.md → Bundled Resources
- 智能按需:Always Skills(常驻)+ Available Skills(按需)
- 低 Token 开销:~81% token 节省(20 技能场景)
- 零依赖:简化版 YAML 解析,无第三方库
- 可扩展:支持技能市场、版本管理、依赖安装等未来扩展
这种设计使得 nanobot 在保持超轻量级(~4,000 行核心代码)的同时,具备了强大的可扩展性,是轻量级 AI 助手框架的优秀参考实现。