Галлюцинированный 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 — Атрибут/метод не определён для типа