Tool calling transforma o LLM em orquestrador de funções. O modelo decide qual chamar, com quais argumentos, e usa o resultado para continuar. Aqui você aprende a desenhar tools robustos — JSON Schema bem definido, erro tipado, sandbox obrigatório.
🛠️ Tool é interface: schema + executor
Cada tool tem dois lados: o schema que o LLM lê, e o executor que roda quando chamado. Ambos vivem juntos.
- •Schema: nome, descrição, JSON Schema dos params.
- •Executor: função Python que recebe args validados, retorna resultado.
- •Sandbox: executor roda jailed se tocar FS/rede.
- •Erro: tipo claro (retry-able, fatal, partial).
💡 Description é prompt — itere
Comece com descrição básica. Em casos onde o modelo escolhe errado, refine a descrição (não a lógica do tool). Eval no harness mostra impacto.
📋 JSON Schema: o contrato do tool
Tipo, descrição, validação
JSON Schema é o contrato declarativo entre você e o modelo. Define o que cada parâmetro é, se é obrigatório, qual o domínio de valores. Modelos modernos validam o schema antes de retornar — você quase nunca recebe args malformados. Schema bom = chamada confiável; schema vago = surpresa em produção.
{
'name': 'buscar_em_corpus',
'description': 'Busca chunks relevantes em um corpus indexado.',
'parameters': {
'type': 'object',
'required': ['query', 'top_k'],
'properties': {
'query': {'type': 'string', 'minLength': 3, 'maxLength': 500},
'top_k': {'type': 'integer', 'minimum': 1, 'maximum': 20, 'default': 5},
'filtros': {
'type': 'object',
'properties': {
'ano_min': {'type': 'integer'},
'idioma': {'type': 'string', 'enum': ['pt', 'en']},
}
}
}
}
}
📑 Resumo navegável
✏️ Descrição do tool: o prompt do prompt
Onde a magia acontece
Description é o prompt do tool. O modelo escolhe a tool com base nela. 'busca_web' vs 'busca_web_para_eventos_recentes_e_noticias' muda comportamento drasticamente. Inclua quando usar (and quando NÃO usar), com 1 frase de exemplo. Itere com eval — descrição vaga é causa #1 de tool calling errado.
# RUIM (vago)
description = 'Busca informação na web.'
# BOM (when-to-use explícito)
description = '''Busca em um índice local de documentos do curso FEC.
USE quando o usuário fizer pergunta sobre conceitos, definições ou referências do curso.
NÃO USE para perguntas matemáticas (use `calculadora`) ou eventos atuais (não temos cobertura web).
Exemplo: usuário pergunta 'o que é prompt caching?' → use esta tool com query='prompt caching'.'''
📑 Resumo navegável
🚦 Erro tipado: sucesso, retry, abort
Como o tool comunica falha
Tool não pode retornar exception genérica — modelo fica perdido. Padrão: result tipado com {ok: bool, content?, error?, retry_able?: bool}. Modelo lê e decide: retry, fallback, abort. Recovery inteligente vem disso.
@dataclass
class ToolResult:
ok: bool
content: Any | None = None
error: str | None = None
error_class: str | None = None # 'rate-limit', 'invalid-input', 'fatal'
retry_able: bool = False
def buscar(query: str) -> ToolResult:
try:
r = retriever.search(query)
return ToolResult(ok=True, content=r)
except RateLimitError as e:
return ToolResult(ok=False, error=str(e), error_class='rate-limit', retry_able=True)
except InvalidQueryError as e:
return ToolResult(ok=False, error=str(e), error_class='invalid-input', retry_able=False)
📑 Resumo navegável
🔒 Sandbox obrigatório (PLAN item 62a)
Filesystem, processo, rede
Sem sandbox, prompt injection consegue fazer um tool de leitura ler ~/.aws/credentials ou .env. FEC exige FilesystemSandbox + NetworkPolicy em todo tool com side-effect. Bateria tests/sandbox/test_traversal.py (19 casos) é gate de GA — falha bloqueia merge.
from fec_sdk.sandbox import FilesystemSandbox
with FilesystemSandbox() as fs:
def ler_arquivo(path: str) -> ToolResult:
try:
return ToolResult(ok=True, content=fs.read_text(path))
except SandboxViolation as e:
return ToolResult(ok=False, error=str(e), error_class='sandbox',
retry_able=False)
📑 Resumo navegável
🔄 Loop: tool → resultado → próximo passo
ReAct simplificado
O loop simples é: modelo decide → tool executa → resultado vira role=tool message → modelo continua. Esse é o building block; agentes de T4.2 são apenas múltiplas iterações desse loop com critério de parada. Cada iteração é um chat completion.
msgs = [Message(role=USER, content=query)]
resp = client.chat(msgs, tools=tools)
if resp.tool_calls:
for call in resp.tool_calls:
result = executar_tool(call)
msgs.append(Message(role=ASSISTANT, content='', tool_calls=resp.tool_calls))
msgs.append(Message(role=TOOL, content=str(result), name=call.name))
final = client.chat(msgs) # modelo gera resposta final
📑 Resumo navegável
🎚️ Tool choice: forçar ou deixar o modelo decidir
auto, any, specific
tool_choice controla a disposição do modelo: auto (decide), any (deve chamar alguma tool), specific (deve chamar X). Útil em fluxos forçados (extração obrigatória) e em pipelines where você sabe que precisa do tool.
# Anthropic
resp = client.messages.create(
model='claude-sonnet-4-6',
tools=[extract_entities_tool],
tool_choice={'type': 'tool', 'name': 'extract_entities'},
messages=[{'role': 'user', 'content': texto}],
)
# Garantido: resp.tool_calls tem chamada para extract_entities.
📑 Resumo navegável
📑 Resumo navegável dos tópicos
1 📋 JSON Schema: o contrato do tool — Tipo, descrição, validação
2 ✏️ Descrição do tool: o prompt do prompt — Onde a magia acontece
3 🚦 Erro tipado: sucesso, retry, abort — Como o tool comunica falha
4 🔒 Sandbox obrigatório (PLAN item 62a) — Filesystem, processo, rede
5 🔄 Loop: tool → resultado → próximo passo — ReAct simplificado
6 🎚️ Tool choice: forçar ou deixar o modelo decidir — auto, any, specific
✓ O que FAZER
- ✓Sandbox obrigatório para tools com side-effect
- ✓Erro tipado com retry-able vs fatal
- ✓Description com 'when to use' explícito
- ✓Validar JSON Schema dos args antes de executar
✗ O que NÃO fazer
- ✗
os.system(arg)direto - ✗Levantar exceção genérica
- ✗Description vaga: 'busca informação'
- ✗Confiar que o modelo preencheu certo
🚫 Quando NÃO usar
- •Tarefa pode ser feita só com prompt: tool adiciona complexidade desnecessária.
- •Modelo OSS pequeno sem suporte estável a tool: força adapter ou mock.
- •Latência crítica: tool call adiciona 1+ roundtrip.
💻 Exemplo de código
from fec_sdk import Message, MessageRole, Tool, ToolResult
from fec_sdk.adapters import get_adapter
from fec_sdk.sandbox import FilesystemSandbox
tool_ler_arquivo = Tool(
name="ler_arquivo",
description="Lê arquivo .txt ou .md em path RELATIVO ao sandbox.",
parameters={
"type": "object",
"properties": {"path": {"type": "string"}},
"required": ["path"],
}
)
with FilesystemSandbox() as fs:
fs.write_text("nota.md", "FEC v1.0")
client = get_adapter("mock")
resp = client.chat(
[Message(role=MessageRole.USER, content="Leia nota.md")],
tools=[tool_ler_arquivo],
)
if resp.tool_calls:
for call in resp.tool_calls:
try:
conteudo = fs.read_text(call.arguments["path"])
resultado = ToolResult(tool_call_id=call.id, content=conteudo)
except Exception as e:
resultado = ToolResult(tool_call_id=call.id, content=str(e), is_error=True)
🏋️ Exercício hands-on
Defina 3 tools (ler arquivo, calcular, buscar termo) com sandbox e schema. Bateria tests/sandbox/ deve passar em todos. Em exercicios/modulo-4-1/.
📚 Bibliografia
- OpenAI (2023) — Function calling guide
- Anthropic (2024) — Tool use with Claude
- JSON Schema — JSON Schema 2020-12 spec
- Schick et al. (2023) — Toolformer: Language Models Can Teach Themselves to Use Tools
🎯 Resumo do Módulo
- ✓Tool = schema + executor; ambos vivem juntos.
- ✓JSON Schema como contrato; validar args antes de executar.
- ✓Description é o prompt do prompt — itere com eval.
- ✓Sandbox obrigatório (PLAN item 62a) para FS/rede/processo.
- ✓Erro tipado: retry-able vs fatal; modelo recupera melhor.
Próximo Módulo:
Agentes single (ReAct, planner/executor)