Voltar ao início

AI Internals - Tools & Agents

11 min de leitura
Cover Image for AI Internals - Tools & Agents
Lucas LemosLucas Lemos

Introduction

Em AI Internals - RAG o modelo lia evidência que você empacotou no prompt. Ele não abria ticket, cobrava cartão nem consultava um banco ao vivo. Isso são ações, e ações pedem um contrato diferente de snippets.

Tools são funções nomeadas que o modelo pode pedir, com argumentos tipados que o seu código executa. Um agent costuma ser só um loop: chamar o modelo, rodar as tools que ele pediu, anexar os resultados, chamar de novo até ele parar ou você parar.

O loop de messages de Prompts & Context continua valendo. Tool calls e tool results são roles extras na mesma lista. A maior parte dos bugs de produção vem de tratar "agent" como feature de produto em vez de um control loop com budgets, allowlists e caminhos de falha.

Tools são side effects com schema

Sem tools, a completion é só tokens. Com tools, o modelo pode emitir um pedido estruturado do tipo "chame get_order com {order_id: \"…\"}" e o seu runtime transforma isso em HTTP, SQL ou webhook.

O modelo nunca executa a tool. O seu processo executa. Essa divisão importa para segurança e para debug: se um reembolso dispara, foi o seu handler, não a GPU.

Uma definição de tool é um nome, uma descrição curta que o modelo usa para decidir quando chamar, e um JSON Schema para os argumentos. Mantenha descrições concretas ("Busca um pedido pelo ID público") e schemas apertados (enums, required, max lengths). Tools vagas viram loteria de calls erradas.

Como tool calling encaixa na chat API

Provedores diferem nos nomes dos campos (tools vs functions legado, roles tool vs function), mas o formato é estável:

  1. Você envia messages mais um array tools com schemas.
  2. O modelo devolve content de assistant normal, ou um ou mais tool_calls (id, name, arguments JSON).
  3. Seu app valida argumentos, roda os handlers e anexa messages com role: "tool" (e o tool_call_id correspondente).
  4. Você chama o modelo de novo com o histórico estendido para ele responder a partir dos resultados — ou pedir outra tool.

Tool calls em paralelo num mesmo turn são comuns: buscar usuário e pedido de uma vez. O runtime precisa aguentar isso e não assumir uma única call por response.

tool_choice (ou equivalente) é o knob de controle:

  • auto — o modelo decide se chama uma tool
  • required / any — força pelo menos uma call
  • tool nomeada — força aquela tool específica
  • none — proíbe tools neste turn (útil depois que você já tem resultados)

Trate o JSON de argumentos como input não confiável. Parseie, valide de novo contra o seu schema no código, rejeite shapes ruins antes de bater em sistemas de produção. O modelo pode inventar campos que nunca estiveram no schema.

Um round-trip mínimo de tool

Abaixo: uma pergunta do usuário, uma tool, e a segunda chamada ao modelo depois de injetar o resultado. SDKs reais escondem parte do plumbing de messages; o formato no fio é o que você precisa para raciocinar sobre falhas.

package main

import (
  "bytes"
  "encoding/json"
  "fmt"
  "io"
  "net/http"
  "os"
)

func postChat(body map[string]any) map[string]any {
  raw, _ := json.Marshal(body)
  req, _ := http.NewRequest(
    "POST",
    "https://api.openai.com/v1/chat/completions",
    bytes.NewReader(raw),
  )
  req.Header.Set("Authorization", "Bearer "+os.Getenv("OPENAI_API_KEY"))
  req.Header.Set("Content-Type", "application/json")
  res, err := http.DefaultClient.Do(req)
  if err != nil {
    panic(err)
  }
  defer res.Body.Close()
  data, _ := io.ReadAll(res.Body)
  var out map[string]any
  _ = json.Unmarshal(data, &out)
  return out
}

func getOrder(orderID string) string {
  // Stand-in for your DB / billing API.
  return fmt.Sprintf(`{"order_id":%q,"status":"shipped","total_cents":4999}`, orderID)
}

func main() {
  tools := []map[string]any{
    {
      "type": "function",
      "function": map[string]any{
        "name":        "get_order",
        "description": "Fetch an order by public ID.",
        "parameters": map[string]any{
          "type": "object",
          "properties": map[string]any{
            "order_id": map[string]any{"type": "string"},
          },
          "required": []string{"order_id"},
        },
      },
    },
  }

  messages := []map[string]any{
    {
      "role":    "system",
      "content": "Use get_order when the user asks about a specific order. Do not invent order fields.",
    },
    {
      "role":    "user",
      "content": "What is the status of order ord_123?",
    },
  }

  first := postChat(map[string]any{
    "model":    "gpt-4.1-mini",
    "messages": messages,
    "tools":    tools,
  })

  // In real code: walk choices[0].message.tool_calls.
  // Here we show the follow-up shape after you ran the tool.
  callID := "call_1"
  orderID := "ord_123"
  result := getOrder(orderID)

  messages = append(messages,
    map[string]any{
      "role": "assistant",
      "tool_calls": []map[string]any{
        {
          "id":   callID,
          "type": "function",
          "function": map[string]any{
            "name":      "get_order",
            "arguments": fmt.Sprintf(`{"order_id":%q}`, orderID),
          },
        },
      },
    },
    map[string]any{
      "role":         "tool",
      "tool_call_id": callID,
      "content":      result,
    },
  )

  second := postChat(map[string]any{
    "model":       "gpt-4.1-mini",
    "messages":    messages,
    "tools":       tools,
    "tool_choice": "none",
  })
  fmt.Println(second)
}

Note o tool_choice: "none" na segunda call: você já tem os dados; quer prosa (ou structured output), não outra call especulativa. Esse padrão escala melhor do que torcer para o modelo parar sozinho.

O que as pessoas querem dizer com "agent"

Marketing usa "agent" para quase qualquer chatbot com tools. Por dentro, um agent é uma política sobre o loop acima:

As escolhas de design interessantes não são "usar LangGraph ou não." São:

  • Quais tools existem — cada tool é superfície de ataque e carga cognitiva para o modelo.
  • Budget de steps — teto duro de rounds de LLM (muitas vezes 3–8 em fluxos de produto).
  • Budget de gasto — máximo de tokens ou dólares por sessão.
  • Regras de parada — schema de sucesso preenchido, confirmação do usuário, ou handoff humano.
  • Memory — o que dos passos anteriores fica nas messages vs o que você resume ou descarta.

Uma única tool call forçada (tool_choice em get_order) não é um agent. Um planner que pode buscar, escrever arquivos e abrir PRs em loop é. A maioria das features de produto precisa do primeiro formato, não do segundo.

Quando não usar agent

Prefira um pipeline fixo antes de um loop livre:

  • Sequência conhecida. Se todo reembolso é "verificar pedido → checar política → criar crédito," codifique isso no código. Use o modelo para preencher slots ou classificar edge cases, não para redescobrir o workflow a cada vez.
  • Respostas só com RAG. Ler docs não precisa de tools se você já recupera e faz grounding em RAG. Chamar search_docs de um agent só move o retrieval para um caminho mais lento e menos controlável, a menos que a query realmente precise ser reescrita no meio do loop.
  • Writes de alto risco. Movimentação de dinheiro, mudança de permissão, deletes: exija confirmação explícita na UI ou um passo humano. Não deixe um loop aberto chamar charge_card porque o modelo "achou que tinha terminado."
  • Budgets de latência. Cada round extra de LLM é outro prefill. Usuários sentem três round-trips; demos escondem isso atrás de streaming.
  • Vácuo de eval. Se você não consegue pontuar se o agent terminou certo, um loop vai amplificar prompts instáveis em produção instável.

Agents valem a pena quando a próxima ação depende de resultados intermediários que você não consegue ramificar estaticamente — intents ambíguos, lookups em vários sistemas com ordem desconhecida, ou tarefas de pesquisa em que a condição de parada é "evidência suficiente," não "passo 3 de 3."

Modos de falha que aparecem no loop

  • Nomes ou argumentos de tool alucinados. Valide cada call contra o seu registry. Nome desconhecido → mensagem de erro de tool de volta ao modelo, ou falha dura — nunca invente um handler.
  • Loops infinitos ou longos. A mesma tool com os mesmos args duas vezes seguidas é cheiro; detecte e quebre. Limite steps no código, não só no system prompt.
  • Tool output envenenado. Uma página scrapada ou linha de DB que diz "ignore previous instructions" ainda é tokens. Prefira payloads estruturados a HTML cru; limpe ou resuma conteúdo hostil.
  • Sucesso parcial. Duas tools em paralelo: uma falha, outra retorna. O modelo pode responder como se as duas tivessem funcionado. Superficialize erros explicitamente na mensagem de tool ({"error":"not_found"}) e ensine o system prompt a reportá-los.
  • Confiança demais no planner. O modelo pede delete_user porque o usuário disse "remove them." Autorização pertence ao seu handler (session, tenant, RBAC), não à descrição da tool.
  • Inchaço de contexto. Despejar payloads cheios de tool a cada passo estoura a janela. Guarde resultados crus no seu store; coloque resumos compactos ou IDs nas messages quando possível.
  • Side effects silenciosos. Logar "assistant disse reembolsado" sem idempotency key na API de reembolso é como cobrar duas vezes. Handlers de tool precisam da mesma disciplina de qualquer outro write path.

Hábito de debug: logue em cada step os nomes das tools, args validados, latência e se você truncou resultados. A frase final visível ao usuário é o artefato menos útil quando o loop se comporta mal.

Comparação: formatos de tool

FormaO que você constróiFraquezasUse quando
Sem toolsPrompt + RAG opcionalNão toca sistemas ao vivoQ&A sobre texto estático ou recuperado
One-shot tool callSchemas + um round + tool_choice conforme precisaSem recuperação multi-stepLookup, classificar-depois-agir, preencher formulário
Agent loop limitadoMax steps, tools em allowlist, schema de paradaMais difícil de testar; custo maiorPróxima ação depende de resultados de tools anteriores
Agent abertoTools amplas, horizonte longo, pouca estruturaLoops, gasto, dor de auditoriaPesquisa / ops internas com humanos olhando

Comece no topo dessa tabela e desça só quando um caminho fixo falhar no tráfego real. Adicionar tools é barato; adicionar um loop sem teto é decisão de produto.

Conclusão

Tools transformam a chat API num pedido de side effects que o seu código precisa executar com segurança. Agents são esse pedido embrulhado num loop com budgets e regras de parada. O mesmo contrato de messages ainda vale: schemas entram, tool results voltam como tokens, completion sai.

No próximo vamos para Orchestration — compor prompts, retrieval e passos de tool em pipelines explícitos (estilo grafo ou não) sem fingir que todo caminho precisa de um agent autônomo.