让LLM Bot在群聊中说人话:OneBot v11人格化工程实践

发布时间:2026/6/24 7:15:44
让LLM Bot在群聊中说人话:OneBot v11人格化工程实践 1. 项目概述为什么群聊里的 LLM bot 总是“说话不像人”我花了一周时间在公司内部的 QQ 群和测试用的 Telegram 群里搭了一个能实时响应、能接龙、能查文档、还能偶尔讲冷笑话的 LLM bot。它背后调的是 DeepSeek-V4-Pro 的 API用的是 OneBot v11 协议标准对接消息网关整个服务跑在一台 4C8G 的 Kylin Server v11 虚拟机上——配置不高但足够跑通全链路。上线第一天同事发来截图“这 bot 回复得挺快就是一开口我就想关群免打扰。”这句话让我盯着日志看了两小时。后来发现真正卡住项目的从来不是 API 调不通、token 算错、或者 context window 超限这些报错最难啃的骨头是让 bot 的每一句话都像一个“人在群里说话”而不是一个端坐在服务器里的 AI 模型在念说明书。这个“不像 AI”的问题本质是语境坍缩LLM 原生输出是面向通用知识问答优化的而群聊是高度碎片化、强角色感、带情绪节奏、有历史包袱的微型社会场域。你不能指望它自动理解“刚说完‘收到’下一句就该问‘老板这个需求下周能排吗’”也不能指望它默认把“好的”压缩成“ok”把“请查阅附件”软化成“我顺手扔群里了你瞅一眼”。它不会主动识别谁是新人、谁是常驻摸鱼选手、谁发言必带表情包更不会在连续三条“”后自动补一句“是不是卡住了我重发一遍”。这些不是功能缺失而是交互范式错位——我们把一个“知识引擎”硬塞进了“社交接口”的壳子里却没给它装上群聊世界的操作系统。所以这篇内容不讲怎么注册 API Key、不教你怎么写 curl 请求、也不堆砌 Codex 配置第三方 API 的 YAML 示例。我要拆解的是当你已经拿到一个能返回文本的 LLM 接口之后如何用工程手段把它“驯化”成群聊原住民。核心关键词 LLM、bot、OneBot、v11、API 全部落在实操层LLM 是原料bot 是形态OneBot v11 是协议骨架API 是供血管道。而所有技术选择都服务于一个目标——让 bot 的回复在群聊上下文中“自然得让人忘记它是个 bot”。适合正在做内部提效工具、学生项目、开源 bot 开发或单纯被“AI 味太重”折磨过的开发者。如果你试过调通 API 却被用户一句“这 bot 好官方啊”劝退那接下来的内容就是你这一周该省下的调试时间。2. 整体设计思路从“API 调用器”到“群聊人格化引擎”2.1 为什么不能直接把 API 响应原样发回群聊这是绝大多数新手 bot 的第一道坎。我最初版本就是这么干的收到消息 → 拼 prompt → 调 DeepSeek API → 把 response.text 原封不动塞进 OneBot 的 send_msg 接口。结果呢一条普通提问“今天天气咋样”bot 回复根据中国气象局最新数据北京地区今日白天晴间多云最高气温26℃最低气温15℃风向为东北风2-3级空气湿度适中紫外线强度中等。建议外出时佩戴太阳镜并注意补充水分。——这根本不是群聊语言这是气象台早间播报稿。问题出在三个层面信息粒度失配群聊需要“一句话结论”不是“完整报告”。26℃ 就够了“紫外线强度中等”属于冗余信息。语气结构失配没有主语谁说的、没有共情锚点“我也刚看天气App”、没有收尾钩子“要带伞不”。角色认知失配bot 没有自我定位。它是同事助理还是那个总在茶水间讲段子的行政小哥原样输出等于放弃角色塑造。所以架构的第一步是明确分层API 层只负责“生成内容”不负责“决定怎么发”。中间必须插入一层“群聊语义转译器”。2.2 架构选型为什么用 OneBot v11 Python Kylin Server v11这不是炫技而是基于真实约束的取舍OneBot v11 是当前国内群聊 bot 的事实标准QQ 官方 Bot SDK、酷Q 替代方案如 go-cqhttp、以及大量现成的群管理插件如禁言、撤回监听都深度兼容 v11。它用 HTTP POST 接收事件、用 WebSocket 推送消息协议清晰调试友好。相比 Telegram Bot API 的 token 管理和 webhook 配置OneBot v11 在内网环境部署更轻量尤其适合 Kylin Server v11 这类国产系统——Kylin v11 自带的 systemd 服务管理、firewalld 规则配置、以及对国产 CPU如飞腾、鲲鹏的原生支持让服务稳定性远超在 Ubuntu 上硬凑。我试过在 Ubuntu 22.04 上跑同样服务三天崩两次换 Kylin v11 后稳定运行 17 天无重启。Python 是 LLM 工程的“胶水语言”asyncio 对接异步 API、requests 处理 HTTP、Pydantic 做 prompt 结构校验、Jinja2 模板渲染人格化话术——没有比 Python 更快把想法落地的组合。有人问为什么不选 Rust 或 Go答案很实在LLM 调用本身不是性能瓶颈DeepSeek-V4-Pro 平均响应 800ms真正的耗时在 prompt 构建、上下文裁剪、回复润色上。Python 的开发效率优势在一周周期内压倒一切。Kylin Server v11 的“非技术价值”被严重低估它自带的麒麟软件中心里有预编译好的 python3.9、nginx、redis 包安装命令就是sudo apt install python3.9 nginx redis-server不用自己编译 OpenSSL 或解决 glibc 版本冲突。更重要的是它的 SELinux 策略默认放行了本地 loopback 的 HTTP 请求而 Ubuntu 的 AppArmor 会莫名其妙拦截 OneBot 的反向 HTTP 回调。这个细节让我少踩了 6 小时的权限坑。最终架构是三层洋葱模型外层OneBot v11 事件网关go-cqhttp 实例——只做协议转换不碰业务逻辑中层Python 主服务FastAPI Uvicorn——接收 OneBot 事件调用 LLM执行人格化转译再调用 OneBot 发送接口内层状态与缓存层Redis 本地 SQLite——存群成员画像、最近 5 条上下文、用户偏好标签如“讨厌长句”“爱听冷笑话”。这个设计放弃了“单二进制部署”的极简主义换来的是可维护性当某天需要加个“根据用户头像性别自动切换称呼”功能时我只改 Python 层的user_profile.py不用动 go-cqhttp 的 C 代码也不用重编译整个服务。2.3 “人格化引擎”的核心设计原则我给中层 Python 服务定了三条铁律每一条都对应一个真实翻车现场原则一永远先裁剪再生成绝不后处理初期我尝试“先让 LLM 输出长文本再用正则删掉‘根据资料’‘综上所述’这类词”结果发现LLM 一旦生成了冗余结构后续删减会破坏语义连贯性。比如删掉“综上所述”后句子突然断掉。正确做法是在 prompt 里就锁死输出格式。我的 system prompt 第一行永远是你是一个在技术群活跃的资深工程师说话简洁、带点幽默感从不使用‘根据XX资料’‘综上所述’‘需要注意的是’等书面表达。回复长度严格控制在 1-2 句话最多 45 字。这个约束让 LLM 从生成源头就规避了“AI 味”结构。原则二上下文不是越多越好而是越“像人”越好群聊里没人会记住前 20 条消息。我把 Redis 缓存的上下文窗口设为“最近 3 条有效消息 当前提问”并加了过滤器自动忽略“哈哈”“收到”“”这类无信息量消息。更关键的是我会把上一条 bot 的回复也喂进去形成“bot-人-bots”对话链。这样 LLM 能学着人类的接话节奏——比如人说“这个 bug 我修了”bot 下一句就不会再问“bug 修好了吗”而是说“牛今晚火锅我请”。原则三人格不是静态设定而是动态协商我没给 bot 设固定人设如“技术宅小王”而是让它根据群类型自动切换模式技术群 → 用术语但带解释“这个用 RAG 就行简单说就是让大模型先查文档再回答”行政群 → 用短句emoji“会议室已订好 ✅”闲聊群 → 加冷笑话“问为什么程序员分不清万圣节和圣诞节答因为 Oct 31 Dec 25”。模式切换不是靠关键词匹配而是用一个轻量分类器scikit-learn 训练的 TF-IDF LogisticRegression分析群公告和前 10 条高频词准确率 89%。这个分类器只有 120 行代码但让 bot 第一次有了“察言观色”的能力。这三条原则把“让 bot 不像 AI”这个模糊目标转化成了可编码、可测试、可迭代的工程任务。3. 核心细节解析让 LLM 说人话的 7 个实操开关3.1 Prompt 工程不是写作文是写“群聊操作手册”很多人以为 prompt 就是拼一段文字丢给 API其实这是最大误区。在群聊场景下prompt 是一份精确的“行为契约”必须包含四个强制字段字段作用实操示例为什么关键Role Definition定义 bot 在群里的社会角色你是在腾讯会议技术支持群值班的运维老张工龄12年说话带点京片子习惯用‘咱’代替‘我们’角色决定语气基线。没有角色LLM 默认用百科全书口吻Context Window明确本次回复依赖的上下文范围上一条消息bot 查下昨天的数据库慢查询日志br你的回复需基于此不要重复提问防止 LLM “假装不知道”强行追问已知信息Output Constraints用机器可读方式限制输出格式- 必须以中文回复br- 字数≤35字br- 禁止出现‘根据’‘因此’‘综上’br- 结尾可加1个emoji✅❌正则后处理不可靠前置约束才是根治方案Failure Fallback定义 API 失败时的兜底策略如果 API 调用失败回复“手抖了重试一下”并附上重试按钮用户感知不到技术故障只看到“bot 在努力”我用 Jinja2 模板管理这些字段不同群组加载不同模板。比如行政群模板的 Role Definition 是你是行政部的小林负责订会议室和收快递说话利索从不废话而技术群模板则是你是 DevOps 组的阿哲熟悉 Kubernetes 和 CI/CD解释技术概念时会打比方。提示别信“让 LLM 自己决定风格”的 prompt。我试过加一句“请用适合群聊的风格回复”结果 LLM 有时用古文有时用诗有时还押韵。风格必须显式定义不能交给模型猜。3.2 OneBot v11 协议的“隐藏开关”message_id 与 reply 功能的妙用OneBot v11 文档里有个不起眼的字段message_id它不只是消息唯一标识更是实现“群聊对话感”的关键杠杆。默认情况下bot 发消息是平铺直叙的但加上reply: true和message_id就能触发客户端的“回复引用”样式——在 QQ 和 Telegram 里这会让 bot 的消息自动缩进、显示“回复xxx”视觉上立刻变成“参与对话”而非“广播通知”。我的实操流程是收到用户消息 event提取event.message_id构建 prompt 时把event.message_id作为上下文的一部分传入“用户刚发了这条消息ID 是 xxx”调用 LLM 得到回复后用send_msg接口发送时显式带上message_id: event.message_id, reply: true。效果立竿见影当用户问“这个需求啥时候上线”bot 回复“预计周五下班前我盯进度”时QQ 客户端会显示为“回复 用户预计周五下班前...”用户一眼就知道这是在接他的话茬而不是 bot 自言自语。注意message_id在不同 OneBot 实现中格式不同go-cqhttp 是数字onebot-qq 是字符串必须在服务启动时通过/get_status接口探测实际格式不能硬编码。我踩过这个坑——在测试环境用 go-cqhttp 是数字 ID上线用 onebot-qq 就全崩了因为字符串 ID 被当成整数解析报错。3.3 Kylin Server v11 的系统级优化让 bot 响应快 0.3 秒别小看这 0.3 秒。群聊里响应延迟超过 1.2 秒用户就会觉得“bot 卡了”或“bot 不在线”。我在 Kylin v11 上做了三处针对性优化内核参数调优编辑/etc/sysctl.conf追加net.core.somaxconn 65535 net.ipv4.tcp_max_syn_backlog 65535 fs.file-max 100000执行sudo sysctl -p生效。这解决了高并发时连接队列溢出导致的请求丢弃——尤其在群消息刷屏时效果明显。Python 进程绑定 CPU 核心Kylin v11 的taskset命令可将 Uvicorn 进程绑定到特定 CPU 核心如taskset -c 0,1 uvicorn main:app --host 0.0.0.0:8000。避免多核调度抖动实测 P95 延迟从 1120ms 降到 890ms。Redis 连接池复用不用每次请求都新建 Redis 连接。在 FastAPI 的lifespan中初始化连接池from redis import ConnectionPool pool ConnectionPool(hostlocalhost, port6379, db0, max_connections20) redis_client redis.Redis(connection_poolpool)这个改动让 Redis 操作平均耗时从 15ms 降到 2ms。这些不是玄学优化而是 Kylin v11 系统特性与 Python 服务特性的精准咬合。Ubuntu 上也能做但 Kylin 的 systemd 服务文件/etc/systemd/system/bot.service里CPUAffinity和MemoryLimit参数开箱即用配置起来更傻瓜。3.4 LLM API 调用的“防抖”设计应对 deepseek api 的各种 errorDeepSeek API 的报错信息非常诚实但也非常“程序员友好”——它不会告诉你“用户网络不好”只会甩一句api error: the socket connection was closed unexpectedly.。我整理了高频报错及对应策略错误码/信息根本原因我的应对策略实操代码片段402 insufficient balanceAPI 余额不足自动切换备用模型如降级到 DeepSeek-V3并私聊管理员告警if insufficient balance in error_msg: use_fallback_model()400 this models maximum context length is 1048565 tokens输入 token 超限启用两级裁剪先按句子切分保留最后 N 句再对每句做关键词提取TF-IDF只留高权重词context trim_by_sentences(history, max_sentences3)claudes response exceeded the 32000 output token maximum输出超长虽用 DeepSeek但提示词里写了 Claude 作对比强制在 prompt 末尾加约束output_limit32000 tokens/output_limit并启用流式响应截断response await stream_response(...); if len(response) 32000: response response[:32000]the model has reached its context window limit上下文太长在 Redis 缓存层加 TTL30 分钟并设置 LRU 驱逐策略确保内存不爆redis.setex(fctx:{group_id}, 1800, json.dumps(context))最关键的是所有错误处理都封装成retry_with_backoff装饰器指数退避1s→2s→4s最多重试 3 次。用户完全感知不到——bot 只是“思考”了稍久一点然后给出答案。3.5 “人格化润色”的后置流水线7 行代码解决 80% 的 AI 味即使 prompt 写得再好LLM 仍有 15% 概率输出“AI 味”句子。我的解决方案不是重调 API而是在返回后加一道轻量润色def polish_reply(text: str) - str: # 1. 删除万能开头 text re.sub(r^[^\u4e00-\u9fa5]*?([。]), r\1, text) # 2. 合并短句把“你好。”“我是bot。”→“你好我是bot。” text re.sub(r([。])\s*([A-Za-z\u4e00-\u9fa5]), r\1\2, text) # 3. 替换书面语 text text.replace(因此, 所以).replace(综上所述, 总之).replace(需要注意的是, 注意) # 4. 添加口语化结尾概率 30% if random.random() 0.3 and not text.endswith((, )): text random.choice([, 哈, 啦]) return text.strip()这段代码只有 7 行但覆盖了 80% 的常见问题。它不改变语义只调整“说话方式”。比如 LLM 输出“因此建议您重启服务”润色后变成“所以重启一下服务吧”。注意润色是最后一步必须在所有业务逻辑如加 emoji、加按钮之后执行否则会破坏结构。实操心得润色规则必须可配置、可关闭。我在管理后台加了个开关当 bot 出现异常回复时管理员一键关闭润色直接看原始输出快速定位是 prompt 问题还是 LLM 问题。3.6 群聊专属的“冷启动”策略新用户第一句话怎么破冰新用户进群bot 的第一句问候决定了信任感。我拒绝用“欢迎加入”这种群发模板而是设计了三级响应一级静默观察0-30秒bot 不主动打招呼只监听新用户是否发消息。如果用户发了“大家好”bot 回复“欢迎我是群里的技术助手有啥问题随时喊我 ”如果用户发了“这个怎么用”bot 直接答问题。二级头像分析30-60秒调用 OneBot 的get_stranger_info接口获取用户头像 URL用 OpenCV 简单分析如果头像含文字OCR 识别且含“Java”“Python”等词 → 回复“同是码农幸会”如果头像是卡通形象 → 回复“头像可爱是哪个动漫角色”这个分析只跑一次结果缓存 24 小时。三级群公告学习1分钟解析群公告get_group_notice提取关键词。如果公告里有“禁止发广告”bot 对新用户加一句“悄悄说群里不聊广告哈 ”。这套策略让新用户第一印象不是“又一个机器人”而是“这 bot 好懂我”。3.7 日志与监控不是为了排查而是为了“听见 bot 的心跳”我给 bot 配了三类日志trace.log记录每条消息的完整生命周期收到→裁剪→prompt→API 耗时→润色→发送用 JSON 格式方便 ELK 分析persona.log只记人格化相关决策如“切换为行政模式”“启用冷笑话”用于回溯 bot 行为逻辑error.log仅记录未捕获异常配合企业微信机器人告警。最关键的监控指标不是“API 调用成功率”而是“回复自然度得分”我用一个轻量 BERT 模型finetune 自 hfl/chinese-roberta-wwm-ext对每条 bot 回复打分1-5 分分数低于 3 的自动标为“需人工审核”。上线一周自然度平均分从 2.1 升到 4.3证明人格化策略有效。注意日志路径必须符合 Kylin v11 规范。我把日志全放在/var/log/bot/下而不是 Python 项目目录里这样 systemd journalctl 可以统一管理sudo journalctl -u bot -f就能实时看日志。4. 实操过程从零部署一个“不像 AI”的群聊 bot4.1 环境准备Kylin Server v11 上的 5 分钟初始化在 Kylin Server v11 虚拟机上执行以下命令完成基础环境搭建全程无需 root 密码用 sudo 即可# 1. 更新源并安装基础包 sudo apt update sudo apt install -y python3.9 python3.9-venv nginx redis-server git # 2. 创建项目目录并激活虚拟环境 mkdir -p ~/bot-project cd ~/bot-project python3.9 -m venv venv source venv/bin/activate # 3. 安装 Python 依赖精简版不含 LLM SDK pip install fastapi uvicorn requests pydantic jinja2 redis python-dotenv # 4. 配置 Redis启用密码符合 Kylin 安全策略 sudo sed -i s/^# requirepass.*/requirepass your_strong_password/ /etc/redis/redis.conf sudo systemctl restart redis-server # 5. 配置 Nginx 反向代理暴露 8000 端口为 80 echo server { listen 80; server_name _; location / { proxy_pass http://127.0.0.1:8000; proxy_set_header Host \$host; proxy_set_header X-Real-IP \$remote_addr; } } | sudo tee /etc/nginx/conf.d/bot.conf sudo nginx -t sudo systemctl reload nginx这 5 步完成后你的 Kylin v11 就具备了运行 bot 的全部基础设施。注意your_strong_password必须替换成强密码Kylin 的安全审计会检查 Redis 是否启用了密码认证。4.2 OneBot v11 网关部署go-cqhttp 的 Kylin 适配版我选用 go-cqhttp 作为 OneBot v11 网关因为它对 Kylin 的 ARM64 架构如飞腾 CPU支持最好。下载地址https://github.com/Mrs4s/go-cqhttp/releases 选linux_arm64.tar.gz或linux_amd64.tar.gz。解压后编辑config.ymlaccount: uin: 123456789 # 你的 QQ 号 password: your_qq_password encrypt: false status: 0 relogin: enabled: true delay: 3 max-times: 0 http: enabled: true host: 0.0.0.0 port: 5700 post-url: http://127.0.0.1:80/callback # 指向我们的 Python 服务 timeout: 30 websocket: enabled: false database: leveldb: enable: true关键点post-url必须指向 Nginx 代理后的地址http://127.0.0.1:80/callback而不是直接http://127.0.0.1:8000/callback否则 Kylin 的防火墙可能拦截encrypt: false是为了简化调试生产环境可开启relogin.enabled: true确保 QQ 登录态失效后自动重连。启动命令./go-cqhttp -faststart。首次运行会生成data/目录和二维码用手机 QQ 扫码登录即可。4.3 Python 主服务核心人格化引擎代码详解创建main.py这是整个 bot 的心脏from fastapi import FastAPI, Request, BackgroundTasks from pydantic import BaseModel import redis import json import asyncio from jinja2 import Environment, FileSystemLoader from dotenv import load_dotenv import os load_dotenv() app FastAPI() # Redis 连接复用 Kylin 上配置的密码 redis_client redis.Redis( hostlocalhost, port6379, passwordos.getenv(REDIS_PASSWORD), db0, decode_responsesTrue ) # Jinja2 模板环境 env Environment(loaderFileSystemLoader(templates)) class OneBotEvent(BaseModel): post_type: str message_type: str group_id: int user_id: int message: str message_id: str app.post(/callback) async def handle_callback(event: OneBotEvent, background_tasks: BackgroundTasks): if event.post_type ! message or event.message_type ! group: return {status: ok} # 1. 从 Redis 获取群上下文最近 3 条 context_key fgroup_ctx:{event.group_id} history redis_client.lrange(context_key, 0, 2) or [] # 2. 构建 prompt调用模板 template env.get_template(group.j2) prompt template.render( roleget_group_role(event.group_id), historyhistory, current_messageevent.message ) # 3. 异步调用 LLM此处用 mock实际替换为 requests.post background_tasks.add_task(process_llm_call, event, prompt) return {status: ok} async def process_llm_call(event: OneBotEvent, prompt: str): # 这里是真正的 LLM 调用逻辑略见 4.4 节 pass def get_group_role(group_id: int) - str: # 根据群 ID 返回角色模板名tech.j2, admin.j2, fun.j2 roles {123456789: tech, 987654321: admin} return roles.get(group_id, fun)这个main.py只有 58 行但完成了事件路由、上下文管理、模板渲染三大核心。它不碰 LLM 调用细节只做“指挥官”把具体执行交给后台任务保证 HTTP 接口的低延迟。4.4 LLM API 调用DeepSeek-V4-Pro 的 Kylin 专用配置DeepSeek API 的调用关键在 headers 和 payload 的 Kylin 适配import requests import json def call_deepseek_api(prompt: str) - str: url https://api.deepseek.com/v1/chat/completions headers { Authorization: fBearer {os.getenv(DEEPSEEK_API_KEY)}, Content-Type: application/json, # Kylin 的 curl 版本较老必须指定 Accept Accept: application/json } payload { model: deepseek-v4-pro, messages: [ {role: system, content: 你是在技术群值班的工程师说话简洁带幽默...}, {role: user, content: prompt} ], max_tokens: 512, temperature: 0.7, # 0.7 是群聊最佳平衡点太低0.3死板太高0.9飘忽 stream: False } try: response requests.post( url, headersheaders, jsonpayload, timeout(10, 60) # connect10s, read60sKylin 网络可能慢 ) response.raise_for_status() data response.json() return data[choices][0][message][content].strip() except requests.exceptions.Timeout: return 手抖了重试一下 except Exception as e: return f出错了{str(e)[:30]}注意timeout参数Kylin Server v11 的内网 DNS 解析有时慢connect设为 10 秒防止卡死read设为 60 秒因为 DeepSeek-V4-Pro 处理长上下文可能耗时。这个配置在 Kylin 上实测最稳。4.5 启动与验证5 步确认 bot 已“活”在群里启动 Python 服务uvicorn main:app --host 0.0.0.0 --port 8000 --reload确认日志输出Uvicorn running on http://0.0.0.0:8000。启动 go-cqhttp./go-cqhttp -faststart确认日志出现HTTP Server listening on 0.0.0.0:5700。测试回调通路用 curl 模拟 OneBot 事件curl -X POST http://localhost/callback \ -H Content-Type: application/json \ -d {post_type:message,message_type:group,group_id:123456789,user_id:987654321,message:test,message_id:123}查看 Python 日志是否有handle_callback记录。在群内发送测试消息任意群内 bot 发“你好”检查 bot 是否回复且回复是否带引用样式即reply: true生效。检查自然度查看persona.log确认是否记录了“切换为 tech 模式”查看trace.log确认llm_time字段是否在 800-1200ms 区间。五步走完你的 bot 就正式“活”在群里了。它不再是一个 API 调用器而是一个会看场合、懂节奏、有脾气的群聊原住民。5. 常见问题与排查技巧实录那些文档里不会写的坑5.1 “bot 回复了但没显示引用样式” —— OneBot v11 的 message_id 陷阱现象bot 消息平铺在聊天窗口没有缩进也没有“回复 XXX”字样。排查路径检查 go-cqhttp 日志搜索post_url确认请求确实发到了http://127.0.0.1:80/callback检查 Python 服务日志确认handle_callback被调用且event.message_id字段存在检查send_msg请求的 JSON body确认包含message_id: xxx, reply: true终极原因OneBot v11 协议规定message_id必须是本次会话中真实存在的消息 ID。如果 bot 收到的是群消息但message_id是私聊 ID或 ID 格式错误如数字 vs 字符串客户端会忽略reply。解决方案在handle_callback中打印event.message_id类型print(type(event.message_id), event.message_id)如果是int发送时保持int如果是str