Resumo

  • Transição mais rápida da fase de prova de conceito para a produção: Conforme descrito na documentação da Langchain, “o LCEL é uma forma declarativa de unir cadeias com facilidade. O LCEL foi projetado desde o início para permitir a implantação de protótipos em produção, sem a necessidade de alterações no código”.

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

  • Streaming e processamento em lote prontos para uso: o LCEL oferece recursos de processamento em lote, streaming e assíncrono gratuitamente.

  • Interface unificada: oferece paralelização automática, recursos de tipagem e quaisquer funcionalidades futuras que a LangChain venha a desenvolver.

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

O LangChain tornou-se uma das bibliotecas Python mais utilizadas para interagir com LLMs em menos de um ano, mas era principalmente uma biblioteca destinada a POCs, pois não oferecia a capacidade de criar aplicações complexas e escaláveis.
Tudo mudou em agosto de 2023, quando foi lançada a LangChain Expression Language (LCEL), uma nova sintaxe que preenche a lacuna entre o POC e a produção. Este artigo irá guiá-lo pelos detalhes da LCEL, mostrando como ela simplifica a criação de cadeias personalizadas e por que você deve aprendê-la se estiver desenvolvendo aplicativos LLM!

Prompts, LLM e cadeias: vamos refrescar a memória

Antes de nos aprofundarmos na sintaxe do LCEL, acho que seria útil refrescar a memória sobre conceitos do LangChain, como LLM, prompt ou mesmo uma cadeia.

LLM: No LangChain, LLM é uma abstração do modelo utilizado para gerar respostas, como o OpenAI GPT-3.5, o Claude, etc…

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

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

class="lazyload

Agora que já esclarecemos as definições, vamos supor que queremos criar uma empresa! Precisamos de um nome bem legal e cativante e de um modelo de negócios para ganhar dinheiro!

Exemplo — Nome da empresa e modelo de negócios com redes tradicionais

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

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

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

prompt_template_business = "Dê-me a melhor ideia de modelo de negócios para minha empresa chamada: "
business_model_chain = LLMChain(llm=llm, prompt=PromptTemplate.from_template(prompt_template_business))
business_model_output = business_model_chain(company_name_output["text"])

print(nome_da_empresa_resultado)
print(modelo_de_negócio_resultado)

>>>
>>>

Isso é bastante fácil de entender; dá para perceber um pouco de redundância, mas é algo que dá para lidar.

Vamos adicionar algumas personalizações para lidar com os casos em que o usuário não estiver utilizando nossa cadeia da maneira esperada.
Talvez o usuário insira algo completamente alheio ao objetivo da nossa cadeia? Nesse caso, queremos detectar isso e responder de forma adequada.

Exemplo — Personalização e roteamento com cadeias antigas

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

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

# —- O mesmo código de antes
prompt_template_product = "Qual seria um bom nome para uma empresa que fabrica ?"
company_name_chain = LLMChain(llm=llm, prompt=PromptTemplate.from_template(prompt_template_product))

prompt_template_business = "Dê-me a melhor ideia de modelo de negócios para minha empresa chamada: "
business_model_chain = LLMChain(llm=llm, prompt=PromptTemplate.from_template(prompt_template_business))

# —- Novo código

prompt_template_is_product = (
"Seu objetivo é determinar se a entrada do usuário é um nome de produto plausível"
"Perguntas, saudações, frases longas, nomes de celebridades ou outras entradas irrelevantes não são consideradas nomes de produtos"
"entrada: n"
"Responda apenas com 'Verdadeiro' ou 'Falso' e nada mais"
)

prompt_template_cannot_respond = (
"Não é possível responder à entrada do usuário: n"
"Peça ao usuário para inserir o nome de um produto para que você possa criar uma empresa a partir dele.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, o resultado 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 fica 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 ramificar entre as cadeias.

Começam a surgir vários problemas:

  • O código é um pouco redundante, pois contém muitas partes repetitivas.

  • É difícil distinguir a que LLMChain o LLMChain está vinculado; precisamos rastrear as entradas e saídas para entender isso.

  • É fácil cometer erros nos tipos de saída das cadeias; por exemplo, a saída de `is_a_product_chain()` é um `str` que deve ser posteriormente avaliado como `bool`.

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

O LCEL é uma interface e sintaxe unificadas para escrever cadeias modulares prontas para produção; há muito o que explorar para entender o que isso significa.

Primeiro, vamos tentar entender a nova sintaxe reescrevendo a sequência anterior.

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

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

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

prompt_template_product = "Qual seria um bom nome para uma empresa que fabrica ?"
prompt_template_business = "Dê-me a melhor ideia de modelo de negócios para minha empresa chamada: "

chain = (
PromptTemplate.from_template(prompt_template_product)
| llm
|
| PromptTemplate.from_template(prompt_template_business)
| llm
)

business_model_output = chain.invoke()

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 para o objeto à direita”.

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

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

  • Por que precisamos de um método específico .invoke() em vez de escrever chain()?Vamos entender isso na próxima parte, mas esta é uma prévia do motivo pelo qual o LCEL facilita a industrialização!

Mas será que é realmente mais fácil de combinar criar cadeias dessa maneira?
Vamos testar isso adicionando a função is_a_product_chain() e a ramificação caso a entrada do usuário não esteja correta. Podemos até mesmo definir o tipo da cadeia com o Python Typing; vamos fazer isso como uma boa prática.

Exemplo — Personalização e roteamento com 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(temperature=0)

prompt_template_product = "Qual seria um bom nome para uma empresa que fabrica ?"
prompt_template_cannot_respond = (
"Você não pode responder à entrada do usuário: n"
"Peça ao usuário para inserir o nome de um produto para que você possa criar uma empresa a partir dele.n"
)
prompt_template_business = "Dê-me a melhor ideia de modelo de negócios para minha empresa chamada: "
prompt_template_is_product = (
"Seu objetivo é verificar se a entrada do usuário é um nome de produto plausível"
"Perguntas, saudações, frases longas, nomes de celebridades ou outras entradas irrelevantes não são consideradas nomes de produtos"
"entrada: n"
"Responda apenas com 'Verdadeiro' ou 'Falso' e nada mais"
)

answer_user_chain = (
PromptTemplate.from_template(prompt_template_product)
| llm
|
| 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),
cannot_respond_chain
).with_types(input_type=Dict[str, str], output_type=str)

print(full_chain.invoke())

Vamos listar as diferenças:

  • A sintaxe é diferente, tudo bem, isso é óbvio.

  • Existem cadeias intermediárias definidas e chamadas dentro de uma cadeia maior, quase como funções.

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

  • Isso não parece Python.

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

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

Execução, transmissão e processamento em lote prontos para uso

Ao utilizar o LCEL, sua rede passa a contar automaticamente com:

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

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

  • .stream(): Isso permite que você comece a exibir o início da sugestão antes que ela seja totalmente gerada.

my_chain = prompt | llm

# ———invoke——— #
result_with_invoke = my_chain.invoke(“hello world!”)

# ———lote——— #
result_with_batch = my_chain.batch([“hello”, “world”, “!”])

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

Ao realizar uma iteração, você pode usar o método `invoke` para facilitar o processo de desenvolvimento. Mas, ao exibir o resultado da sua cadeia em uma interface de usuário, é recomendável transmitir a resposta. Agora você pode usar o método `stream` sem precisar reescrever nada.

Métodos assíncronos prontos para uso

Na maioria das vezes, o front-end e o back-end da sua aplicação estarão separados, o que significa que o front-end enviará uma solicitação ao back-end. Se você tiver vários usuários, talvez seja necessário processar várias solicitações no back-end ao mesmo tempo.

Como a maior parte do código no LangChain consiste basicamente em esperar entre chamadas de API, podemos aproveitar o código assíncrono para melhorar a escalabilidade da API. Se você quiser entender por que isso é importante, recomendo ler a história dos “hambúrgueres simultâneos” na documentação do 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 LCEL” da documentação do LangChain, que traz exemplos para cada método síncrono e assíncrono.

O Langchain conseguiu esses recursos “prontos para uso” criando uma interface unificada chamada “Runnable”. Agora, para aproveitar ao máximo o LCEL, precisamos nos aprofundar no que é essa nova interface Runnable.

A interface Runnable

Todos os objetos que usamos na sintaxe LCEL até agora são Runnables. Trata-se de um objeto Python criado pelo LangChain; esse objeto herda automaticamente todas as funcionalidades que mencionamos anteriormente e muito mais. Ao usar a sintaxe LCEL, criamos 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 do tipo Runnable ou dicionários que são automaticamente convertidos em 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
| # <— 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()?

Isso porque todos os Runnable dentro de um RunnableParallel são executados em paralelo. Isso significa que, se você tiver três etapas independentes no seu Runnable, elas serão executadas simultaneamente em diferentes threads da sua máquina, aumentando a velocidade da sua cadeia sem nenhum custo adicional!

Desvantagens do LCEL

Apesar de suas vantagens, o LCEL apresenta algumas desvantagens potenciais:

  • Não está totalmente em conformidade com a PEP: o LCEL não respeita integralmente a PEP 20, o “Zen do Python”, que afirma que “o explícito é melhor do que o implícito”. (Para verificar a PEP 20, você pode executar o comando `import this` no Python.) Além disso, a sintaxe do LCEL não é considerada “Pythônica”, pois parece uma linguagem diferente; isso pode tornar o LCEL menos intuitivo para alguns desenvolvedores de Python, que podem se recusar a usá-lo.

  • O LCEL é uma linguagem de domínio específico (DSL): espera-se que os usuários tenham algum conhecimento sobre prompts, cadeias ou modelos de linguagem de grande escala (LLMs) para poderem utilizar 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á mantê-la em todas as etapas subsequentes. Isso pode resultar em argumentos adicionais na maioria das suas cadeias, que podem não ser utilizados, mas são necessários caso você queira 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 o desenvolvimento de aplicativos em Python. Apesar de sua sintaxe pouco convencional, recomendo vivamente o uso da LCEL pelas seguintes razões:

  • Interface unificada: oferece uma interface consistente para todas as cadeias, facilitando a industrialização do seu código com modelos prontos para uso de fluxo, assíncrono e fallback, tipagem, 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 das suas cadeias e melhorando a experiência do usuário.

  • Componibilidade: permite compor e modificar cadeias com facilidade, tornando seu código mais flexível e adaptável.

Para saber mais…

A abstração executável

Em alguns casos, acredito que seja importante compreender a abstração que o LangChain implementou para que a sintaxe LCEL funcione.

Você pode reimplementar facilmente as funcionalidades básicas da interface Runnable da seguinte maneira:

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 reescrito.
Na prática, o LangChain adicionou diversas funcionalidades, como a conversão de dicionários em Runnable, recursos de tipagem, recursos de configuração e métodos como invoke, batch, stream e async!

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

Se você quiser saber mais, recomendo fortemente que dê uma olhada no livro de receitas do LangChain no LCEL.

class="lazyload

Blog do Medium pela Artefact.

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