DeepSeek V4: второй запрос 400 — разбор логов

Данные, которые нужно проверить в первую очередь: официальный DeepSeek API прямо указывает, что при tool call в режиме thinking поле reasoning_content должно полностью передаваться в последующих запросах; иначе API может вернуть 400. Поэтому при сценарии «первый вызов успешен, второй запрос DeepSeek V4 получает 400» не следует начинать с повторных попыток, увеличения ресурсов или глобальной замены полей. Сначала нужно сравнить одну и ту же сессию по цепочке: ответ модели → объект в памяти → запись в базе → сообщение очереди → финальный JSON. (api-docs.deepseek.com)
Эта статья предназначена для разработчиков Agent-систем, которые поддерживают многошаговый function calling, а также для инженеров API-шлюзов, хранилищ сообщений и SDK-обёрток. Отдельный случай — команды, которые переключают один клиент между официальным DeepSeek API и собственной конечной точкой vLLM.
Важно.
reasoning_contentиreasoningнельзя считать взаимозаменяемыми именами без проверки конечной точки. В актуальной документации vLLM основным полем являетсяreasoning, аreasoning_contentобозначено как старое название. Совместимость конкретной версии нужно проверять на установленном сервере, а не переносить из одного окружения в другое. (docs.vllm.ai)
Почему второй запрос 400 важнее самого текста ошибки
Если первый ответ модели содержит tool_calls, приложение обычно выполняет функцию, добавляет сообщение с ролью tool, а затем отправляет историю обратно. Ошибка появляется именно на следующем запросе. Это сужает область поиска.
При таком признаке описание инструмента уже прошло первичную проверку. Формат name, схема аргументов и сам факт вызова не являются единственными подозреваемыми. Наиболее вероятен дефект при восстановлении истории:
- объект assistant был сокращён до
contentиtool_calls; reasoning_contentсохранился в памяти, но не попал в JSON;- шлюз удалил неизвестное поле;
- база данных не содержит колонки или JSON-пути для reasoning;
- vLLM вернул
reasoning, а адаптер ожидалreasoning_content; - сообщение
toolдобавлено не после соответствующего assistant; tool_call_idизменился при сериализации;- пустой
contentбыл заменён на несовместимое значение.
Официальный пример DeepSeek показывает, что ответ assistant можно добавлять в историю целиком: объект содержит content, reasoning_content и tool_calls. После этого добавляется сообщение tool с тем же идентификатором вызова. (api-docs.deepseek.com)
Сценарий из журнала
В обезличенной записи проблема обычно выглядит так:
{
"request": 1,
"response_message_keys": [
"role",
"content",
"reasoning_content",
"tool_calls"
],
"tool_call_ids": ["call_redacted_01"],
"status": 200
}
После выполнения функции приложение формирует второй запрос:
{
"request": 2,
"messages": [
{
"role": "assistant",
"content": null,
"tool_calls": ["call_redacted_01"]
},
{
"role": "tool",
"tool_call_id": "call_redacted_01",
"content": "[результат удалён]"
}
],
"status": 400
}
Второй объект уже даёт направление. reasoning_content исчез до отправки запроса. Повторная отправка того же JSON не исправит проблему. Нужно найти слой, на котором пропало поле.
Первый признак: в финальном JSON нет reasoning_content
Это наиболее короткая ветка диагностики. Сначала не нужно изучать всю цепочку вызовов. Достаточно вывести безопасный набор ключей прямо перед HTTP-отправкой:
def inspect_message(message, index):
print({
"index": index,
"role": message.get("role"),
"keys": sorted(message.keys()),
"tool_call_ids": [
item.get("id")
for item in message.get("tool_calls", [])
if isinstance(item, dict)
],
})
Полное содержимое reasoning, аргументы инструмента и секреты в такой журнал не попадают. Фиксируются только индексы, роли, набор полей и идентификаторы.
Для официального DeepSeek API в режиме thinking проверяется наличие следующих полей в assistant-сообщении, которое вызвало инструмент:
{
"role": "assistant",
"content": "[может быть пустым]",
"reasoning_content": "[значение сохранено без раскрытия]",
"tool_calls": [
{
"id": "call_redacted_01",
"type": "function",
"function": {
"name": "example_tool",
"arguments": "[JSON скрыт]"
}
}
]
}
Это не означает, что приложение должно записывать или показывать пользователю полный внутренний текст рассуждений. Требование относится к корректной передаче контекста между запросами. Официальная документация отдельно различает обычные thinking-ходы и ходы с tool call: для вызова инструмента reasoning_content должен участвовать в последующем контексте. (api-docs.deepseek.com)
Проверить нужно четыре места:
- Что вернул SDK сразу после ответа.
- Что добавилось в список
messages. - Что прочитано из базы или кэша.
- Что сериализатор подготовил для HTTP-клиента.
Если поле есть в первом объекте и отсутствует в четвёртом, проблема находится не в DeepSeek API. Это ошибка модели данных, сериализации или фильтрации.
Официальный API против vLLM: где возникает конфликт имён
Главная ловушка появляется при использовании общего внутреннего объекта для двух типов серверов. Официальный DeepSeek API документирует поле reasoning_content. А актуальная документация vLLM описывает reasoning-модели через поле reasoning и предупреждает, что прежнее имя reasoning_content относится к старому интерфейсу. (docs.vllm.ai)
| Элемент | Официальный DeepSeek API | Актуальный интерфейс vLLM |
|---|---|---|
| Поле рассуждений | reasoning_content |
reasoning |
| Основная проверка | Полное возвращение поля после tool call | Контракт установленной версии и протокола |
| Действие адаптера | Передать reasoning_content в assistant |
Прочитать reasoning и преобразовать по контракту |
| Риск | Потеря поля после реконструкции сообщения | Использование старого имени без проверки версии |
| Безопасная стратегия | Отдельная схема для выхода | Отдельная схема для входа и выхода |
Эта разница не означает, что каждый сервер vLLM обязательно отвергнет reasoning_content. Некоторые версии сохраняли обратную совместимость. Документация vLLM прямо рекомендовала миграцию к reasoning, поэтому поведение нужно тестировать на фактической версии, parser-конфигурации и шаблоне чата. Нельзя превращать совместимость отдельного релиза в постоянную гарантию. (docs.vllm.ai)
Безопаснее не передавать внутренний объект SDK напрямую:
def normalize_reasoning(message, endpoint):
if endpoint == "deepseek-api":
return message.get("reasoning_content")
if endpoint == "vllm":
return message.get("reasoning")
raise ValueError("Неизвестный тип конечной точки")
Ещё лучше — хранить в приложении нейтральное поле:
internal_message = {
"role": "assistant",
"content": assistant_content,
"reasoning": internal_reasoning,
"tool_calls": tool_calls,
}
Затем делать явное исходящее преобразование:
def to_deepseek_api(message):
result = {
"role": message["role"],
"content": message.get("content"),
"tool_calls": message.get("tool_calls"),
}
if message.get("reasoning") is not None:
result["reasoning_content"] = message["reasoning"]
return result
Для vLLM применяется обратное отображение только после проверки конкретного протокола. Такой слой лучше, чем глобально добавлять оба поля во все сообщения. Дублирование может скрыть ошибку и создать неоднозначность при обновлении сервера.
Если поле исчезает между памятью и шлюзом
При наличии reasoning_content в памяти и отсутствии его в конечном JSON нужно проходить цепочку по одному переходу. Не следует сразу менять базу, SDK и прокси одновременно. Иначе невозможно установить, что именно исправило ситуацию.
Шаг 1: зафиксировать исходный ответ
Сразу после ответа assistant сохраните:
- индекс сообщения;
- роль;
- список ключей;
- наличие
tool_calls; - хэш значения reasoning без раскрытия текста;
- список
tool_call_id; - тип конечной точки;
- версию клиента и сервера.
Хэш нужен для сравнения, но не для восстановления содержимого. В продакшене полный reasoning лучше защищать теми же правилами доступа, что и внутренние журналы модели.
Шаг 2: проверить реконструкцию объекта
Многие Agent-фреймворки используют собственную модель сообщения. Она может разрешать только role, content и tool_calls. Если объект создаётся заново, неизвестное поле исчезает:
saved = {
"role": response.role,
"content": response.content,
"tool_calls": response.tool_calls,
}
Правильная проверка должна показать, какие поля были отброшены:
allowed = {
"role",
"content",
"reasoning_content",
"reasoning",
"tool_calls",
}
dropped = set(response.__dict__.keys()) - allowed
print({"dropped_fields": sorted(dropped)})
В реальной библиотеке названия атрибутов могут отличаться. Смысл проверки остаётся тем же: сравнивается исходный набор ключей и набор после преобразования.
Шаг 3: проверить схему базы данных
JSON-колонка не гарантирует сохранность вложенного поля. Частые причины:
- сериализатор удаляет
null, пустые строки и неизвестные ключи; - миграция разрешает только старое имя;
- ORM возвращает объект без новых атрибутов;
- лимит размера поля обрезает длинное значение;
- версия потребителя очереди использует старую схему.
Для одной тестовой сессии нужно сравнить хэш reasoning до записи и после чтения. Если хэши различаются, запрос к API пока не имеет смысла — ошибка уже доказана на уровне хранения.
Шаг 4: проверить шлюз и очередь
Прокси часто использует белый список полей. Это удобно для валидации, но опасно для новых параметров. Проверьте:
- правила удаления неизвестных ключей;
- преобразование
snake_caseиcamelCase; - фильтр персональных данных;
- очистку пустых значений;
- повторную сериализацию после очереди;
- версию потребителя, который отправляет запрос.
Полезен журнал разности, а не полный payload:
def field_diff(before, after):
return {
"before": sorted(before.keys()),
"after": sorted(after.keys()),
"removed": sorted(set(before) - set(after)),
"added": sorted(set(after) - set(before)),
}
Такой журнал показывает, что пропало поле reasoning_content, но не раскрывает его значение.
Когда reasoning есть, но 400 остаётся
Если нужное поле присутствует в финальном запросе, проверка переходит к структуре сообщения. Не стоит считать любую ошибку 400 следствием имени поля.
Сначала проверяются роли и порядок:
user
assistant с tool_calls
tool с соответствующим tool_call_id
assistant или следующий запрос
Для каждого tool_call_id должно существовать соответствующее сообщение tool. Идентификатор нельзя пересоздавать при чтении из базы. Если в первом сообщении был call_redacted_01, в ответе инструмента должен остаться тот же идентификатор.
Затем проверяется content. В некоторых обёртках отсутствующее содержимое превращается в пустой объект, список или строку, хотя конечная точка ожидает строковое значение или null. Нельзя механически заменять null на {}.
Отдельно проверяются параметры thinking-режима. Документация DeepSeek указывает, что в этом режиме ряд параметров, включая temperature, top_p, presence_penalty и frequency_penalty, не поддерживается как рабочая настройка. Совместимый клиент может принять их без немедленной ошибки, но это не делает их корректными для всех сценариев. (api-docs.deepseek.com)
Также нужно сравнить:
tool_choice;- список
toolsмежду первым и вторым запросом; - имя модели;
- включение thinking;
- структуру аргументов функции;
- наличие лишних полей, добавленных конкретным SDK.
Проверка должна опираться на сообщение об ошибке или на воспроизводимый минимальный запрос. Если приложение одновременно меняет поле reasoning, tool_choice и список инструментов, результат диагностики будет недостоверным.
Решение по веткам: какой слой исправлять
Используется следующая последовательность условий:
- Если в ответе модели нет нужного reasoning-поля, проверить parser, режим thinking и контракт ответа конкретного сервера. Не добавлять выдуманное значение в историю.
- Если поле есть в ответе, но исчезает в памяти, исправить модель сообщения или преобразование SDK.
- Если оно есть в памяти, но отсутствует после чтения, исправить схему базы, сериализатор или потребителя очереди.
- Если оно исчезает только перед официальным API, проверить белый список шлюза и исходящее отображение в
reasoning_content. - Если оно исчезает только перед vLLM, проверить версию сервера и преобразование в
reasoning. - Если поля, роли и идентификаторы совпадают, перейти к порядку сообщений,
content,tools,tool_choiceи прочим параметрам. - Если ошибка воспроизводится только на одной версии vLLM, закрепить версию в тесте и отдельно проверить её protocol definition. Не объявлять это общей несовместимостью всех релизов.
Это и есть рабочий выбор между двумя действиями: если нарушен контракт поля, исправляется адаптер; если контракт соблюдён, исследуется структура сообщения и параметры запроса. Повторные попытки полезны только после изменения входного JSON.
Минимальный воспроизводимый журнал без утечки данных
Для расследования достаточно сохранить четыре снимка:
- Ответ assistant после первого запроса.
- Историю после добавления
tool. - Запись, прочитанную из базы или очереди.
- Финальный запрос с ответом 400.
Каждый снимок должен содержать:
{
"session": "hash_session",
"message_index": 2,
"endpoint": "deepseek-api или vllm",
"server_version": "зафиксированная версия",
"roles": ["user", "assistant", "tool"],
"message_keys": ["role", "content", "tool_calls", "reasoning_content"],
"tool_call_ids": ["hash_call_id"]
}
В лог не включаются API-ключи, полный reasoning, реальные аргументы функций и результаты инструментов. Для сравнения применяются хэши, длины и наборы полей. Если проблема связана с потоком, нужно отдельно проверить объединение delta: reasoning может приходить частями, а итоговый объект должен формироваться до сохранения assistant-сообщения.
Финальная проверка: одна сессия, три маршрута
Исправление нужно подтвердить на одной и той же обезличенной истории. Сначала запрос отправляется напрямую в официальный DeepSeek API. Затем тот же сценарий — напрямую в целевую версию vLLM. После этого выполняется полный маршрут через SDK, шлюз, базу и очередь.
Для каждого маршрута фиксируются:
- тип конечной точки;
- версия сервера;
- версия SDK;
- набор полей до отправки;
- HTTP-статус;
- текст ошибки без секретов;
- различия между рабочим и нерабочим JSON.
Официальный API и vLLM должны оставаться отдельными колонками в отчёте. Нельзя проверять только общий OpenAI-совместимый интерфейс: совпадение URL-формата не означает совпадение схемы reasoning. Для vLLM дополнительно следует сверять текущую документацию reasoning outputs и фактическое поведение установленного релиза. (docs.vllm.ai)
Если причина найдена в шлюзе, исправление должно находиться в шлюзе. Если причина в преобразовании ответа vLLM, исправляется адаптер vLLM. Глобальное копирование одновременно reasoning и reasoning_content в каждый запрос не является надёжным решением: оно маскирует границу контрактов и усложняет будущую миграцию.
Частые вопросы
Почему первый вызов инструмента проходит, а второй запрос DeepSeek V4 получает 400?
Потому что первый запрос проверяет генерацию tool call, а второй — способность клиента корректно вернуть историю. При thinking-режиме официальный API ожидает полный reasoning_content для assistant-сообщения, которое вызвало инструмент. Если фреймворк оставил только content и tool_calls, ошибка проявится уже после результата функции. (api-docs.deepseek.com)
Почему сохранённый reasoning_content не гарантирует успешный многошаговый вызов?
Значение может существовать в базе, но отсутствовать в HTTP-запросе. Между этими точками работают ORM, сериализатор, очередь, фильтр шлюза и SDK. Каждый переход нужно проверять по набору ключей и хэшу. Особое внимание уделяется очистке пустых значений, реконструкции объектов и объединению потоковых фрагментов.
Можно ли отправлять reasoning из vLLM в официальный API без преобразования?
Нет, такой подход нельзя считать безопасным. Документация vLLM использует reasoning, а официальный DeepSeek API — reasoning_content. Даже если конкретная версия временно принимает старое имя, это не является гарантией для другого релиза или parser-конфигурации. Нужен явный слой отображения по типу конечной точки. (docs.vllm.ai)
Какой журнал нужен для поиска потерянного поля?
Нужен один обезличенный сеанс с четырьмя снимками: ответ assistant, история после tool, запись после чтения и финальный запрос. В каждом снимке сохраняются роли, индексы, ключи, идентификаторы вызовов, тип endpoint и версия сервера. Полный reasoning, секреты и реальные результаты инструментов в журнал не записываются.
Что делать после исправления
Если текущая цепочка зависит от ручного просмотра логов, следующий сбой после обновления SDK или сервера снова потребует полного расследования. Для Agent-систем полезно сохранить минимальную неуспешную сессию как регрессионный тест: официальный DeepSeek API, целевая версия vLLM и полный бизнес-маршрут должны проходить её отдельно.
Для повторяемого облачного окружения можно начать с справочного раздела ProxyMac, а параметры доступной среды и состояние запущенных машин проверять через консоль ProxyMac. Это особенно уместно, когда тест включает macOS-клиент, автоматизацию в Xcode или задачу, которая должна оставаться онлайн во время многошагового прогона.
Текущий локальный сервер или случайная общая машина часто создают три реальных недостатка: версии SDK и vLLM меняются без единого журнала, повторный прогон зависит от занятого оборудования, а доступ к macOS-инструментам приходится собирать отдельно. В такой ситуации аренда среды ProxyMac может быть практичнее для временной регрессии, проверки адаптера и воспроизведения 400. Для постоянной тяжёлой нагрузки, физического оборудования или долгосрочной фиксированной инфраструктуры собственный сервер всё равно может оказаться рациональнее. Последнее обновление: 15 августа 2026 года; сведения проверены по документации DeepSeek API и актуальным материалам vLLM.