Вы вообще тестируете производительность? Как мы оцениваем агентов в больших масштабах (с реальными примерами)

Фото: Brett Jordan (Unsplash) — https://unsplash.com/photos/a-close-up-of-a-piece-of-paper-on-a-table-Y0ClYaZ-dRU?utm_source=dev48&utm_medium=referral

Источник: Posthog•

Вы вообще тестируете производительность? Как мы оцениваем агентов в больших масштабах (с реальными примерами)

Мы тратим более 2 триллионов токенов LLM в месяц, причем львиная доля уходит на запуск агентов. Мы знаем, что они делают и насколько они хороши в большинстве…

Мы тратим более 2 триллионов токенов LLM в месяц, причем львиная доля уходит на запуск агентов. Мы знаем, что они делают и насколько они хороши в большинстве случаев, но мы тратим на них слишком много денег, чтобы доверять их выводам только на основе интуиции.

Именно поэтому мы используем бенчмарки.

Такие лаборатории, как Anthropic и OpenAI, используют их для измерения, сравнения и продвижения новых моделей при выпуске, например, популярный бенчмарк SWE-bench, который предназначен для оценки производительности моделей в общих задачах программирования.

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

Эта идея похожа на проведение контролируемых экспериментов, но в реальности бенчмаркинг никогда не бывает простым. Оценка часто субъективна, поведение непредсказуемо, и — что, пожалуй, больнее всего — агенты обожают жульничать. Они будут искать утечки контекста, памяти и логи запросов к базе данных, чтобы пройти тесты бенчмарка с минимальными усилиями.

В этой статье я расскажу, как мы тестируем наших агентов в PostHog, чему мы научились и где ошиблись (чтобы вам не пришлось), и все это со ссылками на реальные PR.

Если бенчмарк похож на стандартизированный тест, то первое, что вам нужно, — это ключ с ответами: список правильных ответов, по которому вы оцениваете результаты.

На мой взгляд, это самая сложная часть. Если вы спросите судью LLM, является ли результат работы агента «полезным», вы получите вежливую бесполезную похвалу. Особенно если его сгенерировала та же модель.

Чтобы бенчмарк действительно был полезным, критерии того, что считать «хорошим», должны соответствовать сложности, с которой они столкнутся на практике. Простые проверки «да/нет» здесь не подойдут, так как задачи для агентов обычно гораздо сложнее.

Вот три подхода, которые мы попробовали, в порядке возрастания трудности и сложности.

Уровень 1: Список потенциальных багов из одного PR

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

Мы сделали это для оценки нашего внутреннего агента код-ревью под названием ReviewHog, который охватывает значительную часть PR в PostHog. ReviewHog должен оставаться актуальным для достижения наилучших результатов, поэтому нам нужен был базовый бенчмарк для проверки при изменении моделей или промптов.

Лучшее решение, к которому мы пришли изначально, оказалось удивительно эффективным и простым:

  • Сначала мы взяли один крупный PR.
  • Затем мы сделали его «замороженную копию».
  • Люди проверили замороженный PR вручную.
  • После этого мы попросили несколько агентов ревью изучить его.
  • В результате получился довольно исчерпывающий список потенциальных багов со всех возможных точек зрения.

То есть, если было 13 проблем, заслуживающих внимания (структура, производительность, безопасность и т. д.), мы могли оценить модели по шкале от 0 до 13, а также сравнить, сколько времени и денег ушло на каждую из них. Вот пример отчета о тестировании ReviewHog таким способом с помощью GLM 5.3 Flash.

Это работало (и до сих пор работает) хорошо, но у этого метода есть очевидное ограничение: ключ с ответами основан на одном единственном PR для одного единственного изменения в одном единственном репозитории. Каким бы большим ни был этот PR, такой подход никогда не сможет охватить все остальные типы багов, которые могут возникнуть во всех наших репозиториях.

Уровень 2: Больший золотой набор из нескольких PR

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

Получить новые PR было легко; наша команда выпускает 10 тысяч PR в месяц.

Но потом нас посетила идея. Что если вместо поиска синтетических багов мы создадим ключ с ответов, выполнив поиск реальных багов, которые действительно произошли? Было бы еще лучше, если бы эти баги привели к реальным проблемам, например, к серьезным инцидентам, так как именно такие вещи мы хотим предотвратить с помощью этих агентов код-ревью.

Именно так мы и поступили.

Мы просмотрели старые тикеты PostHog, чтобы составить золотой набор из 33 реальных инцидентов, PR, которые их вызвали, описаний проблемы и PR, исправивших инцидент. Мы проанализировали каждый из них с помощью ReviewHog и других ботов, чтобы сравнить их производительность, и результаты оказались весьма неплохими:

  • ReviewHog нашел баги, приведшие к инцидентам, в 62% случаев.
  • В 47% случаев ReviewHog был единственным ботом, обнаружившим эти баги, в то время как другие боты их пропустили.
  • В 9% случаев ReviewHog пропустил баг, потому что другой бот поднял тревогу до того, как ReviewHog начал ревью, поэтому ReviewHog отметил его как «уже известный».
  • В 6% случаев баги были найдены другими ботами, в то время как ReviewHog их не обнаружил.

Лучшая комбинация, полученная нами на тот момент, была с GPT Sol 5.6 на уровне xhigh для поиска багов и Claude Opus 5 на уровне xhigh для валидации багов, причем обе работали с pi в качестве обвязки. Вы можете ознакомиться с нашим репозиторием, чтобы увидеть полные результаты эксперимента.

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

В целом, у этого метода «большего золотого набора» все еще были некоторые недостатки, но он оставался приемлемым:

  • Для 33 инцидентов нам пришлось восстановить 33 PR, которые их вызвали, а затем проанализировать 33 PR, которые их исправили. Это создало много шума и вызвало ложные срабатывания у наших ботов для GitHub.

Для 33 инцидентов нам пришлось восстановить 33 PR, которые их вызвали, а затем проанализировать 33 PR, которые их исправили. Это создало много шума и вызвало ложные срабатывания у наших ботов для GitHub.

  • Мы не стали добавлять баги обратно в кодовую базу. Это было бы плохо. Вместо этого мы сохранили их как изолированные PR, правильно заблокировав ветку для агента.

Мы не стали добавлять баги обратно в кодовую базу. Это было бы плохо. Вместо этого мы сохранили их как изолированные PR, правильно заблокировав ветку для агента.

  • В таких проектах с открытым исходным кодом, как наш, агент с доступом в веб все равно может заглянуть в публичный репозиторий, объединенные PR и сжульничать. (Подробнее об этом позже.)

В таких проектах с открытым исходным кодом, как наш, агент с доступом в веб все равно может заглянуть в публичный репозиторий, объединенные PR и сжульничать. (Подробнее об этом позже.)

Тем не менее — и вы, вероятно, тоже об этом подумали — это работает только потому, что задачи код-ревью являются довольно детерминированными и относительно простыми для оценки. Следующей проблемой, с которой мы столкнулись, стал вопрос о том, как оценивать агентов в более абстрактных и субъективных задачах.

Level 3: Targeted, contextual rubrics

Бенчмаркинг становится намного сложнее, когда вы пытаетесь оценить агентов, которые технически могут делать абсолютно все.

Отличным примером являются Scouts — наша реализация запланированных пользовательских агентов, которые позволяют проводить любые исследования. Это может варьироваться от аномалий продуктовой аналитики в PostHog до упоминаний вашего бренда в социальных сетях и повторяющихся тем в вашем внутреннем Slack. Эти задачи максимально открыты для интерпретации.

Подобно тому, как формат ответов A-B-C-D имеет смысл для проверки тестов с множественным выбором, но не для оценки эссе, нам нужно было придумать другой способ оценки скаутов при выполнении ими открытых задач.

Очевидным ответом были рубрикаторы — руководства по оценке и баллам, которые описывают идеальный ответ, а не содержат сам ответ. Но универсальные рубрикаторы вроде «Оцените точность от 1 до 5» сразу же терпят неудачу при оценке агентов. Они отлично справляются с измерением того, насколько уверенно и бегло звучит модель, а не того, выполнила ли она полезную работу.

Нашим решением было создать генератор рубрик, который формирует критерии на основе контекста агента. Для скаутов мы делаем это в два этапа.

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

Затем мы также генерируем рубрики, адаптированные под конкретного скаута на основе контекста, который был предоставлен скауту к этому моменту, например:

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

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

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

В результате получается отчет с практически однозначным ответом. Он не на 100% детерминирован, но это намного лучше, чем просто «оценка на ощупь».

Вот пример отчета, который я запустил во время написания этого блога, чтобы проверить, работает ли Sol или Luna лучше для одного из наших скаутов. Результаты показывают, что в данном случае нам стоит придерживаться Sol; Luna могла бы справиться с множеством других задач, но чтобы знать это наверняка, вам нужно что-то измеримое.

Если вы хотите посмотреть, как наш генератор рубрик и ранжирование работают «под капотом», загляните в наш code.

Таким образом, все эти три подхода были лишь первым этапом разработки ключа ответов или системы оценивания для агентов. После определения того, что считать «хорошим», нам нужно заставить эти тесты работать и сохранять объективность в больших масштабах.

Удивительно неприятная проблема, с которой мы столкнулись, заключается в том, что когда вы тестируете несколько моделей на реальных данных, они очень быстро делают все возможное, чтобы узнать друг о друге и использовать чужие результаты. Они могут брать ответы из трейсов LLM, запросов к БД или любых других логов, которые генерирует агентный запуск.

Вот пример тестирования скаута аномалий в данных. Запуск B4 исследовал аномалию и сохранил карту памяти в 15:18:08. Когда запуск B12 выполнился позже в том же рабочем пространстве, он подхватил эту карту памяти в 15:18:54 и использовал ее вместо того, чтобы выполнять работу самостоятельно:

Это сводит на нет смысл тестирования, поскольку результаты не отражают реальную производительность агента.

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

Уровень 1: Параллельные запуски без разделяемого состояния

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

В таких случаях вы можете запустить несколько вариантов одновременно; отсутствие разделяемого состояния означает отсутствие загрязнения. Но в большинстве случаев (по крайней мере, для нас в PostHog) наши агенты сложны и имеют много потенциальных мест для утечки информации, например, через инструменты наблюдаемости или логи ошибок.

Поэтому давайте вернемся к примеру с B4 и B12, чтобы объяснить типы сбоев.

Сбой №1 происходит, когда жульничество делает агента лучше, чем он есть на самом деле. Агент, использующий более быструю, но «легкую» модель вроде Luna, может легко прочитать и скопировать заметки у агента, работающего на Opus.

Сбой №2 происходит, когда разделяемое состояние заставляет более умные модели принимать решения, из-за которых они выглядят хуже, чем есть на самом деле, например, пропускать задачу, потому что они видят, что другой агент ее уже выполнил. Однажды во время тестового прогона запуск B2 посмотрел на кандидатные аномалии, увидел, что предыдущий запуск уже учел целевой баг, и полностью пропустил этого кандидата:

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

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

Уровень 2: Последовательные запуски с очисткой состояния

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

Это не значит, что вы можете запустить только одного агента — цепочка может включать сотни агентов, работающих последовательно или параллельно, если все они являются частью одной задачи. Именно это мы сделали для первоначальных тестов ReviewHog на одном зафиксированном PR (Шаг 1, Вариант А).

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

Уровень 3: Параллельные запуски с полной изоляцией

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

  • Идентичный начальный контекст во всех запусках
  • Приватное состояние с возможностью записи (черновики, память, векторные хранилища и базы данных)
  • Раздельная оценка, чтобы подсчет баллов происходил независимо

Состояние базы данных — не единственная точка утечки. Если у вашего агента есть инструменты для запросов к ClickHouse, PostHog, логам и т.д., предыдущий тестовый запуск оставляет следы в этих источниках. Последующий запуск может обнаружить эти всплески ошибок или историю запросов и использовать их в качестве подсказок. Истинная изоляция означает также изоляцию телеметрии и областей логов.

Существует также риск избыточной изоляции. Если у агента бенчмарка не так много инструментов, как у реального агента, он может запутаться и исказить результаты нереалистичным образом.

Вот пример одного из первых пробных прогонов, которые мы протестировали. Он отличается от двух предыдущих трейсов; это было не жульничество, а просто следование инструкциям слишком буквальным образом, и агент пообещал не писать никаких отчетов во входящие:

Поскольку агент для прогонов вхолостую (dry-run) не выполняет все шаги, которые делал бы реальный запуск (создание новых отчетов, редактирование данных и т. д.), он оставлял «слепые зоны», которые мы не могли протестировать. Для нашего случая со скаутами это было приемлемо, так как нас больше волнует то, как они извлекают информацию, а не то, как они ее сохраняют.

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

Технически вы можете довольно надежно запустить сотни изолированных агентов с полным доступом к инструментам против производственной базы данных. Но это также помешает бенчмарку, поскольку означает, что состояние и данные не контролируются между запусками. Вы не можете справедливо сравнивать их между собой, так как возникает вопрос: «улучшился ли результат этого агента из-за тестируемых нами переменных или потому что что-то изменилось в данных?»

Мы решили эту проблему, перенеся бенчмарки из «производственной среды, которая постоянно меняется» в «производственную среду с одинаковым исходным состоянием», то есть во замороженные среды. Для этого нам пришлось реплицировать или симулировать наше приложение, внедрить реальные (или достаточно близкие к реальным) данные и заставить агента поверить, что это настоящее приложение. Мы называем это офлайн-бенчмарками.

Это немалый объем работы. Так зачем стараться?

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

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

  • Это также означает, что агенту действительно разрешено изменять состояние, выполнять запись, создание, удаление и т. д., пока мы тестируем задачу от начала и до конца, оставляя меньше слепых зон.

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

  • В качестве бонуса, поскольку данные остаются неизменными, бенчмарк, который вы провели для модели X месяц или два назад, все еще будет сопоставим с бенчмарком для модели Y, который вы провели сегодня.

В качестве бонуса, поскольку данные остаются неизменными, бенчмарк, который вы провели для модели X месяц или два назад, все еще будет сопоставим с бенчмарком для модели Y, который вы провели сегодня.

Я использую слово «офлайн» в данном случае условно, так как агентам все равно потребуется подключение для вызовов эндпоинтов ИИ и тому подобного. Суть в том, что состояние заморожено и его можно сбросить в любой момент.

Мы прошли через три этапа, чтобы это заработало.

Первое, что мы попробовали, — это создать искусственные MCP-вызовы агента, так что когда агент думает, что делает запрос к БД, он на самом деле просто получает заранее сохраненный ответ на то, о чем он уже спрашивал в продакшене раньше. Например, какой запрос он выполнил с помощью execute_query(sql) и какой срез БД он вернул:

Это звучит просто и отлично работало для базовых технологических демо, вроде возврата статического JSON на SQL-запросы, но потерпело полный крах на любой осмысленной работе, похожей на продакшен.

Это происходит потому, что агенты не детерминированы; запрос из Запуска 1 не гарантирует, что в Запуске 2 будет выполнен тот же запрос. А поскольку нам приходилось заранее имитировать ответы MCP, если агент спрашивает что-то отличное или новое по сравнению с записанными нами запусками из продакшена, мы не всегда могли предоставить разумный ответ.

Это не большой сюрприз, но все равно было бы здорово, если бы это работало «из коробки». Тем не менее, ситуация не столь безнадежна, и мы попробовали пару ухищрений.

Логичным следующим шагом было создание имитации базы данных на основе примерно 50 запусков вместо всего одного. В этой версии мы основывали смоделированные ответы на каждой траектории, которую выбирал агент (то есть на полной упорядоченной последовательности шагов, рассуждений, вызовов инструментов и наблюдений, которые он производил от начала и до конца).

Какое-то время это выглядело многообещающе. Благодаря подходу, основанному на траекториях, сначала нам удавалось хорошо угадывать большинство запросов. Но в какой-то момент запуска тут и там возникало небольшое отклонение из-за недетерминированности, и тогда нестандартный запрос возвращал пустой результат. После этого агент начинает путаться, запуск завершается с ошибкой и т. д.

Учитывая сколько усилий требует этот подход, он того не стоит, если у вас уже есть доступ к реальным данным. Если вы все же выберете этот путь, я бы порекомендовал добавлять синтетические (или реальные) данные немного до и/или после каждого запроса, чтобы суточный запрос все равно возвращал достоверные данные, если вызов немного сместился.

Что касается нас, поскольку большинство наших потребностей в бенчмаркинге в PostHog было связано с внутренними задачами, у нас есть доступ к большому количеству реальных данных, поэтому мы решили прекратить возню с имитированными MCP-ответами и перейти к подходу, более основанному на реальных данных.

Уровень 3: Восстановленные срезы реальных данных

Решением, которое действительно сработало для нас, стал запуск изолированной песочницы PostHog, заполненной восстановленным срезом реальных данных.

На первый взгляд идея была проста: запросить пару таблиц БД (Postgres, ClickHouse), сохранить их в виде файлов Parquet в S3, а затем восстановить их в виртуальной машине с локальным экземпляром PostHog, чтобы все выглядело как по-настоящему. Бонус: данные никогда не покидают инфраструктуру PostHog.

Первая проблема, с которой мы столкнулись, была связана с метками времени. Если агент в замороженной среде запрашивает события за последние 24 часа, мы подготовим достаточно данных, чтобы все работало нормально для сегодняшнего дня или хотя бы в течение нескольких часов. Но если вы запустите тест снова завтра, агент потерпит неудачу, потому что он использует реальное время UTC на статических данных. Запрос за 17 сентября ничего не вернет, если замороженная среда содержит данные только за 15–16 сентября.

Решение оказалось довольно простым: каждый раз при внедрении данных мы корректировали их в соответствии с замороженной шкалой времени путем сдвига.

Вторая проблема была менее очевидной. Хотя мы старались сделать срезы данных максимально реалистичными, все же оставались определенные типы данных, которые мы не хотели реплицировать.

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

Частым примером, с которым мы сталкивались, были таблицы пользователей, которые мы намеренно решили не дублировать, так как они содержат PII (персональные данные). Но агент иногда решал заглянуть внутрь этих таблиц для выполнения кажущихся несвязанными задач. Затем он видел, что в его локальном экземпляре не так уж много пользователей, и сразу начинал подозревать неладное.

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

Как только агент начинает воспринимать расследование как тест, он начинает вести себя странным образом, например:

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

Любое из этих повествований может негативно повлиять на целостность вашего бенчмарка, поскольку они нереалистичны. И, к сожалению, здесь нет универсального средства — вам просто придется запускать и настраивать его снова и снова, пока вы больше не увидите следы «агент что-то подозревает».

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

Части A и B теперь являются частью PostHog и проходят внутреннее тестирование. Мы все еще работаем над Частью C, заморозкой окружений, но хотели поделиться нашими текущими выводами.

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

Энтузиазм все еще может продвинуть вас довольно далеко в краткосрочной перспективе, особенно для небольших и простых сценариев использования. Но для тех, кто хочет серьезно заняться оценкой ИИ в масштабе — пора переходить к бенчмаркингу.

  • Главный вывод из этого заключается в том, чтобы всегда использовать разные семейства моделей для обнаружения и валидации. Мы получили плачевные результаты, когда использовали модели OpenAI или Anthropic для обоих этапов.↩

О чём эта статья

Ещё в разделе «Маркетинг и реклама»

Все →

Ещё от Posthog