多彩编程 多彩编程MZPH · CODE BLOG
ARTICLE DETAIL

文章详情

深耕前端与后端开发技术的一线实战笔记与踩坑复盘。

用Nonebot+TextRank实现QQ群聊自动总结机器人

用Nonebot+TextRank实现QQ群聊自动总结机器人 简介一套基于Nonebot框架的QQ群机器人项目代码面向Python初学者与AI应用开发者演示如何借助机器学习算法从每日群聊记录中自动提炼摘要。压缩包内共28个文件整体仅2.05MB其中18个py文件涵盖机器人入口、算法演示、数据处理与工具函数另有txt配置、json依赖清单、png示意图、TextRank算法PDF及开源许可等目录结构清晰方便按需查阅。目前已有275人学习下载。资源价值在于既有可运行的bot.py与MyTextRankDemo.py等核心代码又有NetUtils、JsonUtils等模块化组件完整呈现聊天数据预处理、关键词提取和自动总结流程配套的TextRank-algorithm.pdf与配图帮助快速理解算法原理适合用于二次开发或作为QQ群智能总结功能的落地参考。1. 用 Nonebot 做群聊总结机器人机器学习不是玄学是 TextRank 的工程化落地群主每天要花十分钟翻聊天记录才能弄清群里今天聊了什么话题管理员想知道一个技术问题被多少人反复问过靠翻聊天记录解决不了。这个基于 Nonebot 的 QQ 群机器人把聊天记录抓取、数据清洗、关键词提取和总结生成串成一条完整的自动化流程每天定时跑一遍就能输出一组带权重的主题词和代表性句子。它面向两类人一类是给群聊做信息管理的机器人维护者另一类是想在项目里低成本接入文本摘要能力的中级 Python 开发者。整体不依赖大模型不依赖 GPU一套 TextRank 加上 jieba 分词就能跑起来适合先做工程验证再慢慢调效果这也是我推荐它而不是硬套深度模型的原因。2. 项目结构与框架搭建从 bot.py 到 ml 模块的调用链拿到资源第一件事不是解压运行而是先把目录结构过一遍。这个项目不是单文件脚本是一套带工具类和算法模块的功能包我拆开之后发现它的调用链大致是 bot.py 作为入口加载 config.py 里的配置再调用 ml 目录中的 TextRank 实现期间各种数据格式转换全部交给 Utils 下的四个工具模块处理。先把这个链路理顺后面运行和改代码都不容易翻车。2.1 文件清单与模块职责十个文件各管一段我把资源解压后的主要文件整理成下面这张表方便你对照着看。assets 目录下的 TextRank-algorithm.pdf 是算法原论文的讲解文档01.png 和 02.png 是演示截图jetbrains-variant-2.png 是说明文档配图这些不用管真正核心的是 ml 和 Utils 两个目录。文件/目录职责在调用链中的位置bot.pyNonebot 程序入口负责初始化和插件注册起点config.py机器人账号、群白名单、定时时间等配置被 bot 加载ml/TextRank 算法实现含 MyTextRankDemo.py 演示脚本核心算法层Utils/NetUtils.py网络请求负责与 QQ 框架接口通信数据来源Utils/JsonUtils.pyJSON 解析与序列化数据格式层Utils/ConversionUtils.py聊天记录格式清洗转换数据格式层Utils/IOUtils.py文件读写存取聊天记录中间结果持久化assets/算法 PDF、演示截图说明文档requirements.txtPython 依赖清单安装前置setup.py包封装配置安装用这里提醒一句NetUtils 和 JsonUtils 这类工具模块在 Nonebot 项目里非常常见它们主要处理消息事件 bean 和协议字段的转换。这份资源里的 FG-mirai 意味着它默认的适配链路与 mirai 框架相关实际部署时如果你不走同一个协议就会出现事件格式对不上的问题这一点我在第 4 章重点展开。细看这份清单你会发现它其实不依赖外部大模型所有算法逻辑都收在 ml 目录里所以排查问题的时候把 Utils 里的格式转换看好基本能定位八成故障。2.2 bot.py 与 config.pyNonebot 入口的两板斧Nonebot2 是 Python 生态里很成熟的异步机器人框架入口文件的写法相对固定我拆过的多个项目里绝大多数都是下面这套结构。用 nonebot.init() 初始化运行时注册 OneBot V11 适配器再加载功能插件最后启动事件循环。import nonebot from nonebot.adapters.onebot.v11 import Adapter nonebot.init() # 初始化 Nonebot 运行时 driver nonebot.get_driver() # 拿到 driver 对象注册协议适配器与生命周期钩子 driver.register_adapter(Adapter) # 注册 OneBot V11 协议适配器 nonebot.load_plugin(ml.group_summary) # 加载群总结功能模块 if __name__ __main__: nonebot.run() # 启动服务默认监听 127.0.0.1:8080逻辑说明这四行的执行顺序不能乱。init 没执行就注册适配器会直接抛配置异常load_plugin 也要在适配器注册之后再执行因为插件内部的导入逻辑依赖协议对象初始化。如果你拿到的资源里ml目录没有__init__.py那么 load_plugin 的目录名路径会找不到模块这也是入门阶段最常见的启动失败原因之一。config.py 在这个结构里承担的是运行时配置的角色常见写法是把群白名单、每天生成总结的时间点、文本长度上限这些放进 config 对象里bot 启动时通过nonebot.get_driver().config读取。想改行为先动配置再动代码。参数说明init()默认读取.env文件里的配置项包括HOST和PORT反向代理场景下要格外注意监听地址load_plugin的入参是插件模块名标准要求是点分路径如果你传的是目录名字符串Nonebot 内部会尝试按模块名导入。这些参数错误都会在启动阶段暴露日志里能看到ModuleNotFoundError或ConfigError。以下进入算法部分这是整份资源最有价值、也最容易在参数上调不明白的地方。3. 核心算法实现TextRank 的完整计算流程TextRank 和人们印象中的深度学习模型很不一样它不需要训练样本没有反向传播没有让人摸不着头脑的隐藏层向量最容易被误解成黑匣子的部分其实只有那个迭代公式。它把“词”当作网页把“共现关系”当作超链接用 PageRank 那套排序思路去算词与词之间的权重关系。这种做法的工程价值在于部署成本极低在聊天记录这类非结构化文本上反而比需要大量标注数据的模型更容易落地。ml 目录里那份 TextRank-algorithm.pdf 建议扫一遍你会发现算法本身就二十来页核心概念一两页能讲完真正的坑都在数据预处理和参数选择上。3.1 TextRank 原理从 PageRank 到词图排序搜索引擎当年用 PageRank 解决“哪个网页更重要”的问题。核心思想是一个网页的权重取决于有多少其他重要网页链接到它且链接来源越多越重要这个页面就越靠前。TextRank 几乎原封不动移植了这套逻辑把一句话分词后得到的词当作节点如果两个词在一个固定大小的滑动窗口内共同出现就在它们之间连一条边。于是每个词从邻居词那里收集权重邻居越重要自身也越重要。迭代结束后权重排名靠前的词就被认定为文本的关键词或主题词。迭代公式在实现层通常写成循环WS(Vi) (1 - d) d * Σ[ Wji / (Σ Wjk) * WS(Vj) ]公式里的WS(Vi)表示词节点 Vi 的 TextRank 分数d是阻尼系数一般取经验值 0.85它代表文本中随机跳转到任意一个词的概率余量。分母 ΣWjk 是节点 Vj 所有出边权重的和起到归一化作用防止个别连接特别多的中心节点把权重过度集中到少数邻居上。整个迭代过程没有任何可学习的参数因为模型权重固定为共现关系本身。对于中文聊天文本来说这个设计天然有优势——群聊内容口语化、语法碎片化、句式很短很多在文档里有效的语法规则在这里全部失效反倒是共现关系这种纯统计特征最稳定。实际写代码时的参数只有两个。第一个是阻尼系数d一般不建议改0.85 在绝大多数场景下都收敛稳定。第二个是窗口大小window它决定一个词向前向后各看多少个位置。窗口太小两句完整的话中间断开的词就建立不了连接图很稀疏很多冷门词拿不到分窗口太大所有词都变成邻居权重趋同关键词就没有区分度了。对短小的群聊句子窗口取 4 到 6 是比较稳的范围我站 5。如果群聊里消息偏口语化、句子极短甚至可以缩小到 3如果群里是在讨论技术问题、长句多那就往 6 走。这也是后面 4.4 节那个坑的主要导火索。3.2 手写一个 TextRank 关键词提取 Demo下面这段代码不是从 zip 里原样抄出来的但它和 MyTextRankDemo.py 的核心流程基本一致先分词、过滤停用词再构建共现图最后做 30 轮迭代取 TopK 关键词。写这份代码的目的就是让你不被项目里的大量工具类干扰先跑通核心过程。import jieba import re from collections import defaultdict STOP_WORDS {的, 了, 吗, 呢, 啊, 嗯, 在, 是} # 输入一整天多条消息组成的句子列表 def textrank_keywords(sentences: list, window: int 5, top_k: int 10): # 第一步分词并过滤停用词和纯数字/英文字符 tokens [] for sent in sentences: for w in jieba.cut(sent.strip()): if not re.match(r^[\u4e00-\u9fa5]$, w): continue if w in STOP_WORDS or len(w) 2: continue tokens.append(w) # 第二步构建共现图窗口内两个词互为邻居 graph defaultdict(set) n len(tokens) for i, word in enumerate(tokens): start max(0, i - window) end min(n, i window 1) for j in range(start, end): if i j or tokens[j] word: continue graph[word].add(tokens[j]) # 第三步PageRank 式迭代初始权重全部设为 1 scores {w: 1.0 for w in graph} d 0.85 for _ in range(30): prev scores.copy() for w in graph: sum_wj sum(prev[nei] / len(graph[nei]) for nei in graph[w] if nei in graph) scores[w] (1 - d) d * sum_wj # 第四步按分数降序取前 top_k 个词 ranked sorted(scores.items(), keylambda x: x[1], reverseTrue) return [w for w, _ in ranked[:top_k]]逻辑说明第二步里graph用的是set会自动去重防止同一对共现词在多次遍历中被重复计数第三步迭代 30 轮是工程惯例TextRank 理论分析证明在这个迭代次数下权重已经趋于收敛继续增加轮数对排序结果影响极小。过滤单字词是为了避免“呢”“呀”“啦”这类口语语气词干扰共现图这些词在群聊文本里频率很高如果不过滤排前面的全是它们。参数说明window是最值得手动调的参数。当你发现提取出的关键词彼此毫无关联像“手机”“电脑”“橘子”这种不相干名词同时挤进 TopK多半是窗口太大词与词之间产生了远距离错误关联当你发现 TopK 里全是“今天”“大家”“觉得”这类泛化高频词那就需要把窗口调小一点同时扩充停用词表。top_k建议按群活跃度去设一个百人群一天的有效聊天记录通常能提供两三段话的信息量取 10 到 15 个词足以覆盖当天话题。对比实验时至少要把 window 从 3 到 7 交替跑一遍再下结论否则结果很容易被随机性带偏。4. 部署运行与常见问题排查五个高频翻车现场这一章直接给结论资源能跑通但我在复现过程中踩了五个坑每个都算得上“现象看似奇怪、原因其实很直接”。按部署前、部署中、部署后三个阶段排列前两条是环境与数据问题后三条集中在算法参数和运行稳定性上很多人到了第四和第五条才知道问题究竟出在哪。4.1 部署前准备依赖安装和事件源配置常见做法是先把 requirements.txt 里的依赖装进干净的虚拟环境。Nonebot2 项目跟普通 Python 项目没有本质区别但它对 Python 版本有要求建议不低于 3.8否则异步事件循环的写法会报一堆语法兼容错。事件源如果走 mirai 网关需要确认机器人连接的框架真正支持 OneBot 协议转换这一步没打通后面所有群消息都是空的。我自己通常是先起一个最小 echo 插件让机器人原样复读一条群消息确认事件能进到代码里才继续跑总结逻辑。提示如果机器人收不到群消息先别查算法先查事件流。用日志打印 driver 收到的原始事件确认 message_type 和 group_id 字段是否存在再做后续处理。4.2 坑一聊天记录里混着表情符号、图片链接和系统通知现象TextRank 跑出来的关键词里出现“CQ”“image”“http”这类非中文词每日总结被它们带偏话题识别效果差。原因群聊记录包含大量富媒体内容原始消息里既有 CQ 码也有 URL。分词库会把[CQ:image,filexxx]和http://xxx当作普通词切出来这些词参与共现图构建后会污染整个权重排序。解决在进入 ml 模块之前先做一次正则清洗。我通常在 Utils/ConversionUtils.py 里加两行规则第一行去掉 CQ 码段第二行去掉链接。import re def clean_chat_msg(raw: str) - str: raw re.sub(r\[CQ:[^\]]*\], , raw) # 去掉酷Q/OneBot 的 CQ 码 raw re.sub(rhttps?://\S, , raw) # 去掉网页链接 return raw.strip()逻辑说明清洗函数放在 Utils 层而不是 ml 层是为了保证算法模块对数据纯净度无感后续换数据源也不用改。参数说明\[CQ:[^\]]*\]里的[^\]]*是非贪婪匹配确保一个 CQ 码整段被抹掉而不是只去前几个字符URL 正则里的\S会把空格前的剩余部分都吃掉群聊里常见的短链和带参长链基本都能覆盖。跑完清洗再进分词TopK 里就不会再看到奇怪的非中文 token。4.3 坑二文件读写编码不一致Windows 下直接闪退现象在 Windows 上跑 setup.py 或 demo 时读入聊天记录文件后报UnicodeDecodeError命令行窗口里输出全是乱码。原因聊天记录导出文件可能是 GBK 编码而 IOUtils.py 默认按 UTF-8 打开文件或者反过来文件是 UTF-8 但 Windows 控制台用的是本地代码页。两种情况的表象一样都是读不进来。解决统一在 IOUtils.py 里显式声明编码并增加一个 gbk 回退。代码怎么写不重要核心是别依赖操作系统的默认编码。Linux 服务器上运行时还要设置PYTHONIOENCODINGutf-8避免打印中文日志时把信息截断。以我习惯的写法为例def load_text(path: str) - str: for enc in (utf-8, gbk): try: with open(path, r, encodingenc) as f: return f.read() except UnicodeDecodeError: continue raise UnicodeDecodeError(f无法识别文件编码: {path})逻辑说明这里先尝试 UTF-8 后尝试 GBK因为在中文聊天记录场景里 UTF-8 占多数能更快返回结果都失败就抛异常而不是静默吞掉错误。参数说明如果你明确知道历史聊天记录全是 GBK 导出可以把顺序调成 gbk 优先省去一次无效读取的开销。编码问题在很多项目里都会被忽略但它会直接让你连数据都摸不着属于最基础的工程坑。4.4 坑三TextRank 窗口参数设成 10关键词全部趋同现象无论群聊当天聊的是技术问题还是日常闲聊总结出来的关键词都是“大家”“我们可以”“这个”“那个”这类高频泛化词没有话题区分度。原因窗口设置过大导致几乎所有词在图中都互相连接迭代后的权重分布趋向均匀最后的排序结果基本等价于词频排序TextRank 的核心筛选能力被彻底压平。解决把 window 从 10 拉回 5同时扩充停用词表把“大家”“我们”“这个”“那个”“因为”“所以”这类无信息量的口语词加进去。改完跑一遍 MyTextRankDemo.py 看输出词的区分度有没有提升。这里必须做对比实验拿两天的聊天记录分别用 window5 和 window10 跑一遍如果结论定性一致那把参数固定下来如果不一致继续调整停用词覆盖范围。这个调参过程没有银弹只能靠肉眼观察 TopK 输出做回归验证。4.5 坑四定时生成总结触发时机器人连接已经断开现象每天定时任务准时触发脚本能正常跑但抓不到当天的聊天记录最终生成的是一份空总结或只有模板没有内容的报告。原因机器人长时间运行后与后端的连接因为心跳超时或网络波动被断开定时任务启动时没有触发重连逻辑事件轮询实际上已经停了。解决把定时任务挂到nonebot_plugin_apscheduler上让它集成到 Nonebot 的生命周期管理里而不是用 sleep 循环自己卡着。定时任务执行前主动检查驱动状态拿到可用的 driver 实例再拉取消息。这个设计能避免“任务跑得很勤快但拿不到数据”的空转问题也方便后续接入跨日租期清理等操作。5. 把每日总结做得更像总结从关键词到摘要句的三步进阶到这里你已经能把一个群一天的聊天记录跑出一组关键词。但对“每日总结”这个场景来说关键词列表只是半成品。用户要的是几句能直接读、能转发的话而不是一串名词。我的做法分三步。第一步把关键词重新定位回原始句子。分词阶段记录每个词的来源句子索引TextRank 拿到 TopK 词之后反向扫描原始句子把包含这些关键词的句子按累计权重排序取前三句作为候选摘要。sentence_scores {} for word, score in ranked_keywords: for idx, sent in enumerate(sentences): if word in sent: sentence_scores[idx] sentence_scores.get(idx, 0) score top_sentences sorted(sentence_scores.items(), keylambda x: x[1], reverseTrue)[:3]第二步给候选摘要句做去重和合并。群聊里经常出现同一个人把同一句话改了标点连发三次的情况如果不去重三句话会占满候选位。相似度判断用最简单的方法把句子切成字符集合算 Jaccard 系数超过 0.7 就只保留权重较高的那一条。第三步加上模板头尾输出一条适合转发的“今日群讨论摘要”。模板虽然原始但好处是格式统一、群里刷屏时辨识度高。把当天出现次数最多的词放最前面和总结句拼接起来整体效果比直接丢十个词要有人味得多。如果想让总结再带上情绪色彩可以在预处理阶段按句做简单的情感极性判断统计当天的正负向占比把这条加进模板作为附加字段。没有训练数据时可以先用情感词典打分之后再决定是不是要接现成模型。从那以后我每次拿到这类机器人资源都会先跑通事件流再碰算法参数然后定期检查心跳状态。改任何一个参数之前先保留旧输出的截图做对比否则你永远不知道是代码变了还是数据变了这大概是我复现项目这么多年踩坑踩出来的最值钱习惯。把它写在这里希望帮到你。本文还有配套的精品资源点击获取
返回列表