Разветвлённые и параллельные цепочки
Создайте конструкции 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 — локальная установка не требуется.
Все уроки этого курса
- Архитектура LangChain и основные абстракции
- Создание цепочек с LCEL
- Разветвлённые и параллельные цепочки
- Потоковый вывод в LangChain