Как я написал шахматы с LLM на Python без галлюцинаций нейросетей

Введение

Я работаю с LLM довольно давно и застал модели времен GPT-3.5, примерно в то же время мне нужно было сделать проект по учебе в этой области, тогда я выбрал именно тему шахмат, потому-что не видел конкретно таких решений раньше, конечно ИИ в онлайн шахматах и так был практически непобедим, но мысль сыграть конкретно с нейросетью уровня Chat GPT, мне показалась интересной. Основная проблема - заставить чат бот играть в игру и не делать ничего лишнего.

В этой статье я разберу архитектуру своего проекта: шахмат на Python, где в качестве соперника выступает LLM:

  1. Как объяснить текстовой нейросети, что происходит на доске 8х8.
  2. Как заставить ее делать валидные ходы и не ломать игру.
  3. Как собрать такую систему бесплатно, используя 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. Не знаю принято тут так писать или нет, но, Спасибо за внимание.

Начать дискуссию