Назад до блогу

Вирішення проблемПосібник

400 після виклику інструмента: надсилайте reasoning_content назад

Чому другий запит циклу виклику інструментів повертає 400 на моделі з міркуванням, виправлення на Python, що додає повне повідомлення, і коли це можна пропустити.

Автор: CheapTokens TeamОпубліковано 4 хв читання

Обкладинка: цикл виклику інструментів, де reasoning_content подорожує з повідомлення асистента в наступний запит
На цій сторінці
  1. Що відбувається між першим і другим запитом
  2. Виправлення: додавайте повідомлення як повернуто
  3. Поширений винуватець: повідомлення, перебудоване вручну
  4. Коли це не потрібно
  5. Якщо все ще повертає 400

Ваш цикл агента працює на першому запиті й падає на другому з 400. На моделі з міркуванням одна задокументована причина — відсутнє поле: повідомлення асистента, яке ви надсилаєте назад, втратило свій reasoning_content. Якщо причина саме в цьому, додавання всього повідомлення точно в тому вигляді, як його повернув API, виправляє ситуацію.

Посібник DeepSeek з режиму міркування (документація, перевірено 8 листопада 2026 року) формулює правило одним реченням: коли в запиті є параметр tools, reasoning_content потрібно передавати назад повністю в усіх наступних запитах, включно з ходами без виклику інструмента. Інакше API повертає 400. На тій самій сторінці сказано, що міркування ввімкнено за замовчуванням, з рівнем зусиль high. Тож цикл, написаний для моделі без міркування, може зламатися без жодної зміни рядка.

Що відбувається між першим і другим запитом#

У режимі міркування ланцюжок думок повертається в reasoning_content — полі на тому самому рівні, що й content. Уявіть цикл:

  1. Ви надсилаєте запитання та список tools.
  2. Модель відповідає tool_calls (і reasoning_content поруч із ними).
  3. Ви запускаєте інструмент і надсилаєте все назад: історію, повідомлення асистента та повідомлення tool з результатом.

400 виникає на запиті з кроку 3, якщо повідомлення асистента в ньому не має reasoning_content.

Три кроки циклу інструмента: запит, повідомлення асистента з reasoning_content, другий запит
Де має пройти поле: від повідомлення асистента в наступний запит

Сторінка DeepSeek документує 400 лише для цього випадку з викликом інструмента й не наводить тексту помилки, тож перевіряючи, чи маєте ви цю проблему, дивіться на HTTP-статус, а не на конкретне повідомлення.

Виправлення: додавайте повідомлення як повернуто#

Найбезпечніший крок — взагалі не перебудовувати повідомлення асистента. Візьміть його з відповіді й додайте цілком. Ось повний цикл з OpenAI Python SDK, націленим на CheapTokens. Інструмент погоди — заглушка, тож його можна запускати як є.

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

Єдиний рядок, що має значення, — messages.append(msg.model_dump(exclude_none=True)). SDK зберігає поля, про які не знає, тож усе, що API поклав на повідомлення, включно з reasoning_content, подорожує назад разом із ним. Фінальна відповідь також додається, і саме цього вимагає правило: після першого виклику інструмента кожен наступний запит несе міркування попередніх ходів асистента.

Сама механіка виклику інструментів (схема, tool_call_id, розбір аргументів) описана в Function calling in Python with an OpenAI-compatible API. Базовий URL, ключ і ідентифікатор моделі є на сторінці документації CheapTokens; модель, використана тут, описана на сторінці DeepSeek: контекст 380K і до 32 768 вихідних токенів на відповідь, що залишає місце для довгого міркування плюс відповіді.

Поширений винуватець: повідомлення, перебудоване вручну#

Поширений спосіб втратити поле — код, який не додає об'єкт відповіді, а будує новий dict. Це виглядає нешкідливо: role, content, tool calls. Фреймворки та обгортки роблять те саме, коли зберігають історію у власному форматі. Єдине поле, про яке вони ніколи не чули, — це те, що зникає.

Ліворуч: повідомлення асистента, перебудоване з role, content, tool_calls. Праворуч: повідомлення як повернуто, з reasoning_content
Різниця між двома повідомленнями асистента — одне поле

Порівняйте

Перебудоване вручну проти доданого цілком

Перебудоване вручну (ламається)

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

Додане цілком (працює)

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

Якщо ви використовуєте фреймворк і не можете торкнутися того рядка, перевірте, як він зберігає ходи асистента. Історія, що проходить через серіалізатор, який зберігає лише content і tool_calls, має той самий баг, просто на один рівень нижче.

Коли це не потрібно#

Не кожен виклик моделі з міркуванням потребує цієї додаткової обережності. Для багатоходового чату без інструментів посібник DeepSeek каже, що reasoning_content не потрібно передавати назад; якщо ви все одно надсилаєте його, він ігнорується, і попередні міркування не додаються до контексту. Тож звичайний чат може й далі відкидати його, і нічого не ламається.

Оберіть свій випадок

Який випадок ваш?

У запиті є tools, модель міркує

Додавайте все повідомлення асистента щоразу, як на ходах з інструментами, так і на фінальних відповідях.

Звичайний чат, без параметра tools

Виправлення не потрібне. Можете відкидати reasoning_content з історії або надсилати його; він ігнорується.

Фреймворк будує історію за вас

Знайдіть, де він серіалізує повідомлення асистента, і переконайтеся, що reasoning_content зберігається. Якщо ні, викликайте API власним циклом для частини з інструментами.

Побічний ефект, який варто мати на увазі: історія, яку ви надсилаєте повторно, є вхідними даними в кожному запиті, тож довгі міркування в довгому циклі накопичуються. Як рахуються токени міркування, описано в Reasoning effort and thinking tokens, а що робити, коли запит перевищує ліміти моделі, — у Request too long: 400 vs 413.

Якщо все ще повертає 400#

Пройдіть це по порядку:

  • Надрукуйте messages безпосередньо перед викликом, що падає. Чи має кожне повідомлення асистента, що прийшло від моделі, reasoning_content, якщо відповідь його мала?
  • Чи перебудовуєте ви повідомлення десь: у хелпері, класі історії, json.dumps і назад із фіксованим списком ключів?
  • Чи присутнє повідомлення tool для кожного tool_call_id, з тим самим id?
  • Чи це справді довжина? Запит понад ліміт контексту — це інший 400, описаний у статті вище.

Швидка перевірка

Що ви додаєте після того, як модель просить інструмент?

Показати відповідь

Відповідь: Усе повідомлення асистента як повернуто, включно з reasoning_content

На tool_calls відповідають ваші повідомлення інструментів, і коли в запиті є інструменти, міркування має їхати разом із ними.

Ось і все виправлення: одне додавання, жодної перебудови. Додайте ліміт кроків із прикладу до будь-якого циклу, який випускаєте, щоб поганий хід коштував п'яти запитів, а не п'ятисот.