Логотип ConstructiCat
CodeBust.
Навигация по разделу ▾

Галлюцинированный API.

Сгенерированный ИИ код, который вызывает функции, методы, параметры, ключи конфигурации или пакеты, выглядящие правдоподобно, но не существующие в той версии библиотеки, от которой вы зависите.

##Признаки и симптомы

Ревьюер замечает галлюцинированный API, когда код читается гладко и "выглядит как" идиоматичное использование библиотеки, однако конкретный вызов, опцию или импорт невозможно найти в реальном API этой библиотеки. Характерные признаки:

  • Метод или опция, имя которых слишком удобно — они делают ровно то, о чём просил запрос, с именем, смешивающим два реальных API (например, findLastWhere, includesAll, parseDateSafe).
  • Выдуманные параметры у реальной функции (опасный вариант — он часто компилируется и падает лишь во время выполнения или молча игнорирует опцию).
  • import пакета, которого нет в package.json / requirements.txt, или именованный импорт, который модуль никогда не экспортирует.
  • Формы API, смешивающие версии: сигнатура v2, вызванная на клиенте v5, или метод, удалённый/переименованный несколько релизов назад.
  • Уверенные встроенные комментарии, утверждающие, что вызов корректен ("// возвращает возмещённый платёж").
// Сгенерировано ИИ — гладко, правдоподобно и неверно
import { formatRelative } from 'date-fns';

// `roundingMethod` здесь выдуман — formatRelative не принимает такой опции (она есть у formatDistanceStrict), поэтому она молча игнорируется
const label = formatRelative(date, new Date(), { roundingMethod: 'floor' });

// `findLastWhere` не существует у Array — TypeError во время выполнения
const lastActive = items.findLastWhere(i => i.active);

// Выдуманный метод SDK: в stripe-node нет refunds.createForCharge — настоящий вызов: stripe.refunds.create({ charge, amount })
await stripe.refunds.createForCharge(chargeId, { amount: 500 });

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

##Причины проблемы

Почему модели это порождают

  • Правдоподобие следующего токена, а не поиск. LLM предсказывает статистически наиболее вероятное продолжение, что фактически является усреднением всех похожих API, которые она видела. Это усреднение часто оказывается методом, который должен бы существовать — модель выдаёт "удобное имя", а не настоящее. В одном исследовании рекомендаций API для Java 8 (JRE) от 58,1% до 84,1% API, предложенных тремя моделями, не существовали в указанной библиотеке, а большинство ошибок приходилось на несуществующие имена методов (arXiv 2409.15228).
  • Смешение версий. Обучающие корпуса смешивают множество версий библиотеки, поэтому модель сплавляет сигнатуры v2 и v5 в одну, которая не соответствует ни одной из них. Конфликты со знаниями об API — например, вызов с несуществующим параметром — задокументированная категория галлюцинаций на уровне репозитория (Zhang et al., ISSTA 2025), и авторы отмечают, что часть галлюцинаций может пройти и статические проверки, и все тесты.
  • Устаревание из-за порога обучения. Модель уверенно использует API, переименованные, объявленные устаревшими или удалённые после её порога обучения, и хватается за то, что чаще всего встречалось в обучающем корпусе, — а это может оказаться более старая версия библиотеки, чем установленная у вас.
  • Отсутствие контекста репозитория/зависимостей. Без вашего package.json или исходного кода реального модуля модель выдумывает вспомогательные функции, которые "на ощупь" кажутся частью вашего стека.
  • Подхалимство / стремление ответить. Ассистент почти никогда не говорит "я не уверен, что этот метод существует" — он выдаёт уверенный, выглядящий работоспособным код, что снижает настороженность ревьюера.
  • Детерминизм делает это эксплуатируемым. Галлюцинация пакетов — не случайный шум: на 16 моделях и 576 000 образцах кода (2,23 млн ссылок на пакеты) 19,7% ссылок на пакеты оказались галлюцинациями (205 474 уникальных несуществующих имени), а 58% галлюцинированных пакетов повторялись более одного раза при 10 повторных запусках одного и того же промпта (Spracklen et al., USENIX Security 2025; обзор SecurityWeek).

Почему это вредит

  • Корректность. Выдуманные параметры и молча игнорируемые опции порождают неверное поведение на путях кода, которые тесты редко покрывают; сбой проявляется в продакшене, а не на этапе компиляции.
  • Безопасность / цепочка поставок. Галлюцинированное имя пакета — это цель для регистрации: злоумышленники публикуют вредоносное ПО под предсказанным именем, и следующий разработчик, принявший подсказку, его устанавливает — атака slopsquatting (термин ввёл Seth Larson из PSF). Вариант с устареванием молча возвращает устаревшие или содержащие CVE API.
  • Нагрузка на ревью. Правдоподобный код перекладывает на ревьюеров бремя проверки каждого незнакомого вызова по документации; беглый, гладкий текст делает такую проверку менее вероятной.
  • Накопление техдолга. Разработчики часто вставляют "почти работающую" галлюцинированную заготовку и обкладывают её заплатками, вместо того чтобы исправить корневой вызов — подпитывая более широкий тренд эпохи ИИ: рост копипасты и спад рефакторинга (GitClear 2025: доля перемещённых (рефакторинговых) строк — 25% (2021) → менее 10% (2024); копипаста — 8,3% → 12,3%; блоков дублирования из 5+ строк — примерно в 8 раз больше за 2024 год).

##Лечение

Процесс и тактики промптинга

  • Опирайте модель на реальный API. Вставьте в контекст настоящие заглушки типов, нужную страницу документации или исходный код установленной версии, либо используйте инструмент извлечения документации (в стиле Context7 / RAG). Сообщите ей точные версии зависимостей из вашего lock-файла.
  • Требуйте ссылок-обоснований. Просите модель называть официальную запись в документации или сигнатуру типа для каждого используемого стороннего вызова. Практическое правило: каждый вызов метода сторонней библиотеки должен быть прослежен до записи в её документации до одобрения PR.
  • Заставьте это запускаться, в цикле. Требуйте, чтобы tsc / mypy / pylint / сборка и набор тестов проходили, и пусть агент действительно выполняет npm install / pip install, чтобы галлюцинированный пакет падал сразу, а не доходил до ревью.
  • Просите переиспользовать, а не выдумывать. Подайте ей результат grep по существующему модулю и предписывайте вызывать уже имеющиеся вспомогательные функции (reuse the helpers in utils/http.ts), а не создавать новые — это также противодействует смежному запаху дублирования переизобретённых вспомогательных функций.
  • Поставьте зависимости на контроль. Используйте lock-файл, белый список разрешённых к установке пакетов и сканер цепочки поставок (Socket/Snyk) до добавления любого нового пакета, чтобы имена-slopsquat не могли просочиться.

Рефакторинг кода

Замените выдуманный вызов на проверенный настоящий. Если вам действительно нужно удобство, которое вообразила модель, реализуйте его один раз поверх реального API в отдельной обёртке (приём «Извлечение метода»), вместо того чтобы рассеивать поддельный вызов.

// До — выдуманный метод SDK + недопустимое значение опции
async function refundLast(chargeId: string) {
  // `stripe.refunds.createForCharge` не существует, а 'requested' — недопустимая причина возврата
  return stripe.refunds.createForCharge(chargeId, { amount: 500, reason: 'requested' });
}

// После — проверено по типам/документации установленного Stripe SDK,
// а "удобство" обёрнуто один раз, чтобы реальный API не повторялся
async function refundCharge(chargeId: string, amountCents: number) {
  return stripe.refunds.create({
    charge: chargeId,
    amount: amountCents,
    reason: 'requested_by_customer',
  });
}

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

Ограничения. Проверяющие типы и резолверы ловят разрешимое подмножество (типизированные объекты, неразрешённые импорты). Динамический/нетипизированный остаток — выдуманные опции на any, строковые ключи конфигурации, поля REST-нагрузки — не имеет надёжного автоматического детектора и должен проверяться человеком по реальной документации.

##Обнаружение анализаторами

  • eslint-plugin-import import/no-unresolved — Неразрешённый импорт
  • eslint-plugin-import import/named — Несуществующий именованный экспорт
  • Pylint no-member (E1101) — Обращение к несуществующему члену
  • mypy attr-defined — Атрибут/метод не определён для типа