Agent IA

DeepSeek V4 multi-tour 400 : reproduire au plus court

MacHTML Lab2026.08.18 ~17 min de lecture
DeepSeek V4 multi-tour 400 : reproduire au plus court

Symptôme : le premier function calling aboutit, puis le tour suivant renvoie une erreur 400 DeepSeek V4 multi-tour.

Solution la plus rapide : créez deux reproductions HTTP indépendantes. Pour l’API officielle, conservez reasoning_content dans chaque message assistant qui a déclenché un outil. Pour vLLM, vérifiez le champ reasoning réellement produit par votre version et ne le renommez pas automatiquement.

Cet article s’adresse aux développeurs d’Agents qui doivent fournir une preuve reproductible à l’équipe modèle ou au mainteneur du framework, aux ingénieurs backend qui cherchent à localiser la panne entre client, passerelle et endpoint, ainsi qu’aux équipes plateforme qui préparent une régression sur deux runtimes.

Dernière mise à jour : 18 août 2026. Les points de contrat ont été vérifiés dans la documentation DeepSeek du thinking mode et des Chat Completions, dans la documentation vLLM sur les sorties de raisonnement, le parseur DeepSeek V4 et la recette de déploiement DeepSeek V4 Flash. (api-docs.deepseek.com)

Le même nom de modèle ne garantit pas le même contrat

Avant de modifier votre boucle Agent, notez le base_url, le modèle demandé, le proxy éventuel et la version du serveur. Un nom comme deepseek-v4-pro ou DeepSeek-V4-Flash ne suffit pas à déterminer le protocole réellement appliqué.

Vous devez traiter séparément :

Élément à vérifier API officielle DeepSeek Endpoint vLLM auto-hébergé
Adresse cible URL officielle configurée dans votre client Adresse du serveur vLLM ou de votre passerelle
Champ de raisonnement à observer reasoning_content reasoning dans la documentation actuelle vLLM
Appel d’outil tool_calls au format Chat Completions Format OpenAI compatible, dépendant du parseur activé
Rejeu après outil reasoning_content conservé dans l’assistant Contrat défini par la version et le template utilisés
Paramètres critiques thinking mode, tools, messages --reasoning-parser, --tool-call-parser, auto tool choice
Preuve acceptable réponse et erreur de l’API officielle réponse réelle et configuration du déploiement

La documentation DeepSeek indique que, lorsqu’un tour en mode réflexion effectue un appel d’outil, le champ reasoning_content doit être renvoyé dans les requêtes suivantes. Une omission peut provoquer une réponse 400. (api-docs.deepseek.com)

vLLM suit une autre convention de sortie. Sa documentation de raisonnement décrit le champ reasoning, et son parseur DeepSeek V4 reconstruit notamment content, reasoning et tool_calls. Cette différence ne prouve pas que l’API DeepSeek acceptera reasoning en entrée. (docs.vllm.ai)

Les coûts cachés sont généralement ailleurs :

  • un SDK peut convertir un objet assistant en dictionnaire incomplet ;
  • un journal de conversation peut supprimer les champs considérés comme sensibles ;
  • une passerelle OpenAI compatible peut normaliser reasoning et reasoning_content sans connaître la cible ;
  • un serveur vLLM peut utiliser un parseur ou un template différent de celui attendu ;
  • un test qui vérifie uniquement le statut 200 ne prouve pas que les identifiants d’appel et l’ordre des messages sont corrects.

Ne comparez donc pas deux payloads après passage dans le même adaptateur. Comparez d’abord les requêtes HTTP réellement envoyées.

Première étape : préparer deux reproductions qui ne se contaminent pas

Utilisez un seul outil sans effet de bord. Par exemple, l’outil get_build_status peut retourner une valeur fixe et ne doit ni modifier une base ni lancer une tâche réelle.

Préparez exactement les mêmes éléments fonctionnels pour les deux tests :

{
  "tools": [
    {
      "type": "function",
      "function": {
        "name": "get_build_status",
        "description": "Retourne l'état d'une compilation.",
        "parameters": {
          "type": "object",
          "properties": {
            "job_id": { "type": "string" }
          },
          "required": ["job_id"],
          "additionalProperties": false
        }
      }
    }
  ],
  "messages": [
    {
      "role": "user",
      "content": "Vérifiez l'état de la compilation job-17."
    }
  ]
}

Conservez séparément les variables suivantes :

export BASE_URL="https://api.example.test"
export MODEL="deepseek-v4-pro"
export API_KEY="redacted"

Dans votre dossier de reproduction, enregistrez :

01-request-first.json
02-response-first.json
03-request-with-tool.json
04-response-second.json
05-request-without-reasoning.json
06-error.json

Ne consignez jamais la clé, le texte complet du raisonnement, les données utilisateur ou le résultat réel d’un outil. Remplacez ces valeurs par des marqueurs de même type. L’objectif est de conserver la structure, pas de publier une conversation privée.

Votre première table de décision doit rester factuelle :

Si vous observez… Hypothèse prioritaire Vérification immédiate
400 après le premier outil uniquement sur l’API officielle reasoning_content absent ou altéré Comparer le message assistant brut et le message rejoué
reasoning dans la réponse vLLM Contrat vLLM appliqué Lire la version, le parseur et le template
tool_call_id différent entre assistant et tool Association de messages rompue Vérifier les identifiants avant sérialisation
200 sans tool_calls côté vLLM Parseur ou auto tool choice incomplet Vérifier les options de service et le format généré
400 sur les deux endpoints Payload commun invalide Retirer le proxy et tester directement chaque endpoint

Deuxième étape : déclencher un appel d’outil avec HTTP brut

Pour l’API officielle, adaptez l’URL et les paramètres à votre contrat documenté. Le point important est de ne pas laisser un framework ajouter automatiquement tool_choice, des champs de raisonnement ou une structure de message que vous ne contrôlez pas.

Exemple volontairement minimal :

curl "$BASE_URL/v1/chat/completions" \
  -H "Authorization: Bearer $API_KEY" \
  -H "Content-Type: application/json" \
  --data-binary @01-request-first.json \
  | tee 02-response-first.json

Dans le fichier de réponse, conservez uniquement les éléments nécessaires à la reconstruction :

{
  "id": "redacted-response-id",
  "choices": [
    {
      "finish_reason": "tool_calls",
      "message": {
        "role": "assistant",
        "content": "Je vérifie l'état de la compilation.",
        "reasoning_content": "[raisonnement supprimé]",
        "tool_calls": [
          {
            "id": "call_redacted",
            "type": "function",
            "function": {
              "name": "get_build_status",
              "arguments": "{\"job_id\":\"job-17\"}"
            }
          }
        ]
      }
    }
  ]
}

Le finish_reason, l’identifiant de réponse et l’identifiant tool_calls sont utiles pour l’enquête. Ils ne remplacent pas le contrat du message assistant. La documentation DeepSeek décrit reasoning_content, content et tool_calls comme des champs distincts du message assistant. (api-docs.deepseek.com)

Pour vLLM, envoyez le même scénario à votre endpoint local ou privé, mais archivez également la commande de lancement. La recette DeepSeek V4 Flash montre notamment l’usage d’un parseur d’outil DeepSeek V4 et d’un parseur de raisonnement DeepSeek V4 ; ces options appartiennent au déploiement vLLM, pas à l’API officielle. (recipes.vllm.ai)

Réponse assistant : conserver la structure, pas le texte sensible

Le point le plus souvent mal diagnostiqué est la phrase « nous avons bien conservé la réponse ». Il faut distinguer trois niveaux :

  1. la réponse JSON reçue sur le réseau ;
  2. l’objet interne après désérialisation ;
  3. le message ajouté à l’historique avant le second appel.

Pour l’API officielle, le message assistant du tour d’outil doit être reconstruit avec ses champs pertinents :

{
  "role": "assistant",
  "content": "Je vérifie l'état de la compilation.",
  "reasoning_content": "[raisonnement supprimé]",
  "tool_calls": [
    {
      "id": "call_redacted",
      "type": "function",
      "function": {
        "name": "get_build_status",
        "arguments": "{\"job_id\":\"job-17\"}"
      }
    }
  ]
}

Ajoutez ensuite le résultat minimal :

{
  "role": "tool",
  "tool_call_id": "call_redacted",
  "content": "{\"status\":\"success\"}"
}

Ne placez pas reasoning_content dans le message tool. Ne le transformez pas en content. Ne le recopiez pas sous le nom reasoning si la destination est l’API DeepSeek officielle.

Ajoutez des assertions avant l’envoi :

assert assistant["role"] == "assistant"
assert assistant.get("tool_calls")
assert assistant["tool_calls"][0]["id"] == tool_result["tool_call_id"]
assert "reasoning_content" in assistant
assert "content" in assistant

Si vous masquez le raisonnement dans les journaux, faites-le uniquement dans la copie destinée à la journalisation. La copie envoyée au serveur doit conserver la structure exigée par le endpoint. Le masquage des logs, la conversion en dictionnaire et la logique de confidentialité sont des couches applicatives ; ils ne modifient pas le contrat de l’API.

Troisième étape : fabriquer l’échec, puis le succès

Construisez deux demandes presque identiques. Cette paire est plus probante qu’un simple « cela fonctionne maintenant ».

Échantillon d’échec

{
  "messages": [
    {
      "role": "user",
      "content": "Vérifiez l'état de la compilation job-17."
    },
    {
      "role": "assistant",
      "content": "Je vérifie l'état de la compilation.",
      "tool_calls": [
        {
          "id": "call_redacted",
          "type": "function",
          "function": {
            "name": "get_build_status",
            "arguments": "{\"job_id\":\"job-17\"}"
          }
        }
      ]
    },
    {
      "role": "tool",
      "tool_call_id": "call_redacted",
      "content": "{\"status\":\"success\"}"
    }
  ]
}

Échantillon conforme pour l’API officielle

{
  "messages": [
    {
      "role": "user",
      "content": "Vérifiez l'état de la compilation job-17."
    },
    {
      "role": "assistant",
      "content": "Je vérifie l'état de la compilation.",
      "reasoning_content": "[raisonnement supprimé]",
      "tool_calls": [
        {
          "id": "call_redacted",
          "type": "function",
          "function": {
            "name": "get_build_status",
            "arguments": "{\"job_id\":\"job-17\"}"
          }
        }
      ]
    },
    {
      "role": "tool",
      "tool_call_id": "call_redacted",
      "content": "{\"status\":\"success\"}"
    }
  ]
}

Envoyez d’abord l’échantillon d’échec, capturez le corps JSON complet de l’erreur, puis envoyez l’échantillon conforme. Si les deux résultats diffèrent, vous avez isolé un problème de contrat. Si les deux échouent, vérifiez le modèle, le mode de réflexion, les paramètres non pris en charge et le base_url.

Cette méthode explique pourquoi un premier appel réussi ne suffit pas. Le premier appel ne teste pas encore la capacité de votre client à rejouer l’assistant après un appel d’outil. C’est le second POST qui révèle la perte de champ, le mauvais nom ou le mauvais ordre.

API officielle et vLLM : comparaison sans règle de remplacement globale

La comparaison suivante sert à localiser la frontière d’adaptation. Elle ne constitue pas une règle selon laquelle un champ pourrait toujours remplacer l’autre.

Question API officielle DeepSeek vLLM actuel
Champ de raisonnement documenté reasoning_content reasoning
Message à rejouer après un outil Assistant complet, dont reasoning_content pour les tours concernés Dépend de la version, du parseur et du template
Parsing des appels Contrat API DeepSeek Parseur activé au lancement
Compatibilité d’entrée À vérifier dans le schéma DeepSeek ciblé À vérifier dans le protocole OpenAI compatible et le code de version
Risque principal Omission de reasoning_content entraînant 400 Confusion entre sortie structurée et champ d’entrée accepté

La documentation vLLM actuelle précise que le parseur DeepSeek V4 extrait un champ reasoning et des appels d’outil au format structuré. Le code d’encodage vLLM traite également reasoning comme une donnée de message interne. Cela ne permet pas d’inférer que DeepSeek acceptera ce même nom dans un historique destiné à son API. (docs.vllm.ai)

Concevez donc un modèle interne neutre :

{
  "role": "assistant",
  "summary": "...",
  "private_reasoning": "[redacted]",
  "tool_calls": []
}

À la sortie :

  • pour DeepSeek, mappez private_reasoning vers reasoning_content seulement lorsque le contrat l’exige ;
  • pour vLLM, mappez-le vers reasoning uniquement si la version et le template le prévoient ;
  • pour un endpoint sans raisonnement rejouable, refusez la conversion silencieuse et produisez une erreur explicite.

Une substitution globale de reasoning_content par reasoning peut réparer un test vLLM tout en cassant l’API officielle. L’adaptateur doit être unidirectionnel, associé à un base_url connu et couvert par des tests.

Questions fréquentes sur le diagnostic HTTP

La première erreur apparaît-elle toujours au deuxième tour ?
Non. Le deuxième tour est le cas le plus visible, mais une passerelle peut supprimer le champ dès la première désérialisation, puis l’erreur n’apparaître qu’après un nouvel appel. Journalisez donc la réponse réseau, l’objet interne et le JSON final séparément.

Faut-il conserver le raisonnement d’un tour sans outil ?
Ne généralisez pas la règle des tours sans outil aux tours avec outil. La documentation DeepSeek distingue ces deux situations : pour un tour qui effectue un appel d’outil, le raisonnement doit être renvoyé dans les requêtes suivantes. (api-docs.deepseek.com)

Que faire si vLLM renvoie une liste vide de tool_calls ?
Vérifiez le parseur d’appel, le parseur de raisonnement, l’activation de l’auto-sélection d’outil, le template de conversation et le format réellement généré par le modèle. Une réponse textuelle contenant une pseudo-invocation n’est pas équivalente à un tableau tool_calls structuré.

Dernière étape : transformer la reproduction en régression

Une fois l’origine identifiée, ne supprimez pas le payload d’échec. Il doit devenir un test négatif stable. Votre suite minimale doit contenir quatre scénarios :

  • conversation sans outil ;
  • appel d’un outil unique ;
  • deux appels d’outil consécutifs dans le même tour ;
  • bascule d’un endpoint à l’autre avec adaptation explicite.

Pour chaque scénario, vérifiez :

[ ] L’URL de base correspond à l’endpoint attendu
[ ] Le modèle envoyé correspond à la cible réellement testée
[ ] Le message assistant conserve les champs requis
[ ] Chaque tool_call_id correspond au message tool associé
[ ] L’ordre user → assistant → tool est conservé
[ ] Le corps d’erreur est archivé lorsqu’un 400 est attendu
[ ] Le payload final est comparé après sérialisation
[ ] L’échantillon ne contient aucune clé ni donnée privée
[ ] Les tests API officielle et vLLM restent indépendants
[ ] Une modification de parseur déclenche la suite complète

Le critère de correction n’est pas « tous les tests retournent 200 ». Il est plus précis : le cas conforme doit passer sur chaque endpoint prévu, le cas volontairement incomplet doit échouer de manière stable, et les journaux doivent montrer le champ qui a changé.

Pour vos tickets d’incident, joignez le base_url anonymisé, la version du serveur, les options de lancement, les cinq champs de réponse utiles et les deux payloads comparés. La documentation d’aide MacHTML peut servir de point de départ si vous devez préparer un environnement de test isolé, tandis que la console MacHTML convient à une exécution où le script HTTP, les journaux et les versions doivent rester séparés de votre poste de développement.

Quand remplacer le poste local par un environnement Mac isolé

Un poste local est acceptable pour une reproduction ponctuelle. Il devient moins fiable lorsque vous devez conserver deux endpoints, plusieurs versions de vLLM, des fichiers de payload anonymisés et une procédure de retour arrière. Les limites habituelles sont concrètes : environnement CUDA difficile à reproduire sur macOS, dépendances globales qui contaminent les essais et absence de séparation entre les tests d’un développeur et ceux de la plateforme.

Dans ce cas, louer un environnement Mac auprès de MacHTML peut être plus pertinent pour les tests d’intégration, les scripts HTTP, les projets audio ou vidéo qui doivent rester proches de l’écosystème Apple, et les validations qui nécessitent un poste propre pendant quelques jours. Ce n’est pas le meilleur choix pour un service de production à charge lourde et permanente, ni pour une équipe qui exige directement des accélérateurs NVIDIA ou des interfaces matérielles spécifiques. Pour un besoin temporaire de reproduction, de comparaison et de retour arrière, l’environnement isolé évite surtout de confondre un problème de contrat API avec un problème de machine.

Commencez par conserver vos deux chaînes HTTP indépendantes. Si votre machine ne peut pas garder cet état propre assez longtemps, utilisez un environnement Mac distant dédié, puis rejouez exactement le même paquet de tests avant et après chaque changement de framework, de parseur ou de modèle.

FAQ

Pourquoi le premier appel d’outil fonctionne-t-il avant l’erreur 400 ?+
Le premier appel peut fonctionner parce que le modèle n’a encore reçu aucun historique assistant à rejouer. L’échec apparaît souvent au tour suivant, lorsque votre client conserve content et tool_calls mais supprime reasoning_content, le renomme ou le sérialise dans une structure non reconnue. Le serveur détecte alors une séquence d’outil incomplète.
Où faut-il placer reasoning_content dans l’appel suivant ?+
Pour un tour de thinking mode qui contient un appel d’outil, reasoning_content doit rester dans le message assistant correspondant, au même niveau que content et tool_calls. Après ce message, ajoutez le message tool avec son tool_call_id, puis envoyez l’ensemble au même endpoint DeepSeek. Ne placez pas ce champ dans le message tool.
La sortie reasoning de vLLM peut-elle être renvoyée telle quelle à DeepSeek ?+
Non, pas par défaut. vLLM expose actuellement reasoning comme champ de sortie dans ses interfaces de raisonnement, tandis que l’API DeepSeek documente reasoning_content pour ses tours d’outil en mode réflexion. Vous devez convertir explicitement le modèle interne vers le contrat de l’endpoint cible, puis vérifier le résultat avec une requête brute.
Comment isoler l’erreur sans OpenAI SDK ?+
Utilisez curl ou une bibliothèque HTTP minimale, un seul outil sans effet de bord et trois journaux séparés : réponse brute, messages envoyés et erreur HTTP. Rejouez d’abord la réponse assistant sans modification, puis retirez uniquement le champ suspect. Cette méthode permet de distinguer une erreur de contrat d’un problème de logique Agent.

Reproduisez vos tests API dans un environnement Mac fiable

Louez un Mac à distance avec MacHTML pour isoler vos scénarios HTTP et reproduire vos erreurs dans des conditions maîtrisées. Accédez à une machine dédiée depuis votre navigateur afin de vérifier chaque requête, chaque réponse et chaque étape d’un échange multitou​​r. Profitez d’un environnement Mac prêt à l’emploi pour vos tests techniques, sans installer de SDK ni de framework superflu. Choisissez la formule MacHTML adaptée à vos besoins et lancez rapidement vos expérimentations, validations et débogages à distance.

Louer un Mac mini cloud
Mac cloud Apple Silicon