LLM과 파이썬으로 체스 AI 만들기 — 비용 없고 환각 현상도 없음
개발자들은 게임 앱에 LLM을 통합해야 할 때가 많지만, 오픈아이의 API는 비용이 많이 듭니다. 저렴한 대안으로는 무료 오픈소스 모델을 제공하는 오픈라우터(OpenRouter)가 있습니다. 이 서비스는 :free 태그가 붙은 모델을 통해 라마 4(Llama 4)를 무료로 사용할 수 있으며, 메시지 양 제한이 있습니다.
클라이언트 초기화는 표준 openai 인터페이스를 사용합니다:
class LLMAI:
def __init__(self, model_name="meta-llama/llama-4-maverick-17b-128e-instruct:free"):
self.client = openai.OpenAI(
base_url="https://openrouter.ai/api/v1",
api_key=OPENROUTER_API_KEY,
)
self.model_name = model_name
self.move_count = 0
base_url를 설정하면 기존 코드와 호환됩니다. 각 수마다 모델에게 프롬프트를 전달해 계산 부담을 최소화합니다.
보드 상태를 프롬프트에 전달하는 방법
LLM은 시각적 보드를 처리하지 못하므로, 텍스트 기반으로 상황을 구성해야 합니다. 표준 FEN과 PGN 형식에 더해 python-chess에서 추출한 유효한 수 목록을 추가해 환각 현상을 방지합니다.
상황 구성 코드:
# 현재 위치를 FEN 형식으로 가져오기
fen = board.fen()
# 수의 이력을 PGN 형식으로 가져오기
pgn_moves = []
temp_board = chess.Board()
# 수의 이력
for move in board.move_stack:
pgn_moves.append(temp_board.san(move))
temp_board.push(move)
pgn_history = " ".join(pgn_moves) if pgn_moves else "시작 위치"
# 유효한 수 목록 가져오기
legal_moves = [move.uci() for move in board.legal_moves]
legal_moves_str = ", ".join(legal_moves)
시스템 프롬프트는 UCI 표기법(예: e2e4)으로 응답을 제한합니다:
prompt = f"""당신은 {'백' if board.turn == chess.WHITE else '흑'} 쪽으로 체스를 두고 있습니다.
현재 위치 (FEN): {fen}
수 이력: {pgn_history}
수 번호: {self.move_count}
사용 가능한 수 (UCI 형식): {legal_moves_str}
가능한 선택지 중 가장 좋은 수를 고르고, 단지 UCI 수 코드만 반환하세요 (예: e2e4, g1f3, e7e8q).
설명, 분석, 추가 텍스트는 포함하지 마세요.
응답에는 반드시 UCI 수 코드만 포함되어야 합니다."""
FEN(현재 상태), PGN(이력), UCI 목록(제약 조건)을 결합하면 기본 프롬프트보다 정확도가 80~90% 증가합니다.
수 검증 및 백업 메커니즘
LLM은 지시사항을 무시하고 텍스트나 불법 수를 반환할 수 있습니다. 해결책은 바로 'AI 심판' 패턴입니다. python-chess가 UCI 수를 보드에 적용하기 전에 유효성 검사를 수행합니다.
UI 루프 로직:
if move_uci:
try:
move = chess.Move.from_uci(move_uci)
if move in self.board.legal_moves:
# 수 실행 전 애니메이션
self.animate_move(move)
self.board.push(move)
self.move_history.append(move.uci())
self.last_ai_response = f"LLM이 수를 두었습니다: {move.uci()}"
print(self.last_ai_response)
else:
# 백업: 무작위 수 선택
move = random.choice(list(self.board.legal_moves))
self.animate_move(move)
self.board.push(move)
self.move_history.append(move.uci())
self.last_ai_response = f"LLM 실패, 무작위 수: {move.uci()}"
print(self.last_ai_response)
except ValueError:
# 백업: 무작위 수 선택
move = random.choice(list(self.board.legal_moves))
예시 로그:
- 요청 #1: LLM이 'e7e5' 응답 → 유효한 수.
- 요청 #2: LLM이 '좋아요, 제가 생각하기엔 가장 좋은 수는 d7d5예요' → 백업으로 a7a6 선택.
이 방식은 충돌 없이 원활한 게임 플레이를 보장합니다.
핵심 아키텍처 구성 요소
이 프로젝트는 다음과 같은 요소에 의존합니다:
- python-chess: 보드 관리, 수 검증, FEN/PGN/UCI 처리.
- pygame: 보드 렌더링, 수 애니메이션.
- openai-client + OpenRouter: LLM 추론 호출.
- 백업 로직:
random.choice를 활용한 내구성 확보.
모듈러 설계: LLMAI 클래스는 UI와 분리되어 있으며, 유효한 수만 전달합니다. 체스 외에도 바둑이나 체스와 유사한 규칙 기반 게임으로 쉽게 확장 가능합니다.
| 구성 요소 | 역할 | 라이브러리 |
|-----------|------|---------|
| 보드 | 상태 및 검증 | python-chess |
| LLM | 수 선택 | OpenRouter Llama 4 |
| UI | 렌더링 및 입력 | pygame |
| 심판 | 검증 | try/except |
무엇이 가장 중요한가?
- 오픈라우터는 프로토타이핑용으로 강력한 LLM에 무료로 접근할 수 있게 하며, 요청량 제한이 있습니다.
- FEN, PGN, UCI 목록을 활용한 엄격한 프롬프트는 환각 현상을 10~20%로 줄입니다.
- 'AI 심판' 패턴과 백업 메커니즘은 안정성을 보장합니다: LLM은 제안하고, 라이브러리는 검증하고 적용합니다.
python-chess는 파이썬에서 체스 논리를 다루는 사실상의 표준이며, 캐슬링, 엔파산 등 모든 규칙을 커버합니다.- 이 프로젝트는 비결정론적 모델을 결정론적 애플리케이션에 통합하는 방법을 보여줍니다.
— Editorial Team
아직 댓글이 없습니다.