TL;DR

  • POC mais rápido para o produto: Conforme descrito na documentação do langchain, “o LCEL é uma forma 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 correntes personalizadas: O LCEL simplifica o processo de criação de cadeias personalizadas com uma nova sintaxe.

  • Streaming e lote fora da caixa: O LCEL oferece ao senhor recursos de lote, streaming e assíncrono gratuitamente.

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

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

LangChain tornou-se um dos mais usados Biblioteca Python 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 Linguagem de expressão LangChain (LCEL), uma nova sintaxe que preenche a lacuna de Do POC à produção. Este artigo o guiará pelos prós e contras do LCEL, mostrando-lhe como ele simplifica o processo de criação de cadeias personalizadas e por que o senhor deve aprendê-la se estiver construindo Inscrições para o 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: Em langchain, o llm é uma abstração em torno do modelo usado para fazer as conclusões, como openai gpt3.5, claude, etc...

Prompt: Esta é 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 data.

Agora que as definições estão resolvidas, vamos supor que o senhor queira 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
de langchain_community.llms import OpenAI

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

prompt_template_product = "Qual é 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(company_name_output)
print(business_model_output)

>>>
>>>

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

Vamos adicionar alguma personalização, tratando os casos em que o usuário não está usando nossa cadeia conforme o 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
de langchain_community.llms import OpenAI
importar ast

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

# -- O mesmo código anterior
prompt_template_product = "Qual é 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 é descobrir se a entrada do usuário é um nome de produto plausível."
"Perguntas, saudações, frases longas, celebridades ou outros insumos não relevantes não são considerados produtosn"
"input: n"
"Responda apenas com 'Verdadeiro' ou 'Falso' e nada mais."
)

prompt_template_cannot_respond = (
"O senhor não pode responder à entrada do usuário: n"
"Peça ao usuário para inserir o nome de um produto para que o senhor crie 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, ela 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)
E mais:
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 LangChain Expression Language (LCEL)?

O LCEL é uma interface e uma sintaxe unificadas para escrever cadeias prontas de produção compostas, mas há muito o que desvendar 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
de langchain_community.llms import OpenAI

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

prompt_template_product = "Qual é 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()

Um monte de 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 o senhor leu o #1, sabe que o operador | espera que as entradas sejam um dicionário, portanto, damos ao argumento product o produto dentro de um dicionário.

  • Por que há um nome de função RunnablePassthrough() em vez do nome da empresa? 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 significa o termo “Runnable” nas próximas partes; por enquanto, não há problema em ignorá-lo.

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

Mas será que é realmente mais fácil criar cadeias dessa forma?
Vamos testá-lo adicionando 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
de langchain_community.llms import OpenAI

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

prompt_template_product = "Qual é um bom nome para uma empresa que fabrica ?"
prompt_template_cannot_respond = (
"O senhor não pode responder à entrada do usuário: n"
"Peça ao usuário para inserir o nome de um produto para que o senhor crie 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 é descobrir se a entrada do usuário é um nome de produto plausível."
"Perguntas, saudações, frases longas, celebridades ou outros insumos não relevantes não são considerados produtosn"
"input: 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, ok, isso é um fato.

  • 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 exatamente 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(): O senhor deseja passar sua entrada e obter a saída, nada mais, nada menos.

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

  • .stream(): Isso permite que o senhor 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, o senhor 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, o senhor deseja transmitir a resposta. Agora o senhor 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 tiver vários usuários, talvez seja necessário 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. história de 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 o senhor usar o LCEL:

.ainvoke() / .abatch() / .astreamVersão assíncrona de invoke, batch e stream.

Também recomendo a leitura do Por que usar a página LCEL da documentação do LangChain? com exemplos para cada método sincronizado/assíncrono.

A Langchain obteve esses recursos “prontos para uso” ao criar uma interface unificada chamada “Runnable” (executável)”. 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. O senhor 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 Runnable:

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

chain_number_one = (
PromptTemplate.from_template(prompt_template_product)
| llm
| # <- ISSO VAI MUDAR
| PromptTemplate.from_template(prompt_template_business)
| llm
)

chain_number_two = (
PromptTemplate.from_template(prompt_template_product)
| llm
| RunnableParallel(company=RunnablePassthrough()) # <- ISSO MUDOU
| 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 o senhor tiver três etapas independentes no seu Runnable, elas serão executadas ao mesmo tempo em diferentes threads da sua máquina, aumentando a velocidade da sua cadeia gratuitamente!

Desvantagens do LCEL

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

  • Não está totalmente em conformidade com o 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 o senhor quiser usar a saída de uma etapa intermediária como saída final, deverá transmiti-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 o senhor 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 o senhor componha e modifique cadeias com facilidade, 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.

O senhor pode reimplementar as funcionalidades básicas do Runnable facilmente da seguinte forma:

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

def __or__(self, other):
def chained_func(*args, **kwargs):
# self.func está à esquerda, o outro 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):
Retorno x + 10

def divide_by_two(x):
retornar x / 2

runnable_add_ten = Runnable(add_ten)
runnable_divide_by_two = Runnable(divide_by_two)
chain = runnable_add_ten | runnable_divide_by_two
resultado = 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 o senhor quiser saber mais, recomendo vivamente que navegue Livro de receitas do LangChain no LCEL.

Média Blog por Artefact.

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