搞了两天,终于把 nanobot 的技能系统跑通了。说实话,一开始我觉得"插件化"这词挺虚的——不就是动态加载模块吗?但真正做下来才发现,这里面的坑比我想象的多。

今天聊聊怎么给 AI 助手设计一个技能系统,让它能像装 App 一样扩展能力。

为什么要搞技能系统?

最开始 nanobot 只能聊天,后来加了文件操作、Shell 命令、网页搜索……代码越来越臃肿。每次加新功能都要改核心代码,耦合得一塌糊涂。

更麻烦的是,有些功能用户根本用不上。比如我的机器人项目需要人脸识别,但普通用户谁需要这个?

所以技能系统的目标很明确:

  1. 核心要轻:只保留最基础的能力
  2. 按需加载:用户想用什么就装什么
  3. 热插拔:不用重启就能增删技能

技能长什么样?

一个技能就是一个目录,里面至少有一个 SKILL.md 文件:

skills/
├── weather/
│   ├── SKILL.md      # 技能描述和使用说明
│   └── scripts/      # 可选:脚本文件
├── tavily-search/
│   ├── SKILL.md
│   └── requirements.txt
└── my-skill/
    └── SKILL.md

SKILL.md 是技能的"身份证",告诉 AI 这个技能是干嘛的、怎么用:

---
name: weather
description: 获取天气信息,支持城市查询和天气预报
---

# Weather Skill

## 使用方法

用户问天气时自动调用。

## 示例

用户:北京今天天气怎么样?
助手:[调用 weather skill]

就这么简单。AI 读到这个文件,就知道有这个技能可用。

动态加载:importlib 的魔法

核心代码其实不多,主要用 importlib 动态导入:

import importlib.util
from pathlib import Path

def load_skill(skill_path: Path):
    """加载单个技能"""
    skill_file = skill_path / "SKILL.md"
    if not skill_file.exists():
        return None
    
    # 读取技能描述
    skill_info = parse_skill_md(skill_file)
    
    # 如果有 Python 脚本,动态导入
    scripts_dir = skill_path / "scripts"
    if scripts_dir.exists():
        for py_file in scripts_dir.glob("*.py"):
            spec = importlib.util.spec_from_file_location(
                py_file.stem, py_file
            )
            module = importlib.util.module_from_spec(spec)
            spec.loader.exec_module(module)
            # 注册到工具系统
            register_tools(module)
    
    return skill_info

关键是 spec_from_file_location,它让你可以在运行时加载任意路径的 Python 文件。

依赖管理:别让用户装一堆包

技能可能有依赖,比如 tavily-search 需要 tavily-python。怎么处理?

我的方案是:延迟安装

def check_dependencies(skill_path: Path):
    """检查技能依赖,提示用户安装"""
    req_file = skill_path / "requirements.txt"
    if not req_file.exists():
        return True
    
    missing = []
    for line in req_file.read_text().splitlines():
        pkg = line.split("==")[0].split(">=")[0].strip()
        try:
            importlib.import_module(pkg.replace("-", "_"))
        except ImportError:
            missing.append(pkg)
    
    if missing:
        print(f"缺少依赖: {missing}")
        print(f"运行: pip install {' '.join(missing)}")
        return False
    return True

不在加载时自动安装,而是提示用户手动装。为什么?因为自动装包可能搞乱用户的环境,还是让用户自己控制比较好。

热更新:改了配置不用重启

这个功能我折腾了最久。用户改了技能配置,不想重启整个服务怎么办?

答案是:文件监听

from watchdog.observers import Observer
from watchdog.events import FileSystemEventHandler

class SkillReloader(FileSystemEventHandler):
    def on_modified(self, event):
        if event.src_path.endswith("SKILL.md"):
            skill_name = Path(event.src_path).parent.name
            reload_skill(skill_name)
            print(f"技能 {skill_name} 已重新加载")

observer = Observer()
observer.schedule(SkillReloader(), path="skills/", recursive=True)
observer.start()

watchdog 监听文件变化,一旦 SKILL.md 被修改,就重新加载对应的技能。

踩坑提醒:watchdog 在某些系统上可能有延迟,改完文件等个 1-2 秒再测试。

技能注册表:让 AI 知道有什么可用

加载完技能,得让 AI 知道。我的做法是在系统提示里注入技能列表:

def build_skill_prompt(skills: list[Skill]) -> str:
    """构建技能提示词"""
    lines = ["# Available Skills\n"]
    for skill in skills:
        lines.append(f"- **{skill.name}**: {skill.description}")
        if not skill.available:
            lines.append(f"  - ⚠️ Needs: {skill.requires}")
    return "\n".join(lines)

这样 AI 在对话时就能看到:

# Available Skills

- **weather**: 获取天气信息
- **tavily-search**: AI优化的网页搜索
- **summarize**: ⚠️ Needs install - 总结网页和文件

实际效果

现在 nanobot 启动时会自动扫描 skills/ 目录:

[INFO] Loading skills...
[INFO] ✓ weather (available)
[INFO] ✓ tmux (available)
[INFO] ✓ tavily-search (needs API key)
[INFO] ✓ summarize (needs install)
[INFO] Loaded 4 skills in 0.3s

用户想加新技能?扔个目录进去就行。想禁用?删掉目录或者改个名字。

踩坑记录

坑1:循环导入

技能脚本里 import 核心模块,核心模块又加载技能,直接死循环。

解决:技能脚本只暴露纯函数,不要反向依赖核心。

坑2:路径问题

技能脚本里用相对路径读文件,结果找不到。

解决:用 __file__ 获取脚本绝对路径:

# 在技能脚本里
SKILL_DIR = Path(__file__).parent.parent  # scripts/ 的上级

坑3:依赖冲突

两个技能依赖同一个包的不同版本。

解决:目前还没完美方案,建议用虚拟环境或者容器化。长期考虑给每个技能独立的 venv。

写在最后

技能系统让 AI 助手变得可扩展,但设计时要注意平衡:

  • 太简单:功能受限,不够灵活
  • 太复杂:学习成本高,用户不愿意用

nanobot 目前的方案是"约定优于配置":技能目录结构固定,SKILL.md 格式固定,用户只要按规矩写就行。

如果你也在做 AI 助手,强烈建议早点把技能系统搞起来。一开始可能觉得多余,等功能多了就知道香了。

有问题欢迎评论区交流,下一篇聊聊定时任务系统。

Logo

AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。

更多推荐