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:
# 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:
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.
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.
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.
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 grafoCompilando 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.
graph = builder.compile()
initial_state = {"messages": [{"role": "user", "content": "hi!"}]}
result = graph.invoke(initial_state)Agora vamos juntar tudo e construir um grafo simples:
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 worldQuando você chama invoke(), o grafo é executado na ordem START → say_hello → END. 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:
from typing_extensions import TypedDict
class AgentState(TypedDict):
messages: list # lista de mensagens
llm_calls: int # contagem de chamadas ao LLMA 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:
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: sobrescreveradd_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:
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:
from langgraph.graph import MessagesState
class AgentState(MessagesState):
llm_calls: int # comportamento padrão de sobrescrever15.2.3) Nós: Funções Que Atualizam o State
Um nó é uma função Python que executa uma única tarefa específica dentro do grafo.
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:
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 explicitamente15.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.
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 grafoArestas 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ó:
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 → encerraadd_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á:
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.
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:
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ó.
Nó llm_call — Chama o LLM e retorna a resposta:
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.
Nó tool_node — Executa as ferramentas solicitadas pelo LLM e retorna os resultados:
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:
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 ENDSe 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 deEND. ComoLiteralsó aceita literais de string, escrevemos"__end__"em vez deEND.
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:
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:
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: 3O 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:
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.
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.
agent.get_graph().draw_mermaid_png(output_file_path="agent_graph.png")Imagem gerada:
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:
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.