Retour aux guides
Dépannage·28 août 2026·6 min de lecture

API Claude 400 « tool_use ids were found without tool_result blocks » — la règle d’ordre

Il s’agit d’une erreur d’ordre des messages, pas d’une erreur d’outil. Claude exige que chaque bloc tool_use d’un tour assistant reçoive une réponse sous forme de bloc tool_result dans le tour user immédiatement suivant — mêmes identifiants, rien entre les deux. Votre boucle en a omis un, généralement parce que l’outil a levé une exception.

Dernière vérification le .

Il s’agit d’une erreur d’ordre des messages, pas d’une erreur d’outil. Claude exige que chaque bloc tool_use d’un tour assistant reçoive une réponse sous forme de bloc tool_result dans le tour user immédiatement suivant — mêmes identifiants, rien entre les deux. Votre boucle en a omis un, généralement parce que l’outil a levé une exception.

L’erreur

response (HTTP 400)
{
  "type": "error",
  "error": {
    "type": "invalid_request_error",
    "message": "messages.1: tool_use ids were found without tool_result blocks immediately after: toolu_01A... Each tool_use block must have a corresponding tool_result block in the next message."
  }
}

Causes et solutions en bref

CauseSolution
Votre outil a levé une exception, donc rien n’a été ajoutéEnvoyez quand même un tool_result, avec is_error: true et le texte de l’erreur.
Le tool_result est arrivé dans un message ultérieurIl doit se trouver dans le message immédiatement suivant — aucun tour assistant ou user ne doit s’intercaler.
tool_use_id ne correspond pasRenvoyez l’identifiant exact du bloc tool_use ; n’en générez pas un nouveau.
Historique tronqué au milieu d’un aller-retourTronquez uniquement entre des allers-retours complets, jamais entre les deux moitiés d’un même aller-retour.

L’invariant, énoncé une fois pour toutes

Chaque bloc tool_use d’un tour assistant nécessite exactement un bloc tool_result dans le message user immédiatement suivant, avec le même tool_use_id. Si un tour contient plusieurs blocs tool_use, ce message suivant doit contenir plusieurs blocs tool_result. Rien ne doit s’intercaler entre les deux tours.

Répondez toujours, même lorsque l’outil a échoué

Le modèle gère parfaitement un outil en échec ; il ne gère pas un outil manquant. Renvoyer l’erreur sous forme de tool_result maintient la conversation valide et permet généralement une récupération cohérente au lieu d’une erreur 400.

tool_loop.py
results = []
for block in (b for b in resp.content if b.type == "tool_use"):
    try:
        out = run_tool(block.name, block.input)
        results.append({
            "type": "tool_result",
            "tool_use_id": block.id,
            "content": str(out),
        })
    except Exception as e:
        # A failed tool still owes the model an answer.
        results.append({
            "type": "tool_result",
            "tool_use_id": block.id,
            "content": f"Tool failed: {e}",
            "is_error": True,
        })

messages.append({"role": "assistant", "content": resp.content})
messages.append({"role": "user", "content": results})

Validez les deux derniers tours avant l’envoi

Une douzaine de lignes d’assertions détectent le problème au point d’appel plutôt qu’après une erreur 400 du réseau : parcourez les tool_use ids du tour assistant et vérifiez que le tour user suivant répond à chacun d’eux.

validate.py
def check_pairs(messages):
    for i, m in enumerate(messages):
        if m["role"] != "assistant" or not isinstance(m.get("content"), list):
            continue
        ids = {b.get("id") for b in m["content"]
               if isinstance(b, dict) and b.get("type") == "tool_use"}
        if not ids:
            continue
        nxt = messages[i + 1] if i + 1 < len(messages) else None
        answered = {b.get("tool_use_id") for b in (nxt or {}).get("content", [])
                    if isinstance(b, dict) and b.get("type") == "tool_result"}
        missing = ids - answered
        assert not missing, f"message {i}: unanswered tool_use {missing}"

Tronquez l’historique aux frontières des allers-retours

Une troncature de la fenêtre de contexte fondée sur le nombre de messages finira par couper entre un tool_use et son tool_result. Traitez la paire comme une unité indivisible lorsque vous décidez quoi supprimer.

Si vous appelez via Kunavo

Le problème vient de votre contenu, et Kunavo ne le masque pas : 400 figure parmi les erreurs pour lesquelles il ne faut pas réessayer ; une boucle d’outil mal formée échoue donc une seule fois au lieu de consommer un second aller-retour amont pour atteindre la même erreur, et la requête rejetée est enregistrée avec un coût nul. Sur /v1/messages, vous utilisez directement le protocole Messages ; les blocs tools, tool_use et tool_result sont donc transmis sans traduction. Le 400 revient alors typé invalid_request_error, comme dans l’API d’Anthropic, avec le message de l’amont suivi de son propre identifiant de requête et sans champ request_id ; jusqu’au 24 septembre 2026, le type était api_error, alors utilisez le statut HTTP et le texte du message si vos journaux sont antérieurs.

Questions fréquentes

Puis-je simplement supprimer le tour tool_use au lieu d’y répondre ?

Oui, si vous supprimez le tour assistant entier. Ce qui est invalide, c’est de conserver le tool_use et d’omettre son tool_result.

Le point de terminaison compatible OpenAI applique-t-il la même règle ?

Le même appariement est requis, avec une autre syntaxe : tool_calls dans le message assistant, puis un message role: "tool" par appel, contenant tool_call_id.

Une requête rejetée est-elle facturée ?

Sur Kunavo, non. Les requêtes échouées sont enregistrées avec un coût nul et n’atteignent jamais un appel amont facturé.

Guides associés

La sémantique détaillée des erreurs est disponible dans référence des erreurs ; obtenir une clé prend une minute via inscription et la guide d’authentification.