LangChain e LCEL: Facilitando a Interação com Modelos de Linguagem
Iniciando com LangChain Expression Language
Me siga no LinkedIn | Apoie a Newsletter | Curso: Converse com seus documentos
O LangChain é um framework que apoia no desenvolvimento de aplicativos baseados por modelos de linguagem. O framework simplifica tarefas como adição de contexto nos prompts, ou de reasoning dos modelos de linguagem.
Uma das formas de interagir com o LangChain é através da LCEL, LangChain Expression Language. A LCEL é uma abordagem declarativa que simplifica o uso do framework para compor chains (ou cadeias) mais facilmente.
A LCEL abrange desde as cadeias mais simples, que combinam duas etapas (um prompt e uma LLM), até cadeias mais complexas, como um pipeline que envolve dezenas ou até centenas de etapas. Além do suporte, como linguagem declarativa, o LCEL é de fácil uso e entendimento.
Nesse texto, vamos demonstrar o uso LCEL, criando pequenas cadeias de iteração de prompts.
Comunicando com o modelo
Antes de começar, precisamos configurar nosso ambiente para trabalhar com LangChain e uma LLM. Para esse texto, utilizaremos a LLM da OpenAI. Logo, precisamos instalar as seguintes dependências:
!pip install langchain openai langchain_openai
Nosso Hello World consiste em realizar uma chamada ao modelo e imprimir o resultado. Para isso, podemos usar a classe ChatOpenAI
.
model = ChatOpenAI(api_key="chave", model="gpt-4", max_tokens=200, temperature=0)
Perceba que para construi o objeto ChatOpenAI
precisamos passar três parâmetros:
api_key
: Chave de acesso ao modelo da OpenAI. Automaticamente inferida da variável de ambiente OPENAI_API_KEY se não fornecida.model
: Escolha do tipo de modelo generativo. O modelo padrão é o gpt-3.5-turbo.max_tokens
: Limite máximo de tokens para gerar respostas.temperature
: Define a variação na resposta. Quanto maior, mais diversa será a resposta. Se zero, as respostas tendem a ser consistentes em requisições consecutivas com o mesmo prompt.
Com o objeto model criado, é possível realizar chamadas ao modelo.
model.invoke("oi, meu nome é Gustavo. Tudo bem com você?")
=> AIMessage(content='Olá, Gustavo! Eu sou uma inteligência artificial, então não tenho sentimentos, mas estou aqui para ajudar. Como posso ajudá-lo hoje?')
Note que o retorno é envelopado na classe AIMessage
. Caso queria acessar somente a resposta, basta acessar a variável content
.
Importante saber que cada chamada ao modelo é stateless. Ou seja, o modelo não guarda o estado das informações que foram passadas previamente. Para observar esse comportamento, basta executar uma nova requisição perguntando sobre algo da requisição anterior:
model.invoke("oi, meu nome é Gustavo. Tudo bem com você?")
=> AIMessage(content='Olá, Gustavo! Eu sou uma inteligência artificial, então não tenho sentimentos, mas estou aqui para ajudar. Como posso ajudá-lo hoje?')
model.invoke("Qual é mesmo o meu nome?")
AIMessage(content='Desculpe, como sou uma inteligência artificial, não tenho acesso a essa informação.')
Como forma de lidar com essa característica, o LangChain fornece recursos para uma construção de memória, mas que não abordaremos nesse texto.
📚 Você é dev e quer aprender um pouco mais sobre a criação de aplicações baseadas em LLM?
Eu criei um curso que aborda aspectos teóricos e práticos do desenvolvimento de aplicações baseadas em LLMs. Alguns dos tópicos cobertos:
🟠 O que são e como criar embeddings
🟡 Como selecionar partes relevantes nos seus documentos
🔵 Como integrar essas partes documentos com uma LLM
Separando o prompt do modelo
No exemplo anterior, usamos o método invoke
da classe ChatOpenAI
para iniciar uma conversa com o modelo. No entanto, há casos em que gostaríamos de fornecer uma instrução prévia para que o modelo se comporte de acordo com alguma característica.
Por exemplo, se estamos construindo um sistema especializado em design de software devemos instruir nosso modelo a se comportar como um expert nessa área. Somente após essa instrução, podemos solicitar ao modelo uma resposta a pergunta do usuário.
Para isso, devemos criar um prompt com essa característica. Para criar um prompt no LangChain, podemos usar a classe ChatPromptTemplate
. Vejamos no exemplo abaixo:
from langchain_core.prompts import ChatPromptTemplate
prompt = ChatPromptTemplate.from_template("Você deve atuar como um expert em design de software. Ajude nosso usuário: {input}")
Agora que temos a definição do modelo
e do prompt
, criaremos nossa primeira cadeia, usando o operador |
(pipe):
chain = prompt | model
O pipe em comportamento similar ao pipe que utilizamos em sistemas unix, em que uma operação é realizada após a execução da operação anterior.
Embora não seja uma sintaxe muito pythonica, pipe é uma construção de código disponível no LCEL que simplifica muito o encadeamento de operações em LLMs. Debaixo dos panos, quando o interpretador Python encontra o operador |
entre dois objetos (como a | b
), ele tenta passar o objeto a
para o método __or__
do objeto b
.
Isso significa que esses padrões são equivalentes:
# padrão tradicional
chain = a.__or__(b)
chain("some input")
# padrão com pipe
chain = a | b
chain("some input")
Após a construção da nossa chain, basta invoca-la usando o método invoke
.
chain.invoke({"input": "quando não devemos refatorar um código?"})
Perceba que para invocar a chain, precisamos passar um dicionário {}
como parâmetro para o método invoke. Passamos como chave para esse dicionário a string "input"
, e como valor, passamos a string da pergunta do usuário ("quando não devemos refatorar um código?")
. Perceba que a chave "input"
é a mesma que usamos no ChatPromptTemplate.from_template
, e o LangChain trata da interpolação de strings. O uso de um dicionário é necessário, uma vez que o prompt pode ter várias parâmetros (passados pelo usuário, ou por outros prompts).
Formatando a saída
Um outro elemento frequentemente empregado na chain, é o analisador de saída (output_parser).
Os analisadores de saída são responsáveis por pegar a saída de um LLM e transformá-la em um formato mais adequado. Isso é muito útil para modificar a forma que os dados estão estruturados. Alguns dos analisadores de saída implementados pelo framework são:
Lista (CommaSeparatedListOutputParser): Retorna os elementos em uma lista separados por vírgula
JSON (): Retorna um objeto JSON conforme especificado. É possível especificar um modelo Pydantic, e ele retornará JSON para esse modelo. Provavelmente o analisador de saída mais confiável para obter dados estruturados que NÃO usam chamadas de função.
XML (): Retorna um dicionário de tags. Use quando a saída XML for necessária, especialmente com modelos eficientes na escrita XML, como o da Anthropic.
Pydantic (): Aceita um modelo Pydantic definido pelo usuário e retorna dados nesse formato.
YAML (): Aceita um modelo Pydantic definido pelo usuário e retorna dados nesse formato, utilizando YAML para a codificação.
PandasDataFrame: Útil para realizar operações com pandas DataFrames.
Por padrão, se utiliza o StrOutputParser, que é um parser de string. Como é um parser padrão, não é necessário invocá-lo. Mas, sua utilização é a mesma que a seguinte:
from langchain_core.output_parsers import StrOutputParser
chain = prompt | model | StrOutputParser()
Para usar outros parsers, basta alterar o objeto a ser fornecido. Por exemplo:
from langchain_core.output_parsers import CommaSeparatedListOutputParser
chain = prompt | model | CommaSeparatedListOutputParser()
chain.invoke({"input": "explique 3 principais práticas de design?"})
Para outros parsers, como o JSON, é preciso também definir qual será o formato do documento resultante. Para isso, podemos definir uma classe usando pydantic, indicando o corpo do documento JSON.
from langchain_core.output_parsers import JsonOutputParser
from langchain_core.pydantic_v1 import BaseModel, Field
from langchain.prompts import PromptTemplate
class Design(BaseModel):
design: str = Field(description="nome da pratica de design")
descricao: str = Field(description="descricao da pratica de design")
# criando parser com formato esperado
parser = JsonOutputParser(pydantic_object=Design)
prompt = ChatPromptTemplate.from_template("Você deve atuar como um expert em design de software. Ajude nosso usuário: {format_instructions}\n\n{input}")
prompt = prompt.partial(format_instructions=parser.get_format_instructions())
chain = prompt | model | parser
chain.invoke({"input": "a prática de design chamada SOLID"})
Além de criar o objeto Design
e passa-lo para nosso parser JsonOutputParser(pydantic_object=Design)
, foi necessário também mudar nosso prompt, para que este seja instruído da nova formatação. Como resultado, tivemos nossa saída formatada como JSON, que facilita a integração da resposta da LLM com demais serviços. Isso tudo, adicionando um pipe a mais na nossa chain.
Conclusão
O LangChain é um framework que simplifica o desenvolvimento de aplicativos baseados em modelos de linguagem. Um dos seus destaques reside na LangChain Expression Language (LCEL), uma abordagem declarativa que facilita a criação de cadeias de operações, desde prompts simples até pipelines complexos.
Demonstrando seu uso, o texto ilustra a interação com a OpenAI's LLM, exemplificando a criação de cadeias de iteração de prompts e a separação do prompt do modelo. Além disso, aborda recursos como construção de memória e formatação de saída, destacando a versatilidade do LangChain na construção de aplicações baseadas em LLMs.