Objetivo da Aula
Ao concluir esta aula, você será capaz de:
Projetar schemas de tools com descrições que guiam o LLM corretamente
Implementar tools de negócio com validação de inputs e erros informativos
Construir um agente de e-commerce com tools de busca, cálculo e criação de pedido
Adicionar validações de estoque e endereço na tool criar_pedido
Aplicar princípios de idempotência em tools de mutação
Por que isso importa
O design de tools é a interface entre o LLM e o mundo real. Uma tool mal nomeada ou com descrição vaga vai ser usada incorretamente. Uma tool que lança exceção sem mensagem clara vai travar o agente.
Mas o impacto vai além da correção técnica. Tools bem projetadas são: - Auto-documentadas: o LLM sabe quando usar cada uma sem instrução extra - Compostas: o agente pode encadear buscar_produto → calcular_preco → criar_pedido sem que você escreva scripts para cada fluxo - Seguras: validações impedeem que dados inválidos cheguem ao sistema
O starter desta unidade simula um agente de atendimento de e-commerce. Em produção, as mesmas tools poderiam fazer chamadas HTTP para a API real da loja — a estrutura é idêntica.
Conceitos Fundamentais
Anatomia de um Schema de Tool
{
'name': 'calcular_preco', # snake_case, verbo_objeto
'description': 'Calcula o preço total com desconto de cupom', # O QUE faz
'input_schema': {
'type': 'object',
'properties': {
'produto_id': {
'type': 'string', # tipo preciso
},
'quantidade': {
'type': 'number',
'minimum': 1, # constraint de validação
},
'cupom': {
'type': 'string',
'description': 'Código de cupom (opcional)', # hint para o LLM
},
},
'required': ['produto_id', 'quantidade'], # cupom é opcional
},
}4 decisões de design aqui: 1. Nome: calcular_preco é claro — verbo + objeto. Evite tool1, process_data, handle_request 2. Description: “Calcula o preço total com desconto de cupom” — o LLM usa isso para decidir quando chamar 3. Tipos e constraints: 'minimum': 1 para quantidade — o LLM vai respeitar o schema 4. required vs opcional: cupom fora do required → o LLM não vai inventar um cupom
Por que Description é Mais Importante que Nome
Decisão de Arquitetura: Design de Tools para LLM
Quatro decisões não-óbvias ao projetar tools para agentes:
1. A description é o mecanismo de controle: o LLM decide qual tool chamar baseado principalmente na description, não no nome. Descreva quando usar, não só o que faz.
2. Erros são para o LLM, não para humanos: mensagens de erro devem sugerir a correção. "Parâmetro 'data' deve ser YYYY-MM-DD" ajuda o modelo a corrigir e tentar de novo; "invalid parameter" não ajuda.
3. Idempotência é obrigatória: agentes podem chamar a mesma tool múltiplas vezes. Operações de criação/modificação precisam de idempotency key.
4. Limite de ~10 tools: estudos mostram degradação de qualidade acima de 10 tools por agente. Se você precisa de mais, decomponha em sub-agentes especializados, cada um com conjunto menor.
O LLM escolhe qual tool usar baseado principalmente na description. Experimento mental:
# Tool A: o LLM pode confundir com "listar preços"
{'name': 'calcular_preco', 'description': 'Preço do produto'}
# Tool B: clara sobre o fluxo esperado
{'name': 'calcular_preco', 'description': 'Calcula preço total com desconto opcional. Use ANTES de criar_pedido para mostrar o valor ao cliente.'}A Tool B instrui o LLM sobre o fluxo correto: calcular antes de criar. Isso elimina a necessidade de scripts rígidos — o agente vai fazer a coisa certa.
Validação de Inputs na Tool
def criar_pedido(produto_id: str, quantidade: int, endereco: str) -> str:
produto = next((p for p in PRODUTOS if p['id'] == produto_id), None)
if not produto:
return f'Produto {produto_id} não encontrado' # ← mensagem que o LLM pode usar para se corrigir
# TODO do starter: validar estoque e endereco
if produto['estoque'] == 0:
return f'Produto "{produto["nome"]}" sem estoque. Não é possível criar pedido.'
if quantidade > produto['estoque']:
return (f'Estoque insuficiente: {produto["estoque"]} unidade(s) disponíveis, '
f'você pediu {quantidade}. Ajuste a quantidade ou escolha outro produto.')
if not endereco or len(endereco.strip()) < 10:
return 'Endereço inválido. Forneça o endereço completo (mínimo 10 caracteres).'
# Criar pedido apenas se tudo válido
pedido = {
'id': f'PED{len(pedidos)+1:04d}',
'produto': produto['nome'],
'quantidade': quantidade,
'endereco': endereco,
'total': produto['preco'] * quantidade,
'status': 'confirmado',
}
pedidos.append(pedido)
# Decrementar estoque
produto['estoque'] -= quantidade
return json.dumps(pedido, ensure_ascii=False)A mensagem de erro é projetada para o LLM, não para o humano. “Estoque insuficiente: X unidades disponíveis” dá ao agente a informação necessária para se auto-corrigir — ele pode sugerir uma quantidade menor.
O Ciclo Completo de Tool Use
User: "Quero comprar 2 mouses. Preço com cupom DESCONTO10?"
Iter 1 (tool_use):
→ buscar_produto({'query': 'mouse'})
← [{'id':'PROD002','nome':'Mouse Sem Fio','preco':89.90,'estoque':50}]
Iter 2 (tool_use):
→ calcular_preco({'produto_id':'PROD002','quantidade':2,'cupom':'DESCONTO10'})
← {"subtotal":179.80,"desconto":"10%","total":161.82}
Iter 3 (end_turn):
"2 Mouses Sem Fio com cupom DESCONTO10: R$161,82 (de R$179,80).
Deseja confirmar o pedido? Se sim, me passe o endereço de entrega."O agente encadeou 3 passos de forma autônoma. O “script” não foi necessário — a estrutura das tools guiou o comportamento.
Aprofundamento Técnico
Idempotência em Tools de Criação
Uma tool de criação (como criar_pedido) chamada duas vezes com os mesmos argumentos deve ser segura — não criar 2 pedidos duplicados.
def criar_pedido(produto_id: str, quantidade: int, endereco: str) -> str:
# Verificar duplicata: mesmo produto + endereço criado nos últimos 60s
import time
agora = time.time()
duplicata = next(
(p for p in pedidos
if p.get('produto_id') == produto_id
and p.get('endereco') == endereco
and agora - p.get('criado_em', 0) < 60),
None
)
if duplicata:
return json.dumps({'aviso': 'Pedido duplicado detectado', 'pedido_existente': duplicata})
# ... criar pedido normalmenteEm produção, use um idempotency_key como hash dos argumentos + timestamp da sessão.
Cupons: Case-Insensitive e Normalização
CUPONS = {'DESCONTO10': 0.10, 'FRETE20': 0.20, 'BLACK30': 0.30}
def calcular_preco(produto_id: str, quantidade: int, cupom: str = '') -> str:
# O usuário pode digitar 'desconto10', 'Desconto10', etc.
cupom_normalizado = cupom.upper().strip() if cupom else ''
desconto = CUPONS.get(cupom_normalizado, 0)
if cupom and not desconto:
return f'Cupom "{cupom}" inválido. Cupons válidos: {", ".join(CUPONS.keys())}'
# ...Isso permite que o LLM passe o cupom exatamente como o usuário digitou — sem precisar normalizar antes de chamar.
Tool com Resultado Enriquecido
def buscar_produto(query: str) -> str:
encontrados = [p for p in PRODUTOS if query.lower() in p['nome'].lower() or query.lower() in p['categoria'].lower()]
if not encontrados:
return json.dumps({
'erro': f'Nenhum produto encontrado para "{query}"',
'sugestao': 'Tente: eletrônicos, periféricos, notebook, mouse, teclado, monitor',
})
# Enriquecer com status de estoque
for p in encontrados:
p['status_estoque'] = 'disponível' if p['estoque'] > 0 else 'esgotado'
return json.dumps({
'total_encontrado': len(encontrados),
'produtos': encontrados,
}, ensure_ascii=False)O LLM usa status_estoque para já informar ao usuário se o produto está disponível, sem precisar de tool call adicional.
Exemplos Anotados
Exemplo 1: criar_pedido com Validações Completas (TODO do Starter)
def criar_pedido(produto_id: str, quantidade: int, endereco: str) -> str:
# 1. Encontrar produto
produto = next((p for p in PRODUTOS if p['id'] == produto_id), None)
if not produto:
return f'Produto {produto_id} não encontrado. IDs válidos: {[p["id"] for p in PRODUTOS]}'
# 2. Validar estoque (TODO do starter)
if produto['estoque'] == 0:
return json.dumps({
'erro': f'Produto "{produto["nome"]}" está esgotado',
'acao_sugerida': 'Escolha outro produto ou aguarde reposição',
}, ensure_ascii=False)
if quantidade > produto['estoque']:
return json.dumps({
'erro': f'Quantidade solicitada ({quantidade}) excede estoque disponível ({produto["estoque"]})',
'maximo_disponivel': produto['estoque'],
}, ensure_ascii=False)
# 3. Validar endereço (TODO do starter)
if not endereco or not endereco.strip():
return 'Endereço não pode ser vazio. Forneça: Rua, número, cidade e estado.'
if len(endereco.strip()) < 15:
return f'Endereço muito curto ("{endereco}"). Forneça o endereço completo com rua, número e cidade.'
# 4. Criar pedido
produto['estoque'] -= quantidade # decrementar estoque
pedido = {
'id': f'PED{len(pedidos)+1:04d}',
'produto_id': produto_id,
'produto': produto['nome'],
'quantidade': quantidade,
'endereco': endereco,
'preco_unitario': produto['preco'],
'total': round(produto['preco'] * quantidade, 2),
'status': 'confirmado',
}
pedidos.append(pedido)
return json.dumps({
'pedido': pedido,
'mensagem': f'Pedido {pedido["id"]} criado com sucesso! Total: R${pedido["total"]:.2f}',
}, ensure_ascii=False)Exemplo 2: Agente Lidando com Estoque Zerado
User: "Quero comprar 3 teclados mecânicos."
Iter 1:
→ buscar_produto({'query': 'teclado mecânico'})
← {"produtos": [{"id":"PROD003","estoque":0,"status_estoque":"esgotado"}]}
Iter 2 (end_turn):
"O Teclado Mecânico RGB está esgotado no momento.
Posso verificar outra opção? Temos:
- Mouse Sem Fio (50 em estoque, R$89,90)
- Notebook Pro 15\" (10 em estoque, R$5.999,99)"O agente usou o status_estoque do response enriquecido e sugeriu alternativas sem que você programasse esse fluxo explicitamente.
Padrões e Armadilhas
Padrões
Padrão 1: Erro como JSON com acao_sugerida
return json.dumps({
'erro': 'Produto esgotado',
'acao_sugerida': 'Verifique outros produtos com buscar_produto',
})O campo acao_sugerida instrui o LLM sobre o próximo passo, reduzindo iterações desnecessárias.
Padrão 2: Tool names como verbos de ação
buscar_produto ✓ (verbo + objeto)
calcular_preco ✓
criar_pedido ✓
produto_busca ✗ (confuso)
tool_ecommerce ✗ (vago)Padrão 3: Sempre retornar string da tool
# ERRADO: retorna dict
def buscar_produto(query: str) -> dict:
return {'produtos': [...]}
# CORRETO: serializar para string
def buscar_produto(query: str) -> str:
return json.dumps({'produtos': [...]}, ensure_ascii=False)Armadilhas
⚠️ Armadilha 1: Exception não tratada quebra o agente
# PERIGOSO: KeyError vai propagar
def calcular_preco(produto_id: str, quantidade: int, cupom: str = '') -> str:
produto = {p['id']: p for p in PRODUTOS}[produto_id] # KeyError se não encontrado!
# SEGURO: tratamento explícito
def calcular_preco(produto_id: str, quantidade: int, cupom: str = '') -> str:
produto = next((p for p in PRODUTOS if p['id'] == produto_id), None)
if not produto:
return f'Produto {produto_id} não encontrado'⚠️ Armadilha 2: Description ambígua gera tool call errada
# AMBÍGUO: o LLM pode usar para listar, buscar ou calcular preço
'description': 'Produto da loja'
# CLARO: ação + contexto + quando usar
'description': 'Busca produtos por nome ou categoria. Use para encontrar o ID do produto antes de calcular preço ou criar pedido.'⚠️ Armadilha 3: Muitas tools → LLM confuso Estudos mostram degradação de performance com 10+ tools. Para e-commerce, 5-7 tools é o limite prático. Se precisar de mais, agrupe em “categorias” de tools ou use sub-agentes especializados.
Se não for realizar o laboratório, pule para o próximo capítulo.
Ponte para o Lab
O TODO do starter está em criar_pedido:
def criar_pedido(produto_id: str, quantidade: int, endereco: str) -> str:
produto = next((p for p in PRODUTOS if p['id'] == produto_id), None)
if not produto:
return f'Produto {produto_id} não encontrado'
# TODO: adicione validação de estoque e endereco
pedido = {...}Implemente as validações: 1. produto['estoque'] == 0 → retornar erro de estoque esgotado 2. quantidade > produto['estoque'] → retornar erro com máximo disponível 3. not endereco or len(endereco.strip()) < 10 → retornar erro de endereço inválido
Após as validações, decremente o estoque: produto['estoque'] -= quantidade.
Teste com a query “Crie um pedido de 100 Mouse Sem Fio” — deve retornar erro de estoque insuficiente (50 disponíveis).
Agora você está pronto para o lab.