之前一直想给 QQ 群接入一个“真正能聊起来”的 AI 机器人,但试过几种方案之后发现一个问题:要么回复太机械,要么每条消息都秒回,看起来特别假。后来用 DeepSeek 官方 API 配合 OneBot 11 协议自己写了一个机器人,加入了“概率回复”和“拟人化提示词”之后,群聊体验一下子自然了很多。
这篇文章就把全过程整理出来,从环境准备、协议端配置、Python 代码实现,到最后的常见报错排查,全部展开。适合有一定 Python 基础、想给 QQ 群接入 AI 聊天的开发者,也适合第一次接触 OneBot 和 DeepSeek API 的新手。文章里的代码都是可以直接复制运行的,按步骤配置就能跑通。
1. 背景与整体架构
1.1 为什么要做“拟人化聊天”
很多 QQ 机器人的实现方式,是把关键词和固定回复做成映射表。这种方案维护成本低,但只能处理预设内容,一旦群友换个说法,机器人就“掉线”了。接入大模型之后,机器人终于能理解自然语言了,但新的问题又出现了:大模型默认的回复风格太“官方”。
比如你发一句“今天好累”,普通 API 接入的机器人可能回:“听起来你今天很疲惫,建议适当休息,保持良好的作息习惯。”这种话放在真实的群聊里,会显得非常突兀。真人通常会说:“确实,周一的班谁上谁累。”
拟人化聊天的核心,就是通过系统提示词、随机人设、温度和字数限制,让模型输出更像一个普通网友,而不是 AI 助手。
1.2 什么是“概率回复”
概率回复的意思是:不是每条消息都触发回复,而是按照一定概率决定要不要回。这个设计很关键,因为真实群聊中,没有人会接每一句话。有时候看到消息觉得没必要回,就默默划过去了。
如果机器人每条消息都回,会出现两个问题:
- 群聊刷屏,群友体验差,容易被屏蔽。
- 高频连续回复容易触发平台风控。
通过概率回复,机器人可以模拟“偶尔冒个泡”的真实存在感。本文还会把“被 @ 时必定回复”作为一个例外逻辑,保证用户主动呼叫时不会漏掉。
1.3 整体架构流程
整个系统的数据链路如下:
QQ 群内用户消息 ↓ OneBot 协议端(NapCat / Lagrange / LLOneBot 等) ↓ 正向 WebSocket(ws://127.0.0.1:3001) Python 机器人主程序(bot.py) ↓ HTTPS 调用 DeepSeek 官方 API ↓ 返回回复文本 机器人按原通道发回群聊OneBot 11 是一套通用的 QQ 机器人通信协议。它负责把 QQ 收到的消息转换成标准 JSON 事件,再通过正向 WebSocket 推给我们的 Python 程序。Python 程序只需要关心事件格式、调用 DeepSeek API、再发送回复动作,不需要直接接触 QQ 底层协议。
2. 环境准备与版本说明
2.1 需要的软件环境
开始之前,先确认以下环境:
- Python 3.8 及以上版本,推荐 3.10+。
- 一个 OneBot 11 协议实现端,社区常用有 NapCat、Lagrange、LLOneBot 等,选一个就行。
- DeepSeek 开放平台账号,并创建 API Key。
- 一个用于登录机器人的 QQ 号。
版本说明:本文示例代码基于websockets和httpx这两个 Python 库,版本号见下文 requirements.txt。OneBot 协议端的具体版本不需要完全一致,只要支持 OneBot 11 正向 WebSocket 即可。
2.2 安装 Python 依赖
建议在项目目录下创建虚拟环境,避免依赖冲突:
python -m venv venv # Windows venv\Scripts\activate # Linux / macOS source venv/bin/activate pip install -r requirements.txtrequirements.txt内容如下:
websockets==12.0 httpx==0.27.0websockets是 Python 的 WebSocket 客户端库,负责连接 OneBot 正向 WebSocket。httpx用来调用 DeepSeek API,支持异步请求。
2.3 申请 DeepSeek API Key
打开 DeepSeek 开放平台,注册账号后进入“API Keys”页面,创建一个新的 Key。创建后立即复制保存,因为页面刷新后不会再次显示完整 Key。
这里有一个需要注意的点:新注册的 DeepSeek 账号需要充值后才能正常调用 API,否则接口可能返回余额不足或402 Payment Required。充值金额不用太多,日常测试和群聊完全够用。API Key 是敏感信息,建议放到环境变量中,不要硬编码在代码里,更不要提交到 Git 仓库。
2.4 配置 OneBot 协议端
以 NapCat 为例(其他协议端类似):
- 下载对应系统的 release 包并解压。
- 启动程序,扫描二维码登录机器人 QQ 号。
- 在管理后台或配置文件中,开启“正向 WebSocket”服务。
- 保持默认端口 3001,记下这个端口。
启动成功后,协议端日志应该会显示类似下面这样的内容:
WebSocket 服务已启动: ws://0.0.0.0:3001如果开启了 access_token 鉴权,需要记下 token,后续 Python 连接时要配置。不建议在局域网外暴露 WebSocket 端口,避免被陌生人连接后操纵机器人。
3. 核心概念拆解
3.1 DeepSeek API 使用要点
DeepSeek 的接口兼容 OpenAI 的 Chat Completions 格式。我们可以直接通过 HTTP 请求调用,不依赖额外的 SDK。核心信息如下:
- 请求地址:
https://api.deepseek.com/chat/completions - 请求头:
Authorization: Bearer <API_KEY> - 请求体:包含
model、messages、temperature、max_tokens等字段。
常见的模型名有两个:
deepseek-chat:通用对话模型,适合聊天、写作、日常问答。deepseek-reasoner:带推理链的模型,适合逻辑分析,但回复内容更长,聊天场景不一定合适。本文使用deepseek-chat。
temperature是拟人化最关键的一个参数,取值范围 0~2。值越大,回复越发散、口语化,但也更不稳定。聊天场景可以设置 1.2 左右,比默认的 1.0 更有随口感;如果是做知识问答机器人,建议设到 0.3~0.7,保证准确性。
max_tokens控制单次回复的最大 token 数。群聊场景建议 80~150,太长会显得像论文,而且降低聊天节奏感。
3.2 OneBot 11 的消息事件格式
OneBot 11 会把消息统一成 JSON 事件。正向 WebSocket 模式下,客户端连接后,服务端每收到一条消息就会推送一个事件。
群消息事件示例如下:
{ "post_type": "message", "message_type": "group", "group_id": 123456, "user_id": 987654, "message": "你好", "raw_message": "你好", "sender": { "nickname": "小明" } }私聊事件结构与群消息类似,区别是message_type为private,会话对象通过user_id标识。
如果机器人要回复群消息,Python 端需要发送一个 action:
{ "action": "send_group_msg", "params": { "group_id": 123456, "message": "你好呀" } }私聊则发送send_private_msg,参数换成user_id。
还有一个细节需要注意:群消息里的图片、@、表情,在 OneBot 11 中通常以 CQ 码形式出现在raw_message中,例如[CQ:at,qq=123]、[CQ:image,file=xxx]。直接把这些内容喂给 DeepSeek,模型会看到奇怪的标签字符串。所以代码里需要先过滤掉 CQ 码,只保留纯文本。
3.3 拟人化聊天的几个设计维度
想让 AI 回复像真人,需要从多个维度同时下手,只靠一个系统提示词是不够的。
- 系统提示词:明确告诉模型“你是普通网友,不要说教,不要自我解释”。这是最有效的手段,直接决定输出风格。
- 随机人设:准备多套系统提示词,每次随机选一套。这样聊久了不会因为单一风格让人腻,更像不同状态下的真实网友。
- 高温参数:适当调高 temperature,回复会有更多自然变化。
- 短回复:限制 max_tokens,让模型被迫说短句。短句更容易像真人,长篇大论一眼就露馅。
- 上下文记忆:只保留最近几轮对话,让连续聊天有连贯性,但不会因为历史太长而跑题。
3.4 概率回复的触发逻辑设计
本文的触发逻辑采用“分层判断”:
- 先判断消息类型是群聊还是私聊。
- 如果是群聊,先看消息里是否 @ 了机器人。被 @ 说明用户在显式呼叫,概率设为 100%。
- 如果没有 @,则按配置的概率值随机决定是否回复,默认 35%。
- 私聊场景回复概率默认 100%,因为用户主动找机器人私聊,基本就是想互动。
- 最后加一个冷却时间兜底:同一个群或同一个私聊会话,短时间(如 15 秒)内最多回复一次。
这套逻辑兼顾了“拟人化”和“可用性”。
4. 完整实战案例
4.1 创建项目结构
在本地新建一个项目目录:
qq_deepseek_bot/ ├── requirements.txt ├── config.py ├── deepseek_client.py └── bot.py其中config.py存放所有配置项,deepseek_client.py封装 DeepSeek API 调用,bot.py是机器人主逻辑。
4.2 编写配置文件 config.py
文件路径:qq_deepseek_bot/config.py
import os # DeepSeek 配置 DEEPSEEK_API_KEY = os.getenv("DEEPSEEK_API_KEY", "sk-你的key") DEEPSEEK_BASE_URL = "https://api.deepseek.com" DEEPSEEK_MODEL = "deepseek-chat" # OneBot 正向 WebSocket 配置 WS_URL = "ws://127.0.0.1:3001" WS_TOKEN = os.getenv("WS_TOKEN", "") # 如果 OneBot 端开了 access_token 就填 # 机器人 QQ 号,用于判断是否被 @ BOT_QQ = "123456789" # 回复策略 GROUP_REPLY_PROBABILITY = 0.35 # 群聊普通消息触发概率 PRIVATE_REPLY_PROBABILITY = 1.0 # 私聊触发概率 MAX_HISTORY = 10 # 保留的上下文轮数 COOLDOWN_SECONDS = 15 # 同一会话最小回复间隔 # DeepSeek 生成参数 TEMPERATURE = 1.2 MAX_TOKENS = 120配置说明:
DEEPSEEK_API_KEY优先从环境变量读取,避免密钥硬编码。BOT_QQ必须改成你自己的机器人 QQ 号,否则 @ 判断会失效。GROUP_REPLY_PROBABILITY是群聊普通消息的回复概率,0.35 表示大约三分之一的消息会触发回复。COOLDOWN_SECONDS是防刷屏兜底,同一个群 15 秒内最多回复一次。
4.3 编写 DeepSeek 客户端 deepseek_client.py
文件路径:qq_deepseek_bot/deepseek_client.py
import httpx class DeepSeekClient: def __init__(self, api_key: str, base_url: str): self.api_key = api_key self.base_url = base_url self.headers = { "Authorization": f"Bearer {api_key}", "Content-Type": "application/json", } async def chat( self, messages: list, model: str = "deepseek-chat", temperature: float = 1.2, max_tokens: int = 120, ) -> str: url = f"{self.base_url}/chat/completions" payload = { "model": model, "messages": messages, "temperature": temperature, "max_tokens": max_tokens, "stream": False, } async with httpx.AsyncClient(timeout=30) as client: resp = await client.post(url, json=payload, headers=self.headers) if resp.status_code != 200: print(f"[DeepSeek 异常] HTTP {resp.status_code}: {resp.text}") resp.raise_for_status() data = resp.json() return data["choices"][0]["message"]["content"].strip()这个类的职责很单一:接收消息列表,返回 DeepSeek 的文本回复。非 200 状态码时打印完整响应体,方便定位是密钥问题、余额问题还是参数问题。
4.4 编写机器人主逻辑 bot.py
文件路径:qq_deepseek_bot/bot.py
import asyncio import json import random import re import time from collections import defaultdict from websockets import connect, exceptions from config import ( BOT_QQ, COOLDOWN_SECONDS, DEEPSEEK_API_KEY, DEEPSEEK_BASE_URL, DEEPSEEK_MODEL, GROUP_REPLY_PROBABILITY, MAX_HISTORY, MAX_TOKENS, PRIVATE_REPLY_PROBABILITY, TEMPERATURE, WS_TOKEN, WS_URL, ) from deepseek_client import DeepSeekClient # 按会话维度保存最近对话上下文 context_map = defaultdict(list) # 记录每个会话最后一次回复时间,用于冷却 last_reply_time = defaultdict(float) # 多套人设,每次随机抽取,避免风格固定 PERSONAS = [ { "role": "system", "content": ( "你是一个普通QQ群网友,性格随和,说话口语化。" "偶尔可以用网络用语,但不要过度热情,不要老是发emoji。" "回复控制在3句话以内,不要用'作为人工智能'这类开头," "不要反问太多问题,不要说教。" ), }, { "role": "system", "content": ( "你是一个有点幽默感的群聊搭子,喜欢接梗和吐槽,但不要阴阳怪气。" "说话自然随意,字数尽量控制在30字以内。" "不要分析对方心理,不要长篇大论。" ), }, ] CQ_CODE_RE = re.compile(r"\[CQ:[^\]]*\]") def clean_message(raw_message: str) -> str: """去掉 CQ 码,只保留纯文本内容""" return CQ_CODE_RE.sub("", raw_message or "").strip() def is_at_bot(raw_message: str) -> bool: """判断消息中是否 @ 了机器人""" return f"[CQ:at,qq={BOT_QQ}]" in (raw_message or "") def should_reply(event: dict, raw_message: str) -> bool: """根据消息类型和概率决定是否回复""" msg_type = event.get("message_type", "") if msg_type == "group": if is_at_bot(raw_message): return True return random.random() < GROUP_REPLY_PROBABILITY if msg_type == "private": return random.random() < PRIVATE_REPLY_PROBABILITY return False def build_action(event: dict, reply: str): """根据消息来源构造对应的发送 action""" msg_type = event.get("message_type", "") if msg_type == "group": return { "action": "send_group_msg", "params": { "group_id": event["group_id"], "message": reply, }, } if msg_type == "private": return { "action": "send_private_msg", "params": { "user_id": event["user_id"], "message": reply, }, } return None async def handle_event(ws, event: dict, client: DeepSeekClient): """处理单条消息事件""" if event.get("post_type") != "message": return raw_message = event.get("raw_message") or event.get("message") or "" text = clean_message(raw_message) if not text: return msg_type = event.get("message_type", "") if msg_type == "group": key = f"group_{event['group_id']}" elif msg_type == "private": key = f"user_{event['user_id']}" else: return # 概率是否回复 if not should_reply(event, raw_message): return # 冷却检查 now = time.time() if now - last_reply_time[key] < COOLDOWN_SECONDS: return last_reply_time[key] = now # 组装 messages:随机人设 + 最近上下文 + 当前消息 history = context_map[key] messages = [random.choice(PERSONAS)] + history[-MAX_HISTORY:] + [ {"role": "user", "content": text} ] try: reply = await client.chat( messages, model=DEEPSEEK_MODEL, temperature=TEMPERATURE, max_tokens=MAX_TOKENS, ) except Exception as exc: print(f"[DeepSeek 调用失败] {exc}") return if not reply: return # 更新上下文 context_map[key].append({"role": "user", "content": text}) context_map[key].append({"role": "assistant", "content": reply}) context_map[key] = context_map[key][-MAX_HISTORY:] # 发送回复 action = build_action(event, reply) if action: await ws.send(json.dumps(action)) print(f"[回复] {key}: {reply}") async def main(): client = DeepSeekClient(DEEPSEEK_API_KEY, DEEPSEEK_BASE_URL) headers = {"Authorization": f"Bearer {WS_TOKEN}"} if WS_TOKEN else {} while True: try: print(f"正在连接 OneBot WebSocket: {WS_URL}") async with connect(WS_URL, additional_headers=headers) as ws: print("连接成功,等待消息...") async for raw in ws: try: event = json.loads(raw) await handle_event(ws, event, client) except json.JSONDecodeError: continue except exceptions.ConnectionClosed as exc: print(f"连接已断开: {exc}") except Exception as exc: print(f"连接异常: {exc}") print("3 秒后尝试重连...") await asyncio.sleep(3) if __name__ == "__main__": asyncio.run(main())代码逻辑说明:
context_map使用defaultdict(list),按group_群号或user_QQ号维度保存对话历史,这样每个群的上下文互不干扰。PERSONAS是多套系统提示词,每次随机选一套,这是拟人化保鲜的核心设计。clean_message用正则去掉图片、@ 等 CQ 码,避免把[CQ:image,...]这类内容传给 DeepSeek。build_action根据群聊/私聊构造不同的发送 action。- 主循环使用
async for监听消息,连接断开后自动重连。
需要注意,Python 版本建议 3.8 以上,代码使用了asyncio和websockets的异步语法。
4.5 配置 OneBot 正向 WebSocket
以 NapCat 为例,运行后进入配置页面:
- 找到“网络服务”或“OneBot”设置。
- 选择“正向 WebSocket”。
- 确认监听端口,默认 3001。
- 如果配置了 access_token,在
config.py里填上对应的 token。 - Python 端的
WS_URL设置为ws://127.0.0.1:3001。
这里要注意:同一时间只能有一个机器人协议端登录同一个 QQ 号,否则会出现互踢情况。如果之前用手机或电脑登录过该 QQ,建议先退出,再让协议端登录。
4.6 运行与验证
先启动 OneBot 协议端并登录机器人 QQ,然后启动 Python 程序:
python bot.py看到以下日志说明连接成功:
正在连接 OneBot WebSocket: ws://127.0.0.1:3001 连接成功,等待消息...验证步骤:
- 私聊机器人,发送一条消息,正常情况下机器人会立即回复。
- 在群里发一条普通消息,有 35% 概率触发回复。
- 在群里 @ 机器人,必定触发回复。
如果希望快速验证链路是否通,可以临时把概率调到 1.0,确认没问题后再调回正常值。
5. 常见问题与排查思路
| 问题现象 | 常见原因 | 解决思路 |
|---|---|---|
| Python 连不上 WebSocket | OneBot 端未开启正向 WebSocket,或端口不一致 | 检查协议端日志和监听端口,确认 WS_URL 正确 |
| 连接后一直收不到消息 | 机器人 QQ 未登录,或登录状态异常 | 检查协议端二维码登录状态,重新扫码 |
| API 返回 401 | API Key 错误或未正确配置 | 检查环境变量和代码中的 Key,确认没有多余空格 |
| API 返回余额不足或 402 | DeepSeek 账户未充值 | 登录开放平台充值后重试 |
| 无响应但日志正常 | 概率没命中,或冷却时间拦截 | 临时把概率设为 1.0、冷却设为 0 排查 |
| 回复里出现 CQ 码标签 | 没有清理 CQ 码 | 确认代码中调用了 clean_message |
| 群里刷屏被限制 | 回复频率太高 | 调大 COOLDOWN_SECONDS,调低回复概率 |
| 回复太官方、太像 AI | system prompt 不够明确 | 换更口语化的人设,调高 temperature,调小 max_tokens |
几个排查要点:
- 所有问题先看日志:连接成功了吗?DeepSeek 是否返回了内容?是哪个环节断了?
- 不确定概率逻辑时,先把两个概率都改成 1.0,保证链路可复现。
- 不要用同一个仓库保存 API Key;如果怀疑 Key 泄露,第一时间到平台删除并重建。
6. 最佳实践与工程建议
6.1 密钥与配置管理
API Key 一定不要硬编码在代码里。建议使用环境变量,或者本地.env文件,并在.gitignore中忽略:
export DEEPSEEK_API_KEY=sk-xxxxxxxx export WS_TOKEN=xxxxxx如果多人协作,配置模板可以提交,真实密钥不提交。团队内部可以统一走密钥管理服务。
6.2 回复频率与风控
- 同一个群设置冷却时间,例如 10~20 秒。
- 群聊概率不要设太高,建议 20%~40%。
- 不要对每条 @ 都秒回,可以加 1~2 秒随机延迟,模拟人类查看消息和打字的时间。
添加随机延迟的简单方式:
import asyncio import random # 发送回复前增加延迟 await asyncio.sleep(random.uniform(0.5, 2.0))6.3 上下文与性能优化
- 上下文只保留最近 N 轮,默认 10 轮足够日常聊天。
- 如果机器人会加入很多群,内存字典会持续增长,建议定期清理长时间不活跃的会话。
- 生产环境可以换成 Redis 存储上下文,方便多实例部署和横向扩容。
清理不活跃会话的思路是:记录每个 key 的最后活跃时间,定时把超过 1 小时没发言的会话从字典中删除。
6.4 合规与安全边界
- 本文方案仅用于个人学习和小范围测试,请遵守 QQ 平台的使用规范和社区要求。
- 不要用机器人批量加群、群发广告、诱导分享。
- 不要尝试让模型输出违法违规内容,也不要试图绕过模型的安全限制。
- 部署机器人时,WebSocket 端口不要暴露到公网,避免被恶意控制。
AI 模型本身自带安全策略,这是保护用户的正向能力,不应该去“破甲”或想办法绕过。做拟人化聊天时,重点是调整语气风格,而不是突破内容安全边界。
6.5 部署与运维建议
本地运行没问题后,可以部署到云服务器。Linux 下推荐用 systemd 管理进程,这样程序崩溃后能自动重启。
systemd 服务文件示例:
[Unit] Description=QQ DeepSeek Bot After=network.target [Service] WorkingDirectory=/opt/qq_deepseek_bot ExecStart=/opt/qq_deepseek_bot/venv/bin/python bot.py Restart=always Environment=DEEPSEEK_API_KEY=sk-xxxx [Install] WantedBy=multi-user.target启动服务:
sudo systemctl daemon-reload sudo systemctl enable qq-deepseek-bot sudo systemctl start qq-deepseek-bot查看日志:
journalctl -u qq-deepseek-bot -f这样即使遇到偶发断线、内存异常,服务也能自动恢复。
7. 总结与扩展方向
本文完成了一套完整的 DeepSeek 拟人化 QQ 机器人方案。核心内容包括:
- 用 OneBot 11 协议端接收 QQ 消息,通过正向 WebSocket 推给 Python 程序。
- Python 程序过滤 CQ 码,判断触发概率和冷却时间,再调用 DeepSeek 官方 API。
- 通过多套随机人设、高温参数、短字数限制,让回复更自然。
- 通过概率回复和冷却机制,让机器人在群里的存在感更真实,同时降低刷屏风险。
代码完整放在文中,建议先在小群或私聊环境试运行,确认稳定后再放到活跃大群。
下一步可以继续尝试的方向:
- 接入语音识别,让机器人能听语音消息。
- 用数据库保存每个群的个性化人设和记忆。
- 增加管理员命令,例如“暂停本群机器人”“重置上下文”。
- 接入更多模型能力,比如绘图 API,做图片回复。
- 将上下文存储迁移到 Redis,支持多实例部署。
如果本文对你有帮助,可以收藏备用,后续调试时直接对照排查表。也欢迎在实际运行中多观察日志,根据群聊风格调整人设和概率,找到最适合自己群的参数组合。