Почему большой контекст в LLM не спасает от галлюцинаций
25 июня 2025

Миф о “больше контекста = меньше галлюцинаций”
Мне кажется, что в последнее время радость по поводу увеличений размера контекста немного поутихла, но не угасла совсем. Непонятно, с чем именно это связано: либо новости про LLM в принципе уже всем замылили глаза и не вызывают особого восторга, либо пользователи и инженеры полуинтуитивно почувствовали, что действительно большого выигрыша размер контекста не даёт.
Я склонен полагать, что сама такая идея «большой контекст решит мои проблемы» всё ещё жива, поэтому поговорим сегодня о том, почему это очередная ловушка, почему нас вообще беспокоит размер контекста, что мы пытаемся решить на самом деле и как это надо решать по-хорошему.
Почему инженеры попадают в эту ловушку? Потому что инженеры — люди. У людей далеко не всегда самые свежие, актуальные и правильные картины мира. Нам всегда не хватает тех или иных знаний, чтобы интерпретировать «нечто» в минимальном отрыве от реальности. Да и не всегда мы вообще любим сильно напрягаться и что-то медленно, рационально интерпретировать, моделировать.
Это не обязательно связано с ленью, но ещё и, например, с горящими сроками и иными подталкивающими факторами.
Что мы действительно решаем, надеясь на то, что больший контекст поможет нашей AI-системе стать… точнее? Само это стремление к точности ответов чаще всего вызвано попыткой убежать от возникающих галлюцинаций – мы по умолчанию хотели точных ответов, но в процессе выбора архитектуры вообще даже не задумывались о том как бы ее достигнуть.
Часто такой подход приводит к тому, что запросы распухают, в контекст попадает слишком много если не лишней, то размытой информации. Вот она – причина, и мостик к надежде на спасение контекстом подлиннее.
Анатомия проблемы: почему длинный контекст не работает
Рассмотрим очень популярный сценарий, когда на базе LLM мы хотим построить NLI (natural language interface) чатик, «ну вот чтобы прям как ChatGPT», но с нашими данными: с базой данных, или с нашим большим и сложным API.
Часто такая задача слишком абстрактна; кажется, что интегрировать наш API в AI-систему просто — вон же, много агентских фреймворков, бери и делай. Мы закрываем глаза на то, какой именно цели мы пытаемся достичь: нужен ли вообще этот чатик кому-то? Если нужен, то какую ценность он предоставляет? Какие у нас требования к той самой точности? Будем ли мы еще какие то фичи туда интегрировать?
Тупо интуитивно от такого проекта мы ожидаем, что на выходе будет мало галлюцинаций, ведь задача вроде бы простая: сходить в API, достать данные, отдать конечному пользователю в читаемом формате.
Это действительно простая задача, когда речь идёт о простых API, вроде погоды, покемонов или любых других с ограниченным и детально проработанным форматом ответа, понятным без большого дополнительного контекста в виде документации и иных аннотаций-спецификаций.
Много ли вы таких API видели? Особенно в бизнесе?
В сценарии с интуитивным подходом мы просто и быстро, с использованием какого-нибудь фреймворка и, вероятнее всего, архитектуры типа Agent с function-calls, строим AI-интерфейс вокруг нашего API, и он… не работает хоть сколько-нибудь адекватно, точно, вменяемо.
Мы пытаемся поправить промпт, подтянуть тут и там, и в какой-то момент результаты становятся чуть лучше, или нет, даже вполне себе неплохими! Мы радостно собираем релиз, выкатываем его на демонстрационное окружение, также радостно зовём коллег попробовать продукт, и тут выясняется, что нормально ничего не работает: система думает, что она в 1743 году разбивает французов в Баварии, и формат ответа то такой, то сякой.
Вы с выпученными глазами запускаете ту же сборку локально, и всё работает… ну, нормально.
Алиса в стране галлюцинаций. Непонятно, что с этим делать и как дальше разрабатывать такую систему эффективно. Вы начинаете исследовать глубже и оказывается, что function calling — это просто «приляпка» в виде JSON-описаний каждой функции, которые попадают в контекст при запросах, и LLM пытается выбрать нужную функцию, чтобы получить недостающие данные для ответа.
Вы таких функций сделали 75, у каждой возможны по 20 аргументов.
Копнув ещё немного глубже, оказывается, что вы почти никак не обрабатываете ответы от ваших function calls, почти все эндпоинты вашего API возвращают JSON-простыни, которые вы сохраняете для истории диалога и… отправляете со следующими запросами.
Галлюцинации берутся именно отсюда: очень сложно заставить AI-систему сделать «всё и сразу» накидав мусора в запрос и сложив лапки в молитвенном жесте.
Вы думаете: «Ну, раз всё попадает в контекст, значит… нам нужен контекст побольше! Возьмём модель подороже», — и… ничего существенно не поменяется. Но почему?
Давайте рассмотрим некоторые технологические особенности и проблемы.
Если эти технологические «особенности», связанные с контекстом в LLM вас не интересуют, смело проматывайте на подзаголовок «А нужна ли она вообще, эта память?».
Эффект “Lost-in-the-middle” и позиционные биасы
LLM демонстрируют U-образную кривую внимания: хорошо обрабатывается информация в начале и конце контекста, но данные в середине «теряются».
Наши замечательные модели имеют “встроенный” bias к позиции токенов в контексте: они преимущественно обучаются на относительно коротких текстах, и модель «привыкает» к тому, что вся самая ценная информация — вопрос и его детали — находится либо в начале промпта, либо в конце.
Увеличение контекста не помогает, а скорее создаёт ещё большую «мёртвую зону» в середине.
Выходит, что зачастую критически важная для точности ответа информация, попавшая в середину длинного промпта, может быть просто «не учтена».
Пример: наш неидеальный агент собирает function calls, вызывает несколько тех Самых эндпоинтов, которые отдают «простынки» JSON, и в итоге происходит вызов для подготовки финального ответа. Вот в этом вызове как раз всё самое ценное — данные, полученные из API, — частично могут попадать как раз в эту «мёртвую зону».
Вдобавок к этому, в прошлом году был ряд исследований, раскрывших интересные нюансы о точности контекста. Например, тут при исследовании open-source моделей выяснилось, что эффективная длина контекста зачастую не превышает и половины заявленной, тренировочной длины.
Можно сказать: «Ну так это open-source». Да, но и все более горячо любимые нами модели основаны на той же архитектуре, а значит с какой-то вероятностью имеют схожие проблемы. Это подтверждается как минимум эмпирически – проблемы галлюцинаций встречаются часто и горячо обсуждаются.
Деградация внимания в длинных последовательностях
Представьте, что вы участвуете в большом онлайн-семинаре. Первые 15 минут вы внимательно слушаете, делаете заметки, стараетесь изо всех сил не упустить что-нибудь важное.
В лучшем случае через час (на деле же…) ваше внимание начинает всё больше рассеиваться, вы думаете про обед, про игры в Steam, иногда возвращаетесь к разговору. И дальше, чем дольше будет проходить звонок без перерывов, тем сильнее вы будете отвлекаться.
LLM работают удивительно похоже при обработке длинного контекста.
Механизм внимания в трансформерах — ресурс не безграничный. По мере увеличения длины последовательности модель начинает всё больше «слушать саму себя» вместо исходного контекста.
Феномен “Hallucinate at the Last”
Другие исследования показывают, что галлюцинации концентрируются в конце длинных генераций. Например, если вы попросите модель написать длинное резюме документа, то начало будет довольно-таки точным, основанным на исходном тексте, а к концу модель начнёт глючить, добавляя детали, которых в исходнике не было вообще.
Причина в том, что по мере генерации модель всё больше обращает внимание на свои собственные предыдущие токены, а не на исходный контекст. Это как если бы во время презентации вы начали цитировать не исходный материал, а то, что сами говорили 10 минут назад, потом цитировать цитату из цитаты… К концу получается игра в испорченный телефон с самим собой: «Я никогда не повторяюсь, я никогда не повторяюсь, я никогда не повторяюсь…»
В контексте нашего агента это выглядит так: модель сделала несколько function calls, получила данные, начала формировать ответ. В начале ответа она ещё помнит и использует реальные данные из API. Но чем длиннее становится её собственный ответ, тем больше она полагается на свои предыдущие формулировки и паттерны из обучения. Результат: начало ответа правильное, а к концу появляются «факты», которых не было ни в одном API-вызове.
В добавок очень забавно выглядит, когда агент «вспоминает» функции, которые не вызывал, или данные, которые «должны быть» согласно его «внутренним представлениям» (модели под капотом).
Накопление вычислительных ошибок
Каждая операция в нейронной сети — это математическое приближение. Когда мы работаем с очень длинными последовательностями или используем сжатие модели (квантизация), небольшие ошибки округления и приближения начинают накапливаться. Но последнее, если мы используем большие и крутые модели от OpenAI, Google, Anthropic, нам, вроде бы, не грозит.
Вся работа LLM — это длинная цепочка таких приближений, связанных друг с другом последовательно. Если на каждом шаге у вас погрешность в 0.1%, то через 100 шагов «суммарная ошибка» может составить уже 10%.
Короче, чем длиннее контекст и чем больше вычислительных операций требуется для его обработки, тем выше вероятность, что накопленные ошибки исказят результат.
Самое поганое тут то, что эти ошибки практически невозможно отловить в процессе разработки вашей системы — они проявляются стохастически и зависят от конкретной комбинации входных данных.
Наверное, вас уже посетила мысль, что все эти проблемы могут “работать” в связке, создавая эффект, где увеличение контекста не только не решает проблему галлюцинаций, но потенциально даже усугубляет её.
Именно поэтому опытные инженеры AI-систем уже давно перестали пытаться запихивать «всё и сразу» в контекст и перешли к более инженерным решениям по управлению памятью и состоянием диалога. По этой же причине у этих же инженеров не было wow-эффекта, когда вышла, например, Gemini 2.5 Pro.
А нужна ли она вообще, эта память?
Да, концептуально дело именно в памяти. Контекст моделей по существу используется нами в лоб как «элемент памяти». Мы пытаемся сгрести в кучу всю информацию, которая необходима для обработки конкретного запроса.
Интуитивно-по умолчанию нам кажется, что наша AI-система должна детально, исчерпывающе помнить всю историю каждого нашего разговора с ней (на каждой итерации беседы), примерно так, как работает, например, Claude Desktop. Мы бы очень хотели, чтобы наш собственный AI-интерфейс был таким же умным, внимательным и чётким. Как мы уже убедились ранее, большое количество подводных камней скрыто от наших глаз.
Ну и вообще, чисто технически, клиент от «богатой» и хорошей модели по типу Claude и наш собственный ассистент, в которого мы на скорую руку напихали наш API, — концептуально разные вещи.
Благо, в этом достаточно легко убедиться: если у вас уже есть агентская система с кучей проблем и галлюцинаций, возьмите все function call (в том виде, в котором они есть) и «конвертируйте» их с помощью LLM (vibe coding) в MCP-сервер. Подключите этот MCP-сервер в Claude Desktop и попробуйте пообщаться с вашим API таким образом.
С большой долей вероятности значительного улучшения не произойдёт, главное, не жульничайте и сохраните всю сложность и избыточность функций и их описаний.
Серьёзная память
В минимальном варианте историю разговоров с нашей системой мы сохраняем в базе данных — реляционной или документоориентированной, не суть важно в свете сегодняшней темы.
Внешние системы памяти, RAG, Knowledge Graph и прочие — отдельный и интересный топик, которого сегодня мы касаться не будем. В точности векторного или гибридного поиска в RAG-like системах тоже возникают боли, но проблема с контекстом может возникать и в них, как следствие ситуации, когда мы, например, «слишком много достали из хранилища» или всё ещё пытаемся скрещивать архитектуру Agent с RAG за function call’ами.
И давайте всё-таки сделаем шаг назад
Все технические нюансы и ограничения контекста LLM — это интересно и познавательно, но являются ли они коренными причинами галлюцинаций? Ну, в сферическом вакууме — да, но вполне очевидно то, что мы наступаем на эти грабли в результате наших предварительных решений (или их отсутствия).
Разбирая практически любую проблему, возникающую при построении AI-системы, прозаически на самом дне находишь старые добрые пробелы в техническом задании, в моделировании реальной бизнес-задачи, а не только в самом решении.
Такие пробелы возникают из-за вынужденной спешки, или просто из-за когнитивных искажений, когда мы склонны вместо того, чтобы задержаться и детально продумать задачу, принимаем первое «интуитивно» возникшее решение за чистую монету.
Продолжим рассматривать наш пример: чатик с LLM вокруг нашего API от которого мы хотим глубину памяти/понимания истории диалога.
Зачем? Для чего? На поверку, подавляющее число кейсов покрывается использованием всего лишь 3–5 последних пар «вопрос-ответ». А возможно, даже меньше, особенно если у вас высокие требования к точности и свежести данных. В гонке за хорошей памятью о разговоре мы стреляем себе в ноги, ставя под угрозу то, что нам на самом деле важно, — точность.
Подводные камни function calling
LLM-провайдеры и их API, которые поддерживают function calling, прогоняют всю цепочку вызовов функций и их ответов. Агентские фреймворки следуют этому интерфейсу и сохраняют все эти сообщения в структуры данных.
Можно подумать:
- Ну что ж, раз надо — значит надо, будем хранить все промежуточные вызовы!
- И вообще это хорошо, на каждой итерации диалога LLM будет получать весь контекст предыдущих вызовов! Это же просто обязано хорошо работать, в кейсах когда пользователь задаёт уточняющие вопросы! Все уже есть в контексте!
Да, это должно хорошо работать и будет хорошо работать… когда ваши функции отвечают короткими, информативными, ёмкими джейсонами или типа того. Мы уже рассмотрели проблему длинных ответов от вашего API – контекст диалога может распухать чрезвычайно быстро.
Что мы можем сделать? Ну, во-первых, снова вернуться на уровень проектных требований и задаться вопросами:
- Все ли эти функции нам вообще нужны?
- Все ли аргументы в интерфейсе функции нужны?
- На какие запросы клиента эти функции должны отвечать?
Выкинув лишние, можно пойти дальше и начать очищайть ответы ваших функций от всего мусора, null-полей и прочего, что никак не поможет LLM ответить на поставленный вопрос.
А теперь, внимание, бонус для тех, кому важна точность/свежесть:
Попробуйте вообще не сохранять промежуточные результаты вызовов функций. Да, это трейд-офф: на случай если пользователь задаёт вопрос, который обслуживается той же, только что вызванной функцией, нам придётся вызывать её ещё раз, но чисто семантически, пар из вопроса пользователя и итогового ответа от вашей системы может быть более чем достаточно для поддержания высокого уровня «осознанности» разговора.
Маленькие советы про историю диалогов
Обработка и сжатие
Если вы всё-таки решили, что вам история нужна, то надо разобраться, насколько точно нужно ей следовать и как вы будете эту историю хранить.
При разработке NLI-чатиков часто мы мешаем мух с котлетами — используем историю сообщений для двух вещей:
- Контекст диалога: на каждый новый запрос мы смотрим последние сообщения.
- История диалогов для UI: фронтенд для отображения чатика ходит за теми же сообщениями.
Котлеты и мух нужно можно разделять. Например, для каждого диалога мы будем вести отдельную запись — граф фактов с каким-нибудь mem0, либо периодически делать отдельным запросом в LLM выжимки по истории диалога и использовать их как дополнительный контекст, но намного более сжатый и емкий чем тупо история всех сообщений.
Все эти решения сильно зависят от задачи, лично я нашёл здесь подход мухи+котлеты в базе данных вполне приемлемым для MVP, при учёте, что размер сообщений, глубина и чистота подтягиваемого контекста остаются адекватными.
На случай, если ваш «чатик» преследует цели моделирования сложных сценариев общения с клиентом и таких разных сценариев может быть несколько, рассмотрите вариант отслеживания состояния диалога. Тут тоже нет ничего сверхсложного: это тупо state machine или, ещё проще, enum-статус, на базе которого принимается прозрачное решение, какому сценарию следовать.
От такого переключателя, как правило, зависит активный промпт или основная/большая часть промпта.
Система мониторинга и оценки
Если уж решили что-то мониторить, лучше смотреть за тем что реально влияет на пользователей.
Метрики для conversation awareness
Забудьте сразу бросаться на всякие semantic similarity scores и прочие академические метрики.
В дополнение к базовым must-have (сервер жив?) стоит задуматься о том чтобы собирать подобное:
- Сколько раз пользователь переспрашивает одно и то же — если часто, значит, система не понимает контекст.
- Как быстро достигается цель — время от первого сообщения до «Спасибо, чатик, ты мне помог!»
- Процент ответов «я не понял» — как от вашей системы, так и от пользователя в ответ на финальный результат итерации диалога.
- Количество сессий на пользователя — возвращаются ли люди или решают задачу с первого раза (если метрика вообще применима, потому что у вас может другого интерфейса в компании/стартапе вовсе и не быть).
Здесь может возникнуть резонный вопрос – а как вообще это мониторить? Подсказка дальше – структурные логи можно будет анализировать проще и даже автоматически с использованием тех же LLM.
Детекция галлюцинаций в рантайме
По моему скромному мнению, самый надёжный детектор галлюцинаций — логирование источников. Каждый условный «факт» в ответе должен быть привязан к конкретному API-вызову, известному источнику данных, function call, и так далее.
Ответ: «Ваш заказ #12345 отправлен вчера»
Источник: GET /orders/12345, поле "shipped_date": "2025-01-20"
Если не можете привязать или вообще удобно найти эту информацию в логах, это огромный блокер, и надо срочно-срочно исправлять.
А как?
LLM-based, так называемые детекторы галлюцинаций, — это как лечить алкоголизм водкой.
Есть вариант попроще и получше — структурное логирование. Оно ничего особенного из себя не представляет и легко может быть внедрено вами одним коммитом прямо сегодня.
Структурное логирование очень прозаично: вы просто определяете условный словарь, и на каждой итерации общения с вашей системой наполняете его экземпляр ключевыми данными о решениях вашего AI-пайплайна.
Польза структурного логирования особенно расцветает, если у вас именно AI-пайплайн, а не oneshot-prompt агент для Pokémon API, в котором бывает подчас крайне сложно понять на каком именно этапе да и что вообще пошло не так.
Получившийся JSON как минимум сохраняйте туда же, куда сохраняете логи, но можно и писать в базу – пригодится, поверьте.
Каждый такой лог становится некоторым «паспортом» взаимодействия с вашей системой, в котором будет написано, на каком этапе и что она «подумала», какую функцию решила вызвать и, возможно, даже с пояснением почему (если используете Custom Chain of Thoughts паттерн.)
A/B-тестирование диалоговых систем
Вообще в тестировании AI систем большой акцент делают на тестировании промптов — evaluation-тесты, которые нужно прогонять после каждого изменения этих самых промптов. Это всё очень хорошо и нужно, может обезопасить от неприятных сюрпризов после очередного деплоя.
Но что тестировать, если мы стремимся к действительным улучшениям продукта на базе LLM? Архитектурные решения! Больший импакт чаще приносят вещи в чуть большем отдалении, чем просто изменение промпта.
Примеры полезных сплитов для тестирования:
- Глубина истории: используем 3 последних сообщения vs 10 последних.
- Function calling: тупой агентский подход vs чуть более сложный, но на базе семантического роутера.
- Промежуточные результаты функций (отфильтрованные!): сохранять vs не сохранять.
- Сжатие контекста: есть vs нет.
- И т.д.
Заключение
Ключевые takeaways, как обычно: думать, прежде чем делать
Большой контекст практически никогда не решает проблему плохой архитектуры. Сложные системы памяти не решают проблему плохих, незаземлённых требований. Модные фреймворки не решают проблему нашего нежелания думать.
В разрезе сегодняшней темы — «Проблемы раздувания контекста и conversation awareness» — прежде чем бежать писать код для “памяти”, ответьте себе на вопросы:
- Что именно нужно запомнить?
- Кому это нужно?
- Что будет, если не запомнить?
- Сколько это стоит vs какую боль решает?
- А решает ли вообще?
Часто окажется, что 80% «проблем памяти» — вообще не проблемы. А оставшиеся 20% решаются за пару часов работы по затаскиванию базы данных или, ещё лучше, паттернами промпт инжиниринга.
Эволюция от просто «больших моделей» к «умным системам»
Умная система — не та, у которой больше контекста или сложнее архитектура. Умная система — та, которая решает задачу пользователя с минимальными усилиями и максимальной надёжностью. Та неживая нейронная система, которая работает как усилитель живой нейронной системы в наших головах.
Метрики успеха должны быть из мира бизнеса, а не из наших технарьских пещер. Решил ли пользователь задачу? Потратил ли меньше/больше времени? Вернулся ли снова? А не только точность на синтетических бенчмарках.
Небольшой чеклист для старта
- Вынесите весь мусор из контекста — куча проблем исчезнет без дополнительных усилий.
- Если ведёте историю в базе — загружайте только нужное — не пихайте всё подряд в каждый запрос. Сначала вообще поймите, что это — «нужное» и как много его требуется на каждой итерации разговоров.
- Не сохраняйте промежуточные результаты function calls, если точность важнее удобства. Как минимум — не сохраняйте их сырыми. И не сохраняйте их, если они всё равно дублируются в финальном ответе от системы. Зачем?
- Мерьте простые метрики — переспросы, время до решения, возвраты пользователей и т. д.
- Добавляйте сложность тогда и только тогда, когда простые решения перестают работать.
Начните с записи (user_message: ..., assistant_response: ...)
в базу и подгрузки последних 3–5 пар на каждый новый запрос. Если этого недостаточно — тогда думайте про суммаризацию, RAG и прочее.
Так и быть, начните с популярного (и очень ненадёжного для сложных систем) паттерна — Агент, если ваш кейс требует лишь нескольких простых функций, возвращающих корректные и простые структуры данных – скорее всего агента хватит.
Особенно если в вашем случае нет высоких требований к точности и надёжности (вы не финтех, а просто удобный чат-бот).
А если система работает надёжно и приносит пользу, пользователям будет всё равно, какой у вас размер контекста, архитектура и какая вообще модель под капотом.