Wstęp do LCEL
LCEL (LangChain Expression Language) to potężne narzędzie do orchestracji pracy, które pozwala budować złożone łańcuchy zadań z podstawowych składników i obsługuje funkcje takie jak przetwarzanie strumieniowe, przetwarzanie równoległe i logowanie.
Prosty przykład: Prompt + Model + Analizator wyników
W tym przykładzie pokażemy, jak używać LCEL (LangChain Expression Language) do połączenia trzech składników - szablonu promptu, modelu i analizatora wyników - w celu stworzenia kompletnego procesu implementacji zadania "opowiadania żartów". Kod pokazuje, jak tworzyć łańcuchy, używać symbolu rury |
do połączenia różnych składników i wprowadza rolę każdego składnika wraz z wynikami wyjściowymi.
Najpierw zobaczmy, jak połączyć szablon promptu z modelem, aby wygenerować żart na określony temat:
Zainstaluj zależności
%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("Powiedz mi żart na temat {topic}")
model = ChatOpenAI(model="gpt-4")
output_parser = StrOutputParser()
chain = prompt | model | output_parser
chain.invoke({"topic": "lody"})
Wyjście
"Dlaczego na imprezy nie zaprasza się lodów? Ponieważ topnieją, gdy jest gorąco!"
W tym kodzie używamy LCEL do połączenia różnych składników w łańcuch:
chain = prompt | model | output_parser
Symbol |
jest podobny do operatora potoku w systemie Unix, który łączy różne składniki i przekazuje wynik jednego składnika jako wejście do następnego składnika.
W tym łańcuchu wejście użytkownika jest przekazywane do szablonu promptu, a następnie wynik promptu jest przekazywany do modelu, a ostatecznie wynik modelu jest przekazywany do analizatora wyników. Przejrzyjmy każdy składnik osobno, aby lepiej zrozumieć, co się dzieje.
1. Prompt
prompt
to BasePromptTemplate
, który akceptuje słownik zmiennych szablonu i generuje PromptValue
. PromptValue
jest opakowanym obiektem zawierającym prompt, który może być przekazany do LLM
(jako wejście w postaci ciągu znaków) lub ChatModel
(jako wejście w postaci sekwencji wiadomości). Może być używany z każdym rodzajem modelu językowego, ponieważ definiuje logikę generowania BaseMessage
i generowania ciągów znaków.
prompt_value = prompt.invoke({"topic": "lody"})
prompt_value
Wyjście
ChatPromptValue(messages=[HumanMessage(content='Powiedz mi żart na temat lody')])
Poniżej konwertujemy wynik promptu na format wiadomości używany przez modele czatu:
prompt_value.to_messages()
Wyjście
[HumanMessage(content='Powiedz mi żart na temat lody')]
Może też być bezpośrednio zamieniony na ciąg znaków:
prompt_value.to_string()
Wyjście
'Human: Powiedz mi żart na temat lody.'
2. Model
Następnie przekaż PromptValue
do modelu
. W tym przykładzie nasz model
to ChatModel
, co oznacza, że generuje BaseMessage
.
Spróbuj wywołać model
bezpośrednio:
message = model.invoke(prompt_value)
message
Zwraca:
AIMessage(content="Dlaczego lody nigdy nie są zapraszane na imprezy?\n\nPonieważ zawsze są wykapane, gdy sytuacja się rozgrzeje!")
Jeśli nasz model
jest zdefiniowany jako typ LLM
, wygeneruje ciąg znaków.
from langchain_openai.llms import OpenAI
llm = OpenAI(model="gpt-3.5-turbo-instruct")
llm.invoke(prompt_value)
Model zwraca:
'\n\nBot: Dlaczego ciężarówka z lodami się zepsuła? Bo przeżyła stopienie!'
3. Analizator wyników
Na koniec przekaż wynik z naszego modelu
do output_parsera
, który jest BaseOutputParser
, co oznacza, że akceptuje ciąg znaków lub BaseMessage
jako wejście. StrOutputParser
konwertuje dowolne wejście na prosty ciąg znaków.
output_parser.invoke(message)
Dlaczego lody nigdy nie są zapraszane na imprezy? \n\nPonieważ zawsze są wykapane, gdy sytuacja się rozgrzeje!
4. Cały proces
Proces wykonania wygląda następująco:
- Wywołaj
chain.invoke({"topic": "lody"})
, co jest równoważne zainicjowaniu zdefiniowanego przez nas workflow i przekazaniem parametru{"topic": "lody"}
w celu wygenerowania dowcipu o "lody." - Przekaż parametr wywołania
{"topic": "lody"}
do pierwszego elementu łańcucha, czyliprompt
, który formatuje szablon pytania, aby uzyskać pytaniePowiedz mi mały żart o lodach
. - Przekaż pytanie
Powiedz mi mały żart o lodach
do komponentumodel
(model gpt4). - Przekaż wynik zwrócony przez
model
do parsera wyjściaoutput_parser
, który formatuje wynik modelu i zwraca ostateczną treść.
Jeśli jesteś zainteresowany wynikiem dowolnego komponentu, możesz w dowolnym momencie przetestować mniejszą wersję łańcucha, na przykład prompt
lub prompt | model
, aby zobaczyć pośrednie wyniki:
input = {"topic": "lody"}
prompt.invoke(input)
(prompt | model).invoke(input)
Przykład wyszukiwania RAG
Następnie wyjaśnijmy nieco bardziej złożony przykład LCEL. Uruchomimy przykład ulepszonego wyszukiwania, aby generować łańcuchy w celu dodania kilku informacji wstępnych przy odpowiadaniu na pytania.
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 pracował w kensho", "niedźwiedzie lubią jeść miód"],
embedding=OpenAIEmbeddings(),
)
retriever = vectorstore.as_retriever()
template = """Odpowiedz na pytanie, opierając się tylko na następującym kontekście:
{context}
Pytanie: {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("gdzie harrison pracował?")
W tym przypadku skomponowany łańcuch wygląda następująco:
chain = setup_and_retrieval | prompt | model | output_parser
W skrócie, powyższy szablon pytania akceptuje context
i question
jako wartości do zastąpienia w pytaniu. Przed skonstruowaniem szablonu pytania chcemy pozyskać istotne dokumenty do wykorzystania jako część kontekstu.
W teście używamy DocArrayInMemorySearch
do symulowania bazy danych wektorów opartych na pamięci, definiując wydobywarkę, która może wyszukać podobne dokumenty na podstawie zapytań. Jest to także komponent uruchamialny podlegający łączeniu, ale można go również spróbować uruchomić osobno:
retriever.invoke("gdzie harrison pracował?")
Następnie używamy RunnableParallel
do przygotowania wejścia dla szablonu pytania, wyszukania dokumentów za pomocą wydobywarki i przekazania pytania użytkownika za pomocą RunnablePassthrough
:
setup_and_retrieval = RunnableParallel(
{"context": retriever, "question": RunnablePassthrough()}
)
Podsumowując, kompletny łańcuch wygląda następująco:
setup_and_retrieval = RunnableParallel(
{"context": retriever, "question": RunnablePassthrough()}
)
chain = setup_and_retrieval | prompt | model | output_parser
Proces wygląda następująco:
- Najpierw należy utworzyć obiekt
RunnableParallel
, zawierający dwie pozycje. Pierwsza pozycjacontext
będzie zawierać wyniki dokumentów wydobytych przez wydobywarkę. Druga pozycjaquestion
będzie zawierać oryginalne pytanie użytkownika. Aby przekazać pytanie, używamyRunnablePassthrough
, aby skopiować tę pozycję. - Przekaż słownik z poprzedniego kroku do komponentu
prompt
. Akceptuje on wejście użytkownika (czyliquestion
) oraz wydobyte dokumenty (czylicontext
), konstruuje szablon pytania i generuje wartośćPromptValue
. - Komponent
model
pobiera wygenerowany szablon pytania i przekazuje go do modelu LLM firmy OpenAI w celu oceny. Wygenerowany przez model wynik to obiektChatMessage
. - Na koniec komponent
output_parser
pobieraChatMessage
, konwertuje go na łańcuch znaków w języku Python i zwraca go z metodyinvoke
.