Seu primeiro Agent com smolagents
Agora vamos construir o mesmo Alfred judiciário — mesmas Tools, mesma base, mesmo teto de sigilo — usando o smolagents. E vamos comparar linha a linha com a versão do capítulo anterior.
O curso original da Hugging Face faz este capítulo em um Space no Hub, com HF_TOKEN e modelo servido pela API deles. Aqui não: tudo roda na sua máquina, contra o Ollama em 127.0.0.1. Nenhum dado sai do computador — o que, no nosso contexto, não é preferência, é requisito.
Instalação
Com o ambiente virtual ativado (veja a Unidade 0):
pip install "smolagents[litellm]"
O extra [litellm] traz o LiteLLM, que é a camada que fala com o Ollama. O smolagents sozinho assume os modelos da Hugging Face.
Confira a versão — o curso foi escrito e testado com a 1.26.0:
python -c "import smolagents; print(smolagents.__version__)"
O arquivo inteiro
Está em unidade1/exemplos/alfred_smolagents.py. Vamos pelas partes que mudaram.
As Tools ganham um decorador
from smolagents import CodeAgent, LiteLLMModel, tool
@tool
def consultar_assunto(termo: str) -> int:
"""Converte o nome de um assunto judicial no código numérico correspondente.
Args:
termo: nome (ou parte do nome) do assunto, por exemplo "violência doméstica".
"""
É o mesmo @tool que você escreveu do zero com o módulo inspect, no capítulo de Tools. O smolagents lê a assinatura e a docstring e monta o bloco de texto que vai para a system message. Nada de novo — só automatizado.
Duas exigências que o framework impõe, e que valem a pena:
- Anotações de tipo em tudo — parâmetros e retorno.
- Docstring com seção
Args:, uma linha por parâmetro. Sem isso osmolagentslevanta erro na hora de registrar a Tool.
Isso é mais rígido que a versão manual, e é rigidez bem colocada: uma Tool mal descrita é uma Tool que o modelo vai usar errado.
O corpo das Tools não mudou nada
WHERE a.codigo = ?
AND p.id_nivel_sigilo <= ?
(int(codigo_assunto), TETO_SIGILO, limite),
Ponto que merece ser dito em voz alta: trocar de framework não mexeu no controle de acesso. O TETO_SIGILO continua sendo uma constante do seu programa, fora do alcance do modelo, aplicada dentro da consulta. Se a proteção dependesse de algo que o framework oferece, ela dependeria do framework — e a decisão certa é ela não depender de nada além do banco.
O modelo
modelo = LiteLLMModel(
model_id="ollama_chat/qwen2:7b",
api_base="http://127.0.0.1:11434",
num_ctx=8192,
temperature=0,
)
O prefixo ollama_chat/ diz ao LiteLLM para usar o endpoint de chat do Ollama. O api_base aponta para a sua máquina.
Confira o api_base sempre. É a linha que decide se o texto da consulta — que pode conter nome de assunto, número de processo, o que você mandar — fica no seu computador ou vai para um servidor de terceiro.
Trocar LiteLLMModel por InferenceClientModel, como faz o curso original, muda exatamente isso. Não faça essa troca com dados do Tribunal.
O Agent
agente = CodeAgent(
tools=[consultar_assunto, buscar_processos],
model=modelo,
max_steps=6,
additional_authorized_imports=[], # nenhum import além do básico
)
Compare com o capítulo anterior. Desapareceram: o system prompt do ciclo, a lista de stop sequences, o extrair_action, o laço for, a montagem das mensagens, o tratamento de Tool inexistente. Tudo isso o CodeAgent faz.
Ficaram: as Tools, o modelo, e o max_steps — que é o mesmo MAX_PASSOS = 6.
O additional_authorized_imports=[] é a lista de módulos que o código gerado pode importar. Vazia, por padrão. Deixe vazia enquanto não houver motivo concreto para o contrário: é o que impede o código gerado pelo modelo de abrir arquivos, rede ou processos.
Rodando
resposta = agente.run(
"Traga os 3 processos mais recentes sobre violência doméstica. "
"Use somente os dados devolvidos pelas Tools."
)
python exemplos\alfred_smolagents.py
A primeira execução — que falhou
Vale mostrar como foi de verdade, porque a falha ensina mais que o acerto.
Na primeira versão do arquivo, consultar_assunto devolvia uma string legível para humano — "codigo=1001 (Violência Doméstica Contra a Mulher)" — igual à versão sem framework. Resultado:
━━━━━━━━━━━━━━━━━━ Step 1 ━━━━━━━━━━━━━━━━━━
─ Executing parsed code: ────────────────────
codigo_assunto = consultar_assunto("violência doméstica")
print(f"Código do assunto violência doméstica: {codigo_assunto}")
processos_recentes = buscar_processos(codigo_assunto, limite=3)
print(processos_recentes)
─────────────────────────────────────────────
Execution logs:
Código do assunto violência doméstica: codigo=1001 (Violência Doméstica Contra a Mulher)
Code execution failed at line 'processos_recentes = buscar_processos(codigo_assunto, limite=3)'
due to: ValueError: invalid literal for int() with base 10:
'codigo=1001 (Violência Doméstica Contra a Mulher)'
[Step 1: Duration 186.68 seconds| Input tokens: 2,197 | Output tokens: 106]
Repare em duas coisas de uma vez.
Primeira: o Code Agent encadeou as duas Tools em um bloco só. No capítulo anterior isso levou duas voltas do ciclo — Action, Observation, Action, Observation. Aqui, uma. É a vantagem que o capítulo de Actions prometia, aparecendo na prática.
Segunda: o erro voltou ao modelo, e ele se corrigiu. No passo seguinte:
━━━━━━━━━━━━━━━━━━ Step 2 ━━━━━━━━━━━━━━━━━━
─ Executing parsed code: ────────────────────
codigo_assunto = consultar_assunto("violência doméstica")
# Extracting the code from the string
codigo = int(codigo_assunto.split('=')[1].strip().split(' ')[0])
print(f"Código extraído: {codigo}")
processos_recentes = buscar_processos(codigo, limite=3)
print(processos_recentes)
─────────────────────────────────────────────
Execution logs:
Código extraído: 1001
[{"numero_processo": "0000184-40.2026.8.19.0001", ...}]
Ele leu o ValueError, entendeu que o retorno era uma string com o código embutido, escreveu o split para extrair e seguiu. É o ReAct funcionando como anunciado — a Observation de erro produziu correção de rumo, sem intervenção humana.
Custou 186 segundos de passo desperdiçado. Nada mal para uma recuperação automática, e caro demais para deixar como está.
O que a falha ensinou
O problema não foi do modelo. Foi da Tool.
Em um Code Agent, o tipo de retorno da Tool é contrato de API. O modelo vai escrever código que consome esse retorno, e uma string formatada para leitura humana quebra na primeira composição. Na versão sem framework isso não aparecia, porque lá o retorno ia direto para o texto da Observation e o próprio modelo lia o 1001 de dentro dela.
A correção:
@tool
def consultar_assunto(termo: str) -> int:
...
if linha is None:
con = sqlite3.connect(BASE)
nomes = [r[0] for r in con.execute("SELECT nome FROM dim_assunto")]
con.close()
# A exceção vira Observation e volta ao modelo, sem sujar o tipo de retorno.
raise ValueError(
"Assunto nao encontrado. Assuntos disponiveis: " + "; ".join(nomes)
)
# Devolve o código puro: é isso que buscar_processos espera receber.
return linha[0]
Repare que a mensagem de erro informativa não se perdeu. Ela virou exceção — e o smolagents entrega a exceção ao modelo como Observation, exatamente como fazíamos à mão. O que se ganhou foi o tipo de retorno limpo.
Duas regras que valem para todas as Tools que você vier a escrever:
- Devolva o dado, não a frase.
1001, não"codigo=1001 (...)". - Erros pelo mecanismo de erro. Levante exceção com mensagem útil; não devolva sentinelas como
-1nem texto de erro misturado com o tipo normal.
A execução depois da correção
┌───────────────────────── New run ─────────────────────────┐
│ Traga os 3 processos mais recentes sobre violência │
│ doméstica. Use somente os dados devolvidos pelas Tools. │
└─ LiteLLMModel - ollama_chat/qwen2:7b ─────────────────────┘
━━━━━━━━━━━━━━━━━━ Step 1 ━━━━━━━━━━━━━━━━━━
─ Executing parsed code: ────────────────────
codigo_assunto = consultar_assunto('violência doméstica')
print(f"Código do assunto violência doméstica: {codigo_assunto}")
processos_recentes = buscar_processos(codigo_assunto, limite=3)
print(f"Processos mais recentes sobre violência doméstica: {processos_recentes}")
─────────────────────────────────────────────
Execution logs:
Código do assunto violência doméstica: 1001
Processos mais recentes sobre violência doméstica: [{"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", "classe": "Medida Protetiva de Urgência"}, {"numero_processo":
"0000031-41.2025.8.19.0001", ...}, {"numero_processo":
"0000116-27.2025.8.19.0001", ...}]
[Step 1: Duration 91.62 seconds| Input tokens: 2,197 | Output tokens: 121]
━━━━━━━━━━━━━━━━━━ Step 2 ━━━━━━━━━━━━━━━━━━
─ Executing parsed code: ────────────────────
final_answer(processos_recentes)
─────────────────────────────────────────────
Final answer: [{"numero_processo": "0000184-40.2026.8.19.0001", ...}]
[Step 2: Duration 151.16 seconds| Input tokens: 4,983 | Output tokens: 161]
Dois passos. Um para consultar, um para responder — contra os três da versão sem framework, porque as duas Tools couberam no mesmo bloco de código.
E os processos são os mesmos três: 0000184-40.2026.8.19.0001, 0000031-41.2025.8.19.0001 e 0000116-27.2025.8.19.0001. Com a mesma ausência, pelo mesmo motivo: o 0000121-49.2025.8.19.0001, de 27/07/2025, continua fora porque é Sigilo Absoluto e o teto da sessão é 1.
O framework mudou o encanamento. Não mudou nem podia mudar quem decide o que pode ser lido.
Comparando as duas versões
| Sem framework | Com smolagents |
|
|---|---|---|
| Linhas de código | 244 | 128 |
| Formato da Action | JSON, parser escrito à mão | Código Python, parser do framework |
| Passos para responder | 3 | 2 |
| System prompt do ciclo | Escrito por você | Gerado pelo framework |
| Parada | "stop": ["Observation:"] explícito |
Cuidada pelo framework |
| Tratamento de erro | try/except no laço |
Exceção vira Observation |
| Execução de código | Não existe | Interpretador restrito |
max_steps |
MAX_PASSOS = 6 |
max_steps=6 |
| Teto de sigilo | Constante no WHERE |
Constante no WHERE |
A última linha é a que resume o capítulo.
O final_answer
Você viu no Step 2:
final_answer(processos_recentes)
O smolagents injeta essa função no ambiente de execução. Chamá-la é como o Agent encerra o laço — o equivalente ao if "Final Answer:" in saida do capítulo anterior.
Repare que o modelo chamou final_answer passando a variável, não um texto reescrito por ele. É o melhor caso possível: a resposta é literalmente o que a consulta devolveu, sem intermediação.
Na versão sem framework, o modelo reescrevia a lista em português — e foi justamente aí que, no experimento dos 500 processos, ele afirmou uma contagem que não existia. Passar o dado adiante em vez de narrá-lo é menos bonito e mais seguro.
Exercícios
- Mude a pergunta.
agente.run("Quantos processos de tráfico de drogas existem na base?")— repare que não existe Tool de contagem. Acompanhe o que ele faz com isso. - Peça um assunto inexistente e veja a exceção virar Observation, com a lista de assuntos disponíveis chegando ao modelo.
- Mude
TETO_SIGILOpara 0 e confirme que o resultado é vazio, igual à versão sem framework. - Autorize um import —
additional_authorized_imports=["datetime"]— e peça algo que dependa da data de hoje. Depois volte a lista para vazia e entenda por que o padrão é esse.
Fixando o capítulo
Q1: Por que consultar_assunto devolvendo \"codigo=1001 (Violência Doméstica Contra a Mulher)\" quebrou o Code Agent?
Q2: A correção trocou o retorno para int e passou a levantar ValueError quando o assunto não existe. O que se ganhou?
Q3: Ao trocar o laço manual pelo CodeAgent, o que não mudou?
Você tem dois Agents funcionando, e sabe o que cada linha do framework está escondendo. Hora do quiz final da unidade.