Como fazer o DeepSeek V4 Pro 0813 devolver JSON confiável com tool calling e JSON Output

tool calling DeepSeek V4 Pro com JSON Output e strict mode para respostas em JSON
Resposta rápida

Se teu agente depende de resposta em JSON, tool calling DeepSeek V4 Pro é um dos dois caminhos oficiais, e o outro é o JSON Output via response_format. O build 0813 é a versão de disponibilidade geral por trás do endpoint deepseek-v4-pro, com tool calls no formato compatível com a API da OpenAI: array tools na requisição, message.tool_calls na resposta e devolução com role: "tool". Pro JSON puro, ligue response_format com type json_object, escreva a palavra json no prompt com exemplo e ajuste max_tokens. Quem precisa de schema fechado usa o strict mode Beta, que valida o JSON Schema no servidor

Fala aí, beleza? Agente não quebra por falta de inteligência do modelo, quebra quando o json.loads recebe um "Claro! Aqui está o resultado que você pediu:" no lugar do objeto que teu código esperava 😀

O DeepSeek V4 Pro saiu do preview e o build 0813 é a versão de disponibilidade geral por trás do endpoint deepseek-v4-pro

E ele te dá dois caminhos oficiais pra saída estruturada: tool calling no formato compatível com a API da OpenAI e JSON Output ligado pelo parâmetro response_format

A ideia aqui é essa: qual dos dois usar, como desenhar a ferramenta, qual contrato de retorno a tua integração deve assumir e o que fazer quando a chamada falha (porque falha, e tem caso reportado no repositório oficial)

Bora ver na prática?

JSON Output ou tool calling: qual dos dois usar em cada cenário

Antes de sair colando código, decide o objetivo, porque os dois recursos resolvem problemas diferentes

JSON Output é pra quando você só quer o TEXTO FINAL num formato fixo

Extração de campos de um documento, classificação de ticket, resposta que vai direto pro teu endpoint

O modelo não precisa acionar nada, ele só precisa responder em objeto e não em prosa

Tool calling é pra quando o modelo precisa chamar código teu e receber o resultado de volta

Consulta a banco, busca, ação em sistema externo

É o modelo dizendo "roda essa função com esses argumentos" e esperando o retorno pra fechar a resposta

Strict mode (Beta) entra quando o schema não pode variar e você quer a validação do lado do servidor, não só na tua camada

Recurso Use quando Como liga
JSON Output a resposta final precisa de formato fixo response_format: {"type": "json_object"}
Tool calling o modelo precisa executar código seu array tools na request
Strict mode (Beta) o schema é requisito e não sugestão base_url beta + "strict": true
Formação Claude Code
Formação Recomendada

Formação Claude Code

Domine Claude Code do absoluto zero até o avançado

  • 120 aulas
  • 4 projetos
  • 9h 45min

E se liga: os dois convivem na mesma integração

Dá pra ter um agente com tools pra buscar dados e, na resposta final ao usuário, exigir JSON pra alimentar tua UI

Se você ainda está decidindo qual modelo colocar em cada etapa do fluxo, essa comparação entre V4 Pro e Flash ajuda a separar o que vale rodar no caro e o que vale rodar no barato

O que você precisa antes de começar

Lista curta, sem mistério:

  • chave da API DeepSeek
  • um cliente compatível com a API da OpenAI (o formato das requisições é o mesmo)
  • o identificador de modelo deepseek-v4-pro
  • noção dos limites de janela: 1.000.000 de tokens de contexto e 384.000 de saída máxima
  • pro strict mode, o base_url apontando pro endpoint beta https://api.deepseek.com/beta

Tome cuidado com o nome do modelo! Os nomes legados deepseek-chat e deepseek-reasoner foram aposentados

Se tua integração antiga ainda manda um deles hardcoded, é aí que o teu primeiro 400 vai nascer

E tem os limites do array tools, que é onde muita gente escorrega sem perceber:

  • máximo de 128 functions por chamada
  • o único type aceito hoje é function
  • o nome da função só aceita a-z, A-Z, 0-9, underscore e hífen, com até 64 caracteres

Nome de função com acento, espaço ou ponto não passa, então nada de buscar.pedido, buscar pedido ou buscar_pedido_do_usuário

Se o teu interesse é mais o uso do modelo no dia a dia do que a integração, tem outro post aqui sobre onde o V4 ajuda a programar

Como ligar o JSON Output no DeepSeek V4 Pro 0813 passo a passo

Esse é o caminho mais simples e é por onde eu sugiro começar

  1. Envie response_format com type json_object na requisição
from openai import OpenAI
import json

client = OpenAI(api_key="SUA_CHAVE", base_url=DEEPSEEK_BASE_URL)

system = """
Você extrai dados de pedido e responde SEMPRE em json.
Exemplo do formato esperado:
{"cliente": "Maria", "itens": 3, "total": 199.9}
"""

resp = client.chat.completions.create(
    model="deepseek-v4-pro",
    messages=[
        {"role": "system", "content": system},
        {"role": "user", "content": "Maria comprou 3 itens e pagou 199,90"},
    ],
    response_format={"type": "json_object"},
    max_tokens=2048,
)

O erro comum deste passo: achar que ligar o parâmetro basta e ir embora feliz

  1. Escreva a palavra json no prompt, junto de um exemplo do formato

Isso não é superstição de blogueiro, é exigência da documentação oficial do JSON Output: a palavra json precisa aparecer no prompt de sistema ou de usuário, acompanhada de um exemplo do JSON desejado

O erro comum deste passo: escrever "responda em formato estruturado" achando que é a mesma coisa

Não é, e o exemplo do objeto no prompt faz metade do trabalho de te entregar as chaves com os nomes que você quer

  1. Defina max_tokens de forma razoável

A própria documentação orienta ajustar max_tokens pro JSON não ser truncado no meio

O erro comum deste passo: deixar o limite curto demais e receber um objeto cortado no meio de uma string

Aí o parse estoura e você vai culpar o modelo, quando o problema era o teu teto

Lembra que a saída máxima do V4 Pro é 384.000 tokens, então espaço tem, o que falta normalmente é você pedir

  1. Valide o parse do teu lado antes de usar
raw = resp.choices[0].message.content

if not raw:
    raise RuntimeError("content vazio, tratar como falha e repetir")

dados = json.loads(raw)

O erro comum deste passo: confiar cegamente no parse

A DeepSeek avisa na própria documentação que o JSON Output pode OCASIONALMENTE retornar content vazio, e a orientação oficial é ajustar o prompt

Ou seja: content vazio é falha, não é resposta, e teu código precisa saber a diferença

Como desenhar a tool e ler o tool_calls na resposta

Agora o ciclo completo de function calling, que é onde mora o agente de verdade

Se você já mexeu com function calling na API da OpenAI, aqui é bem semelhante: mesmo desenho de request e mesmos campos na resposta

  1. Monte o array tools com type: "function"
tools = [
    {
        "type": "function",
        "function": {
            "name": "buscar_pedido",
            "description": "Busca um pedido pelo id no banco interno",
            "parameters": {
                "type": "object",
                "properties": {
                    "pedido_id": {
                        "type": "string",
                        "description": "ID do pedido, ex: PED-1042"
                    }
                },
                "required": ["pedido_id"]
            }
        }
    }
]

O erro comum deste passo: nome de função fora do padrão permitido

a-z, A-Z, 0-9, underscore e hífen, até 64 caracteres, e no máximo 128 functions no array

  1. Descreva os parâmetros no JSON Schema com carinho

A description de cada campo é o que o modelo lê pra decidir o que colocar ali

Campo sem descrição é campo que vem preenchido no chute

O erro comum deste passo: schema genérico demais, tipo um params do tipo string que "aceita qualquer coisa"

  1. Leia message.tool_calls e confirme o finish_reason
choice = resp.choices[0]

if choice.finish_reason == "tool_calls":
    for call in choice.message.tool_calls:
        args = json.loads(call.function.arguments)
        # roda a tua função aqui

Quando o modelo chama uma ferramenta, o finish_reason volta como tool_calls

Os outros valores possíveis são stop, length, content_filter e insufficient_system_resource

O erro comum deste passo: tratar toda resposta como texto e nunca checar finish_reason

Esse é o campo que separa "o modelo respondeu" de "o modelo pediu pra você executar algo"

  1. Execute a função e devolva o resultado como mensagem com role: "tool"
messages.append(choice.message)
messages.append({
    "role": "tool",
    "tool_call_id": call.id,
    "content": json.dumps(resultado),
})

O erro comum deste passo: esquecer de devolver a mensagem com role tool e ficar reclamando que o modelo "ignorou a ferramenta"

Ele não ignorou, ele está esperando o retorno que nunca chegou

  1. Mande o contexto de volta pro modelo fechar a resposta
final = client.chat.completions.create(
    model="deepseek-v4-pro",
    messages=messages,
    tools=tools,
)

O erro comum deste passo: remontar o histórico pela metade

A mensagem do assistant que continha o tool_calls precisa estar lá também, não só o retorno da tua função

A referência completa do ciclo está na documentação de tool calls

Strict mode: como forçar o schema com validação no servidor

Até aqui o schema é uma sugestão forte

No strict mode, que é um recurso Beta, o servidor valida o JSON Schema que você mandou

  1. Aponte o base_url pro endpoint beta
client = OpenAI(
    api_key="SUA_CHAVE",
    base_url="https://api.deepseek.com/beta",
)

O erro comum deste passo (e é o campeão): manter o base_url padrão, marcar strict no schema e sair achando que a validação está ativa

Não está, e você só descobre em produção 😛

  1. Marque strict como true em cada function do array tools
tools = [
    {
        "type": "function",
        "function": {
            "name": "buscar_pedido",
            "strict": True,
            "description": "Busca um pedido pelo id no banco interno",
            "parameters": {
                "type": "object",
                "properties": {
                    "pedido_id": {"type": "string"}
                },
                "required": ["pedido_id"]
            }
        }
    }
]

O erro comum deste passo: marcar em uma function e esquecer das outras do array

  1. Escreva o schema só com os tipos suportados

O strict mode aceita object, string, number, integer, boolean, array, enum e anyOf

E o schema pode usar $defs e $ref, inclusive pra estruturas recursivas (comentário aninhado, árvore de categorias, esse tipo de coisa)

O erro comum deste passo: colar um schema enorme que veio de outra stack, cheio de tipo que aqui não roda

  1. Trate o erro que o servidor devolve

Quando o schema não está conforme ou usa um tipo não suportado, o servidor retorna erro

Isso é bom, viu? Melhor estourar na tua esteira do que receber um argumento fora de forma no meio de uma execução

O erro comum deste passo: engolir a exceção no try e seguir a vida sem log

E um detalhe que resolve dúvida de muita gente: o strict mode funciona tanto no modo thinking quanto no non-thinking

Modo thinking: como remontar o contexto sem tomar erro 400

Esse aqui é o pega mais silencioso da integração

  1. Leia o campo reasoning_content

No modo thinking, o raciocínio volta em reasoning_content, no MESMO nível do content dentro da mensagem do assistant

O erro comum deste passo: procurar o raciocínio dentro do content e concluir que "não veio nada"

  1. Se houve tool call, guarde e devolva esse reasoning_content nas requisições seguintes

A ausência dele na remontagem do contexto gera erro 400

messages.append({
    "role": "assistant",
    "content": choice.message.content,
    "reasoning_content": choice.message.reasoning_content,
    "tool_calls": choice.message.tool_calls,
})

O erro comum deste passo: montar o histórico só com role, content e tool_calls, que é exatamente o helper que todo mundo escreve no primeiro dia

A conversa funciona no turno 1 e morre no primeiro follow-up depois de uma tool

  1. Sem tool call no meio, o reasoning_content intermediário pode ficar de fora

Quando não houve tool call entre duas mensagens do usuário, o raciocínio intermediário não precisa entrar na concatenação do contexto

O erro comum deste passo: guardar TUDO pra sempre e inflar o histórico sem necessidade

A janela é de 1.000.000 de tokens, mas contexto entupido também é contexto pior

Quando a chamada falha: sintomas conhecidos e o que fazer

Agora a parte que ninguém coloca no tutorial bonitinho

Tem comportamento reportado em issue ABERTA no repositório oficial da DeepSeek, e eu não achei changelog dizendo que foi corrigido no 0813

Então o certo é você programar defensivamente, não confiar na sorte

Sintoma: a chamada de função vem como texto puro dentro do content

O finish_reason volta stop, o tool_calls volta nulo e a "chamada" está escrita ali no meio do texto, de forma intermitente

É o que descreve a issue #1244

Contorno: nunca assuma que stop significa resposta final útil

Se teu fluxo exigia tool e veio stop com tool_calls nulo, trata como falha e repete a chamada

Sintoma: tool_choice required ou de função específica retornando 400

Reportado na issue #1376 pro V4

Contorno: não faça a tua arquitetura depender de forçar a ferramenta por tool_choice

Deixa o roteamento explícito no prompt e valide o resultado depois

Sintoma: resposta vazia depois de devolver o resultado da tool

Acontece em cenário de streaming com function calling, conforme a issue #1453

Contorno: se o retorno final vier vazio, isso é erro, não é "o modelo não tinha nada a dizer"

Sintoma: content vazio no JSON Output

Esse a própria documentação avisa que pode acontecer ocasionalmente, e a orientação oficial é ajustar o prompt

Juntando tudo, o teu guardrail mínimo é esse:

def resposta_valida(choice, exige_tool=False):
    if exige_tool and choice.finish_reason != "tool_calls":
        return False
    if choice.finish_reason == "length":
        return False
    if not choice.message.content and not choice.message.tool_calls:
        return False
    return True

Três regras que salvam a tua integração:

  • valide o finish_reason SEMPRE, nunca só o texto
  • trate content vazio como falha, não como resposta
  • tenha caminho de repetição, com o prompt ajustado quando o padrão de erro se repetir

Próximo passo para colocar isso no seu agente

O contrato que a tua integração precisa assumir é bem direto: a resposta pode vir como texto, como tool call ou como falha silenciosa, e o teu código tem que distinguir os três antes de qualquer json.loads

O caminho de adoção que faz sentido é em escada

Começa pelo JSON Output com a palavra json e o exemplo no prompt, que resolve extração e classificação com pouquíssimo esforço

Sobe pra tool calling quando o agente precisar executar ação de verdade, lendo message.tool_calls e devolvendo role tool

E migra pro strict mode Beta quando o schema deixar de ser preferência e virar requisito, com validação no servidor

E tem um caminho a mais pra avaliar: além do Chat Completions compatível com OpenAI, a DeepSeek documenta uma Responses API própria

Se você está começando integração nova agora, vale ao menos ler antes de fechar a arquitetura

até o próximo post! =)

Perguntas frequentes

Como habilitar o strict mode no tool calling do DeepSeek V4 Pro?

O strict mode é um recurso Beta, então você precisa apontar o base_url para https://api.deepseek.com/beta e marcar "strict": true em cada function do array tools. Com isso ligado, o servidor valida o JSON Schema enviado e retorna erro se o schema estiver fora do padrão ou usar um tipo não suportado. Os tipos aceitos são object, string, number, integer, boolean, array, enum e anyOf, com suporte a $defs e $ref, inclusive para estruturas recursivas.

Por que a API do DeepSeek V4 Pro retorna erro 400 depois de um tool call no modo thinking?

Porque no modo thinking o raciocínio vem no campo reasoning_content, no mesmo nível de content, e esse campo precisa ser devolvido no contexto das requisições seguintes quando houve tool call. Se a integração remonta o histórico sem o reasoning_content do assistant, a API responde 400. Quando não houve tool call entre duas mensagens do usuário, esse campo intermediário pode ficar de fora da concatenação sem problema.

O que significa cada valor possível do finish_reason no tool calling do DeepSeek?

O finish_reason volta tool_calls quando o modelo decide chamar uma ferramenta, e os outros valores possíveis são stop, length, content_filter e insufficient_system_resource. Vale checar esse campo antes de tentar ler message.tool_calls, porque só faz sentido processar chamada de função quando o valor é tool_calls. Tratar qualquer outro valor como se fosse chamada de ferramenta é fonte comum de bug silencioso.

O tool_choice required funciona no DeepSeek V4 Pro?

Há uma issue aberta no repositório oficial da DeepSeek (#1376) relatando que o tool_choice required e o tool_choice apontando para uma função específica retornam erro 400. Isso é comportamento reportado, não documentação oficial confirmando suporte. Se a tua integração depende de forçar uma chamada de ferramenta específica, vale testar antes de assumir que vai funcionar em produção.

Por que o DeepSeek V4 Pro às vezes escreve a chamada de função como texto em vez de preencher tool_calls?

Existe uma issue aberta no repositório oficial da DeepSeek (#1244) relatando que o modelo, de forma intermitente, escreve a chamada de função como texto puro dentro de content, com finish_reason stop e tool_calls nulo. Ou seja, o parser não pode assumir que toda intenção de chamar ferramenta sempre vem estruturada. É um motivo a mais pra validar a resposta antes de seguir o fluxo do agente.

Function calling em streaming pode dar resposta vazia no DeepSeek V4 Pro?

Sim, há uma issue aberta no repositório oficial da DeepSeek (#1453) relatando resposta vazia do deepseek-v4-pro depois que o resultado da ferramenta é devolvido, especificamente em cenário de streaming com function calling. Se a tua aplicação depende de streaming com tools, vale ter um tratamento pra esse caso antes de colocar em produção.



Escrito por | Matheus Battisti

Matheus Battisti
Fundador da Hora de Codar

Programador apaixonado pelo mundo das tecnologias, sempre buscando em aprender e se aprofundar em linguagens, frameworks e o que mais for necessário para executar um bom trabalho. Agora tem uma nova missão que é de passar seu conhecimento adiante para formar novos programadores e especializar mais os que já são.

Subscribe
Notify of
guest

0 Comentários
Oldest
Newest Most Voted

Formações

Formação Vibe Coding

Formação Vibe Coding

Do Prompt ao Produto: Crie Software Real com IA

  • 474 aulas
  • 20 projetos
  • 39h 27min

Blog | Mais populares