Python & AI Tutorials Logo
LangChain & LangGraph

15. Construindo Seu Primeiro Grafo com LangGraph

Na Parte IV, definimos ferramentas, conectamos elas a um LLM e completamos um loop de agente que repete o ciclo de decidir-e-executar. Dirigir o loop, executar ferramentas quando o LLM as solicitava, saber quando parar — codificamos cada parte desse fluxo à mão.

Neste capítulo, vamos construir o mesmo agente de uma maneira totalmente diferente. Em vez de escrever o fluxo diretamente, vamos registrar etapas (nós) e regras de conexão (arestas) com o framework LangGraph e deixar que ele cuide da execução. O comportamento é idêntico ao do Capítulo 14, mas a maneira como construímos muda.

Este capítulo aborda quatro conceitos centrais — StateGraph, nós, arestas e State (estado) — e depois refatora o loop de agente do Capítulo 14 em um grafo do LangGraph. Nos capítulos seguintes, o Capítulo 16 aborda roteamento condicional e componentes pré-construídos, e o Capítulo 17 aborda persistência de estado que permite que um agente retome de onde foi interrompido.

15.1) Por Que Grafos?

15.1.1) Limitações do Loop de Agente Existente

Vamos revisitar o loop de agente do Capítulo 14. Retirando o tratamento de erros e outros detalhes, a estrutura central era assim:

python
# Loop de agente do Capítulo 14 — estrutura central (simplificada)
messages = [
    SystemMessage(content="You are a helpful assistant."),
    HumanMessage(content=user_input),
]
 
for step in range(max_steps):
    # Pede ao LLM para decidir a próxima ação
    ai_message = llm_with_tools.invoke(messages)
    messages.append(ai_message)
 
    # Se nenhuma chamada de ferramenta for solicitada, retorna a resposta final
    if not ai_message.tool_calls:
        return ai_message.content
 
    # Executa as ferramentas solicitadas
    for tool_call in ai_message.tool_calls:
        selected_tool = tool_map[tool_call["name"]]
        tool_message = selected_tool.invoke(tool_call)
        messages.append(tool_message)

Este código cobre apenas o básico e nada mais. Em um ambiente de produção real, porém, muito mais é necessário. Aqui estão alguns exemplos.

  • Recuperação de falhas — Se um agente falhar na etapa 7 de uma tarefa de pesquisa de 10 etapas, ele deveria ser capaz de retomar a partir da etapa 7 em vez de começar tudo de novo desde o início.
  • Solicitações de aprovação — Antes de um agente realizar uma operação crítica, ele deveria ser capaz de pausar e perguntar a um humano "Posso prosseguir?".
  • Monitoramento em tempo real — Os usuários deveriam ser capazes de ver o que o agente está fazendo no momento e quais ferramentas ele está chamando.
  • Visualização e depuração — Um diagrama mostrando como o agente opera deveria estar disponível para que, quando surgirem problemas, você possa rastrear qual etapa deu errado.

Implementar esses recursos por conta própria não é impossível, mas também não é fácil. Só a recuperação de falhas exige escrever código para serializar o estado a cada etapa, salvá-lo em disco, restaurá-lo e retomar exatamente na posição certa. Você poderia acabar com mais código de infraestrutura do que lógica de negócio.

O LangGraph foi construído para fornecer esses recursos no nível do framework. Recuperação de falhas, solicitações de aprovação, monitoramento, visualização — o framework cuida de tudo isso. Mas há um requisito: você precisa construir seu agente em uma estrutura que o framework consiga entender.

O loop de agente do Capítulo 14 lida com toda a lógica diretamente, então não há nada em que o framework possa se conectar. Para aproveitar o que o LangGraph oferece, precisamos reconstruir o agente em uma estrutura que o LangGraph entenda — um grafo. É disso que trata este capítulo.

15.1.2) O Que É o LangGraph?

O LangGraph é um framework de orquestração que define e executa fluxos de trabalho de agentes como grafos. Um grafo aqui significa uma estrutura em que cada nó (etapa) que o agente executa é conectado por arestas (regras de conexão).

No LangGraph, você divide o fluxo de trabalho em nós independentes e os conecta com arestas. O LangGraph então percorre o grafo, executando cada nó ao longo do caminho. Veja como o loop de agente do Capítulo 14 fica quando expresso como um grafo:

Sim

Não

START

Chamada do LLM

Chamada de ferramenta solicitada?

Execução da Ferramenta

END

As caixas retangulares são nós, e as setas são arestas. O losango representa uma aresta condicional que se ramifica para caminhos diferentes com base em uma condição.

No Capítulo 14, todo o fluxo de trabalho vivia em loops for, verificações if e outro código escrito à mão. Com o LangGraph, você define o que cada nó faz e conecta os nós entre si com arestas. Em resumo, você passa de codificar o fluxo de trabalho para declará-lo como estrutura.

O LangGraph não substitui nada que você aprendeu nos Capítulos 12–14. Definições de ferramentas, bind_tools(), tool_calls, ToolMessage — tudo isso ainda é usado dentro dos nós, exatamente como antes.

A próxima seção aborda os componentes centrais do LangGraph — StateGraph, State, nós e arestas — um de cada vez.

15.2) Componentes do LangGraph: StateGraph, State, Nós, Arestas

Esta seção percorre os quatro componentes centrais do LangGraph um de cada vez. Vamos começar com o StateGraph — a classe que agrupa State, nós e arestas em um grafo — e depois abordar cada uma das partes (State, nós, arestas) que ficam dentro dele.

15.2.1) StateGraph

O StateGraph é a classe usada para construir grafos no LangGraph. Você especifica o State que o grafo irá gerenciar, adiciona nós, os conecta com arestas e então faz o compile para produzir um grafo executável.

Vamos ver como funciona.

Criando uma Instância de StateGraph

Chame o construtor StateGraph para criar uma instância. Você precisa passar o esquema do State (a própria classe) como parâmetro. Aqui estamos usando MessagesState, um State predefinido que o LangGraph fornece para gerenciar listas de mensagens. Abordaremos os detalhes em 15.2.2.

python
from langgraph.graph import StateGraph, MessagesState
 
builder = StateGraph(MessagesState)

Adicionando Nós

Use add_node() para registrar um nó. Um nó é uma função Python que recebe o State atual e retorna as partes que deseja alterar. Abordaremos as funções de nó em detalhes em 15.2.3.

python
def say_hello(state: MessagesState):
    return {"messages": [{"role": "ai", "content": "hello world"}]}
 
builder.add_node(say_hello)    # o nome do nó se torna "say_hello"

Conectando Arestas

Use add_edge(source, target) para conectar nós. source é onde a aresta começa; target é para onde ela vai. START e END são marcadores especiais para os pontos de entrada e saída do grafo. Abordaremos arestas em 15.2.4.

python
from langgraph.graph import START, END
 
builder.add_edge(START, "say_hello")   # o grafo começa → executa say_hello
builder.add_edge("say_hello", END)     # say_hello termina → encerra o grafo

Compilando e Executando

Chamar compile() valida a estrutura do grafo e produz um objeto executável. Você executa o grafo compilado com invoke(), passando os valores iniciais do State.

python
graph = builder.compile()
 
initial_state = {"messages": [{"role": "user", "content": "hi!"}]}
result = graph.invoke(initial_state)

Agora vamos juntar tudo e construir um grafo simples:

python
from langgraph.graph import StateGraph, MessagesState, START, END
 
def say_hello(state: MessagesState):
    return {"messages": [{"role": "ai", "content": "hello world"}]}
 
builder = StateGraph(MessagesState)
 
builder.add_node(say_hello)
builder.add_edge(START, "say_hello")
builder.add_edge("say_hello", END)
 
graph = builder.compile()
 
initial_state = {"messages": [{"role": "user", "content": "hi!"}]}
result = graph.invoke(initial_state)
print(result["messages"][-1].content)

Saída:

hello world

Quando você chama invoke(), o grafo é executado na ordem STARTsay_helloEND. say_hello retornou um dicionário com messages como chave, e esse valor foi anexado à lista messages em MessagesState. Vamos nos aprofundar em como isso funciona em 15.2.2. O resultado é que extrair o conteúdo da última mensagem nos dá "hello world".

15.2.2) State: Dados Que Fluem Pelo Grafo

O State são os dados que todos os nós do grafo compartilham. Quando um nó é executado, ele recebe o State atual, faz seu trabalho e retorna apenas as partes que deseja alterar. O LangGraph incorpora essas alterações de volta ao State e entrega a versão atualizada ao próximo nó.

Definindo o State

Você define o State fazendo subclasse de TypedDict. Escolha os campos e tipos que correspondem ao que seu agente precisa rastrear. Aqui está um exemplo simples:

python
from typing_extensions import TypedDict
 
class AgentState(TypedDict):
    messages: list       # lista de mensagens
    llm_calls: int       # contagem de chamadas ao LLM

A partir daqui, você passa AgentState ao criar um StateGraph e o usa como dica de tipo para suas funções de nó.

Reducers

Quando um nó retorna um valor, o campo correspondente do State é atualizado. O comportamento padrão é sobrescrever — se um nó retornar {"llm_calls": 3}, llm_calls simplesmente se torna 3, não importa o que fosse antes.

Mas alguns campos precisam anexar, não sobrescrever. O que acontece se messages for sobrescrito? Toda vez que um nó retorna uma nova mensagem, todo o histórico da conversa desaparece. Para messages, anexar é o comportamento correto.

O LangGraph permite definir uma estratégia de atualização diferente por campo através de uma função reducer. Você especifica o reducer como o segundo argumento em Annotated:

python
from typing_extensions import TypedDict, Annotated
from langgraph.graph.message import add_messages
 
class AgentState(TypedDict):
    messages: Annotated[list, add_messages]   # reducer: anexar
    llm_calls: int                            # sem reducer: sobrescrever

add_messages é um reducer fornecido pelo LangGraph. Em vez de substituir a lista, ele anexa novas mensagens ao que já existe. Como esse reducer está definido no campo messages, qualquer valor que um nó retorne para messages é anexado. llm_calls não tem reducer, então os valores retornados simplesmente sobrescrevem o que estava lá antes.

É exatamente por isso que a mensagem que say_hello retornou em 15.2.1 foi anexada a messages em vez de substituí-la — o reducer cuidou disso.

MessagesState

O LangGraph vem com um State predefinido chamado MessagesState. Veja como ele funciona por baixo dos panos:

python
class MessagesState(TypedDict):
    messages: Annotated[list[AnyMessage], add_messages]

A mesma estrutura que acabamos de abordar — um campo messages com o reducer add_messages já conectado.

Se você precisar de campos extras, basta fazer subclasse dele:

python
from langgraph.graph import MessagesState
 
class AgentState(MessagesState):
    llm_calls: int  # comportamento padrão de sobrescrever

15.2.3) Nós: Funções Que Atualizam o State

Um é uma função Python que executa uma única tarefa específica dentro do grafo.

python
def say_hello(state: MessagesState):
    return {"messages": [{"role": "ai", "content": "hello world"}]}

Duas coisas a saber ao escrever funções de nó:

Regra 1: Ela recebe o State atual como seu argumento. O LangGraph passa o objeto State atual quando executa o nó.

Regra 2: Ela retorna apenas as partes que deseja alterar, não o State completo. Os nós não modificam o State diretamente. Basta retornar os campos que você quer atualizar, e o LangGraph os mescla no State existente de acordo com as regras de reducer de cada campo.

Use add_node() para adicionar um nó ao StateGraph:

python
builder.add_node(say_hello)          # o nome da função "say_hello" se torna o nome do nó
builder.add_node("my_node", my_func) # você também pode especificar o nome explicitamente

15.2.4) Arestas: Regras Que Conectam Nós

Uma aresta determina "depois que este nó termina, o que é executado a seguir?". Existem dois tipos.

Arestas Normais

Uma aresta normal conecta um "vá-para" fixo entre dois nós. Use add_edge(source, target)source é o nó de partida, target é o destino.

python
builder.add_edge(START, "say_hello")       # quando o grafo começa, executa say_hello
builder.add_edge("say_hello", "llm_call")  # depois de say_hello, executa llm_call
builder.add_edge("llm_call", END)          # depois de llm_call, encerra o grafo

Arestas Condicionais

Uma aresta condicional escolhe o próximo nó em tempo de execução com base no State atual. Use add_conditional_edges(source, routing_function)source é o nó de partida, e routing_function é uma função que recebe o State atual e retorna o nome do próximo nó:

python
from langgraph.graph import END
 
def should_continue(state: AgentState):
    last_message = state["messages"][-1]
    if last_message.tool_calls:
        return "tool_node"   # chamada de ferramenta solicitada → vai para tool_node
    return END               # sem chamada de ferramenta → encerra

add_conditional_edges("llm_call", should_continue) diz ao LangGraph: "quando llm_call terminar, chame should_continue para decidir o que executar a seguir". should_continue roteia para "tool_node" se a última mensagem tiver tool_calls, ou para END se não tiver. Na prática, isso significa que o grafo continua para o nó de execução de ferramentas quando o LLM solicita uma chamada de ferramenta, e termina quando não solicita.

Agora que abordamos todos os quatro componentes, a próxima seção os utiliza para refatorar o loop de agente do Capítulo 14 em um grafo do LangGraph.

15.3) Refatorando o Loop de Agente em um Grafo

Vamos reconstruir o loop de agente do Capítulo 14 usando o LangGraph. O comportamento é idêntico ao do Capítulo 14 — o LLM decide, as ferramentas são executadas com base nas solicitações do LLM, e o ciclo se repete até a conclusão. A única coisa que muda é como estruturamos esse fluxo.

Veja como o grafo finalizado ficará:

Sim

Não

START

llm_call

Chamada de ferramenta solicitada?

tool_node

END

O grafo alterna entre llm_call e tool_node até que o LLM pare de solicitar chamadas de ferramenta, momento em que ele sai para END. Vamos construí-lo passo a passo.

15.3.1) Definindo o State

Fazemos subclasse de MessagesState de 15.2.2 para definir o State do agente. O campo messages é herdado de MessagesState, e adicionamos um campo llm_calls para rastrear o número de chamadas ao LLM.

python
from langgraph.graph import MessagesState
 
class AgentState(MessagesState):
    llm_calls: int    # contagem de chamadas ao LLM (sobrescrever)

A lista messages acumulará entradas do usuário (HumanMessage), respostas do LLM (AIMessage) e resultados de execução de ferramentas (ToolMessage) em ordem.

15.3.2) Construindo os Nós

Primeiro, vamos configurar as ferramentas e o modelo do Capítulo 14:

python
from langchain_openai import ChatOpenAI
from langchain_core.messages import ToolMessage
from langchain.tools import tool
 
@tool
def get_weather(city: str) -> str:
    """Obtém a previsão do tempo atual de uma cidade."""
    fake_data = {"Tokyo": "18°C, cloudy", "Cairo": "31°C, sunny"}
    return fake_data.get(city, f"No weather data for {city}.")
 
@tool
def calculate(expression: str) -> str:
    """Calcula uma expressão aritmética simples. Exemplo: '3 * 21'."""
    return str(eval(expression))  # Aviso: eval() é um risco de segurança. Não use em produção.
 
tools = [get_weather, calculate]
tool_map = {t.name: t for t in tools}
 
llm = ChatOpenAI(model="gpt-5-mini")
model_with_tools = llm.bind_tools(tools)

Agora vamos escrever as duas funções de nó.

llm_call — Chama o LLM e retorna a resposta:

python
def llm_call(state: AgentState):
    """Chama o LLM e retorna a resposta."""
    response = model_with_tools.invoke(state["messages"])
    return {
        "messages": [response],
        "llm_calls": state.get("llm_calls", 0) + 1,
    }

model_with_tools.invoke() chama o LLM, e a resposta é empacotada sob a chave messages no dicionário de retorno. O reducer a anexa às messages existentes em AgentState. llm_calls retorna a contagem atual mais 1, sobrescrevendo o valor anterior.

tool_node — Executa as ferramentas solicitadas pelo LLM e retorna os resultados:

python
def tool_node(state: AgentState):
    """Executa as ferramentas solicitadas pelo LLM."""
    last_message = state["messages"][-1]
    results = []
    for tool_call in last_message.tool_calls:
        selected_tool = tool_map[tool_call["name"]]
        tool_message = selected_tool.invoke(tool_call)
        results.append(tool_message)
    return {"messages": results}

Como tool_node sempre é executado logo após llm_call, a última mensagem em messages é garantidamente a AIMessage que o LLM acabou de produzir. O campo tool_calls dessa mensagem contém as chamadas de ferramenta que o LLM solicitou. O nó executa cada ferramenta, coleta os resultados em results e os retorna sob a chave messages — o reducer se encarrega de anexá-los à lista existente.

15.3.3) Aresta Condicional

Assim que llm_call termina, precisamos de uma aresta condicional para decidir se executamos tool_node ou encerramos o grafo. Isso segue o mesmo padrão de 15.2.4:

python
from typing import Literal
from langgraph.graph import END
 
def should_continue(state: AgentState) -> Literal["tool_node", "__end__"]:
    """Decide se executa ferramentas ou encerra o grafo."""
    last_message = state["messages"][-1]
    if last_message.tool_calls:
        return "tool_node"
    return END

Se last_message.tool_calls estiver presente, o LLM está solicitando uma chamada de ferramenta, então roteamos para tool_node. Caso contrário, roteamos para END e o grafo termina.

A dica de tipo de retorno Literal["tool_node", "__end__"] declara os destinos possíveis que essa função pode retornar. O LangGraph precisa dessa dica para desenhar corretamente os caminhos de arestas condicionais nas visualizações do grafo. Ela não tem efeito no comportamento em tempo de execução.

"__end__" é o valor de string subjacente de END. Como Literal só aceita literais de string, escrevemos "__end__" em vez de END.

15.3.4) Montando e Executando o Grafo

Hora de conectar tudo. Vamos montar o State, os nós e a aresta condicional em um StateGraph e compilar:

python
from langgraph.graph import StateGraph, START, END
 
builder = StateGraph(AgentState)
 
builder.add_node("llm_call", llm_call)
builder.add_node("tool_node", tool_node)
 
builder.add_edge(START, "llm_call")                         # início → llm_call
builder.add_conditional_edges("llm_call", should_continue)  # llm_call → tool_node ou END
builder.add_edge("tool_node", "llm_call")                   # tool_node → llm_call (loop)
 
agent = builder.compile()

A aresta de tool_node de volta para llm_call cria um loop. A execução continua ciclando até que o LLM responda com uma resposta final em vez de solicitar outra chamada de ferramenta, momento em que o loop sai.

Vamos executá-lo:

python
from langchain_core.messages import HumanMessage
 
result = agent.invoke({
    "messages": [HumanMessage(content="Obtenha a temperatura no Cairo e depois multiplique o número por 3.")],
    "llm_calls": 0,
})
 
print(result["messages"][-1].content)
print(f"\nTotal de chamadas ao LLM: {result['llm_calls']}")

Saída:

Temperatura atual no Cairo: 31°C. Multiplicada por 3 = 93.
 
Total de chamadas ao LLM: 3

O agente chamou get_weather("Cairo"), viu o resultado, chamou calculate("31 * 3") e produziu a resposta final — o mesmo resultado que obtivemos no Capítulo 14.

Inspecionar o histórico completo de mensagens mostra cada etapa registrada em messages, em ordem:

python
for message in result["messages"]:
    message.pretty_print()

Saída:

================================ Human Message =================================
Obtenha a temperatura no Cairo e depois multiplique o número por 3.
================================== Ai Message ==================================
Tool Calls:
  get_weather (call_DiL9WF)
  Args:
    city: Cairo
================================= Tool Message =================================
Name: get_weather
31°C, sunny
================================== Ai Message ==================================
Tool Calls:
  calculate (call_wa6RqWST)
  Args:
    expression: 31 * 3
================================= Tool Message =================================
Name: calculate
93
================================== Ai Message ==================================
Temperatura atual no Cairo: 31°C. Multiplicada por 3 = 93.

15.3.5) Visualização do Grafo

Em um notebook Jupyter, agent.get_graph().draw_mermaid_png() renderiza a estrutura do grafo como uma imagem diretamente na saída da célula.

python
from IPython.display import Image, display
 
display(Image(agent.get_graph().draw_mermaid_png()))

Em um ambiente de terminal, salve-o como um arquivo PNG em vez disso.

python
agent.get_graph().draw_mermaid_png(output_file_path="agent_graph.png")

Imagem gerada:

__start__

llm_call

tool_node

__end__

Linhas sólidas são arestas normais e linhas pontilhadas são arestas condicionais. Este diagrama é gerado automaticamente a partir do código.

15.3.6) Limite de Recursão

Assim como usamos max_steps para nos proteger contra loops infinitos no Capítulo 14, o LangGraph tem uma rede de segurança embutida. Toda vez que um nó é executado durante a execução do grafo, um contador interno incrementa em um. Quando esse contador excede o limite configurado, o LangGraph levanta um GraphRecursionError.

Para ver como a contagem funciona, observe a execução anterior. Chamar tanto get_weather quanto calculate visitou os nós nesta ordem:

llm_call(1) → tool_node(2) → llm_call(3) → tool_node(4) → llm_call(5) → END

São 5 visitas a nós no total. Se você definir recursion_limit como 3, o limite entra em ação na 3ª visita e a execução é interrompida:

python
from langgraph.errors import GraphRecursionError
 
try:
    result = agent.invoke(
        {"messages": [HumanMessage(content="Obtenha a temperatura no Cairo e depois multiplique o número por 3.")],
         "llm_calls": 0},
        config={"recursion_limit": 3},
    )
except GraphRecursionError:
    print("O agente atingiu o limite de recursão — interrompendo a execução.")

Saída:

O agente atingiu o limite de recursão — interrompendo a execução.

Defina o limite passando config={"recursion_limit": número} para invoke(). O valor certo depende do seu caso de uso e da complexidade do seu grafo. Comece com um número generoso e ajuste-o através de testes.