0Pricing
AI Engineering Academy · Урок

Разветвлённые и параллельные цепочки

Создайте конструкции RunnableParallel и RunnableBranch для одновременного запуска нескольких цепочек или направления ввода в разные цепочки в зависимости от динамических условий.

«Разветвлённые и параллельные цепочки» — бесплатный урок AI Engineering Academy на CoddyKit. Это урок 3 из 4. Ты можешь прочитать весь урок бесплатно ниже — а потом практиковать его прямо в браузере с встроенным редактором кода и ИИ-репетитором 24/7. Это часть пути обучения AI Engineering Academy, и твой прогресс синхронизируется между веб-версией и приложением CoddyKit. Курс AI Engineering Academy содержит 4 уроков всего.

Зачем нужны параллельность и ветвление

В реальных конвейерах LLM часто требуется выполнять несколько действий одновременно или по-разному направлять запросы в зависимости от их содержимого. Параллельные цепочки одновременно запускают несколько ветвей, уменьшая задержку, когда задачи независимы. Разветвлённые цепочки направляют входные данные в разные специализированные цепочки на основе динамических условий. LCEL изначально поддерживает оба шаблона с помощью RunnableParallel и RunnableBranch.

Основы RunnableParallel

RunnableParallel принимает словарь, в котором каждому ключу соответствует Runnable. При вызове он одновременно запускает все ветви и возвращает словарь, где каждый ключ содержит результат своей ветви. Это удобно, когда нужно получить несколько результатов из одних и тех же входных данных — например, одновременно создать краткое содержание и извлечь ключевые слова.

from langchain_core.runnables import RunnableParallel
from langchain_openai import ChatOpenAI
from langchain_core.prompts import ChatPromptTemplate
from langchain_core.output_parsers import StrOutputParser

model = ChatOpenAI(model='gpt-4o-mini')
parser = StrOutputParser()

parallel = RunnableParallel(
    summary=(
        ChatPromptTemplate.from_template('Summarize: {text}') | model | parser
    ),
    keywords=(
        ChatPromptTemplate.from_template('Extract keywords from: {text}') | model | parser
    )
)

result = parallel.invoke({'text': 'Long document text here...'})
print(result['summary'])
print(result['keywords'])

Параллельное выполнение с сокращённой записью через dict

LCEL предоставляет удобную сокращённую запись: передача обычного dict в качестве шага цепочки с конвейером автоматически оборачивает его в RunnableParallel. Благодаря этому параллельные ветви выглядят естественно, а явное создание класса не требуется. Ключи словаря становятся ключами результата, а значения — ветвями, выполняющимися параллельно.

from langchain_core.runnables import RunnablePassthrough

# Dict shorthand creates RunnableParallel automatically
chain = (
    RunnablePassthrough.assign(
        sentiment=(
            ChatPromptTemplate.from_template('Sentiment of: {review}')
            | model | parser
        ),
        aspects=(
            ChatPromptTemplate.from_template('List aspects mentioned in: {review}')
            | model | parser
        )
    )
)

result = chain.invoke({'review': 'Great battery but poor camera quality.'})
print(result['sentiment'])
print(result['aspects'])

Изучение RunnableBranch

RunnableBranch направляет входные данные в разные цепочки в зависимости от условия. Вы передаёте список пар (condition, runnable) и Runnable по умолчанию. Ветвь проверяет условия по порядку и запускает первую подходящую ветвь. Это позволяет маршрутизировать запросы по намерению: направлять обращения в службу поддержки в цепочку поддержки, а технические вопросы — в цепочку документации.

from langchain_core.runnables import RunnableBranch

technical_chain = (
    ChatPromptTemplate.from_template('Technical answer: {query}') | model | parser
)
general_chain = (
    ChatPromptTemplate.from_template('General answer: {query}') | model | parser
)

branch = RunnableBranch(
    (lambda x: 'error' in x['query'].lower() or 'bug' in x['query'].lower(),
     technical_chain),
    general_chain  # default branch
)

result = branch.invoke({'query': 'I got a TypeError in my code'})
# Routes to technical_chain because 'error' is in the query

Семантическая маршрутизация с классификацией LLM

Более гибкий шаблон маршрутизации использует вызов LLM-классификатора, чтобы определить нужную ветвь. Сначала маршрутизатор вызывает небольшую модель для классификации намерения входных данных, а затем использует результат классификации, чтобы направить запрос в подходящую специализированную цепочку. Такой подход обрабатывает тонкие случаи, которые не удаётся распознать сопоставлением ключевых слов, но требует дополнительного вызова API.

from langchain_core.output_parsers import StrOutputParser

# Step 1: classify intent
classify_prompt = ChatPromptTemplate.from_template(
    'Classify this query as exactly one of: billing, technical, general.\nQuery: {query}'
)
classifier = classify_prompt | model | StrOutputParser()

# Step 2: route based on classification
def route(classification_result: dict):
    topic = classification_result['topic'].strip().lower()
    if topic == 'billing':
        return billing_chain
    elif topic == 'technical':
        return technical_chain
    return general_chain

full_chain = (
    RunnablePassthrough.assign(topic=lambda x: classifier.invoke(x))
    | RunnableLambda(route)
)

Параллельный RAG: несколько Retriever

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

from langchain_core.runnables import RunnableParallel, RunnablePassthrough

# Assume these retrievers are already set up
faq_retriever = faq_vectorstore.as_retriever(search_kwargs={'k': 3})
doc_retriever = doc_vectorstore.as_retriever(search_kwargs={'k': 3})

retrieval = RunnableParallel(
    faq_results=faq_retriever,
    doc_results=doc_retriever
)

def merge_docs(retrieved: dict) -> str:
    all_docs = retrieved['faq_results'] + retrieved['doc_results']
    return '\n\n'.join(d.page_content for d in all_docs)

pipeline = (
    retrieval
    | RunnableLambda(merge_docs)
    | ChatPromptTemplate.from_template('Context: {context}\nAnswer: {question}')
    | model | parser
)

Условные цепочки с itemgetter

Если нужно выбрать определённый ключ из результата параллельного выполнения или передать на следующий шаг только часть контекста, operator.itemgetter в Python работает как лёгкий селектор Runnable. Это особенно полезно после параллельного шага, когда разные ветви создают результаты с разными ключами и на следующий этап обработки нужно извлечь только нужный результат.

from operator import itemgetter
from langchain_core.runnables import RunnablePassthrough

# After a parallel step, extract just the summary
chain = (
    RunnableParallel(
        summary=summary_chain,
        sentiment=sentiment_chain
    )
    | itemgetter('summary')  # pass only the summary onward
    | translate_chain
)

# itemgetter works because dict.__getitem__ is a valid transform

Измерение ускорения за счёт параллельного выполнения

Главное преимущество RunnableParallel — сокращение реального времени выполнения. Три последовательных вызова LLM, каждый из которых занимает 2 секунды, в общей сложности выполнялись бы 6 секунд. При параллельном выполнении они завершаются примерно за 2 секунды — то есть за время работы самой медленной ветви. Однако параллельные вызовы одновременно увеличивают расход токенов, поэтому следите за ограничениями частоты запросов. При необходимости используйте max_concurrency в batch() или задайте ограничения частоты для отдельных ключей.

import time

# Measure sequential time
start = time.time()
result1 = chain_a.invoke(input_data)
result2 = chain_b.invoke(input_data)
result3 = chain_c.invoke(input_data)
seq_time = time.time() - start
print(f'Sequential: {seq_time:.2f}s')

# Measure parallel time
start = time.time()
results = RunnableParallel(a=chain_a, b=chain_b, c=chain_c).invoke(input_data)
par_time = time.time() - start
print(f'Parallel: {par_time:.2f}s')
print(f'Speedup: {seq_time/par_time:.1f}x')

Асинхронные параллельные цепочки

Для действительно неблокирующего параллельного выполнения в асинхронных приложениях используйте RunnableParallel.ainvoke(). Внутри LCEL применяет asyncio.gather(), чтобы параллельно запускать ветви в цикле обработки событий. Это особенно важно в службах FastAPI, где каждый обработчик запроса является сопрограммой: асинхронный интерфейс не блокирует цикл обработки событий даже при нескольких одновременных вызовах LLM.

import asyncio

async def analyze_document(text: str) -> dict:
    parallel = RunnableParallel(
        summary=summary_chain,
        keywords=keyword_chain,
        sentiment=sentiment_chain
    )
    # All three chains run concurrently with asyncio.gather internally
    result = await parallel.ainvoke({'text': text})
    return result

# In FastAPI:
from fastapi import FastAPI
app = FastAPI()

@app.post('/analyze')
async def analyze(request: dict):
    return await analyze_document(request['text'])

Вложенные параллельные и последовательные цепочки

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

# Full pipeline: classify → parallel retrieval → generate
pipeline = (
    RunnablePassthrough.assign(
        intent=classify_chain  # sequential: classify first
    )
    | RunnablePassthrough.assign(
        context=RunnableParallel(  # parallel: retrieve from both sources
            faq=faq_retriever,
            docs=doc_retriever
        )
    )
    | format_context_chain  # sequential: format merged context
    | generate_answer_chain  # sequential: call LLM
)

Обработка ошибок в ветвях

Если одна из ветвей RunnableParallel завершается с ошибкой, по умолчанию весь параллельный вызов возбуждает исключение. Используйте .with_fallbacks() для отдельных выполняемых объектов ветвей, чтобы корректно обрабатывать ошибки на уровне ветви. Для RunnableBranch оберните каждую ветвь в конструкцию try-except внутри RunnableLambda или используйте цепочку с резервным вариантом по умолчанию для обработки ошибок маршрутизации.

from langchain_core.runnables import RunnableParallel

# Wrap each branch with a fallback
safe_summary = summary_chain.with_fallbacks([
    RunnableLambda(lambda x: 'Summary unavailable')
])
safe_sentiment = sentiment_chain.with_fallbacks([
    RunnableLambda(lambda x: 'Sentiment unavailable')
])

robust_parallel = RunnableParallel(
    summary=safe_summary,
    sentiment=safe_sentiment
)

# Now one branch failing won't kill the entire parallel call

Быстрая проверка

Проверьте, насколько хорошо Вы понимаете ветвление и параллельные цепочки в LCEL.

Итоги урока

В этом уроке Вы узнали, что RunnableParallel одновременно запускает несколько цепочек и возвращает словарь результатов, уменьшая задержку при независимых вызовах LLM; RunnableBranch направляет входные данные в разные специализированные цепочки на основе условий или классификации LLM; а вложенность параллельных и последовательных шагов позволяет создавать сложные многоветочные конвейеры, которые при этом остаются понятными. Далее мы рассмотрим потоковую передачу вывода в LangChain.

Часто задаваемые вопросы

Урок «Разветвлённые и параллельные цепочки» бесплатный?

Да — полный текст урока «Разветвлённые и параллельные цепочки» бесплатно доступен здесь в веб-версии. Чтобы практиковать его интерактивно (встроенный редактор кода и ИИ-репетитор 24/7) и разблокировать остальной курс AI Engineering Academy, подпишись на CoddyKit PRO. Курс AI Engineering Academy содержит 4 уроков всего.

Чему я научусь в уроке «Разветвлённые и параллельные цепочки»?

Создайте конструкции RunnableParallel и RunnableBranch для одновременного запуска нескольких цепочек или направления ввода в разные цепочки в зависимости от динамических условий. Ты практикуешь AI Engineering Academy с помощью реального кода, который запускаешь прямо в браузере, и ИИ-репетитор 24/7 отвечает на твои вопросы во время урока.

Нужен ли мне опыт, чтобы начать AI Engineering Academy?

Предыдущий опыт не требуется. AI Engineering Academy на CoddyKit структурирован для всех уровней — от новичков до продвинутых, поэтому ты можешь начать отсюда или с самого начала и учиться в своем темпе. Это урок 3 из 4.

Сколько времени занимает урок «Разветвлённые и параллельные цепочки»?

Большинство уроков CoddyKit занимают около 5–10 минут. Каждый из них компактный и интерактивный, поэтому ты постоянно делаешь прогресс и продолжаешь с того же места в веб-версии и приложении.

Можно ли писать и запускать код в этом уроке AI Engineering Academy?

Да. Каждый урок AI Engineering Academy включает встроенный редактор кода, поэтому ты пишешь и запускаешь реальный код прямо в браузере и получаешь моментальную обратную связь от AI — локальная установка не требуется.

Все уроки этого курса

  1. Архитектура LangChain и основные абстракции
  2. Создание цепочек с LCEL
  3. Разветвлённые и параллельные цепочки
  4. Потоковый вывод в LangChain
← Назад к AI Engineering Academy