From 146e6b3982226d5ca82dec12abb3c4f99f7e85fb Mon Sep 17 00:00:00 2001 From: kswdev0 Date: Thu, 26 Mar 2026 12:51:40 +0900 Subject: [PATCH] =?UTF-8?q?fix:=20AIPlanner=EC=97=90=EC=84=9C=20HTTP=20429?= =?UTF-8?q?=20=EC=98=A4=EB=A5=98=20=EC=B2=98=EB=A6=AC=20=EB=B0=8F=20?= =?UTF-8?q?=EC=9E=AC=EC=8B=9C=EB=8F=84=20=EB=A1=9C=EC=A7=81=20=EA=B0=9C?= =?UTF-8?q?=EC=84=A0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - GLM API에서 HTTP 429 (Rate limit) 오류 발생 시, 'Retry-After' 헤더를 확인하여 적절한 대기 시간을 설정하도록 로직 추가 - 재시도 시 기본 백오프 대신 'Retry-After' 값이 있을 경우 이를 우선 적용하여 대기 시간 조정 - README.md에 변경 사항 반영 --- README.md | 1 + ai_planner.py | 38 +++++++++++++++++++++++++++++++++++++- 2 files changed, 38 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index beb57c4..2c45c0e 100644 --- a/README.md +++ b/README.md @@ -147,6 +147,7 @@ planner.set_goal( - `ai_planner.py`는 GLM 응답이 잘리거나(중괄호/대괄호 불일치) 마크다운이 섞여도 JSON 파싱을 복구하도록 `{} / []` 균형 추적과 보정 로직을 사용합니다. 또한 최상위에서 JSON 배열(`[...]`)로 답하는 경우도 `actions`로 래핑해 처리합니다. (추가) `finish_reason=length` 등으로 `message.content`가 비거나(또는 JSON 형태가 아니면) `message.reasoning_content`를 우선 사용합니다. 그리고 응답 안에 여러 개의 `{...}`가 섞여 있어도 그중 `actions`를 포함한 계획 객체를 우선 선택합니다. 또한 JSON 파싱 실패가 감지되면 다음 재시도에는 `JSON-only repair` 프롬프트를 덧붙여 모델이 스키마를 다시 따르도록 유도합니다. - 재시도(attempt>0)에서는 `max_tokens`를 줄여(기본 `900`) 분석 텍스트가 길어져 JSON이 잘리는 패턴을 줄입니다. - `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)로 바꿀 수 있습니다. - `ai_planner.py`의 `_call_glm()`에서 GLM 지연 원인 분석을 위한 타이밍 로그가 출력됩니다. - `total`: 요청 시작~콘텐츠 반환까지 전체 소요 diff --git a/ai_planner.py b/ai_planner.py index fb09a4c..eb7d915 100644 --- a/ai_planner.py +++ b/ai_planner.py @@ -240,6 +240,12 @@ class AIPlanner: ) if _glm_debug_enabled(): print("[GLM][디버그] JSON-only repair 프롬프트 적용") + # 429 Rate limit이면 prompt 길이를 늘리면 안 되므로 repair_suffix를 끈다. + if isinstance(e, ConnectionError) and ( + "429" in detail or "rate limit" in detail.lower() or "Too Many Requests" in detail + ): + repair_suffix = "" + print( f"[경고] GLM 처리 실패 (시도 {attempt+1}/3): " f"{type(e).__name__} 재시도..." @@ -247,7 +253,21 @@ class AIPlanner: print(f" [GLM 원인] {detail}") if _glm_debug_enabled(): traceback.print_exc() - time.sleep(2 + attempt * 3) + sleep_s = 2 + attempt * 3 + if isinstance(e, ConnectionError) and ( + "429" in detail or "rate limit" in detail.lower() or "Too Many Requests" in detail + ): + # retry_after=...를 우선 사용하고, 없으면 기본 백오프를 적용한다. + m = re.search(r"retry_after=([0-9]+(?:\\.[0-9]+)?)s", detail) + ra = float(m.group(1)) if m else None + if ra is not None: + sleep_s = max(sleep_s, ra) + else: + # Rate limit은 보통 짧게 끝나지 않으므로 더 길게 기다린다. + sleep_s = max(sleep_s, 20 + attempt * 10) + if _glm_debug_enabled(): + print(f"[GLM][디버그] 429 백오프 대기: {sleep_s:.1f}s") + time.sleep(sleep_s) continue print(f"[오류] GLM 처리 3회 실패. 상태 요약 기반 휴리스틱 폴백 사용.") print(f" [GLM 원인] {detail}") @@ -417,8 +437,24 @@ class AIPlanner: body = e.read().decode("utf-8", errors="replace") except Exception: body = "" + + retry_after: float | None = None + try: + # urllib.error.HTTPError는 headers를 들고 있는 경우가 많다. + ra = None + try: + ra = getattr(e, "headers", None).get("Retry-After") + except Exception: + ra = None + if ra: + ra_s = str(ra).strip() + retry_after = float(ra_s) + except Exception: + retry_after = None + raise ConnectionError( f"GLM API HTTP {e.code}: {body[:1200]}" + + (f" | retry_after={retry_after:.1f}s" if retry_after is not None else "") ) from e @staticmethod