Введение в LCEL
LCEL (LangChain Expression Language) - это мощный инструмент оркестрации рабочих процессов, который позволяет создавать сложные цепочки задач из базовых компонентов и поддерживает функции, такие как потоковая обработка, параллельная обработка и ведение журналов.
Базовый пример: Запрос + Модель + Парсер вывода
В этом примере мы продемонстрируем, как использовать LCEL (LangChain Expression Language) для связи трех компонентов - шаблона запроса, модели и парсера вывода - для формирования полной цепочки задач для реализации задачи "рассказать шутку". В коде показано, как создавать цепочки, использовать символ |
для соединения различных компонентов и представлять роль каждого компонента вместе с результатами вывода.
Сначала давайте посмотрим, как соединить шаблон запроса и модель для генерации шутки на определенную тему:
Установите зависимости
%pip install --upgrade --quiet langchain-core langchain-community langchain-openai
from langchain_core.output_parsers import StrOutputParser
from langchain_core.prompts import ChatPromptTemplate
from langchain_openai import ChatOpenAI
prompt = ChatPromptTemplate.from_template("Расскажи шутку про {topic}")
model = ChatOpenAI(model="gpt-4")
output_parser = StrOutputParser()
цепочка = prompt | model | output_parser
цепочка.invoke({"topic": "мороженое"})
Вывод
"Почему на вечеринки не приглашают мороженое? Потому что оно тает, когда становится жарко!"
В этом коде мы используем LCEL, чтобы соединить различные компоненты в цепочку:
цепочка = prompt | model | output_parser
Символ |
здесь аналогичен оператору конвейера в Unix, который соединяет различные компоненты вместе и передает вывод одного компонента вводом для следующего компонента.
В этой цепочке пользовательский ввод передается шаблону запроса, затем вывод шаблона запроса передается модели, и, наконец, вывод модели передается парсеру вывода. Давайте рассмотрим каждый компонент отдельно, чтобы лучше понять, что происходит.
1. Запрос
prompt
- это BasePromptTemplate
, который принимает словарь переменных шаблона и генерирует PromptValue
. PromptValue
- это упакованный объект, содержащий запрос, который может быть передан LLM
(в форме строки) или ChatModel
(в форме последовательности сообщений). Он может использоваться с любым типом языковой модели, потому что он определяет логику генерации BaseMessage
и создания строк.
prompt_value = prompt.invoke({"topic": "мороженое"})
prompt_value
Вывод
ChatPromptValue(messages=[HumanMessage(content='Расскажи шутку про мороженое')])
Ниже мы преобразуем результат запроса в формат сообщения, используемый моделями чатов:
prompt_value.to_messages()
Вывод
[HumanMessage(content='Расскажи шутку про мороженое')]
Также его можно преобразовать непосредственно в строку:
prompt_value.to_string()
Вывод
'Человек: Расскажи шутку про мороженое.'
2. Модель
Затем передаем PromptValue
модели. В этом примере наша модель
- это ChatModel
, что означает, что она будет выводить BaseMessage
.
Попробуйте вызвать модель
напрямую:
сообщение = model.invoke(prompt_value)
сообщение
Возвращает:
AIMessage(content="Почему мороженое никогда не приглашают на вечеринки?\n\nПотому что оно всегда тает, когда нагревается обстановка!")
Если наша модель
определена как тип LLM
, то она будет выводить строку.
from langchain_openai.llms import OpenAI
llm = OpenAI(model="gpt-3.5-turbo-instruct")
llm.invoke(prompt_value)
Модель возвращает:
'\n\nБот: Почему ледовая машина сорвала себе крышку? Потому что произошло таяние!'
3. Парсер вывода
Наконец, передаем вывод из нашей модели
в output_parser
, который является BaseOutputParser
, что означает, что он принимает строку или BaseMessage
в качестве ввода. StrOutputParser
специально преобразует любой ввод в простую строку.
output_parser.invoke(message)
Почему мороженое никогда не приглашают на вечеринки? \n\nПотому что оно всегда тает, когда нагревается обстановка!
4. Вся процедура
Процесс выполнения выглядит следующим образом:
- Вызовите
chain.invoke({"topic": "ice cream"})
, что эквивалентно инициированию определенного нами рабочего процесса и передаче параметра{"topic": "ice cream"}
для генерации шутки о "мороженом". - Передайте параметр вызова
{"topic": "ice cream"}
первому компоненту цепочки,prompt
, который форматирует шаблон приглашения, чтобы получить приглашениеРасскажи мне шутку о мороженом
. - Передайте приглашение
Расскажи мне шутку о мороженом
вmodel
(модель gpt4). - Передайте результат, возвращенный
model
, в выходной парсерoutput_parser
, который форматирует результат модели и возвращает окончательное содержимое.
Если вас интересует вывод какого-либо компонента, вы можете в любое время протестировать более маленькую версию цепочки, такую как prompt
или prompt | model
, чтобы увидеть промежуточные результаты:
input = {"topic": "ice cream"}
prompt.invoke(input)
(prompt | model).invoke(input)
Пример поиска RAG
Теперь рассмотрим слегка более сложный пример LCEL. Мы запустим пример расширенного поиска для создания цепочек для добавления некоторой фоновой информации при ответе на вопросы.
from langchain_community.vectorstores import DocArrayInMemorySearch
from langchain_core.output_parsers import StrOutputParser
from langchain_core.prompts import ChatPromptTemplate
from langchain_core.runnables import RunnableParallel, RunnablePassthrough
from langchain_openai.chat_models import ChatOpenAI
from langchain_openai.embeddings import OpenAIEmbeddings
vectorstore = DocArrayInMemorySearch.from_texts(
["harrison worked at kensho", "bears like to eat honey"],
embedding=OpenAIEmbeddings(),
)
retriever = vectorstore.as_retriever()
template = """Ответьте на вопрос, основываясь только на следующем контексте:
{context}
Вопрос: {question}
"""
prompt = ChatPromptTemplate.from_template(template)
model = ChatOpenAI()
output_parser = StrOutputParser()
setup_and_retrieval = RunnableParallel(
{"context": retriever, "question": RunnablePassthrough()}
)
chain = setup_and_retrieval | prompt | model | output_parser
chain.invoke("где работал харрисон?")
В данном случае составленная цепочка выглядит так:
chain = setup_and_retrieval | prompt | model | output_parser
Простыми словами, указанный шаблон приглашения принимает context
и question
в качестве значений для замены в приглашении. Прежде чем создать шаблон приглашения, мы хотим извлечь соответствующие документы для использования в качестве контекста.
В качестве теста мы используем DocArrayInMemorySearch
для имитации базы данных векторов на основе памяти, определяя получателя, который может извлекать похожие документы на основе запросов. Это также цепной компонент, но вы также можете попробовать выполнить его отдельно:
retriever.invoke("где работал харрисон?")
Затем мы используем RunnableParallel
для подготовки ввода для приглашения, поиска документов с использованием получателя и передачи вопроса пользователя с помощью RunnablePassthrough
:
setup_and_retrieval = RunnableParallel(
{"context": retriever, "question": RunnablePassthrough()}
)
В итоге полная цепочка выглядит следующим образом:
setup_and_retrieval = RunnableParallel(
{"context": retriever, "question": RunnablePassthrough()}
)
chain = setup_and_retrieval | prompt | model | output_parser
Процесс выглядит следующим образом:
- Сначала создайте объект
RunnableParallel
, содержащий две записи. Первая записьcontext
будет содержать результаты документов, извлеченные получателем. Вторая записьquestion
будет содержать начальный вопрос пользователя. Чтобы передать вопрос, мы используемRunnablePassthrough
для копирования этой записи. - Передайте словарь из предыдущего шага компоненту
prompt
. Он принимает ввод пользователя (т.е.question
) а также извлеченные документы (т.е.context
), составляет приглашение и выдаётPromptValue
. - Компонент
model
берет сгенерированное приглашение и передает его для оценки модели LLM от OpenAI. Результат, сгенерированный моделью, — это объектChatMessage
. - Наконец, компонент
output_parser
беретChatMessage
, преобразует его в строку Python и возвращает его из методаinvoke
.