From c30fb426a8f7ec372d8c9cf1bb90dfc158e8828b Mon Sep 17 00:00:00 2001 From: kswdev0 Date: Thu, 26 Mar 2026 11:58:08 +0900 Subject: [PATCH] =?UTF-8?q?fix:=20AIPlanner=20JSON=20=EC=B6=9C=EB=A0=A5=20?= =?UTF-8?q?=EC=9A=94=EA=B5=AC=EC=82=AC=ED=95=AD=20=EB=B0=8F=20max=5Ftokens?= =?UTF-8?q?=20=EC=A1=B0=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - JSON 출력 요구사항을 명확히 하여 응답의 첫 비공백 문자가 반드시 `{` 이어야 함을 추가 - 재시도 시 max_tokens를 줄여 JSON 잘림 문제를 완화하는 로직 추가 - README.md에 변경 사항 반영 --- README.md | 1 + ai_planner.py | 14 ++++++++++---- 2 files changed, 11 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index ea1e744..beb57c4 100644 --- a/README.md +++ b/README.md @@ -145,6 +145,7 @@ planner.set_goal( - `ai_planner.py`는 LLM이 move 순서를 놓쳐 `place_entity`/`insert_to_entity`/`set_recipe`가 건설 거리 제한으로 실패하는 경우를 줄이기 위해, 해당 액션 직전에 최근 `move`가 같은 좌표가 아니면 자동으로 `move`를 끼워 넣습니다 - `agent_log.jsonl`에 모든 행동과 타임스탬프가 기록됩니다 - `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 읽기 제한 시간은 기본 120초이며, `GLM_HTTP_TIMEOUT_SECONDS`로 조정할 수 있습니다. 광맥은 플레이어와 200타일 이상 떨어진 경우에만 폴백에서 `move`를 끼우며, 임계값은 `GLM_FALLBACK_MOVE_THRESHOLD`(기본 200)로 바꿀 수 있습니다. - `ai_planner.py`의 `_call_glm()`에서 GLM 지연 원인 분석을 위한 타이밍 로그가 출력됩니다. diff --git a/ai_planner.py b/ai_planner.py index 21b5a51..fb09a4c 100644 --- a/ai_planner.py +++ b/ai_planner.py @@ -116,7 +116,7 @@ state_reader가 상태 요약에 포함하는 `마지막 행동(기억)` 섹션 ### 대기 - "wait" → {"seconds": int} -## 절대 중요: 순수 JSON만 출력하세요. ```json 같은 마크다운 블록, 설명 텍스트, 주석 없이 오직 { } 만.""" +## 절대 중요: 순수 JSON만 출력하세요. ```json 같은 마크다운 블록, 설명 텍스트, 주석 없이 오직 { } 만. 또한 응답의 첫 비공백 문자는 반드시 `{` 입니다.""" def _glm_debug_enabled() -> bool: @@ -197,7 +197,7 @@ class AIPlanner: "현재 상태를 분석하고, 장기 목표를 향해 지금 해야 할 행동 시퀀스를 계획하세요.\n" "⚠️ 순수 플레이입니다. 건설/채굴/삽입 전에 반드시 move로 가까이 이동하세요.\n" "⚠️ 제작은 재료가 있어야 합니다. 인벤토리를 확인하세요.\n" - "반드시 JSON만 반환하세요. 마크다운 블록(```)이나 설명 텍스트 없이 순수 JSON만." + "반드시 JSON만 반환하세요. 마크다운 블록(```)이나 설명 텍스트 없이 순수 JSON만. 그리고 응답의 첫 비공백 문자는 반드시 `{` 입니다." ) print(f"\n[GLM] 생각 중...") @@ -229,6 +229,7 @@ class AIPlanner: "\n\n[중요] 이전 응답은 JSON 요구사항을 위반했습니다.\n" "지금은 아래 스키마의 JSON 객체만 '그대로' 반환하세요.\n" "마크다운/설명 금지.\n" + "응답의 첫 비공백 문자는 반드시 `{` 입니다.\n" "키 이름과 구문은 동일해야 합니다.\n" "{" "\"thinking\":\"\"," @@ -332,6 +333,12 @@ class AIPlanner: return "\n".join(lines) + "\n\n" def _call_glm(self, user_message: str, attempt: int) -> str: + # attempt가 올라갈수록 "분석 텍스트"가 길어지면서 JSON이 잘리는 패턴이 있어 + # retry에서는 출력 길이를 줄인다. + base_max_tokens = int(os.environ.get("GLM_MAX_TOKENS", "2000")) + retry_max_tokens = int(os.environ.get("GLM_RETRY_MAX_TOKENS", "900")) + max_tokens = base_max_tokens if attempt == 0 else min(base_max_tokens, retry_max_tokens) + payload = json.dumps({ "model": GLM_MODEL, "messages": [ @@ -339,12 +346,11 @@ class AIPlanner: {"role": "user", "content": user_message}, ], "temperature": 0.3, - "max_tokens": 2000, + "max_tokens": max_tokens, }).encode("utf-8") prompt_chars = len(user_message) system_chars = len(SYSTEM_PROMPT) - max_tokens = 2000 http_timeout = float(os.environ.get("GLM_HTTP_TIMEOUT_SECONDS", "120")) if _glm_debug_enabled():