Введение
Я работаю с LLM довольно давно и застал модели времен GPT-3.5, примерно в то же время мне нужно было сделать проект по учебе в этой области, тогда я выбрал именно тему шахмат, потому-что не видел конкретно таких решений раньше, конечно ИИ в онлайн шахматах и так был практически непобедим, но мысль сыграть конкретно с нейросетью уровня Chat GPT, мне показалась интересной. Основная проблема - заставить чат бот играть в игру и не делать ничего лишнего.
В этой статье я разберу архитектуру своего проекта: шахмат на Python, где в качестве соперника выступает LLM:
Как объяснить текстовой нейросети, что происходит на доске 8х8.
Как заставить ее делать валидные ходы и не ломать игру.
Как собрать такую систему бесплатно, используя OpenRouter.
Проблема первая: бюджет
Обычно для таких экспериментов все берут OpenAI, но гонять запросы на каждый шахматный ход в их моделях для такого проекта - это сложно и дорого. Моей целью было сделать проект с нулевым бюджетом.
Поэтому я выбрал OpenRouter. Это агрегатор нейросетей, у которого есть доступ к ряду мощных опенсорсных моделей абсолютно бесплатно (хоть и с ограничением по количеству запросов) - у них есть тег :free.
Вот как выглядит инициализация клиента в моем проекте:
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
Интерфейс библиотеки openai позволяет легко переопределить base_url, поэтому мы бесшовно подключаемся к бесплатной Llama 4 через OpenRouter, не меняя привычный код.
Проблема вторая: LLM не видит доску. Как передать контекст?
Если просто написать промпт «Ты играешь черными, твой ход такой то / такой то», модель огорчится вами и сойдет с ума. Ей нужен контекст. В шахматах есть два стандарта записи:
FEN (Forsyth-Edwards Notation) — слепок текущего состояния доски.
PGN (Portable Game Notation) — история ходов.
Чтобы минимизировать галлюцинации, я передаю в модель и 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)
Собрав эти данные, формируется жесткий системный промпт. Моя задача была отучить модель болтать и заставить её вернуть ровно 4 символа (например, 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 код хода."""
Третья проблема: Паттерн «AI-Рефери» и Fallback
Даже с указанием «выбери из списка легальных ходов», Llama (или любая другая модель) может выдать галлюцинацию, предложить невозможный ход или вернуть текст с рассуждениями. Если передать это напрямую в графический движок (pygame), приложение упадет с ошибкой.
Нужен слой валидации. В моем коде за это отвечает блок try/except внутри логики UI. Если LLM возвращает недопустимый ход (не проходит проверку python-chess), срабатывает Fallback-механизм - скрипт делает случайный валидный ход, чтобы игра не прерывалась.
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: # Fallback к случайному ходу 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: # Fallback к случайному ходу move = random.choice(list(self.board.legal_moves))
В консоли этот процесс выглядит так:
Запрос к LLM для хода #1... LLM ответил: 'e7e5' LLM сделал ход: e7e5 Запрос к LLM для хода #2... LLM ответил: 'Хорошо, я думаю лучший ход это d7d5' LLM дал некорректный ответ, случайный ход: a7a6
Архитектура проекта

Выводы
Бесплатный ИИ существует (хоть и с условностями): OpenRouter и опенсорсные модели отлично справляются с подобными проектами, позволяя не тратить деньги на API.
Контекст решает всё: Чем больше жестких рамок вы зададите в промпте (FEN + PGN + список легальных ходов), тем адекватнее будет ответ.
Защищайте свой код: Паттерн «Рефери», когда классический детерминированный код (в данном случае
python-chess) стоит между LLM и состоянием приложения, - это база для построения надежных AI-систем.
P.S. Не знаю принято тут так писать или нет, но, Спасибо за внимание.
