Resumo
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 .

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:
Começam a surgir vários problemas:
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:
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:
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:
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:
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:
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.

BLOG






