nobirds
RU / EN

май 2026 · Борис Ванин

Как мы строим LLM-пайплайны

По сути базовых паттернов всего два. Всё остальное, что выглядит как умная архитектура, — их комбинация.

Два паттерна

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

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

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

Когда без тулов не обойтись

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

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

С тулами модель меняет состояние декларативно: addItem(...), updateNpc(id=42, ...), removeQuest(id=17). Контекст остаётся компактным: модели хватает текущей версии состояния и описания изменений. Это масштабируется, проверяется и хорошо ложится в логи.

Планер + имплементоры

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

Главное здесь — качество плана. Если планер выдал «обнови NPC по имени Гарри» вместо «обнови NPC id=42, добавь черту X, сними статус Y», то ты обречён: имплементоры будут гадать, ошибаться и тиражировать ошибку дальше по цепочке. И наоборот, если план достаточно конкретный, имплементором может быть и Haiku, и Flash, и любая другая недорогая модель — пусть даже не самая умная, лишь бы аккуратно исполняла.

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

Стейт-машина внутри лупа

Внутри одного агентского лупа полезно держать маленькую стейт-машину: «сгенерируй → проверь себя → исправь → готово». Поскольку system + tools между итерациями стабильны, явный кеш всё это время горячий — self-review получается дешёвым.

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

Кеши

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

Неявный (implicit). Провайдер сам замечает, что префикс твоего запроса совпадает с префиксом одного из недавних, и переиспользует вычисления. Плюсы: ноль усилий со стороны разработчика, ничего не надо ни создавать, ни поддерживать. Минусы: ты полностью зависишь от эвристик провайдера, гарантий нет, диагностики «почему сейчас не попало» — нет тоже.

Явный (explicit). Ты создаёшь именованный объект кеша с TTL и ссылаешься на него по ID в последующих запросах. Плюсы: хиты предсказуемы, ты сам решаешь, что и когда кешировать. Минусы: лишний API-механизм, плата за создание и хранение, иммутабельность содержимого, ручное продление TTL и чистка после использования.

Оба сломаны, но выбора нет

Неявный — лотерея. Предсказуемость нулевая. На свежем Gemini 3.5 Flash сразу после релиза хит-рейт у нас был около нуля. На моделях постарше — в лучшем случае 40–50%, и того никто не обещает. Никакого «сделай вот так — и попадёшь в кеш» в документации нет; о промахе узнаёшь из биллинга. Канонический баг — googleapis/python-genai#1880 (стабильный префикс, меняющийся суффикс, хиты пляшут 40–60%), свежее веселье — issue #2064 (у Gemini 3 Flash в диапазоне 9K–17K токенов cached_content_token_count молча падает в ноль), отдельная ветка боли — «Has anyone gotten implicit caching to work?» на форуме разработчиков.

Явный — игра не стоит свеч. Кеш-объект обязан включать в себя полный конфиг промпта — системный промпт, описание тулов, генерация-config. Любое изменение этой части — и кеш надо пересоздавать; содержимое неизменяемо. Это сразу режет эффективность на порядок: общего «кеша системного промпта» не существует, для каждой связки system + tools у тебя свой кеш. И в нашем домене даже между двумя запусками одного и того же пайплайна переиспользование почти невозможно — потому что кеш дробится уже внутри одного запуска: планер идёт с одним набором тулов, имплементоры — с другим, разные имплементоры между собой — тоже с разными. Каждый шаг живёт со своим кеш-объектом, осмысленных совпадений между запусками остаётся мало. Сверху — TTL, который надо продлевать, и чистка после использования, иначе платишь за хранение мусора.

Итог: выбирать всё равно приходится явный — только он даёт хоть какую-то предсказуемость. Просто надо понимать, что в сложном пайплайне эффективность у него скромная, и в экономику его можно закладывать только там, где связка system + tools реально стабильна между запросами — чаще всего это итерации одного агентского лупа. Опираться на неявный кеш в base case — нельзя; на явный — можно осторожно, понимая ограничения.

Ничего лишнего

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