上周同事在群里报了个 VDI 更新后黑屏的问题,我这边的 AI 分身回了一句「收到,问题已解决就好」——可那问题压根没解决。查下来是个典型的上下文被污染事故,这次 v0.4.8 主要就是把这类答非所问摁回去,顺手把排版、引用回复降级和安全兜底一起收了。

事故:AI 对「已解决」的事答非所问

根因挺隐蔽:轮询器用 list-all 把历史消息重新拉了一遍,其中一张 8-25 的旧截图(上面写着「桌面分配失败」)因为去重环节漏标,在 8-31 被当成新消息重放,和当天同事的真消息拼在一起喂给了模型。模型看到「桌面分配失败 → 已解决」这个旧上下文,误判成已经完结的话题,于是回了句客套话。三件事叠在一起才出事:去重表标记不全、历史窗口太粗、OCR 兜底把系统提示当成了图片内容。

修复 1:把「新消息窗口」和「历史窗口」解耦

原来轮询的年龄门槛直接吃 history_days(天级),对这个场景太粗了——一天前的消息都算「新」。这次新增 poll_new_message_max_age_hours(默认 24h),让新消息只认最近 N 小时内的,历史窗口仍然按天走,各管各的:

poller:
  history_days: 3                    # 历史上下文回溯窗口(天)
  poll_new_message_max_age_hours: 24 # 只处理最近 N 小时内的新消息(默认 24)

修复 2:批内防抖,剔除陈旧记录

就算年龄门槛放开,同一批里也可能混入超过最新消息 1 天的旧记录(比如缺时间戳的连发)。加了一道 _drop_stale_messages_in_batch():以批内最新消息时间为锚,剔除超过 1 天的陈旧项,但保守保留正常连发和缺时间戳的消息。

原则:宁可漏掉一条旧消息,也不能让旧消息污染当前上下文。

修复 3:清理 OCR caption 噪声

OCR 兜底 caption 会把 [图片消息](mediaId=...) 注意:如需下载使用dws命令下载 这种系统提示当成图片内容喂进去,模型可能对它产生幻觉。正则会排掉这类噪声,只保留真正的视觉描述。

长回复排版:让 IM 输出读得下去

另一个被吐槽的点:长回复(比如一段故障排查步骤)在钉钉里全挤成一段,标题列表全丢,读着像一坨。这次加了一套排版指令约束输出:

原则:把格式约束写进 prompt,让模型自己排版,而不是堆正则后处理——后者只会越修越歪。

引用回复降级:别再刷 ERROR 了

单聊走原生引用回复会因为话题圈不可用必然失败,群聊守卫之前把这种降级记成 ERROR,日志天天刷屏,看着像出了大故障。改成:单聊直接普通发送,群聊降级降到 debug 级。日志干净了,也不误报。

承诺边界:别把主人当成第三方

之前承诺边界写「把决定权交回本人」,隐含主人在场,模型就会反问对话者「这条消息是 XX 发来的吗」——明知故问。改成如实告诉对方「这事得主人拍板」,不再虚构主人在场。这一处提醒我:写指令时要检查每个代词在替模型定义「对面坐着谁」,否则模型会用错误的前提去补全。

安全加固两处

升级注意

无破坏性变更,新增 poll_new_message_max_age_hours(默认 24,一般无需调整)。含轮询器 / Web 运行时改动,升级后需重启 Linkora 服务生效。

完整变更见 CHANGELOG,代码在 GitHub。