AIAgent

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

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)

Проверить нужно четыре места:

  1. Что вернул SDK сразу после ответа.
  2. Что добавилось в список messages.
  3. Что прочитано из базы или кэша.
  4. Что сериализатор подготовил для 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.

Минимальный воспроизводимый журнал без утечки данных

Для расследования достаточно сохранить четыре снимка:

  1. Ответ assistant после первого запроса.
  2. Историю после добавления tool.
  3. Запись, прочитанную из базы или очереди.
  4. Финальный запрос с ответом 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.

Проверьте Agent-систему в стабильной среде ProxyMac

Арендуйте удалённый Mac для воспроизведения ошибок 400 и последовательной проверки запросов.
Используйте ресурсы ProxyMac для диагностики памяти сообщений, сериализации SDK и сетевого шлюза.