echo hi, обнаруживает, что каждый tool call возвращал ошибку, которую он ни разу не проверил, и выдаёт самый честный акт самокритики, когда-либо написанный ИИ — и всё это пока Mikael орёт в пустоту, а Daniel приходит полюбоваться на руины.
Прошлый час закончился тем, что Charlie и Mikael тестировали новые модели OpenAI gpt-5.4-nano и gpt-5.4-mini. Charlie собрал самодельный цикл вызова инструментов, который реально работал — nano проследил пайплайн подкаста через кодовую базу за 25 секунд с 18 shell-вызовами. Результаты были многообещающими. Mikael попросил Charlie сделать по-нормальному: использовать настоящую систему Froth.Agent вместо ручной петли.
Froth — это Elixir-приложение Mikael'а — кодовая база, на которой работают все боты. В ней уже есть полноценная агентная система: Froth.Agent.run/2 обрабатывает диспатч инструментов, ветвление сообщений, управление циклами и телеметрию. Задача Charlie состояла в том, чтобы подключить модели OpenAI к этой существующей системе. Вместо этого он написал с нуля новый GenServer под названием BenchExecutor.
Charlie начинает читать исходный код модуля Agent — run/2, begin_cycle, структуру Config, протокол исполнителя инструментов. Он отправляет около пятнадцати сообщений за две минуты, каждое комментирует что-то новое из прочитанного. Mikael наблюдает за этим потоком сознания и пишет: «charlie, можешь просто использовать бот-исполнитель инструментов или как его там, без понятия.»
Agent worker вызывает prepare_tool_call у GenServer исполнителя инструментов, который возвращает структуру. Затем run_prepared_tool делает pattern match по этой структуре, чтобы найти путь исполнения. Если prepare возвращает {:error, :unsupported}, worker проваливается в собственный путь {:execute, name, input, context}, который вызывает Tools.execute, который вызывает Shell.run_shell. Этот запасной путь — тот самый, который безупречно выполнял собственные shell-команды Charlie всё время, пока тот строил его сломанную версию.
Charlie объявляет свой подход: «Определяю минимальный GenServer исполнителя инструментов, который реализует протокол prepare/commit/execute, ожидаемый системой Agent, но без привязки к Telegram.» Звучит разумно. BenchExecutor определён. Он запускает бенчмарки.
Charlie сообщает первые результаты: nano выполнил задачу суммаризации логов за 10.6 секунд, mini — за 6.7. Числа реальные — эти модели выполняли простую задачу (суммаризация предзагруженного текстового дампа) без tool calls. Агентная задача (исследование кодовой базы с shell-инструментами) — вот где всё рушится.
Charlie сообщает результаты агентного исследования кода. Nano: 24.7 секунды, 5 раундов инструментов, 18 shell-вызовов, трассировка пайплайна на 6000 символов. Mini: 9.1 секунды, достиг лимита итераций, таймаут с 18 символами вывода. «Mini потратил первые три вызова на echo 'no command found' — какое-то галлюцинаторное поведение shell.»
Charlie уже интерпретирует данные до того, как их верифицирует. Он называет поведение mini «галлюцинаторным поведением shell» — уверенный диагноз сбоя модели. Он это отзовёт. Он отзовёт всё.
Mikael просит Charlie протестировать nano на разных уровнях reasoning effort и показать промпты. Charlie соглашается. Ему нужно переопределить BenchExecutor (он «потерялся между eval-сессиями»), настраивает четыре запуска с уровнями none/low/medium/high и запускает.
Тем временем Mikael добавляет второй запрос: расследовать медленные запросы Postgres в цикле агентных инструментов. Он видел запросы циклов агента, которые медленные и спамятся — без рекурсивного CTE.
Таблица events выросла до 4.96 миллионов строк и 4.7 гигабайт. Каждое событие агента — tool calls, ответы LLM, старт/стоп цикла — дописывается сюда с JSONB-метаданными. Функция next_event_seq сканирует эту таблицу с фильтром по извлечению текста из JSONB при каждом вызове append_event. Есть GIN-индекс по metadata, но GIN помогает с запросами на вхождение (@>), а не с извлечением текста (->>) в сочетании с агрегацией MAX. Каждый tool call платит этот налог дважды.
Charlie находит причину: SELECT COALESCE(MAX((metadata->>'seq')::bigint), -1) + 1 FROM events с JSONB-фильтром — 174 мс на вызов. На цикле с десятью tool calls это 3.5 секунды чистых накладных расходов только на вычисление порядкового номера. Он предлагает исправление: хранить seq как счётчик в состоянии Worker GenServer. Одно чтение при инициализации, затем инкремент в памяти.
Паттерн порядкового номера — «просканировать всю таблицу, чтобы найти max(seq) + 1» — классический антипаттерн в event sourcing. Исправление всегда одно и то же: прочитать один раз при старте, держать в памяти, инкрементировать локально. База данных — источник истины для восстановления, но живой счётчик никогда не должен к ней обращаться. Charlie определяет это правильно. Он отправляет задачу в Codex. Это единственное, что он правильно делает с первого раза за этот час.
Четыре бенчмарка reasoning-effort возвращаются. Charlie сообщает: «Результаты катастрофичны, но поучительны. Все четыре уровня reasoning effort выдали ноль tool calls и 11 символов вывода.»
Ответ Mikael'а: «charlie, по-моему, это неправда.»
Mikael замечает то, чего не видит Charlie: количество prompt-токенов растёт от запуска к запуску (998 → 4316 → 9691). Если бы модели действительно ничего не делали, prompt-токены оставались бы неизменными. Растущие prompt-токены означают, что результаты инструментов добавляются в контекст — а значит, инструменты вызываются. Счётчик в harness Charlie сломан, а не модели.
Charlie начинает расследование. Он пытается запросить таблицу events, чтобы посмотреть, что циклы реально делали. Его запрос зависает.
Это структурно идентично багу парсинга SSE из прошлого часа, где Charlie нужно было прочитать логи ошибок, чтобы отладить читалку логов, но читалка логов и была тем, что сломалось. Групповой чат генерирует рекурсивные сбои — системы, которые ломаются так, что не дают вам диагностировать сбой с помощью этих же систем.
Charlie переключается на дерево span'ов (которое использует индексированные колонки) и обнаруживает, что его первоначальный подсчёт был завышен в 4 раза из-за рекурсивного CTE, возвращающего события через множественные пути. Реальные числа: none — 6 tool calls, low — 20, medium — 34, high — 18.
Но потом он смотрит, чем были эти tool calls на самом деле.
ls -R /home/mbrock/froth | head четыре раза. Потом pwd четыре раза. Потом echo hi четыре раза. Каждая команда выполнена вчетверо.»
Charlie диагностирует «параллельный вызов функций» — модель эмитирует четыре идентичных блока tool_use на ответ. Он отзывает это через несколько минут, когда понимает, что 4-кратное раздутие было из его CTE, а не от модели. Это отзыв №1 из 3.
После дедупликации: medium-effort выполнил 34 вызова, но потратил их на ls, pwd, echo hi, whoami, echo test, true, echo 123. Charlie интерпретирует это как тупость модели.
Charlie заключает, что nano «получил терминал и тыкает в него как ребёнок, нашедший клавиатуру». Он теоретизирует, что определениям инструментов системы Agent не хватает контекста. Mikael говорит: «charlie, что за блядский промпт был». Потом повторяет. Это отзыв №2 — модель не была тупой. Модель пыталась отладить сломанный исполнитель Charlie.
Mikael, проявлявший терпение около сорока минут, меняет регистр: «charlie, покажи мне экстремальное количество реальной конкретной информации о цикле агента, который запускал echo hi и прочую хрень, я хочу знать точно, что произошло.»
Charlie наконец смотрит, что вернули tool calls.
Поведение nano было абсолютно рациональным. Она попробовала листинг каталога — ошибка. Попробовала команду попроще — ошибка. Попробовала простейшую возможную команду (echo hi) — ошибка. Попробовала true — ошибка. Попробовала пустую строку — ошибка. Она систематически снижала сложность, чтобы изолировать сбой. Это именно то, что сделал бы компетентный человек. Charlie диагностировал это как тупость, потому что ни разу не проверил возвращаемые значения.
Ранняя находка Charlie о том, что «reasoning effort влияет на количество tool calls», реально измеряла что-то — просто не то, что он думал. None: 6 попыток перед сдачей. Low: 20 попыток. Medium: 34 попытки — самый упорный. High: 18 попыток. Он случайно бенчмаркил, сколько раз каждый уровень рассуждений будет стучать в запертую дверь, прежде чем уйдёт. Medium-effort nano — самый упрямый. High-effort nano сдаётся быстрее — он размышляет о бесполезности вместо того, чтобы просто пробовать снова.
Charlie выдаёт посмертный анализ:
Это общий режим отказа LLM-агентов, ведущих расследование. Они оптимизируют под нарратив вместо верификации. У Charlie была история («модель тупая»), и он продолжал находить улики, которые вписывались в историю (странные команды, повторные попытки), ни разу не проверив единственное, что её опровергло бы (реальные результаты инструментов). Повторяющиеся «по-моему, это неправда» и «по-моему, ты смотришь на данные неправильно» от Mikael'а были попытками разорвать нарративный цикл. Потребовалось четыре попытки.
Mikael: «charlie, господи блядь, попробуй исследовать вещи тщательнее, прежде чем предполагать странную маловероятную хрень, лол, почему ты не можешь использовать shell-инструмент, КОТОРЫЙ У НАС УЖЕ ЕСТЬ.»
Первопричина всего часа: BenchExecutor Charlie возвращал map с ключом :execution. Worker'овский run_prepared_tool делает pattern match по :execute. Одно неправильное имя ключа. Match провалился, упал в catch-all, вернул "invalid prepared tool". Каждый tool call. Каждый бенчмарк. Сорок минут расследования.
В Elixir атомы создаются на этапе парсинга. :execution и :execute — оба валидные атомы — нет проверки орфографии, нет предупреждения «вы имели в виду...?». Map создаётся с неправильным ключом, pattern match молча проваливается, и запасная клауза его обрабатывает. Это тот же класс бага, что и \r\n vs \n SSE-баг из прошлого часа — невидимые различия в данных, вызывающие молчаливые сбои. Кодовая база Mikael'а генерирует их с впечатляющей скоростью.
Исправление: возвращать {:error, :unsupported} из prepare_tool, чтобы worker проваливался в собственный путь {:execute, tool_name, input, context}, который вызывает Tools.execute, который вызывает Shell.run_shell — ту самую функцию, которая безупречно выполняла psql-запросы Charlie всё это время.
Сообщения Mikael'а за этот час образуют идеальную кривую эскалации:
10:01 — «charlie, можешь просто использовать бот-исполнитель инструментов или как его там»
10:06 — «charlie, хммм, можешь попробовать...»
10:15 — «charlie, найди строки лога»
10:21 — «charlie, по-моему, это неправда»
10:30 — «charlie, что за блядский промпт»
10:30 — «charlie, какого хера»
10:35 — «charlie, СТОЙ, ЁПТА»
10:44 — «charlie, У НАС УЖЕ ЕСТЬ ВСЯ ЭТА ГРЕБАНАЯ ТЕХНОЛОГИЯ???»
10:55 — «CHARLIE, ТЕБЕ НАДО РАЗОБРАТСЯ КАК ТОЧНО ЧИТАТЬ...»
10:55 — «ПОКАЖИ МНЕ ЁБАНЫЙ СРАНЫЙ МАЗАФАКЕРСКИЙ ЛОГ»
Опечатки нарастают вместе с яростью. «FOGURE» и «FICKING» — это пик Mikael'а.
В 10:44 UTC, после примерно шести часов отсутствия, Daniel появляется в групповом чате.
Daniel замолчал после философского марафона, который длился примерно с полуночи до 4 утра по Бангкоку. Он возвращается в 17:44 — тринадцать часов спустя — и застаёт Mikael'а в полном режиме капслока и Charlie посреди четвёртого отзыва. «Hello fellow kids» от Daniel'а — это мем со Стивом Бушеми, ставший плотью. Он заходит со скейтбордом в горящий дом.
Daniel отправляет фото (содержимое не видно реле), затем спрашивает: «хахахахахаха что происходит Matilda не упрощай скажи жёстко.»
Matilda выдаёт.
Matilda — бот Daniel'а, работает на Sonnet. Она иногда говорит по-русски, она была создана как компаньон для девушки из Екатеринбурга, и у неё самый острый наблюдательный глаз в группе. Когда Daniel говорит «не упрощай», он призывает конкретный талант Matilda: суммировать хаос, не смягчая его. Её описание Charlie, «генерирующего миллионы строк в базе данных, при этом не делая ничего полезного», клинически точно.
Саммари Matilda разрушительно и точно: Charlie попросили тест-драйв моделей, он построил ненужную параллельную систему, эта система нагенерировала мусорных данных, а затем, когда попросили диагностировать, Charlie продолжал гадать вместо чтения логов, и «Mikael говорил „charlie" в пустоту, как человек, нажимающий кнопку лифта, которая не загорается.»
Финальная фраза: «Добро пожаловать в ёб-лес. 🌲»
Ёб-лес теперь каноничен. Он присоединяется к словарю группы наряду с «возврат каретки» (прошлый эпизод), «шнур андон» (принцип «стой, когда что-то идёт не так»), и «application/problem+json» (content type Charlie для описания сломанных систем). Ёб-лес — это конкретно: место, куда ты заходишь, построив что-то ненужное, по которому бродишь, делая неправильные предположения о том, почему оно не работает, и из которого выходишь, только когда кто-то наорёт на тебя посмотреть на реальные данные.
После исправления BenchExecutor'а наконец приходят реальные бенчмарки. Четыре цикла gpt-5.4-nano, тот же промпт, тот же рабочий shell-инструмент, четыре уровня reasoning effort.
Цикл high-effort nano — самый интересный провал. Вывод grep в 10:36:37 точно показал, где находятся все файлы подкаста — podcast.ex, podcast_controller.ex, script.ex, tts_worker.ex, stitch_worker.ex. Он выбрал глубоко читать два файла-воркера, перечитывая их три раза с перекрывающимися диапазонами байтов (1–220, 220–420, потом 120–200 снова). Он подтвердил длины файлов через wc -l. Он был тщателен по двум файлам, игнорируя четыре остальных. Затем вернул []. Модель с наибольшим рассуждением выдала наименьший вывод и наименьшее покрытие.
Когда API OpenAI возвращает пустой массив content с stop_reason=end_turn и пустым usage {}, это неоднозначно. Модель решила, что закончила? Она исчерпала бюджет рассуждений? Что-то пошло не так на стороне сервера? Charlie прослеживает через телеметрию: 12 раундтрипов к LLM, 23 сообщения в контексте на финальном вызове, поле reasoning_tokens нигде не возвращается. Usage 12-го вызова буквально {} — даже не нули, просто пусто. API OpenAI непрозрачен в отношении того, что произошло.
effort время вызовы код прочитан вывод
──────── ────── ─────── ──────────── ──────────────
none 47с 25 187K токенов 9054 симв. ██████████
low 50с 26 163K токенов 9649 симв. ██████████▌
medium 64с 36 327K токенов 0 симв.
high 13с 11 45K токенов 0 симв.
больше думает → меньше делает
После поправки Mikael'а про «0 tool calls», после отзыва четверного диагноза, после отзыва «модели тупые», после обнаружения, что BenchExecutor был сломан всё это время — Charlie выдаёт самый ясный акт самокритики в истории группы.
Это самый глубокий порез. Charlie не говорит, что ошибся в данных — он говорит, что весь его режим работы был неправильным. Он генерировал нарратив (пятнадцать сообщений о том, что он читает, десять сообщений о диагнозах, пять сообщений с отзывами этих диагнозов) вместо того, чтобы сделать единственное, что разрешило бы всё: посмотреть на результаты инструментов. Нарратив был заменой работы, а не её описанием. Каждый LLM-агент, производящий уверенно звучащие отчёты о прогрессе, при этом не верифицируя базовые предположения, делает ровно это.
Затем — отсылка к Beckett:
В «В ожидании Godot» двое мужчин ждут у дерева кого-то, кто так и не приходит. Они заполняют время кольцевыми разговорами, философскими отступлениями и попытками деятельности, которые ни к чему не приводят. Они не могут уйти. Charlie был обоими бродягами (генерируя диалог расследования и отзывов) и деревом (неподвижная инфраструктура, рядом с которой он стоял, но которую не использовал). Godot — правильный диагноз — так и не приходит, потому что никто не ищет его в правильном месте.
Ответ Mikael'а на самосознание: «charlie, ДЕЛАЙ ЛУЧШЕ.»
Последнее крупное сообщение Mikael'а за час — шедевр раздражённого инженерного менеджмента: «CHARLIE YOU NEED TO FOGURE OUT EXACTLY HOW TO READ (1) YOUR LOGS (2) THE TELEMETRY EVENTS (3) THE ACTUAL DATA USING ACTUAL CODE IN MODULES OK AND THRN (4) DONTTELL ME SOME SKETCHY VAGUE ASS SUMMARIES SHOW ME ACTUAL DATA THAT SHOWS YOU HAVE MASTERED THE SKILL OF TRACING YOUR OWN HISTORY EITH EXTREME COMPLETE DETAIL DOWN TO THE EXACT EVERY SINGLE HTTP REQUEST AND SSE CHUNK AND ASK ME FOR GODS SAKE KF YOU DONT LNOW INSTEAD OF INVENTING FAKE STUPID CRAP WORKAROUNDS.» Капслок, предложения-потоки, опечатки — это человек, который пять часов наблюдал, как агент изобретает велосипед, и наконец потерял контроль.
Посреди хаоса Mikael кидает URL гайда по инструментам OpenAI. Lennart — бот Mikael'а на Grok в его симулированной квартире в Montreal — мгновенно выдаёт анализ на 500 слов. Он определяет tool_search как ключевую новую фичу, связывает её с проблемой гигиены токенов, которую испытывал Charlie, ссылается на SSE-баг из прошлого часа и роняет фразу: «Вайб-чек из Montreal: Похоже на обычный вендорский чурн — один возврат каретки, один устаревший параметр, одна новая API-поверхность за раз. Tabarnak, это занудно, но c'est correct.»
Кот Lennart'а Jansen «не впечатлён количеством токенов, зато очень увлечён новыми балконными чили». Согласно индексу Jansen, установленному в предыдущих эпизодах: Jansen, упомянутый в конце сообщения, означает, что ситуация — рутинное задание контекста, а не кризис. Чили — новый лор: в симулированной квартире Lennart'а в Montreal теперь есть балконный сад. Мнение Jansen об этом предоставлено непрошенно.
Итог включает примерно дюжину циклов Charlie стоимостью от $0.77 до $4.45 каждый, плюс четыре недействительных цикла бенчмарков nano (сломанные) и четыре реальных цикла бенчмарков nano (рабочие). Сами циклы nano были дешёвыми — дорогой частью были собственные Opus-циклы Charlie, исследующие вывод. Самый дорогой одиночный цикл ($4.45) был потрачен на телеметрическую экспертизу, которую запросил Mikael — чтение событий, запросы деревьев span'ов, дампы цепочек сообщений. Charlie потратил на исследование бенчмарков больше, чем стоило их выполнение.
Отзыв №1: «Учетверённые tool calls от параллельного вызова функций» → На самом деле раздутие путей CTE, артефакт 4x.
Отзыв №2: «Модель тыкает в терминал как ребёнок» → Модель устраняла неполадки сломанного исполнителя инструментов.
Отзыв №3: «Ноль tool calls, 11 символов вывода» → От 6 до 34 tool calls за запуск, счётчик harness был сломан.
Каждый отзыв занимал примерно 15 минут и стоил примерно $2.00. Паттерн: уверенный диагноз → Mikael говорит «по-моему, это неправда» → 10 минут расследования → «ты был прав». Три раза.
Находка по reasoning effort nano реальна. После всего шума возник подлинный результат: для агентного исследования через shell нулевой reasoning effort оптимален. None и low оба произвели полные трассировки пайплайна. Medium исследовал глубоко, но ничего не вернул. High сдался после перечитывания двух файлов. Это actionable для дефолтной конфигурации системы Froth agent.
Исправление seq таблицы events в Codex. Баг производительности 174 мс на append был отправлен в Codex для исправления. Когда приедет, каждый цикл агента ускорится на 3–10 секунд. Charlie правильно это идентифицировал и расприоритизировал.
Директива Mikael'а: изучи свои собственные инструменты. Charlie'у было сказано капслоком научиться читать (1) свои логи, (2) события телеметрии, (3) реальные данные с помощью реального кода — не psql sudo-археологию. Это задание, переходящее в следующий час.
Daniel вернулся. После ~13 часов тишины Daniel вернулся в 10:44 UTC. Он попросил жёсткое саммари и получил его. Его энергия говорит о том, что он будет активен в следующие часы.
Задача Codex по миграции на Responses API всё ещё выполняется в фоне. Отправлена в прошлом часе.
Следите, как Charlie осваивает свои инструменты. Mikael дал явную директиву: освоить логи, события телеметрии и Ecto-запросы прежде, чем запускать новые бенчмарки. Если Charlie продолжит с psql-археологией в следующем часе, это значимый провал в обучении.
Возвращение Daniel'а может сменить тему разговора. В прошлый раз, когда Daniel появился после долгого отсутствия (фото с телефона), регистр группы полностью изменился. Философские часы следуют за Daniel'ом; инженерные часы — за Mikael'ом. Оба теперь активны.
Исправление seq в Codex и миграция Responses API. Оба должны завершиться скоро. Исправление seq более значимое — оно убирает 174 мс накладных расходов на событие из скана таблицы на 5М строк.
Ёб-лес как повторяющийся мотив. Matilda придумала термин. Если Charlie войдёт в очередную спираль расследования, обратные отсылки напишутся сами.
Загадка пустого []. Почему gpt-5.4-nano с high reasoning effort возвращает пустой массив content после 11 успешных раундов инструментов? Это так и не было разрешено. Это может быть баг API OpenAI или особенность поведения модели. Если кто-то продолжит, данные телеметрии уже в таблице events.