补充 ai_auto_response 的完整模型决策日志

变更项:
1. 新增 LLM_RESULT 日志,记录模型输出的 should_reply、reply_mode、topic、reply 预览和原始响应预览。
2. 新增 BLOCKED_REPLY 日志,记录模型原本想回复但被 post_llm_cooldown、过期、覆盖或重复回复拦下的具体原因。
3. 保留原有 SKIP 与 SENT 日志,使模型判定、发送阻断和最终发出三段链路可以串起来排查。
This commit is contained in:
liuwei
2026-04-28 17:51:10 +08:00
parent 348353fe8c
commit ec29bc7551
2 changed files with 81 additions and 0 deletions

View File

@@ -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,

View File

@@ -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} "