diff --git a/README.md b/README.md index 5919565..6a3a0bb 100644 --- a/README.md +++ b/README.md @@ -148,6 +148,7 @@ planner.set_goal( - 재시도(attempt>0)에서는 `max_tokens`를 줄여(기본 `900`) 분석 텍스트가 길어져 JSON이 잘리는 패턴을 줄입니다. - `JSON-only repair` 프롬프트가 붙는 경우에는 `max_tokens`를 더 낮추고(기본 `250`) `temperature`도 더 낮춰(기본 `0.0`) JSON 포맷 준수율을 올리려 요청합니다. - `JSON-only repair`를 한 번 적용했는데도 비JSON 텍스트가 계속 오면, 불필요한 API 재시도 반복 대신 즉시 상태 기반 휴리스틱 폴백으로 전환합니다. 또한 LLM이 `actions=[]`를 반환하면 같은 이유로 휴리스틱 플랜으로 즉시 대체합니다. +- `finish_reason=length`이면서 응답이 비JSON 텍스트(`{' 없음`)로 시작하면, repair 재시도를 기다리지 않고 즉시 휴리스틱 폴백으로 전환해 턴 지연을 줄입니다. - `ai_planner.py`의 `AIPlanner.decide()`는 `TimeoutError`/`ConnectionError`/`urllib.error.URLError` 같은 GLM HTTP 지연/연결 오류도 3회 재시도한 뒤, **상태 요약에 나온 광맥(앵커) 좌표가 있으면 `mine_resource`(먼 경우 `move` 후 채굴)로 폴백**하고, 광맥 정보가 없을 때만 `explore` 방향을 순환하며 탐색합니다(동일 방향 탐색 루프 완화). - GLM이 `HTTP 429 (Rate limit)`에 걸리면 `Retry-After`가 있으면 그만큼 기다렸다가(없으면 기본 백오프) 재시도하도록 처리합니다. - GLM HTTP 읽기 제한 시간은 기본 120초이며, `GLM_HTTP_TIMEOUT_SECONDS`로 조정할 수 있습니다. 광맥은 플레이어와 200타일 이상 떨어진 경우에만 폴백에서 `move`를 끼우며, 임계값은 `GLM_FALLBACK_MOVE_THRESHOLD`(기본 200)로 바꿀 수 있습니다. diff --git a/ai_planner.py b/ai_planner.py index 91e0e75..0bb4cb1 100644 --- a/ai_planner.py +++ b/ai_planner.py @@ -177,6 +177,7 @@ class AIPlanner: self.step = 0 self.feedback_log: list[dict] = [] + self._last_glm_finish_reason: str | None = None # GLM 전부 실패 시 explore 방향 순환 (동일 방향 탐색 루프 완화) self._fallback_explore_turn = 0 self.long_term_goal = ( @@ -222,6 +223,17 @@ class AIPlanner: detail = describe_glm_exception(e) parse_no_brace = isinstance(e, ValueError) and "JSON 파싱 실패 ('{' 없음)" in str(e) + # 첫 시도에서 이미 finish_reason=length + 비JSON 텍스트면, + # repair 재시도도 같은 패턴으로 반복되는 경우가 많아 즉시 폴백한다. + if parse_no_brace and self._last_glm_finish_reason == "length": + if _glm_debug_enabled(): + print("[GLM][디버그] finish_reason=length + 비JSON -> 즉시 휴리스틱 폴백") + plan = self._fallback_plan_from_summary( + state_summary, + last_error=detail, + ) + break + # JSON-only repair를 이미 한 번 적용했는데도 여전히 비JSON 텍스트만 오는 경우: # 추가 API 호출을 반복하지 말고 즉시 상태 기반 휴리스틱으로 진행한다. if parse_no_brace and repair_applied_once: @@ -437,6 +449,8 @@ class AIPlanner: if _glm_debug_enabled(): finish_reason = data.get("choices", [{}])[0].get("finish_reason") print(f"[GLM][디버그] finish_reason={finish_reason!r}") + finish_reason = data.get("choices", [{}])[0].get("finish_reason") + self._last_glm_finish_reason = str(finish_reason) if finish_reason is not None else None content = self._extract_glm_assistant_text(data).strip() if not content and _glm_debug_enabled(): # content가 비어있으면 아래 파서에서 원인 추적이 어려워지므로 raw 일부를 남긴다. @@ -466,6 +480,7 @@ class AIPlanner: ) return content except urllib.error.HTTPError as e: + self._last_glm_finish_reason = None body = "" try: body = e.read().decode("utf-8", errors="replace")