LangChain

Зачем нужен LangChain | Курс LangChain урок 2

Зачем нужен LangChain | Курс LangChain урок 2
Михаил Омельченко
Автор
Михаил Омельченко
Опубликовано 22.09.2026
0,0
Views 6

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

Необходимые знания:

1) урок 0: окружение собрано, ключ работает, переменные MODEL_NAME и MODEL_BASE_URL заполнены

2) урок 1: токены, контекстное окно, температура, счётчики расхода в usage_metadata

3) представление о запросе по HTTP: метод, заголовки, тело в формате JSON

4) Python на уровне джуниора: словари, списки, обработка исключений

Ключевые концепции:

1) что слой над API провайдера даёт и чем вы за него платите

2) единый интерфейс модели: invoke, stream, batch

3) четыре пакета под вашим кодом и зачем их четыре

4) langchain-classic, куда уехали LLMChain и остальные цепочки

5) langchain-community и почему он больше не поддерживается

6) LCEL: где он остался и почему курс на нём не стоит

7) create_agent одним взглядом


Зачем вообще нужен слой между вами и моделью

Вызов модели, это запрос по HTTP к чужому серверу. Есть адрес, есть ключ в заголовке, есть тело в формате JSON. Стандартная библиотека Python умеет такое без единой зависимости. Зачем ставить фреймворк ради одного запроса?

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

Начну с рук.

Пример 01_raw_http.py

import json
import os
import urllib.request

from dotenv import load_dotenv

load_dotenv()

base_url = os.getenv("MODEL_BASE_URL")

if not base_url:
    print("MODEL_BASE_URL пуст.")
    print("Этот пример показывает голый вызов адреса, совместимого с OpenAI.")
    print("Впишите адрес своего провайдера в .env и запустите снова.")
    raise SystemExit(0)

url = base_url.rstrip("/") + "/chat/completions"

payload = {
    "model": os.environ["MODEL_NAME"],
    "messages": [
        {"role": "user", "content": "Ответьте одним словом: столица Франции"}
    ],
    "temperature": 0,
}

request = urllib.request.Request(
    url,
    data=json.dumps(payload).encode("utf-8"),
    headers={
        "Content-Type": "application/json",
        "Authorization": "Bearer " + os.environ["OPENAI_API_KEY"],
    },
    method="POST",
)

with urllib.request.urlopen(request, timeout=60) as connection:
    body = json.loads(connection.read().decode("utf-8"))

choice = body["choices"][0]

print("КЛЮЧИ ОТВЕТА:", sorted(body))
print("ТЕКСТ:", choice["message"]["content"])
print("ПРИЧИНА ОСТАНОВКИ:", choice.get("finish_reason"))
print("РАСХОД:", body.get("usage"))

# Вывод:
# КЛЮЧИ ОТВЕТА: ['choices', 'created', 'id', 'model', 'object', 'system_fingerprint', 'usage']
# ТЕКСТ: Париж
# ПРИЧИНА ОСТАНОВКИ: stop
# РАСХОД: {'prompt_tokens': 15, 'completion_tokens': 29, 'total_tokens': 44, 'cost': 0.001168}

Ни одной зависимости, кроме python-dotenv, который нужен здесь только чтобы прочитать ваш файл .env. Всё остальное, это стандартная библиотека Python. Запрос при этом настоящий и оплачивается по токенам, как любой другой.

Метод POST, путь /chat/completions, заголовки Content-Type: application/json и Authorization: Bearer <ключ>, обязательные поля тела model и messages, поля ответа choices и usage, всё это лежит в справочнике API провайдера. Формат у DeepSeek совместим с форматом OpenAI, и потому тот же код работает с любым адресом, совместимым с OpenAI Chat Completions API. На этой совместимости стоит и параметр base_url, которым вы пользуетесь с урока 0.

Ветка if not base_url нужна тем, кто работает с провайдером напрямую и оставил MODEL_BASE_URL пустым. Собрать голый запрос к произвольному провайдеру по одному имени модели нельзя. У каждого свой адрес, и брать его нужно из его документации, а не из этого урока.

Теперь то же самое через фреймворк.

Пример 02_langchain_call.py

from course_model import build_model

model = build_model(temperature=0)

response = model.invoke("Ответьте одним словом: столица Франции")

print("ТИП ОТВЕТА:", type(response).__name__)
print("ТЕКСТ:", response.text)
print("КЛЮЧИ response_metadata:", sorted(response.response_metadata))
print("РАСХОД:", response.usage_metadata)

# Вывод:
# ТИП ОТВЕТА: AIMessage
# ТЕКСТ: Париж
# КЛЮЧИ response_metadata: ['finish_reason', 'id', 'logprobs', 'model_name', 'model_provider', 'system_fingerprint', 'token_usage']
# РАСХОД: {'input_tokens': 15, 'output_tokens': 27, 'total_tokens': 42, 'input_token_details': {}, 'output_token_details': {}}

Функция build_model находится в отдельном модуле course_model.py, это тот же модуль, что в уроке 1. Положите его рядом с примерами урока 2, и дальше каждый пример вызывает сборку одной строкой. Вот он целиком, чтобы вам не пришлось его искать:

import os

from dotenv import load_dotenv
from langchain.chat_models import init_chat_model

load_dotenv()


def build_model(**kwargs):
    """Собирает модель курса.

    Все именованные аргументы уходят в init_chat_model как есть: temperature,
    max_tokens, model_kwargs и прочее из раздела Parameters.
    """
    model_name = os.environ["MODEL_NAME"]
    base_url = os.getenv("MODEL_BASE_URL")

    if base_url:
        # Путь для любого адреса, совместимого с OpenAI Chat Completions API.
        return init_chat_model(
            model=model_name,
            model_provider="openai",
            base_url=base_url,
            api_key=os.environ["OPENAI_API_KEY"],
            **kwargs,
        )

    # Путь напрямую к провайдеру.
    return init_chat_model(model_name, **kwargs)

Что видно в этих двух выводах

Сорок строк против семи, и четыре из них, это печать. Но сорок строк пишутся один раз и прячутся в свою функцию, поэтому смотреть надо не туда.

Первое. Имена счётчиков расхода разные. В голом ответе провайдера они называются prompt_tokens и completion_tokens. В usage_metadata те же величины называются input_tokens и output_tokens, и так они называются у любого провайдера, потому что это стандартное поле сообщения LangChain. Ваш счётчик денег из урока 1 переживёт смену провайдера, а код, который лезет в prompt_tokens, придётся переписывать.

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

Второе. Ответ, это объект, а не словарь. В голом варианте вы достаёте текст выражением body["choices"][0]["message"]["content"] и обязаны помнить эту структуру наизусть. Во втором варианте есть response.text, response.usage_metadata и response.response_metadata, куда складывается всё, что провайдер прислал сверх стандарта. Разница не в удобстве записи, а в том, что первая форма привязывает ваш код к схеме конкретного провайдера.

Третье. Смена провайдера. Первый файл написан под адрес, совместимый с OpenAI. Чтобы позвать Anthropic или Gemini, его придётся переписать целиком: другой путь, другие заголовки, другая структура тела и ответа. Второй файл меняется в одной строке, и это главное обещание фреймворка, которое вы проверяли в уроке 0.

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

Чем вы платите за этот слой

1) Зависимость и её вес. Вместо одной библиотеки для HTTP у вас в проекте пакет, его ядро, пакет интеграции и среда исполнения агента. Обновления приходят часто: патчи выходят до нескольких раз в неделю

2) Чужая абстракция между вами и ошибкой. Когда провайдер отвечает не то, вы смотрите не в свой словарь, а в объект фреймворка, а иногда и в его исходники

3) Отставание от новинок провайдера. Стандартный интерфейс покрывает то, что есть у всех. Нестандартные поля стороннего провайдера через ChatOpenAI не извлекаются и не сохраняются, и лечится это пакетом интеграции нужного провайдера или маршрутизатором. Иллюстрация лежит в выводе двух примеров выше: шлюз вернул в usage поле cost со стоимостью запроса, а в usage_metadata этого поля нет, стандартный счётчик такого не предусматривает. Ни единицы измерения, ни тариф этого числа шлюз не объявляет, поэтому строить на нём учёт нельзя. Деньги считаются по ставкам того, кому вы платите, а механика счёта разобрана в уроке 1

4) Скорость изменений самого фреймворка. Об этом вся вторая половина урока

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

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

С другой стороны к тому же вопросу подходят через выбор уровня.

1) фреймворк (LangChain), когда нужны стандартные абстракции для моделей, инструментов и цикла агента, нужен быстрый старт и общий способ для команды делать приложения

2) среда исполнения (LangGraph), когда нужен низкоуровневый контроль над оркестрацией, устойчивое выполнение и долгие процессы с состоянием

3) готовый харнесс (Deep Agents), когда нужен более автономный агент с уже собранными планированием, файловой системой и субагентами

Соседи по полке: Vercel AI SDK, CrewAI, OpenAI Agents SDK, Google ADK, LlamaIndex. Это ответ на вопрос, единственный ли это выбор. Не единственный.

Что именно стандартизировано

Каждая тема разбирается в курсе.

Что стандартизировано Где в курсе
Три метода вызова: invoke, stream, batch этот урок, дальше урок 7
Роли и типы сообщений, контент-блоки урок 3
Счётчики токенов в usage_metadata уроки 1 и 4
Вызов инструментов: bind_tools, tool_calls уроки 9 и 10
Вывод по схеме и две стратегии его получения урок 6
Типы исключений и повторы при сбое сети урок 0, отказоустойчивость целиком в уроке 23 (выйдет позже)
Ограничитель частоты запросов урок 4

Три метода вызова стоит увидеть сразу, потому что дальше в курсе они встречаются постоянно и всегда у одного и того же объекта модели.

Пример 03_standard_interface.py

from course_model import build_model

model = build_model(temperature=0)

# 1. invoke: список сообщений с ролями вместо одной строки.
conversation = [
    {"role": "system", "content": "Вы переводите с русского на французский. Отвечайте только переводом."},
    {"role": "user", "content": "Переведите: я люблю программировать."},
    {"role": "assistant", "content": "J'adore programmer."},
    {"role": "user", "content": "Переведите: я люблю собирать приложения."},
]

print("INVOKE:", model.invoke(conversation).text)
print()

# 2. batch: три независимых запроса уходят параллельно, порядок сохраняется.
questions = [
    "Одним предложением: зачем нужен HTTP?",
    "Одним предложением: зачем нужен DNS?",
    "Одним предложением: зачем нужен TLS?",
]

print("BATCH:")
for question, answer in zip(questions, model.batch(questions)):
    print(f"{question}")
    print(f"{answer.text}")
print()

# 3. stream: тот же объект отдаёт ответ кусками по мере генерации.
print("STREAM:")
for chunk in model.stream("Одним предложением: зачем нужен фреймворк?"):
    print(chunk.text, end="|", flush=True)
print()

# Вывод:
# INVOKE: J'adore créer des applications.
#
# BATCH:
# Одним предложением: зачем нужен HTTP?
# HTTP нужен для стандартизированной передачи гипертекстовых документов и других данных между клиентами и серверами в сети.
# Одним предложением: зачем нужен DNS?
# DNS нужен для преобразования удобных для человека доменных имен в числовые IP-адреса, чтобы компьютеры могли находить и связываться друг с другом в интернете.
# Одним предложением: зачем нужен TLS?
# TLS нужен для обеспечения безопасного, зашифрованного соединения между клиентом и сервером в интернете, защищая передаваемые данные от перехвата и подмены.
#
# STREAM:
# ||||||||||||||||||||||||||||||||||||||||||||||||||Фрейм|ворк нужен,| чтобы ускорить| и упростить| разработку, предоставляя готов|ую структуру, инстру|менты и стандартные| решения типовых| задач вместо написания всего| кода с нуля.|||||

Три вещи в нём стоит отметить отдельно.

Список сообщений вместо строки. Модель принимает и одну строку, и список сообщений с ролями, причём роли можно задавать обычными словарями. Это тот самый формат, который провайдер получил в теле запроса в первом примере, только теперь его собирает не ваш код. Роли, типы сообщений и то, почему assistant в списке пишете вы сами, это тема урока 3.

batch это параллель на вашей стороне. К пакетным API провайдеров этот метод отношения не имеет: он распараллеливает вызовы на стороне клиента. Скидки провайдера за пакетную обработку вы через него не получите, а вот три запроса вместо трёх последовательных ожиданий получите. Число одновременных вызовов ограничивается атрибутом max_concurrency в конфигурации вызова. А если нужны результаты по мере готовности, а не все разом, есть batch_as_completed, который отдаёт их в произвольном порядке вместе с индексом входа.

stream отдаёт частями. У каждой части есть атрибут text, и в этом примере он печатается через разделитель, чтобы границы частей были видны. Что делать с потоком в интерфейсе и где он ломает вашу логику обработки, это урок 7.

И сразу про частокол разделителей. Куски с пустым полем text идут и в начале вывода, и в конце: между разделителями ничего не печатается, потому что печатать нечего. Ни один символ ответа при этом не потерян. Сколько кусков придёт у вас и сколько из них будут пустыми, решает провайдер. На одном и том же запросе разбивка от прогона к прогону разная, и в выводе выше она своя. Разбор потока целиком, это урок 7. Если поток идёт пользователю, пустые куски отбрасываются проверкой if chunk.text.

Четыре пакета под вашим кодом

Слово LangChain обозначает не один пакет, а набор, экосистему из компонентных пакетов.

Пакет Что в нём Где вы его встретите в курсе
langchain-core базовые абстракции: сообщения и контент-блоки, инструменты, конфигурация вызова импорты вида langchain_core.*, уроки 3, 10, 12
langchain-openai и другие пакеты интеграций реализация стандартного интерфейса для конкретного провайдера ChatOpenAI из урока 0
langchain create_agent плюс реэкспорт ядра под коротким именем почти каждый урок
langgraph среда исполнения: устойчивое выполнение, стриминг, пауза на человеке, хранение состояния уроки 12 и 13, а уроки 23 и 24 выйдут позже

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

Цена: чем выше уровень, тем меньше кода и тем меньше контроля.

Отдельно про langchain и langchain-core. Большая часть того, что вы импортируете из langchain, физически лежит в langchain-core и оттуда реэкспортируется. Практический вывод: если увидите в чужом коде from langchain_core.messages import HumanMessage вместо from langchain.messages import HumanMessage, это не ошибка и не устаревший код, это тот же класс, взятый из слоя ниже.

Хроника: как фреймворк пришёл к текущей версии

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

Когда Что произошло
24.10.2022 вышел пакет версии 0.0.1. В нём две вещи: абстракции над моделями и цепочки, заранее заданные шаги вычисления под типовые задачи. Имя LangChain, это Language плюс Chains
12.2022 первые агенты общего назначения, по статье ReAct. Модель порождала JSON, который представлял вызов инструмента, а фреймворк его разбирал
01.2023 OpenAI выпускает Chat Completion API: на входе не строка, а список сообщений, на выходе сообщение. Остальные провайдеры повторяют, фреймворк переходит на списки сообщений
03.2023 у OpenAI появляется вызов функций. Это становится предпочтительным способом вызова инструментов вместо разбора JSON
01.2024 версия 0.1.0, первый выпуск не из ветки 0.0.x. Отрасль переходит от прототипов к продакшену, фреймворк усиливает внимание к стабильности
02.2024 выходит LangGraph. Причина: в исходном фреймворке не хватало низкоуровневого слоя оркестрации, который даёт разработчику контроль над точным ходом агента
06.2024 интеграций больше семисот. Их выносят из основного пакета: крупные в отдельные пакеты, остальные в langchain-community
10.2024 LangGraph объявлен предпочтительным способом строить любое приложение сложнее одного вызова модели. Большинство цепочек и агентов помечены устаревшими, к ним выпущены руководства по переходу
04.2025 модели становятся мультимодальными, формат сообщений в ядре меняется под файлы, картинки и видео
20.10.2025 версия 1.0.0. Все цепочки и агенты заменены одной абстракцией верхнего уровня, агентом. Старое уезжает в langchain-classic. Появляется стандартный формат содержимого сообщения, контент-блоки
15.03.2026 выходит Deep Agents, готовый харнесс поверх LangGraph

Минорные выпуски ветки 1.x шли так. Версия 1.1.0 от 25.11.2025 принесла профили моделей и средний слой повтора вызова модели. Версия 1.2.0 от 15.12.2025 добавила поддержку особенностей инструментов у конкретных провайдеров. Версия 1.3.0 от 12.05.2026 добавила третью версию потока событий. Версия 1.4.0 от 01.09.2026 внесла поддержку MCP внутрь пакета, в пространство имён langchain.mcp, вместо отдельного langchain-mcp-adapters. На этой ветке и стоит курс.

Теперь посчитайте сами. Статья, написанная в 2023 году, застала фреймворк до перехода на вызов функций. Статья первой половины 2024 года застала цепочки живыми и рекомендованными, а написанная в конце того же года, уже помеченными устаревшими. Статья до октября 2025 года описывает пакет, у которого другое пространство имён. Без этой хроники чужой код читается как набор случайных ошибок.

Куда уехали LLMChain и остальные цепочки

Вот кусок из статьи 2024 года. Это код прошлой версии, он приведён как иллюстрация, среди примеров урока его нет и запускать его не нужно.

# КОД ПРОШЛОЙ ВЕРСИИ, НЕ ЗАПУСКАЕТСЯ НА ТЕКУЩЕЙ СБОРКЕ
from langchain.chains import LLMChain
from langchain_core.prompts import PromptTemplate

prompt = PromptTemplate.from_template("Переведи на французский: {text}")
chain = LLMChain(llm=llm, prompt=prompt)

result = chain.run(text="я люблю программировать")

Первая строка в текущей версии не сработает. Пространство имён пакета langchain в версии 1 сокращено, и модуля chains среди оставшегося нет.

Модуль Что доступно Откуда взято
langchain.agents create_agent, AgentState своё, создание агента
langchain.messages типы сообщений, контент-блоки, trim_messages реэкспорт из langchain-core
langchain.tools @tool, BaseTool, помощники инъекции реэкспорт из langchain-core
langchain.chat_models init_chat_model, BaseChatModel единая инициализация модели
langchain.embeddings init_embeddings, Embeddings модели эмбеддингов

Столько перечисляет руководство по переходу. В самом пакете есть ещё два адреса. Оба вам понадобятся: langchain.rate_limiters с ограничителем частоты, это урок 4, и langchain.agents.middleware это уроки 15 и 16. Строка from langchain.rate_limiters import InMemoryRateLimiter стоит на странице моделей в разделе про ограничение частоты. А импорт из langchain.agents.middleware встречается на семнадцати страницах раздела LangChain. Проверяется это импортом, чем вы и займётесь ниже.

Всё остальное, что было в пакете раньше, уехало в langchain-classic, который ставится отдельно. Вот что там теперь лежит:

1) устаревшие цепочки: LLMChain, ConversationChain и прочие

2) ретриверы, например MultiQueryRetriever, и всё, что раньше лежало в langchain.retrievers

3) API индексации

4) модуль hub для работы с промптами через сервис

5) модули эмбеддингов, например CacheBackedEmbeddings

6) реэкспорты пакета langchain-community

7) прочая устаревшая функциональность

Переход выглядит как замена имени пакета в строке импорта: from langchain.chains import LLMChain превращается в from langchain_classic.chains import LLMChain, а from langchain import hub в from langchain_classic import hub.

Учиться ли по такому коду, документация не говорит. Установить langchain-classic и поменять строку импорта, это способ не чинить старый код прямо сейчас. Способ рабочий, и авторы его дали намеренно. Пакет предназначен тем, кто продолжает пользоваться старыми цепочками и не хочет переходить, хотя переходить рекомендуют. Но учиться на этом коде не стоит, и вот почему. Документация вокруг него больше не пишется, примеры в ней собраны на другой абстракции. И любой ваш вопрос по цепочкам упрётся в архив версии 0.3.

Как выглядит замена без пакета совместимости, вы уже видели. Цепочка из примера выше делала три вещи: подставляла текст в шаблон, звала модель и доставала строку из ответа. Первое делает форматирование строк в Python, второе делает model.invoke, третье делает атрибут text. Курс идёт этим путём, и урок 5 показывает, что стало с шаблонами промптов и почему их место занял системный промпт агента.

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

Проверьте свои импорты

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

Пример 04_old_imports.py

import importlib

# (модуль, имя внутри модуля или None, как строка импорта выглядит в статье)
V0_IMPORTS = [
    ("langchain.chains", "LLMChain", "from langchain.chains import LLMChain"),
    ("langchain", "hub", "from langchain import hub"),
    ("langchain.retrievers", "MultiQueryRetriever",
     "from langchain.retrievers import MultiQueryRetriever"),
    ("langchain.indexes", None, "import langchain.indexes"),
    ("langgraph.prebuilt", "create_react_agent",
     "from langgraph.prebuilt import create_react_agent"),
    ("langchain_core.prompts", "ChatPromptTemplate",
     "from langchain_core.prompts import ChatPromptTemplate"),
    ("langchain_core.output_parsers", "StrOutputParser",
     "from langchain_core.output_parsers import StrOutputParser"),
    ("langchain_classic.chains", "LLMChain",
     "from langchain_classic.chains import LLMChain"),
    ("langchain_community.document_loaders", "TextLoader",
     "from langchain_community.document_loaders import TextLoader"),
]

V1_IMPORTS = [
    ("langchain.agents", "create_agent",
     "from langchain.agents import create_agent"),
    ("langchain.chat_models", "init_chat_model",
     "from langchain.chat_models import init_chat_model"),
    ("langchain.messages", "HumanMessage",
     "from langchain.messages import HumanMessage"),
    ("langchain.tools", "tool", "from langchain.tools import tool"),
    ("langchain.embeddings", "init_embeddings",
     "from langchain.embeddings import init_embeddings"),
    ("langchain.rate_limiters", "InMemoryRateLimiter",
     "from langchain.rate_limiters import InMemoryRateLimiter"),
    ("langchain.agents.middleware", None, "import langchain.agents.middleware"),
]


def check(module_name, attribute):
    """Возвращает пару: получилось или нет, и причина отказа."""
    try:
        module = importlib.import_module(module_name)
    except Exception as error:
        return False, f"{type(error).__name__}: {error}"

    if attribute is None:
        return True, ""

    if not hasattr(module, attribute):
        return False, f"AttributeError: в модуле {module_name} нет имени {attribute}"

    return True, ""


def report(title, imports):
    print(title)
    for module_name, attribute, line in imports:
        works, reason = check(module_name, attribute)
        status = "ЕСТЬ" if works else "НЕТ"
        print(f"{status:<5} {line}")
        if reason:
            print(f"{reason}")
    print()


report("СТРОКИ ИЗ СТАТЕЙ ПРО ВЕРСИЮ 0.x", V0_IMPORTS)
report("СТРОКИ ТЕКУЩЕЙ ВЕРСИИ", V1_IMPORTS)

# Вывод:
# СТРОКИ ИЗ СТАТЕЙ ПРО ВЕРСИЮ 0.x
# НЕТ   from langchain.chains import LLMChain
# ModuleNotFoundError: No module named 'langchain.chains'
# НЕТ   from langchain import hub
# AttributeError: в модуле langchain нет имени hub
# НЕТ   from langchain.retrievers import MultiQueryRetriever
# ModuleNotFoundError: No module named 'langchain.retrievers'
# НЕТ   import langchain.indexes
# ModuleNotFoundError: No module named 'langchain.indexes'
# ЕСТЬ  from langgraph.prebuilt import create_react_agent
# ЕСТЬ  from langchain_core.prompts import ChatPromptTemplate
# ЕСТЬ  from langchain_core.output_parsers import StrOutputParser
# НЕТ   from langchain_classic.chains import LLMChain
# ModuleNotFoundError: No module named 'langchain_classic'
# НЕТ   from langchain_community.document_loaders import TextLoader
# ModuleNotFoundError: No module named 'langchain_community'
#
# СТРОКИ ТЕКУЩЕЙ ВЕРСИИ
# ЕСТЬ  from langchain.agents import create_agent
# ЕСТЬ  from langchain.chat_models import init_chat_model
# ЕСТЬ  from langchain.messages import HumanMessage
# ЕСТЬ  from langchain.tools import tool
# ЕСТЬ  from langchain.embeddings import init_embeddings
# ЕСТЬ  from langchain.rate_limiters import InMemoryRateLimiter
# ЕСТЬ  import langchain.agents.middleware

Последние две строки списка текущей версии, это те самые два адреса, которых нет в таблице руководства по переходу. Здесь и видно, что таблица неполна, а пакет знает больше, чем она перечисляет.

Два предупреждения к скрипту.

Первое: часть строк не пройдёт не потому, что имя устарело, а потому, что пакета нет в окружении курса. Ни langchain-classic, ни langchain-community курс не ставит, и отказ на них, это ожидаемый результат, а не ошибка. Отличить один случай от другого можно по названию ошибки, скрипт её печатает.

Второе: словом "ЕСТЬ" скрипт говорит только то, что имя импортируется. Он не обещает, что вызов с прежними аргументами отработает как раньше. Пример из соседнего раздела: параметр prompt у агента переименован, и импорт этого не покажет.

Почему langchain-community теперь реэкспорт

В июне 2024 года интеграций стало больше семисот, их вынесли из основного пакета. Крупные разъехались по своим пакетам, а остальные собрались в langchain-community.

1) реэкспорты langchain-community перечислены в списке того, что переехало в langchain-classic. То есть импорт через основной пакет больше не работает

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

Если вам нужна интеграция и вы нашли её только в langchain-community, у вас три варианта. Взять отдельный пакет интеграции этого провайдера, если он есть. Поставить langchain-community и принять, что код не поддерживается. Написать свой тонкий слой поверх API сервиса. Для моделей первый вариант почти всегда доступен: у каждого крупного провайдера свой пакет.

Что стало с LCEL

Если вы проходили курс по LangChain до 2026 года, вы почти наверняка учили LCEL, LangChain Expression Language. Это способ собирать цепочку оператором |, где выход одного шага становится входом следующего.

Вот как такая цепочка выглядит. Это иллюстрация, среди примеров урока её нет.

# ИЛЛЮСТРАЦИЯ. Так пишут цепочку оператором |
from langchain_core.output_parsers import StrOutputParser
from langchain_core.prompts import ChatPromptTemplate

prompt = ChatPromptTemplate.from_messages([
    ("system", "You are a helpful assistant that translates {input_language} to {output_language}."),
    ("human", "{text}"),
])

chain = prompt | model | StrOutputParser()

result = chain.invoke({
    "input_language": "English",
    "output_language": "Spanish",
    "text": "Hello, how are you?",
})

Первое. В разделе ядра LCEL нет. Основной раздел документации по LangChain состоит из восьми страниц. Это агенты, модели, сообщения, инструменты, короткая память, поток событий, стриминг, вывод по схеме. Страницы про LCEL среди них нет, как нет её и в разделах для продвинутого использования.

Второе. Упоминания остались только по краям. LCEL встречается на страницах интеграций отдельных провайдеров и в разделе про сервис наблюдаемости. Там он упомянут как то, с чего люди мигрируют. В ядре, в разделе про агентов, в руководстве по переходу на текущую версию его нет.

Третье. Из кода он никуда не делся. Оператор | остался в langchain-core, и авторы сами им пользуются. В рецепте голосового агента три шага конвейера соединены именно так. А в пояснении сказано, что это внутренняя абстракция фреймворка для передачи потока между компонентами. Рядом лежит и RunnableConfig, конфигурация вызова, которую вы встретите в уроках 10 и 12 в виде словаря с ключом configurable.

У владельца старого кода вопрос: сломается или нет.

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

Два моих прежних курса по LangChain были построены вокруг LCEL как центральной идеи фреймворка. Оба устарели.

Куда идёт курс: агент одним вызовом

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

Агент это модель, вызывающая инструменты в цикле, пока задача не решена. Всё вокруг этого цикла, промпт, инструменты и middleware, влияющие на поведение, называется харнессом.

Пример 05_first_agent.py

from langchain.agents import create_agent

from course_model import build_model


def get_weather(city: str) -> str:
    """Возвращает погоду в указанном городе."""
    return f"В городе {city} всегда солнечно!"


agent = create_agent(
    model=build_model(temperature=0),
    tools=[get_weather],
    system_prompt="Вы помощник. Отвечайте коротко.",
)

result = agent.invoke(
    {"messages": [{"role": "user", "content": "Какая погода в Краснодаре?"}]}
)

print("ЧТО ЛЕЖИТ В СОСТОЯНИИ:", sorted(result))
print()

for message in result["messages"]:
    print(f"{type(message).__name__}: {message.content!r}")

print()
print("ОТВЕТ:", result["messages"][-1].text)

# Вывод:
# ЧТО ЛЕЖИТ В СОСТОЯНИИ: ['messages']
#
# HumanMessage: 'Какая погода в Краснодаре?'
# AIMessage: ''
# ToolMessage: 'В городе Краснодар всегда солнечно!'
# AIMessage: 'В Краснодаре всегда солнечно! ☀️'
#
# ОТВЕТ: В Краснодаре всегда солнечно! ☀️

Строка документации функции уходит модели как описание инструмента, поэтому она написана на языке задачи. Разбор того, как инструмент попадает в запрос, идёт в уроке 9.

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

Одна строка вывода выглядит ошибкой, и это не она. У первого AIMessage в содержимом либо пусто, либо один пробел. Словами модель в этом ходе ничего не писала, она просила вызвать инструмент. Сама просьба лежит в отдельном поле сообщения, tool_calls, а печатается content, вот и строка без слов. Как устроено сообщение с вызовом инструмента, разберу в уроке 3, а что с ним делает агент, в уроке 9.

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

1) модели можно передать строку с именем или готовый объект модели. Здесь передаётся объект, собранный функцией build_model, потому что курс работает через адрес, совместимый с OpenAI

2) инструментом стала обычная функция Python с аннотациями типов и строкой документации, никакого декоратора не понадобилось. Почему строка документации важнее имени функции, разберу в уроке 9

3) invoke у агента принимает не строку, а словарь с ключом messages, и возвращает не сообщение, а состояние. Это уже LangGraph под капотом, и я приоткрою его в уроках 11 и 12

Распространённые ошибки

Ошибка 1: ставят langchain и импортируют цепочку

# Неправильно: модуля chains в пакете больше нет
from langchain.chains import LLMChain

Что происходит: импорт падает, до всякой сети и до всяких денег.

Почему так: пространство имён пакета сокращено, модуля chains в нём нет, а цепочки переехали в langchain-classic.

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

# Правильно для нового кода
from course_model import build_model

model = build_model(temperature=0)
response = model.invoke("Переведите на французский: я люблю программировать")

# Правильно для старого проекта, который пока не переписывают
# pip install langchain-classic
from langchain_classic.chains import LLMChain

Ошибка 2: берут агента из статьи прошлого года

# Неправильно: два изменения в трёх строках
from langgraph.prebuilt import create_react_agent

agent = create_react_agent(
    model="claude-sonnet-4-6",
    tools=[check_weather],
    prompt="You are a helpful assistant",
)

Что происходит: зависит от вашего окружения, и падения может не быть вовсе. Пакет langgraph-prebuilt приезжает вместе с агентом как зависимость, поэтому строка импорта может и отработать. Что именно случится у вас, покажет диагностика из примера 4, там эта строка проверяется отдельным пунктом.

Почему так: изменилось два имени. Функция переехала из langgraph.prebuilt в langchain.agents и называется теперь create_agent, а параметр prompt переименован в system_prompt. Оба изменения надо внести вместе.

# Правильно
from langchain.agents import create_agent

agent = create_agent(
    model="claude-sonnet-4-6",
    tools=[check_weather],
    system_prompt="You are a helpful assistant",
)

Ещё одна деталь из той же области, чтобы вы не запутались в противоречивых советах. Руководство по переходу говорит: если в старом коде в промпт передавался объект SystemMessage, возьмите из него строку. При этом в версии 1.1.0 поддержку SystemMessage в параметре system_prompt вернули.

Ошибка 3: ставят langchain-community ради одной интеграции

# Рискованно: пакет больше не поддерживается
from langchain_community.chat_models import ChatSomething

Что происходит: код может работать, а может отказать после обновления соседнего пакета, и чинить это будет некому.

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

Как правильно: сначала искать отдельный пакет интеграции провайдера, они есть у всех крупных. Если нужен именно реэкспорт из langchain-community, он теперь идёт через langchain-classic.

Ошибка 4: ждут от агента строку

# Неправильно: агент возвращает состояние, а не текст
result = agent.invoke({"messages": [{"role": "user", "content": "..."}]})
print(result.upper())   # AttributeError: 'dict' object has no attribute 'upper'

Что происходит: invoke у агента возвращает словарь состояния, где под ключом messages лежит вся история хода, включая вызовы инструментов.

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

# Правильно
result = agent.invoke({"messages": [{"role": "user", "content": "..."}]})

print(result["messages"][-1].text)             # текст последнего сообщения
print(result["messages"][-1].content_blocks)   # контент-блоки

Практическое задание

Вам достался фрагмент из статьи 2024 года. Он не запускается, и ваша задача не починить его, а перевести на текущую версию.

# КОД ПРОШЛОЙ ВЕРСИИ, ЕГО НАДО ПЕРЕПИСАТЬ
from langchain.chains import LLMChain
from langchain_core.prompts import PromptTemplate

prompt = PromptTemplate.from_template(
    "Переведи на {language}: {text}"
)
chain = LLMChain(llm=llm, prompt=prompt)

print(chain.run(language="французский", text="я люблю программировать"))

Требования:

1) перепишите фрагмент без установки langchain-classic и без единой цепочки

2) модель берите из course_model.py, как в примерах урока

3) подстановку значений в шаблон сделайте средствами Python, форматированием строки

4) напечатайте текст ответа и словарь usage_metadata

5) допишите в 04_old_imports.py три строки импорта из статьи, которую вы читали до курса, или из своего старого проекта, и прогоните диагностику

Как проверить результат:

1) скрипт печатает перевод и словарь с числами расхода, при этом langchain-classic в окружении отсутствует

2) для каждой из трёх ваших строк диагностика печатает ЕСТЬ или НЕТ, а при отказе ещё и название ошибки

3) на месте StrOutputParser из старого кода в новом нет ничего: текст берётся атрибутом text у ответа

Подсказка: системную часть промпта можно передать первым сообщением списка, как в примере 3 этого урока.

Итоги урока

Теперь вы знаете, за что платите зависимостью. Фреймворк даёт единый интерфейс поверх разных API, нормализованные счётчики токенов, типизированный ответ и три способа вызова. Даёт общий язык для инструментов, вывода по схеме и обработки сбоев. Взамен вы получаете четыре пакета в проекте, чужую абстракцию между собой и ошибкой провайдера и обязанность следить за версиями. На узкой задаче с одним провайдером голый запрос по HTTP уместнее.

И вы знаете, что случилось с версией 0. Цепочки, ретриверы, индексация и модуль работы с промптами через сервис уехали в langchain-classic. Пространство имён основного пакета сократилось до пяти модулей, названных в руководстве по переходу, плюс ограничитель частоты и средние слои агента. Пакет langchain-community больше не поддерживается, а его реэкспорты собраны в том же пакете совместимости. LCEL из ядра документации ушёл, хотя оператор | в коде остался и встречается на страницах интеграций. Агент собирается одним вызовом create_agent, и это единственная абстракция верхнего уровня, которую фреймворк теперь предлагает.

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

В уроке 3, "Сообщения и контент-блоки", разберу это устройство: какие бывают роли и типы сообщений и почему содержимое стало списком блоков вместо строки. Там же, где лежат токены рассуждения, как отправить модели картинку и что происходит с сообщениями при сохранении.

Код урока

Примеры этого урока лежат в репозитории курса, папка lesson_02. Закреплённые версии, на которых получен вывод в тексте, лежат в requirements.txt в корне репозитория.


Предыдущий урок: Как устроена языковая модель

Следующий урок: Сообщения и контент-блоки ---

Подписывайтесь на мой Telegram канал

Если вам нужен ментор и вы хотите научиться разрабатывать AI агентов, пишите, обсудим условия

Авторизуйтесь, чтобы оставить комментарий.

Комментариев: 0

Нет комментариев.

Тут может быть ваша реклама

Пишите info@aisferaic.ru

Похожие туториалы