ИИ-агент

DeepSeek V4: многошаговый 400 и минимальный запрос

MacHTML Lab2026.08.18 ~14 мин чтения
DeepSeek V4: многошаговый 400 и минимальный запрос

По документации DeepSeek, если в thinking mode был вызов инструмента, неполный возврат reasoning_content приводит к HTTP 400 уже на следующем запросе. Поэтому не пытайтесь «починить» один общий payload заменой reasoning_content на reasoning: сначала соберите две отдельные raw HTTP-цепочки — для официального API DeepSeek и для vLLM. Затем сделайте одностороннее преобразование на границе адаптера. (документация DeepSeek по thinking mode)

Эта инструкция рассчитана на вас, если вы:

  • разрабатываете AI Agent и должны передать владельцу модели воспроизводимый запрос;
  • ищете границу между клиентом, шлюзом и inference endpoint;
  • готовите двусторонний regression-тест для официального API DeepSeek и собственного vLLM;
  • хотите проверить многошаговый 400 DeepSeek V4 без SDK, оркестратора и скрытых преобразований сообщений.

Последнее обновление: 18 августа 2026 года. Факты сверены с официальной документацией DeepSeek по thinking mode, схемой Chat Completions, текущими материалами vLLM по reasoning outputs и парсером DeepSeek V4.

Разные endpoint — разные контракты

Начните не с имени модели, а с фактического адреса запроса. Один и тот же идентификатор модели может проходить через официальный API, reverse proxy, совместимый шлюз или локальный сервер vLLM. Для диагностики это разные системы.

Зафиксируйте перед первым тестом:

  • полный base_url без API-ключа;
  • имя модели;
  • режим мышления;
  • наличие tools;
  • версию развёрнутого vLLM;
  • параметры tool_choice, stream и max_tokens;
  • исходный JSON после всех middleware;
  • статус, тело ответа и идентификатор запроса.

Официальный API DeepSeek документирует reasoning_content как поле assistant-сообщения рядом с content и tool_calls. В инструкции по tool calls отдельно указано: если ход включал вызов инструмента, это поле нужно полностью передавать в последующих запросах. (документация DeepSeek по thinking mode)

В текущей документации vLLM используется имя reasoning. Для DeepSeek V4 также выделен отдельный parser, который разбирает thinking-блок и DSML-вызовы инструментов. Из этого нельзя делать вывод, что официальный API DeepSeek примет reasoning вместо reasoning_content. Это разные выходные контракты. (документация vLLM по reasoning outputs)

Что фиксировать Официальный API DeepSeek vLLM с DeepSeek V4
Адрес Документированный API endpoint Ваш фактический /v1/chat/completions
Поле рассуждения reasoning_content reasoning в актуальной схеме vLLM
Вызовы инструментов tool_calls в assistant-сообщении OpenAI-совместимое представление после parser
Контракт replay Полный reasoning_content после tool call Зависит от версии, parser и chat template
Что проверять Документацию API и тело 400 Версию vLLM, parser, шаблон и реальный ответ

Не скрывайте эту разницу общим типом ReasoningMessage. Внутри приложения можно иметь нормализованную модель, но до отправки нужно явно выбрать схему сообщения по base_url.

Первый ответ — чистый триггер

Для воспроизведения используйте один инструмент без побочных эффектов. Например, функцию, которая возвращает фиксированную строку. Не подключайте базу данных, файловую систему или сеть: ошибка должна зависеть только от сообщений.

Ниже — сокращённый raw HTTP-шаблон. Значения API_KEY и URL подставьте локально. Не сохраняйте секреты в файле воспроизведения.

curl "$DEEPSEEK_BASE_URL/chat/completions" \
  -H "Authorization: Bearer $DEEPSEEK_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "model": "deepseek-v4-pro",
    "messages": [
      {
        "role": "user",
        "content": "Проверь статус заказа 123."
      }
    ],
    "tools": [
      {
        "type": "function",
        "function": {
          "name": "get_order_status",
          "description": "Возвращает фиксированный тестовый статус",
          "parameters": {
            "type": "object",
            "properties": {
              "order_id": { "type": "string" }
            },
            "required": ["order_id"],
            "additionalProperties": false
          }
        }
      }
    ],
    "tool_choice": "required",
    "thinking": { "type": "enabled" },
    "stream": false
  }'

Для официального API сохраните из ответа только диагностические поля:

{
  "message": {
    "role": "assistant",
    "content": "<redacted>",
    "reasoning_content": "<redacted>",
    "tool_calls": [
      {
        "id": "call_test_001",
        "type": "function",
        "function": {
          "name": "get_order_status",
          "arguments": "{\"order_id\":\"123\"}"
        }
      }
    ]
  },
  "finish_reason": "tool_calls",
  "id": "resp_test_001"
}

Здесь важны не текст рассуждения и не реальный заказ, а наличие полей. Официальная схема также предупреждает, что аргументы функции могут оказаться некорректным JSON, поэтому тестовый клиент должен валидировать их до исполнения. (схема DeepSeek Chat Completions)

Для vLLM не переименовывайте ответ вручную во время сбора доказательств. Сначала запишите, что реально вернул ваш endpoint: reasoning, content, tool_calls, finish_reason и id. В текущем parser DeepSeek V4 вызовы представлены через DSML-формат модели и преобразуются в структурированное сообщение parser-ом vLLM. (парсер DeepSeek V4 в vLLM)

Важно. Если вы сначала превратили объект ответа в «универсальное» сообщение, а затем увидели 400, это ещё не доказывает проблему endpoint. Сначала сравните сырой JSON ответа с JSON, который действительно ушёл во втором POST. Фильтрация логов, Pydantic-модель, dict() и middleware — отдельные точки отказа.

Replay assistant-сообщения без потери полей

После первого ответа не конструируйте assistant-сообщение по памяти. Восстановите его из ответа, сохранив:

  • role: "assistant";
  • content, даже если значение null или пустая строка;
  • поле рассуждения именно в имени текущего endpoint;
  • весь массив tool_calls;
  • каждый tool_calls[].id;
  • имя функции;
  • строку arguments.

Для официального API минимальный второй запрос должен выглядеть так:

{
  "model": "deepseek-v4-pro",
  "messages": [
    {
      "role": "user",
      "content": "Проверь статус заказа 123."
    },
    {
      "role": "assistant",
      "content": null,
      "reasoning_content": "<redacted-original-value>",
      "tool_calls": [
        {
          "id": "call_test_001",
          "type": "function",
          "function": {
            "name": "get_order_status",
            "arguments": "{\"order_id\":\"123\"}"
          }
        }
      ]
    },
    {
      "role": "tool",
      "tool_call_id": "call_test_001",
      "content": "Тестовый статус: отправлен"
    }
  ],
  "tools": [
    {
      "type": "function",
      "function": {
        "name": "get_order_status",
        "description": "Возвращает фиксированный тестовый статус",
        "parameters": {
          "type": "object",
          "properties": {
            "order_id": { "type": "string" }
          },
          "required": ["order_id"]
        }
      }
    }
  ],
  "thinking": { "type": "enabled" },
  "stream": false
}

Не вставляйте настоящее reasoning-содержимое в публичный issue или общий лог. Для воспроизводимости достаточно показать, что поле присутствует, а его значение совпадает с оригинальным ответом по длине, хешу или внутреннему идентификатору. Сам текст храните только в закрытом тестовом артефакте.

Добавьте четыре проверки до отправки:

assert assistant["role"] == "assistant"
assert "tool_calls" in assistant
assert assistant["tool_calls"][0]["id"] == tool["tool_call_id"]
assert assistant["reasoning_content"] == original_reasoning_content

Последняя проверка нужна именно для официального endpoint. Для vLLM она должна обращаться к reasoning, если это поле возвращает ваша установленная версия и настроенный parser. Не делайте глобальную замену названий: один и тот же внутренний объект может быть отправлен в разные endpoint только после явного преобразования.

Пара неуспешных и успешных запросов

Сначала отправьте негативный образец. Удалите reasoning_content из assistant-сообщения, но оставьте tool_calls и сообщение tool. Для официального thinking mode ожидаемый результат — ошибка 400 с сообщением о необходимости передать reasoning-содержимое. Это подтверждает контракт API, но ещё не показывает, где произошло удаление.

Затем отправьте положительный образец:

  1. оставьте исходное reasoning_content;
  2. сохраните tool_calls без пересоздания идентификаторов;
  3. добавьте tool после assistant;
  4. используйте тот же model;
  5. отправьте тот же набор tools;
  6. сохраните тот же режим thinking.

Зафиксируйте в одном JSON-отчёте:

{
  "case": "official_api_missing_reasoning",
  "base_url": "<redacted>",
  "http_status": 400,
  "error_body": "<redacted>",
  "assistant_keys": ["role", "content", "tool_calls"]
}

И отдельно:

{
  "case": "official_api_full_replay",
  "base_url": "<redacted>",
  "http_status": 200,
  "finish_reason": "stop",
  "assistant_keys": ["role", "content", "reasoning_content", "tool_calls"]
}

Если оба варианта дают 400, остановитесь. Причина может быть не в reasoning-поле: проверьте порядок сообщений, формат tool_call_id, схему инструмента, модель и реальный адрес. Если негативный вариант проходит, а положительный ломается, ищите ошибку сериализации или несовместимое значение поля.

Так вы отвечаете на главный вопрос: почему первый function calling проходит, а второй запрос падает? Первый запрос проверяет возможность создать tool call. Второй проверяет, можете ли вы воспроизвести assistant-состояние в точности.

Сопоставление API и vLLM

Запустите один и тот же тестовый сценарий дважды, но не один и тот же JSON вслепую.

Для официального API критерий успеха такой:

  • ответ содержит reasoning_content;
  • assistant-сообщение с tool call возвращается без потери этого поля;
  • следующий POST содержит полный reasoning_content;
  • tool_call_id совпадает с результатом инструмента;
  • endpoint возвращает не 400, а следующий assistant-ответ.

Для vLLM критерии зависят от версии и запуска. В актуальной документации для reasoning outputs используется отдельное поле reasoning, а для DeepSeek V4 предусмотрены parser reasoning и tool-call parser. В некоторых рецептах запуска одновременно указываются --tool-call-parser deepseek_v4, --reasoning-parser deepseek_v4 и автоматический выбор инструментов. Это параметры конкретного развёртывания, а не универсальное обещание совместимости с официальным API.

Проверьте команду запуска и запишите её в отчёт:

vllm serve <model-path> \
  --served-model-name deepseek-v4-test \
  --enable-auto-tool-choice \
  --tool-call-parser deepseek_v4 \
  --reasoning-parser deepseek_v4

Не копируйте эту команду без сверки с установленной версией. У vLLM менялись parser-ы, названия модулей и поведение совместимых полей. Текущая реализация DeepSeek V4 также содержит отдельную обработку thinking mode, tool calls и формата сообщений.

Нормализованная внутренняя модель может выглядеть так:

{
  "role": "assistant",
  "content": null,
  "reasoning": "<internal-redacted>",
  "tool_calls": []
}

Но на выходе должны быть две разные функции:

to_deepseek_api(message):
    reasoning -> reasoning_content

to_vllm(message):
    reasoning -> reasoning

Это не означает, что любое значение reasoning можно отправить в официальный API. Адаптер обязан знать, откуда получено сообщение и куда оно направляется. Если история уже прошла через vLLM, проверьте, есть ли у конкретной версии входная совместимость с reasoning_content; документация по выходному полю сама по себе этого не подтверждает.

Регрессионный набор перед исправлением

После успешного ручного replay превратите запросы в тесты. Минимальный набор должен включать четыре сценария:

  • обычный запрос без инструментов;
  • один tool call и один результат;
  • два последовательных tool call в одной сессии;
  • переключение между официальным API и vLLM с нормализацией истории.

Для каждого сценария проверяйте не только HTTP 200:

[ ] Base URL соответствует ожидаемому endpoint
[ ] Имя модели записано в отчёт
[ ] Режим thinking зафиксирован
[ ] Assistant-сообщение сохранено целиком
[ ] Для DeepSeek API присутствует reasoning_content
[ ] Для vLLM зафиксировано фактическое поле reasoning
[ ] tool_call_id совпадает с id сообщения tool
[ ] tool_calls идут до соответствующего tool
[ ] Порядок user → assistant → tool не изменён
[ ] Ошибочный payload стабильно возвращает ожидаемый 400
[ ] Исправленный payload проходит два последовательных запроса
[ ] В артефакте нет ключей, персональных данных и полного reasoning

Успешное исправление — это не один зелёный ответ. Считайте задачу закрытой, когда положительные сценарии проходят на обоих endpoint, отрицательный пример продолжает предсказуемо ломаться, а проверка base_url не позволяет случайно отправить vLLM-историю в официальный API.

Такой подход также отделяет ошибку клиента от ошибки шлюза. Если raw HTTP к официальному API проходит, а тот же разговор через адаптер получает 400, проблема находится между ними. Если оба raw-запроса ломаются одинаково, проверяйте контракт endpoint и версию модели. Если официальный API проходит, а vLLM нет, изучайте parser, chat template и входную совместимость именно вашей сборки.

Частые вопросы

Почему первая итерация работает

Первая итерация не содержит предыдущего assistant-сообщения, которое нужно восстановить. Поэтому сервер ещё не проверяет, сохранили ли вы рассуждение и идентификатор вызова в истории. Ошибка появляется на границе между ответом с tool_calls и следующим запросом.

Где должен находиться reasoning_content

Он остаётся в assistant-сообщении. Не переносите его в tool. Сообщение инструмента содержит только role, tool_call_id и результат. Если поле было удалено при преобразовании объекта ответа в словарь, исправлять нужно сериализацию, а не prompt.

Почему reasoning из vLLM нельзя считать заменой

Имя поля — часть протокола. vLLM может использовать reasoning как внутреннее структурированное представление, а официальный API — reasoning_content. Даже при одинаковом содержимом это не доказывает одинаковый формат входа. Проверяйте deployed version и фактический ответ.

Как отправить доказательство владельцу endpoint

Приложите два обезличенных JSON: провальный и успешный. В каждом оставьте base_url с удалённым доменом или окружением, список ключей assistant-сообщения, tool_call_id, статус и тело ошибки. Полный reasoning, API-ключ, пользовательский запрос и реальные результаты инструмента замените безопасными маркерами.

Изолированная среда для повторного прогона

Локальная машина часто не сохраняет две чистые конфигурации одновременно. Один процесс использует старый parser, другой — новый; переменная BASE_URL остаётся в shell; контейнер с адаптером продолжает работать после изменения схемы. В результате исправление выглядит случайным.

Для повторяемого теста вам нужны:

  • отдельная среда для официального API;
  • отдельная среда для vLLM;
  • версия запуска в manifest-файле;
  • один скрипт raw HTTP;
  • очищенная история между тестами;
  • сохранённые негативный и позитивный payload;
  • контроль фактического endpoint перед каждым POST.

Если локальная машина не позволяет долго держать такие конфигурации раздельно, можно использовать изолированный облачный Mac-тестовый стенд MacHTML: это удобно для хранения скрипта, проверки сетевого маршрута и повторения сценария после обновления адаптера. Перед выбором посмотрите доступные параметры консоли MacHTML, а вопросы по подключению и окружению сверяйте с разделом помощи MacHTML.

Однако аренда не заменяет inference-сервер. Она решает задачу воспроизводимой клиентской среды, а не автоматически предоставляет вам нужную модель или GPU-производительность. Для постоянной тяжёлой нагрузки, физических интерфейсов или долгоживущего self-hosted inference разумнее оценить собственное оборудование и отдельный сервер. Для временной проверки, регрессионного прогона и сравнения двух endpoint облачный Mac обычно практичнее: не нужно менять рабочую машину, смешивать зависимости и вручную восстанавливать окружение после неудачного эксперимента.

FAQ

Почему первый вызов инструмента проходит, а следующий запрос DeepSeek V4 получает 400?+
Первый ответ может содержать корректные tool_calls, но следующий запрос уже проверяет всю историю сообщений. В thinking mode официальный API ожидает полный reasoning_content из assistant-сообщения, в котором был вызов инструмента. Если сериализатор, логирование или адаптер удалили это поле, сервер получает неполную цепочку и возвращает 400.
Куда помещать reasoning_content в следующем запросе?+
Поле reasoning_content должно оставаться внутри того же сообщения с role assistant, где находятся content и tool_calls. Затем добавьте отдельное сообщение role tool с соответствующим tool_call_id. Не переносите reasoning_content в сообщение tool, user или system и не заменяйте его на reasoning без проверки контракта конкретного endpoint.
Можно ли напрямую отправить reasoning из vLLM обратно в DeepSeek API?+
Автоматически считать поля взаимозаменяемыми нельзя. Текущая документация vLLM использует reasoning как структурированное поле вывода, тогда как официальный API DeepSeek документирует reasoning_content. Для смешанной схемы нужен адаптер, который знает источник сообщения, проверяет версию и выполняет одностороннее преобразование только на границе.
Как воспроизвести ошибку DeepSeek V4 без OpenAI SDK?+
Сохраните base URL, модель, заголовки без секретов и минимальный JSON с одним инструментом. Выполните первый POST, вручную соберите assistant-сообщение, добавьте role tool с тем же tool_call_id и повторите POST. Запустите сначала вариант без reasoning_content, затем вариант с полным полем. Сравнивайте не только статус, но и тело ошибки и историю сообщений.

Тестируйте многошаговые API-сценарии на удалённом Mac

MacHTML предоставляет удалённые Mac для воспроизведения ошибок 400 и проверки интеграций без настройки локального оборудования. Вы сможете отправлять минимальные HTTP-запросы, сравнивать варианты обработки ответа и проверять поведение function calling. Доступ через VNC помогает сохранить привычную macOS-среду и полный контроль над инструментами отладки. Выберите подходящий тариф MacHTML и превратите исправление в стабильный регрессионный тест.

Аренда облачного Mac mini
Облачный Mac на Apple Silicon