화면 전체를 읽고, 블록의 최종 위치를 결정하는 언어모델 기반 게임 에이전트
책 안에서 이 장이 맡는 역할
앞 장에서는 DQN으로 플레이 정책을 학습했다. 이 장에서는 LLM/VLM이 보드 상태를 읽고 블록 배치를 선택하는 구조를 살펴본다. 현재 블록, 다음 블록, 구멍, 높이와 표면의 굴곡을 함께 판단하되, 모델의 응답은 코드에서 검증한다.
학습 목표
- 테트리스 보드를 LLM이 읽을 수 있는 표현으로 바꾸는 방법을 이해한다.
- VLM에게 보드 이미지를 보여 주고 target_rot, target_x를 JSON으로 받는 구조를 만든다.
- 텍스트 LLM에게는 전체 화면을 ASCII 보드와 후보 목록으로 제공해 환각을 줄인다.
- LLM의 자유 답변을 그대로 믿지 않고, 후보 생성과 검증을 결합해 안정적인 게임 조작으로 바꾼다.
- LLM 판단을 실제 Qt 테트리스 화면에 연결해 플레이 가능한 에이전트로 만든다.
문제 정의: 블록을 어디에 놓을 것인가
테트리스에서 모델이 매 순간 해야 할 결정은 매우 구체적이다. 현재 떨어지는 블록을 몇 번 회전하고, 어느 열에 놓을 것인가. 이 장에서는 그 결정을 두 값으로 표현한다.
{"target_rot": 1, "target_x": 4}target_rot은 시계 방향 회전 횟수이고, target_x는 블록을 놓을 목표 열이다. LLM은 이 두 값을 결정하고, 프로그램은 그 값을 실제 키 입력으로 번역한다. 이렇게 하면 LLM이 매 프레임 왼쪽, 오른쪽, 회전을 직접 누르는 부담이 줄어든다.
전체 설계
Qt Tetris screen/state
-> grid extraction
-> image or ASCII board
-> LLM/VLM decision
-> target_rot, target_x
-> micro action translator
-> Left/Right/Rotate/Drop command| 파일 | 이 장에서의 역할 |
|---|---|
| vlm_macro_client.py | 보드 이미지를 VLM에게 보여 주고 목표 회전과 목표 열을 직접 받는 가장 직관적인 구현 |
| llm_macro_client.py | 후보 배치를 계산한 뒤 LLM에게 Top-K 중 하나를 고르게 하는 안정형 구현 |
| vlm_teacher.py | 관측 그리드를 컬러 이미지로 렌더링하고 VLM이 다음 행동과 점수를 반환하게 하는 교사 모듈 |
| llm_teacher.py | 관측 그리드를 ASCII로 바꾸어 텍스트 LLM이 판단할 수 있게 하는 교사 모듈 |
| tetris/src/tetris_engine.* | 규칙과 물리, 라인 삭제, 보드 상태를 제공하는 오래된 문제의 시뮬레이터 |
| tetris/src/tetris_c_api.cpp | C++ 엔진을 Python에서 사용할 수 있게 연결하는 브릿지 |
1단계: 화면을 모델이 볼 수 있는 데이터로 바꾸기
LLM이 게임을 풀려면 먼저 화면을 읽어야 한다. 이 프로젝트에는 두 가지 표현 방식이 있다. 하나는 VLM용 이미지이고, 다른 하나는 텍스트 LLM용 ASCII 보드다.
이미지 표현
vlm_teacher.py와 vlm_macro_client.py는 보드의 숫자 그리드를 색상 이미지로 바꾼다. 빈 칸은 어두운 색, I/O/T/S/Z/J/L 블록은 서로 다른 색으로 칠한다. 그 다음 각 칸을 cell_px 크기로 확대해 VLM이 보기 쉬운 이미지로 만든다.
rgb = palette[g]
rgb = np.repeat(np.repeat(rgb, cell_px, axis=0), cell_px, axis=1)
image = Image.fromarray(rgb, mode="RGB")텍스트 표현
텍스트 LLM은 이미지를 직접 보지 못하므로 보드를 문자열로 바꾼다. 빈 칸은 '.', 채워진 칸은 '#', 또는 조각 번호로 표시한다. 행 번호를 붙이면 모델이 위아래 위치를 더 안정적으로 읽을 수 있다.
00|..........|
01|..........|
18|..###.....|
19|.#####....|2단계: VLM에게 렌더링한 보드 이미지를 보여 주기
가장 책 제목에 잘 맞는 방식은 VLM 방식이다. 모델에게 현재 보드 이미지를 보여 주고, 지금 블록의 최종 배치를 JSON으로 답하게 한다. 이 접근은 사람이 게임 화면을 보고 판단하는 방식과 가장 비슷하다.
prompt = (
"You are an expert Tetris AI.\n"
"Look at the current board state and decide the best final resting position.\n"
"Return JSON: {\"target_rot\": 0..3, \"target_x\": 0..9}."
)vlm_macro_client.py는 새 블록이 나타날 때마다 VLM을 호출한다. 응답에서 target_rot과 target_x를 뽑고, 실제 조작은 별도의 변환 함수가 맡는다. LLM이 '왼쪽으로 세 번 가고 회전해'처럼 절차를 말하게 하지 않고, 최종 목표만 말하게 하는 것이 핵심이다.
3단계: 목표 위치를 실제 조작으로 번역하기
게임 엔진은 여전히 한 번에 하나의 행동만 받는다. 그래서 LLM의 목표 배치를 실제 키 입력으로 바꾸는 작은 컨트롤러가 필요하다.
if cur_rot != target_rot:
return RotateCW or RotateCCW
if cur_x < target_x:
return Right
if cur_x > target_x:
return Left
return HardDrop이 번역기 덕분에 LLM은 고수준 판단만 담당하고, 세부 입력은 프로그램이 처리한다. 이것이 LLM 게임 에이전트를 만들 때 매우 중요한 패턴이다. 모델에게 모든 것을 시키는 대신, 모델이 잘하는 판단과 코드가 잘하는 실행을 나눈다.
4단계: 텍스트 LLM에는 후보를 고르게 하기
VLM 방식은 직관적이지만, 모델이 잘못된 좌표를 말하거나 보드를 착각할 수 있다. llm_macro_client.py는 더 안전한 방식을 사용한다. 프로그램이 가능한 모든 배치를 먼저 계산하고, 각 후보의 점수를 만든 다음, LLM에게 Top-K 후보 중 하나만 고르게 한다.
| 후보 지표 | 의미 |
|---|---|
| cleared | 이 배치로 지워지는 줄 수 |
| new_holes | 새로 생기는 구멍 수 |
| on_hole | 이미 구멍이 있는 열 위에 쌓는 블록 수 |
| bumpiness | 표면이 얼마나 울퉁불퉁한지 |
| max_h | 가장 높은 열의 높이 |
| landing_height | 블록이 얼마나 높은 위치에 놓이는지 |
| look | 현재 후보 점수에 다음 블록의 최선 점수를 더한 1수 앞보기 점수 |
Pre-evaluated candidate placements:
[0] rot=0 x=4 cleared=1 new_holes=0 bump=8 max_h=6 look=+2.40
[1] rot=1 x=7 cleared=0 new_holes=1 bump=12 max_h=8 look=-4.10
Reply EXACTLY as: {"choice": 0}이 방식은 LLM의 창의성을 줄이는 대신 안정성을 얻는다. 모델은 좌표를 새로 발명하지 않고, 이미 검증된 후보 중 하나를 고른다. 이 패턴은 LLM을 의사결정자로, 코드를 검산자로 사용하는 구조다.
5단계: 환각을 줄이는 프롬프트 설계
게임 에이전트에서 LLM 환각은 곧 잘못된 조작이다. 그래서 프롬프트는 자유로운 설명보다 제한된 출력 형식을 강하게 요구해야 한다.
- 응답은 JSON 하나만 받는다.
- 선택 가능한 행동 또는 후보 인덱스의 범위를 명시한다.
- 구멍 생성 금지, 높이 낮추기, 표면 평탄화처럼 판단 기준의 우선순위를 적는다.
- 모델이 직접 좌표를 만들게 하기보다 가능한 후보 목록을 주고 고르게 한다.
- JSON 파싱 실패 시 휴리스틱 1위 후보로 되돌아가는 fallback을 둔다.
6단계: 오래된 게임과 최신 LLM의 만남
테트리스는 1980년대 게임이지만, LLM에게는 작지 않은 추론 문제다. 화면에는 숫자 계산, 공간 인식, 단기 계획, 위험 회피가 동시에 들어 있다. 그래서 이 장은 단순한 게임 자동화가 아니라, LLM을 시각적 의사결정 엔진으로 사용하는 예제가 된다.
실습 순서
- Qt 테트리스 실행: 테트리스 GUI를 실행하고 RL/agent 포트를 연다.
- VLM 클라이언트 실행: vlm_macro_client.py를 실행해 화면 이미지를 VLM에게 전달한다.
- JSON 응답 확인: target_rot과 target_x가 정상적으로 파싱되는지 로그를 확인한다.
- 조작 번역 확인: 목표 회전과 목표 열이 Left/Right/Rotate/Drop 명령으로 바뀌는 과정을 관찰한다.
- 텍스트 LLM 방식 비교: llm_macro_client.py로 후보 기반 선택을 실행해 VLM 방식과 안정성을 비교한다.
- 프롬프트 개선: 구멍 금지, 낮은 높이 유지, 라인 삭제 우선순위를 바꿔 플레이 스타일 변화를 본다.
실행 예시
./tetris/build/tetris_rl --rl-port 5555
python vlm_macro_client.py \
--port 5555 \
--model-dir llms/Qwen2.5-VL-3B-Instruct
python llm_macro_client.py \
--port 5555 \
--model-path llms/qwen2.5-3b-instruct-q4_k_m.gguf \
--top-k 6챕터 마무리
이 장에서 우리는 테트리스를 강화학습 점수 게임이 아니라 LLM 의사결정 문제로 다시 보았다. 숫자 격자를 색상 이미지나 ASCII 보드로 표현하고, 모델에게 최종 배치를 묻고, 코드는 그 결정을 실제 조작으로 바꾸었다.
핵심은 LLM에게 모든 책임을 넘기지 않는 것이다. 화면 인식과 판단은 LLM이 맡고, 후보 생성, 좌표 검증, JSON 파싱, fallback, 키 입력 변환은 코드가 맡는다. 이 분업 구조가 오래된 문제를 최신 LLM으로 풀 때 가장 안정적인 기본형이다.
연습 문제
- VLM 프롬프트에서 'clear lines if possible'을 제거하면 플레이가 어떻게 달라질지 예상해 보자.
- target_x를 직접 말하게 하는 방식과 후보 index만 고르게 하는 방식의 장단점을 비교해 보자.
- ASCII 보드에 행 번호와 열 번호를 모두 추가하면 LLM의 선택 안정성이 좋아질지 실험해 보자.
- JSON 파싱 실패가 반복될 때 사용할 fallback 정책을 직접 설계해 보자.
댓글
0개아직 댓글이 없습니다.