Appearance
团队 Bot 的必回复工程
第二部 · Agent 实践 · 第19章
撰写日期:2026-07-06
结论
一个坐在团队 IM 里的 bot,最伤信任的行为不是答错,而是已读不回:用户分不清它是没听见、听不懂、还是出故障了,几次之后就不再找它。我给团队 bot 定的铁律:
凡是 bot 能收到的消息,能不能办都必须有回应;凡是 bot 收不到的消息,bot 要能向用户解释"为什么我收不到"。
这条铁律的工程含义比看上去多——"没回复"不是一种故障,是五种故障的共同表象。
1. "没回复"的故障分类
排查沿接收链路走,每个断点的证据和治法都不同:
| 断点 | 证据特征 | 治法 |
|---|---|---|
| 平台不投递 | 事件流查无此消息,bot 视角的消息 API 也查无此消息 | bot 端无解;引导用户换发送方式 + bot 自知边界(§3) |
| 消息类型被静默过滤 | 日志里连 handle 记录都没有 | 非文本兜底回复(§2) |
| 线上进程是旧代码 | 功能"代码里明明有"但线上无响应 | 改代码必须显式重启进程(§4) |
| 处理中异常被吞 | 有 handle 记录、无 reply 记录 | catch-all 兜底:"没处理成 + 原因 + 找谁" |
| 长连接重连间隙 | 断线的十几秒内消息丢失且平台不重放 | 无解,低频容忍;重要的是知道它存在 |
排查心法沿用"查事实源,不做剧情推理":先查事件流有没有、再查 bot 视角的消息 API 有没有——两处都没有,就是平台没给你,别再在自己代码里找。
2. 非文本消息的兜底设计
大多数 bot 框架的默认写法是 if message_type != "text": return——一行静默吞掉图片、富文本、文件、语音。按"能真处理就不敷衍、不能处理就说明"分层:
| 消息类型 | 策略 | 理由 |
|---|---|---|
| 富文本 | 抽出纯文本和链接,走正常对话流程 | 内容就在里面,降级成"读不了"是偷懒 |
| 图片 / 表情 / 文件 / 语音 | 私聊回一句:读不了这类 + 我能读什么(文字、链接) | 有回应 + 顺便教育了正确用法 |
| 群聊里的非文本 | 保持沉默 | 表情包没法 @bot,逢图必回等于刷屏 |
注意私聊和群聊的必回复标准不同:私聊 = 100% 回应;群聊 = 被点到才回应。把两者混为一谈,要么群里刷屏,要么私聊装死。
3. Bot 要能解释自己的边界
有些消息 bot 永远收不到(我踩到的实例:通过平台"分享面板"发给 bot 的文档卡片,IM 平台根本不投递给机器人——事件流和消息 API 里都不存在这条消息)。收不到就无法回复,这是物理边界;但可以做到自知:把已确认的接收盲区写进 bot 的人设 / 系统提示,当用户问"刚发你的怎么没反应"时,bot 能给出准确解释和替代做法("分享卡片我收不到,把链接粘贴成文字发我")。
能力有边界不丢人,不可解释的沉默才丢人。
4. 只救死不换血:守护进程的盲区
daemon guardian(定时把挂掉的常驻进程拉起来)有一个容易忘掉的隐含语义:它只保证"有进程在跑",不保证"跑的是最新代码"。我踩过的版本:给事件监听进程加了新的回调处理器,代码合入后按钮点击依然全部超时——因为线上跑的还是加处理器之前启动的旧进程,守护脚本看它活得好好的,永远不会替换它。
纪律:改常驻进程的代码,部署动作里必须包含显式重启。表现为"这功能代码里明明有"的灵异故障,第一嫌疑人就是旧进程。
5. 消息里的任务识别:附言才是任务,链接只是材料
实翻车:有人给 bot 发了两个代码仓库链接,问"有啥区别?"——bot 只取了第一个链接、丢掉了问题文本,自顾自输出了第一个仓库的推荐介绍。根因是把"消息里含链接"直接映射成了"任务 = 写链接推荐",消息的其余部分根本没进 prompt。
| 消息形态 | 正确的任务 |
|---|---|
| 单链接、无附言 | 推荐 / 摘要(默认任务成立) |
| 链接 + 问题 | 回答问题,链接内容只是材料 |
| 多链接 + 比较词 | 对比,落到"什么场景该用哪个" |
实现很简单:用户问题 = 原文剔除所有 URL 后的剩余文本;非空就从"推荐模式"切到"回答模式",prompt 首句变成"有人发来 N 个链接并问:「{question}」,直接回答"。消息表面形态相同(都含链接),意图可以完全不同——意图判定要基于整条消息,不是基于触发特征。
6. 用合成事件做端到端验证
bot 的行为验证不需要拉真人配合:往事件流里注入一条构造的消息事件(假装某用户发来一张图片、或两个链接加一句提问),让它走完真实管线,在目标会话里看到真实回复。这比单元测试更接近真相——它测的不是函数返回值,是用户实际会收到什么。修完 §2 和 §5 的当天,我就是用两条合成事件各验了一遍才收工的。
7. "该不该说话"的闸门要写进代码,别托付给平台投递范围
必回复的反面是必不回复——群里绝大多数消息 bot 不该接话。我踩过的坑是把这个判断托付给了平台:"反正平台只会把 @我 的消息投递给我,我自己不用校验。"后台把消息订阅范围从"仅 @我"放开到"全群消息"之后,这个隐式假设瞬间崩成刷屏——bot 对每条消息都应了一句。
把业务规则寄存在外部平台的投递行为上,是一个你不控制、且会静默漂移的依赖。触发判断(是否被 @、该不该主动插话)必须在自己代码里显式校验。
硬闸门很简单:处理前校验 mentions 里是否真含本 bot 的 id,不含就不接话——配置怎么放开都兜得住。
主动插话的配额也值得一提:从"固定 N 次/天"改成"不超过当天真人消息量的百分比",能让 bot 的活跃度随群的活跃度自适应——冷群不打扰、热群多参与,而不是在安静的群里按固定频率刷存在感。