LangChain обещает простоту: переключите модель одной строкой, подключите RAG за две, дайте агенту инструменты за три. На лендинге это выглядит как конструктор LEGO — соединил, и работает. На хакатоне — действительно работает. В production — уже нет.
Тезис «LangChain — это overhead для production» не нов. Его обсуждают на Reddit, в комментариях на Хабре. Компания Octomind использовала LangChain в продакшене больше года — и убрала, заменив на модульные компоненты. Ниже — мой опыт: где именно абстракции сломались, что я построил вместо них и во что это вылилось.
У меня в продакшене мультиагентная система — AI-ассистент, обрабатывающий обращения клиентов через Telegram, WhatsApp и Max (мессенджер VK). Он получает сообщение, классифицирует (вопрос по услуге? техническая проблема? нужен менеджер?), маршрутизирует к нужному агенту. Тот ищет ответ в базе знаний через RAG, подтягивает данные клиента из CRM и отвечает. Если не справляется — эскалирует на человека.
Масштаб скромный — около 50 обращений в день. Но архитектурные проблемы, о которых пойдёт речь, не зависят от масштаба. Они проявляются уже на первом обращении, где модель повела себя не так, как ожидалось.
Систему я построил без LangChain. Сейчас объясню, почему.
Сразу оговорюсь: LangChain — не плохой инструмент. Для прототипа, PoC, демо инвестору — он отличен. Эта статья не про «фреймворки — зло». Она про то, что происходит, когда маркетинговые обещания фреймворка сталкиваются с реальностью production-системы.
«Просто возьми модель получше»
Многие подумают: «Автор использует слабые модели. Возьми Claude Opus или GPT-5.4 — и tool calling будет работать идеально, structured output пойдёт из коробки».
Справедливо. Мощные модели действительно понимают лучше. Но посчитаем, во что это обойдётся.
Одно обращение клиента — это не один вызов LLM. Это цепочка: классификация → маршрутизация → RAG-запрос (embedding + генерация) → формирование ответа. Минимум 3–5 вызовов, в сложных случаях — до 10. Средний запрос — ~2K токенов на входе, ~500 на выходе.
Примерные цены (март 2026, актуальные уточняйте в документации):
GPT-5.4 mini: $300 в месяц при 50 обращениях в день — ещё терпимо. Но при росте до 500 обращений — $3000/мес за один LLM-сервис. За эти деньги можно нанять менеджера, который ещё и кофе себе сварит.
Автоматизация оправдана, только если стоимость inference ниже стоимости ручного труда. Поэтому в production мы используем дешёвые модели: GPT-5.4 nano, YandexGPT 5 Lite. И именно с ними начинаются все проблемы, о которых пойдёт речь.
1. «Переключите модель одной строкой»
LangChain обещает: «Абстрагируем провайдера. Хотите OpenAI — пожалуйста. Хотите Anthropic — одна строка. Хотите YandexGPT — ещё одна. Код не меняется».
Формально — правда. Меняете строку в конфиге. А потом наблюдаете, как система начинает вести себя непредсказуемо.
Один провайдер, разные модели — уже проблема
Возьмём OpenAI. GPT-5.4 и GPT-5.4 nano — один API, один формат. Казалось бы, переключение безболезненно. На практике:
- Температура 0.3, дающая стабильный structured output на флагманской модели, на nano выдаёт «креативные» вариации: category с большой буквы, лишнее поле explanation.
- При длинном контексте nano «забывает» инструкции из системного промпта. Не всегда — в этом и подлость. В 90% случаев всё хорошо, в 10% — сюрприз.
- Few-shot примеры, задающие формат ответа на GPT-5.4, на nano иногда воспринимаются как ещё один пример для классификации. Модель начинает классифицировать сам пример, а не запрос пользователя.
И это — две модели одного провайдера.
Разные провайдеры — другая вселенная
Теперь переключимся с OpenAI на YandexGPT. Тот же промпт, та же задача — классификация. Что происходит:
- JSON? Какой JSON? YandexGPT может вернуть ответ в свободной форме: «Я считаю, что обращение относится к категории "вопрос по курсу"...». А мой парсер ожидает
{"category": "course_question", "confidence": 0.95}. - Четвёртая категория из воздуха. Промпт чётко задаёт три категории: course_question, technical_issue, escalation. YandexGPT придумывает general_inquiry или complaint. Downstream-логика падает с ошибкой валидации.
- Few-shot — не для всех. Примеры, задающие формат на OpenAI, YandexGPT может интерпретировать иначе. Ответ отличается не только по формату, но и по содержанию.
Что это значит для «переключения одной строкой»
Переключение модели — это не инфраструктурная задача. Это задача качества продукта. За каждой «простой» строкой — проверка промптов, адаптация few-shot, обновление валидации, прогон тестового набора и решение о допустимом уровне деградации.
LangChain прячет разницу между моделями за единым интерфейсом. Но разница никуда не девается — она всплывает в поведении системы. И чем позже вы её обнаружите, тем дороже будет починить.
Абстракция полезна, когда упрощает работу с одинаковыми вещами. Абстракция вредна, когда делает вид, что разные вещи одинаковы.
2. Фоллбек на YandexGPT: инженерия, а не конфиг
OpenAI лег на 20 минут. Или нужен провайдер с серверами в РФ. Логично — фоллбек на YandexGPT. Но это не две строки кода.
Что нужно для честного фоллбека
Для каждого агента фоллбек означает:
- Адаптированный промпт. Не копия, а отдельная версия. Другие формулировки, другие few-shot примеры, иногда — другая структура. И учтите разницу в контекстных окнах: промпт, влезающий в 1M у GPT-5.4, может не влезть в YandexGPT 5 Lite.
- Единая валидация на выходе. Какая бы модель ни отвечала, результат проходит через одну и ту же Pydantic-схему. Модель добавила лишние поля, придумала четвёртую категорию — Pydantic поймает это до отправки пользователю.
- Тестовый набор и порог допуска. 50–100 реальных обращений. Прогоняете через обоих провайдеров. Мой порог — 85%. Ниже — фоллбек не включается. Лучше «сервис временно недоступен», чем неправильный ответ с уверенным лицом.
Protocol-based абстракция
Каждый провайдер реализует один и тот же Protocol — structural subtyping, утиная типизация на уровне типов. Это позволяет подменять реализации без общего базового класса, упрощая тестирование.
Внутри OpenAIProvider и YandexGPTProvider — разные промпты, разный формат запросов, разная авторизация (например, IAM-токены с ротацией у Яндекса). Общее — только контракт на входе и выходе.
LangChain предлагает абстракцию, которая прячет разницу. Я — абстракцию, которая делает её явной и даёт контроль над каждым аспектом.
3. RAG: подключить за две строки, поменять — за две недели
RAG в LangChain выглядит магически:
retriever = vectorstore.as_retriever()
Три строки. Работает. Можно идти пить кофе.
А потом вы решаете что-нибудь поменять.
Ловушка embedding-модели
Вы начали с text-embedding-3-small. OpenAI выпускает новое поколение — точнее, дешевле, лучше. Меняете одну строку в конфиге. Запускаете. Всё работает.
Только поиск теперь возвращает ерунду.
Потому что база знаний проиндексирована старой моделью. Новая генерирует векторы в другом пространстве. Косинусное сходство между вектором запроса (новая модель) и документом (старая модель) — математически бессмысленно.
LangChain молча это проглотит. Никакого предупреждения. Поиск формально работает — просто находит не то. Вы узнаете об этом, когда клиенты начнут жаловаться.
Решение — переиндексация всей базы знаний. Не «одна строка», а операционная задача.
Ловушка chunk_size
Вы начали с чанков по 500 токенов. Потом поняли, что лучше 1000 — контекст теряется при мелкой нарезке. Поменяли параметр, загрузили новые документы. Старые остались с чанками по 500.
В одной коллекции — чанки разного размера, разной нарезки. Поиск работает, но качество — лотерея.
Когда нужно больше, чем get_relevant_documents()
В production нужно понимать, почему вернулся конкретный чанк: с каким score? Какие ещё кандидаты были? Нужно фильтровать по метаданным, обновлять базу без даунтайма.
Всё это можно сделать через ChromaDB напрямую:
Кода больше? Да, на десять строк. Но когда клиент получает неправильный ответ, вы открываете логи и видите всю цепочку: запрос → чанки → scores → промпт → ответ модели. Не чёрный ящик, а прозрачный конвейер.
4. Tool calling: пошли ловить медведя, а она берёт удочку
Tool calling — модель сама решает, какой инструмент использовать. В теории — мощно. На практике — самое хрупкое место.
Классические поломки
- Не тот инструмент. Клиент спрашивает: «Когда у меня следующее занятие?» Это вопрос про данные клиента → нужен CRM. Модель видит «занятие» → идёт в базу знаний → возвращает общее расписание.
- Правильный инструмент, неправильные параметры. Модель вызывает
search_crm, но передаёт имя в поле phone или придумывает несуществующее значение enum. - Отказ использовать инструмент. Модель решает, что знает ответ сама. Не вызывает ни один tool и уверенно галлюцинирует. Это хуже ошибки — это незаметная ошибка.
- Зависимость от модели. Всё вышеперечисленное меняется при смене модели. Флагманская стабильна, nano путается, YandexGPT может игнорировать tool calling целиком.
Медведь, удочка и тихая охота
Вы поправили промпт: «Если клиент спрашивает про своё расписание — используй search_crm». Работает.
Через неделю: «Хочу записаться на тихую охоту». Модель видит «охоту» → вспоминает ваш пример → вызывает search_crm вместо search_knowledge_base. За грибами с ружьём.
Добавляете уточнение. Работает. До следующего edge case. Это не баг промпта. Это фундаментальное свойство: модель принимает решения на основе вероятностей, а не логики. Каждый новый пример сдвигает распределение. Классическая игра в whack-a-mole.
Архитектурный ответ: не доверяй модели критичные решения
В моей системе модель не выбирает инструменты. За маршрутизацию отвечает rule-based классификатор:
- Если есть номер телефона —
search_crm. - Если есть вопрос про расписание —
search_crm. - Если есть упоминание курса, FAQ —
search_knowledge_base.
Это не костыль. Это осознанное архитектурное решение: детерминированная логика там, где нужна предсказуемость, LLM — там, где нужна гибкость и понимание языка.
Rule-based классификатор не перепутает CRM с базой знаний из-за слова «охота». А LLM подключается только для случаев, когда правила не сработали — и даже тогда его ответ проходит валидацию.
5. Security: меньше зависимостей — меньше attack surface
Это может казаться паранойей, пока не посмотришь на CVE:
- CVE-2025-68664 (CVSS 9.3): Уязвимость сериализации в LangChain Core. Через prompt injection можно извлечь API-ключи из переменных окружения.
- CVE-2026-34070 (CVSS 7.5): Path traversal в загрузке промптов. Специально сформированный шаблон даёт доступ к файлам на сервере.
- CVE-2025-67644 (CVSS 7.3): SQL-инъекция в LangGraph через metadata filter keys в SQLite checkpoint.
Три критические уязвимости за полгода. И это только те, что опубликованы.
Дело не в том, что LangChain плохо написан. Дело в принципе: чем больше кода и транзитивных зависимостей, тем больше attack surface.
Мой стек: httpx, chromadb, pydantic. Три зрелые, хорошо проаудированные библиотеки. Input sanitization, prepared statements, PII-маскирование — всё под моим контролем.
6. Что я построил вместо LangChain
Архитектура: никакого фреймворка. FastAPI, нативные SDK провайдеров, ChromaDB напрямую, Bitrix24 API через httpx. Всё на async Python.
Каждый агент — явный контракт
Что на входе — понятно. Что на выходе — понятно. Где может сломаться — понятно. Каждый шаг логируется через structlog с маскированием персональных данных.
Для тестов — MockLLMProvider, MockKnowledgeBase, MockCRMClient. Подменяются через dependency injection, потому что все зависимости — Protocol, а не конкретные классы. Никакого monkey-patching.
Обработка ошибок: иерархия, а не try/except
Таймаут LLM-провайдера — RetryableError, повторяем с экспоненциальным backoff. Невалидный ответ модели после трёх попыток — PipelineError, логируем и эскалируем на человека. Неизвестная ошибка — в dead letter queue, разбираем потом.
170 тестов: unit, integration, e2e, security. 84% покрытие по строкам на ~4500 строк кода (без тестов). CI/CD с security-сканированием (bandit, safety).
Кода больше, чем с LangChain? Да. Но каждая строка — моя, понятная, тестируемая, дебажимая. Когда клиент получает неправильный ответ — я вижу всю цепочку, а не стектрейс из недр RunnableSequence.
7. Когда LangChain всё-таки стоит использовать
Было бы нечестно критиковать и не сказать, когда фреймворк — правильный выбор.
- Прототип, PoC, демо. Нужно за день показать инвестору работающего чат-бота с RAG. LangChain — идеален. Три строки — и у вас есть демка. Проблемы из статьи для прототипа не существуют.
- Одна модель, один провайдер, простой RAG. Если не нужен фоллбек, мультипровайдерность, базу обновляете раз в месяц — LangChain справится. Overhead не помешает.
- Обучение. Для новичка LangChain — отличная точка входа. Он показывает паттерны: цепочки, агенты, RAG. Потом, поняв, как устроено под капотом, решите, нужен ли фреймворк.
Production с мультипровайдерностью, кастомной логикой, требованиями к безопасности — пишите своё. Не потому что это героически, а потому что вам всё равно придётся. LangChain в этом случае не сэкономит время — он его съест на борьбу с абстракциями.
Заключение: три вопроса перед выбором
Три вопроса перед тем, как тащить LangChain (или любой фреймворк-обёртку) в production:
- Сколько провайдеров LLM вам нужно поддерживать? Один — фреймворк ок. Два и больше — он создаёт иллюзию простоты, а реальную работу по адаптации промптов и валидации всё равно придётся делать вручную.
- Как часто вы будете менять компоненты RAG? Embedding-модель, chunk strategy, reranking — если это будет меняться (а оно будет), вам нужен контроль над каждым стыком. Фреймворк его спрячет.
- Что произойдёт, когда модель ответит неправильно? Если это учебный проект — ничего страшного. Если это ответ реальному клиенту — нужна полная прозрачность: от входящего сообщения до ответа модели, с логами каждого шага. Чёрные ящики недопустимы.
Если на все три вопроса ответ указывает на сложность — пишите своё. Не потому что так модно, а потому что production LLM-инженерия — это управление неопределённостью. А управлять ей можно только тем, что видишь и контролируешь.