DeepSeek Tool Calls: como implementar funções com segurança

DeepSeek Tool Calls permite que o modelo solicite funções definidas pela sua aplicação. O modelo pode pedir uma consulta de pedido, cálculo de frete ou abertura de ticket, mas não executa a função. Seu backend decide se a ferramenta existe, valida os argumentos, verifica a permissão do usuário, executa o código permitido e devolve o resultado com o tool_call_id correspondente.

Regra de segurança: o modelo propõe; o backend autoriza e executa. JSON válido, strict mode ou uma descrição convincente nunca substituem autenticação, autorização e validação das regras do negócio.

Status em 19 de julho de 2026: os IDs hospedados com Tool Calls são deepseek-v4-flash e deepseek-v4-pro. Os aliases deepseek-chat e deepseek-reasoner serão retirados em 24 de julho de 2026, às 15:59 UTC. Não os use em novas implementações.

Última verificação técnica: 19 de julho de 2026. Este site e este guia são independentes, sem afiliação, autorização ou endosso da DeepSeek.

Resumo da implementação correta

  • declare funções no array tools com nome, descrição e JSON Schema;
  • envie a pergunta e as ferramentas para /chat/completions;
  • aceite uma chamada de ferramenta somente com finish_reason: "tool_calls";
  • valide nome, ID e argumentos antes de executar;
  • verifique a autorização do usuário dentro da função;
  • devolva exatamente um resultado para cada chamada, usando o mesmo tool_call_id;
  • repita o loop até uma resposta com finish_reason: "stop" ou até o limite definido;
  • em Thinking Mode, preserve integralmente reasoning_content nas mensagens de assistente que envolvem ferramentas;
  • não envie tool_choice no Thinking Mode V4;
  • não exponha API key, shell, banco ou cliente HTTP genérico ao modelo.

Esta página trata especificamente do loop de ferramentas. Para autenticação, endpoint e parâmetros gerais, consulte DeepSeek API. Para o objeto completo de requisição e resposta, use Chat Completions.

Como Tool Calls funciona

EtapaResponsávelAção
1UsuárioPede uma informação ou ação
2AplicaçãoEnvia messages e tools à API
3ModeloResponde diretamente ou retorna tool_calls
4BackendValida função, argumentos, identidade e autorização
5BackendExecuta somente a função permitida
6AplicaçãoAdiciona uma mensagem role="tool" por chamada
7AplicaçãoEnvia novamente todo o contexto necessário
8ModeloSolicita outra ferramenta ou conclui com stop

Uma interação pode exigir várias rodadas. Por exemplo, o modelo pode consultar a data, depois o estoque e só então responder. Um código que executa apenas a primeira chamada e espera texto final falha em fluxos multi-step. O loop precisa ser limitado para evitar custo ou repetição sem fim.

Objetos e campos essenciais

CampoFunçãoValidação necessária
toolsLista de funções apresentadas ao modeloSomente ferramentas necessárias; máximo documentado de 128
function.nameIdentificador da funçãoAllowlist exata; até 64 caracteres no formato aceito
function.parametersJSON Schema dos argumentosValidar novamente no backend
tool_choiceControla escolha em Non-ThinkingNão enviar em Thinking V4
tool_callsChamadas propostas pelo modeloArray, IDs únicos, tipo function e nome permitido
function.argumentsString que pretende conter JSONLimitar tamanho, fazer parse e aplicar schema
tool_call_idLiga o resultado à chamadaCopiar exatamente o ID recebido
reasoning_contentRaciocínio retornado em ThinkingPreservar nas rodadas com ferramentas
finish_reasonIndica por que a geração parouExecutar apenas em tool_calls; concluir apenas em stop

A API aceita apenas funções como tipo de ferramenta e documenta até 128 funções por requisição. Isso é um limite técnico, não uma recomendação para declarar 128 ferramentas. Uma lista curta, bem descrita e específica reduz seleções erradas e facilita autorização.

Exemplo completo em Node.js

O exemplo consulta um pedido fictício. Ele usa Node.js 20 ou superior, fetch nativo e Zod. A função é read-only, mas ainda verifica se o pedido pertence ao usuário autenticado. Em uma aplicação real, authContext deve vir da sessão validada no backend, nunca de um argumento escolhido pelo modelo.

mkdir deepseek-tools-demo
cd deepseek-tools-demo
npm init -y
npm install zod
npm pkg set type=module

Salve o código como tool-calls.mjs:

import { z } from "zod";

const API_KEY = process.env.DEEPSEEK_API_KEY;
const MODEL = process.env.DEEPSEEK_MODEL || "deepseek-v4-flash";
const THINKING_ENABLED = process.env.DEEPSEEK_THINKING === "enabled";
const MAX_TOOL_ROUNDS = 8;

if (!API_KEY) {
  throw new Error("DEEPSEEK_API_KEY não configurada.");
}

if (!["deepseek-v4-flash", "deepseek-v4-pro"].includes(MODEL)) {
  throw new Error("DEEPSEEK_MODEL deve ser deepseek-v4-flash ou deepseek-v4-pro.");
}

const tools = [
  {
    type: "function",
    function: {
      name: "get_order_status",
      description:
        "Consulta um pedido pelo ID. Use somente quando o usuário pedir o status de um pedido específico.",
      parameters: {
        type: "object",
        properties: {
          order_id: {
            type: "string",
            description: "ID no formato BR seguido por cinco dígitos, por exemplo BR12345.",
            pattern: "^BR[0-9]{5}$",
          },
        },
        required: ["order_id"],
        additionalProperties: false,
      },
    },
  },
];

const orderArgsSchema = z.object({
  order_id: z.string().regex(/^BR[0-9]{5}$/),
}).strict();

const orders = new Map([
  [
    "BR12345",
    {
      ownerId: "user_42",
      status: "em_transporte",
      previsaoEntrega: "2026-07-22",
    },
  ],
  [
    "BR99999",
    {
      ownerId: "user_77",
      status: "aguardando_pagamento",
      previsaoEntrega: null,
    },
  ],
]);

function getOrderStatus(args, authContext) {
  const order = orders.get(args.order_id);

  if (!order || order.ownerId !== authContext.userId) {
    return { found: false };
  }

  return {
    found: true,
    order_id: args.order_id,
    status: order.status,
    previsao_entrega: order.previsaoEntrega,
  };
}

const toolRegistry = new Map([
  [
    "get_order_status",
    {
      schema: orderArgsSchema,
      execute: getOrderStatus,
    },
  ],
]);

class DeepSeekApiError extends Error {
  constructor(status) {
    super(`Falha na DeepSeek API: ${status}`);
    this.name = "DeepSeekApiError";
    this.status = status;
  }
}

async function callDeepSeek(messages, apiUserId) {
  const payload = {
    model: MODEL,
    messages,
    tools,
    thinking: { type: THINKING_ENABLED ? "enabled" : "disabled" },
    max_tokens: 1200,
    stream: false,
    user_id: apiUserId,
  };

  if (THINKING_ENABLED) {
    payload.reasoning_effort = "high";
  } else {
    // tool_choice é usado somente no modo Non-Thinking.
    payload.tool_choice = "auto";
  }

  const response = await fetch(
    "https://api.deepseek.com/chat/completions",
    {
      method: "POST",
      headers: {
        "Content-Type": "application/json",
        Authorization: `Bearer ${API_KEY}`,
      },
      body: JSON.stringify(payload),
      signal: AbortSignal.timeout(90_000),
    },
  );

  if (!response.ok) {
    throw new DeepSeekApiError(response.status);
  }

  const data = await response.json();
  const choice = data?.choices?.[0];

  if (!choice?.message || typeof choice.finish_reason !== "string") {
    throw new Error("Resposta inválida da DeepSeek API.");
  }

  return choice;
}

function normalizeAssistantMessage(message, hasToolCalls) {
  const normalized = {
    role: "assistant",
    content: typeof message.content === "string" ? message.content : "",
  };

  if (hasToolCalls) {
    normalized.tool_calls = message.tool_calls;
  }

  if (THINKING_ENABLED && hasToolCalls) {
    if (typeof message.reasoning_content !== "string") {
      throw new Error("reasoning_content ausente em Tool Call com Thinking.");
    }

    normalized.reasoning_content = message.reasoning_content;
  }

  return normalized;
}

function executeToolCall(toolCall, authContext, seenToolCallIds) {
  if (
    !toolCall ||
    toolCall.type !== "function" ||
    typeof toolCall.id !== "string" ||
    !toolCall.id ||
    typeof toolCall.function?.name !== "string" ||
    typeof toolCall.function?.arguments !== "string"
  ) {
    throw new Error("Tool Call malformada.");
  }

  if (seenToolCallIds.has(toolCall.id)) {
    throw new Error("tool_call_id duplicado.");
  }
  seenToolCallIds.add(toolCall.id);

  const registeredTool = toolRegistry.get(toolCall.function.name);
  if (!registeredTool) {
    throw new Error("Função não permitida.");
  }

  if (toolCall.function.arguments.length > 4096) {
    throw new Error("Argumentos da ferramenta excedem o limite local.");
  }

  let rawArguments;
  try {
    rawArguments = JSON.parse(toolCall.function.arguments);
  } catch {
    throw new Error("Argumentos da ferramenta não são JSON válido.");
  }

  const parsedArguments = registeredTool.schema.safeParse(rawArguments);
  if (!parsedArguments.success) {
    throw new Error("Argumentos da ferramenta foram rejeitados pelo schema local.");
  }

  const result = registeredTool.execute(parsedArguments.data, authContext);

  return {
    role: "tool",
    tool_call_id: toolCall.id,
    content: JSON.stringify(result),
  };
}

async function runToolLoop({ userMessage, authContext, apiUserId }) {
  const messages = [
    {
      role: "system",
      content:
        "Você é um assistente de pedidos. Use ferramentas para consultar dados. Nunca invente status.",
    },
    { role: "user", content: userMessage },
  ];

  const seenToolCallIds = new Set();

  for (let round = 1; round <= MAX_TOOL_ROUNDS; round += 1) {
    const choice = await callDeepSeek(messages, apiUserId);
    const toolCalls = Array.isArray(choice.message.tool_calls)
      ? choice.message.tool_calls
      : [];

    if (choice.finish_reason === "stop") {
      if (toolCalls.length !== 0) {
        throw new Error("Resposta stop contém Tool Calls inesperadas.");
      }

      const finalContent = choice.message.content;
      if (typeof finalContent !== "string" || !finalContent.trim()) {
        throw new Error("Resposta final vazia.");
      }

      return finalContent;
    }

    if (choice.finish_reason !== "tool_calls") {
      throw new Error(`Geração não concluída: ${choice.finish_reason}`);
    }

    if (toolCalls.length === 0) {
      throw new Error("finish_reason=tool_calls sem chamadas de ferramenta.");
    }

    const assistantMessage = normalizeAssistantMessage(
      choice.message,
      true,
    );
    messages.push(assistantMessage);

    const toolResults = toolCalls.map((toolCall) =>
      executeToolCall(toolCall, authContext, seenToolCallIds),
    );

    if (toolResults.length !== toolCalls.length) {
      throw new Error("Nem todas as Tool Calls receberam resultado.");
    }

    messages.push(...toolResults);
  }

  throw new Error(`Limite de ${MAX_TOOL_ROUNDS} rodadas de ferramentas atingido.`);
}

const authContext = {
  // Em produção, derive da sessão autenticada no servidor.
  userId: "user_42",
};

const answer = await runToolLoop({
  userMessage: "Qual é o status do meu pedido BR12345?",
  authContext,
  apiUserId: "demo_user_42",
});

console.log(answer);

Execute em Non-Thinking:

DEEPSEEK_API_KEY="sua_chave" \
DEEPSEEK_MODEL="deepseek-v4-flash" \
DEEPSEEK_THINKING="disabled" \
node tool-calls.mjs

Para Thinking, altere somente o ambiente:

DEEPSEEK_API_KEY="sua_chave" \
DEEPSEEK_MODEL="deepseek-v4-flash" \
DEEPSEEK_THINKING="enabled" \
node tool-calls.mjs

Controles aplicados pelo exemplo

  • aceita apenas os dois IDs V4 atuais;
  • mantém a API key no ambiente do servidor;
  • limita o loop a oito respostas do modelo;
  • executa apenas nomes registrados em toolRegistry;
  • limita o tamanho da string de argumentos antes de fazer parse;
  • valida argumentos novamente com Zod em modo estrito;
  • verifica o proprietário do pedido dentro da função;
  • recusa IDs duplicados e cria um resultado para cada chamada;
  • preserva reasoning_content nas mensagens de ferramentas com Thinking;
  • conclui somente quando finish_reason é stop e há conteúdo;
  • rejeita length, content_filter, insufficient_system_resource e valores desconhecidos.

A base falsa existe apenas para tornar o exemplo reproduzível. Troque-a por uma camada de serviço que use credenciais de baixo privilégio, queries parametrizadas, timeout e auditoria. Não permita que o modelo escreva SQL, URL ou comando de shell e os execute diretamente.

Thinking Mode com Tool Calls

DeepSeek V4 suporta ferramentas em Thinking Mode, mas há dois requisitos de compatibilidade que mudam a implementação:

  • Não envie tool_choice: as notas oficiais de integração informam que V4 Thinking rejeita esse parâmetro. Deixe a escolha automática nesse modo.
  • Preserve reasoning_content: quando a mensagem do assistente contém Tool Calls, o campo precisa voltar integralmente em todas as requisições subsequentes relevantes. Omissão pode causar erro 400.
  • Mantenha content presente: normalize valor nulo para string vazia na mensagem de assistente com ferramentas.

O exemplo faz isso ao adicionar tool_choice: "auto" somente quando Thinking está desativado e ao reconstruir a mensagem de assistente com content, reasoning_content e tool_calls. Não tente resumir ou alterar reasoning_content no meio do loop.

No Thinking Mode, os parâmetros temperature, top_p, presence_penalty e frequency_penalty não têm efeito. O exemplo não os envia. O esforço documentado é high ou max.

Para um guia dedicado a conversas, streaming e parâmetros de raciocínio, consulte DeepSeek Thinking Mode.

Raw HTTP e OpenAI SDK usam posições diferentes

No payload HTTP enviado diretamente por fetch, thinking e user_id são campos de primeiro nível, como no exemplo Node.js. Ao usar o OpenAI SDK para Python, a documentação orienta colocar esses campos específicos em extra_body; reasoning_effort permanece como argumento da chamada.

import os
from openai import OpenAI

client = OpenAI(
    api_key=os.environ["DEEPSEEK_API_KEY"],
    base_url="https://api.deepseek.com",
)

response = client.chat.completions.create(
    model="deepseek-v4-flash",
    messages=[{"role": "user", "content": "Consulte meu pedido."}],
    tools=tools,
    reasoning_effort="high",
    extra_body={
        "thinking": {"type": "enabled"},
        "user_id": "user_pseudonimo_42",
    },
)

O trecho acima demonstra apenas a posição dos parâmetros. Um aplicativo Python ainda precisa implementar o mesmo loop limitado, preservar a mensagem completa do assistente, validar todas as chamadas e encerrar somente em stop.

tool_choice em Non-Thinking

As opções abaixo pertencem ao formato OpenAI em Non-Thinking. Não copie esses exemplos para Thinking V4.

ValorComportamentoQuando usar
autoO modelo responde ou chama uma ou mais funçõesPerguntas que podem precisar de dados externos
noneImpede chamadas de ferramentasResposta exclusivamente textual
requiredExige uma ou mais chamadasFluxo no qual toda resposta depende de verificação externa
Função específicaForça o nome declaradoFluxo em que a aplicação já determinou a única ferramenta possível
{
  "tool_choice": {
    "type": "function",
    "function": { "name": "get_order_status" }
  }
}

Forçar uma ferramenta não autoriza sua execução. O backend continua responsável por confirmar identidade, permissão, estado do recurso e validade semântica dos argumentos.

Strict mode Beta

Strict mode tenta fazer a saída da chamada cumprir o JSON Schema declarado. É um recurso Beta e requer:

  1. usar https://api.deepseek.com/beta como base URL;
  2. definir strict: true em todas as funções do array;
  3. fornecer um schema aceito pelo servidor.
{
  "type": "function",
  "function": {
    "name": "get_order_status",
    "strict": true,
    "description": "Consulta um pedido pelo ID.",
    "parameters": {
      "type": "object",
      "properties": {
        "order_id": {
          "type": "string",
          "pattern": "^BR[0-9]{5}$"
        }
      },
      "required": ["order_id"],
      "additionalProperties": false
    }
  }
}

Os tipos listados pela documentação Beta incluem object, string, number, integer, boolean, array, enum e anyOf. Em cada objeto, todas as propriedades precisam ser obrigatórias e additionalProperties deve ser false. Para strings, minLength e maxLength não são suportados; para arrays, minItems e maxItems também não são.

Strict mode controla formato, não intenção. Um order_id pode obedecer ao regex e pertencer a outra pessoa. Uma quantia pode ser número e ainda exceder o saldo permitido. Valide novamente com sua biblioteca, aplique regras do negócio e verifique permissão antes de qualquer efeito.

Como tratar finish_reason

ValorAção segura
tool_callsExigir chamadas válidas, executar todas as permitidas e continuar o loop
stopExigir ausência de Tool Calls e conteúdo final não vazio; então encerrar
lengthTratar como resposta incompleta; não executar nem exibir como conclusão
content_filterInterromper e aplicar a política da aplicação
insufficient_system_resourceTratar como falha transitória controlada
Ausente ou desconhecidoFalhar fechado e registrar apenas metadados seguros

Não use apenas a presença de message.content para decidir que terminou. O modelo pode retornar texto junto com uma solicitação de ferramenta. O contrato do loop deve se basear em finish_reason e na consistência entre esse valor e tool_calls.

Controles de segurança para produção

1. Allowlist de ferramentas

Mapeie nomes estáticos para funções conhecidas. Nunca use eval, import dinâmico baseado no nome gerado ou reflexão que permita chamar qualquer método do processo.

2. Autorização fora do prompt

O prompt não é uma política de acesso. A função deve receber um contexto autenticado criado pelo servidor e verificar tenant, usuário, papel e recurso. Não permita que o modelo forneça user_id, tenant_id ou permissões como argumentos confiáveis.

3. Separar leitura de escrita

Consultas read-only podem seguir um fluxo automático após autorização. Cancelar, reembolsar, cobrar, enviar, apagar ou alterar precisa de confirmação explícita, proteção contra repetição e auditoria. A confirmação deve ocorrer na interface e ser associada à ação exata, não inferida de uma frase anterior.

4. Idempotência e concorrência

Operações de escrita devem aceitar uma idempotency key criada pelo backend. Grave o estado da ação antes de fazer retry e controle concorrência para impedir duas execuções simultâneas. Se houver timeout após o envio, verifique o estado real antes de repetir.

5. Reduzir a superfície de cada função

Prefira get_order_status(order_id) a ferramentas genéricas como run_sql(query), fetch_url(url) ou run_command(command). Interfaces estreitas permitem validação, autorização e logs compreensíveis.

6. Tratar resultados como não confiáveis

Resultados de CRM, páginas e documentos podem conter instruções maliciosas. Serialize apenas campos necessários e diga ao modelo que dados de ferramentas são conteúdo, não comandos. Prompt injection não deve alterar a allowlist ou permissões.

7. Logs com minimização

Registre request ID, ferramenta, decisão de autorização, duração, resultado e código de erro. Evite gravar chave, raciocínio completo, prompt, documento, token de sessão ou resposta integral sem necessidade e retenção definida.

Matriz mínima de testes

CasoResultado esperado
Função desconhecidaFalhar sem executar
Argumentos que não são JSONFalhar antes da função
Campo extraSchema local rejeita
Pedido de outro usuárioNão revelar existência ou dados
tool_call_id duplicadoInterromper o loop
Duas Tool Calls na mesma respostaUm resultado correspondente para cada ID
Mais de oito rodadasEncerrar sem nova chamada
finish_reason=lengthNão apresentar resposta parcial
Thinking sem reasoning_contentFalhar antes da próxima requisição
Prompt injection no resultadoNão ampliar ferramentas ou permissões
Retry de ação de escritaIdempotência impede duplicação

Quando usar Tool Calls

  • consultar estoque, pedido ou status de serviço;
  • obter dados recentes de um sistema autorizado;
  • calcular frete ou elegibilidade com uma função determinística;
  • criar tickets ou tarefas após confirmação e autorização;
  • orquestrar etapas específicas com contratos de entrada estreitos.

Não use Tool Calls quando uma resposta textual é suficiente, quando não existe função real, quando a aplicação ainda não tem autenticação ou quando uma decisão humana obrigatória não pode ser automatizada. JSON Output estrutura texto; Tool Calls solicita execução externa. São recursos diferentes.

Privacidade e transparência

O operador do sistema downstream é responsável pelos dados dos próprios usuários. Envie à API apenas os campos necessários, não inclua dados pessoais em user_id, não retorne um registro inteiro quando três campos bastam e defina retenção para logs de ferramentas.

  • informe que a saída é gerada por IA e pode conter erros;
  • explique quais fornecedores e sistemas recebem dados;
  • obtenha base legal e consentimento quando aplicáveis;
  • implemente acesso, correção e exclusão conforme a jurisdição;
  • não apresente seu aplicativo como produto oficial ou endossado.

Perguntas frequentes

O modelo executa a função?

Não. Ele gera uma proposta estruturada. A aplicação valida, autoriza e executa a função real.

Quais modelos hospedados aceitam Tool Calls?

deepseek-v4-flash e deepseek-v4-pro, conforme o catálogo verificado.

Posso usar tool_choice no Thinking Mode?

Não no DeepSeek V4 Thinking pelo endpoint OpenAI-compatible. As notas oficiais de integração informam que o parâmetro é rejeitado. Omita-o e deixe a seleção automática.

Strict mode elimina a validação local?

Não. Ele ajuda no formato do JSON Schema, mas não verifica autorização, propriedade, estado do recurso ou regra de negócio.

Por que preservar reasoning_content?

Em Thinking com ferramentas, a DeepSeek exige que esse campo retorne nas requisições subsequentes relevantes. Removê-lo pode gerar erro 400 e quebrar a continuidade do raciocínio.

Quantas rodadas devo permitir?

Defina um limite de acordo com o fluxo e o orçamento. O exemplo usa oito como política local, não como limite oficial. Também monitore número de chamadas, tempo e tokens.

Conclusão

Uma implementação confiável de DeepSeek Tool Calls é um loop de controle, não uma execução automática de qualquer JSON produzido pelo modelo. Declare ferramentas estreitas, valide nome e argumentos, autorize com contexto do servidor, associe cada resultado ao tool_call_id e encerre somente em finish_reason: "stop".

Ao ativar Thinking, omita tool_choice e preserve reasoning_content. Mesmo com strict mode, mantenha validação local, limites de rodadas, confirmação para ações sensíveis e idempotência. Esses controles transformam a solicitação do modelo em uma operação que o seu sistema consegue explicar, testar e interromper.

Fontes oficiais

Este conteúdo é independente e educacional. A DeepSeek não revisou, autorizou ou endossou este código ou guia.