feat: implement action aliasing and parameter normalization in AIPlanner to enhance compatibility with LM Studio responses

This commit is contained in:
21in7
2026-03-27 20:19:53 +09:00
parent 66f3a327e8
commit d9801ee457
3 changed files with 47 additions and 1 deletions

View File

@@ -145,6 +145,7 @@ planner.set_goal(
- `ai_planner.py`의 Ollama/LM Studio 호출은 서버 스키마 차이를 자동으로 맞추기 위해 payload 후보를 순차 재시도합니다. 먼저 `messages` 기반(legacy chat)으로 시도하고, 400(`unrecognized_keys`/`input required`)이면 `input` 중심 최소 payload로 재시도합니다.
- LM Studio `input-only` 모드에서 `output[].type=reasoning` 형태로 응답이 오는 경우도 파서가 처리하며, 응답 텍스트에 JSON 앞뒤 설명이 섞이면 `{...}` 구간을 복구 추출해 파싱을 시도합니다(복구 불가 시 기존 휴리스틱 폴백).
- 파싱된 계획은 `actions` 스키마와 지원 action 화이트리스트를 통과한 경우에만 채택합니다. 유효 액션이 없거나 형식이 깨지면 LLM 출력을 버리고 상태 기반 휴리스틱 폴백으로 전환해 `알 수 없는 행동` 루프를 줄입니다.
- LM Studio가 표준과 다른 동의어를 반환할 때(예: `place_building`, `target_x/target_y`, `position_x/position_y`), planner가 표준 스키마(`place_entity`, `x/y`, `name`)로 자동 정규화한 뒤 검증합니다.
- `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` 프롬프트를 덧붙여 모델이 스키마를 다시 따르도록 유도합니다.

View File

@@ -37,6 +37,12 @@ ALLOWED_ACTIONS = {
"start_research",
"wait",
}
ACTION_ALIASES = {
"place_building": "place_entity",
"build_entity": "place_entity",
"mine": "mine_resource",
"research": "start_research",
}
SYSTEM_PROMPT = """당신은 팩토리오 게임을 순수하게 플레이하는 AI 에이전트입니다.
@@ -466,11 +472,15 @@ class AIPlanner:
if not isinstance(a, dict):
continue
act = a.get("action")
if not isinstance(act, str) or act not in ALLOWED_ACTIONS:
if not isinstance(act, str):
continue
act = ACTION_ALIASES.get(act, act)
if act not in ALLOWED_ACTIONS:
continue
params = a.get("params")
if not isinstance(params, dict):
params = {}
params = AIPlanner._normalize_params(act, params)
reason = a.get("reason")
if not isinstance(reason, str):
reason = ""
@@ -482,6 +492,23 @@ class AIPlanner:
"after_this": str(plan.get("after_this", "")),
}
@staticmethod
def _normalize_params(action: str, params: dict) -> dict:
p = dict(params)
if action == "move":
if "x" not in p and "target_x" in p:
p["x"] = p.get("target_x")
if "y" not in p and "target_y" in p:
p["y"] = p.get("target_y")
elif action == "place_entity":
if "name" not in p and "item" in p:
p["name"] = p.get("item")
if "x" not in p and "position_x" in p:
p["x"] = p.get("position_x")
if "y" not in p and "position_y" in p:
p["y"] = p.get("position_y")
return p
@staticmethod
def _ensure_move_before_build_actions(actions: list[dict]) -> list[dict]:
"""

View File

@@ -69,6 +69,24 @@
---
## 2026-03-27 LM Studio action/params 동의어 정규화 계획
### 문제 요약
- LM Studio가 JSON은 지키지만 프로젝트 스키마와 다른 action/params를 반환함.
- 예: `place_building` vs `place_entity`, `target_x`/`target_y`, `position_x`/`position_y`.
### 구현 계획
1. `ai_planner.py`에서 action alias를 표준 action으로 매핑한다.
2. params alias를 표준 키로 정규화한다.
3. 정규화 후 화이트리스트 검증을 적용해 실행 가능한 액션만 남긴다.
4. README에 동의어 자동 정규화 동작을 반영한다.
### 검증 계획
- `python -m py_compile ai_planner.py`
- 런타임에서 `move`/`place_entity`로 정상 변환되는지 확인
---
## 채굴 시 mining_state 반복 설정 제거 (우클릭 유지)
### 문제