O que são Tools?

O que distingue um Agent de um chat é a capacidade de agir. E ele age através de Tools.
Nesta seção vamos ver o que é uma Tool, como projetá-la bem e como ela chega até o modelo. É o capítulo mais importante da unidade: a diferença entre um Agent que serve para alguma coisa e um que não serve está quase inteiramente aqui.
O que é uma Tool
Uma Tool é uma função entregue ao LLM, com um objetivo claro e delimitado.
Exemplos comuns em agentes genéricos:
| Tool | Para quê |
|---|---|
| Busca na web | Buscar informação atualizada na internet |
| Geração de imagem | Criar imagens a partir de descrição textual |
| Retrieval | Recuperar informação de uma base documental |
| Interface de API | Falar com um sistema externo |
E, no nosso caso:
| Tool | Para quê |
|---|---|
consultar_assunto |
Traduzir "violência doméstica" para o código de assunto |
buscar_processos |
Consultar a base e devolver os processos mais recentes daquele assunto |
detalhar_processo |
Trazer classe, vara, comarca e último movimento de um processo |
Uma boa Tool complementa o LLM: ela faz o que o modelo não faz bem.
Dois motivos práticos para isso importar:
- O modelo é ruim em coisas exatas. Aritmética, datas, contagem. Uma Tool de cálculo dá resultado melhor do que confiar na capacidade nativa dele.
- O modelo não sabe nada depois do treinamento. O conhecimento interno dele para em uma certa data. Um processo distribuído ontem não existe para ele — a não ser que uma Tool vá buscar.

Se você perguntar a um LLM sem Tool de busca qual a previsão do tempo hoje, ele não vai dizer que não sabe. Vai inventar uma previsão plausível. Vale reler essa frase substituindo "previsão do tempo" por "processos sobre violência doméstica".
Uma Tool precisa ter:
- Uma descrição textual do que a função faz.
- Um callable — algo executável.
- Argumentos com tipos declarados.
- (Opcional, mas recomendado) Saída com tipo declarado.
Como as Tools funcionam de verdade
Aqui está o ponto que costuma surpreender.
O LLM não executa nada. Ele não tem como. Ele recebe texto e produz texto — só isso, sem exceção.
Quando dizemos que "demos uma Tool ao Agent", o que fizemos foi:
- Descrever a Tool em texto, dentro da system message.
- Instruir o modelo a escrever, quando julgar necessário, um texto em um formato combinado que representa uma chamada.
- Fazer com que o programa em volta — o Agent — leia esse texto, reconheça a chamada, execute a função de verdade, e devolva o resultado como uma nova mensagem.
Ou seja: quando você pergunta "qual o tempo em Paris?", o modelo não busca nada. Ele gera o texto chamar_tempo("Paris"). Quem executa é o seu código Python. O resultado volta para a conversa, o modelo lê e redige a resposta final.
Essas etapas normalmente não são mostradas ao usuário. Da cadeira dele, parece que o modelo consultou o tempo. Na verdade, o modelo escreveu uma frase e um programa fez o trabalho.
Essa separação é a melhor notícia do curso, do ponto de vista institucional.
Todo o poder de execução está no seu código, não no modelo. O modelo propõe; o seu programa dispõe. Se a sua função buscar_processos filtra por nível de sigilo, não existe frase que o modelo possa gerar que contorne esse filtro — porque não é ele quem consulta o banco.
Isso é o oposto de dar SQL livre ao modelo, e é a razão de fazermos assim.
Como as Tools chegam ao modelo
Pela system message, em texto.

Para funcionar, a descrição precisa ser precisa em dois pontos:
- O que a Tool faz
- Exatamente que entradas ela espera
Por isso as descrições costumam usar formatos estruturados — JSON, assinaturas de função. Não é obrigatório: qualquer formato preciso e consistente serve. Mas ambiguidade aqui vira chamada errada lá na frente.
Você já viu isso, no capítulo anterior
Lembra do template do qwen2? Aquele bloco que passamos rápido:
{{- if .Tools }}
# Tools {: #tools }
You are provided with function signatures within <tools></tools> XML tags:
<tools>{{- range .Tools }}
{"type": "function", "function": {{ .Function }}}{{- end }}
</tools>
For each function call, return a json object with function name and arguments within <tool_call></tool_call> XML tags:
<tool_call>
{"name": <function-name>, "arguments": <args-json-object>}
</tool_call>
{{- end }}<|im_end|>
Aí está, na íntegra, o mecanismo inteiro:
- As Tools são enfiadas dentro da system message, como JSON entre tags
<tools>. - O modelo é instruído em inglês, em texto corrido, a responder com um JSON entre tags
<tool_call>. - E o template tem um papel
tool, que é como o resultado da execução volta para a conversa.
Nenhuma API secreta. Nenhum canal privilegiado. Instruções em linguagem natural, dentro do prompt. É por isso que a qualidade da sua descrição importa tanto: ela é literalmente o que o modelo lê antes de decidir.
Construindo uma Tool do zero
Vamos fazer a nossa primeira Tool sem framework nenhum, para não sobrar mágica. Ela converte o nome de um assunto no código correspondente — o passo 1 do plano do Alfred.
ASSUNTOS = {
"violência doméstica": 1001,
"tráfico de drogas": 1002,
"furto": 1003,
"execução fiscal": 2001,
"guarda de menor": 3001,
}
def consultar_assunto(termo: str) -> int:
"""Converte o nome de um assunto judicial no código numérico correspondente."""
return ASSUNTOS.get(termo.lower().strip(), -1)
É isso. Uma função comum.
Agora, o que o modelo precisa saber sobre ela? Nome, descrição, entradas, saída:
Tool Name: consultar_assunto, Description: Converte o nome de um assunto judicial no código numérico correspondente., Arguments: termo: str, Outputs: int
Essa string é tudo o que o LLM vai saber a respeito. Ele nunca vê o corpo da função.
Gerando a descrição automaticamente
Escrever essa string à mão para cada Tool é frágil — basta esquecer um argumento e o modelo passa a chamar errado. Mas repare: a informação já está toda no código.
- O nome:
consultar_assunto - A descrição: a docstring
- As entradas e seus tipos: os type hints
- O tipo de saída: a anotação de retorno
Dá para extrair isso com o módulo inspect. É o que um decorator @tool faz:
@tool
def consultar_assunto(termo: str) -> int:
"""Converte o nome de um assunto judicial no código numérico correspondente."""
return ASSUNTOS.get(termo.lower().strip(), -1)
print(consultar_assunto.to_string())
Rode o exemplo completo:
python exemplos\tool_do_zero.py
Saída:
Descrição que o LLM recebe no prompt:
Tool Name: consultar_assunto, Description: Converte o nome de um assunto judicial no código numérico correspondente., Arguments: termo: str, Outputs: int
Chamando a Tool como função normal:
consultar_assunto('violência doméstica') -> 1001
consultar_assunto('assunto inexistente') -> -1
Exatamente a mesma string que escrevemos à mão — só que agora ela não pode divergir do código, porque foi extraída dele.
Consequência que vale destacar: em uma Tool, a docstring e os type hints deixam de ser boa prática e viram parte do contrato.
Se a docstring estiver vaga, o modelo chama a Tool na hora errada. Se estiver errada, ele chama com os argumentos errados. Você não está escrevendo comentário para um colega — está escrevendo a especificação que a máquina vai ler.
Regra prática: a descrição deve dizer quando usar a Tool, não só o que ela faz.
Código do decorator (clique para abrir)
import inspect
def tool(func):
"""Decorator que transforma uma função comum em uma Tool."""
assinatura = inspect.signature(func)
argumentos = []
for parametro in assinatura.parameters.values():
anotacao = parametro.annotation
nome_tipo = getattr(anotacao, "__name__", str(anotacao))
argumentos.append((parametro.name, nome_tipo))
retorno = assinatura.return_annotation
if retorno is inspect.Signature.empty:
saida = "sem anotação de retorno"
else:
saida = getattr(retorno, "__name__", str(retorno))
return Tool(
nome=func.__name__,
descricao=(func.__doc__ or "Sem descrição.").strip().split("\n")[0],
func=func,
argumentos=argumentos,
saida=saida,
)
A classe `Tool` que ele instancia está inteira em `exemplos/tool_do_zero.py`. Ela guarda nome, descrição, função, argumentos e saída, expõe `to_string()` para gerar a descrição e implementa `__call__` para que a instância possa ser usada como a função original.
Na prática, você não vai escrever esse decorator: smolagents, LangGraph e LlamaIndex já trazem o deles. Mas agora você sabe o que eles fazem — e por que reclamam quando falta um type hint.
Projetando Tools para uso judicial
O curso original para por aqui. Nós temos três decisões a mais para tomar, e elas não são detalhe de implementação.
1. Uma Tool por intenção, não uma Tool genérica
É tentador escrever uma única Tool poderosa:
# NÃO faça isso {: #não-faça-isso }
@tool
def executar_sql(consulta: str) -> list:
"""Executa uma consulta SQL na base de processos."""
Isso entrega ao modelo tudo o que a credencial alcança, contorna qualquer regra de sigilo que você tenha escrito em outro lugar, e produz um comportamento que ninguém consegue auditar depois. Além disso, a consulta passa a ser um texto gerado por modelo — uma origem sobre a qual você tem ainda menos controle do que sobre entrada de usuário.
O caminho certo é o contrário: Tools estreitas, com parâmetros tipados, e o SQL escrito por você.
@tool
def buscar_processos(codigo_assunto: int, limite: int = 5) -> list:
"""Lista os processos mais recentes de um assunto, do mais novo para o mais antigo.
Use depois de obter o código com consultar_assunto."""
O modelo escolhe qual Tool chamar e com quais valores. Nunca a consulta.
2. O nível de acesso não é argumento
Nenhuma Tool deve receber do modelo o teto de sigilo. Esse valor vem da sessão de quem está operando, fora do alcance do LLM:
def buscar_processos(codigo_assunto: int, limite: int = 5) -> list:
teto = sessao.nivel_de_acesso # não vem do modelo
cursor.execute(
"SELECT ... WHERE a.codigo = ? AND p.id_nivel_sigilo <= ? LIMIT ?",
(codigo_assunto, teto, limite),
)
Repare também nos ?: valores entram como parâmetros, nunca concatenados na string.
3. Sempre um limite
O modelo pode pedir "todos os processos". Se a sua Tool obedecer, você trava a máquina e enche a janela de contexto com dados que ninguém vai ler. Toda consulta tem LIMIT, com um padrão razoável.
Esses três pontos, e o restante do que muda ao apontar para a base real do Tribunal, estão detalhados em dados/MAPEAMENTO.md, junto com o mapa de conceitos para preencher com os nomes das tabelas do seu sistema.
Model Context Protocol (MCP)
Para conhecimento: o MCP é um protocolo aberto que padroniza como aplicações oferecem Tools a LLMs. Ele traz integrações prontas, permite trocar de provedor de modelo sem reescrever as Tools, e define práticas de segurança.
Na prática, significa que uma Tool escrita para MCP funciona em qualquer framework que implemente o protocolo. Se interessar, a Hugging Face tem um curso gratuito de MCP.
Resumindo:
- O que é uma Tool: uma função Python comum, mais uma descrição textual precisa.
- Como ela chega ao modelo: por texto, dentro da system message.
- Quem executa: o seu código — nunca o modelo. Toda a capacidade de agir está do seu lado.
- Por que o projeto importa: a descrição é o contrato, e o formato da Tool é onde as regras de acesso são realmente aplicadas.
Antes de seguir, um quiz curto. Depois, vamos juntar tudo no ciclo do Agent: Thought → Action → Observation.