fix: prevent outbound echo loops and reduce AgentScope warning noise
Some checks failed
Pre-commit / run (ubuntu-latest) (push) Has been cancelled
Deploy Sphinx documentation to Pages / build_en (ubuntu-latest, 3.10) (push) Has been cancelled
Deploy Sphinx documentation to Pages / build_zh (ubuntu-latest, 3.10) (push) Has been cancelled
Python Unittest Coverage / test (macos-15, 3.10) (push) Has been cancelled
Python Unittest Coverage / test (macos-15, 3.11) (push) Has been cancelled
Python Unittest Coverage / test (macos-15, 3.12) (push) Has been cancelled
Python Unittest Coverage / test (ubuntu-latest, 3.10) (push) Has been cancelled
Python Unittest Coverage / test (ubuntu-latest, 3.11) (push) Has been cancelled
Python Unittest Coverage / test (ubuntu-latest, 3.12) (push) Has been cancelled
Python Unittest Coverage / test (windows-latest, 3.10) (push) Has been cancelled
Python Unittest Coverage / test (windows-latest, 3.11) (push) Has been cancelled
Python Unittest Coverage / test (windows-latest, 3.12) (push) Has been cancelled

This commit is contained in:
2026-03-02 18:59:09 +08:00
parent 6c7f1c8ea9
commit 2c09fcf9e6
2 changed files with 34 additions and 0 deletions

View File

@@ -27,6 +27,7 @@ class QingjianClient:
self.pending_images: dict[str, list[str]] = defaultdict(list) self.pending_images: dict[str, list[str]] = defaultdict(list)
self.auto_quote_tasks: dict[str, asyncio.Task] = {} self.auto_quote_tasks: dict[str, asyncio.Task] = {}
self.last_reply_key: dict[str, str] = {} self.last_reply_key: dict[str, str] = {}
self.recent_outbound: dict[str, tuple[str, float]] = {}
@staticmethod @staticmethod
def _customer_key(data: dict) -> str: def _customer_key(data: dict) -> str:
@@ -53,6 +54,9 @@ class QingjianClient:
self.logger.info("[发送] %s", message.get("msg", "")) self.logger.info("[发送] %s", message.get("msg", ""))
async def send_reply(self, data: dict, text: str, trace_id: str = "-") -> None: async def send_reply(self, data: dict, text: str, trace_id: str = "-") -> None:
text = str(text or "").strip()
if not text:
return
msg = { msg = {
"msg_id": "", "msg_id": "",
"acc_id": data.get("acc_id", ""), "acc_id": data.get("acc_id", ""),
@@ -66,8 +70,23 @@ class QingjianClient:
} }
activity_event(self.logger, "send_reply_attempt", trace_id=trace_id, customer_id=data.get("from_id", "-"), msg=text) activity_event(self.logger, "send_reply_attempt", trace_id=trace_id, customer_id=data.get("from_id", "-"), msg=text)
await self.send_message(msg) await self.send_message(msg)
self.recent_outbound[self._customer_key(data)] = (text, time.monotonic())
activity_event(self.logger, "send_reply_success", trace_id=trace_id, customer_id=data.get("from_id", "-"), msg=text) activity_event(self.logger, "send_reply_success", trace_id=trace_id, customer_id=data.get("from_id", "-"), msg=text)
def _is_outbound_echo(self, data: dict, msg: str) -> bool:
"""
轻简可能会把我方刚发送文本回推为“收到消息”。
对同 customer_key 的“短时间完全相同文本”做回环拦截,避免无限对话。
"""
key = self._customer_key(data)
last = self.recent_outbound.get(key)
if not last:
return False
last_msg, ts = last
if (time.monotonic() - ts) > 120:
return False
return str(msg or "").strip() == last_msg
async def _handle_decision(self, data: dict, merged_msg: str, *, auto_quote: bool = False) -> None: async def _handle_decision(self, data: dict, merged_msg: str, *, auto_quote: bool = False) -> None:
key = self._customer_key(data) key = self._customer_key(data)
trace_id = build_trace_id(data.get("acc_id", ""), data.get("from_id", ""), merged_msg) trace_id = build_trace_id(data.get("acc_id", ""), data.get("from_id", ""), merged_msg)
@@ -192,6 +211,15 @@ class QingjianClient:
self.logger.info("[收消息] acc=%s from=%s type=%s msg=%s", data.get("acc_id", ""), data.get("from_id", ""), msg_type, msg) self.logger.info("[收消息] acc=%s from=%s type=%s msg=%s", data.get("acc_id", ""), data.get("from_id", ""), msg_type, msg)
await post_tianwang_callback("message_received", data, extra={"msg_type": msg_type}) await post_tianwang_callback("message_received", data, extra={"msg_type": msg_type})
if self._is_outbound_echo(data, msg):
activity_event(
self.logger,
"inbound_ignored",
customer_id=data.get("from_id", "-"),
reason="outbound_echo_loop_guard",
)
return
if rule.ignore: if rule.ignore:
activity_event(self.logger, "inbound_ignored", customer_id=data.get("from_id", "-"), reason=rule.reason) activity_event(self.logger, "inbound_ignored", customer_id=data.get("from_id", "-"), reason=rule.reason)
return return

View File

@@ -11,4 +11,10 @@ def setup_logger() -> logging.Logger:
formatter = logging.Formatter("[%(asctime)s] %(levelname)s: %(message)s", "%H:%M:%S") formatter = logging.Formatter("[%(asctime)s] %(levelname)s: %(message)s", "%H:%M:%S")
handler.setFormatter(formatter) handler.setFormatter(formatter)
logger.addHandler(handler) logger.addHandler(handler)
# 降低 AgentScope 内部推理/格式器日志噪音,保留本项目活动日志。
logging.getLogger("agentscope").setLevel(logging.ERROR)
logging.getLogger("agentscope.formatter").setLevel(logging.ERROR)
logging.getLogger("agentscope.agent").setLevel(logging.ERROR)
return logger return logger