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:

  1. 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".
  2. Passe o parâmetro chamado {"topic": "sorvete"} para o primeiro componente da cadeia, prompt, que formata o modelo do prompt para obter o prompt Me conte uma piadinha sobre sorvete.
  3. Passe o prompt Me conte uma piadinha sobre sorvete para o modelo (modelo gpt4).
  4. Passe o resultado retornado pelo modelo para o analisador de saída output_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:

  1. Primeiramente, crie um objeto RunnableParallel contendo duas entradas. A primeira entrada contexto incluirá os resultados do documento extraídos pelo recuperador. A segunda entrada pergunta conterá a pergunta original do usuário. Para passar a pergunta, usamos RunnablePassthrough para copiar esta entrada.
  2. 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 um PromptValue.
  3. 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 objeto ChatMessage.
  4. Por fim, o componente analise_saida leva uma ChatMessage, converte-a em uma string Python e a retorna a partir do método invoke.