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.
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.
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.
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.
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.
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.
- Primeira iteração — O LLM solicita
get_weather("Cairo"). - Segunda iteração — Após ver o resultado
"31°C, ensolarado", o LLM solicitacalculate("31 * 3"). Ele só conseguiu formar a expressão após ver a temperatura. - 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.
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.
@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).
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.
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.