NoneBot2 插件化 QQ 机器人架构实践

手里的 QQ 机器人经历过好几代框架,最终落在 NoneBot2 上。这篇文章记录迁移的决策过程、插件化架构的分层方式,以及部署阶段踩到的几个坑。

为什么迁移到 NoneBot2

旧方案是”协议端 + 业务脚本”的模式:协议端(NapCat/LLBot 这类 OneBot v11 实现)负责和 QQ 通信,业务脚本通过 HTTP/WebSocket 收事件。规模小的时候很爽,但有三个绕不开的问题:

  1. 事件分发要自己写:优先级、并发控制、超时重试,全靠手搓;
  2. 插件加载没有标准:加功能就是加文件,依赖关系和生命周期混乱;
  3. 权限控制重复造轮子:每个功能都要写一遍群白名单判断。

NoneBot2 把这三件事都标准化了:事件分发、插件加载、权限系统都是框架能力,业务代码只关心”收到什么消息 → 回什么”。

NoneBot2 的适配器与插件分层结构

协议层与框架层的分工

1
2
3
4
5
6
7
QQ 服务器
↓
OneBot v11 协议端(NapCat / LLBot,反向 WebSocket)
↓
NoneBot2(事件分发 / 插件管理 / 权限)
↓
业务插件(命令 / LLM 对话 / 定时任务)

协议端只做协议翻译,NoneBot2 做框架,业务全部插件化。这个分层最大的好处是协议端可以热替换——换协议实现不用动任何业务代码,改个连接配置重启即可。

插件拆分原则

一个机器人跑久了最容易变成”一个文件三千行”。拆分的粒度经验是:一个插件 = 一个可独立关闭的能力。

1
2
3
4
5
6
7
8
9
10
bot/
├── bot.py # 入口:加载插件、注册启动逻辑
├── plugins/
│ ├── chat/ # LLM 对话
│ │ ├── __init__.py # 插件导出
│ │ ├── handlers.py # 消息匹配与响应
│ │ └── prompts.py # 提示词模板
│ ├── admin/ # 群管理命令
│ └── scheduler/ # 定时任务
└── config.yaml # 配置(不进仓库)

原则:

  • 插件之间不直接 import,需要共享状态走 NoneBot2 的依赖注入或插件级 export;
  • 配置按插件拆分,每个插件读自己的配置段,禁用插件 = 注释一行加载列表;
  • 提示词、模板这类”内容”和”逻辑”分文件放,改话术不动代码。

权限与白名单

图中右下角的 Rule 组合就是下面这套写法。群白名单是刚需,用 NoneBot2 的 Rule 实现,一处定义全局生效:

1
2
3
4
5
6
7
8
9
from nonebot.rule import Rule
from nonebot.adapters.onebot.v11 import GroupMessageEvent

async def whitelist_group(event: GroupMessageEvent) -> bool:
return event.group_id in WHITELIST_GROUPS

whitelist = Rule(whitelist_group)

matcher = on_message(rule=whitelist & keyword("天气"), priority=5)

比在每个 handler 里 if group_id not in xxx: return 干净得多,而且规则可以组合复用。

部署:systemd 守护

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
# /etc/systemd/system/qqbot.service
[Unit]
Description=QQBot
After=network-online.target
Wants=network-online.target

[Service]
Type=simple
User=bot
WorkingDirectory=/opt/qqbot
ExecStart=/opt/qqbot/venv/bin/python bot.py
Restart=on-failure
RestartSec=5

[Install]
WantedBy=multi-user.target

常用运维动作:

1
2
3
systemctl restart qqbot     # 重启
journalctl -u qqbot -f # 看日志
systemctl status qqbot # 看状态

Restart=on-failure 保证进程崩了自动拉起,配合 RestartSec 防止疯狂重启拖垮机器。

踩坑:Python 版本兼容性

这是部署阶段最坑的一个问题。宿主机自带的 Python 是 3.14,代码里用到了 f-string 的嵌套引号写法(3.12+ 才支持的语法),在 3.14 上本该没问题——但项目的某个依赖库内部用了旧式 raw string 转义写法,在 3.14 更严格的词法规则下直接 SyntaxError,依赖装得上、导入即爆炸。

解法:不要用系统 Python,建独立 venv 并锁定版本。

1
2
python3.12 -m venv /opt/qqbot/venv
/opt/qqbot/venv/bin/pip install -r requirements.txt

经验:部署环境永远用显式创建的 venv + 锁死的小版本,系统 Python 的升级不受你控制,指不定哪天某个依赖就和新语法规则打架。

LLM 能力接入

对话插件通过统一的 LLM 网关调用模型,好处是换模型只改配置不改代码:

1
2
3
4
5
6
7
async def chat(messages: list[dict]) -> str:
resp = await http_client.post(
f"{GATEWAY_URL}/v1/chat/completions",
headers={"Authorization": f"Bearer {API_KEY}"},
json={"model": MODEL, "messages": messages},
)
return resp.json()["choices"][0]["message"]["content"]

两个实践:

  1. 模型可配置 + 备选模型:主模型额度耗尽时自动降级到备选,配置里写清楚优先级;
  2. 上下文裁剪:群聊场景按”最近 N 条 + 系统提示词”截断,无限堆历史迟早超 token 上限。

小结

NoneBot2 的价值在于把”机器人”这件事的公共部分(事件、插件、权限)标准化了,业务代码可以专注在能力本身。配合 venv 锁版本 + systemd 守护,这套架构可以长期稳定运行。下一步打算把定时任务也插件化,让每个能力真正独立装卸。