NoneBot2 插件化 QQ 机器人架构实践
手里的 QQ 机器人经历过好几代框架,最终落在 NoneBot2 上。这篇文章记录迁移的决策过程、插件化架构的分层方式,以及部署阶段踩到的几个坑。
为什么迁移到 NoneBot2
旧方案是”协议端 + 业务脚本”的模式:协议端(NapCat/LLBot 这类 OneBot v11 实现)负责和 QQ 通信,业务脚本通过 HTTP/WebSocket 收事件。规模小的时候很爽,但有三个绕不开的问题:
- 事件分发要自己写:优先级、并发控制、超时重试,全靠手搓;
- 插件加载没有标准:加功能就是加文件,依赖关系和生命周期混乱;
- 权限控制重复造轮子:每个功能都要写一遍群白名单判断。
NoneBot2 把这三件事都标准化了:事件分发、插件加载、权限系统都是框架能力,业务代码只关心”收到什么消息 → 回什么”。
协议层与框架层的分工
1 | QQ 服务器 |
协议端只做协议翻译,NoneBot2 做框架,业务全部插件化。这个分层最大的好处是协议端可以热替换——换协议实现不用动任何业务代码,改个连接配置重启即可。
插件拆分原则
一个机器人跑久了最容易变成”一个文件三千行”。拆分的粒度经验是:一个插件 = 一个可独立关闭的能力。
1 | bot/ |
原则:
- 插件之间不直接 import,需要共享状态走 NoneBot2 的依赖注入或插件级
export; - 配置按插件拆分,每个插件读自己的配置段,禁用插件 = 注释一行加载列表;
- 提示词、模板这类”内容”和”逻辑”分文件放,改话术不动代码。
权限与白名单
图中右下角的 Rule 组合就是下面这套写法。群白名单是刚需,用 NoneBot2 的 Rule 实现,一处定义全局生效:
1 | from nonebot.rule import Rule |
比在每个 handler 里 if group_id not in xxx: return 干净得多,而且规则可以组合复用。
部署:systemd 守护
1 | # /etc/systemd/system/qqbot.service |
常用运维动作:
1 | systemctl restart qqbot # 重启 |
Restart=on-failure 保证进程崩了自动拉起,配合 RestartSec 防止疯狂重启拖垮机器。
踩坑:Python 版本兼容性
这是部署阶段最坑的一个问题。宿主机自带的 Python 是 3.14,代码里用到了 f-string 的嵌套引号写法(3.12+ 才支持的语法),在 3.14 上本该没问题——但项目的某个依赖库内部用了旧式 raw string 转义写法,在 3.14 更严格的词法规则下直接 SyntaxError,依赖装得上、导入即爆炸。
解法:不要用系统 Python,建独立 venv 并锁定版本。
1 | python3.12 -m venv /opt/qqbot/venv |
经验:部署环境永远用显式创建的 venv + 锁死的小版本,系统 Python 的升级不受你控制,指不定哪天某个依赖就和新语法规则打架。
LLM 能力接入
对话插件通过统一的 LLM 网关调用模型,好处是换模型只改配置不改代码:
1 | async def chat(messages: list[dict]) -> str: |
两个实践:
- 模型可配置 + 备选模型:主模型额度耗尽时自动降级到备选,配置里写清楚优先级;
- 上下文裁剪:群聊场景按”最近 N 条 + 系统提示词”截断,无限堆历史迟早超 token 上限。
小结
NoneBot2 的价值在于把”机器人”这件事的公共部分(事件、插件、权限)标准化了,业务代码可以专注在能力本身。配合 venv 锁版本 + systemd 守护,这套架构可以长期稳定运行。下一步打算把定时任务也插件化,让每个能力真正独立装卸。