Открытые LLM в продакшене: 8 выводов о llama.cpp, Gemma и Qwen

от автора

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

В WB-Tech мы занимаемся тем, что разворачиваем и сопровождаем открытые модели и агентные системы в собственной инфраструктуре — для внутренних задач и для заказчиков. Это классификация и обогащение входящих заявок, обработка документов, диалоговые системы с вызовом инструментов и другие сценарии, где LLM становятся частью бизнес-процесса, явно выходя за рамки демонстрационных возможностей модели. 

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

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

Ниже — восемь выводов, к которым мы пришли за время эксплуатации открытых LLM. Каждый из — это ответ на реальные проблемы, с которыми рано или поздно сталкивается почти любая команда, разворачивающая модели у себя, и за каждым стоит реальный практический опыт. Наш опыт.

llama.cpp вместо Ollama: меняем удобство на контроль 

Как и у большинства, наше знакомство с локальными моделями началось Ollama. А как иначе — команда pull, команда run и модель уже работает: никакой ручной настройки, зависимостей и возни с форматами весов. Для первых экспериментов лучшего старта, пожалуй, и не придумать. 

Проблемы начинаются позже — когда модель перестает быть демонстрацией и становится частью рабочего пайплайна. В этот момент выясняется, что удобство Ollama построено на скрытых настройках и разумных значениях по умолчанию. Пока их хватает — все отлично. Но как только появляется необходимость управлять инференсом самостоятельно, оказывается, что многие важные параметры просто недоступны.

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

Сразу зафиксируем — дело не в лицензии, и все, что будет сказано дальше, никак не связано с лицензированием. Ядро и CLI Ollama распространяются по лицензии MIT — это полноценный open source (отдельные вопросы вызывает разве что лицензирование десктопного приложения, но к серверному сценарию оно отношения не имеет). Мы ушли не потому, что Ollama «недостаточно открыта». Причины оказались гораздо прозаичнее: контроль, воспроизводимость и безопасность. 

Переход на чистый llama.cpp изменил сразу две вещи. На той же карте, вручную задав раскладку тензоров под точный объем видеопамяти, мы выжали из MoE-модели примерно в два-три раза больше токенов. Причина — в стратегии выгрузки: Ollama выгружает на GPU целые слои, и на разреженной MoE-модели карта простаивает половину времени, в то время как llama.cpp выгружает тензоры точечно и почти полностью загружает GPU. Эффект воспроизводим:независимый замер на RTX 4060 показал 16 токенов в секунду против 45 в пользу llama.cpp — и здесь важен сам факт многократной разницы, а не конкретный множитель на конкретной карте. 

Второй момент — структурированный вывод через грамматику. В llama.cpp грамматика GBNF ограничивает выбор следующего токена на уровне семплера: модель физически не может выдать строку, не соответствующую заданной форме, потому что недопустимые токены отсеиваются еще до выбора. Это гораздо надежнее, чем просить «вернуть JSON» в промпте и надеяться на лучшее. К этому механизму мы вернемся отдельно, когда будем говорить о структурированных выводах, — оно того стоит.

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

Однако для промышленной системы этот обмен удобства на управляемость оправдан. Когда модель становится частью бизнес-процесса, важнее понимать и контролировать каждый этап инференса, чем сэкономить несколько строк конфигурации. Если же вы собираете прототип на вечер, не усложняйте задачу — для этого Ollama и существует.

Универсальной модели нет — есть набор специализаций 

Один из самых живучих мифов вокруг open-source LLM — вера в существование универсально лучшей модели. Мол, достаточно открыть рейтинг, взять лидера и можно ожидать максимального качества во всех сценариях. В продакшене такая логика обычно не выдерживает столкновения с реальностью. Бенчмарки измеряют усредненные результаты на стандартных наборах тестов, тогда как реальные задачи почти всегда имеют собственные требования, ограничения и метрики качества. 

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

  • Gemma 4 31B — отвечает за живой человеческий текст: разобрать резюме, вычитать переписку, объяснить что-то понятным языком, сформулировать окончательный ответ, который отправится клиенту.

  • Qwen3.6 35B-A3B — топ в логике и работе с инструментами: разложить задачу на этапы, принять решение по правилам, вызвать нужный инструмент и вернуть структуру. 

Это не значит, что Gemma не умеет рассуждать, а Qwen плохо справляется с текстом — обе модели многоязычны, и обе поддерживают вызов функций. Речь о другом: на наших задачах, проработанных на десятках реальных кейсов, мы раз за разом убеждаемся, что болтать лучше получается у Gemma, а в пошаговой логике сильнее Qwen. 

Но главное даже не в том, какую модель выбрать для этапа, а в том, что такой выбор — это ложная дихотомия. В зрелых системах конкурируют не модели — конкурируют архитектуры. Пытаться закрыть все задачи бизнеса или производства одной моделью — примерно то же самое, как искать разработчика, который одновременно лучший архитектор, аналитик, дизайнер и, до кучи, верстальщик. 

К примеру: есть входящая заявка, ее нужно классифицировать с пошаговым разбором, отдав человеку понятное резюме. Логику и классификацию берет на себя Qwen — она разбирает заявку по критериям и возвращает структурированный результат (тот самый JSON, к которому мы вернемся ниже). Затем, причесанная структура передается в Gemma, а уже она преобразует ее в естественную для клиента формулировку. В результате и классификация стала точнее, и ответ — естественнее. Не потому, что мы нашли «лучшую» модель, а потому, что перестали заставлять одну модель делать работу за двоих. 

При этом выбор модели нельзя рассматривать в отрыве от аппаратного обеспечения. Обе наши рабочие лошадки рассчитаны на то, чтобы поместиться на одной видеокарте уровня RTX 3090 или 4090 с 24 ГБ видеопамяти. 

Причем 24 ГБ — это именно минимальный объем, в него хорошо помещается модель в кванте Q4_K_M с контекстом из 8–16 тысяч токенов, но стоит повысить качество кванта до Q5 или Q6 — и места под контекст почти не остается, приходится резать. А 3090 — это скорее минимально допустимый стартовый уровень, чем полноценный рабочий инструмент: с нее можно начать, но запас памяти никогда не будет лишним.

Не все модели, которые мы тестировали, дошли до продакшена. Mistral-сборки в наших тестах явно хуже справлялись с русским языком — для задач, в которых модель взаимодействует с русскоязычным клиентом, это дисквалифицирующий фактор.

GPT-oss 20B и 120B к середине 2026 года также заметно отстали от свежих релизов, к тому же у них есть структурное ограничение, не позволяющее использовать для наших целей: они обучаются преимущественно на англоязычном корпусе, что снова создает проблему с корректным пониманием русского языка. 

Сегодня схема выбора модели у нас выглядит так: сначала задача, потом язык, затем ограничения железа — и только в самом конце конкретная модель. Такой порядок почти всегда приводит к лучшему выбору, чем попытки найти нужное среди очередных «героев дня» — лидеров бенчмарков.

Матрица отражает наш набор задач и опыт эксплуатации, а не универсальную истину. В других условиях, с другим языком или железом таблица будет выглядеть иначе — смысл не в конкретных полях, а в порядке выбора: сначала тип задачи и язык, потом то, что помещается в память с нужным контекстом.

Агентный фреймворк — это абстракция, и у нее своя цена 

Когда речь заходит об агенте, идея взять готовый фреймворк кажется почти очевидной. И действительно: зачем писать оркестрацию вручную, если для этого уже существуют готовые инструменты? Но, как мы уже выяснили, чем выше уровень удобства, тем меньше остается контроля над тем, что происходит внутри. Так же, как Ollama скрывает детали взаимодействия с моделью, агентный фреймворк прячет нюансы оркестрации.

Мы попробовали практически все популярные подходы: LangGraph, CrewAI, AutoGen и даже low-code-платформы вроде n8n. У каждого есть сильные/слабые стороны, однако главным вопросом в итоге оказался вовсе не выбор конкретного фреймворка, а необходимость использовать его в принципе.  

Где фреймворк вреден

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

Где фреймворк полезен

Когда нужно было прототипировать многошагового агента с ветвлением — несколько развилок, условные переходы, разные линии в зависимости от промежуточного результата. На такой задаче тот же LangGraph оказался очень кстати: граф состояний удалось собрать за один вечер, тогда как вручную на оркестрацию ушло бы несколько дней. 

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

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

Промпт из ChatGPT ломается в открытой модели 

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

Промпт не существует сам по себе. Перед тем как попасть в модель, он упаковывается в chat template — служебную разметку диалога, а она у каждой модели своя. Gemma ожидает диалог с токенами <start_of_turn> и <end_of_turn> и двумя ролями — user и model. Qwen использует формат ChatML с токенами <|im_start|> и <|im_end|>, а также отдельной системной ролью. 

Если подать модели шаблон чата, на котором ее не обучали, она не упадет с ошибкой, но и не предупредит о проблеме. Она просто перестанет отвечать с ожидаемым качеством. И это одна из самых неприятных поломок: кажется, что модель деградировала или промпт перестал работать, хотя на самом деле проблема в неправильной разметке диалога. 

Поэтому, кстати, в документации Hugging Face отдельно подчеркивается: при ручной сборке промптов необходимо использовать chat template, соответствующий конкретной модели. В противном случае качество снижается и весьма заметно — в отдельных исследованиях говорится о потерях порядка 10–30%

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

Самый очевидный пример непереносимости — системная инструкция. У Qwen в ChatML есть отдельная роль system: достаточно передать системное сообщение, и модель интерпретирует его именно как системное. У Gemma исторически отдельной системной роли не было — рекомендации разработчиков сводятся к тому, чтобы включать инструкции в первое сообщение пользователя. 

Отсюда правило: нельзя переносить промпты из ChatGPT в Claude и обратно «как есть». Начинайте с шаблона, под который обучалась конкретная модель, а системную инструкцию держите короткой и максимально конкретной. 

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

И еще: смена модели почти всегда означает смену промпта. Даже если в одном пайплайне Qwen корректно повторяет результат Gemma, рассчитывать на такое поведение при одной и той же инструкции в будущем не стоит. 

Наверное, красиво было бы подытожить готовой памяткой: «для Gemma делайте так, для Qwen — вот так». Но ее не будет. Зато есть общие принципы промпт-инжиниринга — за ними можно сходить, например, к Prompt Engineering Guide. Но и они не отменяют необходимости адаптировать промпт под конкретную модель. Не существует набора правил, который одинаково хорошо работает везде. Промпт — не слой поверх модели, а ее часть.

Перестаньте просить JSON — начните его гарантировать

Когда модель становится частью бизнес-процесса, ее ответы превращаются в триггер для связанных сервисов. Например, классификатор передает результат CRM, агент вызывает следующий инструмент, а сервис принимает решение по полученным данным. Значит, ответы обязаны иметь строго определенный формат, а не что-то похожее на JSON — другими словами, их структура должна быть гарантирована.

Первое решение, которое приходит в голову, — указать в системной инструкции, что ответ должен быть в формате JSON, а затем разобрать его программно. На тестах этого обычно достаточно. Однако в продакшене выясняется вдруг, что формат остается всего лишь пожеланием, он лишь задает модели ожидаемое поведение. Та же, может вернуть не только код, но и любой дополнительный текст вокруг него. Человек этого даже не заметит, зато программа, ожидающая строго валидный JSON, не сможет разобраться в этом хаосе. Результат — ошибка парсинга, повторный вызов модели или остановившийся пайплайн. 

Выше мы обещали вернуться к грамматикам GBNF — самое время. Их идея в том, что формат ответа перестает быть частью промпта и становится частью механизма генерации. Вместо инструкции «верни JSON» мы задаем набор правил, которым ответ обязан соответствовать. Если очередной токен нарушает эту грамматику, движок не позволяет модели его сгенерировать.

Это принципиально меняет подход. Формат ответа больше не зависит от того, насколько аккуратно модель выполнила инструкцию — он гарантируется самим процессом генерации. Поэтому JSON перестает быть рекомендацией для модели и становится свойством декодера: некорректный ответ просто не может появиться. Говоря языком плаката: мы перестаем договариваться с моделью о формате и начинаем навязывать его инфраструктурой.

Ниже — реальная грамматика GBNF, которую мы используем в классификаторе входящих заявок.

Ключевая строка здесь — action. Грамматика допускает ровно три значения, и модель не может придумать четвертое: никаких «delete», «maybe» или «create_ticket» с лишним пробелом. Для классификатора, результат которого передается дальше по пайплайну в условный оператор, это решает половину проблем — ветка else перестает ловить неожиданные значения, потому что их больше не существует.

Такие грамматики необязательно писать вручную, и для больших схем это не требуется. llama.cpp умеет конвертировать подмножество JSON Schema в GBNF — либо заранее с помощью скрипта json_schema_to_grammar.py, либо при каждом запросе, если передать схему в поле json_schema (или response_format для /chat/completions) серверу llama.cpp. То есть схему вы храните в привычном формате JSON Schema, который и так понятен всему вашему стеку, а движок сам компилирует из нее грамматику. 

Полезная деталь: при конвертации additionalProperties по умолчанию выставляется в false — так ускоряется работа грамматики, а вероятность того, что модель додумает лишние ключи, становится ниже.

И вот теперь вроде все хорошо, но есть нюанс — возможности грамматики не стоит переоценивать. Да, она гарантирует форму ответа, но не гарантирует его содержание. 

  • Во-первых, она ограничивает формат, а не смысл. Грамматика обещает, что confidence — это число, но не факт, что это число осмысленное. За правильность значений по-прежнему отвечаете вы. 

  • Во-вторых, грамматика задает структуру, но не сообщает модели, что в нее нужно поместить. Если не описать ожидаемые поля в самом промпте, можно получить идеально валидный JSON, набитый пустыми или бессмысленными значениями: форма верная, а содержание — мусор. Грамматика и внятный промпт работают в паре, по отдельности ни то ни другое не справляется. 

  • В-третьих, грамматика не гарантирует завершенность на 100%: если модель упрется в лимит токенов в середине объекта, вы получите валидный, но обрезанный результат, потому что грамматика и решение модели «пора заканчивать» существуют независимо друг от друга. Лечится это просто — закладывайте лимит токенов с запасом на случай, если объект окажется больше.

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

За всеми этими решениями стоит общий принцип: не оставлять критически важные свойства системы на усмотрение модели. Если что-то можно гарантировать средствами инфраструктуры, значит, нужно гарантировать. Настройки инференса, шаблон диалога, формат ответа — все это должно определяться системой, а не вероятностью того, что модель в очередной раз сделает «как просили». 

Температура — свойство этапа пайплайна, а не модели 

Температуру генерации часто воспринимают как глобальную настройку: выставил один раз, обычно где-нибудь на уровне 0,7, — и больше к этому вопросу не возвращаешься. Но, извлечение данных, агентная логика и диалог с пользователем предъявляют к модели слишком разные требования, чтобы использовать для них одно значение.  

Для себя мы используем такие диапазоны: 

  • 0–0,3 — извлечение данных, классификация, принятие решений. Любая выдумка модели здесь означает провал.

  • 0,2–0,5 — агентные цепочки и работа с инструментами. Нужна предсказуемость, но иногда полезно оставить модели небольшой простор для выбора.

  • 0,7–0,9 — диалог, объяснения и тексты, которые читает человек. Здесь естественность важнее абсолютной детерминированности.

Чтобы подбор температуры не превращался в гадание, нужно помнить простую вещь: температура генерации не делает модель «умнее» или «креативнее» — она лишь определяет, насколько далеко та будет отходить от наиболее очевидного ответа. Чем ниже температура, тем чаще искусственный интеллект выбирает вариант, в котором уверен сильнее, чем выше — тем охотнее рассмотрит вероятные и невероятные альтернативы.

Отсюда правило: чем выше цена ошибки, тем ниже должна быть температура. И наоборот — чем важнее естественность текста, тем больше свободы можно оставить модели. 

Теперь о том, что в облачных туториалах написано неверно применительно к открытым моделям. Почти везде температуру предлагают крутить в паре с top-p. Но на открытых моделях, развернутых локально через llama.cpp или vLLM, практики к 2026 ушли в другую пару — температура плюс min-p (примерно 0.05–0.1). В отличие от top-p, min-p отсекает хвост относительно уверенности самой модели в текущем шаге, ипо результатам работы с ICLR 2025 обходит top-p, особенно на высоких температурах и творческих задачах.

Такой подход лучше показывает себя на открытых моделях, особенно при высоких температурах. Тем не менее общий принцип остается прежним: не стоит одновременно подбирать несколько параметров семплирования — и OpenAI, и Anthropic советуют менять что-то одно. Сначала выберите механизм отсечения маловероятных токенов — для локального стека это обычно min-p, — а затем уже настраивайте температуру относительно него. 

Также температуру часто путают с грамматикой GBNF, хотя они отвечают за совершенно разные вещи.

  • Грамматика гарантирует форму ответа: если требуется JSON, никакого другого формата модель уже не сможет сгенерировать. 

  • Температура влияет только на выбор между допустимыми вариантами внутри этой формы. Поэтому высокая температура не сломает структуру ответа, но вполне может увести модель от наиболее вероятного решения к менее удачному. 

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

Еще одна деталь, о которой лучше знать заранее: даже температура 0 не гарантирует абсолютно одинаковый ответ при каждом запуске. Из-за особенностей вычислений на GPU небольшие различия все равно возможны. На практике речь обычно идет о стабильном, а не побитово идентичном результате. Поэтому клиенту правильнее обещать не абсолютную детерминированность, а воспроизводимое поведение системы.

И последнее наблюдение: температура, как и промпт, не переносится между моделями автоматически. Значение 0,3 для Gemma и 0,3 для Qwen не означает одинаковую степень вариативности: распределения вероятностей у моделей разные. Поэтому после смены модели температуру всегда приходится калибровать заново. Универсальных значений здесь, как и в случае с промптами, не существует.

Контекст — это не память модели, это ее внимание

Интуитивно кажется: чем больше контекста получит модель, тем лучше она ответит. На практике же все наоборот — чем меньше контекста, тем выше качество результата.

Ограниченное окно — лишь первая, очевидная половина проблемы. На видеокарте с 24 ГБ памяти, о которой мы говорили выше, модель в кванте Q4_K_M обычно работает в пределах 8–16 тысяч токенов. Длинный диалог, документы или результаты работы инструментов постепенно занимают все доступное пространство.

Но куда опаснее другое: качество ответов начинает незаметно снижаться задолго до того, как свободное место закончится. Чем объемнее инструкции, тем труднее модели удерживать внимание на действительно важных фрагментах. Это явление известно как Lost in the Middle: современные LLM лучше работают с началом и концом контекста, а информация в середине постепенно теряет вес. 

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

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

Поэтому большое окно вредит гигиене контекста. Если оставлять в нем все подряд, лишняя информация начинает мешать сильнее, чем помогать.

Мы давно перестали воспринимать контекст как память, которую нужно заполнить. Для нас, это скорее рабочее пространство с ограниченным объемом, где каждый токен должен оправдывать свое присутствие. И ключевая установка теперь не «что еще добавить в контекст», а «что уже можно убрать».

Рабочая схема сейчас выглядит так: 

  • последние несколько реплик сохраняем дословно — это самая свежая информация, к которой модель обращается чаще всего;

  • все, что старше, сворачиваем в короткую структурированную сводку и переносим в начало окна;

  • мусор — ответы инструментов, дампы API, служебные сообщения и любые промежуточные данные удаляем сразу после того, как они выполнили свою задачу.

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

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

Веса нельзя одалживать — ими нужно владеть

Последний вывод самый незаметный, потому что касается не модели, а файла с весами — того, на что обычно не смотрят вовсе. Обычно как: нашли нужную модель на Hugging Face, скачали файл весов, подключили его к системе — и, забыли. В прототипе этого достаточно. В промышленной системе — уже нет.

Файл с весами — такая же зависимость, как библиотека, Docker-образ и любой другой внешний артефакт. Если завтра автор удалит репозиторий, заменит файл новой версией или просто изменит тег, ваша система уже не будет воспроизводимой. Поэтому веса моделей стоит версионировать, хранить в собственном репозитории и обновлять так же осознанно, как любую другую критическую зависимость.

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

Начнем с воспроизводимости. Хранилище модели на Hugging Face — это обычный git-репозиторий. Если вы указываете модель по имени или плавающему тегу, то фактически доверяете тому, что лежит в этом репозитории сегодня. Через месяц там может оказаться уже другой файл: новая версия GGUF, измененный токенизатор или обновленный шаблон чата — тот самый, который мы выше называли частью модели. Изменился он — и промпты тихо поехали, хотя ваш код остался цел.

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

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

Мы используем GGUF и llama.cpp, поэтому один из самых известных классов атак — выполнение произвольного Python-кода при загрузке pickle-моделей — нас не касается. Но это не означает, что проверять происхождение моделей больше не нужно. В сети полно примеров атак через шаблоны чата внутри GGUF, поэтому скачивать случайный файл весов по первой найденной ссылке — такая же плохая идея, как запускать неизвестный Docker-образ в продакшене.

Из этого следует новое правило! Одобренные веса модели фиксируют по конкретной версии, зеркалируют в собственную инфраструктуру — внутренний registry, S3 или постоянный том — и уже оттуда используют. Это одновременно делает систему воспроизводимой, избавляет от внешней зависимости и блокирует незаметную подмену файла. 

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

На первый взгляд такая дисциплина выглядит скучной: фиксированные версии, собственное хранилище, зеркала, контроль зависимостей. Она лишена того творческого азарта, с которым команды берутся за LLM-проекты и в ней нет ничего, что принято считать магией ИИ. Но именно здесь проходит граница между перспективным прототипом и стабильной системой, которой можно доверить реальный бизнес-процесс.

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

Заключение

Когда только начинаешь работать с открытыми моделями, кажется, что главная задача — выбрать лучшую. Потом кажется, что все решает идеальный промпт. Затем — что достаточно подобрать правильный агентный фреймворк. 

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

  • Именно поэтому мы отказались от высокоуровневых оберток ради полного контроля над инференсом. 

  • Перестали искать универсальную модель и начали разделять роли между несколькими. 

  • Признали, что промпт — часть модели, а не универсальная инструкция. 

  • Перенесли контроль формата ответа из промпта в грамматику. 

  • Научились управлять контекстом вместо бесконечного накопления токенов. 

  • Стали относиться к весам модели как к обычной критической зависимости, которую нужно версионировать и хранить у себя.

Все эти решения кажутся разрозненными только на первый взгляд. На самом деле их объединяет один принцип: если критически важное свойство системы можно забрать у модели и превратить в управляемый инструмент — именно так и стоит поступить.

В этом, пожалуй, и заключается главное отличие демоверсии продукта от эффективной промышленной LLM-системы. Прототип строится на предположении, что модель будет вести себя правильно. Промышленная система проектируется так, чтобы критические вещи не зависели от этого предположения.

Именно такой подход оказался для нас самым ценным уроком. А чтобы вы добавили к нашему списку?

ссылка на оригинал статьи https://habr.com/ru/articles/1059094/