feat: AIPlanner에서 건설 전 이동 보장 로직 추가
- `_ensure_move_before_build_actions` 메서드를 추가하여, LLM이 "move" 후 "place_entity"/"insert_to_entity"/"set_recipe" 순서를 놓치는 경우를 방지 - 최근 이동 좌표와 비교하여 필요 시 자동으로 이동 액션을 삽입하는 로직 구현 - 관련 단위 테스트 추가 및 README.md에 변경 사항 반영
This commit is contained in:
@@ -142,6 +142,7 @@ planner.set_goal(
|
||||
|
||||
- 순수 플레이이므로 **걷기, 채굴, 제작에 실제 시간이 소요**됩니다
|
||||
- AI가 "move 먼저 → 작업" 패턴을 학습하도록 프롬프트가 설계되어 있습니다
|
||||
- `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` 프롬프트를 덧붙여 모델이 스키마를 다시 따르도록 유도합니다.
|
||||
- `ai_planner.py`의 `AIPlanner.decide()`는 `TimeoutError`/`ConnectionError`/`urllib.error.URLError` 같은 GLM HTTP 지연/연결 오류도 3회 재시도한 뒤, **상태 요약에 나온 광맥(앵커) 좌표가 있으면 `mine_resource`(먼 경우 `move` 후 채굴)로 폴백**하고, 광맥 정보가 없을 때만 `explore` 방향을 순환하며 탐색합니다(동일 방향 탐색 루프 완화).
|
||||
|
||||
@@ -265,9 +265,49 @@ class AIPlanner:
|
||||
print(f"[AI] 완료 후: {plan.get('after_this', '')}")
|
||||
|
||||
actions = plan.get("actions", [])
|
||||
actions = self._ensure_move_before_build_actions(actions)
|
||||
print(f"[AI] {len(actions)}개 행동 계획됨")
|
||||
return actions
|
||||
|
||||
@staticmethod
|
||||
def _ensure_move_before_build_actions(actions: list[dict]) -> list[dict]:
|
||||
"""
|
||||
LLM이 "move 후 배치" 순서를 놓치는 경우가 있어,
|
||||
place_entity/insert_to_entity/set_recipe 앞에 최근 move가 같은 좌표가 아니면
|
||||
안전장치로 move를 끼워 넣는다.
|
||||
"""
|
||||
out: list[dict] = []
|
||||
last_move_xy: tuple[object, object] | None = None
|
||||
|
||||
for a in actions:
|
||||
if not isinstance(a, dict):
|
||||
continue
|
||||
act = a.get("action")
|
||||
params = a.get("params") if isinstance(a.get("params"), dict) else {}
|
||||
|
||||
if act == "move":
|
||||
mx = params.get("x")
|
||||
my = params.get("y")
|
||||
last_move_xy = (mx, my)
|
||||
out.append(a)
|
||||
continue
|
||||
|
||||
if act in ("place_entity", "insert_to_entity", "set_recipe"):
|
||||
tx = params.get("x")
|
||||
ty = params.get("y")
|
||||
if tx is not None and ty is not None:
|
||||
if last_move_xy != (tx, ty):
|
||||
out.append({
|
||||
"action": "move",
|
||||
"params": {"x": tx, "y": ty},
|
||||
"reason": "안전장치: 건설/삽입 거리 제한 때문에 move 먼저",
|
||||
})
|
||||
last_move_xy = (tx, ty)
|
||||
|
||||
out.append(a)
|
||||
|
||||
return out
|
||||
|
||||
def record_feedback(self, action: dict, success: bool, message: str = ""):
|
||||
self.feedback_log.append({
|
||||
"action": action.get("action", ""),
|
||||
|
||||
30
tests/test_ai_planner_action_enforcement.py
Normal file
30
tests/test_ai_planner_action_enforcement.py
Normal file
@@ -0,0 +1,30 @@
|
||||
import os
|
||||
import unittest
|
||||
|
||||
from ai_planner import AIPlanner
|
||||
|
||||
|
||||
class TestAIPlannerActionEnforcement(unittest.TestCase):
|
||||
def setUp(self):
|
||||
os.environ.setdefault("ZAI_API_KEY", "dummy")
|
||||
|
||||
def test_insert_move_before_place_entity_if_last_move_differs(self):
|
||||
actions = [
|
||||
{"action": "craft_item", "params": {"item": "coal", "count": 1}},
|
||||
{"action": "place_entity", "params": {"name": "stone-furnace", "x": 1, "y": 2, "direction": "north"}},
|
||||
]
|
||||
out = AIPlanner._ensure_move_before_build_actions(actions)
|
||||
self.assertEqual(out[0]["action"], "craft_item")
|
||||
self.assertEqual(out[1]["action"], "move")
|
||||
self.assertEqual(out[1]["params"]["x"], 1)
|
||||
self.assertEqual(out[1]["params"]["y"], 2)
|
||||
self.assertEqual(out[2]["action"], "place_entity")
|
||||
|
||||
def test_do_not_insert_move_if_last_move_same_coords(self):
|
||||
actions = [
|
||||
{"action": "move", "params": {"x": 1, "y": 2}, "reason": "test"},
|
||||
{"action": "place_entity", "params": {"name": "stone-furnace", "x": 1, "y": 2, "direction": "north"}},
|
||||
]
|
||||
out = AIPlanner._ensure_move_before_build_actions(actions)
|
||||
self.assertEqual([a["action"] for a in out], ["move", "place_entity"])
|
||||
|
||||
Reference in New Issue
Block a user