Zurück zum Blog

FehlerbehebungAnleitung

400 nach einem Tool-Call: reasoning_content zurücksenden

Warum der zweite Request einer Tool-Call-Schleife bei einem Thinking-Modell 400 zurückgibt, eine Python-Lösung, die die vollständige Nachricht anhängt, und wann du sie überspringen kannst.

Von CheapTokens TeamVeröffentlicht am 5 Min. Lesezeit

Cover: eine Tool-Call-Schleife, in der reasoning_content von der Assistant-Nachricht in den nächsten Request mitreist
Auf dieser Seite
  1. Was zwischen Request eins und Request zwei passiert
  2. Die Lösung: die Nachricht so anhängen, wie sie zurückkam
  3. Ein häufiger Übeltäter: eine von Hand neu gebaute Nachricht
  4. Wann du das nicht brauchst
  5. Wenn es immer noch 400 zurückgibt

Deine Agent-Schleife funktioniert beim ersten Request und scheitert beim zweiten mit einem 400. Bei einem Thinking-Modell ist eine dokumentierte Ursache ein fehlendes Feld: Die Assistant-Nachricht, die du zurückschickst, hat ihr reasoning_content verloren. Wenn das die Ursache ist, behebt das Anhängen der gesamten Nachricht genau so, wie die API sie zurückgegeben hat, das Problem.

DeepSeeks Thinking-Mode-Guide (Doku, geprüft am 8. November 2026) fasst die Regel in einem Satz zusammen: Wenn der Request den Parameter tools hat, muss reasoning_content in allen folgenden Requests vollständig zurückgegeben werden, auch in Turns ohne Tool-Call. Andernfalls gibt die API 400 zurück. Dieselbe Seite sagt, dass Thinking standardmäßig aktiv ist, mit Effort high. Eine Schleife, die du für ein Nicht-Thinking-Modell geschrieben hast, kann also brechen, ohne dass sich eine einzige Zeile ändert.

Was zwischen Request eins und Request zwei passiert#

Im Thinking-Mode kommt die Gedankenkette in reasoning_content zurück, einem Feld auf derselben Ebene wie content. Stell dir die Schleife vor:

  1. Du sendest die Frage und die Liste tools.
  2. Das Modell antwortet mit tool_calls (und reasoning_content daneben).
  3. Du führst das Tool aus und sendest alles zurück: die Historie, die Assistant-Nachricht und eine tool-Nachricht mit dem Ergebnis.

Das 400 kommt beim Request in Schritt 3, wenn die Assistant-Nachricht darin kein reasoning_content hat.

Drei Schritte einer Tool-Schleife: Request, Assistant-Nachricht mit reasoning_content, zweiter Request
Wo das Feld mitreisen muss: von der Assistant-Nachricht in den nächsten Request

Die DeepSeek-Seite dokumentiert das 400 nur für diesen Tool-Call-Fall und zitiert keinen Fehlertext. Schau also auf den HTTP-Status, nicht auf eine bestimmte Meldung, wenn du prüfst, ob du dieses Problem hast.

Die Lösung: die Nachricht so anhängen, wie sie zurückkam#

Der sicherste Weg ist, die Assistant-Nachricht gar nicht neu zu bauen. Nimm sie aus der Response und hänge sie vollständig an. Hier ist eine vollständige Schleife mit dem OpenAI Python SDK, das auf CheapTokens zeigt. Das Weather-Tool ist ein Stub, du kannst sie also direkt ausführen.

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)),
        })

Die eine Zeile, auf die es ankommt, ist messages.append(msg.model_dump(exclude_none=True)). Das SDK behält Felder, die es nicht kennt, also reist alles, was die API auf die Nachricht gesetzt hat, inklusive reasoning_content, mit ihr zurück. Auch die finale Antwort wird angehängt, was genau der Regel entspricht: Nach dem ersten Tool-Call trägt jeder spätere Request das Reasoning früherer Assistant-Turns mit.

Die Tool-Call-Mechanik selbst (Schema, tool_call_id, Argumente parsen) steht in Function calling in Python with an OpenAI-compatible API. Base-URL, Key und Modell-ID findest du auf der CheapTokens-Dokumentationsseite; das hier verwendete Modell ist auf der DeepSeek-Seite beschrieben: 380K Kontext und bis zu 32.768 Output-Tokens pro Antwort, was Platz für langes Thinking plus eine Antwort lässt.

Ein häufiger Übeltäter: eine von Hand neu gebaute Nachricht#

Ein häufiger Grund, das Feld zu verlieren, ist Code, der nicht das Response-Objekt anhängt, sondern ein neues Dict baut. Es sieht harmlos aus: role, content, tool calls. Frameworks und Wrapper machen dasselbe, wenn sie die Historie in ihrem eigenen Format speichern. Das eine Feld, von dem sie nie gehört haben, ist genau das, das fehlt.

Links: Assistant-Nachricht aus role, content, tool_calls neu gebaut. Rechts: die Nachricht so, wie sie zurückkam, mit reasoning_content
Der Unterschied zwischen den beiden Assistant-Nachrichten ist ein Feld

Vergleichen

Von Hand neu gebaut vs. vollständig angehängt

Von Hand neu gebaut (bricht)

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

Vollständig angehängt (funktioniert)

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

Wenn du ein Framework verwendest und diese Zeile nicht anfassen kannst, prüfe, wie es Assistant-Turns speichert. Eine Historie, die durch einen Serializer geht, der nur content und tool_calls behält, hat denselben Bug, nur eine Ebene tiefer.

Wann du das nicht brauchst#

Nicht jeder Aufruf eines Thinking-Modells braucht die zusätzliche Sorgfalt. Für Multi-Turn-Chat ohne Tools sagt DeepSeeks Guide, dass reasoning_content nicht zurückgegeben werden muss; wenn du es trotzdem sendest, wird es ignoriert und früheres Reasoning nicht zum Kontext hinzugefügt. Ein einfacher Chat kann es also weiterhin weglassen, und nichts bricht.

Wähle deinen Fall

Welcher Fall ist deiner?

Der Request hat tools, das Modell denkt

Hänge in jedem Turn die gesamte Assistant-Nachricht an, bei Tool-Turns ebenso wie bei finalen Antworten.

Einfacher Chat, kein tools-Parameter

Keine Korrektur nötig. Du kannst reasoning_content aus der Historie weglassen oder senden; es wird ignoriert.

Ein Framework baut die Historie für dich

Finde heraus, wo es Assistant-Nachrichten serialisiert, und stelle sicher, dass reasoning_content überlebt. Wenn nicht, rufe die API für den Tool-Teil mit deiner eigenen Schleife auf.

Ein Nebeneffekt, den du im Blick behalten solltest: Die Historie, die du erneut sendest, ist bei jedem Request Input, also summiert sich langes Reasoning in einer langen Schleife. Wie Thinking-Tokens gezählt werden, steht in Reasoning effort and thinking tokens, und was zu tun ist, wenn ein Request die Grenzen des Modells überschreitet, in Request too long: 400 vs 413.

Wenn es immer noch 400 zurückgibt#

Geh das der Reihe nach durch:

  • Gib messages direkt vor dem fehlschlagenden Aufruf aus. Hat jede Assistant-Nachricht, die vom Modell kam, reasoning_content, wenn die Response es hatte?
  • Baust du irgendwo eine Nachricht neu: ein Helper, eine History-Klasse, ein json.dumps und zurück mit einer festen Liste von Keys?
  • Ist die tool-Nachricht für jeden tool_call_id vorhanden, mit derselben id?
  • Ist es wirklich die Länge? Ein Request über dem Kontextlimit ist ein anderes 400, beschrieben im Artikel oben.

Kurzer Check

Was hängst du an, nachdem das Modell ein Tool anfordert?

Antwort anzeigen

Antwort: Die gesamte Assistant-Nachricht so, wie sie zurückkam, inklusive reasoning_content

Die tool_calls sind das, worauf deine Tool-Nachrichten antworten, und wenn Tools im Request sind, muss das Reasoning mit ihnen mitreisen.

Das ist die ganze Lösung: ein Append, kein Neubauen. Füge das Schrittlimit aus dem Beispiel zu jeder Schleife hinzu, die du auslieferst, damit ein schlechter Turn fünf Requests kostet, nicht fünfhundert.