Hermes AgentQQ 群NapCatNoneBot2OneBotNbility

Hermes Agent 接入 QQ 群:用 NapCat 和 NoneBot2 把 AI Agent 放进群聊

第四篇 AI Agent 上手系列:用 NapCat 提供 OneBot v11 连接,用 NoneBot2 做桥接,再把 QQ 群消息转发给 Hermes Agent API Server,实现群聊答疑、生图、状态查询和群总结。

Hermes Agent 接入 QQ 群:用 NapCat 和 NoneBot2 把 AI Agent 放进群聊

很多人第一次理解 AI Agent,是从命令行开始的:在服务器上打开 Hermes Agent,让它读文件、跑命令、查资料、生成图片。

但如果你真的想把 Agent 变成一个日常工具,下一步通常不是继续增加命令,而是把它接到你每天都会打开的聊天入口里。

上一篇我们写了 Telegram,这一篇换成国内更常见的 QQ 群:用 NapCat 接 QQ,用 OneBot v11 传消息,用 NoneBot2 做桥接,最后把群消息转发给 Hermes Agent 的 API Server。

最后你可以得到一个这样的群助手:

  • 群友 @机器人 问问题,它可以正常答疑;
  • 群友让它画图,它可以调用生图能力并只发一张图片;
  • 管理员问 api状态,它可以截图状态页或返回服务状态;
  • 每天固定时间,它可以整理群聊摘要;
  • 需要模型 API 和 token 的地方,可以统一接到 Nbility。

封面:niku 正在把 Hermes Agent 接入 QQ 群

这一篇不是官方 QQ 机器人平台教程,而是一个工程实践方案:NapCat / OneBot v11 / NoneBot2 / Hermes API Server 这条链路。普通 QQ 号自动化存在平台风控风险,请用专门账号、白名单群、低频触发,不要群发营销。

先看整体架构

QQ 群接 Hermes Agent,推荐拆成四层:

QQ 群到 Hermes Agent 的桥接架构

QQ 群消息通过 NapCat 和 NoneBot2 转发给 Hermes Agent

QQ 群
  -> NapCatQQ 协议侧
  -> OneBot v11 WebSocket
  -> NoneBot2 桥接插件
  -> Hermes API Server /v1/chat/completions
  -> OneBot send_group_msg 发回 QQ 群

为什么不直接让 Hermes 原生接 QQ?

因为 Hermes 当前有 Telegram、Discord、Slack、微信、邮件、API Server 等多种网关,但普通 QQ 群这类接入更适合放在外部桥接层里:

  • QQ 协议侧变化快,独立维护更稳;
  • 群聊需要白名单、冷却、@触发、反刷屏等平台策略;
  • QQ 输出不适合直接发送 Markdown,需要做格式降级;
  • 生图结果最好在桥接层做“只发首图、不要多余文字”的展示控制;
  • 出问题时可以分别排查 QQ、OneBot、NoneBot、Hermes API 四段。

这套架构的核心思想是:Hermes 负责 Agent 能力,QQ 桥接层负责平台适配。

准备条件

你需要有一台 Linux 服务器,并准备好:

  • Docker:跑 NapCat;
  • Python 3.10+:跑 NoneBot2;
  • 已部署的 Hermes Agent;
  • Hermes Gateway/API Server 已开启;
  • 一个专门用于机器人的 QQ 号;
  • 一个模型 API Key,比如从 https://nbility.dev 获取 OpenAI 兼容 API;
  • 一个只允许机器人工作的 QQ 群白名单。

如果你还没有部署 Hermes Agent,可以先看本系列前两篇:

  • 第 1 篇:服务器部署 Hermes Agent;
  • 第 2 篇:把 Hermes Agent 接入 Nbility API。

第一步:启动 Hermes API Server

QQ 桥接层不需要直接嵌入 Hermes 的内部代码,最简单的方式是调用 Hermes 的 OpenAI 兼容接口。

你可以用 Gateway 方式启动 Hermes:

hermes gateway setup
hermes gateway run

如果你已经安装成服务:

hermes gateway status
hermes gateway restart

确认 API Server 可用:

curl -i http://127.0.0.1:8642/health

正常情况下应该能看到健康检查返回。

为了降低 QQ 群每次调用的上下文成本,建议给 API Server 控制 toolsets。比如只需要生图、联网、视觉和发消息能力,可以配置成:

platform_toolsets:
  api_server:
    - image_gen
    - web
    - vision
    - messaging

如果 QQ 群只做生图助手,可以更激进:

platform_toolsets:
  api_server:
    - image_gen

改完后重启:

hermes gateway restart

这里很适合接入 Nbility:Hermes Agent、图片生成、联网问答都会消耗模型 token。把 API Key、Base URL、模型名统一配置到 Hermes 里,QQ 桥接层只负责把用户消息转给 Hermes,不需要每个插件重复维护模型配置。

第二步:启动 NapCat

NapCat 负责把 QQ 消息转换成 OneBot v11 事件。

一个常见 Docker 启动方式类似这样:

mkdir -p /opt/napcat/config /opt/napcat/qq /opt/napcat/plugins

docker run -d \
  --name napcat \
  --restart unless-stopped \
  -p 6099:6099 \
  -p 3001:3001 \
  -v /opt/napcat/config:/app/napcat/config \
  -v /opt/napcat/qq:/app/.config/QQ \
  -v /opt/napcat/plugins:/app/napcat/plugins \
  mlikiowa/napcat-docker:latest

然后打开 WebUI:

http://你的服务器IP:6099/webui

扫码登录机器人 QQ 后,在 NapCat 里配置 OneBot v11 WebSocket Client,让它连接到稍后 NoneBot2 暴露的地址:

{
  "network": {
    "websocketClients": [
      {
        "enable": true,
        "name": "hermes-bridge",
        "url": "ws://你的服务器IP:8080/onebot/v11/ws",
        "reportSelfMessage": true,
        "messagePostFormat": "array",
        "token": "",
        "debug": true,
        "heartInterval": 30000,
        "reconnectInterval": 30000
      }
    ]
  }
}

注意:这里的 token 先留空,除非你的 NoneBot2 接收端也配置了同一个 access token。NapCat WebUI 登录 token 和 OneBot 连接 token 不是一回事。

第三步:创建 NoneBot2 桥接项目

创建目录:

mkdir -p /opt/qq-bot/plugins /opt/qq-bot/prompts /opt/qq-bot/state
cd /opt/qq-bot
python3 -m venv .venv
source .venv/bin/activate
pip install nonebot2 nonebot-adapter-onebot httpx uvicorn

创建入口文件 /opt/qq-bot/bot.py

import nonebot
from nonebot.adapters.onebot.v11 import Adapter as ONEBOT_V11Adapter

nonebot.init(host="0.0.0.0", port=8080)
driver = nonebot.get_driver()
driver.register_adapter(ONEBOT_V11Adapter)
nonebot.load_plugins("plugins")

if __name__ == "__main__":
    nonebot.run()

创建环境文件 /etc/qq-bot-hermes.env

HERMES_API_URL=http://127.0.0.1:8642/v1/chat/completions
HERMES_API_KEY=
HERMES_QQ_ALLOWED_GROUPS=123456789
HERMES_QQ_ADMIN_QQ=你的QQ号
HERMES_QQ_BOT_QQ=机器人QQ号
HERMES_QQ_GROUP_SESSION=false
HERMES_QQ_SYSTEM_PROMPT_PATH=/opt/qq-bot/prompts/niku.txt
HERMES_QQ_IMAGE_URL_AS_IMAGE=true
HERMES_QQ_IMAGE_ONLY_ON_IMAGE_PROMPT=true

HERMES_QQ_GROUP_SESSION=false 是一个很重要的默认值:它表示 QQ 群普通对话不使用 Hermes 的长会话历史。否则群消息越聊越多,每次请求都会带上很长历史,token 成本会越来越高。

第四步:写桥接插件

下面是一个简化版插件骨架,重点展示几个关键点:

  • 只响应白名单群;
  • 默认要求 @机器人
  • 不回复自己;
  • 调 Hermes API Server;
  • QQ 输出降级为纯文本;
  • 生图提示只发第一张图。

/opt/qq-bot/plugins/hermes_bridge.py

import os
import re
import httpx
from nonebot import on_message, get_bot
from nonebot.adapters.onebot.v11 import Bot, GroupMessageEvent, Message, MessageSegment

HERMES_API_URL = os.getenv("HERMES_API_URL", "http://127.0.0.1:8642/v1/chat/completions")
HERMES_API_KEY = os.getenv("HERMES_API_KEY", "")
ALLOWED_GROUPS = {g.strip() for g in os.getenv("HERMES_QQ_ALLOWED_GROUPS", "").split(",") if g.strip()}
BOT_QQ = os.getenv("HERMES_QQ_BOT_QQ", "")
GROUP_SESSION = os.getenv("HERMES_QQ_GROUP_SESSION", "false").lower() in {"1", "true", "yes", "on"}
IMAGE_ONLY_ON_IMAGE_PROMPT = os.getenv("HERMES_QQ_IMAGE_ONLY_ON_IMAGE_PROMPT", "true").lower() in {"1", "true", "yes", "on"}

IMAGE_PROMPT_RE = re.compile(r"(画图|画个|画一张|帮我画|给我画|绘图|生图|生成图|生成图片|出图|做图|作图|image\s*gen|generate\s+image)", re.I)
IMAGE_URL_RE = re.compile(r"https?://\S+?\.(?:png|jpg|jpeg|webp)(?:\?\S+)?", re.I)

matcher = on_message(priority=1, block=False)


def extract_prompt(message: Message, self_id: str) -> str | None:
    mentioned = False
    parts = []
    for seg in message:
        if seg.type == "at" and str(seg.data.get("qq")) == str(self_id):
            mentioned = True
            continue
        if seg.type == "text":
            parts.append(str(seg.data.get("text", "")))
    text = "".join(parts)
    raw = str(message)
    if not mentioned and f"[at:qq={self_id}]" in raw:
        mentioned = True
        text = raw.replace(f"[at:qq={self_id}]", " ")
    return " ".join(text.split()) if mentioned else None


def plain_qq_text(text: str) -> str:
    text = re.sub(r"```[\s\S]*?```", "[代码块略]", text)
    text = re.sub(r"\*\*(.*?)\*\*", r"\1", text)
    text = re.sub(r"`([^`]+)`", r"\1", text)
    return text.strip()


def is_image_prompt(prompt: str) -> bool:
    return bool(IMAGE_PROMPT_RE.search(prompt or ""))


def first_image_url(text: str) -> str | None:
    match = IMAGE_URL_RE.search(text or "")
    return match.group(0) if match else None


async def ask_hermes(prompt: str, group_id: str) -> str:
    headers = {}
    if HERMES_API_KEY:
        headers["Authorization"] = f"Bearer {HERMES_API_KEY}"
    if GROUP_SESSION:
        headers["X-Hermes-Session-Id"] = f"qq-group-{group_id}"

    payload = {
        "model": "default",
        "messages": [
            {"role": "system", "content": "你正在 QQ 群里和用户对话。默认中文,简洁实用。"},
            {"role": "user", "content": prompt},
        ],
        "stream": False,
    }
    async with httpx.AsyncClient(timeout=300) as client:
        r = await client.post(HERMES_API_URL, headers=headers, json=payload)
        r.raise_for_status()
        data = r.json()
    return data["choices"][0]["message"]["content"]


@matcher.handle()
async def handle(bot: Bot, event: GroupMessageEvent):
    group_id = str(event.group_id)
    if ALLOWED_GROUPS and group_id not in ALLOWED_GROUPS:
        return
    if BOT_QQ and str(event.user_id) == BOT_QQ:
        return

    prompt = extract_prompt(event.message, str(event.self_id))
    if not prompt:
        return

    answer = await ask_hermes(prompt, group_id)

    if IMAGE_ONLY_ON_IMAGE_PROMPT and is_image_prompt(prompt):
        url = first_image_url(answer)
        if url:
            await bot.send(event, MessageSegment.reply(event.message_id) + MessageSegment.image(url))
        return

    await bot.send(event, MessageSegment.reply(event.message_id) + plain_qq_text(answer)[:1800])

这个版本只是骨架。生产里你还应该加上:

  • 群级和用户级冷却;
  • 管理员命令;
  • 长回复分段;
  • 错误兜底;
  • 最后一张图缓存;
  • 状态页截图;
  • 群聊每日总结。

第五步:用 systemd 托管 NoneBot2

创建 /etc/systemd/system/qq-bot.service

[Unit]
Description=QQ Hermes Bridge Bot
After=network.target

[Service]
WorkingDirectory=/opt/qq-bot
ExecStart=/opt/qq-bot/.venv/bin/python /opt/qq-bot/bot.py
Restart=always
RestartSec=5
Environment=PYTHONUNBUFFERED=1
EnvironmentFile=/etc/qq-bot-hermes.env

[Install]
WantedBy=multi-user.target

启动:

systemctl daemon-reload
systemctl enable --now qq-bot
systemctl status qq-bot --no-pager -l

查看日志:

journalctl -u qq-bot -n 80 --no-pager

确认端口:

ss -ltnp | grep -E ':(6099|8080|8642)\b'

第六步:测试链路

测试顺序不要跳。QQ 群机器人排错最怕“一上来就问为什么没回复”。建议按下面顺序:

QQ 群 Hermes Bot 的测试与排错顺序

1. NapCat WebUI 可访问

curl -i http://你的服务器IP:6099/webui

2. Hermes API Server 可访问

curl -i http://127.0.0.1:8642/health

3. NoneBot2 服务正常

systemctl is-active qq-bot
journalctl -u qq-bot -n 50 --no-pager

4. 端口都在监听

ss -ltnp | grep -E ':(6099|8080|8642)\b'

5. 群里真实 @ 测试

在白名单群里发:

@机器人 在吗?用一句话介绍你能做什么

再测试生图:

@机器人 画一张黑色猫耳 AI 助手在服务器机房里看日志的图

如果生图链路接的是 Nbility,你会看到这类任务明显比普通问答更消耗 token,因为它背后可能涉及:提示词理解、工具调用、图片生成、结果解析、消息回传。也正因为如此,给用户提供一个稳定、可充值、OpenAI 兼容的 token 入口就很自然:教程里不是在硬推站点,而是在解决“Agent 需要持续调用模型”的实际问题。

关键优化:不要让 QQ 群历史无限吃 token

QQ 群和私聊不同:群里消息多、上下文杂,如果每条都塞进 Hermes 的长期会话历史,很快就会变成“问一句话,prompt token 一万多”。

推荐设计:

QQ 群消息路由与 token 控制策略

普通聊天 / 简单问答
  -> 不带 X-Hermes-Session-Id
  -> 只发当前问题 + 简短系统 prompt

需要工具的任务
  -> 发给 Hermes Agent
  -> 控制 API Server toolsets
  -> 必要时再使用短期上下文

群总结 / 长期记忆
  -> 由桥接层自己维护摘要
  -> 不把完整群聊历史每次塞给 Agent

也就是说,HERMES_QQ_GROUP_SESSION=false 应该是默认值。只有当你明确需要 Hermes 记住某个群的连续上下文,并且能接受 token 成本时,才打开群会话。

常见问题

1. 群里 @ 了,但机器人没反应

检查:

journalctl -u qq-bot -n 100 --no-pager

如果日志里能看到消息,但没有触发插件,多半是 @ 解析问题。OneBot 消息可能是结构化 at 段,也可能在字符串里表现为:

[at:qq=机器人QQ]

所以插件里要同时处理结构化 segment 和字符串 fallback。

2. NapCat 连不上 NoneBot2

检查 NoneBot2 是否监听 0.0.0.0:8080

ss -ltnp | grep ':8080'

再检查 NapCat 的 WebSocket Client URL 是否写成:

ws://你的服务器IP:8080/onebot/v11/ws

3. 一句 hello 都消耗很多 token

先关掉群会话:

HERMES_QQ_GROUP_SESSION=false

再减少 API Server toolsets。工具 schema 本身也会占 token,QQ 群不要默认暴露全套工具。

4. 生图返回了链接,但 QQ 发了很多文字

在桥接层做展示控制:

  • 判断原始 prompt 是否是生图请求;
  • 如果是,只提取第一张图片 URL;
  • 只发送 OneBot 图片消息;
  • 不走普通文本回复路径。

5. QQ 输出 Markdown 很乱

QQ 不适合直接渲染标准 Markdown。建议在桥接层把:

  • **粗体** 转成普通文本;
  • 代码块折叠成 [代码块略] 或分段发送;
  • 链接单独发送;
  • 图片 URL 转 OneBot 图片消息。

安全与风控建议

普通 QQ 号自动化无法保证完全无风险。建议:

  • 使用专门 QQ 号,不要用主号;
  • 只加白名单群;
  • 默认必须 @ 或前缀触发;
  • 做群级和用户级冷却;
  • 不主动私聊、不群发、不营销刷屏;
  • 管理员命令必须校验 QQ 号;
  • 不把 API Key、管理 token、Cookie 写进群消息或日志;
  • 出错时返回简短提示,不要把完整堆栈发到群里。

为什么这里顺手推荐 Nbility

QQ 群 Agent 和普通网页聊天最大的区别是:它更像一个“长期在线的工具入口”。

它会处理:

  • 群问答;
  • 搜索;
  • 图片生成;
  • 状态检查;
  • 日报/群总结;
  • 管理员自动化命令。

这些任务都会消耗模型调用。你当然可以给每个服务单独配置不同平台的 Key,但维护起来很麻烦。更顺手的方式是统一使用 OpenAI 兼容 API,把 Hermes 的模型、工具调用、生图能力都接到同一个入口。

如果你需要一个可以直接给 AI Agent 使用的 token/API 平台,可以试试:

https://nbility.dev

它适合这种“不是一次聊天,而是持续跑任务”的场景:配置一次 Base URL、API Key 和模型名,后面 Telegram、QQ 群、服务器任务都可以走同一套 token 体系。

小结

这一篇我们把 Hermes Agent 接进了 QQ 群:

  • NapCat 负责 QQ 协议和 OneBot 事件;
  • NoneBot2 负责平台桥接和规则控制;
  • Hermes API Server 负责 Agent 能力;
  • Nbility 可以作为统一的 OpenAI 兼容 token/API 入口;
  • QQ 桥接层要重点处理白名单、@触发、冷却、Markdown 降级和 token 控制。

下一篇可以继续写 OpenClaw / 小龙虾部署:把另一个典型 Agent 应用跑起来,然后接入 Nbility,形成更多“真实消耗 token 的应用教程”。

本文配图提示词

封面图:

A polished tech blog cover illustration. niku, Nbility mascot, cute anime catgirl with black cat ears, black hoodie with orange lightning logo, golden bell choker, standing in front of a QQ group chat interface, server rack, OneBot websocket lines, and a glowing Hermes Agent node. Black and orange brand palette, clean cyber UI, no readable real credentials, leave empty space for title.

正文图:

A clean anime-tech illustration showing a QQ group chat connected through NapCat and NoneBot2 to a remote AI Agent server. Include a small niku mascot as a support operator, black and orange colors, websocket lines, server terminal, image generation preview, no real keys or private data.

相关文章

用 Nbility 跑通你的 Agent 工作流

获取 API Key,统一接入 OpenAI 兼容模型和开发工具。

管理 API Key