O ciclo Thought → Action → Observation

Até aqui vimos as peças separadas:
- Como as Tools são apresentadas ao modelo, dentro da system message.
- Como um Agent raciocina, planeja e age sobre o ambiente.
Agora vamos juntar tudo em um único mecanismo — o ciclo. Ele é o coração de qualquer Agent, e depois de entendê-lo você vai reconhecer a mesma estrutura em todos os frameworks.
Os três componentes
O trabalho de um Agent é um ciclo contínuo:
- Thought — o LLM decide qual deve ser o próximo passo.
- Action — o Agent executa esse passo, chamando uma Tool com os argumentos escolhidos.
- Observation — o resultado da Tool volta para o modelo, que o lê antes de decidir de novo.

Em termos de programação, o Agent é um while: o laço continua até o objetivo ser cumprido — ou até bater no limite de passos que você definiu.
while não_terminou:
pensamento = modelo.pensar(historico)
resultado = executar(pensamento.tool, pensamento.argumentos)
historico.append(resultado)
Isso é literalmente o que um framework de agentes faz. O resto é conveniência.

Repare no que a system message precisa conter para esse laço funcionar:
- O comportamento esperado do Agent.
- As Tools disponíveis, no formato que vimos no capítulo anterior.
- O próprio ciclo — a instrução de escrever um Thought, depois uma Action, e esperar a Observation.
Sim: o ciclo também é texto no prompt. O modelo não tem um "modo agente". Ele foi instruído, em linguagem natural, a escrever nesse formato — e o programa em volta é que dá sentido a isso.
O ciclo, no caso do Alfred judiciário
Vamos acompanhar um pedido do começo ao fim.
Usuário: "Alfred, traga os 3 processos mais recentes sobre violência doméstica."
Alfred tem três Tools: consultar_assunto, buscar_processos e detalhar_processo.
Passo 1 — Thought
O modelo escreve seu raciocínio:
Thought: O usuário quer processos sobre violência doméstica, mas a base de processos é consultada por código de assunto, não por nome. Preciso primeiro converter o termo em código. Tenho a Tool
consultar_assuntopara isso.
Repare no que aconteceu: ele decompôs o pedido. Ninguém disse que existe um código de assunto intermediário — isso veio da descrição das Tools.
Passo 2 — Action
Com base no Thought, ele produz a chamada, no formato combinado:
{
"action": "consultar_assunto",
"action_input": {
"termo": "violência doméstica"
}
}
Isso é texto gerado pelo modelo. Nada foi executado ainda. O seu programa lê esse texto, reconhece a chamada e roda a função Python de verdade.
Passo 3 — Observation
A função retorna, e o resultado é acrescentado à conversa:
Observation: 1001
Essa linha entra no prompt como uma nova mensagem. É o retorno do mundo real — a confirmação de que a ação funcionou, e o dado de que ele precisava.
Passo 4 — novo Thought
O modelo lê a Observation e decide o próximo passo:
Thought: Obtive o código 1001. Agora posso buscar os processos desse assunto, ordenados do mais recente para o mais antigo, limitando a 3 como o usuário pediu.
Passo 5 — nova Action
{
"action": "buscar_processos",
"action_input": {
"codigo_assunto": 1001,
"limite": 3
}
}
Passo 6 — nova Observation
Estes são os dados reais da base sintética do curso:
Observation: [
{"numero_processo": "0000184-40.2026.8.19.0001", "data_distribuicao": "2026-01-10",
"situacao": "Arquivado", "orgao_julgador": "2ª Vara de Violência Doméstica e Familiar contra a Mulher"},
{"numero_processo": "0000031-41.2025.8.19.0001", "data_distribuicao": "2025-08-23",
"situacao": "Arquivado", "orgao_julgador": "2ª Vara de Violência Doméstica e Familiar contra a Mulher"},
{"numero_processo": "0000116-27.2025.8.19.0001", "data_distribuicao": "2025-06-02",
"situacao": "Suspenso", "orgao_julgador": "1ª Vara de Violência Doméstica e Familiar contra a Mulher"}
]
Vale parar nesta Observation, porque tem uma ausência dentro dela.
Consultando a base sem filtro de sigilo, o terceiro processo mais recente desse assunto seria o 0000121-49.2025.8.19.0001, distribuído em 27/07/2025 — mais novo que o 0000116-27, que apareceu na lista. Ele não está aí porque está classificado como Sigilo Absoluto, e a sessão que fez a consulta tem teto de acesso em Segredo de Justiça.
Três coisas para reter:
- O filtro aconteceu no
WHERE. O processo sigiloso não foi lido, não entrou na lista, não passou pelo prompt. O modelo nunca soube que ele existe — e é assim que tem que ser. - O modelo não teve participação nenhuma nisso. Ele pediu "os 3 mais recentes do assunto 1001" e recebeu 3 registros. Não houve decisão de sigilo a ser tomada por ele, e portanto não houve decisão a ser burlada.
- A resposta não avisa que algo foi omitido — e nem deveria. Dizer "há 1 processo que você não pode ver" já é informação sobre a existência dele.
Rode a mesma consulta com teto 0 (Público) e o retorno é uma lista vazia: nenhum processo de violência doméstica na base é público. É o comportamento correto, e é o tipo de coisa que só aparece quando você testa o que o sistema não deve fazer.
Passo 7 — resposta final
Com os dados em mãos, o modelo encerra o ciclo:
Thought: Tenho os três processos. Não preciso de mais nenhuma Tool. Posso redigir a resposta.
Final answer: Os três processos mais recentes sobre violência doméstica são o 0000184-40.2026.8.19.0001 (2ª Vara, distribuído em 10/01/2026, arquivado), o 0000031-41.2025.8.19.0001 (2ª Vara, 23/08/2025, arquivado) e o 0000116-27.2025.8.19.0001 (1ª Vara, 02/06/2025, suspenso).
Laço encerrado.
O que esse exemplo mostra
O Agent itera até cumprir o objetivo. Se a Observation do passo 3 tivesse voltado -1 (assunto não encontrado), o modelo poderia ter tentado outro termo, ou informado que não localizou o assunto. O ciclo é o que permite corrigir o rumo — algo que uma chamada única não permite.
A Tool traz o que o modelo não sabe. Nenhum desses números de processo estava no treinamento do modelo. Eles vieram da consulta. É isso que separa "buscar" de "inventar".
Cada volta incorpora informação nova. O raciocínio do passo 4 seria impossível no passo 1 — o código 1001 ainda não existia para ele.
Esse padrão tem nome: ReAct (Reason + Act), que vamos detalhar no próximo capítulo.
Onde esse laço dá errado
Vale conhecer os modos de falha antes de encontrá-los.
O laço não fecha. O modelo continua chamando Tools sem chegar a uma conclusão — às vezes repetindo a mesma chamada. Por isso todo framework tem um max_steps. Sem ele, o Agent roda até acabar o contexto.
O contexto estoura. Cada volta acrescenta Thought, Action e Observation ao histórico, e o histórico inteiro é reenviado a cada passo. Uma Observation com 200 processos consome contexto que faria falta adiante — mais um motivo para o LIMIT na consulta.
A Action vem malformada. O modelo escreve um JSON inválido, ou inventa o nome de uma Tool que não existe, ou erra o nome de um argumento. Modelos menores erram mais. O framework normalmente devolve o erro como Observation e deixa o modelo tentar de novo — o que funciona surpreendentemente bem, e é uma boa razão para suas mensagens de erro serem informativas.
O modelo pula a Tool. Ele decide que já sabe a resposta e responde direto, sem consultar nada. Aqui está o modo de falha mais perigoso para o nosso uso: a resposta vem bem formatada, com números de processo plausíveis, e inteiramente inventada.
Esse último caso é o motivo de um hábito que vale adotar desde o primeiro exercício: olhe os passos intermediários, não apenas a resposta final.
Se a resposta traz processos mas o log não mostra nenhuma chamada a buscar_processos, os processos não existem. O smolagents imprime o ciclo inteiro por padrão — leia.
Na prática institucional, isso vira uma regra simples: um resultado que não corresponde a uma consulta registrada não é um resultado.
Fixando o capítulo
Q1: Quem executa a Tool?
Q2: A consulta do exemplo omitiu o processo 0000121-49.2025.8.19.0001, mais recente que um dos listados, porque ele é de Sigilo Absoluto. Por que a resposta final não avisa que houve omissão?
Q3: Para que serve o max_steps?
Agora vamos abrir cada etapa do ciclo separadamente, começando pelo Thought.