Voltar ao blog

Solução de problemasGuia

400 após uma chamada de ferramenta: reenvie reasoning_content

Por que a segunda requisição de um loop de chamada de ferramenta retorna 400 em um modelo de raciocínio, uma correção em Python que anexa a mensagem inteira e quando você pode pular isso.

Por CheapTokens TeamPublicado em 5 min de leitura

Capa: um loop de chamada de ferramenta em que reasoning_content viaja da mensagem do assistente para a próxima requisição
Nesta página
  1. O que acontece entre a primeira e a segunda requisição
  2. A correção: anexar a mensagem como retornada
  3. Um culpado comum: uma mensagem reconstruída à mão
  4. Quando você não precisa disso
  5. Se ainda retornar 400

Seu loop de agente funciona na primeira requisição e falha na segunda com um 400. Em um modelo de raciocínio, uma causa documentada é um campo ausente: a mensagem do assistente que você envia de volta perdeu seu reasoning_content. Se essa for a causa, anexar a mensagem inteira exatamente como a API a retornou resolve o problema.

O guia do modo de raciocínio do DeepSeek (docs, verificado em 8 de novembro de 2026) resume a regra em uma frase: quando a requisição tem o parâmetro tools, reasoning_content precisa ser reenviado por completo em todas as requisições seguintes, incluindo turnos sem chamada de ferramenta. Caso contrário, a API retorna 400. A mesma página diz que o raciocínio vem ativado por padrão, com esforço high. Então um loop que você escreveu para um modelo sem raciocínio pode quebrar sem que uma única linha mude.

O que acontece entre a primeira e a segunda requisição#

No modo de raciocínio, a cadeia de pensamento volta em reasoning_content, um campo no mesmo nível de content. Imagine o loop:

  1. Você envia a pergunta e a lista tools.
  2. O modelo responde com tool_calls (e reasoning_content ao lado deles).
  3. Você executa a ferramenta e envia tudo de volta: o histórico, a mensagem do assistente e uma mensagem tool com o resultado.

O 400 aparece na requisição do passo 3 se a mensagem do assistente nela não tiver reasoning_content.

Três passos de um loop de ferramentas: requisição, mensagem do assistente com reasoning_content, segunda requisição
Onde o campo precisa viajar: da mensagem do assistente para a próxima requisição

A página do DeepSeek documenta o 400 apenas para este caso de chamada de ferramenta e não cita um texto de erro, então observe o status HTTP, não uma mensagem específica, ao verificar se você tem esse problema.

A correção: anexar a mensagem como retornada#

A atitude mais segura é não reconstruir a mensagem do assistente de forma alguma. Pegue-a da resposta e anexe-a inteira. Aqui está um loop completo com o SDK Python da OpenAI apontado para o CheapTokens. A ferramenta de clima é um stub, então você pode executá-lo como está.

python
import json, os
from openai import OpenAI

client = OpenAI(
    base_url="https://api.cheaptokens.dev/v1",
    api_key=os.environ["CHEAPTOKENS_API_KEY"],
)
MODEL = "deepseek-ai/DeepSeek-V4-Flash-0731"

tools = [{
    "type": "function",
    "function": {
        "name": "get_weather",
        "description": "Current weather for a city",
        "parameters": {
            "type": "object",
            "properties": {"city": {"type": "string"}},
            "required": ["city"],
        },
    },
}]

def get_weather(city):
    return {"city": city, "temp_c": 7}  # stub

messages = [{"role": "user", "content": "Do I need a jacket in Oslo today?"}]

for _ in range(5):  # hard step limit
    resp = client.chat.completions.create(model=MODEL, messages=messages, tools=tools)
    msg = resp.choices[0].message
    messages.append(msg.model_dump(exclude_none=True))  # the whole message
    if not msg.tool_calls:
        print(msg.content)
        break
    for call in msg.tool_calls:
        args = json.loads(call.function.arguments)
        messages.append({
            "role": "tool",
            "tool_call_id": call.id,
            "content": json.dumps(get_weather(**args)),
        })

A única linha que importa é messages.append(msg.model_dump(exclude_none=True)). O SDK mantém campos que não conhece, então tudo o que a API colocou na mensagem, reasoning_content incluído, viaja de volta com ela. A resposta final também é anexada, que é o que a regra pede: após a primeira chamada de ferramenta, toda requisição posterior carrega o raciocínio dos turnos anteriores do assistente.

A mecânica da chamada de ferramenta em si (schema, tool_call_id, parsing de argumentos) está em Function calling in Python with an OpenAI-compatible API. A URL base, a chave e o id do modelo estão na página de docs do CheapTokens; o modelo usado aqui é descrito na página do DeepSeek: contexto de 380K e até 32.768 tokens de saída por resposta, o que deixa espaço para raciocínio longo mais uma resposta.

Um culpado comum: uma mensagem reconstruída à mão#

Uma forma comum de perder o campo é um código que não anexa o objeto da resposta, mas monta um novo dict. Parece inofensivo: role, content, tool calls. Frameworks e wrappers fazem o mesmo quando armazenam o histórico em seu próprio formato. O único campo de que nunca ouviram falar é justamente o que desaparece.

Esquerda: mensagem do assistente reconstruída a partir de role, content, tool_calls. Direita: a mensagem como retornada, com reasoning_content
A diferença entre as duas mensagens do assistente é um campo

Comparar

Reconstruída à mão vs anexada inteira

Reconstruída à mão (quebra)

python
messages.append({
    "role": "assistant",
    "content": msg.content,
    "tool_calls": [c.model_dump() for c in msg.tool_calls],
})

Anexada inteira (funciona)

python
messages.append(msg.model_dump(exclude_none=True))

Se você usa um framework e não consegue tocar nessa linha, verifique como ele armazena os turnos do assistente. Um histórico que passa por um serializador que mantém apenas content e tool_calls tem o mesmo bug, só uma camada abaixo.

Quando você não precisa disso#

Nem toda chamada a um modelo de raciocínio precisa desse cuidado extra. Para chat multi-turno sem ferramentas, o guia do DeepSeek diz que reasoning_content não precisa ser reenviado; se você enviar mesmo assim, ele é ignorado e o raciocínio anterior não é adicionado ao contexto. Então um chat simples pode continuar descartando-o e nada quebra.

Escolha o seu caso

Qual é o seu caso?

A requisição tem tools, o modelo está raciocinando

Anexe a mensagem inteira do assistente a cada turno, tanto turnos de ferramenta quanto respostas finais.

Chat simples, sem o parâmetro tools

Nenhuma correção necessária. Você pode descartar reasoning_content do histórico ou enviá-lo; ele é ignorado.

Um framework monta o histórico para você

Encontre onde ele serializa as mensagens do assistente e garanta que reasoning_content sobreviva. Se não for possível, chame a API com seu próprio loop para a parte de ferramentas.

Um efeito colateral a ter em mente: o histórico que você reenvia é entrada em toda requisição, então raciocínio longo em um loop longo se acumula. Como os tokens de raciocínio são contados está em Reasoning effort and thinking tokens, e o que fazer quando uma requisição cresce além dos limites do modelo está em Request too long: 400 vs 413.

Se ainda retornar 400#

Percorra isto em ordem:

  • Imprima messages logo antes da chamada que falha. Toda mensagem do assistente que veio do modelo tem reasoning_content, se a resposta tinha?
  • Você reconstrói alguma mensagem em algum lugar: um helper, uma classe de histórico, um json.dumps e volta com uma lista fixa de chaves?
  • A mensagem tool para cada tool_call_id está presente, com o mesmo id?
  • É realmente o comprimento? Uma requisição acima do limite de contexto é um 400 diferente, descrito no artigo acima.

Verificação rápida

O que você anexa depois que o modelo pede uma ferramenta?

Mostrar resposta

Resposta: A mensagem inteira do assistente como retornada, reasoning_content incluído

As tool_calls são o que suas mensagens de ferramenta respondem, e com ferramentas na requisição o raciocínio precisa ir junto com elas.

Essa é a correção inteira: um append, sem reconstrução. Adicione o limite de passos do exemplo a qualquer loop que você colocar em produção, para que um turno ruim custe cinco requisições, não quinhentas.