LangChain, RAG и LangGraph: от основ до продакшена

Курс о проектировании LLM-приложений на базе LangChain и LangGraph: от промптов и структурированного вывода до RAG, памяти и агентных архитектур. Разберём ключевые паттерны, масштабирование и требования продакшн-деплоя.

1. Основы LLM-приложений в LangChain: промпты, парсеры и композиция

Основы LLM-приложений в LangChain: промпты, парсеры и композиция

LangChain помогает собирать LLM-приложения как конструктор: вы разделяете приложение на понятные блоки (промпт, модель, парсер, память, инструменты, ретриверы) и затем композиционно соединяете их в пайплайны.

Эта статья закладывает фундамент для всего курса: в следующих главах мы будем строить RAG (поиск по данным) и агентные системы на LangGraph. Но прежде важно научиться правильно формулировать вход, надежно разбирать выход и компоновать шаги.

!Базовая схема того, из каких блоков чаще всего состоит LLM-приложение

Что такое LangChain и из каких блоков состоит LLM-приложение

LangChain (в первую очередь пакет langchain-core) задает стандартные интерфейсы и строительные блоки, из которых собираются приложения на LLM.

Типичная архитектура LLM-приложения состоит из компонентов.

  • Вход (сообщение пользователя, контекст, параметры запроса)
  • Промпт (шаблон, который превращает вход в инструкции модели)
  • LLM/Chat model (модель, которая генерирует ответ)
  • Парсер/валидатор (превращает текст модели в строгий формат и проверяет его)
  • Пост-обработка (правила, бизнес-логика, форматирование)
  • Наблюдаемость (логи, трассировка, метрики)
  • Ключевая идея LangChain: каждый блок можно представить как Runnable (исполняемый шаг), а затем соединить шаги в цепочку.

    Полезные ссылки:

  • Документация LangChain
  • LangChain Expression Language (LCEL)
  • Runnable interface
  • Подключение и использование LLM в LangChain

    На практике вы почти всегда работаете не с базовой LLM, а с чат-моделью (Chat Model), которая принимает сообщения ролей (system, user, assistant).

    Пример с OpenAI (Python).

    Что важно понимать про параметры.

  • model: конкретная модель провайдера.
  • temperature: чем выше, тем более вариативные ответы (для извлечения фактов и JSON обычно ставят 0).
  • Таймауты, ретраи и лимиты: в продакшене это не опции, а обязательная часть надежности.
  • Замечание про ключи: провайдеры обычно читают ключ из переменной окружения (например, OPENAI_API_KEY).

    Промпты: переиспользование и шаблонизация

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

    PromptTemplate

    Подходит, когда вы работаете со строковым промптом.

    ChatPromptTemplate

    Предпочтительнее для чат-моделей: вы разделяете инструкции system и вопрос user.

    Практические правила для промптов.

  • Держите инвариантную часть (политику ответа, формат, ограничения) в system.
  • Держите переменные (вопрос, данные пользователя, контекст) в user.
  • Если вам нужен строгий формат ответа, описывайте формат явно и добавляйте парсер (следующий раздел).
  • Структурированный вывод LLM: JSON и парсеры

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

    Поэтому часто требуется структурированный вывод: например, объект с полями answer, confidence, sources, action.

    LangChain предлагает парсеры (output parsers), которые:

  • описывают ожидаемую схему данных;
  • дают модели инструкции, как вернуть данные;
  • валидируют результат и превращают его в объект.
  • Пример: PydanticOutputParser

    Как читать этот пример.

  • Explanation задает контракт данных: какие поля мы хотим получить.
  • parser.get_format_instructions() генерирует текст инструкций для модели (какой формат вернуть).
  • prompt | llm | parser — композиция шагов.
  • На выходе вы получаете валидированный объект, а не произвольный текст.
  • Если модель вернет некорректный формат, парсер обычно выбросит ошибку. Это полезно: ошибка обнаружится раньше, чем вы запишете мусор в базу или сломаете API-ответ.

    Ссылки:

  • Output parsers
  • Runnable Interface — зачем он нужен

    Runnable — это единый интерфейс для компонентов, которые можно запускать и соединять.

    Что дает Runnable-подход.

  • Единый способ вызывать шаги: invoke, batch, stream.
  • Композицию через оператор |.
  • Возможность прозрачно добавлять ретраи, таймауты, трассировку и кэширование поверх шагов.
  • На уровне мышления это означает: вы проектируете приложение как набор маленьких функций, но с согласованными типами входа и выхода.

    Императивная композиция: pipeline вручную

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

    Плюсы.

  • Максимальная прозрачность.
  • Легко отлаживать и логировать.
  • Минусы.

  • Больше шаблонного кода.
  • Труднее переиспользовать и расширять, когда шагов становится много.
  • Декларативная композиция: LCEL

    LCEL (LangChain Expression Language) позволяет описывать пайплайн декларативно: что с чем соединяется, а не как именно это делать шаг за шагом.

    Пример того же пайплайна через LCEL.

    Почему LCEL важен.

  • Цепочки становятся короче и читаемее.
  • Проще добавлять новые шаги (например, парсер, пост-обработку, fallback).
  • Один и тот же chain можно запускать как invoke, batch, stream.
  • Микс Runnable-элементов: подготовка входа

    Частый паттерн: перед промптом нужно подготовить данные (нормализовать текст, добавить параметры, выбрать стиль ответа). В LCEL это делается добавлением шагов-преобразователей.

    Минимальный чек-лист качества для LLM-пайплайнов

  • Промпт хранится как шаблон и имеет параметры, а не склейку строк.
  • Выход либо структурирован, либо проходит через парсер.
  • Для критичных сценариев используется temperature=0 и валидация.
  • Пайплайн собран как Runnable-цепочка, чтобы его можно было масштабировать и сопровождать.
  • Что дальше по курсу

    Дальше мы будем расширять этот базовый пайплайн до RAG.

  • Сначала научимся готовить документы, чанковать текст и строить индекс в векторном хранилище.
  • Затем добавим retrieval-стратегии (rewrite-retrieve-read, multi-query, routing).
  • После этого перейдем к LangGraph: состояния, память, ветвления, агенты и продакшн-архитектура.
  • 2. RAG I: подготовка данных, чанкинг, эмбеддинги и векторные базы

    RAG I: подготовка данных, чанкинг, эмбеддинги и векторные базы

    В прошлой статье мы разобрали базовые строительные блоки LLM-приложений в LangChain: промпты, парсеры и композицию (Runnable, LCEL). Теперь расширим этот фундамент до RAG.

    Retrieval-Augmented Generation (RAG) почти всегда состоит из двух больших частей:

  • Индексация (offline или nearline): подготовка данных, нарезка на чанки, эмбеддинги, запись в векторное хранилище
  • Общение с данными (online): запрос пользователя, поиск релевантных фрагментов, сбор контекста, генерация ответа
  • Эта статья целиком про первую часть: как сделать так, чтобы ваши данные стали находимыми.

    !Пайплайн индексации: от сырых данных до векторного индекса

    Что такое RAG и зачем он нужен

    RAG — это подход, при котором LLM отвечает не только “из памяти модели”, а опирается на внешние знания, найденные через retrieval.

    Практический смысл:

  • вы можете отвечать по вашим документам (внутренняя документация, договоры, база знаний)
  • вы снижаете риск “галлюцинаций”, потому что ответ привязан к найденному контексту
  • вы обновляете знания без переобучения модели: достаточно переиндексировать данные
  • Полезные ссылки:

  • Руководство LangChain по RAG
  • Эмбеддинги: как текст превращается в числа

    Эмбеддинг — это вектор (список чисел), который представляет текст в многомерном пространстве.

    Интуитивно это работает так:

  • модель-эмбеддер читает текст и возвращает вектор фиксированной длины (например, 768, 1024, 1536 чисел)
  • похожие по смыслу тексты получают похожие векторы
  • это позволяет искать “по смыслу”, а не только по ключевым словам
  • Важно: эмбеддинги — это не “сжатие текста” в буквальном смысле и не гарантия идеальной семантики. Это удобное приближение, которое хорошо работает в задачах поиска.

    Семантические эмбеддинги “на пальцах”

    Представьте, что у вас есть два запроса:

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

    Технически поиск часто сводится к сравнению векторов, например через косинусное сходство:

    Где:

  • и — два вектора-эмбеддинга (например, эмбеддинг запроса и эмбеддинг чанка)
  • — скалярное произведение (сумма произведений соответствующих координат)
  • и — длины (нормы) векторов
  • Смысл формулы: мы сравниваем насколько векторы “смотрят в одном направлении”, а не их абсолютный масштаб.

    Подготовка документов для RAG

    В RAG вы почти всегда работаете не с “сырой строкой”, а с документами: текст + метаданные.

    Минимальный набор, который стоит продумать заранее:

  • page_content: текст фрагмента
  • metadata: источник, раздел, дата, права доступа, язык, теги
  • id: стабильный идентификатор (нужен для переиндексации и удаления)
  • Типичные шаги подготовки:

  • Загрузка из источников (файлы, wiki, CRM, базы данных)
  • Нормализация текста (удаление мусора, повторяющихся шапок/подвалов, артефактов PDF)
  • Дедупликация (одинаковые фрагменты могут “забивать” выдачу)
  • Разметка метаданными (особенно важно для фильтрации: продукт, версия, отдел)
  • Ссылки:

  • Документы (Document) в LangChain
  • Чанкинг: зачем делить текст

    LLM не может “съесть” ваш весь корпоративный портал за один раз: контекст ограничен, а retrieval эффективнее работает на небольших фрагментах.

    Чанк — это небольшой фрагмент текста, который мы индексируем отдельно.

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

  • длинный текст “размывает” эмбеддинг: в одном векторе смешиваются разные темы
  • retrieval возвращает слишком большой контекст (дорого и шумно)
  • сложнее сослаться на точное место и показать источники
  • Базовые стратегии чанкинга

    | Стратегия | Идея | Плюсы | Минусы | |---|---|---|---| | Фиксированный размер | режем по N символов/токенов | просто, быстро | может ломать смысловые границы | | Recursive (по разделителям) | пытаемся резать по абзацам/заголовкам | чаще сохраняет смысл | требует настройки разделителей | | По структуре (Markdown/HTML заголовки) | чанки по секциям документа | отличная “смысловая” нарезка | нужно иметь структуру/разметку | | Семантический | чанки по смысловым границам | лучше качество retrieval | сложнее, дороже |

    Overlap: зачем нужен перехлест чанков

    Часто используют перехлест (overlap) между чанками, например 200–300 символов или 50–100 токенов.

    Зачем:

  • важная фраза может оказаться “на границе” чанка
  • overlap помогает сохранить связность, когда модель читает retrieved-контекст
  • Цена overlap:

  • больше чанков → дороже эмбеддинги и хранение
  • больше похожих чанков → выше риск дубликатов в выдаче
  • Пример чанкинга в LangChain

    Настройка chunk_size и chunk_overlap всегда эмпирическая: ее нужно подбирать под ваши документы и тип запросов.

    Ссылки:

  • Text splitters в LangChain
  • Генерация эмбеддингов

    Эмбеддинги обычно генерируют отдельно для:

  • документов (чанков) — чтобы положить их в индекс
  • запроса — чтобы найти ближайшие чанки
  • В LangChain это обычно выглядит как “embeddings object”, который умеет:

  • embed_documents(list[str])
  • embed_query(str)
  • Пример с OpenAI-эмбеддингами.

    Практические советы:

  • батчинг и ретраи обязательны в продакшене (сетевые ошибки и rate limits неизбежны)
  • эмбеддинги зависят от модели: смена модели требует переиндексации
  • храните информацию о версии эмбеддингов в метаданных (например, embedding_model)
  • Ссылки:

  • Интеграция OpenAI в LangChain
  • Векторные хранилища и их роль

    Векторное хранилище (vector store) отвечает за:

  • хранение векторов + текста + метаданных
  • быстрый поиск ближайших векторов (nearest neighbors)
  • фильтрацию по метаданным (например, product="billing" AND version>=3)
  • Минимально вам нужны операции:

  • add_documents (или upsert)
  • similarity_search / similarity_search_with_score
  • delete по ids или фильтру
  • Качество retrieval зависит не только от эмбеддингов, но и от:

  • чанкинга
  • качества текста (очистка и дедуп)
  • метаданных и фильтрации
  • правильной настройки поиска (k, пороги, reranking)
  • Ссылки:

  • Vector stores в LangChain
  • PGVector и работа с PostgreSQL

    pgvector — расширение PostgreSQL для хранения и поиска по векторам. Это удобный вариант, если вы и так используете PostgreSQL и хотите:

  • хранить векторы рядом с бизнес-данными
  • управлять бэкапами и доступами привычными средствами Postgres
  • использовать транзакции и SQL для части задач
  • Ссылки:

  • pgvector (расширение PostgreSQL)
  • Интеграция PGVector в LangChain
  • Минимальная настройка pgvector в базе

    Обычно достаточно:

    Дальше создается таблица/коллекция для документов (конкретная схема зависит от интеграции).

    Пример индексации в PGVector через LangChain

    В разных версиях LangChain PGVector может быть доступен из разных пакетов. Ориентируйтесь на текущую документацию интеграции.

    Что важно продумать для PostgreSQL в продакшене:

  • миграции схемы
  • индексы для векторного поиска (в pgvector есть разные варианты индексации; выбор зависит от размера данных и требований к скорости)
  • права доступа, разделение коллекций по пространствам/тенантам
  • резервное копирование и стратегии восстановления
  • Отслеживание изменений документов

    Если документы меняются, наивная переиндексация “всего” быстро становится дорогой.

    Практичный подход — инкрементальная индексация:

  • Дайте каждому исходному документу стабильный document_id.
  • Для каждого чанка вычисляйте хэш (например, SHA-256) от нормализованного текста.
  • Храните в базе:
  • - chunk_id (стабильный) - document_id - chunk_hash - updated_at

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

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

    Оптимизация индексации и retrieval: когда базового подхода мало

    Базовый рецепт “чанки + эмбеддинги + similarity search” работает удивительно хорошо, но есть случаи, где нужно усложнение.

    MultiVectorRetriever

    Идея: один логический документ может иметь несколько представлений для поиска, например:

  • краткое резюме (для попадания в семантику запроса)
  • исходный чанк (для точного контекста)
  • заголовки и ключевые термины
  • Это помогает, когда пользователи ищут “в общем”, а ответы лежат в деталях.

    Ссылка:

  • MultiVectorRetriever в LangChain (how-to)
  • RAPTOR

    RAPTOR строит древовидную структуру: снизу — чанки, выше — их абстрактные резюме. Поиск может начинаться с верхних уровней и уточняться ниже.

    Ссылка:

  • RAPTOR: Recursive Abstractive Processing for Tree-Organized Retrieval (arXiv)
  • ColBERT

    ColBERT-подходи ближе к “точному” поиску с учетом контекста токенов и позднего взаимодействия (late interaction). Обычно это сложнее и тяжелее, но может дать выигрыш в качестве на некоторых корпусах.

    Ссылка:

  • ColBERT: Efficient and Effective Passage Search via Contextualized Late Interaction over BERT (arXiv)
  • Минимальный чек-лист качества для индексации

  • Документы имеют стабильные ids и полезные метаданные
  • Текст очищен от мусора и по возможности дедуплицирован
  • Чанкинг выбран осознанно, overlap не “раздувает” индекс без необходимости
  • Вы знаете, какой моделью эмбеддингов индексировали данные, и фиксируете это
  • Есть стратегия инкрементальной переиндексации (хэши/версии/удаление старых чанков)
  • Что дальше

    В следующей статье перейдем ко второй части RAG: как строить retrieval-пайплайн (rewrite-retrieve-read, multi-query, fusion, routing), как фильтровать по метаданным и как подготавливать запросы так, чтобы LLM отвечала точно и с опорой на найденные источники.

    3. RAG II: стратегии поиска, роутинг запросов и Text-to-SQL

    RAG II: стратегии поиска, роутинг запросов и Text-to-SQL

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

    Эта статья продолжает линию курса:

  • из первой статьи вы берёте базовые кирпичики LangChain (промпт → модель → парсер, LCEL)
  • из второй статьи вы берёте готовый индекс (чанки, метаданные, векторная база)
  • в этой статье вы собираете retrieval-пайплайны, которые дают качество в реальных сценариях
  • !Общая карта того, где именно применяются стратегии retrieval

    Базовый RAG-пайплайн: Retrieve-Augment-Generate

    Минимальная рабочая схема RAG выглядит так:

  • вы получаете вопрос пользователя
  • находите релевантные фрагменты (чанки) через retrieval
  • добавляете эти фрагменты в промпт как контекст
  • LLM генерирует ответ, опираясь на контекст
  • В LangChain это обычно выражается как runnable-цепочка.

    Даже в этой простой схеме есть места, где чаще всего падает качество:

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

    Ручки качества retrieval: что настраивают в первую очередь

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

  • k (top-k): сколько чанков возвращать
  • score threshold: порог релевантности, ниже которого документы отбрасываются
  • фильтры по метаданным: продукт, версия, язык, права доступа
  • diversity: в выдаче должны быть не только дубликаты одного и того же фрагмента
  • Практическое правило: если у вас нет метаданных и фильтрации, вы почти всегда будете проигрывать в качестве на корпоративных данных.

    Rewrite-Retrieve-Read: переписываем запрос, затем ищем, затем отвечаем

    Rewrite-Retrieve-Read — паттерн, который добавляет шаг переписывания запроса перед retrieval.

    Зачем переписывать запрос:

  • пользователь пишет «как это починить», но не уточняет сущность
  • в вопросе есть лишние детали, которые ухудшают поиск
  • вопрос сложный и его нужно превратить в короткую «поисковую формулировку»
  • Минимальная схема

  • LLM получает исходный вопрос
  • LLM возвращает улучшенную поисковую формулировку
  • ретривер ищет по улучшенному запросу
  • LLM отвечает по найденному контексту
  • Практический совет: сохраняйте оригинальный вопрос для ответа пользователю, а переписанный используйте только для retrieval. Это уменьшает риск «потери смысла».

    Multi-Query Retrieval: несколько переформулировок вместо одной

    Если один запрос даёт нестабильную выдачу, помогает стратегия Multi-Query: LLM генерирует несколько вариантов запроса, затем вы ищете по каждому и объединяете результаты.

  • плюс: растёт recall (шанс найти нужное)
  • минус: дороже, потому что retrieval вызывается несколько раз
  • В LangChain есть готовый компонент.

    Ссылка: MultiQueryRetriever (LangChain how-to)

    Когда Multi-Query особенно полезен:

  • пользователи используют разные термины для одного и того же
  • документация содержит синонимы, аббревиатуры, внутренние названия
  • вопросы часто «не в терминах документации»
  • RAG-Fusion: объединяем выдачи разных запросов аккуратно

    Если вы делаете Multi-Query, возникает вопрос: как объединять результаты?

    RAG-Fusion — популярный подход, в котором вы:

  • генерируете несколько запросов
  • получаете несколько списков документов
  • объединяете их через Reciprocal Rank Fusion (RRF): документ тем выше, чем чаще и чем выше он встречается в ранжированных списках
  • Формула RRF часто записывается так:

    Где:

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

    Ссылка: Reciprocal rank fusion

    HyDE: поиск через гипотетический документ

    HyDE (Hypothetical Document Embeddings) — техника, где LLM сначала пишет гипотетический ответ-документ на вопрос, а затем retrieval делается не по исходному вопросу, а по эмбеддингу этого гипотетического текста.

    Зачем это может работать:

  • короткий вопрос плохо «попадает» в эмбеддинг-пространство
  • гипотетический документ содержит больше терминов, которые совпадают с документацией
  • Важное ограничение: HyDE повышает recall, но может привести и к более уверенным ошибкам, если гипотетический текст «увёл» семантику.

    Ссылка: HyDE: Precise Zero-Shot Dense Retrieval without Relevance Labels (arXiv)

    Query Routing: выбираем правильный путь для запроса

    Когда в проекте появляется несколько источников знаний, один ретривер перестаёт быть достаточным:

  • векторная база по документации
  • отдельная база по инцидентам
  • SQL-база с транзакциями
  • внешние API
  • Роутинг запросов — это выбор маршрута: куда идти за данными и какой пайплайн применять.

    !Понимание, что «RAG» часто не один, а несколько пайплайнов

    Логический роутинг (правила)

    Подход, который часто стоит внедрять первым:

  • правила по ключевым словам и паттернам
  • явные команды пользователя
  • роутинг по контексту продукта/экрана (если вы встраиваетесь в UI)
  • Плюсы:

  • предсказуемость
  • простая отладка
  • Минусы:

  • плохо масштабируется на множество интентов
  • Семантический роутинг (по смыслу)

    Идея: вы описываете «навыки» или «домены» короткими текстами, считаете эмбеддинги и выбираете ближайший.

  • домен A: «вопросы по документации продукта и настройкам»
  • домен B: «вопросы по данным продаж и агрегатам»
  • домен C: «инциденты и статус систем»
  • Плюсы:

  • лучше ловит перефразирования
  • Минусы:

  • нужно следить за ошибками классификации
  • часто нужен fallback и логирование решений
  • Практичный гибрид

    Часто в продакшене делают так:

  • сначала быстрые правила для явных случаев
  • затем семантический роутер для «остального»
  • затем fallback на самый безопасный сценарий (например, RAG по документации с отказом, если контекст не найден)
  • Query Construction и Text-to-Metadata Filter: превращаем текст в фильтры

    В реальных базах знаний метаданные так же важны, как и текст:

  • product, version
  • department
  • lang
  • access_level
  • Если пользователь спрашивает: «как настроить SSO в версии 3.2», правильное поведение — отфильтровать документы по version=3.2 и только потом делать векторный поиск.

    SelfQueryRetriever: LLM строит фильтр сама

    LangChain поддерживает паттерн, где LLM преобразует естественный язык в:

  • поисковую строку
  • структурированный фильтр по метаданным
  • Это часто называют Text-to-Metadata Filter.

    Ссылка: SelfQueryRetriever (LangChain how-to)

    Query Construction: когда вы строите запросы к внешним системам

    Иногда retrieval — это не только векторный поиск, но и запрос к API/поисковому движку/каталогу. Тогда вы решаете задачу конструирования запроса под конкретный backend.

    Ссылка: Query construction (LangChain how-to)

    Практические правила безопасности и качества для Text-to-Metadata Filter:

  • храните allowlist полей, по которым вообще разрешено фильтровать
  • валидируйте типы значений (даты, числа, перечисления)
  • логируйте фильтры, которые построила модель
  • добавляйте fallback: если фильтр невалиден, используйте поиск без фильтра или задайте уточняющий вопрос
  • Text-to-SQL: когда данные живут в таблицах

    Вопросы вроде «сколько оплат было вчера по тарифу Pro» плохо решаются чистым RAG по текстам. Это задача для SQL.

    Text-to-SQL — пайплайн, в котором LLM:

  • генерирует SQL-запрос
  • вы выполняете его в базе
  • отдаёте результат обратно в LLM для финального ответа на человеческом языке
  • Минимальная архитектура Text-to-SQL

  • вход: вопрос пользователя
  • контекст: схема БД (таблицы, поля, связи)
  • выход: SQL
  • выполнение SQL
  • финальный ответ + (желательно) показ SQL пользователю или в логах
  • Ссылка: SQL QA tutorial (LangChain)

    Пример пайплайна (упрощённо)

    Важно: в продакшене почти никогда нельзя «просто выполнить SQL, который придумала модель».

    Как сделать Text-to-SQL безопасным

    Минимальный набор мер:

  • read-only доступ для пользователя БД, от имени которого вы выполняете запросы
  • allowlist таблиц и колонок: модель не должна уметь читать всё
  • лимиты: LIMIT, ограничения по диапазону дат, таймауты выполнения
  • валидация SQL: запрет опасных конструкций и проверка, что запрос только SELECT
  • логирование: сохраняйте вопрос, сгенерированный SQL, время выполнения, ошибки
  • Когда выбирать Text-to-SQL, а когда RAG

    | Сценарий | Лучше RAG | Лучше Text-to-SQL | |---|---|---| | “Как настроить SSO?” | да | нет | | “Какие поля есть в отчёте X?” | да | нет | | “Сколько оплат было вчера?” | нет | да | | “Покажи топ-10 клиентов по выручке за квартал” | нет | да | | “Что означает ошибка E104?” | да | иногда |

    Частая продакшн-архитектура: роутер решает, что запрос «про цифры» уходит в Text-to-SQL, а запрос «про инструкции/политику/описание» уходит в RAG по документам.

    Минимальный чек-лист качества для RAG II

  • вы понимаете, какой паттерн retrieval используете и зачем (rewrite, multi-query, fusion, HyDE)
  • у вас есть фильтрация по метаданным и стратегия, что делать при пустой выдаче
  • у вас есть роутинг между разными источниками знаний
  • для табличных фактов вы используете Text-to-SQL, а не пытаетесь «вытащить цифры из текстов»
  • все решения LLM, влияющие на данные (фильтры, SQL), валидируются и логируются
  • Что дальше по курсу

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

    4. LangGraph: StateGraph, память, управление историей и агентные циклы

    LangGraph: StateGraph, память, управление историей и агентные циклы

    До этого в курсе мы собирали линейные пайплайны в LangChain: промпт → LLM → парсер, затем добавили RAG (индексация и стратегии retrieval), а также роутинг и Text-to-SQL. Следующий шаг в сторону продакшена — научиться описывать не только линейные цепочки, но и состояния, ветвления, циклы и устойчивое хранение прогресса. Для этого и нужен LangGraph.

    LangGraph — библиотека для построения LLM-приложений в виде графа выполнения (nodes + edges), где данные передаются через состояние (state), а результат можно чекпоинтить (сохранять) между вызовами.

    Полезные ссылки:

  • LangGraph (документация)
  • LangGraph (GitHub репозиторий)
  • !Как LangGraph превращает пайплайн в граф с ветвлениями и общим состоянием

    Зачем LangGraph, если есть LCEL

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

  • разные ветки обработки (RAG по документации, Text-to-SQL по метрикам, вызов API по статусам)
  • циклы (агент делает несколько шагов: подумал → вызвал инструмент → посмотрел результат → продолжил)
  • устойчивость (нужно продолжать процесс после ошибки/таймаута, переисполнять только часть)
  • память (сохранить историю диалога и промежуточные артефакты между запросами)
  • LangGraph решает это архитектурно: вы описываете граф и схему состояния, а выполнение становится управляемым и расширяемым.

    Базовые понятия LangGraph

    Чтобы дальше не появлялись «неопределённые слова», зафиксируем термины.

  • Граф: набор узлов (nodes) и переходов (edges) между ними.
  • Узел (node): функция, которая принимает состояние и возвращает обновление состояния.
  • Состояние (state): общий объект данных, который «живёт» на протяжении выполнения (например, сообщения чата, выбранный маршрут, найденный контекст, результаты инструментов).
  • Переход (edge): правило, куда идти дальше после узла.
  • Условный переход: выбор следующего узла на основе состояния (аналог роутинга, но встроенный в граф).
  • Чекпоинтер (checkpointer): компонент, который сохраняет состояние между вызовами (память/устойчивость/возможность продолжить).
  • StateGraph: идея и назначение

    StateGraph — основной конструктор LangGraph. Вы задаёте:

  • тип состояния (какие поля оно содержит)
  • узлы (функции-обработчики)
  • ребра (порядок и ветвления)
  • Ключевая мысль: узлы не обязаны возвращать «готовый ответ пользователю». Они возвращают патч состояния (обновление), а финальный ответ обычно формируется ближе к концу графа.

    Минимальный пример StateGraph

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

    Что здесь происходит:

  • ChatState описывает, что в состоянии есть поле messages.
  • answer_node читает сообщения и дописывает ответ LLM.
  • START и END — служебные точки начала и завершения.
  • Это выглядит похоже на LCEL-цепочку, но отличие проявится сразу, как только вы добавите ветвление, циклы и чекпоинты.

    Память в LangGraph: что именно мы «помним»

    Важно различать два вида памяти:

  • Память диалога: история сообщений (что спрашивали и что отвечали).
  • Память выполнения: промежуточные результаты графа (например, какой route выбран, какие документы уже доставали, какие tool results уже получены).
  • LangGraph позволяет сохранять состояние между запросами с помощью чекпоинтера.

    Чекпоинтер: зачем он нужен

    Чекпоинтер полезен для продакшена по трём причинам:

  • Персистентность: пользователь возвращается через минуту/час, а контекст не потерян.
  • Надёжность: при сбое можно переисполнить процесс, не теряя всё состояние.
  • Параллельные диалоги: можно вести много «веток» (threads) независимо.
  • В LangGraph часто используется идентификатор диалога thread_id. Это ключ, по которому сохраняется и извлекается состояние.

    Пример: память между вызовами через MemorySaver

    Смысл примера:

  • первый вызов сохраняет состояние в памяти чекпоинтера
  • второй вызов под тем же thread_id продолжает диалог, потому что состояние «подтянется»
  • В продакшене вместо in-memory чекпоинтера обычно нужен внешний стор (например, PostgreSQL), но принцип тот же: состояние привязано к идентификатору треда.

    Управление историей чата: обрезка, фильтрация, слияние

    Если просто копить сообщения, история раздувается:

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

    Обрезка истории

    Самый практичный базовый подход — хранить только последние сообщений. Здесь формулы не нужны: вы просто ограничиваете длину списка.

    Рекомендации:

  • обрезайте перед вызовом LLM, чтобы не платить за лишнее
  • держите запас: кроме user/assistant сообщений бывают system/tool сообщения
  • Фильтрация истории

    Иногда в историю попадают сообщения, которые не стоит возвращать модели целиком:

  • служебные лог-сообщения
  • большие tool outputs
  • куски контекста из retrieval, которые лучше пересобирать заново
  • Тогда вы строите «представление истории» для LLM: например, оставляете только user/assistant реплики, а tool результаты храните в состоянии отдельно.

    Слияние сообщений

    В длинных диалогах полезно объединять последовательные сообщения одной роли (например, несколько коротких user-сообщений в одно) или периодически делать сводку.

    Практичный шаблон:

  • Периодически делайте узел summarize_history, который превращает длинную историю в краткую сводку.
  • Сохраняйте сводку отдельным полем состояния (например, memory_summary).
  • В промпт подавайте: сводку + последние сообщений.
  • Этот шаблон особенно важен, если вы строите агента с циклом, где каждое действие добавляет в историю ещё и tool-сообщения.

    Ветвления в графе: роутинг как часть выполнения

    В статье про RAG II мы обсуждали роутинг запросов. В LangGraph роутинг обычно выражается условными переходами.

    Типичный сценарий: один вход пользователя, но разные ветки:

  • RAG по документации
  • Text-to-SQL по аналитическим данным
  • прямой ответ без данных (если вопрос общий)
  • Пример: условный переход (упрощённо)

    Обратите внимание: узел route не отвечает пользователю. Он только фиксирует решение в состоянии (route). Это и есть «правильная декомпозиция» под граф.

    Агентные циклы: когда граф делает несколько шагов

    Агент — это не «один вызов LLM», а процесс, где модель может многократно:

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

    !Классический агентный цикл как цикл в графе

    Минимальная логика Plan-Do

    В упрощённом виде цикл выглядит так:

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

  • цикл выражается ребром «назад»
  • условие остановки выражается условным переходом
  • состояние «накапливает» результаты действий
  • Практический паттерн: Always Call Tool First

    Для некоторых доменов полезно правило: сначала попытайся получить данные инструментом, и только потом отвечай. Например:

  • статус инцидентов нужно брать из API, а не «угадывать»
  • цифры нужно брать из SQL, а не «вспоминать»
  • В графе это часто означает:

  • если запрос относится к домену инструментов, первая ветка всегда идёт в tool_execute
  • LLM подключается уже после того, как данные получены
  • Это снижает галлюцинации, потому что ответ строится на фактах.

    Как LangGraph «склеивает» RAG, SQL и агентность

    Если связать материал курса в одну картину, то получается типовая архитектура:

  • RAG I даёт вам индекс (чанки, эмбеддинги, векторная база)
  • RAG II даёт retrieval-стратегии и роутинг между источниками
  • LangGraph даёт контроль выполнения: ветвления, циклы, память, перезапуск, устойчивость
  • Типовая схема графа для ассистента поддержки:

  • ingest_user: добавить сообщение пользователя
  • route: определить маршрут
  • rag_retrieve: найти контекст
  • sql_query: безопасно получить табличные данные
  • answer: сгенерировать ответ с учётом политики (не выдумывать, ссылаться на источники)
  • summarize_history: периодически ужимать историю
  • Минимальный чек-лист продакшен-практик для LangGraph

  • Состояние описано явно (TypedDict или аналог), поля минимальны и понятны.
  • В графе есть отдельные узлы для: роутинга, retrieval, генерации ответа, управления историей.
  • История ограничена (обрезка/сводка), иначе вы потеряете и деньги, и качество.
  • Чекпоинтер включён для диалоговых сценариев (хотя бы на время разработки), а thread_id стабилен.
  • Узлы делают одно дело: не смешивайте «поиск», «агрегацию», «финальный ответ» в одной функции.
  • Что дальше по курсу

    Дальше мы будем развивать идею графов в сторону когнитивных архитектур и полноценных агентов:

  • как строить router-архитектуры и supervisor-подходы
  • как масштабировать агента на множество инструментов
  • как добавлять рефлексию и подграфы
  • как думать про деплой и платформенные API для LangGraph
  • 5. Паттерны, мультиагенты и продакшн: streaming, HITL, масштабирование, деплой

    Паттерны, мультиагенты и продакшн: streaming, HITL, масштабирование, деплой

    До этого момента вы собрали базовые LLM-пайплайны на LangChain, построили RAG (индексация и online-retrieval), а затем перешли к LangGraph, чтобы получить состояния, ветвления, циклы и память. Эта статья закрывает разрыв между прототипом и продуктом: какие инженерные паттерны делают систему управляемой, как масштабировать архитектуру до мультиагентов, и что обязательно продумать перед деплоем.

    Ключевая мысль: качество и надежность LLM-приложения чаще ломаются не в “модели”, а в контрактах данных, контроле исполнения и операционных практиках.

    !Карта курса от линейных цепочек к продакшен-системе

    Полезные ссылки по темам статьи:

  • Документация LangChain
  • LCEL (LangChain Expression Language)
  • Output parsers
  • Документация LangGraph
  • LangSmith (наблюдаемость и оценка качества)
  • Паттерны работы с LLM, которые чаще всего нужны в продукте

    Ниже паттерны, которые обычно “включают” по мере взросления системы.

  • Structured Output: модель возвращает данные в схеме (JSON), которые можно валидировать.
  • Intermediate Output: приложение сохраняет промежуточные артефакты (переписанный запрос, выбранный роут, кандидаты документов, SQL до выполнения).
  • Streaming Output: пользователь видит ответ по мере генерации, а не “после полной готовности”.
  • Human-in-the-Loop (HITL): человек подтверждает или корректирует критические шаги.
  • Multitasking: выполнение нескольких подзадач параллельно и последующая агрегация.
  • Важно: эти паттерны проще внедрять, если вы уже мыслите системой как набором Runnable-шагов и/или графом LangGraph с явным state.

    Structured Output: контракты данных вместо “текста модели”

    Structured Output нужен, когда результат должен быть:

  • записан в БД
  • отправлен в API как строгий JSON
  • использован как управляющее решение (routing, выбор инструмента, построение фильтра)
  • Практический принцип

  • промпт задает инструкцию по формату
  • парсер валидирует факт соблюдения формата
  • приложение обрабатывает объект, а не “строку”
  • Пример: with_structured_output + Pydantic

    Что это дает в продакшене:

  • вы получаете валидируемый контракт данных
  • вы можете логировать решение и статистику по route
  • вы можете делать тесты “вход → ожидаемый JSON”
  • Intermediate Output: наблюдаемость, отладка, воспроизводимость

    Intermediate Output означает, что вы не теряете важные промежуточные артефакты.

    Типовые примеры артефактов:

  • переписанный запрос (Rewrite-Retrieve-Read)
  • список retrieval-кандидатов до rerank
  • итоговый контекст, который реально попал в промпт
  • выбранный маршрут (rag/sql/direct)
  • сгенерированный SQL до выполнения
  • Зачем это нужно:

  • отладка: почему модель ответила так
  • аналитика качества: где именно падает пайплайн
  • безопасность: можно доказать, что SQL был проверен и утвержден
  • Пример: сохраняем промежуточные поля в LCEL

    Идея: даже если финальный ответ генерируется в другом chain/графе, у вас есть структурированные поля, которые можно логировать.

    Streaming: быстрый UX и контроль задержек

    Streaming полезен, когда:

  • ответы длинные
  • пользователю важна “реакция” системы
  • вы хотите показывать прогресс или частичные результаты
  • Важно понимать ограничение:

  • streaming улучшает восприятие задержки, но не всегда уменьшает реальную latency
  • Минимальный streaming в LangChain

    Практические правила streaming в продукте

  • разделяйте стриминг токенов и стриминг событий
  • - токены: “печатание ответа” - события: “нашли 4 источника”, “сгенерировали SQL”, “ждем подтверждения”
  • никогда не стримьте пользователю необработанные tool-выводы, если они могут содержать секреты или персональные данные
  • логируйте полную версию ответа на стороне сервера, даже если пользователю стримили куски
  • Human-in-the-Loop (HITL): когда модель не должна решать сама

    HITL нужен не “потому что LLM ошибается”, а потому что:

  • есть юридические и финансовые риски
  • есть операции, требующие подтверждения (выставить счет, изменить тариф, выполнить SQL)
  • нужен контроль качества перед отправкой клиенту
  • Три типичных режима HITL

  • Approval: человек подтверждает действие “да/нет”
  • Edit: человек редактирует предложенный текст/SQL/план
  • Escalation: система передает диалог оператору
  • HITL в LangGraph как управление состоянием

    В LangGraph удобно выражать HITL как “паузу” между вызовами, потому что:

  • состояние можно сохранить чекпоинтером
  • следующий вызов продолжит выполнение по thread_id
  • Ниже схема: граф генерирует SQL, останавливается в состоянии needs_approval, затем продолжает после того, как UI прислал approved=True.

    Что важно в реальном проекте:

  • “ожидание человека” должно быть явно отражено в API-контракте
  • чекпоинтер должен быть персистентным, если диалог может продолжиться позже
  • человек подтверждает не “в целом”, а конкретный артефакт: SQL, письмо клиенту, изменение записи
  • Multitasking: параллельные ветки и агрегация результата

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

  • нужно проверить несколько источников знаний
  • нужно сделать несколько видов retrieval
  • нужно собрать ответ из независимых частей
  • Пример: параллельно получить контекст и сделать краткий план ответа

    На практике вы заменяете fake_retriever на vectorstore.as_retriever() или на несколько ретриверов (например, документация и инциденты).

    Мультиагентные архитектуры: когда одного агента становится мало

    Мультиагенты нужны не для “магии”, а когда появляется сложность:

  • много инструментов и доменов
  • разные политики и контракты данных
  • нужна специализация (например, один агент пишет SQL, другой делает RAG, третий проверяет безопасность)
  • Базовые роли в мультиагентной системе

  • Supervisor: принимает решение, кого вызвать и когда остановиться
  • Workers: специализированные исполнители (RAG-воркер, SQL-воркер, воркер по API)
  • Critic/Guard: проверяет результат на ошибки, риски, политику
  • !Супервайзер выбирает специализированного воркера и пропускает ответ через проверку

    Supervisor Architecture: главный продакшен-паттерн

    Supervisor Architecture хорош тем, что:

  • изолирует сложность по доменам
  • позволяет независимо тестировать и улучшать воркеров
  • упрощает безопасность (например, только SQL-воркер имеет доступ к SQL)
  • В LangGraph это часто выражается так:

  • supervisor node пишет в state поле next_worker
  • conditional edge ведет в нужного воркера
  • воркер возвращает worker_result
  • guard node валидирует
  • граф либо завершает, либо делает еще один цикл
  • Подграфы: повторное использование “готовых навыков”

    Подграф в LangGraph удобен, когда один “навык” встречается много раз:

  • безопасный Text-to-SQL пайплайн
  • retrieval-пайплайн с вашим reranking
  • пайплайн HITL-согласования
  • Идея: вы компилируете подграф как самостоятельный app, а затем вызываете его из другого графа как узел.

    Практическое правило: подграф должен иметь четкий контракт по state-входу и state-выходу, иначе вы получите “спагетти-состояние”.

    Масштабирование по числу инструментов: от “tool list” к “tool router”

    Когда инструментов становится много, агент начинает ошибаться по двум причинам:

  • растет стоимость контекста (слишком много описаний инструментов)
  • ухудшается выбор: инструменты похожи, названия неинформативны
  • Паттерны масштабирования

  • Tool Router: отдельный шаг, который выбирает 1–3 инструмента-кандидата
  • Two-step calling: сначала выбрать инструмент (structured output), затем вызвать
  • Allowlist по домену: разные группы инструментов доступны в разных маршрутах
  • Always Call Tool First для фактов: если вопрос про статусы/цифры, сначала инструмент, потом LLM
  • Минимальный безопасный принцип

  • LLM предлагает действие
  • код проверяет действие политикой
  • только потом действие выполняется
  • Это критично для SQL, платежей, изменений в CRM и любых side effects.

    Продакшн-архитектура LLM-приложения: что должно быть вокруг модели

    Продакшен-архитектура почти всегда включает слои, которые не видно в ноутбуке:

  • API слой (HTTP, авторизация)
  • оркестрация (LangGraph)
  • данные (PostgreSQL, векторная БД)
  • кэширование (например, результатов retrieval)
  • очереди/воркеры для тяжелых задач (переиндексация)
  • секреты (менеджер секретов, ротация ключей)
  • наблюдаемость (трейсы, метрики, логи)
  • оценка качества (датасеты, регрессия промптов)
  • !Типовая архитектура: оркестратор, данные, инструменты, наблюдаемость

    Наблюдаемость и регрессия качества

    Без наблюдаемости вы не сможете ответить на вопросы:

  • где именно деградировало качество после изменения промпта
  • какие типы вопросов чаще всего падают
  • какие источники чаще всего дают “мусор”
  • Практичный минимум:

  • трассировка каждого запроса с thread_id
  • логирование: route, rewritten query, список sources, итоговый prompt size
  • отдельный датасет “контрольных вопросов” и прогон при релизе
  • Для этого обычно используют LangSmith.

    Деплой: как довести граф и пайплайны до сервиса

    Деплой LLM-приложения отличается от обычного бэкенда тем, что у вас есть:

  • внешние зависимости (LLM-провайдеры) с rate limits и деградациями
  • вероятностное поведение (даже при temperature=0 возможны вариации)
  • дорогие операции (retrieval, rerank, SQL, внешние API)
  • Рекомендуемый путь деплоя

  • соберите ядро логики как LangChain chain и LangGraph app
  • сделайте один “входной” эндпоинт (например, /chat), который принимает thread_id и сообщение
  • включите чекпоинтер для состояния диалога
  • включите трассировку
  • добавьте конфигурацию “политик” отдельно от кода (лимиты, allowlists)
  • LangServe как простой способ поднять API

    Если вам нужен быстрый продакшен-обвязчик под LangChain runnable, используйте LangServe.

  • LangServe
  • Даже если вы не используете LangServe в финале, полезно пройти путь “runnable → HTTP” и зафиксировать контракт API.

    Минимальные продакшен-чек-листы

    Чек-лист надежности

  • таймауты и ретраи на всех внешних вызовах
  • деградации: если reranker упал, используем top-k; если retrieval пустой, задаем уточнение
  • лимиты по токенам и размерам tool outputs
  • кэширование там, где это безопасно
  • Чек-лист безопасности

  • read-only для Text-to-SQL, запрет не-SELECT
  • allowlist таблиц/полей и инструментов
  • redaction чувствительных данных в логах
  • HITL для действий с риском
  • Чек-лист качества

  • structured output для маршрутизации и фильтров
  • хранение intermediate output для анализа
  • датасет регрессионных тестов
  • мониторинг доли ответов “не найдено в контексте” и доли ошибок retrieval
  • Что дальше

    На этом этапе у вас есть полный контур “от основ до продакшена”: LangChain для композиции, RAG для работы с данными, LangGraph для управления состояниями и циклами, и набор паттернов для масштабирования.

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

  • формализация evaluation (датасеты, метрики, A/B)
  • улучшение retrieval (reranking, hybrid search, доменные индексы)
  • выделение мультиагентов и подграфов в отдельные сервисы
  • повышение устойчивости (очереди, идемпотентность, дедуп запросов)