From ec29bc75516ce2c02014fde9375524da55e2a51e Mon Sep 17 00:00:00 2001 From: liuwei Date: Tue, 28 Apr 2026 17:51:10 +0800 Subject: [PATCH] =?UTF-8?q?=E8=A1=A5=E5=85=85=20ai=5Fauto=5Fresponse=20?= =?UTF-8?q?=E7=9A=84=E5=AE=8C=E6=95=B4=E6=A8=A1=E5=9E=8B=E5=86=B3=E7=AD=96?= =?UTF-8?q?=E6=97=A5=E5=BF=97?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 变更项: 1. 新增 LLM_RESULT 日志,记录模型输出的 should_reply、reply_mode、topic、reply 预览和原始响应预览。 2. 新增 BLOCKED_REPLY 日志,记录模型原本想回复但被 post_llm_cooldown、过期、覆盖或重复回复拦下的具体原因。 3. 保留原有 SKIP 与 SENT 日志,使模型判定、发送阻断和最终发出三段链路可以串起来排查。 --- plugins/ai_auto_response/main.py | 56 +++++++++++++++++++++ plugins/ai_auto_response/runtime/logging.py | 25 +++++++++ 2 files changed, 81 insertions(+) diff --git a/plugins/ai_auto_response/main.py b/plugins/ai_auto_response/main.py index e1bf312..d190ed6 100644 --- a/plugins/ai_auto_response/main.py +++ b/plugins/ai_auto_response/main.py @@ -608,6 +608,21 @@ class AIAutoResponsePlugin(MessagePluginInterface): fallback_reply_mode=reply_mode, fallback_topic=trigger.topic or "", ) + # 这里补一条完整的模型决策日志: + # 1. 无论模型最终决定回还是不回,都先把 should_reply / reply_mode / topic / reply 预览记下来; + # 2. 同时保留 sanitize 后的原始响应预览,方便排查“模型其实回了什么 JSON / 文本”; + # 3. 这样后面即使被 cooldown、过期、去重拦住,也能明确看到“模型本来想怎么做”。 + self._log_event( + "llm_result", + room_id=room_id, + sender=sender, + trigger_type=trigger.trigger_type, + should_reply=bool(llm_result.get("should_reply", True)), + reply_mode=str(llm_result.get("reply_mode", reply_mode) or reply_mode), + topic=str(llm_result.get("topic_summary", "") or llm_result.get("topic_id", "") or trigger.topic or ""), + response_preview=preview_text(str(llm_result.get("reply", "") or ""), 120), + raw_response_preview=preview_text(response, 160), + ) if not llm_result.get("should_reply", True): self._log_event( "skip", @@ -639,6 +654,16 @@ class AIAutoResponsePlugin(MessagePluginInterface): # 2. 模型先统一判断 should_reply,只有当它明确想回时,才进入频率控制; # 3. 仍然保留冷却,是为了守住群内刷屏风险,但职责已经变成“限制发送”,不是“替模型做语义裁决”。 if not self.cooldown.pass_cooldown(room_id, sender, trigger.__dict__): + self._log_event( + "blocked_reply", + room_id=room_id, + sender=sender, + reason=f"post_llm_{trigger.__dict__.get('_cooldown_reason', 'cooldown')}", + trigger_type=trigger.trigger_type, + reply_mode=reply_mode, + topic=selected_topic, + response_preview=preview_text(reply_text, 120), + ) self._log_event( "skip", room_id=room_id, @@ -657,6 +682,17 @@ class AIAutoResponsePlugin(MessagePluginInterface): # 2. 即使消息进模型时还新鲜,等模型回完也可能已经跟不上群聊了; # 3. 这种情况下直接放弃发送,比突然补回旧话更自然。 if self._is_message_stale(message): + self._log_event( + "blocked_reply", + room_id=room_id, + sender=sender, + reason="stale_before_send", + trigger_type=trigger.trigger_type, + reply_mode=reply_mode, + topic=selected_topic, + response_preview=preview_text(final_response_text, 120), + age_sec=round(self._get_message_queue_age_sec(message), 2), + ) self._log_event( "skip", room_id=room_id, @@ -673,6 +709,16 @@ class AIAutoResponsePlugin(MessagePluginInterface): # 2. 这时即使模型产出了结果,也不应该再把旧回复补发出去; # 3. 直接丢弃旧结果,让群里只看到贴着最新现场的回复。 if self._is_message_superseded(message): + self._log_event( + "blocked_reply", + room_id=room_id, + sender=sender, + reason="superseded_before_send", + trigger_type=trigger.trigger_type, + reply_mode=reply_mode, + topic=selected_topic, + response_preview=preview_text(final_response_text, 120), + ) self._log_event( "skip", room_id=room_id, @@ -690,6 +736,16 @@ class AIAutoResponsePlugin(MessagePluginInterface): reply_text=final_response_text, expiry_sec=reply_dedup_expiry, ): + self._log_event( + "blocked_reply", + room_id=room_id, + sender=sender, + reason="duplicate_reply", + trigger_type=trigger.trigger_type, + reply_mode=reply_mode, + topic=selected_topic, + response_preview=preview_text(final_response_text, 120), + ) self._log_event( "skip", room_id=room_id, diff --git a/plugins/ai_auto_response/runtime/logging.py b/plugins/ai_auto_response/runtime/logging.py index c9353a3..555f348 100644 --- a/plugins/ai_auto_response/runtime/logging.py +++ b/plugins/ai_auto_response/runtime/logging.py @@ -76,6 +76,31 @@ def build_log_summary(event: str, data: Dict[str, Any]) -> str: f"err={data.get('last_error', '')}" ).strip() + if event == "llm_result": + return ( + f"[XIAONIU] LLM_RESULT room={room} user={sender} " + f"trigger={data.get('trigger_type', 'none')} " + f"want={yn(data.get('should_reply'))} " + f"mode={data.get('reply_mode', '')} " + f"topic={data.get('topic', '-') or '-'} " + f"reply={data.get('response_preview', '')} " + f"raw={data.get('raw_response_preview', '')}" + ).strip() + + if event == "blocked_reply": + age_text = "" + if data.get("age_sec") not in (None, ""): + age_text = f" age={data.get('age_sec')}" + return ( + f"[XIAONIU] BLOCKED_REPLY room={room} user={sender} " + f"reason={data.get('reason', '')} " + f"trigger={data.get('trigger_type', 'none')} " + f"mode={data.get('reply_mode', '')} " + f"topic={data.get('topic', '-') or '-'} " + f"reply={data.get('response_preview', '')}" + f"{age_text}" + ).strip() + if event == "sent": return ( f"[XIAONIU] SENT room={room} user={sender_name}/{sender} "