From 500d5c11b3f2c0a7c76f28735f4c794fe5dd34b9 Mon Sep 17 00:00:00 2001 From: SengokuCola <1026294844@qq.com> Date: Wed, 29 Apr 2026 17:46:29 +0800 Subject: [PATCH] =?UTF-8?q?fix=EF=BC=9Atiminggate=E9=9D=9E=E6=B3=95?= =?UTF-8?q?=E5=B7=A5=E5=85=B7=E4=BC=9A=E9=87=8D=E8=AF=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- pytests/test_maisaka_timing_gate.py | 69 ++++++++++++++++++++++++++++- src/chat/replyer/replyer_manager.py | 1 - src/maisaka/reasoning_engine.py | 58 +++++++++++++++++++----- 3 files changed, 114 insertions(+), 14 deletions(-) diff --git a/pytests/test_maisaka_timing_gate.py b/pytests/test_maisaka_timing_gate.py index 235dca67..02b314da 100644 --- a/pytests/test_maisaka_timing_gate.py +++ b/pytests/test_maisaka_timing_gate.py @@ -3,7 +3,7 @@ from types import SimpleNamespace import pytest -from src.core.tooling import ToolExecutionResult +from src.core.tooling import ToolExecutionResult, ToolInvocation from src.llm_models.payload_content.tool_option import ToolCall from src.maisaka.chat_loop_service import ChatResponse, MaisakaChatLoopService from src.maisaka.context_messages import AssistantMessage, TIMING_GATE_INVALID_TOOL_HINT_SOURCE @@ -45,8 +45,12 @@ async def test_timing_gate_invalid_tool_defaults_to_no_reply(monkeypatch: pytest runtime._enter_stop_state = _enter_stop_state engine = MaisakaReasoningEngine(runtime) # type: ignore[arg-type] + call_count = 0 + async def _fake_timing_gate_sub_agent(**kwargs: object) -> ChatResponse: + nonlocal call_count del kwargs + call_count += 1 return _build_chat_response([ ToolCall(call_id="invalid-timing-tool", func_name="finish", args={}), ]) @@ -61,6 +65,7 @@ async def test_timing_gate_invalid_tool_defaults_to_no_reply(monkeypatch: pytest action, response, tool_results, tool_monitor_results = await engine._run_timing_gate(object()) # type: ignore[arg-type] assert action == "no_reply" + assert call_count == 3 assert response.tool_calls[0].func_name == "finish" assert runtime.stopped is True assert tool_monitor_results == [] @@ -68,10 +73,72 @@ async def test_timing_gate_invalid_tool_defaults_to_no_reply(monkeypatch: pytest assert runtime._chat_history[0].source == TIMING_GATE_INVALID_TOOL_HINT_SOURCE assert "finish" in runtime._chat_history[0].processed_plain_text assert tool_results == [ + "- retry [非法 Timing 工具]: 返回了 finish,将重试 (1/3)", + "- retry [非法 Timing 工具]: 返回了 finish,将重试 (2/3)", "- no_reply [非法 Timing 工具]: 返回了 finish,已停止本轮并等待新消息", ] +@pytest.mark.asyncio +async def test_timing_gate_invalid_tool_retries_until_valid(monkeypatch: pytest.MonkeyPatch) -> None: + runtime = SimpleNamespace( + _force_next_timing_continue=False, + _chat_history=[], + log_prefix="[test]", + stopped=False, + ) + + def _enter_stop_state() -> None: + runtime.stopped = True + + runtime._enter_stop_state = _enter_stop_state + engine = MaisakaReasoningEngine(runtime) # type: ignore[arg-type] + responses = [ + _build_chat_response([ToolCall(call_id="invalid-timing-tool", func_name="finish", args={})]), + _build_chat_response([ToolCall(call_id="valid-timing-tool", func_name="continue", args={})]), + ] + + async def _fake_timing_gate_sub_agent(**kwargs: object) -> ChatResponse: + del kwargs + return responses.pop(0) + + async def _fake_invoke_tool_call( + tool_call: ToolCall, + latest_thought: str, + anchor_message: object, + *, + append_history: bool = True, + store_record: bool = True, + ) -> tuple[ToolInvocation, ToolExecutionResult, None]: + del latest_thought, anchor_message, append_history, store_record + return ( + ToolInvocation(tool_name=tool_call.func_name, call_id=tool_call.call_id), + ToolExecutionResult( + tool_name=tool_call.func_name, + success=True, + content="继续执行主流程", + metadata={"timing_action": "continue"}, + ), + None, + ) + + monkeypatch.setattr(engine, "_run_timing_gate_sub_agent", _fake_timing_gate_sub_agent) + monkeypatch.setattr(engine, "_invoke_tool_call", _fake_invoke_tool_call) + + action, response, tool_results, tool_monitor_results = await engine._run_timing_gate(object()) # type: ignore[arg-type] + + assert action == "continue" + assert response.tool_calls[0].func_name == "continue" + assert runtime.stopped is False + assert len(runtime._chat_history) == 2 + assert all(message.source != TIMING_GATE_INVALID_TOOL_HINT_SOURCE for message in runtime._chat_history) + assert tool_results == [ + "- retry [非法 Timing 工具]: 返回了 finish,将重试 (1/3)", + "- continue [成功]: 继续执行主流程", + ] + assert tool_monitor_results[0]["tool_name"] == "continue" + + def test_timing_gate_invalid_tool_hint_keeps_only_latest() -> None: old_hint = SimpleNamespace(source=TIMING_GATE_INVALID_TOOL_HINT_SOURCE) runtime = SimpleNamespace(_chat_history=[old_hint]) diff --git a/src/chat/replyer/replyer_manager.py b/src/chat/replyer/replyer_manager.py index bd9bf9d3..6677d1ab 100644 --- a/src/chat/replyer/replyer_manager.py +++ b/src/chat/replyer/replyer_manager.py @@ -36,7 +36,6 @@ class ReplyerManager: generator_type = self._get_maisaka_generator_type() if replyer_type == "maisaka" else "" cache_key = f"{replyer_type}:{generator_type}:{stream_id}" if cache_key in self._repliers: - logger.info(f"[ReplyerManager] 命中缓存 replyer: cache_key={cache_key}") return self._repliers[cache_key] target_stream = chat_stream or _chat_manager.get_session_by_session_id(stream_id) diff --git a/src/maisaka/reasoning_engine.py b/src/maisaka/reasoning_engine.py index 5629b007..ab7484a2 100644 --- a/src/maisaka/reasoning_engine.py +++ b/src/maisaka/reasoning_engine.py @@ -54,6 +54,7 @@ logger = get_logger("maisaka_reasoning_engine") TIMING_GATE_CONTEXT_LIMIT = 24 TIMING_GATE_MAX_TOKENS = 384 +TIMING_GATE_MAX_ATTEMPTS = 3 TIMING_GATE_TOOL_NAMES = {"continue", "no_reply", "wait"} HISTORY_SILENT_TOOL_NAMES = {"finish"} @@ -247,36 +248,69 @@ class MaisakaReasoningEngine: if self._runtime._force_next_timing_continue: return self._build_forced_continue_timing_result() - response = await self._run_timing_gate_sub_agent( - context_message_limit=TIMING_GATE_CONTEXT_LIMIT, - system_prompt=self._build_timing_gate_system_prompt(), - tool_definitions=get_timing_tools(), - ) tool_result_summaries: list[str] = [] tool_monitor_results: list[dict[str, Any]] = [] + response: Any = None selected_tool_call: Optional[ToolCall] = None - for tool_call in response.tool_calls: - if tool_call.func_name in TIMING_GATE_TOOL_NAMES: - selected_tool_call = tool_call + invalid_tool_text = "" + for attempt_index in range(TIMING_GATE_MAX_ATTEMPTS): + response = await self._run_timing_gate_sub_agent( + context_message_limit=TIMING_GATE_CONTEXT_LIMIT, + system_prompt=self._build_timing_gate_system_prompt(), + tool_definitions=get_timing_tools(), + ) + selected_tool_call = None + for tool_call in response.tool_calls: + if tool_call.func_name in TIMING_GATE_TOOL_NAMES: + selected_tool_call = tool_call + break + + if selected_tool_call is not None: break - if selected_tool_call is None: invalid_tool_names = [ str(tool_call.func_name).strip() for tool_call in response.tool_calls if str(tool_call.func_name).strip() ] invalid_tool_text = "、".join(invalid_tool_names) if invalid_tool_names else "无工具" - logger.warning( - f"{self._runtime.log_prefix} Timing Gate 未返回有效控制工具:{invalid_tool_text},将按 no_reply 处理" - ) self._append_timing_gate_invalid_tool_hint(invalid_tool_text) + remaining_attempts = TIMING_GATE_MAX_ATTEMPTS - attempt_index - 1 + if remaining_attempts > 0: + logger.warning( + f"{self._runtime.log_prefix} Timing Gate 未返回有效控制工具:{invalid_tool_text}," + f"将重试 ({attempt_index + 1}/{TIMING_GATE_MAX_ATTEMPTS})" + ) + tool_result_summaries.append( + f"- retry [非法 Timing 工具]: 返回了 {invalid_tool_text},将重试 " + f"({attempt_index + 1}/{TIMING_GATE_MAX_ATTEMPTS})" + ) + continue + + logger.warning( + f"{self._runtime.log_prefix} Timing Gate 连续 {TIMING_GATE_MAX_ATTEMPTS} 次未返回有效控制工具:" + f"{invalid_tool_text},将按 no_reply 处理" + ) self._runtime._enter_stop_state() tool_result_summaries.append( f"- no_reply [非法 Timing 工具]: 返回了 {invalid_tool_text},已停止本轮并等待新消息" ) return "no_reply", response, tool_result_summaries, tool_monitor_results + if selected_tool_call is None: + self._runtime._enter_stop_state() + tool_result_summaries.append( + "- no_reply [非法 Timing 工具]: 已停止本轮并等待新消息" + ) + return "no_reply", response, tool_result_summaries, tool_monitor_results + + if invalid_tool_text: + self._runtime._chat_history = [ + message + for message in self._runtime._chat_history + if message.source != TIMING_GATE_INVALID_TOOL_HINT_SOURCE + ] + append_history = False store_record = selected_tool_call.func_name != "continue" invocation, result, tool_spec = await self._invoke_tool_call(