Pular para o conteúdo

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.

Nota

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.

Atenção

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.

Atenção

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."
)
Atenção

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.

Nota

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.

Nota

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.