TL;DR
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 .

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