Alfred no CNJ
Chegou a hora de juntar tudo. As duas Tools do capítulo anterior entram num CodeAgent, e o Alfred recebe o pedido que motivou este curso:
Traga os 3 processos mais recentes sobre violência doméstica.
Este capítulo mostra quatro execuções desse pedido. Três delas deram errado, cada uma de um jeito diferente.
Elas estão aqui na íntegra, e não por honestidade decorativa: as falhas são o conteúdo do capítulo. Cada linha da versão final existe por causa de uma delas, e um curso que mostrasse só o resultado bom ensinaria a escrever a versão que quebra.
O agente
import sys
from smolagents import CodeAgent, LiteLLMModel, tool
import tools_cnj
sys.stdout.reconfigure(encoding="utf-8")
@tool
def listar_assuntos(termo: str) -> list:
"""Lista os nomes exatos de assunto do TJRJ que contêm o termo procurado.
Args:
termo: parte do nome do assunto, por exemplo "violência doméstica".
"""
return tools_cnj.listar_assuntos(termo)
@tool
def buscar_processos(assunto: str, quantidade: int = 5) -> list:
"""Busca no CNJ os processos mais recentes de um assunto.
Devolve uma lista de dicionários, um por processo, cada um com as chaves
assunto, numero_processo, data_ajuizamento, grau, classe e orgao_julgador.
Args:
assunto: nome EXATO do assunto, tal como devolvido por listar_assuntos.
quantidade: quantos processos devolver (máximo 20).
"""
return tools_cnj.buscar_processos(assunto, quantidade)
# O modelo continua na sua máquina. O que saiu daqui foi a consulta ao CNJ — {: #o-modelo-continua-na-sua-máquina-o-que-saiu-daqui-foi-a-consulta-ao-cnj }
# um filtro por assunto — e não o conteúdo de nenhum processo. {: #um-filtro-por-assunto-e-não-o-conteúdo-de-nenhum-processo }
modelo = LiteLLMModel(
model_id="ollama_chat/qwen2:7b",
api_base="http://127.0.0.1:11434",
num_ctx=8192,
temperature=0,
)
agente = CodeAgent(
tools=[listar_assuntos, buscar_processos],
model=modelo,
max_steps=6,
additional_authorized_imports=[],
)
if __name__ == "__main__":
resposta = agente.run(
"Traga os 3 processos mais recentes sobre violência doméstica. "
"Use somente os dados devolvidos pelas Tools."
)
print("\n" + "=" * 70)
print("RESPOSTA FINAL:")
print(resposta)
Compare com o alfred_smolagents.py da Unidade 1. Nada mudou na estrutura. Mesmo LiteLLMModel, mesmo api_base, mesmo num_ctx, mesmo temperature=0, mesmo max_steps, mesmo additional_authorized_imports=[].
As Tools são invólucros de três linhas que delegam para tools_cnj. O @tool continua lendo a docstring para montar a descrição que o modelo vê. E additional_authorized_imports=[] continua significando que o código gerado não pode importar nada — nem requests, nem os, nem json. Quem fala com a rede é a Tool; o modelo só a chama.
Vale reparar no que não está neste arquivo: nenhuma chave, nenhuma URL, nenhum JSON de consulta.
Tudo isso ficou em cnj.py e tools_cnj.py. O agente não sabe que existe Elasticsearch, e nem precisa. Essa separação é o que vai permitir, num outro dia, trocar o CNJ pelo banco do Tribunal sem tocar em uma linha deste arquivo.
Rode com:
python exemplos\alfred_cnj.py
Primeira execução: a string disfarçada de lista
Na primeira versão, listar_assuntos terminava assim:
return json.dumps(achados[:20], ensure_ascii=False)
Parecia sensato. Tool devolve texto, texto entra no contexto, modelo lê. É exatamente o que se faz num agente sem framework — na Unidade 1, o agente_do_zero.py fazia isso e estava certo.
O modelo escreveu o seguinte no Step 1:
# Buscar os nomes exatos dos assuntos do TJRJ que contêm o termo "violência doméstica" {: #buscar-os-nomes-exatos-dos-assuntos-do-tjrj-que-contêm-o-termo-violência-doméstica }
violencia_domestica_assuntos = listar_assuntos('violência doméstica')
# Imprimir os nomes dos assuntos para conferência {: #imprimir-os-nomes-dos-assuntos-para-conferência }
print(violencia_domestica_assuntos)
# Buscar os 3 processos mais recentes para cada assunto {: #buscar-os-3-processos-mais-recentes-para-cada-assunto }
processos_recentes = []
for assunto in violencia_domestica_assuntos:
processos = buscar_processos(assunto, quantidade=3)
processos_recentes.extend(processos)
# Imprimir os processos mais recentes para conferência {: #imprimir-os-processos-mais-recentes-para-conferência }
print(processos_recentes)
Leia esse código com calma. Ele está certo. É o que qualquer pessoa escreveria: pega os nomes de assunto, percorre, busca os processos de cada um, junta tudo. O raciocínio do modelo não tem defeito nenhum.
Agora o que apareceu na tela:
Execution logs:
["Decorrente de Violência Doméstica", "Violência Doméstica Contra a Mulher"]
['{', '"', 'a', 's', 's', 'u', 'n', 't', 'o', '"', ':', ' ', '"', '[', '"',
',', ' ', '"', 'e', 'n', 'c', 'o', 'n', 't', 'r', 'a', 'd', 'o', 's', '"', ':',
' ', '0', ',', ' ', '"', 'p', 'r', 'o', 'c', 'e', 's', 's', 'o', 's', '"', ':',
' ', '[', ']', '}', '{', '"', 'a', 's', 's', 'u', 'n', 't', 'o', '"', ':', ' ',
...
[Step 1: Duration 95.23 seconds| Input tokens: 2,193 | Output tokens: 301]
A primeira linha é o print do resultado da Tool, e parece perfeito: dois nomes de assunto entre colchetes.
Mas aquilo não era uma lista. Era uma string cujo primeiro caractere é [ e cujo último é ].
for assunto in "..." percorre caracteres. O laço executou buscar_processos('{'), buscar_processos('"'), buscar_processos('a'), buscar_processos('s') — cerca de 78 chamadas, cada uma delas uma requisição HTTP real à API do CNJ, cada uma perguntando por um assunto de um caractere e recebendo, corretamente, zero resultados.
E então o resultado de cada uma, que também era string, foi percorrido caractere a caractere pelo extend.
O que faz essa falha ser pior do que um erro
Nenhuma exceção foi levantada. O programa rodou 95 segundos, o tempo normal de um passo, e produziu uma saída.
Compare com o que aconteceu na Unidade 1, quando o modelo mandou limite="500" em vez de limite=500: aquilo estourou um int(), o erro voltou como Observation, e o modelo corrigiu no passo seguinte. Erro alto é erro barato.
Aqui não houve nada para corrigir, porque do ponto de vista do Python nada deu errado. Iterar uma string é uma operação perfeitamente válida. O resultado é que estava sem sentido.
Esta é a diferença central entre um Code Agent e um agente que só troca texto.
Num agente do zero, a Tool devolve texto porque tudo é texto — a saída vai ser colada no prompt e lida pelo modelo.
Num Code Agent, o modelo escreve Python de verdade, que roda de verdade, sobre o objeto que a Tool devolveu. Uma string que se parece com uma lista passa por lista no print, engana quem lê o log, e se comporta como string em todo o resto.
Num Code Agent, a Tool devolve objeto Python. Lista, dicionário, número — nunca a serialização deles.
Um detalhe que não é detalhe
Aquelas 78 requisições foram para um servidor do CNJ.
A execução foi interrompida assim que ficou claro o que estava acontecendo. Vale registrar por quê: é uma API pública, gratuita, mantida com dinheiro do orçamento, e um laço de agente é capaz de gerar dezenas de chamadas por segundo sem que ninguém perceba.
Daí a guarda que existe na Tool desde então:
if assunto not in _catalogo_de_assuntos():
raise ValueError(...)
O catálogo já está em memória. Um assunto que o índice não conhece agora custa uma exceção local, e não uma viagem até Brasília. A terceira execução, mais adiante, mostra essa guarda funcionando na prática.
Segunda execução: o envelope que não compõe
Tipos corrigidos: listar_assuntos -> list, buscar_processos -> dict. O dicionário era o envelope que a Unidade 1 tinha ensinado a construir:
{"assunto": ..., "quantidade_pedida": 3, "encontrados": 3, "processos": [...]}
Step 1, e o modelo escreve praticamente o mesmo código de antes:
violencia_domestica_assuntos = listar_assuntos('violência doméstica')
processos = []
for assunto in violencia_domestica_assuntos:
processos_assunto = buscar_processos(assunto, quantidade=3)
processos.extend(processos_assunto)
for processo in processos[:3]:
print(processo)
Saída:
Execution logs:
assunto
quantidade_pedida
encontrados
Out: None
[Step 1: Duration 119.25 seconds| Input tokens: 2,193 | Output tokens: 337]
Três palavras. São as chaves do dicionário.
O laço externo agora funciona — listar_assuntos devolve uma lista de verdade, e os dois assuntos foram consultados corretamente. O problema mudou de lugar: processos.extend(processos_assunto) recebe um dicionário, e extend sobre um dicionário adiciona as chaves dele.
Os processos estavam lá dentro, em processos_assunto["processos"]. Foram descartados por uma linha de código correta.
De novo: nenhuma exceção.
O modelo tenta se diagnosticar
O Step 2 é interessante por si só. Vendo Out: None e três palavras soltas, o modelo acrescentou um print de diagnóstico — sem que ninguém pedisse:
for assunto in violencia_domestica_assuntos:
processos_assunto = buscar_processos(assunto, quantidade=3)
print(f"Processos para o assunto {assunto}: {processos_assunto}")
processos.extend(processos_assunto)
E aí os dados apareceram na tela, corretos, completos, com números que existem:
Processos para o assunto Decorrente de Violência Doméstica: {'assunto':
'Decorrente de Violência Doméstica', 'quantidade_pedida': 3, 'encontrados': 3,
'processos': [{'numero_processo': '0007972-81.2026.8.19.0203', ...
O dado estava visível na Observation. E o modelo, mesmo assim, repetiu exatamente o mesmo código no Step 3, e o Step 3 terminou exatamente como o Step 1: assunto, quantidade_pedida, encontrados, Out: None.
A execução foi interrompida no Step 4. O padrão já estava claro, e cada passo custava duas requisições ao CNJ.
Esse é o comportamento que mais surpreende quem está começando: o modelo não reconhece o próprio bug.
Ele viu a saída errada, tentou instrumentar, viu os dados corretos impressos na tela — e escreveu de novo a mesma linha que os jogava fora. Não é falta de informação. É que "esse extend está recebendo um dict" é um raciocínio sobre tipos, e não sobre o texto que ele está lendo.
Por isso max_steps existe. Não é um limite de custo: é o que impede um agente de repetir uma volta inútil até o fim dos tempos. Um agente que não converge em 6 passos raramente converge em 20.
A lição, na forma final
A primeira falha ensinou "devolva objeto, não string". A segunda mostrou que isso não basta:
O retorno de uma Tool tem que ter a forma que o chamador vai compor.
Um agente quase nunca chama uma Tool uma vez só. Ele chama num laço, por item, e junta os resultados. Listas se juntam. Envelopes não — e a junção errada não levanta exceção, devolve outra coisa.
O envelope resolvia um problema real (a contagem explícita, herdada do episódio dos "500 processos" da Unidade 1) e criava outro, pior, porque silencioso. A troca:
def buscar_processos(assunto: str, quantidade: int = 5) -> list:
...
return processos # lista de dicionários, um por processo
com cada processo carregando o próprio assunto, para que a proveniência sobreviva à concatenação — que era justamente o que o envelope guardava do lado de fora.
Terceira execução: os dados certos, e o agente não para
Mesma pergunta, mesmo modelo, mesmo agente. Só o contrato das Tools mudou.
Step 1 — a guarda dispara
processos = buscar_processos(assunto="violência doméstica", quantidade=3)
print(processos)
O modelo pulou a etapa de resolver o nome e mandou o termo do usuário direto. O que aconteceu:
Code execution failed at line 'processos = buscar_processos(assunto="violência
doméstica", quantidade=3)' due to: ValueError: 'violência doméstica' não é um
nome de assunto do TJRJ. Use listar_assuntos() para obter os nomes exatos.
[Step 1: Duration 48.85 seconds| Input tokens: 2,239 | Output tokens: 83]
É a guarda do capítulo anterior fazendo exatamente o que foi escrita para fazer: nenhuma requisição saiu da máquina. O catálogo já estava em memória, a comparação foi local, e o erro custou microssegundos em vez de uma viagem até a API.
Repare também na mensagem. Ela não diz "argumento inválido": diz qual é o problema e qual Tool resolve. Foi escrita para ser lida por um modelo, e o Step 2 mostra que funcionou.
Step 2 — o modelo corrige o rumo
assuntos = listar_assuntos(termo="violência doméstica")
print(assuntos)
Execution logs:
['Decorrente de Violência Doméstica', 'Violência Doméstica Contra a Mulher']
Out: None
[Step 2: Duration 21.44 seconds| Input tokens: 4,731 | Output tokens: 162]
Vinte e um segundos — o passo mais rápido de todas as execuções. O modelo leu o erro, entendeu a instrução contida nele e chamou a Tool certa.
Esse é o ciclo funcionando como deve: Observation ruim vira Thought corrigido. E é a razão de a Unidade 1 insistir tanto que mensagem de erro é interface, não desabafo.
Step 3 — os dados
processos = buscar_processos(assunto="Decorrente de Violência Doméstica", quantidade=3)
print(processos)
Execution logs:
[{'assunto': 'Decorrente de Violência Doméstica', 'numero_processo':
'0007972-81.2026.8.19.0203', 'data_ajuizamento': '22/07/2026', 'grau': 'G1',
'classe': 'Pedido de Prisão Preventiva', 'orgao_julgador': 'JACAREPAGUA
REGIONAL III J VIO DOM FAM'}, {'assunto': 'Decorrente de Violência Doméstica',
'numero_processo': '0001381-92.2026.8.19.0045', 'data_ajuizamento':
'21/07/2026', 'grau': 'G1', 'classe': 'Medidas Protetivas de urgência (Lei
Maria da Penha) Criminal', 'orgao_julgador': 'RESENDE J VIO DOM FAM C/MULH E
ESP ADJ CRIM'}, {'assunto': 'Decorrente de Violência Doméstica',
'numero_processo': '0009356-73.2026.8.19.0205', 'data_ajuizamento':
'21/07/2026', 'grau': 'JE', 'classe': 'Termo Circunstanciado',
'orgao_julgador': 'CAMPO GRANDE REGIONAL XVIII JUI ESP CRIM'}]
[Step 3: Duration 28.67 seconds| Input tokens: 7,404 | Output tokens: 261]
Uma lista de três dicionários. Nenhuma chave solta, nenhum caractere avulso.
Três processos de violência doméstica ajuizados em julho de 2026: um pedido de prisão preventiva no juizado de Jacarepaguá, medidas protetivas da Lei Maria da Penha em Resende, um termo circunstanciado em Campo Grande. Números que existem, órgãos que existem.
O pedido do usuário está atendido. E o agente não parou.
Steps 4, 5 e 6 — o agente não sabe que terminou
# Step 4 {: #step-4 }
processos = buscar_processos(assunto="0007972-81.2026.8.19.0203", quantidade=2)
ValueError: '0007972-81.2026.8.19.0203' não é um nome de assunto do TJRJ.
Use listar_assuntos() para obter os nomes exatos.
[Step 4: Duration 45.51 seconds| Input tokens: 10,606 | Output tokens: 358]
O modelo pegou o número do primeiro processo e o passou como se fosse um assunto. A guarda barrou de novo — outra requisição que não saiu.
O Step 5 repetiu, literalmente, o código do Step 3. O Step 6 repetiu, literalmente, o código do Step 4. O max_steps=6 encerrou o laço.
O que faltou tem nome: final_answer. O CodeAgent só considera a tarefa concluída quando o código gerado chama essa função. O qwen2:7b obteve o dado no Step 3 e simplesmente não chamou — continuou procurando mais alguma coisa para fazer.
Não é um bug do smolagents nem uma falha das Tools. É uma limitação conhecida de modelos pequenos: reconhecer que a tarefa acabou é um julgamento, e julgamento é justamente o que um modelo de 7 bilhões de parâmetros faz pior.
Como o run terminou
Estourado o max_steps, o smolagents faz uma última chamada ao modelo pedindo que ele responda com o que tem. E a resposta veio certa:
Reached max steps.
[Step 7: Duration 175.80 seconds| Input tokens: 20,433 | Output tokens: 819]
======================================================================
RESPOSTA FINAL:
Based on the information provided by the tools, here are the three most recent
processes related to domestic violence:
1. Process number: 0007972-81.2026.8.19.0203
- Date of filing: 22/07/2026
- Court: JACAREPAGUA REGIONAL III J VIO DOM FAM
- Class: Pedido de Prisão Preventiva
...
Os três processos corretos, com os dados corretos. Em inglês.
Duas coisas para guardar dessa saída.
A primeira: o modelo respondeu em inglês uma pergunta feita em português, com dados em português. É comportamento comum em modelos pequenos — o inglês domina o treinamento, e o system prompt do smolagents é em inglês. Não se conserta com jeitinho; conserta-se pedindo.
A segunda: aquele passo final custou 175 segundos, o mais caro de toda a execução, com 20.433 tokens de entrada. É a conta do contexto acumulado em seis passos, sendo que a informação necessária já estava disponível no terceiro. Um agente que não sabe parar é caro exatamente onde deveria ser barato.
O que resolve
Uma frase no pedido:
resposta = agente.run(
"Traga os 3 processos mais recentes sobre violência doméstica. "
"Use somente os dados devolvidos pelas Tools. "
# Sem esta última frase o qwen2:7b obtém os dados e continua andando:
# ele não chama final_answer sozinho, e o run termina por max_steps.
"Assim que tiver os processos, chame final_answer com eles. "
# O qwen2:7b responde em inglês por padrão, mesmo perguntado em
# português. Pedir o idioma é barato; descobrir depois, não.
"Responda em português."
)
Repare no que essa frase é e no que ela não é.
A Unidade 1 estabeleceu que instrução em prompt não é controle — e continua valendo: nada aqui garante que o modelo vá chamar final_answer.
A diferença é o que acontece quando a instrução é ignorada. Se o modelo desobedecer ao teto de sigilo, alguém vê processo protegido. Se desobedecer a esta frase, o agente dá voltas e o max_steps o encerra.
Prompt para orientar; código para garantir. Uma parada limpa é orientação — o max_steps é a garantia atrás dela. Por isso ele é 6, e não 50.
Quarta execução: o agente que para sozinho
Duas frases a mais no pedido. Nada mudou nas Tools, no modelo, no max_steps. Esta é a execução inteira:
┌────────────────────────────── New run ──────────────────────────────┐
│ Traga os 3 processos mais recentes sobre violência doméstica. Use │
│ somente os dados devolvidos pelas Tools. Assim que tiver os │
│ processos, chame final_answer com eles. Responda em português. │
└─ LiteLLMModel - ollama_chat/qwen2:7b ───────────────────────────────┘
━━━━━━━━━━━━━━━━━━━━━━━ Step 1 ━━━━━━━━━━━━━━━━━━━━━━━
assunto = 'violência doméstica'
processos = buscar_processos(assunto, quantidade=3)
print(processos)
Code execution failed at line 'processos = buscar_processos(assunto,
quantidade=3)' due to: ValueError: 'violência doméstica' não é um nome de
assunto do TJRJ. Use listar_assuntos() para obter os nomes exatos.
[Step 1: Duration 23.42 seconds| Input tokens: 2,263 | Output tokens: 92]
━━━━━━━━━━━━━━━━━━━━━━━ Step 2 ━━━━━━━━━━━━━━━━━━━━━━━
assuntos = listar_assuntos('violência doméstica')
print(assuntos)
Execution logs:
['Decorrente de Violência Doméstica', 'Violência Doméstica Contra a Mulher']
[Step 2: Duration 29.01 seconds| Input tokens: 4,787 | Output tokens: 182]
━━━━━━━━━━━━━━━━━━━━━━━ Step 3 ━━━━━━━━━━━━━━━━━━━━━━━
assunto1 = 'Decorrente de Violência Doméstica'
assunto2 = 'Violência Doméstica Contra a Mulher'
processos1 = buscar_processos(assunto1, quantidade=3)
processos2 = buscar_processos(assunto2, quantidade=3)
print(processos1)
print(processos2)
Execution logs:
[{'assunto': 'Decorrente de Violência Doméstica', 'numero_processo':
'0007972-81.2026.8.19.0203', ...}, ...]
[{'assunto': 'Violência Doméstica Contra a Mulher', 'numero_processo':
'0802320-05.2026.8.19.0024', ...}, ...]
[Step 3: Duration 52.02 seconds| Input tokens: 7,501 | Output tokens: 314]
━━━━━━━━━━━━━━━━━━━━━━━ Step 4 ━━━━━━━━━━━━━━━━━━━━━━━
final_answer(processos1 + processos2)
Final answer: [ ... os seis processos ... ]
[Step 4: Duration 89.35 seconds| Input tokens: 11,154 | Output tokens: 366]
Quatro passos. Nenhuma requisição desperdiçada. Fim por final_answer, e não por max_steps.
E a resposta, na íntegra:
======================================================================
RESPOSTA FINAL:
[{'assunto': 'Decorrente de Violência Doméstica',
'numero_processo': '0007972-81.2026.8.19.0203',
'data_ajuizamento': '22/07/2026', 'grau': 'G1',
'classe': 'Pedido de Prisão Preventiva',
'orgao_julgador': 'JACAREPAGUA REGIONAL III J VIO DOM FAM'},
{'assunto': 'Decorrente de Violência Doméstica',
'numero_processo': '0001381-92.2026.8.19.0045',
'data_ajuizamento': '21/07/2026', 'grau': 'G1',
'classe': 'Medidas Protetivas de urgência (Lei Maria da Penha) Criminal',
'orgao_julgador': 'RESENDE J VIO DOM FAM C/MULH E ESP ADJ CRIM'},
{'assunto': 'Decorrente de Violência Doméstica',
'numero_processo': '0009356-73.2026.8.19.0205',
'data_ajuizamento': '21/07/2026', 'grau': 'JE',
'classe': 'Termo Circunstanciado',
'orgao_julgador': 'CAMPO GRANDE REGIONAL XVIII JUI ESP CRIM'},
{'assunto': 'Violência Doméstica Contra a Mulher',
'numero_processo': '0802320-05.2026.8.19.0024',
'data_ajuizamento': '29/05/2026', 'grau': 'G1',
'classe': 'Carta Precatória Criminal',
'orgao_julgador': 'ITAGUAI VARA CRIMINAL'},
{'assunto': 'Violência Doméstica Contra a Mulher',
'numero_processo': '0007418-41.2024.8.19.0002',
'data_ajuizamento': '30/04/2026', 'grau': 'TR',
'classe': 'Apelação Criminal',
'orgao_julgador': 'CAPITAL 1 TURMA RECURSAL DOS JUI ESP CRIMINAL'},
{'assunto': 'Violência Doméstica Contra a Mulher',
'numero_processo': '0029098-20.2026.8.19.0000',
'data_ajuizamento': '30/04/2026', 'grau': 'G2',
'classe': 'Agravo Interno Cível',
'orgao_julgador': 'GAB DES SIMONE DE ARAUJO ROLIM'}]
Seis processos reais do TJRJ, cada um sabendo de qual assunto veio, todos abrindo no sistema do Tribunal. É o que se pediu ao Alfred no primeiro parágrafo desta unidade.
Três coisas nessa execução merecem atenção
O Step 1 continua errando — e isso é bom. O modelo tentou de novo passar 'violência doméstica' direto para buscar_processos. É o mesmo erro da terceira execução, e ele não some com prompt melhor: o modelo não tem como saber que o catálogo do TJRJ tem nomes canônicos. Quem sabe é a Tool, e ela avisa antes da rede — zero requisições ao CNJ nesse passo. A guarda escrita no capítulo anterior é o que transforma um erro previsível numa linha de log de 23 segundos.
O final_answer veio como código, não como texto. Repare no Step 4:
final_answer(processos1 + processos2)
O modelo não redigitou os seis processos: somou duas listas e entregou os objetos. É isso que a lição da Tool comprou. Com o envelope da segunda execução, esse mesmo passo daria TypeError — ou, pior, funcionaria com as chaves erradas.
A resposta veio em português porque não veio em prosa. Vale honestidade sobre o que a instrução "Responda em português" fez aqui. A resposta final é uma lista de dicionários, e os textos dentro dela vieram do CNJ — sempre estiveram em português. O que a instrução evitou foi o parágrafo de resumo em inglês que a terceira execução produziu. Efeito real, mas menor do que parece — e mais um argumento a favor de fazer a Tool devolver dado estruturado: dado não tem idioma.
Vale medir o preço do Step 4: 89 segundos e 11.154 tokens de entrada.
Compare com o passo final da terceira execução — 175 segundos e 20.433 tokens — que produziu uma resposta pior, em inglês, depois de seis passos.
Não é que o modelo tenha melhorado. É que ele teve menos histórico para reler. Parar cedo é otimização de desempenho, e a frase que faz o agente parar cedo custa doze palavras.
Compare as quatro execuções e o arco fica visível:
| Execução 1 | Execução 2 | Execução 3 | Execução 4 | |
|---|---|---|---|---|
Retorno de listar_assuntos |
str |
list |
list |
list |
Retorno de buscar_processos |
str |
dict (envelope) |
list |
list |
Pedido manda chamar final_answer |
não | não | não | sim |
| O que deu errado | laço sobre caracteres | extend sobre chaves |
não chamou final_answer |
nada |
| Houve exceção? | não | não | sim, e útil | sim, e útil |
| O dado correto apareceu? | nunca | no Step 2, e foi descartado | no Step 3 | no Step 3 |
| Requisições desperdiçadas | ~78 | 6 | 0 | 0 |
| Passos até terminar | 6 | interrompida no 4 | 6 | 4 |
| Como terminou | max_steps |
à mão | max_steps |
final_answer |
| Passo mais caro | 95s | — | 175s / 20.433 tokens | 89s / 11.154 tokens |
| Idioma da resposta | — | — | inglês | português |
Duas leituras dessa tabela.
A primeira é sobre tipo. As execuções 1 e 2 falham em silêncio, sem exceção nenhuma, e gastam mais de 80 requisições ao CNJ para não produzir nada. As execuções 3 e 4 têm exceção — no Step 1, sempre a mesma — e é justamente por isso que funcionam. Erro que aparece é barato; erro que não aparece vira Observation e contamina o passo seguinte.
A segunda é sobre parada. Entre a execução 3 e a 4 não mudou uma linha de código: mudou uma frase no pedido. O ganho foi de 6 passos para 4, de 175 para 89 segundos no passo mais caro, e de um resumo em inglês para os dados estruturados que se pediu.
Tool bem escrita deixa o agente mais rápido; pedido bem escrito o faz parar. Nenhum dos dois é enfeite — são as duas metades do desempenho.