尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

DeepSeek接入QQ机器人:OneBot协议与拟人化回复实战

DeepSeek接入QQ机器人:OneBot协议与拟人化回复实战 把 DeepSeek 接入 QQ 机器人让它像真人一样在群里聊天是最近被问得非常多的一块内容。这个需求听起来只是把大模型 API 和 QQ 消息通道连起来真正动手时会发现要处理的细节不少OneBot 协议怎么上报消息、API Key 怎么配置、人设提示词怎么写才不像客服、以及为什么所有消息都回复反而显得特别假。这篇保姆级教程按照一条完整主线展开QQ 群消息通过 OneBot 框架上报到本地 Python 服务服务按概率决定要不要回复命中后调用 DeepSeek 的 chat API再把模型输出发回群聊。整套流程跑通后你可以继续扩展多群人设、上下文记忆、延迟回复和敏感词过滤。1. 先理解整条链路消息从 QQ 群到 DeepSeek 再回来1.1 没有官方接口时社区方案怎么工作普通开发者申请 QQ 官方机器人接口的门槛不低而且官方接口面向的更多是群管理、客服这类的功能型机器人并不适合“个人小号在群里像真人一样聊天”。所以社区普遍采用另一条路径找一个 OneBot 协议实现的框架让它登录一个用来当机器人的 QQ 小号监听这个号收到的消息再把消息按标准格式上报给你自己的程序。把这条链路写成文字就是QQ 群消息 - OneBot 框架 - HTTP POST 上报到本地服务 - Python 程序判断要不要回复 - 调用 DeepSeek API - 本地服务返回回复内容 - OneBot 框架把消息发回群聊。这里最容易误解的是OneBot 不是 QQ 官方提供的东西而是社区定义的一套消息协议标准。它的价值在于只要框架实现了 OneBot 11 协议你的程序就可以用同一套代码对接不同的框架不用为每个封装重写一遍。常见实现有 NapCat、Lagrange.OneBot、LLOneBot 等具体选哪个主要看自己的运行环境。1.2 OneBot 11 协议在链路中的位置OneBot 11 协议有多种连接方式本文只用其中最简单的一种HTTP 上报。框架把消息事件以 JSON 格式 POST 到你在配置里填写的地址例如 http://127.0.0.1:8765/onebot。你的程序收到 JSON 后如果希望立即回复可以在 HTTP 响应体里直接返回{reply: 你好呀}框架会自动把这段文本发回触发消息的群聊或私聊。对保姆级入门来说这种“快速回复”方式足够跑通整条链路不需要额外写发消息的调用。一条典型的群消息事件长这样{ post_type: message, message_type: group, group_id: 123456789, user_id: 987654321, self_id: 111222333, raw_message: [CQ:at,qq111222333]小梦在吗, sender: {user_id: 987654321, nickname: 测试用户} }私聊时没有 group_idmessage_type 是 private。后面写程序时主要看的字段就是 post_type、message_type、raw_message、user_id、self_id。post_type 表示事件类型只有 message 才是消息事件raw_message 是带 CQ 码的原始文本self_id 是机器人的 QQ 号后面判断“是不是在 我”要用它。1.3 概率回复是拟人化的第一层手段真人不会回复群里每一条消息。如果一个机器人对每条消息都秒回哪怕语气再自然群友看一会儿也会觉得假。概率回复的思路是让程序按一定概率决定是否回应其他时候保持沉默。这样既保留了参与感又不会让人持续注意到这是一个自动回复程序。回复策略特点适用场景全部回复链路简单但容易刷屏也容易被识别为机器人测试链路、私聊调试固定关键词回复只回复命中关键词的消息稳定但不够自然功能型机器人关键词优先 概率回复被 或叫到名字必回普通消息按概率回拟人化聊天机器人概率回复不是随机乱回。更合理的设计是分层判断提到机器人名字必回被 必回私聊尽量都回群聊普通消息按概率回。后面第 4 节的代码就按这个顺序实现。2. 环境准备Python、OneBot 框架、API Key2.1 准备的材料清单材料说明注意事项一台电脑或服务器用来跑 Python 服务和 OneBot 框架学习阶段可以是一台能运行 QQ 的电脑QQ 小号用来登录 OneBot 框架不要用主号避免日常使用受影响Python 3.10运行主程序建议 3.10 或更高版本DeepSeek API Key调用大模型接口在开放平台创建按量付费OneBot 框架负责登录 QQ 和收发消息以 NapCat 为例说明其他框架思路相同2.2 安装 Python 依赖项目只需要四个依赖分别是 Web 服务框架 FastAPI、开发服务器 uvicorn、OpenAI 兼容 SDK 和读取环境变量的 python-dotenvpip install fastapi0.110 uvicorn[standard]0.29 openai1.35 python-dotenv1.0安装完成后可以用下面的命令确认环境已经就绪python -c import fastapi, openai, dotenv; print(ok)如果输出 ok说明依赖安装成功。版本号这里只是示例实际以 pip 能拉取到的稳定版本为准。2.3 在 DeepSeek 开放平台创建 API Key登录 DeepSeek 开放平台在 API Keys 页面创建一个新的 Key。创建后立刻复制保存因为 Key 通常只显示一次。然后把账户余额准备好接口是按 tokens 计费的具体价格以开放平台页面为准。这里要确认三件套API Key、base_url、模型名。DeepSeek 提供 OpenAI 兼容接口常见配置是 base_url 为 https://api.deepseek.com模型名常见有 deepseek-chat 和 deepseek-reasoner。模型名和价格会调整落地前先到官方文档确认一次。学习环境先用普通对话模型 deepseek-chat 就够了后面会解释为什么。2.4 启动 OneBot 框架并配置上报地址以社区常用的 NapCat 为例下载对应平台的版本按文档启动然后用你的 QQ 小号登录。登录成功后在连接管理里找到 HTTP 上报相关配置新增一条上报规则地址填http://127.0.0.1:8765/onebot然后勾选启用的消息事件类型。不同版本的配置界面和字段名不完全一样但核心概念一致把消息事件 POST 到本地服务的地址。如果你用的是其他 OneBot 实现只需要找到同等的“HTTP 上报地址”或“HTTP Post URL”配置项。注意上报地址里的端口必须是 Python 服务实际监听的端口。很多新手改了一个端口却忘了改另一个最后怎么测都没有消息进来。配置完成后先不要急着写程序可以查看框架日志确认事件确实有在往外发。这样才能在排错时区分是框架的问题还是自己程序的问题。2.5 学习环境与生产环境的差异整套链路在本机跑通很容易但生产环境至少要再做四件事第一API Key 不要写死在代码里放到环境变量或配置管理平台第二把服务的监听地址从 127.0.0.1 改成 0.0.0.0 并配置防火墙安全组让框架能访问到它第三用 systemd、supervisor 或 Docker 守护进程挂掉后能自动拉起第四日志要落盘方便半夜出问题时排查。这些在第 7 节还会展开。3. 先单独验证 DeepSeek API人设和参数都在这层决定3.1 最小调用脚本在接 QQ 之前先单独验证 DeepSeek API 能正常回复。新建一个 test_deepseek.pyfrom openai import OpenAI client OpenAI( api_keysk-你申请的Key, base_urlhttps://api.deepseek.com, ) resp client.chat.completions.create( modeldeepseek-chat, messages[ {role: user, content: 你好} ], temperature1.0, max_tokens100, ) print(resp.choices[0].message.content)运行python test_deepseek.py如果 Key 正常、账户有余额会输出一段模型生成的文本。这一步通过后再进 QQ 链路至少能确定问题不在 API 层。3.2 影响“说话感觉”的关键参数调用接口时要理解几个参数它们直接决定回复像客服还是像真人参数作用对拟人化的影响建议temperature采样温度越高越随机太低会变成标准客服腔太高容易胡说0.8 到 1.2 之间试验max_tokens最大生成 token 数太长像小作文太短接不上话300 到 600top_p核采样阈值一般保持默认即可1.0presence_penalty提高话题多样性太高容易跑题默认或 0frequency_penalty降低重复用词太高句子会碎默认或 0不是所有参数在每种模型上都支持同样的效果这里说的是通用规律具体以官方文档为准。拟人化调优的核心思路是不要一次只调一个变量先固定人设再慢慢调 temperature。3.3 system prompt 就是人设很多人只把 system prompt 当“给模型的规则”其实在拟人化场景里它就是你给机器人写的人设底稿。下面是示例SYSTEM_PROMPT ( 你叫小梦是群里一个普通群友不是客服。 你说话口语化喜欢短句偶尔开玩笑会用语气词。 不要一开口就报解决方案不要每句话都用感叹号。 不要暴露自己是 AI也不要说明自己是语言模型。 别人问你是不是机器人时你可以笑着岔开话题。 )把这段人设放进 system prompt 后模型回复的语气会明显不同。要注意的是人设不要只写“你是真人”还要写清楚具体行为边界否则模型还是会按默认的助手风格回答。后面第 7 节会补充敏感内容过滤和话题拒绝。4. 编写 QQ 机器人主程序上报接收、概率判定、API 调用4.1 项目结构与配置分离建议按下面结构组织项目qq-bot/ ├── .env ├── config.py ├── main.py └── requirements.txtrequirements.txt 内容fastapi0.110 uvicorn[standard]0.29 openai1.35 python-dotenv1.0.env 存放所有需要经常调整的参数DEEPSEEK_API_KEYsk-这里换成你的Key DEEPSEEK_BASE_URLhttps://api.deepseek.com DEEPSEEK_MODELdeepseek-chat REPLY_PROBABILITY0.3 PRIVATE_REPLY_PROBABILITY0.8 COOLDOWN_SECONDS30 TEMPERATURE1.0 MAX_TOKENS400 BOT_NAME小梦 SYSTEM_PROMPT你叫小梦是群里一个普通群友不是客服。你说话口语化喜欢短句偶尔开玩笑会用语气词。不要一开口就报解决方案不要每句话都用感叹号。不要暴露自己是 AI。把配置放到 .env 而不是写死在代码里是为了不用改代码就能调概率、换人设、换模型。config.py 负责读取import os from dotenv import load_dotenv load_dotenv() DEEPSEEK_API_KEY os.getenv(DEEPSEEK_API_KEY, ) DEEPSEEK_BASE_URL os.getenv(DEEPSEEK_BASE_URL, https://api.deepseek.com) DEEPSEEK_MODEL os.getenv(DEEPSEEK_MODEL, deepseek-chat) REPLY_PROBABILITY float(os.getenv(REPLY_PROBABILITY, 0.3)) PRIVATE_REPLY_PROBABILITY float(os.getenv(PRIVATE_REPLY_PROBABILITY, 0.8)) COOLDOWN_SECONDS float(os.getenv(COOLDOWN_SECONDS, 30)) TEMPERATURE float(os.getenv(TEMPERATURE, 1.0)) MAX_TOKENS int(os.getenv(MAX_TOKENS, 400)) BOT_NAME os.getenv(BOT_NAME, 小梦) SYSTEM_PROMPT os.getenv(SYSTEM_PROMPT, )注意.env 文件里不要出现中文引号或全角字符否则解析会失败。API Key 也不要提交到 Git 仓库建议把 .env 写入 .gitignore。4.2 OneBot 事件上报接收端主程序用 FastAPI 提供一个 POST 接口路径和 OneBot 框架里配置的上报地址保持一致。核心逻辑是过滤事件类型from fastapi import FastAPI, Request app FastAPI() app.post(/onebot) async def onebot_event(request: Request): data await request.json() # 只处理消息事件忽略通知、请求等其他事件 if data.get(post_type) ! message: return {} message_type data.get(message_type) # group 或 private raw_message data.get(raw_message) or user_id data.get(user_id) self_id data.get(self_id) # 防止机器人自己触发自己 if not user_id or user_id self_id: return {} return {}post_type 是 OneBot 事件类型里最重要的一层过滤。不做这层判断的话退群通知、好友请求等事件也会进到你的逻辑里容易干扰判断。4.3 概率回复与冷却时间概率回复的实现用 random 模块就够了但判定顺序要设计好import random import re import time last_reply_time {} def clean_message(raw_message: str) - str: return re.sub(r\[CQ:[^\]]*\], , raw_message).strip() def is_at_me(raw_message: str, self_id: int) - bool: return f[CQ:at,qq{self_id}] in raw_message def should_reply(raw_message: str, message_type: str, self_id: int) - bool: if not clean_message(raw_message): return False # 被 必回 if is_at_me(raw_message, self_id): return True # 被叫到名字必回 if config.BOT_NAME and config.BOT_NAME in raw_message: return True # 私聊默认高概率群聊按配置概率 if message_type private: return random.random() config.PRIVATE_REPLY_PROBABILITY return random.random() config.REPLY_PROBABILITY冷却时间用字典记录每个群或每个用户上次回复的时间def in_cooldown(chat_key: str, cooldown_seconds: float) - bool: now time.time() if now - last_reply_time.get(chat_key, 0) cooldown_seconds: return True last_reply_time[chat_key] now return False冷却时间的意义是防止机器人连续接管聊天节奏。真人不可能每条都接话所以即使概率命中了如果 30 秒内刚回过这个群这次也可以跳过。4.4 清洗消息并判断是否被 raw_message 里带 CQ 码例如 [CQ:at,qq123]、[CQ:image,file...]。如果直接把整段文本送给大模型会出现两个问题第一CQ 码是机器语言模型很难理解第二如果群里只发了一张图清洗后文本为空就不该触发回复。clean_message 的作用就是去掉所有 [CQ:...] 代码只留纯文本。is_at_me 判断字符串里是否包含指向机器人自身的 CQ 码。这里的 self_id 来自事件上报是机器人自己的 QQ 号。4.5 调用 DeepSeek 并返回回复调用部分用 AsyncOpenAI 异步接口避免多个群同时发消息时阻塞from openai import AsyncOpenAI client AsyncOpenAI( api_keyconfig.DEEPSEEK_API_KEY, base_urlconfig.DEEPSEEK_BASE_URL, ) async def ask_deepseek(user_message: str) - str: messages [ {role: system, content: config.SYSTEM_PROMPT}, {role: user, content: user_message}, ] resp await client.chat.completions.create( modelconfig.DEEPSEEK_MODEL, messagesmessages, temperatureconfig.TEMPERATURE, max_tokensconfig.MAX_TOKENS, streamFalse, ) return resp.choices[0].message.content.strip()拿到模型输出后直接在 HTTP 响应里返回return {reply: reply}OneBot 框架收到这个响应后会把 reply 内容发回触发消息的聊天窗口。如果希望群聊回复时自动 发消息的人可以在响应里再加 at_sender: true。拟人化场景建议默认不开除非对方本来就是 机器人的。4.6 完整 main.py把上面几块拼起来完整代码如下import logging import random import re import time from fastapi import FastAPI, Request from openai import AsyncOpenAI import config logging.basicConfig(levellogging.INFO, format%(asctime)s [%(levelname)s] %(message)s) logger logging.getLogger(qq-bot) app FastAPI() client AsyncOpenAI( api_keyconfig.DEEPSEEK_API_KEY, base_urlconfig.DEEPSEEK_BASE_URL, ) last_reply_time {} def clean_message(raw_message: str) - str: return re.sub(r\[CQ:[^\]]*\], , raw_message).strip() def is_at_me(raw_message: str, self_id: int) - bool: return f[CQ:at,qq{self_id}] in raw_message def should_reply(raw_message: str, message_type: str, self_id: int) - bool: if not clean_message(raw_message): return False if is_at_me(raw_message, self_id): return True if config.BOT_NAME and config.BOT_NAME in raw_message: return True if message_type private: return random.random() config.PRIVATE_REPLY_PROBABILITY return random.random() config.REPLY_PROBABILITY def in_cooldown(chat_key: str, cooldown_seconds: float) - bool: now time.time() if now - last_reply_time.get(chat_key, 0) cooldown_seconds: return True last_reply_time[chat_key] now return False async def ask_deepseek(user_message: str) - str: messages [ {role: system, content: config.SYSTEM_PROMPT}, {role: user, content: user_message}, ] resp await client.chat.completions.create( modelconfig.DEEPSEEK_MODEL, messagesmessages, temperatureconfig.TEMPERATURE, max_tokensconfig.MAX_TOKENS, streamFalse, ) return resp.choices[0].message.content.strip() app.post(/onebot) async def onebot_event(request: Request): try: data await request.json() except Exception: return {} if data.get(post_type) ! message: return {} message_type data.get(message_type) raw_message data.get(raw_message) or user_id data.get(user_id) self_id data.get(self_id) if not user_id or user_id self_id: return {} if not should_reply(raw_message, message_type, self_id): return {} chat_key str(data.get(group_id) or data.get(user_id) or user_id) if in_cooldown(chat_key, config.COOLDOWN_SECONDS): return {} user_message clean_message(raw_message) if not user_message: return {} logger.info(触发回复 chat%s user%s msg%s, chat_key, user_id, user_message) try: reply await ask_deepseek(user_message) except Exception as exc: logger.exception(DeepSeek 调用失败: %s, exc) return {} logger.info(模型回复: %s, reply) return {reply: reply}这个版本是学习环境的最小闭环。生产环境还要把日志落盘、加入重试、补充敏感词过滤和更完善的上下文管理。5. 启动运行与效果验证5.1 启动顺序与命令先启动 OneBot 框架并登录 QQ 小号确认框架已经在线再启动 Python 服务python -m uvicorn main:app --host 0.0.0.0 --port 8765监听地址用 0.0.0.0这样即使 OneBot 框架和 Python 服务不在同一台机器也能通过内网 IP 访问。如果都在本机用 127.0.0.1 也可以。启动后看到类似下面的日志说明服务已经就绪INFO: Uvicorn running on http://0.0.0.0:8765 INFO: Application startup complete.5.2 用 curl 模拟一条群消息在真实登录 QQ 之前先用 curl 模拟一条 OneBot 上报验证接收和回复逻辑curl -X POST http://127.0.0.1:8765/onebot \ -H Content-Type: application/json \ -d { post_type: message, message_type: group, group_id: 123456789, user_id: 987654321, self_id: 111222333, raw_message: 小梦在吗, sender: {user_id: 987654321, nickname: 测试用户} }因为消息里包含了机器人名字“小梦”should_reply 会直接返回 True不需要碰概率。正常响应类似{reply:在呀刚看完剧怎么啦}如果消息不包含名字例如只发“今天天气怎么样”那就要看概率是否命中了。没命中时响应是{}这一步能帮你快速确认 FastAPI 服务、DeepSeek 调用、概率判定三个环节是否正常。如果你在 Windows 命令行里粘贴中文遇到编码问题可以改用 Python 的 requests 脚本发送同样的 JSON。5.3 真机测试私聊、群聊、概率观察curl 验证通过后再用 QQ 小号真机测试。建议顺序是先用私聊测试因为私聊不会打扰别人而且默认概率是 0.8更容易触发。私聊正常后再拉一个小群先发一条包含机器人名字的消息确认群回复正常。最后把概率调到 1.0 观察一两次群聊链路确认无误后再调回 0.3。可以在 .env 里临时把 REPLY_PROBABILITY 改成 1.0重启服务验证验证完改回来。这样能避免在群里发十几条消息都因为概率问题以为程序坏了。注意验证链路时临时把概率调到 1.0验证完记得改回来否则机器人会回复群里每条消息很容易被当成骚扰。5.4 日志怎么看main.py 里的 logging 会输出三类关键信息触发记录、模型回复、异常堆栈。例如2025-01-12 21:03:15 [INFO] 触发回复 chat123456789 user987654321 msg小梦在吗 2025-01-12 21:03:18 [INFO] 模型回复: 在呀刚看完剧怎么啦日志里建议至少包含三个信息哪个会话触发的、用户原始消息是什么、模型最终回复是什么。排查“群里有人问为什么没回”时先看日志里有没有触发记录有触发记录再看模型是否报错没有触发记录就说明是概率或冷却跳过了。6. 常见问题排查从“不回复”到“回复太假”6.1 先按链路顺序排查遇到问题不要乱改配置按下面顺序来输入是否正确消息是否真的上报到了本地服务可以用 curl 模拟验证。文件路径和命名config.py、.env、main.py 是否在同一目录有没有拼错文件名。依赖版本openai、fastapi 版本是否过低或不兼容。配置是否生效启动时打印一份 config 里的关键参数确认 .env 被正确读取。端口和网络OneBot 框架能否访问到 Python 服务的地址防火墙有没有拦截。日志异常FastAPI 日志、DeepSeek 返回内容、框架日志是否有明确报错。6.2 典型问题汇总问题现象常见原因检查方式处理建议机器人完全不回复OneBot 上报地址没配或配错看框架日志、curl 测本地接口把上报地址指到 http://127.0.0.1:8765/onebotcurl 有回复QQ 里没回复框架登录态异常、发送频率过高看框架日志换小号测试重新登录降低频率避免短时间连续发消息API 报 401Key 写错或无效检查 .env 是否有多余空格重新复制 KeyAPI 报 402账户欠费查看开放平台余额充值后重试API 报 429触发限流看返回内容增加重试和退避降低调用频率概率回复不生效.env 没加载成功启动时打印 REPLY_PROBABILITY确认 load_dotenv 生效回复像客服人设不够具体、temperature 太低打印实际使用的 SYSTEM_PROMPT重写人设调高 temperature纯图片或表情消息也触发没清洗 CQ 码打印 raw_message用 clean_message 清洗后判断空文本部署到服务器后收不到消息上报地址写了 127.0.0.1检查 OneBot 配置改成服务器内网或公网可达地址6.3 关于 thinking mode 的 reasoning_content 报错如果使用深度思考类模型或者经过某个代理层、封装工具转发请求可能会看到类似这样的报错upstream_status: http 400 cause: the reasoning_content in the thinking mode must be passed back to the api.这个报错的含义是思考模式下接口第一次返回的内容里带有 reasoning_content 字段如果客户端或代理没有把它缓存下来下一次请求时也没有原样传回接口会拒绝继续对话。排查方向有三个日常拟人聊天一般不需要思考链改用普通对话模型 deepseek-chat问题自然消失。如果确实要用思考模式需要按官方文档处理 reasoning_content 字段或者确认你使用的封装工具支持自动处理。先分清报错来自官方 API 还是本地代理层再看请求体里的 messages 是否携带了完整字段。社区里出现的一些封装工具本质是把 Key、模型和 API 调用包装成更顺手的入口。遇到这类报错时不要只盯着工具界面要回到 API 请求本身去排查。7. 最佳实践与扩展方向7.1 拟人化不只是概率概率回复只是第一步拟人化还有几件值得做的事随机延迟。真人看到消息不会秒回。要做得更自然可以把回复从“快速回复”改成调用 OneBot 的动作接口在发送前随机等待 1 到 3 秒。不用句句带名字。真人很少每句话都喊对方名字默认回复时不要总是 对方除非消息本身是 过来的。保留上下文。把最近几条群消息作为历史消息传给模型它才能接上话题。但要注意控制长度历史消息超过一定数量就截断。拒绝能力。遇到不友好或者不想接的话题人设里要允许模型岔开话题而不是硬聊。不回复机器人自己。代码里已经用 user_id self_id 过滤了部署时不要把这个判断删掉。7.2 成本、频率和安全性控制API 是按 tokens 计费的拟人化聊天如果每条都调模型一天下来成本并不低。控制成本的手段主要有三个概率和关键词双重门槛让真正值得回复的消息才调用 API。max_tokens 限制在 400 左右人设里明确要求“回复不超过三句话”。上下文只保留最近对话避免每次请求都携带一长串历史。安全性方面至少要加一份关键词过滤清单。命中违法、暴力、色情等相关内容时让机器人回复“不太想聊这个”或者直接不回复。不要指望大模型自带的安全能力覆盖所有场景对外提供服务时过滤必须在自己这层做。7.3 部署到服务器时的额外工作学习环境跑通后如果要让机器人长期在线还需要用 systemd 或 supervisor 守护进程服务崩溃后自动重启。日志写到文件并按天切割方便回溯问题。API Key 放到服务端环境变量避免写在代码仓库里。给 OneBot 框架配置 token防止本地服务被外部直接 POST 伪造事件。上线前在测试群跑一段时间观察回复质量、频率和账号稳定性。7.4 下一步可以做的扩展这套主程序的扩展点很多同一个服务对接多个群每个群配不同人设把 DeepSeek 换成其他 OpenAI 兼容接口例如本地部署的模型服务主程序几乎不用改在回复前加一层知识库检索让机器人能回答群专属问题把消息记录存到数据库后续做数据分析和回复效果评估。对新手来说最有价值的练习不是急着加功能而是把现有链路反复调试到稳定概率、冷却、清洗、日志、异常处理每一个环节都值得单独验证一遍。链路稳定之后再往上加“更像人”的功能才不会越加越乱。
返回列表