Python & AI Tutorials Logo
LangChain & LangGraph

14. Construindo o Loop do Agente

No Capítulo 13, aprendemos como executar as solicitações de chamada de ferramenta do LLM e retornar os resultados. No entanto, aquele trabalho assumia que toda tarefa poderia ser tratada em uma única chamada de ferramenta. Na prática, as chamadas de ferramenta frequentemente precisam continuar por várias rodadas até que o LLM tenha reunido todas as informações necessárias para uma resposta final.

Considere a solicitação "encontre a população da França e depois multiplique-a por dois". Essa tarefa requer pelo menos duas chamadas de ferramenta. Você precisa primeiro consultar a população — só então pode executar o cálculo. A segunda chamada depende do primeiro resultado, então não há como tratá-la em uma única chamada de ferramenta.

Neste capítulo, transformamos o ciclo único do Capítulo 13 em um loop. Enquanto o LLM solicitar chamadas de ferramenta, continuamos executando-as — repetindo até que o LLM pare de solicitar ferramentas por conta própria. Em seguida, adicionamos limites de segurança para evitar que o loop rode para sempre, e abordamos o tratamento de erros para que o agente não trave quando uma ferramenta falhar.

14.1) Do Ciclo Único ao Loop

14.1.1) Como Funciona o Loop do Agente?

Como vimos na introdução, tarefas em que a próxima ação depende do resultado da etapa anterior frequentemente não podem ser tratadas com uma única chamada de ferramenta. O LLM precisa chamar uma ferramenta, verificar o resultado e decidir novamente. É isso que o loop do agente faz, e ele funciona em três estágios:

  • Pensar (Think) — O LLM lê a conversa até o momento e decide o que fazer em seguida. Se precisar de uma ferramenta, ele solicita uma chamada de ferramenta através de tool_calls. Caso contrário, ele retorna uma resposta final.
  • Agir (Act) — Executa a ferramenta especificada em tool_calls.
  • Observar (Observe) — Verifica o resultado da execução da ferramenta e o adiciona à conversa em um ToolMessage.

O loop do agente repete esses três estágios até que o LLM não solicite mais nenhuma ferramenta. Esse padrão também é conhecido como ReAct (Reason + Act), e a ideia principal é alternar entre raciocínio e ação.

Sim

Não

Entrada do Usuário

PENSAR
Chamar LLM

tool_calls
presente?

AGIR
Executar ferramenta

OBSERVAR
Verificar resultado

Resposta Final

Se tool_calls estiver presente, execute a ferramenta, adicione o resultado à conversa e chame o LLM novamente. Se tool_calls estiver vazio, o LLM retornou uma resposta final e o loop termina. Agora vamos colocar isso em código.

14.1.2) Implementando o Loop Pensar-Agir-Observar

Vamos transformar o loop Pensar-Agir-Observar da seção anterior em código. Primeiro, preparamos as ferramentas e o modelo.

python
from langchain_openai import ChatOpenAI
from langchain_core.messages import HumanMessage, SystemMessage
from langchain.tools import tool
 
# Define as ferramentas
@tool
def get_weather(city: str) -> str:
    """Obtém o clima atual de uma cidade."""
    fake_data = {"Tokyo": "18°C, nublado", "Cairo": "31°C, ensolarado"}
    return fake_data.get(city, f"Sem dados de clima para {city}.")
 
@tool
def calculate(expression: str) -> str:
    """Avalia uma expressão aritmética simples, ex. '3 * 21'."""
    return str(eval(expression))  # Aviso: eval() é um risco de segurança. Não use em produção.
 
# Vincula as ferramentas
tools = [get_weather, calculate]
llm = ChatOpenAI(model="gpt-5-mini")
llm_with_tools = llm.bind_tools(tools)
tool_map = {t.name: t for t in tools}

No Capítulo 13, executamos uma ferramenta uma vez e paramos. Agora repetimos até que o LLM pare de solicitar chamadas de ferramenta. Dentro de um loop while True, chamamos o LLM e, se a resposta contiver tool_calls, executamos as ferramentas e chamamos o LLM novamente. Se não houver tool_calls, o LLM retornou uma resposta final, então saímos do loop.

python
def run_agent(user_input: str) -> str:
    """Executa o loop Pensar-Agir-Observar até que o LLM retorne uma resposta final."""
    messages = [
        SystemMessage(content="Você é um assistente prestativo."),
        HumanMessage(content=user_input),
    ]
 
    while True:
        # PENSAR: Pede ao LLM para decidir a próxima ação
        print("PENSAR: Pedindo ao LLM para decidir")
        ai_message = llm_with_tools.invoke(messages)
        messages.append(ai_message)
 
        # Condição de saída: nenhum tool_calls significa que esta é a resposta final
        if not ai_message.tool_calls:
            return ai_message.content
 
        # AGIR + OBSERVAR: Executa as ferramentas solicitadas e adiciona os resultados à conversa
        for tool_call in ai_message.tool_calls:
            selected_tool = tool_map[tool_call["name"]]
            print(f"AGIR: chamando '{tool_call['name']}', args={tool_call['args']}")
 
            tool_message = selected_tool.invoke(tool_call)
            print(f"OBSERVAR: {tool_message.content}")
 
            messages.append(tool_message)

Vamos comparar isso com o código do Capítulo 13. No Capítulo 13, após executar as ferramentas, chamamos o LLM uma última vez para obter a resposta. No Capítulo 14, colocamos esse mesmo processo dentro de um while True e verificamos tool_calls a cada iteração para decidir se continuamos. Os blocos de construção são os mesmos do Capítulo 13 — apenas os envolvemos em um loop.

Vamos executá-lo.

python
answer = run_agent("Como está o tempo em Tokyo, e está quente o suficiente para uma caminhada?")
print(f'Resposta final: {answer}')

Saída:

PENSAR: Pedindo ao LLM para decidir
AGIR: chamando 'get_weather', args={'city': 'Tokyo'}
OBSERVAR: 18°C, nublado
PENSAR: Pedindo ao LLM para decidir
Resposta final: Agora em Tokyo está 18°C (cerca de 64°F) e nublado.
Essa temperatura é geralmente amena e confortável para uma caminhada para a maioria das pessoas.

A saída mostra o fluxo PENSAR → AGIR → OBSERVAR → PENSAR. Na primeira iteração, o LLM solicitou uma chamada get_weather e, na segunda iteração, viu o resultado do clima e gerou uma resposta final. Como tool_calls estava vazio, a resposta final foi retornada e o loop terminou.

Agora vamos testar o cenário de etapas dependentes da introdução — onde a próxima chamada só pode acontecer após ver o resultado da anterior.

python
answer = run_agent("Obtenha a temperatura no Cairo, depois multiplique o número por 3.")
print(f'Resposta final: {answer}')

Saída:

PENSAR: Pedindo ao LLM para decidir
AGIR: chamando 'get_weather', args={'city': 'Cairo'}
OBSERVAR: 31°C, ensolarado
PENSAR: Pedindo ao LLM para decidir
AGIR: chamando 'calculate', args={'expression': '31 * 3'}
OBSERVAR: 93
PENSAR: Pedindo ao LLM para decidir
Resposta final: Temperatura atual no Cairo: 31°C. Multiplicada por 3 = 93.

Desta vez, o loop rodou três iterações.

  1. Primeira iteração — O LLM solicita get_weather("Cairo").
  2. Segunda iteração — Após ver o resultado "31°C, ensolarado", o LLM solicita calculate("31 * 3"). Ele só conseguiu formar a expressão após ver a temperatura.
  3. Terceira iteração — Após ver ambos os resultados "31°C, ensolarado" e "93", o LLM retornou a resposta final.

14.2) Adicionando Limites de Segurança

O loop que construímos acima tem apenas uma condição de saída: quando o LLM responde sem tool_calls, saímos do loop. Em circunstâncias normais isso é suficiente, mas o que acontece se o LLM nunca parar de solicitar chamadas de ferramenta?

Por exemplo, se uma ferramenta sempre retorna resultados ambíguos, o LLM pode continuar chamando-a na esperança de obter algo melhor. Como o loop é while True, se o LLM não parar, o programa também não para. Os custos de chamadas de API continuam se acumulando à medida que o programa roda indefinidamente.

A correção mais simples é impor um limite máximo de quantas vezes o loop pode rodar. Substitua while True por for step in range(max_steps), e o loop está garantido a terminar após max_steps iterações, independentemente do que o LLM fizer.

python
def run_agent(user_input: str, max_steps: int = 10) -> str:
    """Executa o loop Pensar-Agir-Observar dentro de max_steps iterações."""
    messages = [
        SystemMessage(content="Você é um assistente prestativo."),
        HumanMessage(content=user_input),
    ]
 
    for step in range(max_steps):
        # PENSAR
        ai_message = llm_with_tools.invoke(messages)
        messages.append(ai_message)
 
        # Condição de saída: nenhum tool_calls significa que esta é a resposta final
        if not ai_message.tool_calls:
            return ai_message.content
 
        # AGIR + OBSERVAR
        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)
 
    # max_steps atingido: o loop terminou sem uma resposta final
    return f"[Interrompido após atingir o máximo de iterações ({max_steps})]"

Comparado ao código anterior, duas coisas mudaram. while True tornou-se for step in range(max_steps), e um valor de retorno foi adicionado para quando o loop atinge max_steps. O loop agora termina de uma de duas maneiras: o LLM retorna uma resposta final por conta própria (terminação natural), ou max_steps é atingido (terminação de segurança).

Vamos verificar que o limite de segurança realmente funciona. Criaremos uma ferramenta que nunca retorna resultados úteis, forçando o LLM a uma situação onde ele nunca para de solicitar chamadas de ferramenta.

python
@tool
def unhelpful_search(query: str) -> str:
    """Busca por informações."""
    return "Nenhum resultado encontrado. Tente reformular sua consulta."
 
llm_with_bad_tool = llm.bind_tools([unhelpful_search])
tool_map_bad = {unhelpful_search.name: unhelpful_search}
 
def run_agent_bad(user_input: str, max_steps: int = 5) -> str:
    messages = [
        SystemMessage(content=(
            "Você deve SEMPRE usar a ferramenta unhelpful_search para encontrar informações. "
            "Você NÃO tem permissão para responder com base no seu próprio conhecimento. "
            "Se a ferramenta não retornar resultados, você DEVE reformular e buscar novamente. "
            "Continue buscando até encontrar a resposta."
        )),
        HumanMessage(content=user_input),
    ]
 
    for step in range(max_steps):
        print(f"--- Etapa {step + 1} ---")
        ai_message = llm_with_bad_tool.invoke(messages)
        messages.append(ai_message)
 
        if not ai_message.tool_calls:
            return ai_message.content
 
        for tool_call in ai_message.tool_calls:
            selected_tool = tool_map_bad[tool_call["name"]]
            tool_message = selected_tool.invoke(tool_call)
            print(f"AGIR: '{tool_call['name']}' → {tool_message.content}")
            messages.append(tool_message)
 
    return f"[Interrompido após atingir o máximo de iterações ({max_steps})]"
 
answer = run_agent_bad("Qual é a população da França?")
print(f"\nResposta final: {answer}")

Saída:

--- Etapa 1 ---
AGIR: 'unhelpful_search' → Nenhum resultado encontrado. Tente reformular sua consulta.
--- Etapa 2 ---
AGIR: 'unhelpful_search' → Nenhum resultado encontrado. Tente reformular sua consulta.
--- Etapa 3 ---
AGIR: 'unhelpful_search' → Nenhum resultado encontrado. Tente reformular sua consulta.
--- Etapa 4 ---
AGIR: 'unhelpful_search' → Nenhum resultado encontrado. Tente reformular sua consulta.
--- Etapa 5 ---
AGIR: 'unhelpful_search' → Nenhum resultado encontrado. Tente reformular sua consulta.
 
Resposta final: [Interrompido após atingir o máximo de iterações (5)]

Sem max_steps, esse loop teria rodado para sempre. Graças ao max_steps=5, ele foi forçosamente terminado após cinco iterações.

O valor certo para max_steps depende da complexidade do seu agente. Muito baixo, e tarefas complexas são cortadas cedo demais. Muito alto, e um agente com mau comportamento acumula custos antes de ser interrompido. 15–25 é um ponto de partida comum; ajuste com base nas suas cargas de trabalho reais.

14.3) Tratando Erros de Ferramentas no Loop

O loop que construímos em 14.1 e 14.2 assume que as ferramentas sempre executam com sucesso. Mas o que acontece se uma ferramenta lançar uma exceção? O código atual não tem tratamento de exceções, então se uma exceção ocorrer durante a execução da ferramenta, o agente inteiro trava.

No Capítulo 12, aprendemos como capturar exceções dentro da própria ferramenta com try/except e retornar mensagens de erro como strings. Se uma ferramenta for construída dessa forma, não há problema. Mas nem toda ferramenta trata erros internamente. Ferramentas que chamam bibliotecas externas ou APIs podem lançar exceções inesperadas.

Para se proteger contra isso, é uma boa ideia tratar erros também no nível do loop. A abordagem é direta: envolva a execução da ferramenta em um try/except e, se uma exceção ocorrer, coloque a mensagem de erro em um ToolMessage e passe-a para o LLM. O LLM pode ler essa mensagem de erro e tentar novamente com argumentos corrigidos ou escolher uma abordagem diferente. Isso é chamado de autocorreção (self-correction).

python
from langchain_openai import ChatOpenAI
from langchain_core.messages import HumanMessage, SystemMessage, ToolMessage
from langchain.tools import tool
 
# Define as ferramentas
@tool
def calculate(expression: str) -> str:
    """Avalia uma expressão aritmética simples, ex. '3 * 21'."""
    return str(eval(expression))  # Aviso: eval() é um risco de segurança. Não use em produção.
 
# Vincula as ferramentas
tools = [calculate]
llm = ChatOpenAI(model="gpt-5-mini")
llm_with_tools = llm.bind_tools(tools)
tool_map = {t.name: t for t in tools}
 
def run_agent(user_input: str, max_steps: int = 10) -> str:
    """Loop do agente que passa erros de ferramenta para o LLM, permitindo autocorreção."""
    messages = [
        SystemMessage(content="Você é um assistente prestativo."),
        HumanMessage(content=user_input),
    ]
 
    for step in range(max_steps):
        # PENSAR
        print("PENSAR: Pedindo ao LLM para decidir")
        ai_message = llm_with_tools.invoke(messages)
        messages.append(ai_message)
 
        if not ai_message.tool_calls:
            return ai_message.content
 
        # AGIR + OBSERVAR
        for tool_call in ai_message.tool_calls:
            selected_tool = tool_map[tool_call["name"]]
            try:
                print(f"AGIR: chamando '{tool_call['name']}', args={tool_call['args']}")
                tool_message = selected_tool.invoke(tool_call)
                print(f"OBSERVAR: {tool_message.content}")
            except Exception as e:
                print(f"OBSERVAR: Erro - {e}")
                tool_message = ToolMessage(
                    content=f"Erro: {e}",
                    tool_call_id=tool_call["id"],
                )
            messages.append(tool_message)
 
    return f"[Interrompido após atingir o máximo de iterações ({max_steps})]"

Comparado ao código de 14.2, a única mudança é o try/except. Se uma ferramenta lançar uma exceção, a mensagem de erro é colocada em um ToolMessage e adicionada à conversa. Observe que mesmo uma chamada com falha deve ter um ToolMessage com o tool_call_id correspondente. O LLM verá esse erro na próxima iteração e decidirá o que fazer em seguida.

Vamos verificar que a autocorreção funciona. Acionaremos uma exceção pedindo à ferramenta calculate para dividir por zero.

python
answer = run_agent("Use a ferramenta de calculadora para computar 10 / 0")
print(f"Resposta final: {answer}")

Saída:

PENSAR: Pedindo ao LLM para decidir
AGIR: chamando 'calculate', args={'expression': '10 / 0'}
OBSERVAR: Erro - division by zero
PENSAR: Pedindo ao LLM para decidir
Resposta final: Usei a ferramenta de calculadora e ela retornou um erro: "division by zero."
 
Explicação: 10 / 0 é indefinido na aritmética comum, então não pode produzir um número finito.
 
Você gostaria que eu:
- calculasse os limites laterais,
- mostrasse o resultado de ponto flutuante IEEE-754,
- ou avaliasse uma expressão diferente?

Na primeira iteração, o LLM solicitou calculate("10 / 0") e um ZeroDivisionError foi lançado. O try/except capturou a exceção e passou a mensagem de erro para o LLM. Na segunda iteração, o LLM viu a mensagem de erro e retornou uma resposta final explicando que a divisão por zero é impossível. Sem o try/except, o programa teria travado no primeiro ZeroDivisionError.