Решение проблемИнструкция
400 после вызова инструмента: верните reasoning_content
Почему второй запрос цикла с инструментами даёт 400 на модели с рассуждением, как исправить код на Python и когда поле возвращать не нужно.

Содержание
Ваш агентный цикл отрабатывает первый запрос и падает на втором с ошибкой 400. На модели с рассуждением одна из документированных причин — пропавшее поле: у сообщения ассистента, которое вы отправляете обратно, нет reasoning_. Если дело в этом, поможет добавление сообщения в историю целиком, как его вернул API.
Руководство DeepSeek по режиму рассуждения (документация, проверено 8 ноября 2026) формулирует правило одной фразой: если в запросе есть параметр tools, то reasoning_ нужно полностью передавать во всех следующих запросах, в том числе на ходах без вызова инструмента. Иначе API вернёт 400. Там же сказано, что рассуждение включено по умолчанию, с уровнем high. Поэтому цикл, написанный под модель без рассуждения, может сломаться, хотя вы не изменили ни строчки.
Что происходит между первым и вторым запросом#
В режиме рассуждения цепочка мыслей приходит в reasoning_ — поле на одном уровне с content. Вот как идёт цикл:
- Вы отправляете вопрос и список
tools. - Модель отвечает
tool_calls(а рядом с ними —reasoning_).content - Вы запускаете инструмент и отправляете всё обратно: историю, сообщение ассистента и сообщение
toolс результатом.
Этот запрос из шага 3 и получает 400, если в сообщении ассистента нет reasoning_.

DeepSeek описывает 400 только для случая с вызовом инструмента и текст ошибки не приводит. Поэтому проверяйте, та ли это проблема, по HTTP-статусу, а не по конкретной формулировке.
Исправление: добавляйте сообщение как есть#
Самое надёжное — вообще не пересобирать сообщение ассистента. Берёте его из ответа и добавляете целиком. Ниже полный цикл на OpenAI Python SDK, направленном на CheapTokens. Инструмент погоды — заглушка, так что код можно запускать как есть.
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)),
})Главная здесь одна строка: messages.. SDK сохраняет поля, которых не знает, поэтому всё, что API положил в сообщение, включая reasoning_, уходит обратно вместе с ним. Итоговый ответ тоже добавляется в историю — этого и требует правило: после первого вызова инструмента каждый следующий запрос несёт рассуждения прошлых ходов ассистента.
Устройство самих вызовов (схема, tool_call_id, разбор аргументов) разобрано в статье Function calling в Python с OpenAI-совместимым API. Базовый адрес, ключ и id модели — на странице документации CheapTokens; модель из примера описана на странице DeepSeek: контекст 380K и до 32 768 токенов в ответе, так что хватает и на длинное рассуждение, и на сам ответ.
Частый виновник: сообщение, собранное вручную#
Один из частых способов потерять поле — код, который добавляет в историю не объект ответа, а новый словарь. Выглядит безобидно: роль, текст, вызовы инструментов. Так же делают фреймворки и обёртки, когда хранят историю в своём формате. Единственное поле, о котором они не знают, как раз и теряется.

Сравнение
Собрано вручную и добавлено целиком
Собрано вручную (ломается)
messages.append({
"role": "assistant",
"content": msg.content,
"tool_calls": [c.model_dump() for c in msg.tool_calls],
})Добавлено целиком (работает)
messages.append(msg.model_dump(exclude_none=True))Если вы работаете через фреймворк и эту строку не поправить, посмотрите, как он хранит ходы ассистента. История, проходящая через сериализатор, который оставляет только content и tool_calls, ломается так же, только этажом ниже.
Когда это не нужно#
Не каждый вызов модели с рассуждением требует такой аккуратности. Для многоходового чата без инструментов руководство DeepSeek говорит, что reasoning_ возвращать не обязательно; если всё же отправить, оно игнорируется и прошлые рассуждения в контекст не попадают. Обычный чат может и дальше обходиться без этого поля — ничего не сломается.
Выберите свой случай
Какой у вас случай?
Выберите вариант, чтобы увидеть ответ
В запросе есть tools, модель рассуждает
Добавляйте сообщение ассистента целиком на каждом ходу — и с вызовом инструмента, и с итоговым ответом.
Обычный чат, параметра tools нет
Исправление не нужно. reasoning_ в истории можно не хранить, а можно отправлять — оно игнорируется.
Историю собирает фреймворк
Найдите, где он сериализует сообщения ассистента, и убедитесь, что reasoning_ не теряется. Если не получается, часть с инструментами вызовите своим циклом.
И ещё одно: историю вы отправляете заново, а это входные токены на каждом запросе, так что длинные рассуждения в длинном цикле складываются. Как считаются токены рассуждения — в статье Reasoning effort и токены рассуждения, а что делать, когда запрос вырос за пределы модели, — в Request too long: 400 и 413.
Если 400 всё равно приходит#
Проверьте по порядку:
- Выведите
messagesпрямо перед упавшим вызовом. У каждого сообщения ассистента от модели естьreasoning_, если оно было в ответе?content - Не пересобираете ли вы сообщение где-то ещё: во вспомогательной функции, в классе истории, в
json.dumpsи обратно с фиксированным списком ключей? - Есть ли сообщение
toolдля каждогоtool_call_id, с тем же id? - Может, дело в длине? Запрос больше лимита контекста — это другая 400, она разобрана в статье выше.
Быстрая проверка
Что добавлять в историю после того, как модель попросила инструмент?
Показать ответ
Ответ: Сообщение ассистента целиком, как вернул API, вместе с reasoning_content
На tool_calls отвечают ваши сообщения tool, а при tools в запросе рассуждение должно идти вместе с ними.
Вот и всё исправление: один append, без пересборки. А в любой цикл, который уходит в работу, добавьте лимит шагов из примера: тогда плохой ход обойдётся в пять запросов, а не в пятьсот.



