fundamentos·Fabricio Telles

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:

  1. Recebe um tópico de pesquisa
  2. Gera consultas de busca relevantes
  3. Pesquisa na web usando a API Tavily
  4. Analisa os resultados encontrados
  5. Decide se precisa buscar mais — se sim, volta ao passo 2 com consultas refinadas
  6. 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-mini por 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\activate

Instale 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.0

Nota: O pacote langgraph no 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-aqui

Teste 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 infinitos

Pontos importantes sobre essa definição:

  • messages usa a anotação add_messages — isso faz o LangGraph acumular mensagens ao invés de substituí-las. O histórico de conversação cresce naturalmente.
  • iteracao e max_iteracoes previnem 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:

  1. START → gerar_consultas — cria consultas de busca a partir do tópico
  2. gerar_consultas → buscar_web — executa as consultas
  3. buscar_web → analisar_resultados — avalia o que encontrou
  4. 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.
  5. 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.py

Você 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.