Seu Primeiro Agente com LangGraph: Tutorial Passo a Passo
Tutorial hands-on completo para construir seu primeiro agente de IA com LangGraph em Python — do zero ao agente de pesquisa funcional com busca na web e...
TL;DR — Neste tutorial você vai construir um agente de pesquisa funcional com LangGraph que busca informações na web, avalia se tem dados suficientes e gera um relatório estruturado. O fluxo completo: instalar dependências → definir estado → criar nós de processamento → conectar arestas → adicionar lógica condicional → compilar e executar o grafo. Código completo e pronto para rodar no final do artigo.
O Que Vamos Construir
Vamos criar um agente de pesquisa que faz algo que nenhuma cadeia linear de prompts consegue: ele toma decisões sozinho sobre quando parar de buscar informação.
O fluxo funciona assim:
- Recebe um tópico de pesquisa
- Gera consultas de busca relevantes
- Pesquisa na web usando a API Tavily
- Analisa os resultados encontrados
- Decide se precisa buscar mais — se sim, volta ao passo 2 com consultas refinadas
- Quando tem informação suficiente, escreve um relatório estruturado
Essa capacidade de loop — buscar, avaliar, decidir e voltar — é exatamente o que separa um agente de um simples chain de prompts. É o padrão que empresas como Klarna, Uber e Replit usam em produção com LangGraph desde 2025.
O LangGraph modela esse comportamento como um grafo dirigido: cada etapa é um nó (função Python), cada transição é uma aresta, e as decisões do agente são arestas condicionais. Você define a estrutura, o framework cuida do estado, persistência e fluxo de controle.
Pré-requisitos
Antes de começar, confirme que você tem:
- Python 3.11 ou superior — LangGraph 1.x exige essa versão mínima
- Uma chave de API da OpenAI — usamos
gpt-4o-minipor custo-benefício (funciona com qualquer LLM compatível) - Uma chave de API da Tavily — para busca web (o plano gratuito dá 1.000 buscas por mês)
- Familiaridade básica com Python — funções, dicionários, type hints
Se você nunca trabalhou com agentes antes, vale dar uma olhada no artigo sobre arquitetura de harness e loop para entender o modelo mental por trás do que vamos implementar.
Instalação
Crie um diretório para o projeto e configure um ambiente virtual:
mkdir langgraph-pesquisa
cd langgraph-pesquisa
python -m venv venv
source venv/bin/activate # Windows: venv\Scripts\activateInstale as dependências com versões fixas:
pip install langgraph==0.3.34 \
langchain-openai==0.3.12 \
langchain-community==0.3.7 \
tavily-python==0.5.0 \
python-dotenv==1.1.0Nota: O pacote
langgraphno PyPI está na versão 0.3.x como runtime instalável, embora o framework seja comercialmente identificado como LangGraph 1.0+. Fixe versões em produção para evitar breaking changes.
Crie um arquivo .env na raiz do projeto para suas chaves:
OPENAI_API_KEY=sk-sua-chave-openai-aqui
TAVILY_API_KEY=tvly-sua-chave-tavily-aquiTeste a instalação:
import langgraph
print(f"LangGraph versão: {langgraph.__version__}")Se o número de versão aparecer sem erros, estamos prontos.
Definir o Estado
O estado é a memória de trabalho do agente — um dicionário tipado que flui pelo grafo inteiro. Cada nó lê dele e escreve nele. Essa é a decisão de design mais importante: o estado define o que seu agente sabe e consegue rastrear.
Crie um arquivo agente.py:
"""Agente de pesquisa construído com LangGraph."""
import os
from typing import TypedDict, Annotated
from dotenv import load_dotenv
from langchain_openai import ChatOpenAI
from langchain_core.messages import HumanMessage, SystemMessage
from langgraph.graph import StateGraph, START, END
from langgraph.graph.message import add_messages
from langgraph.checkpoint.memory import MemorySaver
from tavily import TavilyClient
load_dotenv()
# --- Schema do Estado ---
class EstadoPesquisa(TypedDict):
"""Memória de trabalho do agente."""
messages: Annotated[list, add_messages] # Histórico de conversação
topico: str # O que estamos pesquisando
consultas: list[str] # Queries já executadas
fontes: list[dict] # Resultados brutos da busca
analise: str # Análise das fontes
relatorio_final: str # Relatório final de pesquisa
iteracao: int # Quantas rodadas de pesquisa fizemos
max_iteracoes: int # Limite de segurança contra loops infinitosPontos importantes sobre essa definição:
messagesusa a anotaçãoadd_messages— isso faz o LangGraph acumular mensagens ao invés de substituí-las. O histórico de conversação cresce naturalmente.iteracaoemax_iteracoesprevinem loops infinitos. Isso não é opcional — qualquer agente que pode fazer loop precisa de um freio de emergência.- Cada campo tem propósito claro. Quando você precisar debugar o agente (e vai precisar), nomes descritivos economizam horas.
Criar os Nós
Nós são funções Python simples que recebem o estado atual e retornam uma atualização parcial. Sem classes base, sem decoradores obrigatórios. Cada nó faz uma coisa só.
Setup do LLM e Cliente de Busca
# --- Setup ---
llm = ChatOpenAI(
model="gpt-4o-mini",
temperature=0.1, # Temperatura baixa para pesquisa factual
)
tavily = TavilyClient(api_key=os.getenv("TAVILY_API_KEY"))Nó 1: Gerar Consultas de Busca
# --- Nós ---
def gerar_consultas(state: EstadoPesquisa) -> dict:
"""Transforma o tópico em consultas de busca específicas."""
topico = state["topico"]
# Em iterações posteriores, refina baseado no que já encontramos
contexto_existente = ""
if state.get("analise"):
contexto_existente = (
f"\n\nJá sabemos o seguinte:\n{state['analise']}\n\n"
"Gere consultas para preencher lacunas no nosso conhecimento."
)
response = llm.invoke([
SystemMessage(content=(
"Você é um assistente de pesquisa. Gere 3 consultas de busca "
"específicas e diversas para pesquisar o tópico dado. "
"Retorne apenas as consultas, uma por linha. "
"Sem numeração, sem texto extra."
f"{contexto_existente}"
)),
HumanMessage(content=f"Tópico de pesquisa: {topico}"),
])
novas_consultas = [
q.strip() for q in response.content.strip().split("\n") if q.strip()
]
return {
"consultas": state.get("consultas", []) + novas_consultas,
"messages": [response],
}Repare no padrão de comportamento adaptativo: na primeira execução, o nó gera consultas amplas. Em rodadas posteriores, ele lê sua própria análise anterior e gera consultas para preencher lacunas. O agente literalmente fica mais inteligente a cada loop.
Nó 2: Buscar na Web
def buscar_web(state: EstadoPesquisa) -> dict:
"""Executa as consultas de busca e coleta resultados."""
consultas = state.get("consultas", [])
# Busca apenas o último lote de consultas (últimas 3)
consultas_recentes = consultas[-3:]
todos_resultados = state.get("fontes", [])
for consulta in consultas_recentes:
try:
response = tavily.search(
query=consulta,
max_results=3,
include_raw_content=False,
)
for resultado in response.get("results", []):
# Evita URLs duplicadas
if not any(f["url"] == resultado["url"] for f in todos_resultados):
todos_resultados.append({
"titulo": resultado.get("title", ""),
"url": resultado.get("url", ""),
"conteudo": resultado.get("content", ""),
"consulta": consulta,
})
except Exception as e:
# Loga mas não crasha — o agente trabalha com resultados parciais
print(f"Busca falhou para '{consulta}': {e}")
return {"fontes": todos_resultados}A deduplicação e o tratamento de erros são intencionais. Se uma API de busca falhar, o agente continua com os resultados que tem. Resiliência é parte do design.
Nó 3: Analisar Resultados
def analisar_resultados(state: EstadoPesquisa) -> dict:
"""Analisa os resultados e avalia se temos informação suficiente."""
fontes = state.get("fontes", [])
if not fontes:
return {
"analise": "Nenhum resultado encontrado. Preciso tentar consultas diferentes.",
"iteracao": state.get("iteracao", 0) + 1,
}
# Formata as fontes para o LLM
texto_fontes = ""
for i, fonte in enumerate(fontes, 1):
texto_fontes += (
f"\n[{i}] {fonte['titulo']}\n"
f"URL: {fonte['url']}\n"
f"{fonte['conteudo']}\n"
)
response = llm.invoke([
SystemMessage(content=(
"Você é um analista de pesquisa. Analise os resultados de busca "
"sobre o tópico dado. Forneça:\n"
"1. Principais descobertas (o que sabemos)\n"
"2. Lacunas (o que ainda precisamos descobrir)\n"
"3. Nível de confiança (baixo/médio/alto) no nosso entendimento geral\n\n"
"Seja específico e cite números das fontes."
)),
HumanMessage(content=f"Tópico: {state['topico']}\n\nFontes:{texto_fontes}"),
])
return {
"analise": response.content,
"iteracao": state.get("iteracao", 0) + 1,
"messages": [response],
}Nó 4: Escrever Relatório
def escrever_relatorio(state: EstadoPesquisa) -> dict:
"""Escreve um relatório estruturado com base nas descobertas."""
fontes = state.get("fontes", [])
analise = state.get("analise", "")
texto_fontes = ""
for i, fonte in enumerate(fontes, 1):
texto_fontes += (
f"\n[{i}] {fonte['titulo']}\n"
f"URL: {fonte['url']}\n"
f"{fonte['conteudo']}\n"
)
response = llm.invoke([
SystemMessage(content=(
"Você é um redator de pesquisa. Escreva um relatório claro e "
"bem estruturado com base na análise e fontes fornecidas. Inclua:\n"
"- Resumo executivo (2-3 frases)\n"
"- Principais descobertas com citações [1], [2], etc.\n"
"- Conclusões\n"
"- Lista de fontes\n\n"
"Escreva para um público técnico. Seja factual e específico."
)),
HumanMessage(content=(
f"Tópico: {state['topico']}\n\n"
f"Análise:\n{analise}\n\n"
f"Fontes:{texto_fontes}"
)),
])
return {
"relatorio_final": response.content,
"messages": [response],
}Montar o Grafo
Aqui é onde o LangGraph brilha. Conectamos os nós com arestas e definimos o fluxo linear básico:
# --- Construir o Grafo ---
workflow = StateGraph(EstadoPesquisa)
# Adicionar nós
workflow.add_node("gerar_consultas", gerar_consultas)
workflow.add_node("buscar_web", buscar_web)
workflow.add_node("analisar_resultados", analisar_resultados)
workflow.add_node("escrever_relatorio", escrever_relatorio)
# Adicionar arestas lineares
workflow.add_edge(START, "gerar_consultas")
workflow.add_edge("gerar_consultas", "buscar_web")
workflow.add_edge("buscar_web", "analisar_resultados")
workflow.add_edge("escrever_relatorio", END)Até aqui temos um fluxo linear: START → gerar_consultas → buscar_web → analisar_resultados → … → escrever_relatorio → END. O que falta é a decisão no meio — o agente precisa escolher entre continuar pesquisando ou ir para a escrita.
Executar o Agente (Versão Linear)
Antes de adicionar a lógica condicional, vamos compilar e testar o fluxo básico para garantir que tudo funciona. Temporariamente, conecte analisar_resultados direto ao relatório:
# Versão simplificada para teste
workflow.add_edge("analisar_resultados", "escrever_relatorio")
# Compilar
agente = workflow.compile()
# Executar
resultado = agente.invoke({
"topico": "Como empresas estão usando agentes de IA em produção em 2026",
"messages": [],
"consultas": [],
"fontes": [],
"analise": "",
"relatorio_final": "",
"iteracao": 0,
"max_iteracoes": 3,
})
print(resultado["relatorio_final"])Se o relatório aparecer, seu grafo básico está funcionando. Agora vamos adicionar inteligência.
Adicionar Lógica Condicional
A aresta condicional é o coração de qualquer agente. É aqui que o grafo deixa de ser um pipeline linear e vira algo que raciocina sobre o próprio progresso.
A Função de Roteamento
# --- Lógica de Roteamento ---
def deve_continuar_pesquisa(state: EstadoPesquisa) -> str:
"""Decide se continua pesquisando ou escreve o relatório."""
iteracao = state.get("iteracao", 0)
max_iteracoes = state.get("max_iteracoes", 3)
analise = state.get("analise", "")
# Hard stop: previne loops infinitos
if iteracao >= max_iteracoes:
return "escrever_relatorio"
# Se a análise menciona confiança baixa ou lacunas, continua
analise_lower = analise.lower()
if "baixo" in analise_lower and "confiança" in analise_lower:
return "gerar_consultas"
if "lacunas significativas" in analise_lower or "precisamos" in analise_lower:
return "gerar_consultas"
# Caso contrário, temos informação suficiente
return "escrever_relatorio"A função retorna uma string — o nome do próximo nó. É assim que o agente decide se continua buscando ou parte para a escrita. Repare no max_iteracoes como trava de segurança: nunca permita que um agente faça loop sem limite.
Montando o Grafo Completo
Agora substituímos a aresta fixa entre analisar_resultados e escrever_relatorio pela aresta condicional:
# --- Grafo Completo ---
workflow = StateGraph(EstadoPesquisa)
# Nós
workflow.add_node("gerar_consultas", gerar_consultas)
workflow.add_node("buscar_web", buscar_web)
workflow.add_node("analisar_resultados", analisar_resultados)
workflow.add_node("escrever_relatorio", escrever_relatorio)
# Arestas
workflow.add_edge(START, "gerar_consultas")
workflow.add_edge("gerar_consultas", "buscar_web")
workflow.add_edge("buscar_web", "analisar_resultados")
# Aresta condicional: o agente decide se faz loop ou finaliza
workflow.add_conditional_edges(
"analisar_resultados",
deve_continuar_pesquisa,
{
"gerar_consultas": "gerar_consultas",
"escrever_relatorio": "escrever_relatorio",
},
)
workflow.add_edge("escrever_relatorio", END)
# Memória para persistência de estado
memory = MemorySaver()
agente = workflow.compile(checkpointer=memory)O fluxo agora funciona assim:
- START → gerar_consultas — cria consultas de busca a partir do tópico
- gerar_consultas → buscar_web — executa as consultas
- buscar_web → analisar_resultados — avalia o que encontrou
- analisar_resultados → ??? — a aresta condicional entra em ação. Se a análise diz que precisamos mais informação, voltamos a gerar consultas. Se temos o suficiente, seguimos para a escrita.
- escrever_relatorio → END — saída do relatório final
Esse loop — buscar, avaliar, decidir — é o padrão fundamental de agentes. Se quiser entender a diferença entre esse modelo e agent frameworks versus coding agents, vale a leitura para contextualizar.
Resultado Final
Aqui está o código completo e funcional, pronto para copiar e executar:
"""Agente de pesquisa com LangGraph — código completo."""
import os
from typing import TypedDict, Annotated
from dotenv import load_dotenv
from langchain_openai import ChatOpenAI
from langchain_core.messages import HumanMessage, SystemMessage
from langgraph.graph import StateGraph, START, END
from langgraph.graph.message import add_messages
from langgraph.checkpoint.memory import MemorySaver
from tavily import TavilyClient
load_dotenv()
# ============================================================
# ESTADO
# ============================================================
class EstadoPesquisa(TypedDict):
"""Memória de trabalho do agente."""
messages: Annotated[list, add_messages]
topico: str
consultas: list[str]
fontes: list[dict]
analise: str
relatorio_final: str
iteracao: int
max_iteracoes: int
# ============================================================
# SETUP
# ============================================================
llm = ChatOpenAI(model="gpt-4o-mini", temperature=0.1)
tavily = TavilyClient(api_key=os.getenv("TAVILY_API_KEY"))
# ============================================================
# NÓS
# ============================================================
def gerar_consultas(state: EstadoPesquisa) -> dict:
"""Transforma o tópico em consultas de busca específicas."""
topico = state["topico"]
contexto_existente = ""
if state.get("analise"):
contexto_existente = (
f"\n\nJá sabemos:\n{state['analise']}\n\n"
"Gere consultas para preencher lacunas."
)
response = llm.invoke([
SystemMessage(content=(
"Você é um assistente de pesquisa. Gere 3 consultas de busca "
"específicas e diversas para o tópico dado. Retorne apenas as "
"consultas, uma por linha. Sem numeração."
f"{contexto_existente}"
)),
HumanMessage(content=f"Tópico: {topico}"),
])
novas_consultas = [
q.strip() for q in response.content.strip().split("\n") if q.strip()
]
return {
"consultas": state.get("consultas", []) + novas_consultas,
"messages": [response],
}
def buscar_web(state: EstadoPesquisa) -> dict:
"""Executa consultas de busca e coleta resultados."""
consultas_recentes = state.get("consultas", [])[-3:]
todos_resultados = state.get("fontes", [])
for consulta in consultas_recentes:
try:
response = tavily.search(
query=consulta,
max_results=3,
include_raw_content=False,
)
for resultado in response.get("results", []):
if not any(f["url"] == resultado["url"] for f in todos_resultados):
todos_resultados.append({
"titulo": resultado.get("title", ""),
"url": resultado.get("url", ""),
"conteudo": resultado.get("content", ""),
"consulta": consulta,
})
except Exception as e:
print(f"Busca falhou para '{consulta}': {e}")
return {"fontes": todos_resultados}
def analisar_resultados(state: EstadoPesquisa) -> dict:
"""Analisa resultados e avalia se temos informação suficiente."""
fontes = state.get("fontes", [])
if not fontes:
return {
"analise": "Nenhum resultado. Precisamos tentar consultas diferentes.",
"iteracao": state.get("iteracao", 0) + 1,
}
texto_fontes = ""
for i, fonte in enumerate(fontes, 1):
texto_fontes += f"\n[{i}] {fonte['titulo']}\nURL: {fonte['url']}\n{fonte['conteudo']}\n"
response = llm.invoke([
SystemMessage(content=(
"Você é um analista de pesquisa. Analise os resultados sobre o "
"tópico dado. Forneça:\n"
"1. Principais descobertas\n"
"2. Lacunas no conhecimento\n"
"3. Nível de confiança (baixo/médio/alto)\n\n"
"Cite números das fontes."
)),
HumanMessage(content=f"Tópico: {state['topico']}\n\nFontes:{texto_fontes}"),
])
return {
"analise": response.content,
"iteracao": state.get("iteracao", 0) + 1,
"messages": [response],
}
def escrever_relatorio(state: EstadoPesquisa) -> dict:
"""Escreve o relatório final estruturado."""
fontes = state.get("fontes", [])
analise = state.get("analise", "")
texto_fontes = ""
for i, fonte in enumerate(fontes, 1):
texto_fontes += f"\n[{i}] {fonte['titulo']}\nURL: {fonte['url']}\n{fonte['conteudo']}\n"
response = llm.invoke([
SystemMessage(content=(
"Você é um redator técnico. Escreva um relatório de pesquisa "
"claro e estruturado. Inclua:\n"
"- Resumo executivo (2-3 frases)\n"
"- Principais descobertas com citações [1], [2]\n"
"- Conclusões\n"
"- Lista de fontes\n\n"
"Público: técnico. Tom: factual e direto."
)),
HumanMessage(content=(
f"Tópico: {state['topico']}\n\n"
f"Análise:\n{analise}\n\nFontes:{texto_fontes}"
)),
])
return {
"relatorio_final": response.content,
"messages": [response],
}
# ============================================================
# ROTEAMENTO
# ============================================================
def deve_continuar_pesquisa(state: EstadoPesquisa) -> str:
"""Decide: continuar pesquisando ou escrever o relatório."""
iteracao = state.get("iteracao", 0)
max_iteracoes = state.get("max_iteracoes", 3)
analise = state.get("analise", "")
if iteracao >= max_iteracoes:
return "escrever_relatorio"
analise_lower = analise.lower()
if "baixo" in analise_lower and "confiança" in analise_lower:
return "gerar_consultas"
if "lacunas significativas" in analise_lower or "precisamos" in analise_lower:
return "gerar_consultas"
return "escrever_relatorio"
# ============================================================
# GRAFO
# ============================================================
workflow = StateGraph(EstadoPesquisa)
workflow.add_node("gerar_consultas", gerar_consultas)
workflow.add_node("buscar_web", buscar_web)
workflow.add_node("analisar_resultados", analisar_resultados)
workflow.add_node("escrever_relatorio", escrever_relatorio)
workflow.add_edge(START, "gerar_consultas")
workflow.add_edge("gerar_consultas", "buscar_web")
workflow.add_edge("buscar_web", "analisar_resultados")
workflow.add_conditional_edges(
"analisar_resultados",
deve_continuar_pesquisa,
{
"gerar_consultas": "gerar_consultas",
"escrever_relatorio": "escrever_relatorio",
},
)
workflow.add_edge("escrever_relatorio", END)
memory = MemorySaver()
agente = workflow.compile(checkpointer=memory)
# ============================================================
# EXECUÇÃO
# ============================================================
if __name__ == "__main__":
print("=" * 60)
print(" Agente de Pesquisa — LangGraph")
print("=" * 60)
topico = input("\nDigite o tópico de pesquisa: ").strip()
if not topico:
topico = "Como empresas estão usando agentes de IA em produção em 2026"
print(f"\nPesquisando: {topico}")
print("-" * 60)
estado_inicial = {
"topico": topico,
"messages": [],
"consultas": [],
"fontes": [],
"analise": "",
"relatorio_final": "",
"iteracao": 0,
"max_iteracoes": 3,
}
config = {"configurable": {"thread_id": "sessao-001"}}
for evento in agente.stream(estado_inicial, config=config):
for nome_no, saida in evento.items():
print(f"\n>> Nó: {nome_no}")
if nome_no == "gerar_consultas" and "consultas" in saida:
print(f" Consultas: {saida['consultas'][-3:]}")
elif nome_no == "buscar_web" and "fontes" in saida:
print(f" {len(saida['fontes'])} fontes encontradas")
elif nome_no == "analisar_resultados" and "analise" in saida:
print(f" Iteração: {saida.get('iteracao', '?')}")
print(f" Análise: {saida['analise'][:200]}...")
elif nome_no == "escrever_relatorio" and "relatorio_final" in saida:
print(f"\n{'=' * 60}")
print(" RELATÓRIO DE PESQUISA")
print("=" * 60)
print(saida["relatorio_final"])
print(f"\n{'=' * 60}")
print("Pesquisa concluída.")Execute com:
python agente.pyVocê verá o agente trabalhando em tempo real — gerando consultas, buscando, avaliando, potencialmente voltando para buscar mais, e finalmente escrevendo o relatório.
Próximos Passos
Você acabou de construir um agente funcional com LangGraph. Os conceitos que aprendeu — StateGraph, add_node, add_edge, add_conditional_edges, compile — são os mesmos que rodam em produção nas empresas que mencionamos.
A partir daqui, os caminhos se abrem:
Adicionar Human-in-the-Loop. LangGraph permite inserir pontos de aprovação humana em qualquer lugar do grafo. O agente pausa, espera a aprovação e continua. Essencial para agentes que tomam ações no mundo real.
Persistência em banco de dados. O MemorySaver que usamos guarda estado em memória. Para produção, troque por SqliteSaver ou PostgresSaver e seu agente sobrevive a reinícios.
Sistemas multi-agente. No LangGraph, um nó pode ser outro grafo compilado. Você pode ter um agente supervisor delegando para agentes especialistas — pesquisador, redator, revisor — cada um com seu próprio grafo.
Streaming de eventos. Ao invés de .stream() com eventos de nó, use .astream_events() para receber cada token conforme o LLM gera. Dá uma experiência de tempo real para o usuário.
Expandir ferramentas. Adicione leitura/escrita de arquivos, execução de código, chamadas a APIs, navegação web. Cada capacidade é um novo nó no grafo.
O padrão fundamental — estado, nós, arestas, decisão — se mantém independente da complexidade. Você define a estrutura, o LangGraph cuida do resto. Se quer entender como frameworks como esse se comparam a coding agents como Claude Code e Cursor, o artigo sobre agent frameworks vs coding agents faz essa distinção com clareza.
Bom building.