Introdução ao LCEL
O LCEL (LangChain Expression Language) é uma poderosa ferramenta de orquestração de fluxo de trabalho que permite criar cadeias de tarefas complexas a partir de componentes básicos e suporta recursos prontos como processamento de streaming, processamento paralelo e registro de logs.
Exemplo Básico: Prompt + Modelo + Analisador de Saída
Neste exemplo, vamos demonstrar como usar o LCEL (LangChain Expression Language) para vincular três componentes - modelo de prompt, modelo e analisador de saída - para formar um fluxo de trabalho completo para a implementação da tarefa de "contar piadas". O código demonstra como criar cadeias, usar o símbolo de pipe |
para conectar diferentes componentes e introduzir o papel de cada componente junto com os resultados de saída.
Primeiro, vamos ver como conectar o modelo de prompt e o modelo para gerar uma piada sobre um tópico específico:
Instalar dependências
%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("Conta uma piada sobre {topic}")
model = ChatOpenAI(model="gpt-4")
output_parser = StrOutputParser()
chain = prompt | model | output_parser
chain.invoke({"topic": "sorvete"})
Resultado
"Por que as festas não convidam sorvete? Porque ele derrete quando está quente!"
Neste código, usamos o LCEL para conectar diferentes componentes em uma cadeia:
chain = prompt | model | output_parser
O símbolo |
aqui é semelhante ao operador de pipe Unix, que conecta diferentes componentes e passa a saída de um componente como entrada para o próximo componente.
Nesta cadeia, a entrada do usuário é passada para o modelo de prompt, em seguida, a saída do modelo de prompt é passada para o modelo e, finalmente, a saída do modelo é passada para o analisador de saída. Vamos dar uma olhada em cada componente separadamente para entender melhor o que está acontecendo.
1. Prompt
prompt
é um BasePromptTemplate
que aceita um dicionário de variáveis de template e gera um PromptValue
. PromptValue
é um objeto encapsulado contendo o prompt, que pode ser passado para LLM
(como entrada na forma de uma string) ou ChatModel
(como entrada na forma de sequências de mensagens). Pode ser usado com qualquer tipo de modelo de linguagem, pois define a lógica para gerar BaseMessage
e gerar strings.
prompt_value = prompt.invoke({"topic": "sorvete"})
prompt_value
Saída
ChatPromptValue(messages=[HumanMessage(content='Conta uma piada sobre sorvete')])
Abaixo, convertendo o resultado formatado do prompt no formato de mensagem usado pelos modelos de chat:
prompt_value.to_messages()
Saída
[MensagemHumana(conteúdo='Conta uma piada sobre sorvete')]
Também pode ser diretamente convertido em uma string:
prompt_value.to_string()
Saída
'Humano: Conta uma piada sobre sorvete.'
2. Modelo
Em seguida, passe o PromptValue
para o modelo
. Neste exemplo, nosso modelo
é um ChatModel
, o que significa que ele produzirá uma BaseMessage
.
Tente chamar o modelo
diretamente:
mensagem = model.invoke(prompt_value)
mensagem
Retorna:
AIMessage(content="Por que o sorvete nunca é convidado para as festas?\n\nPorque ele sempre derrete quando as coisas esquentam!")
Se nosso modelo
for definido como um tipo LLM
, ele produzirá uma string.
from langchain_openai.llms import OpenAI
llm = OpenAI(model="gpt-3.5-turbo-instruct")
llm.invoke(prompt_value)
Modelo retorna:
'\n\nBot: Por que o caminhão de sorvete quebrou? Porque ele passou por um derretimento!'
3. Analisador de Saída
Finalmente, passe a saída do nosso modelo
para o analisador de saída
, que é um BaseOutputParser
, ou seja, aceita uma string ou BaseMessage
como entrada. StrOutputParser
especificamente converte qualquer entrada em uma simples string.
output_parser.invoke(mensagem)
Por que o sorvete nunca é convidado para as festas? \n\nPorque ele sempre derrete quando as coisas esquentam!
4. O Processo Completo
O processo de execução é o seguinte:
- Chame
chain.invoke({"topic": "sorvete"})
, o que é equivalente a iniciar o fluxo de trabalho que definimos e passar o parâmetro{"topic": "sorvete"}
para gerar uma piada sobre "sorvete". - Passe o parâmetro chamado
{"topic": "sorvete"}
para o primeiro componente da cadeia,prompt
, que formata o modelo do prompt para obter o promptMe conte uma piadinha sobre sorvete
. - Passe o prompt
Me conte uma piadinha sobre sorvete
para omodelo
(modelo gpt4). - Passe o resultado retornado pelo
modelo
para o analisador de saídaoutput_parser
, que formata o resultado do modelo e retorna o conteúdo final.
Se você estiver interessado na saída de qualquer componente, poderá testar uma versão menor da cadeia a qualquer momento, como prompt
ou prompt | modelo
, para ver os resultados intermediários:
input = {"topic": "sorvete"}
prompt.invoke(input)
(prompt | modelo).invoke(input)
Exemplo de Busca RAG
A seguir, explicaremos um exemplo um pouco mais complexo do LCEL. Vamos executar um exemplo de recuperação aprimorada para gerar cadeias, a fim de adicionar algumas informações de contexto ao responder perguntas.
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 trabalhou na kensho", "os ursos gostam de comer mel"],
embedding=OpenAIEmbeddings(),
)
retriever = vectorstore.as_retriever()
template = """Responda a pergunta baseada apenas no seguinte contexto:
{context}
Pergunta: {question}
"""
prompt = ChatPromptTemplate.from_template(template)
modelo = ChatOpenAI()
analise_saida = StrOutputParser()
setup_and_retrieval = RunnableParallel(
{"context": retriever, "question": RunnablePassthrough()}
)
cadeia = setup_and_retrieval | prompt | modelo | analise_saida
cadeia.invoke("onde harrison trabalhou?")
Neste caso, a cadeia composta é:
cadeia = setup_and_retrieval | prompt | modelo | analise_saida
Resumidamente, o modelo de prompt acima aceita contexto
e pergunta
como valores para substituir no modelo de prompt. Antes de construir o modelo de prompt, queremos recuperar documentos relevantes para usar como parte do contexto.
Como teste, utilizamos DocArrayInMemorySearch
para simular um banco de dados de vetores baseado em memória, definindo um recuperador que pode recuperar documentos semelhantes com base em consultas. Este também é um componente executável encadeável, mas você também pode tentar executá-lo separadamente:
retriever.invoke("onde harrison trabalhou?")
Em seguida, utilizamos RunnableParallel
para preparar a entrada para o prompt, buscar documentos usando o recuperador e passar a pergunta do usuário usando RunnablePassthrough
:
setup_and_retrieval = RunnableParallel(
{"context": retriever, "question": RunnablePassthrough()}
)
Em resumo, a cadeia completa é:
setup_and_retrieval = RunnableParallel(
{"context": retriever, "question": RunnablePassthrough()}
)
cadeia = setup_and_retrieval | prompt | modelo | analise_saida
O processo é o seguinte:
- Primeiramente, crie um objeto
RunnableParallel
contendo duas entradas. A primeira entradacontexto
incluirá os resultados do documento extraídos pelo recuperador. A segunda entradapergunta
conterá a pergunta original do usuário. Para passar a pergunta, usamosRunnablePassthrough
para copiar esta entrada. - Passe o dicionário do passo anterior para o componente
prompt
. Ele aceita a entrada do usuário (ou seja,pergunta
) bem como os documentos recuperados (ou seja,contexto
), constrói um prompt e produz umPromptValue
. - O componente
modelo
leva o prompt gerado e o envia para a avaliação do modelo LLM da OpenAI. A saída gerada pelo modelo é um objetoChatMessage
. - Por fim, o componente
analise_saida
leva umaChatMessage
, converte-a em uma string Python e a retorna a partir do métodoinvoke
.