AIAgent

DeepSeek V4 deuxième tour 400 : journaliser la requête

DeepSeek V4 deuxième tour 400 : journaliser la requête

Ne relancez pas immédiatement la requête : comparez d’abord la même conversation à chaque étape, en conservant reasoning_content pour l’API officielle DeepSeek et en convertissant explicitement vers reasoning pour vLLM. Cette méthode s’applique lorsque le premier function calling réussit, puis que le 400 apparaît après l’ajout du résultat d’outil.

Cet article s’adresse aux développeurs d’agents qui maintiennent une boucle d’appels d’outils, aux équipes backend responsables du stockage ou de la passerelle API, ainsi qu’aux ingénieurs qui alternent entre l’API DeepSeek et un endpoint vLLM.

Dernière mise à jour : 15 août 2026. Les éléments ont été vérifiés à partir de la documentation officielle DeepSeek sur le mode réflexion, de la référence officielle de création de chat completion et de la documentation vLLM sur les sorties de raisonnement.

Premier tour accepté, deuxième tour rejeté

Le premier indice est la position exacte du 400 dans la boucle. Si le modèle a déjà produit un tool_calls valide, le serveur a probablement accepté la définition de l’outil, le schéma JSON et les paramètres principaux. Lorsque l’erreur arrive seulement après le retour du résultat, la recherche doit commencer dans l’historique renvoyé au modèle.

Le cas minimal ressemble à ceci :

[
  {
    "role": "user",
    "content": "Consultez le statut de la commande."
  },
  {
    "role": "assistant",
    "content": null,
    "reasoning_content": "[désensibilisé]",
    "tool_calls": [
      {
        "id": "call_redacted",
        "type": "function",
        "function": {
          "name": "get_order_status",
          "arguments": "{\"order_id\":\"redacted\"}"
        }
      }
    ]
  },
  {
    "role": "tool",
    "tool_call_id": "call_redacted",
    "content": "[résultat désensibilisé]"
  }
]

Le point à conserver est le message assistant complet. Pour l’API DeepSeek en mode réflexion, un tour qui contient un appel d’outil doit réinjecter intégralement reasoning_content dans les requêtes suivantes. La documentation officielle précise qu’une omission peut entraîner une réponse HTTP 400. (documentation DeepSeek sur le mode réflexion)

Il ne faut donc pas déduire que le problème vient d’un outil « invalide » simplement parce que la seconde requête échoue. Le bon diagnostic dépend de la différence entre quatre états :

  • la réponse brute reçue du modèle ;
  • l’objet interne créé par le SDK ou le cadre Agent ;
  • la version relue depuis la base de données ou la file de messages ;
  • le corps HTTP finalement envoyé.

Une nouvelle tentative sans modification reproduit le même payload fautif. Une augmentation de capacité ne restaure pas un champ supprimé par sérialisation. Un remplacement global de reasoning_content par reasoning peut même casser l’endpoint officiel.

Outil de décision par signature d’erreur

Utilisez cette grille avant de modifier le code :

  • Si le premier tour ne produit déjà aucun tool_calls, vérifiez la définition de l’outil, le schéma des arguments et la configuration du modèle. Le problème n’est probablement pas la restitution du raisonnement.
  • Si le premier tour produit tool_calls, mais que le payload suivant ne contient aucun champ de raisonnement, recherchez une perte dans le SDK, le stockage, la passerelle ou la reconstruction de messages.
  • Si reasoning_content est présent et que la destination est l’API officielle DeepSeek, contrôlez ensuite l’ordre assistanttool, le tool_call_id et les paramètres du mode réflexion.
  • Si la destination est vLLM et que le serveur documente reasoning, appliquez une conversion explicite depuis le modèle interne. Ne supposez pas que l’ancien nom est accepté par la version installée.
  • Si le champ est visible avant la passerelle mais absent avant l’appel HTTP, le correctif doit être placé dans la sérialisation ou la passerelle, pas dans le modèle.
  • Si le champ est présent dans le payload final et que le 400 persiste, rejouez la session sans paramètres optionnels, puis vérifiez la chaîne des messages et le contrat exact de l’endpoint.

Liste de contrôle avant toute modification

Cochez chaque point avec le même identifiant de conversation. Une case non validée indique la prochaine couche à inspecter :

  • [ ] La réponse brute du premier tour contient bien tool_calls.
  • [ ] Le message assistant conservé possède encore reasoning_content ou reasoning.
  • [ ] Le message tool reprend exactement le tool_call_id émis par l’assistant.
  • [ ] L’ordre des messages reste assistant puis tool, avant la nouvelle requête.
  • [ ] La lecture depuis la base de données conserve les mêmes champs que l’objet écrit.
  • [ ] Le consommateur de file ne reconstruit pas le message avec une liste blanche incomplète.
  • [ ] La passerelle laisse passer le champ correspondant à l’endpoint choisi.
  • [ ] L’API officielle reçoit reasoning_content, et vLLM reçoit le champ prévu par sa version.
  • [ ] Le payload final a été journalisé sous forme désensibilisée, avec les champs et les index.
  • [ ] Le test direct et le test à travers toute la chaîne donnent le même résultat.

La décision de correction suit alors une règle simple : si la première case non validée se situe avant le routage, corrigez la conservation du message ; si elle apparaît au moment du routage, corrigez l’adaptateur ; si toutes les cases sont validées, isolez les paramètres et la chaîne assistanttool avant de modifier l’infrastructure.

Cette liste évite trois faux remèdes : augmenter les ressources, répéter la requête et copier les deux noms de champs dans chaque objet.

Champ absent dans le payload final

Lorsque le dernier journal ne contient ni reasoning_content ni reasoning, la piste prioritaire est la perte de données avant l’envoi. Plusieurs mécanismes produisent ce symptôme.

Réponse réduite au contenu visible

Certains clients ne sauvegardent que content et tool_calls. Cette réduction est acceptable pour une conversation sans appel d’outil, mais elle est insuffisante en mode réflexion avec DeepSeek. La réponse assistant doit être conservée avec ses champs complémentaires, même si content est vide ou nul.

Le code de reconstruction doit rester explicite :

assistant_message = {
    "role": "assistant",
    "content": response_message.content,
    "reasoning_content": response_message.reasoning_content,
    "tool_calls": response_message.tool_calls,
}

Il faut ensuite ajouter le résultat d’outil avec le même identifiant :

tool_message = {
    "role": "tool",
    "tool_call_id": response_message.tool_calls[0].id,
    "content": "[résultat désensibilisé]",
}

La spécification DeepSeek des messages de chat décrit reasoning_content comme le contenu de raisonnement du message assistant en mode réflexion, au même niveau que content. Elle distingue également ce champ de tool_calls, qui porte l’appel de fonction lui-même.

Le raisonnement ne doit pas être confondu avec le résultat de l’outil. Le premier appartient au message assistant généré avant l’exécution. Le second devient un nouveau message tool. Supprimer l’un pour ne conserver que l’autre produit une conversation apparemment lisible, mais contractuellement incomplète.

Reconstruction par liste blanche

Un sérialiseur peut garder uniquement les propriétés connues d’un ancien schéma :

allowed = {"role", "content", "tool_calls", "tool_call_id"}
out = {key: value for key, value in message.items() if key in allowed}

Cette logique supprime silencieusement reasoning_content. Le journal doit alors afficher la liste des champs avant et après chaque conversion :

def trace(label, messages):
    print({
        "label": label,
        "message_count": len(messages),
        "fields": [sorted(message.keys()) for message in messages],
        "roles": [message.get("role") for message in messages],
    })

Le contenu de raisonnement ne doit pas être imprimé en clair. Les noms de champs, les rôles, les index et les longueurs suffisent pour localiser la perte.

Valeur vide ajoutée par le SDK

Un autre cas fréquent est la transformation d’une propriété absente en null. Une application peut recevoir une réponse avec une valeur non nulle, la convertir en modèle typé, puis produire :

{
  "role": "assistant",
  "content": null,
  "reasoning_content": null,
  "tool_calls": []
}

Ce message ne représente plus le tour qui a réellement généré l’appel. Avant l’envoi, une assertion doit contrôler les invariants :

assistant = messages[assistant_index]

assert assistant["role"] == "assistant"
assert assistant.get("tool_calls")
assert assistant.get("reasoning_content")
assert tool_message["role"] == "tool"
assert tool_message["tool_call_id"] == assistant["tool_calls"][0]["id"]

Ces assertions ne remplacent pas la validation du serveur. Elles empêchent simplement l’application d’envoyer un historique déjà manifestement incomplet.

API DeepSeek ou vLLM : deux contrats distincts

Le piège apparaît lorsque le même objet interne est envoyé sans adaptation à deux endpoints. Le nom du champ n’est pas un détail cosmétique : il fait partie du contrat de transport.

Contrat de l’API officielle DeepSeek

Pour un tour avec outil en mode réflexion, l’API officielle attend la restitution de reasoning_content dans les requêtes suivantes. Le modèle peut produire plusieurs sous-tours : message assistant avec appel, message tool, puis nouveau message assistant avec un autre appel ou une réponse finale. La valeur de raisonnement associée au tour d’outil doit rester dans l’historique. (documentation DeepSeek sur le mode réflexion)

La documentation précise aussi que certains paramètres ne sont pas pris en charge en mode réflexion. temperature, top_p, presence_penalty et frequency_penalty ne doivent donc pas être utilisés comme leviers de correction d’un 400. Un champ de raisonnement manquant et un paramètre incompatible sont deux pistes différentes.

Contrat de vLLM

La documentation actuelle de vLLM expose principalement la sortie de raisonnement sous le champ reasoning. Une documentation de version antérieure indique que reasoning a remplacé reasoning_content, avec une compatibilité annoncée pour l’ancien nom à cette période. Cette compatibilité doit être vérifiée contre la version effectivement déployée ; elle ne constitue pas une garantie permanente. (documentation vLLM d’une version antérieure)

vLLM indique également que l’extraction des appels d’outils porte sur le champ content, et non sur le champ reasoning. Le fait que le raisonnement soit présent ne prouve donc pas que le parseur d’outils a reconnu l’appel. (documentation vLLM sur les sorties de raisonnement)

Le modèle interne peut rester neutre :

internal_message = {
    "role": "assistant",
    "content": content,
    "reasoning": reasoning_value,
    "tool_calls": tool_calls,
}

La couche de sortie choisit ensuite le contrat :

def adapt_for_endpoint(message, endpoint):
    result = {
        "role": message["role"],
        "content": message.get("content"),
        "tool_calls": message.get("tool_calls"),
    }

    if endpoint == "deepseek_api":
        result["reasoning_content"] = message.get("reasoning")

    elif endpoint == "vllm":
        result["reasoning"] = message.get("reasoning")

    return result

L’adaptateur doit également enregistrer le type d’endpoint et la version vLLM. Une chaîne comme provider=deepseek_api ou provider=vllm; version=redacted est plus utile qu’un simple identifiant de modèle, car deux serveurs compatibles avec une même bibliothèque cliente peuvent appliquer des règles différentes.

Décision de routage

Le choix peut être formulé sans ambiguïté :

  • Si la destination est l’API officielle DeepSeek, conserver reasoning_content pour les tours d’outils et vérifier les contraintes du mode réflexion.
  • Si la destination est vLLM, produire le champ reasoning attendu par le protocole de la version déployée et vérifier l’activation du parseur de raisonnement.
  • Si une même conversation peut changer d’endpoint, maintenir une représentation interne unique, puis convertir uniquement au moment de la sortie.
  • Si la compatibilité de l’ancienne propriété vLLM n’est pas documentée pour la version installée, ne pas compter sur un alias implicite ; ajouter un test direct contre l’endpoint.
  • Si le payload final contient les deux champs par défaut, revenir à une conversion conditionnelle. Dupliquer les propriétés partout masque la couche responsable et rend les futures migrations plus difficiles.

La distinction « vLLM reasoning contre reasoning_content » doit donc être traitée comme une adaptation de protocole, pas comme un simple renommage dans toute la base de code.

Champ conservé en mémoire, perdu sur le trajet

Si reasoning_content ou reasoning est visible en mémoire mais absent de la requête finale, l’enquête doit suivre le trajet réel des données.

SDK et sérialisation

Le premier contrôle consiste à journaliser le dictionnaire immédiatement avant l’appel HTTP. Un journal au niveau de la réponse ne suffit pas : le SDK peut reformater l’objet entre-temps.

trace("apres_reponse", messages)
payload = build_payload(messages)
trace("avant_http", payload["messages"])

Il faut comparer :

  • le nombre de messages ;
  • la liste des champs de chaque message ;
  • les positions des rôles ;
  • la présence des identifiants d’appel ;
  • la longueur de reasoning_content ou reasoning ;
  • la présence de content, même vide.

Passerelle API

Une passerelle peut appliquer une liste blanche, nettoyer les valeurs nulles ou reconstruire le JSON. Les règles à contrôler sont notamment :

  • filtrage des propriétés inconnues ;
  • suppression des champs dont la valeur est une chaîne vide ;
  • transformation d’un objet assistant en objet générique ;
  • normalisation des rôles ;
  • limitation de taille appliquée au mauvais champ ;
  • fusion incorrecte des fragments en mode flux.

Le test doit être réalisé avec un payload factice et non avec le raisonnement réel. Une valeur telle que [raisonnement-test] permet de voir si le champ traverse la passerelle sans exposer d’information sensible.

Base de données et file de messages

Le schéma de stockage est souvent la cause la moins visible. Une colonne JSON peut accepter le champ, alors qu’un consommateur plus ancien le supprime en recréant un objet à partir de trois propriétés. La comparaison doit être faite avant l’écriture, après la lecture et après la consommation de la file.

La séquence minimale est la suivante :

  1. recevoir la réponse du modèle ;
  2. enregistrer les noms de champs présents ;
  3. écrire le message dans le stockage ;
  4. relire le même identifiant de conversation ;
  5. publier le message dans la file ;
  6. le reconstruire côté consommateur ;
  7. produire le payload final ;
  8. envoyer la requête à l’endpoint choisi.

Un seul champ de contrôle suffit pour démontrer la conservation :

{
  "conversation_id": "session-redacted",
  "message_index": 1,
  "fields": ["content", "reasoning_content", "role", "tool_calls"],
  "reasoning_length": "[non affichée]"
}

Ce journal ne doit jamais contenir de clé API, de raisonnement intégral, d’argument client ou de résultat métier réel. Pour les projets traitant de l’audio, de la vidéo ou du design, cette précaution est importante : les métadonnées de fichier, les noms de plans et les chemins locaux peuvent eux aussi révéler des informations confidentielles.

Pour gérer les accès aux environnements de test, la console ProxyMac peut être utilisée séparément du mécanisme de journalisation applicatif. Les identifiants d’accès ne doivent jamais être mélangés aux traces de conversation.

Raisonnement correct, chaîne de messages incorrecte

Un champ présent ne clôt pas l’enquête. Le serveur peut encore refuser le payload si les messages sont mal ordonnés ou si un paramètre additionnel ne correspond pas au mode activé.

L’ordre attendu dans une boucle classique est :

  1. message user ;
  2. message assistant contenant tool_calls et le raisonnement requis ;
  3. message tool avec le tool_call_id correspondant ;
  4. nouvelle réponse assistant ou nouveau tool call.

Les contrôles prioritaires sont :

  • chaque tool_call_id doit correspondre à un appel réellement émis ;
  • le message tool doit suivre le message assistant qui a demandé l’outil ;
  • un appel parallèle ne doit pas être fusionné avec le résultat d’un autre appel ;
  • tool_calls ne doit pas être remplacé par une liste vide lors de la relecture ;
  • content ne doit pas être transformé arbitrairement en valeur incompatible avec le schéma du client ;
  • la liste messages ne doit pas perdre le premier assistant lors d’une rotation de contexte.

Il faut aussi isoler les paramètres. Les intégrations tierces rapportent parfois des erreurs autour de tool_choice en mode réflexion, mais ces rapports doivent être associés à une version et à un endpoint précis. La documentation officielle DeepSeek reste la référence pour les paramètres acceptés ; un problème de tool_choice ne doit pas être déduit uniquement de la présence d’un 400. (référence DeepSeek des intégrations d’agents)

La bonne pratique consiste à produire trois relectures du même cas :

  • avec raisonnement conservé et sans paramètre optionnel ;
  • avec raisonnement conservé et tool_choice explicitement retiré ;
  • avec message assistant et message tool strictement reconstruits à partir de la réponse brute.

Si seule la deuxième version réussit, le champ n’était probablement pas le seul problème. Si aucune ne réussit et que le serveur signale explicitement un raisonnement manquant, la perte se situe encore dans la chaîne de transport ou dans l’adaptation de l’endpoint.

Rejouer la session avant de déclarer le correctif

Une réparation fiable doit fonctionner sur une session minimale conservée avant modification. La session contient uniquement :

  • une demande utilisateur neutre ;
  • un appel d’outil factice ;
  • un résultat d’outil désensibilisé ;
  • les messages assistant nécessaires ;
  • les métadonnées d’endpoint et de version.

Le replay doit être effectué dans trois conditions :

  1. connexion directe à l’API officielle DeepSeek ;
  2. connexion directe à la version vLLM réellement déployée ;
  3. passage par le SDK, la passerelle, le stockage et la file utilisés en production.

Chaque exécution doit archiver :

  • le payload avant envoi ;
  • la liste des champs par message ;
  • le type d’endpoint ;
  • la version du client ;
  • la version de vLLM lorsque cet endpoint est utilisé ;
  • le code ou la règle d’adaptation appliquée ;
  • le message d’erreur exact ;
  • la différence entre la version en échec et la version corrigée.

Le correctif doit être localisé. Si la passerelle supprime reasoning_content, la modification appartient à la passerelle ou à son schéma. Si le problème vient du routage vers vLLM, la conversion appartient à l’adaptateur de sortie. Copier systématiquement les deux noms dans tous les objets ne résout pas la cause et peut produire un payload ambigu.

Pour les tests récurrents, la page d’aide ProxyMac peut compléter la documentation interne sur l’accès aux environnements. La politique de confidentialité doit également être consultée lorsque les traces contiennent des données issues de projets clients, de productions vidéo ou de fichiers de conception.

Le choix d’environnement après le diagnostic

Un endpoint autogéré vLLM convient lorsque l’équipe doit contrôler le serveur, la version du moteur, les parseurs et le chemin réseau. En contrepartie, elle doit maintenir les images, les pilotes, les journaux, les règles de compatibilité et la reproductibilité des versions. Un poste local peut être préférable pour une tâche ponctuelle liée à macOS, Xcode, à l’audio ou au design, mais il devient moins pratique lorsqu’il faut laisser un agent accessible en continu ou rejouer une session à distance.

Si le projet reste centré sur une API Linux ou sur un serveur vLLM durablement chargé, la location d’un Mac n’est pas nécessairement le meilleur choix. En revanche, pour une validation temporaire d’un client macOS, une automatisation Xcode, un pipeline créatif audio-vidéo ou une régression nécessitant un environnement Apple stable, ProxyMac offre une alternative plus directe qu’un poste personnel toujours allumé. L’intérêt n’est pas de remplacer l’adaptateur DeepSeek : il est de fournir un environnement reproductible pour vérifier que la correction fonctionne réellement dans le logiciel client et dans les tâches qui l’entourent.

Testez vos intégrations IA sur un Mac distant fiable

Avec ProxyMac, vous disposez d’un Mac distant prêt à l’emploi pour reproduire vos appels API et analyser précisément chaque requête.
Accédez à votre environnement depuis n’importe où grâce à une connexion VNC simple et pratique.