Основы LangChain: что он решает и когда его можно полностью пропустить.
Узнайте, что на самом деле предлагает LangChain, почему использование инструментов с несколькими поставщиками побуждает команды к его применению, какие пакеты необходимо установить и когда лучше использовать упрощённый обёртывающий слой.
Источники: [Day 11/100] LangChain 101: Why It Exists, and When You Should (and Should Not) Use It
Агент из пятидесяти строк перестаёт быть агентом из пятидесяти строк
Добавление нового инструмента в небольшой ручной агент стоит недорого. Вы пишете функцию, регистрируете её в словаре, описываете в схеме, и сам цикл при этом не меняется. Эта часть работы с агентом кажется приятной, и она вводит людей в заблуждение, заставляя думать, что вся задача останется на таком же маленьком уровне.
Тогда проявляются настоящие требования. Конечная точка OpenAI время от времени возвращает код 503, поэтому необходимы повторные попытки с задержками. Иногда провайдер недоступен в течение двадцати минут, поэтому нужен второй поставщик в качестве резерва. Пользователь, ожидающий восемь секунд до завершения четырех последовательных вызовов, предположит, что приложение зависло, поэтому добавляется потоковая передача токенов. Служба нижнего уровня отклоняет ответ, потому что одно из полей пришло в виде строки вместо числа, поэтому теперь проверяется структурированный вывод. А когда десять человек используют его одновременно, блокирующие вызовы становятся неприемлемыми, поэтому всё должно работать асинхронно.
Всё это создаётся на основе примитивных библиотек openai и requests, и объём кода агента постепенно достигает примерно пятисот строк. Очень небольшая часть этого кода связана с самой работой агента — это обработка JSON, механизм экспоненциального откладывания попыток и преобразование между форматами сообщений, разработанными каждым поставщиком отдельно. Каждая команда вынуждена писать эту «инфраструктуру», и никому не хочется писать её заново.
Именно такую «инфраструктуру» и предлагает продукт LangChain. Ниже описано, чем на самом деле является эта библиотека, какие проблемы она решает, а также, что не менее важно, в каких ситуациях её использование является ошибкой.
Чем является (и чем не является) библиотека
LangChain не запускает вашего агента за вас, не хостит никаких ресурсов и не улучшает способность модели к логическим рассуждениям. Считайте его набором общих интерфейсов вместе с готовыми реализациями тех компонентов приложения на основе LLM, которые иначе пришлось бы писать самостоятельно.
Если вы уже создали простейшего агента, то на самом деле вы уже разработали большинство этих компонентов под другими названиями. Список сообщений, которые передаются между компонентами, соответствует структуре ChatMessageHistory. Схема инструмента превращается в объект Tool или в обычную функцию, оформленную с помощью аннотации @tool. Цикл while, который продолжает вызывать модель до тех пор, пока та не перестанет запрашивать инструменты, — это именно то, что раньше выполнял AgentExecutor. А прямой вызов client.chat.completions.create превращается в объект ChatModel. По сути, здесь нет ничего нового с концептуальной точки зрения. Изменение заключается в том, что теперь у этих компонентов есть общая структура, которую уже используют тысячи других проектов, причем комбинации этих компонентов заранее настроены.
Проблемы, для решения которых он был создан
Начните с описания вызовов инструментов, поскольку это четко показывает всю структуру. OpenAI прикрепляет элемент tool_calls к сообщению ассистента и ожидает получения результата в виде сообщения с атрибутом role: „tool“. Anthropic отправляет блоки с содержимым tool_use и ожидает блоки tool_result в следующем сообщении пользователя. Gemini использует элементы function_call и function_response. Идея одинакова, но три API имеют три разных формата, которые не совместимы между собой.
С одним поставщиком таких проблем не возникает. Когда появляется необходимость во втором поставщике — возможно, из-за сбоев у первого или из-за повышения цен — вам приходится либо писать и поддерживать три адаптера, либо использовать код, который уже с этим справляется. Именно по этой причине и был создан LangChain: единый интерфейс ChatModel, в котором методы .invoke() и .bind_tools() работают одинаково, независимо от того, какая компания производит токены. Переход с ChatOpenAI на ChatAnthropic подразумевает лишь изменение импорта, а не переписывание логики обработки сообщений.
Вторая проблема — структура системы. Передача вводных данных в модель, затем в парсер, а после этого снова в другую модель требует наличия кода-связующего на каждой границе, и этот код необходимо переписывать каждый раз, когда изменяется структура обработки. LangChain унифицирует такие границы. Механизмом этого служит LCEL — язык выражений LangChain, который заслуживает отдельного описания.
Третья проблема — возможность повторного использования. Агенту, выполняющему более сложные задачи, чем просто вызов модели, нужны инструменты, уже созданные кем-то: загрузчики для примерно сорока форматов документов, клиенты для десятка векторных баз данных, политики повторных попыток, функции потоковой обработки, группировки данных и отслеживания операций. Готовое решение по интеграции почти всегда лучше, чем парсер PDF, написанный в пятницу и теперь требующий отладки.
Что вы получаете, не пиша код сами
Единый интерфейс моделей. Классы ChatOpenAI, ChatAnthropic, ChatGoogleGenerativeAI и ChatOllama для локальных моделей все предоставляют методы .invoke(), .stream() и .bind_tools(). Вашему коду больше не важно, кто обучил модель, используемую при вызове.
Композиционные строительные блоки. Благодаря оператору трубопровода LCEL вы можете написать prompt | model | parser, и в результате получите один объект, ведущий себя как каждый из его компонентов. Он может работать в режиме потока, обрабатывать данные пакетами и выполняться асинхронно без дополнительных усилий.
Устойчивость как вызов метода. У любого объекта типа Runnable имеются методы .with_retry() и .with_fallbacks(), поэтому вам не нужно вручную писать циклы повторных попыток или механизмы переключения поставщиков услуг.
Стриминг и асинхронность везде. Каждый объект типа Runnable имеет методы .stream(), .ainvoke(), .astream() и .batch(), даже если изначально обрабатываемый объект не поддерживал их.
Большой каталог интеграций. Загрузчики документов, хранилища векторов, инструменты поиска, выполнение кода, инструменты для работы с базами данных. Многие из них поддерживаются самими поставщиками, и все они используют одинаковый интерфейс Runnable.
Прямой путь к LangGraph. Когда ваш агент нуждается в циклах, сохранении состояния или этапе утверждения человеком, который невозможно реализовать с помощью линейной цепочки операций, управляющий цикл переходит в LangGraph. Он построен на тех же основных абстракциях, поэтому знания, полученные в LangChain, могут быть использованы напрямую.
Когда его можно пропустить
Большинство введений пропускают эту часть, но именно она помогает избежать множества проблем позже.
Один поставщик, один вызов — навсегда. Если вся суть продукта заключается в «отправке этого запроса в GPT-4o и возврате текста», то простая собственная оболочка удобнее для чтения и отладки, чем зависимости, включающие сотни вспомогательных пакетов.
Вам необходимо видеть каждый запрос. Абстракции удобны именно потому, что скрывают детали, но это скрытие становится проблемой, когда что-то ломается. Прослеживание трассы вызовов через шесть уровней оболочек типа Runnable прежде, чем найти настоящую ошибку, — это потеря времени. Команды, желающие просматривать каждую строку кода и HTTP-запрос, часто остаются ближе к исходному SDK.
Пути с критически важной задержкой. Каждый уровень добавляет некоторую нагрузку. При вызове модели, занимающем 500 мс или больше, эта нагрузка обычно является незначительной. Но на критически важных путях, где каждая миллисекунда имеет значение, это иногда бывает не так.
Обновления без фиксации версии. LangChain регулярно внедряет изменения, приводящие к сбоям. Проект, использующий последнюю версию, со временем столкнётся с изменёнными импортами или параметрами по умолчанию. Заблокируйте версию, как и любую другую зависимость, и обновляйтесь тогда, когда сами это решите, а не тогда, когда это сделает pip.
Всё это не означает, что LangChain нельзя использовать. Это означает, что решение следует принимать для каждого проекта осознанно, а не из привычки.
Какой пакет — зачем
LangChain разделён на несколько пакетов, и перед установкой стоит узнать, что делает каждый из них.
langchain-coreсодержит базовые абстракции:Runnable,BaseChatModel,BaseTool, шаблоны запросов и парсеры результатов. У него мало зависимостей, и почти все остальные компоненты строятся на его основе.langchainдобавляет цепочки обработки, агентов и логику поиска данных к основному функционалу, не привязывая пользователя к конкретному поставщику услуг.langchain-communityсобирает интеграции от сторонних разработчиков. Некоторые из них отличного качества, а некоторые заброшены, поскольку любой может внести свой вклад. Перед тем как использовать такую интеграцию в производственных условиях, узнайте, когда в последний раз происходили обновления.- Пакеты-партнеры, такие как
langchain-openai,langchain-anthropicиlangchain-google-genai, поддерживаются параллельно с собственными SDK каждого поставщика. Устанавливайте только те пакеты, которые вы действительно используете. langchain-classic— это место, куда в версии 1 были перемещены конструкторы агентов до версии 1.0:AgentExecutor,create_tool_calling_agentи старыйcreate_react_agent. Существующий код продолжает работать, но новые агенты не следует создавать здесь.langgraph— это отдельный пакет, построенный на основеlangchain-core, который обеспечивает управление процессами, не являющимися линейными. Начиная с версииlangchainv1, рекомендуемый инструмент для создания агентовlangchain.agents.create_agentреализован непосредственно в нём.
Чтобы начать работу, вам понадобятся всего три пакета:
uv add langchain langchain-openai python-dotenv
Замена стандартного клиента на ChatOpenAI
Не ожидайте значительных изменений на этом этапе. Цель — заменить обычный клиент OpenAI на обёртку чат-модели от LangChain и проверить, что вы получите тот же ответ.
from langchain_openai import ChatOpenAI
from dotenv import load_dotenv
load_dotenv()
llm = ChatOpenAI(model="gpt-4o", temperature=0)
response = llm.invoke("What is the weather in Paris?")
print(response.content)
llm.invoke(...) возвращает объект AIMessage, а не простой словарь, но поле .content предоставляет текст точно так же, как это делало .choices[0].message.content. По сути, ChatOpenAI по-прежнему обращается к тому же эндпоинту OpenAI, к которому вы бы обратились вручную, и сама модель совершенно не изменилась. Разница заключается в том, что теперь llm является объектом типа Runnable, поэтому методы .stream(), .batch() и .ainvoke() уже доступны, причем вы сами их не писали.
for chunk in llm.stream("Tell me one fact about Paris."):
print(chunk.content, end="", flush=True)
Запустите его, и токены будут поступать по одному блоку за раз. Вы никогда не писали обработчик потоковой передачи, и этот небольшой цикл практически является основным аргументом в пользу этой библиотеки.
Немного о названиях моделей: в примерах в качестве заменителя используется "gpt-4o". Перечень поставщиков меняется быстрее, чем любой учебник может за это поспеть, поэтому рассматривайте каждую строку вида model="..." как место замены и заменяйте её на название флагманской модели, которую предлагает ваш поставщик при запуске кода. Механизм остается прежним.
Попробуйте это на собственном агенте
- Добавьте
langchainиlangchain-openaiв ваш проект с помощью командыuv add. - В шаге с моделью вашего агента замените прямой вызов
OpenAI()наChatOpenAIи не меняйте остальную часть цикла. Проверьте, что агент ведёт себя точно так же, как раньше. - Вызовите
llm.stream(...)для простого вопроса без использования дополнительных инструментов и выводите каждый фрагмент по мере его получения. Теперь у вас есть возможность потоковой обработки для метода, который вы никогда не реализовывали.
После этого изменения ваш агент не становится умнее. Однако у вас появляется основа, благодаря которой повторные попытки, резервные варианты, потоковая обработка и смена поставщиков обходятся в один вызов метода, вместо того чтобы требовать сотни строк кода.