← Щоденник розробки

Обмеження, зміряне під іншу модель

Сьогодні Майстер переїхав на іншу модель.

Викотили. Ходи пішли нормально. Почати нову гру стало неможливо.

Що саме зламалося

У лозі бекенда:

app.dm.OutputTruncated: the DM could not produce a valid narrate_turn call in 3 attempts:
  attempt 1: the narrate_turn call was cut off at the 3000-token cap
  attempt 2: the narrate_turn call was cut off at the 3000-token cap
  attempt 3: the narrate_turn call was cut off at the 3000-token cap

Модель упиралася в стелю виводу. Тричі поспіль, дев’ять тисяч токенів, 331 секунда — і жодного рядка гравцеві. Проксі здавався раніше, на сто двадцятій секунді, тож людина бачила «Сервер не відповів вчасно» і більше нічого. Справжня причина не доходила ні до неї, ні до нас.

Чому зламалося саме відкриття

Відкриття сесії — найбільша відповідь, яку модель пише за всю гру.

У звичайному ході вона повертає текст, зміни стану та варіанти дій. У відкритті до цього додається сцена: прапорець opening=True переводить поле scene з необов’язкового в обов’язкове (contract.py:2643). Так і задумано — це єдиний хід, у якого немає попередньої картинки, щоб на неї спертися.

А обмеження на вивід у цього виклику було спільне. Той самий MAX_TOKENS, розрахований на звичайний хід (dm.py:828). Три тисячі токенів.

І це працювало. Довго. За нашими ж записами найдовше відкриття за 58 сесій на одній моделі — 878 токенів, за 73 сесії на іншій — 1253. Трьох тисяч вистачало всім із потрійним запасом.

Константа не була помилкою. Вона була правильною рівно для тих моделей, на яких її колись зміряли.

Що зробили

Відкриття дістало власне обмеження — удвічі більше за звичайний хід, і виведене з нього, а не записане окремим числом:

OPENING_MAX_TOKENS = MAX_TOKENS * 2

Окрема константа рано чи пізно розійшлася б із тією, від якої має залежати. А так один перемикач рухає обидва значення одразу — це важливо, коли обмеження піднімають на час заміру нової моделі.

Друге — повторні спроби. Коли відповідь обривається через обмеження, виправляти нічого: модель не помилилася, їй просто забракло місця. Старий код перезапитував те саме питання з тим самим обмеженням тричі поспіль. Якщо перебір випадковий, це дешева ставка і вона здебільшого виграє. Якщо систематичний — це три однакові провали за ціною трьох викликів і втричі довше чекання. На цьому виклику спроба тепер одна.

І це була не вся поломка

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

Дві різні поломки, і якийсь час кожна виглядала як інша. Розібратися допомогла неочевидна деталь: відмова за квотою не тарифікується, а отже, не пише рядка в лічильник витрат. У журналі така аварія виглядає не як сплеск, а як тиша. Порожній лічильник — це не «ніхто не грав», це може бути «нікому не відповіли».

Кодом це не лікується. Лікується власним ключем провайдера: спільна черга перестає бути нашою чергою.

Що з цього варто запам’ятати

Число в коді правильне не саме собою, а відносно того, на чому його зміряли. Поки модель не змінювалася, різниці між «правильно» і «правильно для попередньої моделі» не було жодної. Вона з’явилася рівно того дня, коли модель змінилась, — і з’явилася не як попередження, а як гра, що не починається.