È un errore di ordinamento dei messaggi, non un errore degli strumenti. Claude richiede che ogni blocco tool_use in un turno assistant riceva risposta da un blocco tool_result nel turno user immediatamente successivo — stessi ID e nient'altro in mezzo. Il tuo ciclo ne ha eliminato uno, di solito perché lo strumento ha generato un'eccezione.
L’errore
{
"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."
}
}Cause e soluzioni in breve
| Causa | Soluzione |
|---|---|
| Lo strumento ha generato un'eccezione, quindi non è stato aggiunto nulla | Invia comunque un tool_result, con is_error: true e il testo dell'errore. |
| Il tool_result è finito in un messaggio successivo | Deve essere il messaggio immediatamente successivo: non può esserci alcun turno assistant o user in mezzo. |
| tool_use_id non corrisponde | Ripeti l'ID esatto del blocco tool_use; non generarne uno nuovo. |
| Cronologia ridotta a metà del round-trip | Riduci la cronologia a round-trip completi dello strumento, mai tra le due metà dello stesso round-trip. |
L'invariante, una volta per tutte
Ogni blocco tool_use in un turno assistant richiede esattamente un blocco tool_result nel messaggio user immediatamente successivo, con lo stesso tool_use_id. Più blocchi tool_use nello stesso turno richiedono più blocchi tool_result in quell'unico messaggio successivo. Nulla può essere inserito tra i due turni.
Rispondi sempre, anche quando lo strumento fallisce
Il modello gestisce perfettamente uno strumento fallito; non può gestire uno strumento mancante. Restituire l'errore come tool_result mantiene valida la conversazione e di solito produce un recupero sensato invece di un 400.
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})Convalida gli ultimi due turni prima dell'invio
Una dozzina di righe di asserzioni intercetta il problema nel punto della chiamata invece di ricevere un 400 dalla rete: esamina gli ID tool_use del turno assistant e verifica che il turno user successivo risponda a tutti.
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}"Riduci la cronologia ai confini del round-trip
La riduzione della finestra di contesto per numero di messaggi prima o poi taglierà tra un tool_use e il suo tool_result. Tratta la coppia come un'unità indivisibile quando decidi cosa eliminare.
Se chiami tramite Kunavo
Questo problema è nel tuo payload e Kunavo non lo maschera: 400 è nell'elenco degli errori per cui non si ritenta, quindi un round-trip malformato fallisce una volta invece di consumare un secondo round-trip upstream di latenza per arrivare allo stesso errore, e la richiesta rifiutata viene registrata a costo zero. Su /v1/messages parli direttamente il protocollo Messages, quindi i blocchi tools, tool_use e tool_result vengono inoltrati senza traduzione. In questo caso il 400 restituisce il tipo invalid_request_error, come nell'API Anthropic, con il testo del messaggio upstream seguito dall'ID della richiesta upstream e senza un campo request_id; fino al 24 settembre 2026 il tipo era api_error, quindi se i tuoi log risalgono a prima conviene basarsi sullo stato HTTP e sul testo del messaggio.
Domande frequenti
Posso semplicemente eliminare il turno tool_use invece di rispondervi?
Sì, se elimini l'intero turno assistant. Ciò che non è valido è mantenere tool_use e omettere il suo tool_result.
L'endpoint compatibile con OpenAI ha la stessa regola?
È richiesta la stessa associazione, con una sintassi diversa: tool_calls nel messaggio assistant, poi un messaggio con role: "tool" per ogni chiamata, contenente tool_call_id.
Una richiesta rifiutata ha un costo?
Su Kunavo, no. Le richieste fallite vengono registrate a costo zero e non raggiungono mai una chiamata upstream fatturata.
Guide correlate
- model_not_found / 404 — denominazione dei modelli tra Claude, Gemini e i gateway
- Claude API 429 rate_limit_error — cause e soluzione affidabile
Ulteriori dettagli sul significato degli errori sono disponibili in riferimento agli errori; ottenere una chiave richiede un minuto tramite registrazione e la guida all’autenticazione.