技能系统:让AI助手拥有“超能力“的插件架构
搞了两天,终于把 nanobot 的技能系统跑通了。说实话,一开始我觉得"插件化"这词挺虚的——不就是动态加载模块吗?但真正做下来才发现,这里面的坑比我想象的多。
今天聊聊怎么给 AI 助手设计一个技能系统,让它能像装 App 一样扩展能力。
为什么要搞技能系统?
最开始 nanobot 只能聊天,后来加了文件操作、Shell 命令、网页搜索……代码越来越臃肿。每次加新功能都要改核心代码,耦合得一塌糊涂。
更麻烦的是,有些功能用户根本用不上。比如我的机器人项目需要人脸识别,但普通用户谁需要这个?
所以技能系统的目标很明确:
- 核心要轻:只保留最基础的能力
- 按需加载:用户想用什么就装什么
- 热插拔:不用重启就能增删技能
技能长什么样?
一个技能就是一个目录,里面至少有一个 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 助手,强烈建议早点把技能系统搞起来。一开始可能觉得多余,等功能多了就知道香了。
有问题欢迎评论区交流,下一篇聊聊定时任务系统。
AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。
更多推荐

所有评论(0)