Como criar um agente de IA em Python (do loop manual ao framework)

Cansado de programar?

Conheça a melhor e mais completa formação de Python e Django e sinta-se um programador verdadeiramente competente. Além de Python e Django, você também vai aprender Banco de Dados, SQL, HTML, CSS, Javascript, Bootstrap e muito mais!

Quero aprender Python e Django de Verdade! Quero aprender!
Suporte

Tire suas dúvidas diretamente com o professor

Projetos práticos

Projetos práticos voltados para o mercado de trabalho

Prática profissional

Formação moderna com foco na prática profissional

Resposta rápida

Um agente de IA é um LLM + ferramentas + um loop de raciocínio: a cada volta, o modelo decide a próxima ação (chamar uma ferramenta ou responder), o código executa e devolve o resultado, até concluir ou atingir o limite de passos. O esqueleto, com um LLM simulado que roda offline:

1
2
3
4
5
6
7
8
9
10
11
12
13
def llm(historico):  # LLM simulado: decide o próximo passo
    if len(historico) == 1:
        return {"ferramenta": "cotacao", "args": {"moeda": "USD"}}
    return {"resposta": f"250 USD = R$ {250 * historico[-1]:.2f}"}

ferramentas = {"cotacao": lambda moeda: 5.40}
historico = ["Quanto é 250 dólares em reais?"]
for passo in range(5):  # condição de parada: no máximo 5 voltas
    decisao = llm(historico)
    if "resposta" in decisao:
        print(decisao["resposta"])  # 250 USD = R$ 1350.00
        break
    historico.append(ferramentas[decisao["ferramenta"]](**decisao["args"]))

Resumo em 30 segundos:

  • Agente = LLM (decide) + ferramentas (agem) + loop (repete), o ciclo ReAct: pensar, agir, observar.
  • Tool calling: o modelo devolve o nome da ferramenta e os argumentos em JSON; quem executa é o seu código.
  • Sempre limite os passos (range(max_iteracoes) no loop manual, recursion_limit no LangGraph).
  • Chatbot só responde; agente age. Se os passos são fixos, um workflow fixo é mais barato e previsível.
  • Em produção, use um framework (create_agent do LangChain, que roda sobre o LangGraph).

Salve salve, Pythonista! :wave:

Você já reparou que o ChatGPT sabe te explicar qualquer coisa, mas não consegue fazer nada por você? Ele não checa a cotação do dólar de hoje, não consulta o seu banco de dados, não roda um cálculo de verdade. Ele só prevê texto. Quando a tarefa exige agir no mundo - buscar um dado, chamar uma API, decidir o próximo passo - o modelo sozinho não dá conta.

É aí que entram os agentes de IA: o tema mais quente de 2026 e a evolução natural de quem já brinca com LLMs. Um agente pega aquele mesmo modelo de linguagem e o coloca para decidir e usar ferramentas em um loop, até concluir a tarefa.

Neste artigo você vai entender o que é um agente de verdade (sem hype) e construir um, em Python, de duas formas: primeiro na mão, com um loop cru, para enxergar o mecanismo por dentro; depois com um framework (LangGraph), para ver quanto trabalho ele te poupa.

Vá Direto ao Assunto…

O que é um agente de IA?

Um agente de IA é um LLM acoplado a um conjunto de ferramentas e a um loop de raciocínio: em vez de só responder, o modelo decide sozinho quais ações executar, observa o resultado de cada uma e repete o ciclo até concluir a tarefa. A diferença para um chatbot comum é essa autonomia - o agente escolhe o que fazer, não apenas o que dizer.

Pense na fórmula:

Agente = LLM (o cérebro) + ferramentas (as mãos) + um loop (a iniciativa).

O LLM continua sendo um previsor de texto. A mágica está em dar a ele ações que pode chamar (uma busca, um cálculo, uma consulta a banco) e um laço de repetição que deixa o modelo encadear essas ações. Se você já viu o Claude Code ou o modo ultracode resolvendo uma tarefa de programação sozinho, viu um agente em ação - ele lê arquivos, roda comandos, observa a saída e segue.

Agente, chatbot ou chain: qual a diferença?

Esse é o ponto que mais confunde quem está começando. Nem todo uso de LLM é um agente. Vale separar:

Abordagem O LLM decide as ações? Usa ferramentas? Exemplo
Chamada única Não Não Resumir um texto colado
Chain (fluxo fixo) Não - caminho pré-definido Sim, em ordem fixa Traduzir e depois resumir
Agente Sim - decide em loop Sim, escolhe quais e quando “Resolva X” e ele se vira

Uma chain (corrente) é um fluxo que você desenhou: passo 1, depois 2, depois 3, sempre na mesma ordem. É exatamente o que vimos no artigo sobre o que é e como funciona o LangChain. Já um agente recebe um objetivo e descobre o caminho sozinho, em tempo de execução. Quando o problema tem passos previsíveis, uma chain basta (e é mais barata). Quando o caminho depende do que for surgindo, você precisa de um agente.

Como um agente funciona por baixo dos panos

Todo agente, por mais sofisticado que pareça, gira em torno do mesmo ciclo. Ele recebe a tarefa e repete estes passos:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
1. pergunta do usuário
        |
        v
2. o LLM pensa e decide:  responder agora  OU  usar uma ferramenta
        |
        v   (escolheu uma ferramenta)
3. o código executa a ferramenta (busca, cálculo, API...)
        |
        v
4. o resultado da ferramenta volta para o LLM como nova informação
        |
        +--> repete os passos 2 a 4 até o LLM decidir que já pode responder
        |
        v
5. resposta final

Esse padrão - pensar, agir, observar, repetir - é conhecido como ciclo ReAct (de Reasoning + Acting). É a espinha dorsal de praticamente todo agente, do mais simples ao mais complexo. As peças que o compõem:

Peça Papel
LLM (o cérebro) Decide o próximo passo e quando parar
Ferramentas (tools) Ações que o LLM pode executar: buscar, calcular, chamar uma API
Loop de raciocínio Repete pensar -> agir -> observar até concluir
Memória O histórico de mensagens que dá contexto entre um passo e outro
Condição de parada Limite de iterações ou “sem mais ferramentas”, para não rodar para sempre

O elo que liga o LLM às ferramentas tem nome: tool calling (ou function calling). Em vez de devolver texto, o modelo devolve um pedido estruturado - “execute a ferramenta cotacao com o argumento USD” - e o seu código se encarrega de rodar a função e devolver o resultado. É isso que vamos implementar agora.

O que é tool calling (function calling)?

Tool calling é o recurso em que o LLM responde com um pedido estruturado (nome da ferramenta + argumentos em JSON) em vez de texto. O modelo nunca executa nada: ele só pede. Quem executa é o seu código, que procura a função num dicionário nome -> função, converte os argumentos com o módulo json e devolve o resultado ao modelo. Sem API nenhuma, o despacho é só isto:

1
2
3
4
5
6
7
8
9
10
11
12
13
import json

def cotacao(moeda):
    return {"USD": 5.40, "EUR": 5.85}.get(moeda, "moeda desconhecida")

ferramentas = {"cotacao": cotacao}

# O que o modelo devolve quando decide usar uma ferramenta (simplificado):
pedido = {"name": "cotacao", "arguments": '{"moeda": "EUR"}'}

funcao = ferramentas[pedido["name"]]
argumentos = json.loads(pedido["arguments"])  # os argumentos chegam como texto JSON
print(funcao(**argumentos))  # 5.85

Repare que arguments é uma string JSON, não um dicionário: por isso o json.loads (se quiser revisar o assunto, veja como manipular JSON no Python). Os provedores (OpenAI, Anthropic, Google) usam formatos um pouco diferentes, mas a ideia é a mesma em todos.

Preparando o ambiente

Para o agente “na mão” precisamos só da biblioteca openai, a mesma que usamos no artigo de RAG na prática e no de integração do ChatGPT com Python:

Se ainda não trabalha com ambientes isolados, vale ler este artigo sobre virtualenv antes - mantém cada projeto com suas próprias dependências.

1
pip install openai

Configure sua chave da OpenAI como variável de ambiente:

1
export OPENAI_API_KEY="sua-chave-aqui"

Prefere não pagar API? Dá para rodar tudo com modelos locais via Ollama, contanto que o modelo suporte tool calling. A lógica do agente é exatamente a mesma.

Está curtindo esse conteúdo? :thumbsup:

Que tal receber 30 dias de conteúdo direto na sua Caixa de Entrada?

Sua assinatura não pôde ser validada.
Você fez sua assinatura com sucesso.

Assine as PyDicas e receba 30 dias do melhor conteúdo Python na sua Caixa de Entrada: direto e sem enrolação!

Construindo um agente do zero (sem framework)

Vamos montar um agente que responde a perguntas como “Tenho 250 dólares, quanto é isso em reais hoje?”. Repare que essa pergunta sozinha derruba um LLM puro: ele não sabe a cotação de hoje e é péssimo em conta. Nosso agente vai resolver em dois passos: buscar a cotação e depois calcular.

Passo 1: as ferramentas

Uma ferramenta é só uma função Python comum, com nome, parâmetros e docstring claros. Vamos criar duas:

1
2
3
4
5
6
7
8
9
10
11
12
COTACOES = {"USD": 5.40, "EUR": 5.85}

def cotacao(moeda: str) -> str:
    """Retorna a cotação de uma moeda estrangeira em reais (BRL)."""
    valor = COTACOES.get(moeda.upper())
    if valor is None:
        return f"Moeda '{moeda}' não encontrada."
    return f"1 {moeda.upper()} = {valor} BRL"

def calcular(expressao: str) -> str:
    """Avalia uma expressão aritmética e devolve o resultado."""
    return str(eval(expressao))

:warning: Cuidado com o eval. Usamos aqui por simplicidade, mas eval em texto vindo de um LLM é uma porta aberta para código malicioso. Em produção, troque por um avaliador seguro (a biblioteca simpleeval, por exemplo). Tratar ferramentas como código não confiável é o tipo de cuidado que separa um protótipo de um sistema sério.

Agora descrevemos essas funções para o modelo no formato que a OpenAI espera. É esse “cardápio” que o LLM lê para decidir o que chamar:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
FERRAMENTAS = [
    {
        "type": "function",
        "function": {
            "name": "cotacao",
            "description": "Retorna a cotação atual de uma moeda estrangeira em reais.",
            "parameters": {
                "type": "object",
                "properties": {
                    "moeda": {"type": "string", "description": "Código, ex: USD, EUR"},
                },
                "required": ["moeda"],
            },
        },
    },
    {
        "type": "function",
        "function": {
            "name": "calcular",
            "description": "Avalia uma expressão aritmética, ex: 250 * 5.40",
            "parameters": {
                "type": "object",
                "properties": {
                    "expressao": {"type": "string", "description": "Expressão a calcular"},
                },
                "required": ["expressao"],
            },
        },
    },
]

# Mapa nome -> função, para o loop saber qual chamar.
DISPONIVEIS = {"cotacao": cotacao, "calcular": calcular}

Passo 2: o loop de raciocínio

Aqui mora o coração do agente. O loop chama o modelo; se ele pedir uma ferramenta, executamos e devolvemos o resultado; se ele responder direto, terminamos. O max_iteracoes é a nossa condição de parada - sem ela, um agente pode girar para sempre (e queimar créditos):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
import json
from openai import OpenAI

cliente = OpenAI()  # lê a chave de OPENAI_API_KEY automaticamente

def agente(pergunta: str, max_iteracoes: int = 5) -> str:
    mensagens = [
        {"role": "system", "content": "Você é um assistente que usa ferramentas quando precisa."},
        {"role": "user", "content": pergunta},
    ]

    for _ in range(max_iteracoes):
        resposta = cliente.chat.completions.create(
            model="gpt-4o-mini",
            messages=mensagens,
            tools=FERRAMENTAS,
        )
        msg = resposta.choices[0].message
        mensagens.append(msg)  # guarda a decisão do modelo no histórico (memória)

        # Sem pedido de ferramenta -> o agente concluiu.
        if not msg.tool_calls:
            return msg.content

        # Executa cada ferramenta pedida e devolve o resultado ao modelo.
        for chamada in msg.tool_calls:
            funcao = DISPONIVEIS[chamada.function.name]
            argumentos = json.loads(chamada.function.arguments)
            resultado = funcao(**argumentos)
            print(f"  -> {chamada.function.name}({argumentos})")
            mensagens.append({
                "role": "tool",
                "tool_call_id": chamada.id,
                "content": resultado,
            })

    return "Limite de iterações atingido sem resposta final."

Repare na lista mensagens: ela é a memória do agente. Cada decisão do modelo e cada resultado de ferramenta são anexados ali, e o histórico inteiro volta ao modelo na iteração seguinte. É assim que ele “lembra” que já buscou a cotação na hora de calcular.

Rodando o agente

1
2
if __name__ == "__main__":
    print(agente("Tenho 250 dólares. Quanto é isso em reais hoje?"))

A saída é parecida com esta (as linhas com -> são o nosso rastro mostrando o loop em ação):

1
2
3
4
  -> cotacao({'moeda': 'USD'})
  -> calcular({'expressao': '250 * 5.40'})
Com 250 dólares você tem cerca de R$ 1.350,00 hoje, considerando a cotação de
1 USD = 5,40 BRL.

Veja o que aconteceu: o modelo percebeu que precisava da cotação, chamou a ferramenta cotacao, recebeu 1 USD = 5.4 BRL, concluiu que ainda faltava a conta, chamou calcular com 250 * 5.40 e só então respondeu. Foram duas voltas no loop, duas ferramentas, uma decisão de parada - tudo orquestrado pelo próprio modelo. Isso é um agente.

Construir essa base sólida em Python - de funções e estruturas de dados a integrações com IA como esta - é o que separa quem só copia código de quem entende o que está fazendo. É exatamente isso que ensinamos, do zero ao avançado, na Jornada Python:

O mesmo agente com um framework (LangGraph)

O loop acima é ótimo para entender o mecanismo, mas em um projeto real você não vai querer reescrever (e manter) todo aquele encanamento: histórico, parsing de argumentos, paralelismo, streaming, persistência. É para isso que servem os frameworks de agentes.

O LangGraph - o irmão do LangChain feito justamente para orquestrar agentes - resume tudo o que escrevemos a poucas linhas. Desde a versão 1.0, a porta de entrada recomendada é a função create_agent, do pacote langchain, que monta o agente e o executa sobre o runtime do LangGraph (é também a resposta para quem procura “agente Python com LangChain”). Instale as dependências (o langchain já traz o langgraph junto):

1
pip install langchain langchain-openai

E o agente inteiro vira isto:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
from langchain.agents import create_agent
from langchain_core.tools import tool
from langchain_openai import ChatOpenAI

@tool
def cotacao(moeda: str) -> str:
    """Retorna a cotação de uma moeda estrangeira em reais (BRL)."""
    return f"1 {moeda.upper()} = {COTACOES.get(moeda.upper(), '?')} BRL"

@tool
def calcular(expressao: str) -> str:
    """Avalia uma expressão aritmética."""
    return str(eval(expressao))

modelo = ChatOpenAI(model="gpt-4o-mini", temperature=0)
agente = create_agent(modelo, tools=[cotacao, calcular])

resposta = agente.invoke(
    {"messages": [("user", "Tenho 250 dólares. Quanto é isso em reais hoje?")]}
)
print(resposta["messages"][-1].content)

O create_agent constrói, internamente, o mesmo loop ReAct que fizemos na mão - só que com memória, controle de iterações, tratamento de erros e streaming já resolvidos. O decorador @tool substitui aquele “cardápio” JSON gigante: o framework lê a assinatura e a docstring da função para montar a descrição sozinho.

Viu create_react_agent em outro tutorial? É a versão antiga: from langgraph.prebuilt import create_react_agent ainda funciona, mas desde o LangGraph 1.0 emite um aviso de descontinuação pedindo para migrar para from langchain.agents import create_agent. A troca é direta, com os mesmos argumentos neste exemplo.

O equivalente ao nosso max_iteracoes é o recursion_limit, passado como config no invoke. Cada passo do grafo (chamar o modelo, executar ferramentas) conta; ao estourar o limite, o LangGraph interrompe a execução com um erro em vez de girar para sempre:

1
2
3
4
resposta = agente.invoke(
    {"messages": [("user", "Tenho 250 dólares. Quanto é isso em reais hoje?")]},
    {"recursion_limit": 10},
)

Loop manual ou framework: qual escolher?

Os dois caminhos levam ao mesmo agente. A escolha é de contexto:

Critério Loop manual Framework (LangGraph)
Linhas de código ~40 ~6
Controle total, você vê cada passo abstraído
Aprendizado entende o mecanismo precisa aprender a API
Memória e persistência você implementa embutida (checkpoints)
Streaming, retries, paralelismo manual embutidos
Melhor para aprender, casos simples, controle fino produção e agentes complexos

E a “diferença entre LangChain e LangGraph”? O LangChain te dá os blocos (modelos, prompts, integrações, chains de fluxo fixo). O LangGraph orquestra esses blocos como um grafo com estado e ciclos - exatamente o que um loop de agente precisa. Para chains lineares, LangChain; para agentes que decidem em loop, LangGraph.

Dando ferramentas melhores ao seu agente

Um agente só é tão útil quanto as ferramentas que você dá a ele. A partir do esqueleto acima, dá para evoluir em várias direções:

  • Saída estruturada e validada: em vez de devolver texto solto, faça as ferramentas retornarem dados validados com Pydantic. Isso reduz erros de parsing e deixa o agente muito mais confiável.
  • Conhecimento próprio (RAG): transforme uma busca no seu RAG em uma ferramenta. Aí o agente decide quando consultar os seus documentos, em vez de fazer isso sempre.
  • Ferramentas padronizadas (MCP): o Model Context Protocol (MCP) é um padrão para expor ferramentas a agentes de forma reaproveitável - você escreve a ferramenta uma vez e qualquer agente compatível a usa.

Quando NÃO usar um agente (e usar um prompt simples)

Agente é poderoso, mas tem custo: cada volta no loop é uma chamada de API a mais (mais lento, mais caro) e mais imprevisível. Não vale a pena quando:

  • A tarefa tem passos fixos. Se você já sabe a sequência exata, uma chain ou até uma chamada única ao LLM resolve - sem o overhead de deixar o modelo “decidir”.
  • Não há ferramentas envolvidas. Resumir, traduzir ou classificar um texto não precisa de agente nenhum. É só um prompt.
  • Você precisa de previsibilidade total. Quanto mais autonomia o agente tem, mais difícil é garantir que ele faça sempre a mesma coisa. Para fluxos críticos e auditáveis, código tradicional ganha.
  • Latência e custo importam muito. Um agente que dá cinco voltas no loop custa (e demora) cinco vezes uma chamada simples.

A regra de ouro: comece com o mais simples que resolve (prompt -> chain -> agente) e só suba de nível quando o problema realmente exigir autonomia.

Workflow fixo, agente ou multiagente: quando usar cada um?

Antes de escrever o primeiro loop, decida a arquitetura. A pergunta-chave é: quem decide o próximo passo, o seu código ou o modelo?

Critério Workflow fixo (chain) Agente único Multiagente
Quem decide os passos Você, no código O LLM, em loop Vários LLMs, cada um com seu papel
Previsibilidade Alta, sempre o mesmo caminho Média Baixa, mais difícil de depurar
Custo e latência Menor (nº de chamadas conhecido) Variável (1 chamada por volta) Maior (agentes conversam entre si)
Quando usar Passos conhecidos: extrair -> resumir -> salvar Caminho depende do resultado de cada passo Tarefa grande que se divide em especialidades (pesquisar, escrever, revisar)
Exemplo Classificar e-mails e responder com template Assistente que consulta cotação, banco e calcula Gerador de relatórios com um agente pesquisador e um revisor
Comece com for + chamadas ao LLM ou LangChain Loop manual ou create_agent LangGraph, só depois que um agente único não der conta

Na dúvida, fique na coluna mais à esquerda que resolve o problema. Multiagente multiplica o custo e os pontos de falha; só compensa quando um agente único, com boas ferramentas, claramente não dá conta.

Erros comuns

1. Loop sem limite de passos. Um while True: que só termina quando o modelo “decide” responder pode girar para sempre se o modelo insistir em chamar ferramentas, e cada volta é uma chamada paga. No loop manual, use for _ in range(max_iteracoes) com uma resposta de fallback, como fizemos no agente(). No LangGraph, o recursion_limit faz esse papel e, ao estourar, interrompe com:

1
langgraph.errors.GraphRecursionError: Recursion limit of 3 reached without hitting a stop condition. You can increase the limit by setting the `recursion_limit` config key.

2. Ferramenta que levanta exceção e derruba o agente (ou vira repetição). No nosso loop, funcao(**argumentos) não tem proteção: se o modelo pedir calcular com "250 / 0", o programa inteiro cai com ZeroDivisionError: division by zero. E se você capturar o erro mas devolver algo vago como "falhou", o modelo tende a repetir exatamente a mesma chamada. A correção é tratar a exceção e devolver ao modelo uma mensagem de erro explicativa, como resultado da ferramenta:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
import json

def calcular(expressao: str) -> str:
    return str(eval(expressao))

DISPONIVEIS = {"calcular": calcular}

def executar_ferramenta(nome: str, argumentos_json: str) -> str:
    """Executa a ferramenta e SEMPRE devolve texto, mesmo quando dá erro."""
    if nome not in DISPONIVEIS:
        return f"Erro: a ferramenta '{nome}' não existe. Use uma de: {list(DISPONIVEIS)}"
    try:
        argumentos = json.loads(argumentos_json)
    except json.JSONDecodeError as erro:
        return f"Erro: argumentos não são um JSON válido ({erro}). Use aspas duplas."
    try:
        return DISPONIVEIS[nome](**argumentos)
    except Exception as erro:
        return f"Erro ao executar {nome}: {type(erro).__name__}: {erro}. Não repita os mesmos argumentos."

print(executar_ferramenta("calcular", '{"expressao": "250 * 5.40"}'))
print(executar_ferramenta("calcular", '{"expressao": "250 / 0"}'))
print(executar_ferramenta("calcular", "{'expressao': '2 + 2'}"))
print(executar_ferramenta("converter_moeda", '{"valor": 250}'))
1
2
3
4
1350.0
Erro ao executar calcular: ZeroDivisionError: division by zero. Não repita os mesmos argumentos.
Erro: argumentos não são um JSON válido (Expecting property name enclosed in double quotes: line 1 column 2 (char 1)). Use aspas duplas.
Erro: a ferramenta 'converter_moeda' não existe. Use uma de: ['calcular']

No loop do agente(), basta trocar as três linhas de funcao, argumentos e resultado por resultado = executar_ferramenta(chamada.function.name, chamada.function.arguments). Para cortar repetições de vez, guarde as chamadas já feitas (veja o exercício 6).

3. JSON de argumentos inválido. Os argumentos chegam como texto gerado pelo modelo, e modelos menores (principalmente locais) às vezes devolvem aspas simples, vírgula sobrando ou JSON truncado. Um json.loads direto quebra o loop:

1
2
3
import json

json.loads("{'moeda': 'USD'}")
1
json.decoder.JSONDecodeError: Expecting property name enclosed in double quotes: line 1 column 2 (char 1)

Correção: capture json.JSONDecodeError (como no executar_ferramenta acima) e devolva o erro ao modelo para ele tentar de novo. Se o modelo inventar um nome de ferramenta que não existe, o DISPONIVEIS[nome] do loop original falha com KeyError: 'converter_moeda' - o mesmo executar_ferramenta cobre esse caso.

4. Descrição de ferramenta vaga. O modelo escolhe a ferramenta só pelo nome e pela descrição que você manda. Com duas ferramentas descritas como "Faz cálculos" e "Busca dados", ele pode tentar calcular a cotação de cabeça em vez de buscá-la, ou passar "dólar" onde você esperava "USD". Descreva o que a ferramenta faz, quando usar e o formato de cada argumento:

1
2
3
4
5
6
7
8
9
10
11
12
# Vago: o modelo tem que adivinhar
vaga = {"name": "cotacao", "description": "Busca dados"}

# Claro: diz o que faz, quando usar e o formato esperado
clara = {
    "name": "cotacao",
    "description": (
        "Retorna a cotação atual de uma moeda estrangeira em reais. "
        "Use sempre que a pergunta envolver conversão de moeda. "
        "O argumento moeda é o código ISO em maiúsculas, ex: USD, EUR."
    ),
}

5. Contexto crescendo a cada iteração. A lista mensagens recebe a decisão do modelo e o resultado de cada ferramenta, e o histórico inteiro é reenviado em toda volta. Em tarefas longas (ou com ferramentas que devolvem páginas inteiras), o custo por chamada sobe e a conversa acaba estourando a janela de contexto do modelo, que passa a devolver erro. Corte resultados grandes na própria ferramenta e apare o histórico antigo:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
def aparar_historico(mensagens: list, manter: int = 6) -> list:
    """Mantém a mensagem de sistema, a pergunta original e as últimas `manter` mensagens."""
    if len(mensagens) <= manter + 2:
        return mensagens
    recentes = mensagens[-manter:]
    # Não começar o recorte por um resultado de ferramenta órfão.
    while recentes and recentes[0]["role"] == "tool":
        recentes = recentes[1:]
    return mensagens[:2] + recentes

historico = [{"role": "system"}, {"role": "user"}]
for i in range(5):
    historico += [{"role": "assistant"}, {"role": "tool"}]
print(len(historico), "->", len(aparar_historico(historico)))
print([m["role"] for m in aparar_historico(historico)])
1
2
12 -> 8
['system', 'user', 'assistant', 'tool', 'assistant', 'tool', 'assistant', 'tool']

O cuidado do while importa: na API da OpenAI, uma mensagem tool precisa vir logo depois da mensagem do assistente que pediu aquela ferramenta; se o recorte começar por um resultado órfão, a API recusa a requisição.

Exercícios resolvidos

Todos os exercícios de código rodam offline, sem chave de API.

Exercício 1. (Estilo prova) Qual característica diferencia um agente de IA de uma chain (workflow fixo)?

a) O agente usa um LLM e a chain não.
b) O agente decide em tempo de execução quais ferramentas chamar e quando parar; na chain, a sequência de passos é definida pelo programador.
c) A chain não pode chamar ferramentas externas.
d) O agente sempre usa mais de um modelo de linguagem.

Ver solução

Resposta: b. As duas abordagens usam LLM (a está errada), uma chain pode chamar ferramentas em ordem fixa (c está errada) e um agente pode perfeitamente usar um único modelo (d está errada). O que define o agente é a autonomia de escolher o próximo passo dentro de um loop.

Exercício 2. Crie as ferramentas somar(a, b) e multiplicar(a, b), um dicionário que mapeia nome -> função e uma função despachar(chamada) que recebe um pedido simulado no formato {"name": ..., "arguments": "<json>"} e devolve o resultado.

Ver solução
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
import json

def somar(a, b):
    return a + b

def multiplicar(a, b):
    return a * b

ferramentas = {"somar": somar, "multiplicar": multiplicar}

def despachar(chamada: dict):
    funcao = ferramentas[chamada["name"]]
    argumentos = json.loads(chamada["arguments"])
    return funcao(**argumentos)

print(despachar({"name": "multiplicar", "arguments": '{"a": 6, "b": 7}'}))

Saída: 42

É exatamente o que o loop do agente faz a cada tool call: busca a função pelo nome no dicionário, converte os argumentos de texto JSON para dicionário e desempacota com **.

Exercício 3. Escreva validar_argumentos(texto, obrigatorios) que devolve o dicionário de argumentos se o JSON for válido e tiver todos os campos obrigatórios, ou uma string começando com "erro:" caso contrário.

Ver solução
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
import json

def validar_argumentos(texto: str, obrigatorios: list):
    try:
        dados = json.loads(texto)
    except json.JSONDecodeError:
        return "erro: JSON inválido"
    if not isinstance(dados, dict):
        return "erro: esperado um objeto JSON"
    faltando = [campo for campo in obrigatorios if campo not in dados]
    if faltando:
        return f"erro: faltando {faltando}"
    return dados

print(validar_argumentos('{"moeda": "USD"}', ["moeda"]))
print(validar_argumentos("{'moeda': 'USD'}", ["moeda"]))
print(validar_argumentos('{"valor": 10}', ["moeda"]))

Saída:

1
2
3
{'moeda': 'USD'}
erro: JSON inválido
erro: faltando ['moeda']

Devolver o erro como texto (em vez de levantar exceção) permite mandar a mensagem de volta ao modelo como resultado da ferramenta, para ele corrigir a chamada.

Exercício 4. (Estilo prova) No agente() deste artigo, o que acontece se você remover a linha mensagens.append(msg) (a que guarda a decisão do modelo no histórico)?

a) Nada muda, porque o resultado da ferramenta continua sendo anexado.
b) O agente fica mais rápido e mais barato, sem efeito colateral.
c) O histórico fica inconsistente: aparecem resultados de ferramenta sem o pedido que os originou, e a API da OpenAI rejeita a requisição.
d) O modelo passa a executar as ferramentas sozinho.

Ver solução

Resposta: c. Cada mensagem role: "tool" referencia um tool_call_id que precisa existir numa mensagem anterior do assistente. Sem o append(msg), esse pedido some do histórico e a próxima chamada é recusada. A alternativa d é o erro conceitual clássico: o modelo nunca executa ferramentas, só pede.

Exercício 5. Implemente agente(pergunta, llm, ferramentas, max_passos=3) que roda o loop e, se o LLM nunca responder, devolve "Parei após N passos sem resposta final.". Teste com um LLM simulado “teimoso” que sempre pede a ferramenta buscar.

Ver solução
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
def llm_teimoso(historico):
    return {"ferramenta": "buscar", "args": {"termo": "python"}}  # nunca responde

def buscar(termo):
    return f"resultados para {termo}"

def agente(pergunta, llm, ferramentas, max_passos=3):
    historico = [pergunta]
    for passo in range(1, max_passos + 1):
        decisao = llm(historico)
        if "resposta" in decisao:
            return decisao["resposta"]
        historico.append(ferramentas[decisao["ferramenta"]](**decisao["args"]))
    return f"Parei após {max_passos} passos sem resposta final."

print(agente("O que é Python?", llm_teimoso, {"buscar": buscar}))

Saída: Parei após 3 passos sem resposta final.

Com while True, esse mesmo LLM travaria o programa (e a fatura da API). O for com limite garante que o agente sempre termina.

Exercício 6. Escreva detectar_repeticao(chamadas) que recebe uma lista de tuplas (nome, argumentos) e devolve True se a mesma ferramenta foi chamada duas vezes com os mesmos argumentos.

Ver solução
1
2
3
4
5
6
7
8
9
10
11
12
13
import json

def detectar_repeticao(chamadas: list) -> bool:
    vistas = set()
    for nome, argumentos in chamadas:
        chave = (nome, json.dumps(argumentos, sort_keys=True))
        if chave in vistas:
            return True
        vistas.add(chave)
    return False

print(detectar_repeticao([("cotacao", {"moeda": "USD"}), ("calcular", {"expressao": "2*3"})]))
print(detectar_repeticao([("cotacao", {"moeda": "USD"}), ("cotacao", {"moeda": "USD"})]))

Saída: False e depois True

Dicionários não podem entrar num set, então os argumentos viram uma string JSON com sort_keys=True (assim {"a": 1, "b": 2} e {"b": 2, "a": 1} geram a mesma chave). No agente, ao detectar a repetição você pode interromper o loop ou avisar o modelo de que aquela chamada já foi feita.

Conclusão

Você acabou de construir um agente de IA funcional das duas formas que importam: na mão, para entender que por baixo do hype existe só um LLM, ferramentas e um loop; e com LangGraph, para ver como um framework empacota tudo isso em poucas linhas. Esse mesmo esqueleto - trocando as duas funções de exemplo por ferramentas de verdade - é o que move desde assistentes de código até sistemas autônomos de produção.

É exatamente esse tipo de arquitetura que está por trás do Ebookr.ai, minha plataforma que gera ebooks profissionais com IA: por dentro, é um sistema de agentes que planejam, pesquisam e escrevem em etapas, cada um com suas ferramentas. Se quer ver agentes aplicados de verdade, em escala, dá uma conferida:

E você, qual ferramenta vai plugar no seu primeiro agente? Conta aqui nos comentários! :point_down:

Nos vemos no próximo artigo! :wave:

Perguntas frequentes

O que é um agente de IA?

Um agente de IA é um LLM acoplado a ferramentas (funções que ele pode chamar, como uma busca, um cálculo ou uma consulta a banco) e a um loop de raciocínio. A cada volta, o modelo decide a próxima ação, o código executa a ferramenta escolhida e o resultado volta para o modelo, até ele concluir a tarefa ou atingir um limite de passos.

Qual a diferença entre um agente e um chatbot comum?

Um chatbot devolve texto. Um agente decide ações, usa ferramentas e repete esse ciclo até concluir uma tarefa concreta, como buscar um dado e fazer um cálculo com ele. O agente age; o chatbot apenas conversa.

O que é tool calling (function calling)?

Tool calling é o recurso em que o LLM, em vez de responder com texto, devolve um pedido estruturado para executar uma ferramenta: o nome da função e os argumentos em JSON, por exemplo cotacao com {"moeda": "USD"}. O modelo não executa nada: é o seu código que roda a função e devolve o resultado para o modelo continuar.

Qual a diferença entre LangChain e LangGraph?

O LangChain oferece os blocos de construção (modelos, prompts, integrações e chains de fluxo fixo) e, desde a versão 1.0, a função create_agent. O LangGraph é o runtime que orquestra agentes: representa o fluxo como um grafo com estado e ciclos, que é o que um loop de raciocínio precisa. O create_agent do LangChain roda sobre o LangGraph.

Preciso de um framework para criar um agente de IA em Python? Quando usar LangGraph?

Não é obrigatório. Um agente é, na essência, um LLM com tool calling dentro de um loop, e dá para fazer só com a biblioteca openai. Use LangChain/LangGraph quando precisar de memória persistente entre conversas, streaming, retries, pausas para aprovação humana, fluxos com vários agentes ou quando o loop manual começar a virar um framework caseiro.

Qual modelo usar para um agente?

Qualquer um com bom suporte a tool calling: GPT-4o e GPT-4o-mini, Claude, Gemini ou modelos locais via Ollama que aceitem ferramentas. Para agentes que encadeiam muitos passos, um modelo mais capaz erra menos a decisão de qual ferramenta chamar e economiza voltas no loop.

Um agente de IA é seguro? Quais são os custos e riscos?

O agente só executa as ferramentas que você define, mas os argumentos são gerados por um LLM. Os riscos principais são ações perigosas (um eval, shell, apagar arquivos ou enviar e-mails sem confirmação) e loops que não terminam. Cada volta no loop é uma chamada paga à API, então um agente preso repetindo a mesma ferramenta queima tokens rápido. Valide argumentos, restrinja permissões, peça confirmação humana para ações irreversíveis e sempre limite o número de passos.

Como limitar o número de iterações de um agente?

No loop manual, troque while True por for _ in range(max_iteracoes) e devolva uma mensagem de fallback quando o limite acabar. No LangChain/LangGraph, passe {"recursion_limit": N} como config no invoke: ao estourar, o LangGraph levanta GraphRecursionError. Também vale detectar chamadas repetidas (mesma ferramenta com os mesmos argumentos) e interromper antes.

Começe agora sua Jornada na Programação!

Não deixe para amanhã o sucesso que você pode começar a construir hoje!

#newsletter Olá :wave: Curtiu o artigo? Então faça parte da nossa Newsletter! Privacidade Não se preocupe, respeitamos sua privacidade. Você pode se descadastrar a qualquer momento.