TL;DR

  • POC mais rápido para produção: Conforme descrito na documentação do langchain, "o LCEL é uma maneira declarativa de compor cadeias facilmente. O LCEL foi projetado desde o primeiro dia para dar suporte à colocação de protótipos em produção, sem alteração de código".

  • Criação de cadeias personalizadas: O LCEL simplifica o processo de criação de cadeias personalizadas com uma nova sintaxe.

  • Transmissão e lote prontos para uso: O LCEL oferece gratuitamente recursos de lote, streaming e assíncrono.

  • Interface unificada: Oferece paralelização automática, recursos de digitação e qualquer recurso futuro que a LangChain possa desenvolver.

  • O LCEL é o futuro do LangChain: O LCEL oferece uma nova perspectiva sobre o desenvolvimento de aplicativos baseados em LLM. Recomendo enfaticamente que você o utilize em seu próximo projeto de LLM.

A LangChain se tornou uma das bibliotecas Python mais usadas para interagir com LLMs em menos de um ano, mas a LangChain era principalmente uma biblioteca para POCs, pois não tinha a capacidade de criar aplicativos complexos e dimensionáveis.
Tudo mudou em agosto de 2023, quando eles lançaram a LangChain Expression Language (LCEL), uma nova sintaxe que preenche a lacuna entre POC e produção. Este artigo o guiará pelos prós e contras do LCEL, mostrando como ele simplifica a criação de cadeias personalizadas e por que você deve aprendê-lo se estiver criando aplicativos LLM!

Prompts, LLM e cadeias, vamos refrescar nossa memória

Antes de mergulhar na sintaxe do LCEL, acho que é bom refrescar nossa memória sobre os conceitos do LangChain, como LLM e Prompt ou até mesmo uma cadeia.

LLM: Na langchain, o llm é uma abstração em torno do modelo usado para fazer as conclusões, como openai gpt3.5, claude, etc...

Prompt: Essa é a entrada do objeto LLM, que fará perguntas ao LLM e fornecerá seus objetivos.

Cadeia: Refere-se a uma sequência de chamadas a um LLM ou a qualquer etapa de processamento do site data .

class="lazyload

Agora que as definições estão resolvidas, vamos supor que queremos criar uma empresa! Precisamos de um nome muito legal e cativante e de um modelo de negócios para ganhar dinheiro!

Exemplo - Nome da empresa e modelo de negócios com cadeias antigas

from langchain.chains import LLMChain
from langchain.prompts import PromptTemplate
from langchain_community.llms import OpenAI

USER_INPUT = "meias coloridas"
llm = OpenAI(temperatura=0)

prompt_template_product = "Qual é um bom nome para uma empresa que fabrica {produto}?"
company_name_chain = LLMChain(llm=llm, prompt=PromptTemplate.from_template(prompt_template_product))
company_name_output = company_name_chain(USER_INPUT)

prompt_template_business = “Give me the best business model idea for my company named: {company}”
business_model_chain = LLMChain(llm=llm, prompt=PromptTemplate.from_template(prompt_template_business))
business_model_output = business_model_chain(company_name_output[“text”])

print(company_name_output)
print(business_model_output)

>>> {‘product’: ‘colorful socks’, ‘text’: ‘Socktastic!’}
>>> {‘company’: ‘Socktastic!’,’text’: “A subscription-based service offering a monthly delivery…”}

Isso é bastante fácil de seguir, podemos ver um pouco de redundância, mas é gerenciável.

Vamos adicionar alguma personalização, tratando os casos em que o usuário não está usando a nossa cadeia como esperado.
Talvez o usuário insira algo completamente não relacionado ao objetivo da nossa cadeia? Nesse caso, queremos detectar isso e responder adequadamente.

Exemplo - Personalização e roteamento com cadeias antigas

from langchain.chains import LLMChain
from langchain.prompts import PromptTemplate
from langchain_community.llms import OpenAI
importar ast

USER_INPUT = "Harrison Chase"
llm = OpenAI(temperatura=0)

# -- O mesmo código anterior
prompt_template_product = "Qual é um bom nome para uma empresa que fabrica {product}?"
cadeia_nome_da_empresa = Cadeia LLMC(llm=llm, prompt=PromptTemplate.from_template(prompt_template_product))

prompt_template_business = “Give me the best business model idea for my company named: {company}”
business_model_chain = LLMChain(llm=llm, prompt=PromptTemplate.from_template(prompt_template_business))

# -- Novo código

prompt_template_is_product = (
“Your goal is to find if the input of the user is a plausible product namen”
“Questions, greetings, long sentences, celebrities or other non relevant inputs are not considered productsn”
“input: {product}n”
“Answer only by ‘True’ or ‘False’ and nothing moren”
)

prompt_template_cannot_respond = (
“You cannot respond to the user input: {product}n”
“Ask the user to input the name of a product in order for you to make a company out of it.n”
)

cannot_respond_chain = LLMChain(llm=llm, prompt=PromptTemplate.from_template(prompt_template_cannot_respond))
company_name_chain = LLMChain(llm=llm, prompt=PromptTemplate.from_template(prompt_template_product))
business_model_chain = LLMChain(llm=llm, prompt=PromptTemplate.from_template(prompt_template_business))
is_a_product_chain = LLMChain(llm=llm, prompt=PromptTemplate.from_template(prompt_template_is_product))

# Se usarmos bool em uma string não vazia, será True, portanto, precisamos de `literal_eval`
is_a_product = ast.literal_eval(is_a_product_chain(USER_INPUT)["text"])
if is_a_product:
company_name_output = company_name_chain(USER_INPUT)
business_model_output = business_model_chain(company_name_output["text"])
print(business_model_output)
else:
print(cannot_respond_chain(USER_INPUT))

Isso se torna um pouco mais difícil de entender, vamos resumir:

  • Criamos uma nova cadeia is_real_product_chain() que detecta se a entrada do usuário pode ser considerada um produto.

  • Implementamos condições if/else para alternar entre as cadeias.

Vários problemas começam a surgir:

  • O código é um pouco redundante, pois há muito padrão.

  • É difícil distinguir qual LLMChain está vinculada a qual LLMChain, precisamos rastrear as entradas e saídas para entendê-la.

  • Podemos facilmente cometer erros nos tipos de saída das cadeias, por exemplo, a saída de is_a_product_chain() é um str que deveria ser avaliado posteriormente como bool.

O que é a Linguagem de Expressão LangChain (LCEL)?

O LCEL é uma interface e uma sintaxe unificadas para escrever cadeias prontas de produção compostas, mas há muito a ser desvendado para entender o que isso significa.

Primeiro, tentaremos entender a nova sintaxe reescrevendo a cadeia anterior.

Exemplo - Nome da empresa e modelo de negócios com a LCEL

from langchain_core.runnables import RunnablePassthrough
from langchain.prompts import PromptTemplate
from langchain_community.llms import OpenAI

USER_INPUT = "meias coloridas"
llm = OpenAI(temperatura=0)

prompt_template_product = “What is a good name for a company that makes {product}?”
prompt_template_business = “Give me the best business model idea for my company named: {company}”

chain = (
PromptTemplate.from_template(prompt_template_product)
| llm
| {‘company’: RunnablePassthrough()}
| PromptTemplate.from_template(prompt_template_business)
| llm
)

business_model_output = chain.invoke({‘product’: ‘colorful socks’})

Muito código incomum, em apenas algumas linhas:

  • Há um operador | estranho entre um PromptTemplate, um llm e um dicionário...! O operador | está aqui simplesmente para dizer: "pegue o dicionário à esquerda e passe-o como entrada do objeto à direita".

  • Por que estamos passando a variável product dentro de um dicionário em vez de uma string como antes? Se você leu o item 1, sabe que o operador | espera que as entradas sejam um dicionário, portanto, fornecemos o argumento product dentro de um dicionário.

  • Por que há um nome de função RunnablePassthrough() em vez do nome da empresa? A função RunnablePassthrough() é um espaço reservado para dizer: "não temos o nome da empresa por enquanto, mas quando o tivermos, coloque-o aqui". Explicarei o que o termo "Runnable" significa nas próximas partes; por enquanto, não há problema em ignorá-lo.

  • Why do we need a specific method .invoke() instead of writing chain({‘product’:’colorful socks’}) ?We will understand this in the next part, but it is a sneak peek to why LCEL makes industrialization easier!

Mas será que é realmente mais fácil criar cadeias dessa forma?
Vamos testá-la adicionando a função is_a_product_chain() e a ramificação se a entrada do usuário não estiver correta. Podemos até mesmo digitar a cadeia com o Python Typing, vamos fazer isso como uma boa prática.

Exemplo - Personalização e roteamento com o LCEL

from typing import Dict
from langchain_core.runnables import RunnablePassthrough, RunnableBranch
from langchain.prompts import PromptTemplate
from langchain_core.output_parsers import StrOutputParser
from langchain.output_parsers import BooleanOutputParser
from langchain_community.llms import OpenAI

USER_INPUT = "Harrrison Chase"
llm = OpenAI(temperatura=0)

prompt_template_product = “What is a good name for a company that makes {product}?”
prompt_template_cannot_respond = (
“You cannot respond to the user input: {product}n”
“Ask the user to input the name of a product in order for you to make a company out of it.n”
)
prompt_template_business = “Give me the best business model idea for my company named: {company}”
prompt_template_is_product = (
“Your goal is to find if the input of the user is a plausible product namen”
“Questions, greetings, long sentences, celebrities or other non relevant inputs are not considered productsn”
“input: {product}n”
“Answer only by ‘True’ or ‘False’ and nothing moren”
)

answer_user_chain = (
PromptTemplate.from_template(prompt_template_product)
| llm
| {‘company’: RunnablePassthrough()}
| PromptTemplate.from_template(prompt_template_business)
| llm
).with_types(input_type=Dict[str, str], output_type=str)

is_product_chain = (
PromptTemplate.from_template(prompt_template_is_product)
| llm
| BooleanOutputParser(true_val='True', false_val='False')
).with_types(input_type=Dict[str, str], output_type=bool)

cannot_respond_chain = (
PromptTemplate.from_template(prompt_template_cannot_respond) | llm
).with_types(input_type=Dict[str, str], output_type=str)

full_chain = RunnableBranch(
(is_product_chain, answer_user_chain),
cadeia_não_responde
).with_types(input_type=Dict[str, str], output_type=str)

print(full_chain.invoke({‘product’: USER_INPUT}))

Vamos listar as diferenças:

  • A sintaxe é diferente, o que é óbvio.

  • Há cadeias intermediárias definidas e chamadas em uma cadeia maior, quase como funções.

  • As entradas e saídas são digitadas, quase como funções.

  • Isso não se parece com o python.

Por que o LCEL é melhor para a industrialização?

Se eu estivesse lendo este artigo até este ponto exato e alguém me perguntasse se eu estava convencido sobre o LCEL, eu provavelmente diria que não. A sintaxe é muito diferente, e provavelmente posso organizar meu código em funções para obter quase o mesmo código. Mas estou aqui, escrevendo este artigo, portanto, deve haver algo mais.

Invocação, fluxo e lote prontos para uso

Ao usar o LCEL, sua cadeia automaticamente tem:

  • .invoke(): Você deseja passar sua entrada e obter a saída, nada mais, nada menos.

  • .batch(): Se você quiser passar várias entradas para obter várias saídas, a paralelização será feita para você (mais rápido do que chamar invoke 3 vezes).

  • .stream(): Permite que você comece a imprimir o início da conclusão antes que a conclusão completa seja concluída.

my_chain = prompt | llm

# ---invoke--- #
result_with_invoke = my_chain.invoke("hello world!")

---batch--- #
result_with_batch = my_chain.batch(["hello", "world", "!"])

# ---stream--- #
for chunk in my_chain.stream("hello world!"):
print(chunk, flush=True, end="")

Ao iterar, você pode usar o método invoke para facilitar o processo de desenvolvimento. Mas, ao mostrar a saída da cadeia em uma interface do usuário, você deseja transmitir a resposta. Agora você pode usar o método stream sem reescrever nada.

Métodos assíncronos prontos para uso

Na maioria das vezes, o frontend e o backend do seu aplicativo estarão separados, o que significa que o frontend fará uma solicitação ao backend. Se você tiver vários usuários, talvez precise lidar com várias solicitações no backend ao mesmo tempo.

Como a maior parte do código no LangChain está apenas aguardando entre as chamadas de API, podemos aproveitar o código assíncrono para melhorar a escalabilidade da API. Se quiser entender por que isso é importante, recomendo a leitura da história dos hambúrgueres simultâneos da documentação da FastAPI.
Não há necessidade de se preocupar com a implementação, pois os métodos assíncronos já estão disponíveis se você usar o LCEL:

.ainvoke() / .abatch() / .astream: versões assíncronas de invoke, batch e stream.

Também recomendo a leitura da página Por que usar o LCEL da documentação da LangChain com exemplos de cada método de sincronização/assincronização.

A Langchain obteve esses recursos "prontos para uso" criando uma interface unificada chamada "Runnable". Agora, para aproveitar totalmente o LCEL, precisamos nos aprofundar no que é essa nova interface Runnable.

A interface Runnable

Todos os objetos que usamos na sintaxe do LCEL até agora são Runnables. Trata-se de um objeto python criado pelo LangChain, que herda automaticamente todos os recursos de que falamos anteriormente e muito mais. Ao usar a sintaxe do LCEL, compomos um novo Runnable a cada etapa, o que significa que o objeto final criado também será um Runnable. Você pode saber mais sobre a interface na documentação oficial.

Todos os objetos do código abaixo são Runnable ou dicionários que são automaticamente convertidos em um Runnable:

from langchain_core.runnables import RunnablePassthrough, RunnableParallel
from langchain.prompts import PromptTemplate
from langchain_community.llms import OpenAI

chain_number_one = (
PromptTemplate.from_template(prompt_template_product)
| llm
| {‘company’: RunnablePassthrough()} # <— THIS WILL CHANGE
| PromptTemplate.from_template(prompt_template_business)
| llm
)

chain_number_two = (
PromptTemplate.from_template(prompt_template_product)
| llm
| RunnableParallel(company=RunnablePassthrough()) # <— THIS CHANGED
| PromptTemplate.from_template(prompt_template_business)
| llm
)

print(chain_number_one == chain_number_two)
>>> True

Por que usamos RunnableParallel() e não simplesmente Runnable()?

Porque cada Runnable dentro de um RunnableParallel é executado em paralelo. Isso significa que, se você tiver 3 etapas independentes em seu Runnable, elas serão executadas ao mesmo tempo em diferentes threads de sua máquina, aumentando a velocidade de sua cadeia gratuitamente!

Desvantagens do LCEL

Apesar de suas vantagens, a LCEL tem algumas possíveis desvantagens:

  • Não é totalmente compatível com a PEP: O LCEL não respeita totalmente o PEP20, o Zen do Python, que afirma que "explícito é melhor do que implícito". (Além disso, a sintaxe do LCEL não é considerada "pitônica", pois parece uma linguagem diferente, o que pode tornar o LCEL menos intuitivo para alguns desenvolvedores Pyhton que podem se recusar a usá-lo.

  • O LCEL é uma linguagem específica de domínio (DSL): Espera-se que os usuários tenham algum conhecimento de prompts, cadeias ou LLMs para aproveitar a sintaxe de forma eficiente.

  • Dependências de entrada/saída: As entradas intermediárias e as saídas finais devem ser transmitidas do início ao fim. Por exemplo, se você quiser usar a saída de uma etapa intermediária como saída final, deverá passá-la por todas as etapas subsequentes. Isso pode levar a argumentos extras na maioria das cadeias, que podem não ser usados, mas são necessários se você quiser acessá-los por meio da saída.

Conclusão

Em conclusão, a LangChain Expression Language (LCEL) é uma ferramenta poderosa que traz uma nova perspectiva para a criação de aplicativos Python. Apesar de sua sintaxe não convencional, recomendo fortemente o uso da LCEL pelos seguintes motivos:

  • Interface unificada: Fornece uma interface consistente para todas as cadeias, facilitando a industrialização de seu código com fluxo pronto para uso, assíncrono, modelos de fallback, digitação, configurações de tempo de execução, etc.

  • Paralelização automática: Execute automaticamente várias tarefas em paralelo, aumentando a velocidade de execução de suas cadeias e melhorando a experiência do usuário.

  • Capacidade de composição: Permite que você componha e modifique cadeias facilmente, tornando seu código mais flexível e adaptável.

Para ir além...

A abstração executável

Em alguns casos, acredito que seja importante entender a abstração que a LangChain implementou para fazer a sintaxe do LCEL funcionar.

Você pode reimplementar as funcionalidades básicas do Runnable facilmente da seguinte forma:

class Runnable:
def __init__(self, func):
self.func = func

def __or__(self, other):
def chained_func(*args, **kwargs):
# self.func está à esquerda, other está à direita
return other(self.func(*args, **kwargs))
return Runnable(chained_func)

def __call__(self, *args, **kwargs):
return self.func(*args, **kwargs)

def add_ten(x):
return x + 10

def divide_by_two(x):
return x / 2

runnable_add_ten = Runnable(add_ten)
runnable_divide_by_two = Runnable(divide_by_two)
chain = runnable_add_ten | runnable_divide_by_two
result = chain(8) # (8+10) / 2 = 9,0 deve ser a resposta
print(result)
>>> 9.0

Um Runnable é simplesmente um objeto python no qual o método .__or__() foi substituído.
Na prática, o LangChain adicionou muitas funcionalidades, como a conversão de dicionários em Runnable, recursos de tipagem, recursos de configuração e métodos de invocação, lote, fluxo e assíncrono!

Então, por que não experimentar o LCEL em seu próximo projeto?

Se você quiser saber mais, recomendo que consulte o livro de receitas do LangChain no LCEL.

class="lazyload

Blog do Medium por Artefact.

Este artigo foi publicado inicialmente no Medium.com.
Siga-nos em nosso blog no Medium!