feat: adaptive debounce and intent-driven quote trigger tuning
This commit is contained in:
@@ -4,6 +4,7 @@ import json
|
||||
import re
|
||||
import logging
|
||||
import random
|
||||
import secrets
|
||||
import time
|
||||
import hashlib
|
||||
from collections import deque
|
||||
@@ -88,6 +89,7 @@ class QingjianAPIClient:
|
||||
|
||||
# 消息防抖:同一客户连续发消息时,等待 N 秒后合并处理
|
||||
self._DEBOUNCE_SECONDS = MESSAGE_DEBOUNCE_SECONDS if isinstance(MESSAGE_DEBOUNCE_SECONDS, int) else 8
|
||||
self._adaptive_debounce_enabled = os.getenv("ADAPTIVE_DEBOUNCE_ENABLED", "true").lower() in ("1", "true", "yes")
|
||||
self._debounce_tasks: dict = {} # customer_key -> asyncio.Task
|
||||
self._pending_msgs: dict = {} # customer_key -> list[data]
|
||||
self._image_enabled = IMAGE_MODULE_ENABLED
|
||||
@@ -436,9 +438,11 @@ class QingjianAPIClient:
|
||||
if old_task and not old_task.done():
|
||||
old_task.cancel()
|
||||
|
||||
debounce_seconds = self._pick_debounce_seconds(data, msg_body)
|
||||
|
||||
# 创建新的延迟处理任务
|
||||
async def _delayed(capture_key, capture_data):
|
||||
await asyncio.sleep(self._DEBOUNCE_SECONDS)
|
||||
async def _delayed(capture_key, capture_data, wait_s: float):
|
||||
await asyncio.sleep(wait_s)
|
||||
msgs = self._pending_msgs.pop(capture_key, [])
|
||||
if not msgs:
|
||||
return
|
||||
@@ -451,9 +455,88 @@ class QingjianAPIClient:
|
||||
merged_data['msg'] = merged_msg
|
||||
await self._agent_reply_serialized(merged_data)
|
||||
|
||||
task = asyncio.create_task(_delayed(key, data))
|
||||
task = asyncio.create_task(_delayed(key, data, debounce_seconds))
|
||||
self._debounce_tasks[key] = task
|
||||
|
||||
@staticmethod
|
||||
def _rand_between(low: float, high: float) -> float:
|
||||
if high <= low:
|
||||
return float(low)
|
||||
# 使用 secrets 增强随机性,避免固定周期导致机械感
|
||||
span = high - low
|
||||
return round(low + span * (secrets.randbelow(1000) / 1000.0), 2)
|
||||
|
||||
def _guess_intent_for_debounce(self, msg: str) -> str:
|
||||
text = (msg or "").strip()
|
||||
if not text:
|
||||
return "unknown"
|
||||
if self._msg_has_image_url(text):
|
||||
return "image"
|
||||
try:
|
||||
from utils.intent_analyzer import detect_intent_keywords
|
||||
intent = detect_intent_keywords(text)
|
||||
except Exception:
|
||||
intent = ""
|
||||
if intent:
|
||||
return intent
|
||||
lower = text.lower()
|
||||
if any(k in lower for k in ["报价", "多少钱", "价格", "贵", "优惠"]):
|
||||
return "询价"
|
||||
if any(k in lower for k in ["做一下", "改一下", "需求", "门头", "上面的字", "处理"]):
|
||||
return "修改"
|
||||
if any(k in lower for k in ["在吗", "你好", "有人"]):
|
||||
return "打招呼"
|
||||
return "unknown"
|
||||
|
||||
@staticmethod
|
||||
def _looks_like_requirement_text(msg: str) -> bool:
|
||||
text = (msg or "").strip().lower()
|
||||
if not text:
|
||||
return False
|
||||
req_kw = (
|
||||
"做一下", "改一下", "处理一下", "这个字", "上面的字", "门头", "去背景", "抠图",
|
||||
"换色", "调色", "清晰", "高清", "尺寸", "比例", "横版", "竖版", "排版", "改字",
|
||||
"按这个做", "照这个做", "就这张", "看看做", "弄一下",
|
||||
)
|
||||
return any(k in text for k in req_kw)
|
||||
|
||||
def _pick_debounce_seconds(self, data: dict, msg: str) -> float:
|
||||
"""意图驱动防抖:不同意图不同等待区间,并引入轻微随机。"""
|
||||
base = max(1.0, float(self._DEBOUNCE_SECONDS))
|
||||
if not self._adaptive_debounce_enabled:
|
||||
return base
|
||||
|
||||
intent = self._guess_intent_for_debounce(msg)
|
||||
is_req = self._looks_like_requirement_text(msg)
|
||||
has_img = self._msg_has_image_url(msg)
|
||||
# 区间策略:越明确、越短消息,等待越短;需求描述类稍长
|
||||
if intent == "打招呼":
|
||||
low, high = 1.0, min(3.0, base)
|
||||
elif intent in ("询价", "砍价"):
|
||||
low, high = 2.0, min(5.0, base)
|
||||
elif intent in ("修改", "批量"):
|
||||
low, high = max(3.0, base * 0.65), min(18.0, base + 2.0)
|
||||
elif intent == "转接":
|
||||
low, high = 1.0, 2.5
|
||||
else:
|
||||
low, high = max(2.0, base * 0.5), base
|
||||
|
||||
# 发图后的需求描述,优先“多等一点”收集完整需求,减少半句回复
|
||||
if is_req and not has_img:
|
||||
low = max(low, min(10.0, base * 0.8))
|
||||
high = max(high, min(20.0, base + 4.0))
|
||||
|
||||
# 短句更快,长句稍慢,避免把连续半句拆开
|
||||
text_len = len((msg or "").strip())
|
||||
if text_len <= 4:
|
||||
high = min(high, max(low + 0.2, 2.5))
|
||||
elif text_len >= 18:
|
||||
low = min(high, low + 0.6)
|
||||
|
||||
wait_s = self._rand_between(low, high)
|
||||
logger.info(f"防抖等待 {wait_s}s | intent={intent} | len={text_len}")
|
||||
return wait_s
|
||||
|
||||
def _msg_has_image_url(self, msg: str) -> bool:
|
||||
"""判断文本消息里是否包含图片URL(客户粘贴了图片链接,可能带前缀文字如 有吗#*#https://...)"""
|
||||
if not msg:
|
||||
|
||||
Reference in New Issue
Block a user