Слогер Создать блог
Разработка

Как проектировать формат фикстур для записей разговоров с инструментами ИИ

Почему фикстуры умирают, что с этим делать и как сделать так, чтобы их читали, перезаписывали и не боялись коммитить.

Фикстуры для записи разговоров с инструментами ИИ abandon-ят по двум причинам: никто не может прочитать дифф, когда одна из них меняется, либо повторная запись переписывает весь файл, и невозможно понять, что сдвинулось. Обе проблемы — свойства формата, которые определяются до того, как написана первая фикстура. Исправить их потом — дорого.

Что формат должен выдерживать

  • Ревью. Ревьюер, смотрящий на изменённую фикстуру, должен видеть в диффе, что модель начала делать иначе. Если дифф — одна строка минифицированного JSON, ревью превращается в «выглядит нормально», и фикстура перестаёт работать.
  • Перезапись. Повторный запуск рекордера после изменения должен дать файл, который отличается только там, где отличается поведение. Случайные id, таймстампы и количество токенов нарушают это правило. Фикстура, которая меняется целиком при каждой записи, перезаписывается без чтения.
  • Смена провайдера. Фикстура, записанная под один API и хранящаяся в его wire-формате, бесполезна в день добавления второго. Хранение нейтральной формы стоит адаптера и спасает от перезаписи всего.
  • Коммит в репозиторий. Реальные разговоры содержат реальные данные. Формат с определённым местом для редактирования — редактируется; формат без него — отправляет адрес клиента в репозиторий.

Форма

Один файл на сценарий, названный по сценарию. Небольшой заголовок, затем плоский список ходов, где каждый ход говорит, кто его создал и что он содержал.

{ "meta": { "scenario": "refund-damaged-item", "recorded": "2026-08-04", "provider": "chat-completions", "model": "recorded-from-production", "note": "policy tool returns eligible; happy path" }, "turns": [ { "from": "user", "text": "order 55219 arrived smashed" }, { "from": "assistant", "calls": [ { "ref": "c1", "tool": "get_order", "args": { "order_id": "55219" } } ]}, { "from": "tools", "results": [ { "ref": "c1", "output": { "sku": "MUG-01", "total_cents": 1499 } } ]}, { "from": "assistant", "calls": [ { "ref": "c2", "tool": "start_refund", "args": { "order_id": "55219", "amount_cents": 1499, "reason": "damaged" } } ]}, { "from": "tools", "results": [ { "ref": "c2", "output": { "state": "pending" } } ]}, { "from": "assistant", "text": "I've refunded EUR 14.99." } ] }

Четыре решения делают всю работу. args — это разобранный объект, хотя Chat Completions передаёт строку: JSON-строка внутри JSON нечитаема в диффе, а адаптер может перекодировать её в одну строку. ref — это c1, а не call_9xKq2LmR, поэтому id стабильны между записями. Результаты инструментов — отдельный ход, а не приложение к вызову, так что две стороны обмена редактируются по отдельности. И meta.note — это проза о том, для чего этот сценарий: поле, которое спасает фикстуру от удаления во время чистки, потому что никто не знал, что она покрывает.

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

Что вырезается и почему

  • Провайдерские id вызовов — случайны при каждом запуске, поэтому превращают каждую запись в изменение всего файла. Заменяйте на c1, c2 в порядке появления.
  • Таймстампы и id запросов — по той же причине. Если ход действительно зависит от времени, это значение тест должен инъектировать, а не фикстура.
  • Количество токенов и задержка — они меняются с версиями моделей и относятся к записи стоимости, а не к поведенческой фикстуре. Фикстура, падающая из-за изменения на три токена, учит всех игнорировать падения фикстур.
  • Персональные данные — заменяйте стабильными псевдонимами, а не удаляйте: исчезнувшее поле меняет форму, с которой тестируется цикл. Запускайте редактирование в рекордере, чтобы оно не забывалось, и проверяйте тестом, что ни одна фикстура не совпадает с вашими очевидными паттернами.
  • Ключи и токены — их вообще не должно быть в аргументах инструментов. Рекордер, отказывающийся писать файл с ними, — дешёвая вторая линия защиты.

Что сохраняется: пробелы и регистр ровно так, как их выдала модель. Если нормализовать их, вы теряете то, что захотите увидеть, когда изменение промпта начнёт давать аргументы в другом формате.

Одна фикстура — несколько тестов

Смысл разделения ходов модели и результатов инструментов в том, что большую часть ценности даёт их перекомбинация. Один записанный разговор поддерживает happy-path тест, использующий обе стороны; тест падения, который оставляет ходы ассистента и подставляет ошибку в один результат; тест усечения, который сокращает один вывод до вашего лимита; тест редактирования, который добавляет секрет в один вывод и проверяет, что он никогда не попадёт в промпт.

Производные варианты лучше делать в тесте, а не коммитить четыре почти одинаковых файла. Заваленный вариант расходится с родительским в первый же раз, когда перезаписывают только один из них, и две фикстуры начинают противоречить друг другу, описывая один разговор.

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

Перезапись без потери диффа

Перезапись — момент, когда набор фикстур либо сохраняет ценность, либо тихо её теряет. Сделайте её явной командой с указанием сценария — ни в коем случае не флагом, который переписывает всё упавшее. Так реальная регрессия может быть закоммичена как обновление.

  1. Перезаписывайте один сценарий за раз, явно называя его, в тот же файл.
  2. Читайте дифф. Если изменения — только id или таймстампы, ваша вырезка неполна; чините рекордер, а не принимайте шум.
  3. Для каждого настоящего изменения решайте, улучшение это или регрессия, до коммита. Весь смысл формата в том, чтобы дифф был достаточно мал, чтобы на этот вопрос можно было ответить.
  4. Обновляйте meta.recorded и, если поведение изменилось, заметку. Заметка, описывающая поведение, которого в фикстуре больше нет, хуже отсутствия заметки.

Храните фикстуры в репозитории, а не в объектном хранилище. Они маленькие, они нужны ревьюеру рядом с изменением кода, а фикстуру, которую нельзя прочитать без креденшелов, никто не читает. Тот же аргумент работает в большем масштабе для переигровки продакшн-трафика.

По материалам: ai. Текст переработан редакцией Слогера.

← На главную

Рекламное место — Конец поста
Реклама · Слогер

Комментарии (0)

Войдите, чтобы комментировать.

Пока нет комментариев. Будьте первым.