- BrainTools - https://www.braintools.ru -

LLM уверенно называет шахматные ходы, которых на доске нет. Как я проверяю каждый ее ответ кодом

Движок знает ответ, но говорит числами: -2.7 и стрелка. Модель говорит словами, но врет. Первый же комментарий, который я получил, звучал так: «сильнее было 14.Nf6, конь врывается в лагерь черных». Коня, который может пойти на f6, в той позиции на доске не было.

Сразу дисклеймер: это мой первый веб-проект. Я играю в шахматы блиц на 1300 и хотел инструмент, который объясняет мои ошибки [1] словами, а не оценками. Часть того, что ниже, фронтендеру покажется очевидной. Мне пришлось выяснять это руками.

Дальше три истории. Как я проверяю ходы из ответа модели кодом и почему промпт тут не работает. Почему анализ в итоге считается у пользователя в браузере. И как HTML-комментарий в <head> вешал главную страницу.

Модель не видит доску

Я перепробовал несколько моделей: сейчас в каскаде Yandex GPT, DeepSeek и локальная Ollama как запасной вариант. Все они делают одно и то же – называют ходы, которых в позиции нет.

Это не дефект конкретной модели. Она получает строку FEN и текст. «Nf6» для нее правдоподобная последовательность символов в шахматном контексте, а не утверждение о легальности. Просьба «проверяй легальность» в промпте частоту снижает, но не обнуляет: у меня оставалось несколько процентов ответов с несуществующими ходами.

Для обучающего продукта несколько процентов вранья это приговор. Новичок не может отличить правильный совет от выдуманного – в этом и смысл, что он новичок.

Ходы проверяет не модель, а chess.js

У шахмат есть то, чего нет у большинства задач с LLM: полная формальная модель предметной области. Легальность хода вычислима. Значит, текст модели можно валидировать так же, как валидируют форму на сервере.

Схема такая. Регуляркой вытаскиваем из ответа все токены, похожие на ходы в алгебраической нотации. Каждый прогоняем через chess.js в позиции до хода и после него. Плюс белый список: сыгранный ход, лучший ход движка и ходы из главного варианта легальны всегда. Хоть один токен не прошел – ответ бракуется, модель просят переписать, а если и со второго раза врет, включается запасной комментарий на правилах.

Самое интересное оказалось не в проверке, а в том, что считать ходом:

function moveTokens(text: string): string[] {
  const out: string[] = [];
  for (const m of text.matchAll(SAN_RE)) {
    const token = m[0];
    const isBareSquare = /^[a-h][1-8]$/.test(normalize(token));
    if (isBareSquare) {
      // «конь на c6» - это описание поля, а не ход пешкой.
      // Ходом считаем, только если перед полем стоит номер хода: «13.f4».
      const before = text.slice(Math.max(0, (m.index ?? 0) - 6), m.index ?? 0);
      if (!/d+.{1,3}s?$/.test(before)) continue;
    }
    out.push(token);
  }
  return out;
}

Условия про isBareSquare в первой версии не было. В результате фраза «конь стоит на c6» читалась как ход пешкой на c6, такой ход в позиции нелегален, весь ответ признавался галлюцинацией и вырезался. На проде это выглядело так: у половины ходов комментарий тренера просто отсутствовал. Валидатор работал слишком хорошо и убивал нормальные тексты.

Отсюда правило, которое стоило мне недели: валидатор для LLM это не «проверка на ошибки», это отдельный кусок логики со своими багами. Пропущенная галлюцинация портит один комментарий. Ложное срабатывание ломает все.

Модель придумывает не только ходы, но и людей

Те же грабли я разложил себе сам, и заметил не сразу.

Кроме разбора партий на сайте есть каталоги: дебюты, ловушки, тактические приемы, эндшпильные мотивы. Всего 161 запись, и черновики линий я генерировал моделью, а потом просматривал глазами. Глаз – плохой валидатор, когда линий больше сотни.

Когда я прогнал все каталоги через chess.js, вылезли ошибки трех разных сортов.

Невозможная механика: в линии стоял ход коня на поле, куда он с той клетки не ходит. Правильная позиция с ложной подписью: текст утверждал, что сторона выигрывает ладью, хотя на доске мат ферзем – то есть позиция и объяснение были из разных вселенных. И выдуманный первоисточник: к одному приему прилагалась партия классиков с подписью, фамилиями и ходом слоном, которого в той позиции быть не может. Модель сочинила не только ходы, но и партнера по партии.

Отдельным пунктом шел невалидный FEN, на котором страница просто падала.

Четыре битые записи на 161 – меньше трех процентов, и ровно поэтому глазами их не видно. Теперь после любой генерации данных запускается npm run validate-data: он проходит по всем каталогам, воспроизводит каждую линию с начальной позиции и падает на первом невозможном ходе. Тридцать строк, которые надо было написать до генератора, а не после.

Что можно поправить регуляркой, не надо просить у модели

Мелочь, но показательная. У текстов в продукте есть правила: обращение на «ты», без длинного тире, без буквы «ё». Правила лежали в промпте.

Модели нарушали их регулярно. Особенно Yandex GPT, у которого, видимо, в обучении [2] очень много книжной типографики: длинное тире он ставит почти в каждом предложении.

Решение заняло десять строк:

export function normalizeTone(text: string): string {
  return text
    .replace(/[—–−]/g, "-")
    .replace(/ё/g, "е")
    .replace(/Ё/g, "Е");
}

Через эту функцию проходит каждый ответ: свежий от модели, поднятый из кэша и запасной на правилах. Принцип тот же, что с ходами, только в мелком масштабе. Если требование формализуемо, его исполняет код. Промпт остается для того, что кодом не проверишь: интонации, глубины объяснения, выбора, о чем вообще говорить.

Почему анализ считается у пользователя, а не у меня

Начинал я с серверного движка, и это было разумно: одна версия Stockfish для всех, глубина под моим контролем, ничего не зависит от телефона пользователя. Локально заработало за вечер. На хостинге не заработало ни разу.

Сервер отвалился дважды, каждый раз по новой причине, и общее у них одно – ни одна не видна на локальной машине:

  1. Неверная посылка: бинарь из репозитория подойдет любой машине. Собран он был под arm64, а функции крутятся на x86-64.

  2. Неверная посылка: файл в репозитории попадет в бандл функции. Сборщик тянет то, что видит в импортах, а файл, который вы запускаете через spawn, он не видит.

Второй отказ заставил меня проверить не очередную сборку, а само требование. Серверный анализ нужен, когда считать надо чужое, много и одинаково. У меня не выполняется ни одно из трех условий: человек считает свою партию, один раз, и у него в кармане устройство, которое простаивает. Плюс у serverless-функции жесткий лимит на время ответа, а в партии под сотню позиций, и каждую надо посчитать дважды – быстрым проходом и потом глубже.

Перенос в браузер занял день. Stockfish собран в WASM, живет в воркере, запросы выстраиваются в очередь. Быстрый проход по всем ходам идет на глубине 12, кнопка «Углубить» считает на 18, на телефоне на 15.

Главное решение в этом переносе – взять однопоточную сборку. Многопоточная требует SharedArrayBuffer, а значит заголовков COOP/COEP на весь сайт, а они ломают встраивание стороннего контента. На глубине 18 разница в силе для разбора партии любителя не значит ничего, а возни с заголовками много.

Цену этого выбора я плачу в двух местах. Пользователь скачивает 1.7 МБ WASM при первом анализе. И на слабом телефоне разбор идет ощутимо медленнее, чем на ноутбуке, а на совсем старом не пойдет вовсе.

Отдельная мелочь, на которой я обжегся ровно один раз: движок дает оценку с точки зрения [3] того, кто ходит. Если класть это в график как есть, партия выглядит пилой. Знак надо приводить к одной стороне сразу на входе.

HTML-комментарий, который вешал главную страницу

Эта история не про ИИ, но я потратил на нее больше, чем на валидатор.

После деплоя главная страница на проде периодически зависала на состоянии загрузки. Локально все работало. В консоли минифицированные ошибки React #418, #423 и #329, то есть расхождение разметки при гидратации.

Расхождения не было. Точнее, оно было не в моем коде: хостинг при отдаче дописывал в <head> свой HTML-комментарий и пару тегов meta. React при гидратации видит в head узлы, которых нет в его дереве, считает разметку испорченной и перерисовывает страницу с нуля. Вместе с этим теряется состояние, и приложение застревает.

Обновление React до следующей мажорной версии, на которое я возлагал надежды, не помогло: комментарий ломал гидратацию и там. Помог инлайн-скрипт, который выполняется до гидратации и вычищает из head лишнее – комментарии, пустые текстовые узлы и конкретные метатеги хостинга:

(function () {
  try {
    var h = document.head, n = h.firstChild, x;
    while (n) {
      x = n.nextSibling;
      if (
        n.nodeType === 8 ||                                  // комментарий
        (n.nodeType === 3 && !/S/.test(n.nodeValue)) ||     // пустой текст
        (n.nodeName === 'META' &&
          /^(hosting-provider|netlify-deploy)$/.test(n.getAttribute('name') || ''))
      ) h.removeChild(n);
      n = x;
    }
  } catch (e) {}
})();

Next и React сами по себе ни комментариев, ни пробельных узлов в head не кладут, так что чистка безопасная. Если у вас статика на хостинге, который дописывает что-то в ответ, и React жалуется на гидратацию без видимой причины, смотрите не свой код, а то, что реально пришло по сети.

Публичный эндпоинт к платной модели живет до первого любопытного

Ровно до первого человека с curl. Поэтому на входе лимит по IP в скользящем окне и общий дневной бюджет обращений к ИИ (по умолчанию 2000 в сутки), который лежит в key-value хранилище хостинга. Бюджет кончился – отдаем комментарий на правилах, сайт работает, счет не растет.

Перед этим стоит кэш по паре «позиция до хода плюс сыгранный ход». Одни и те же зевки в дебюте повторяются у всех, и заметная часть запросов до модели не доходит.

Что я вынес

Формальная модель предметной области это не приятное дополнение, а то, что вообще делает LLM применимой там, где цена ошибки высокая. В шахматах такая модель есть, и любой ход из текста проверяется за микросекунды. Если у вас такая модель есть, странно ее не использовать.

Валидатор надо писать раньше генератора. И тестировать его на хороших текстах тоже, а не только на плохих: ложные срабатывания обошлись мне дороже пропусков.

Когда ломается только на проде, дешевле выписать разницу между своей машиной и продом, чем в третий раз перечитывать свой код.

Проект называется «Думай ходом», кто хочет затестить – живет на dumayhodom.ru.

И вопрос, на который у меня ответа нет. Шахматам повезло: тут есть чем проверить каждое слово модели. А как вы ловите галлюцинации там, где формальной модели предметной области нет и проверить утверждение кодом нельзя?

Автор: Gavrik_Art

Источник [4]


Сайт-источник BrainTools: https://www.braintools.ru

Путь до страницы источника: https://www.braintools.ru/article/36074

URLs in this post:

[1] ошибки: http://www.braintools.ru/article/4192

[2] обучении: http://www.braintools.ru/article/5125

[3] зрения: http://www.braintools.ru/article/6238

[4] Источник: https://habr.com/ru/articles/1086898/?utm_source=habrahabr&utm_medium=rss&utm_campaign=1086898

www.BrainTools.ru

Rambler's Top100