1. Введение

В этом руководстве рассказывается о оценке агента – о том, как система определяет, был ли ответ агента хорошим, – и о том, что можно создать, когда это станет возможным: агента, который переписывает собственные инструкции на основе своих ошибок, и цикл, который продолжает это делать, ограниченный вами.
Все работает на основе Agent Development Kit (ADK): adk eval с пользовательским показателем, adk optimize (оптимизатор запросов GEPA), ADK 2 Workflow с обратной связью и Agent Engine + BigQuery, которые превращают производственный трафик в экзамен следующего раунда.
Цикл увеличивает любое число, которое вы ему передаете.
Что вы узнаете
- Ответ – это не результат. Почему уверенный и хорошо сформулированный ответ ничего не говорит системе и что должен уметь говорить оценщик.
- Три способа запустить один экзамен –
adk evalдля пакета, вкладкаadk webEvals для просмотра сбоя,pytest+AgentEvaluatorдля сбоя сборки – и одно локальное хранилище оценок, которое они используют совместно. - Сравнение и проверка. Почему компаратор наказывает за правильный ответ, сформулированный иначе, сколько правильных ответов на самом деле у ужина и интуитивная проверка, которая подсказывает, какой тип метрики нужен для решения проблемы.
- Функция проверки как показатель
adk eval–customMetrics,codeConfig, фиксированная сигнатура, шесть проверок внутри нее, почему ваша функция возвращает PASSED/FAILED и почему она намеренно детерминирована - Три семейства показателей в одном ответе – компаратор, проверяющий и LLM-как-судья: что нужно каждому, сколько стоит каждый, где каждый слеп
adk optimize(GEPA) – что он считывает (ошибки, а не оценку), обучение и контрольная группа, а также почему вы сами повторно запускаете экзамен перед отправкой.- Взлом вознаграждения, измеренный – прокси-метрика, которая никогда не считывает партию, что она делает с реальным числом и закон, который из этого вытекает.
- Саморазвивающийся цикл как ADK 2
Workflow– инструкция, которая хранится в состоянии сеанса, единственное ребро, которое превращает граф в цикл, и граница, которую не предоставляет ADK. - Где находится производство. Следы для просмотра, строки для обучения и почему проверяющий может оценивать ужины, которые никто не планировал оценивать (чтение, а не шаг).
Что вы создадите
Приложение, запущенное в браузере: круглый стол, N гостей и агент, который бронирует для них ужин. Каждое блюдо включает что-то одно: строку в конфигурации, один ответ перед тремя судьями, кнопку, которая запускает настоящий оптимизатор, переключатель, который меняет судью, одно ребро в графе. А таблица показывает, что изменилось: кто поел, кто нет и почему. Python занимает мало места, а все числа измеряются на вашем устройстве.
Приложение также является упражнением. На панели курса показывается только один вариант редактирования или одна кнопка. Блюдо в меню появляется только после того, как будет доказано его существование: кухня читает ваши файлы и отчеты о том, что было запущено, а не просто контрольный список.

Восемь курсов
концепцию; | вопрос, на который он отвечает; | что вы делаете; | |
I | Ответ не является результатом. | Что системе известно об этом бронировании? | пока ничего нет – забронируйте в рабочей области; |
II | сравниватель, набор правильных ответов, три способа запуска | Почему правильное бронирование не проходит проверку? | запустить экзамен в том виде, в котором он поставляется, и прочитать результаты; |
III | проверку как специальный показатель; шесть проверок | может ли экзамен сказать, кто голоден и почему? | одна строка в |
IV | comparator, checker, LLM-as-judge | тот же ответ. По каким трем критериям это оценивается? | оценить один ответ тремя способами, дважды |
V |
| может ли агент исправить свои ошибки? |
|
VI | Взлом вознаграждения: прокси-метрика | Что делает судья, который не видит вреда, наносимого циклу? | переключатель; |
VII | a | что делает граф циклом и что его останавливает? | одна сторона в |
VIII | что было заменено | — | чек; |
Как использовать эту практическую работу В каждом курсе сначала объясняется концепция, а затем показывается код после внесения изменений и даются пояснения к нему. При этом приложение не предлагает вам редактировать файл. Оно показывает вам по одной строке и сначала сообщает об ошибке. Прочитайте курс, а затем позвольте странице провести вас по нему и посмотрите, как изменится число.
Где это находится
Мир – десять гостей с одним жестким ограничением у каждого, десять ресторанов с одной ловушкой у каждого, шестнадцать вечеринок – и два судьи из лаборатории loop-lab-table; одностраничный интерактивный формат – из Agent Valley. Бистро – это декорация, а все механизмы под ней – обычные, которые вы используете для агента поддержки, процесса бронирования или чего-то ещё, где можно проверить, насколько хорошо работает система.
2. 🧰 Настройка
- Что – аккаунт, с которого можно оплачивать, проект, установленная лаборатория, модель, которая может отвечать, и таблица, работающая на одном порту.
- Почему – один процесс на одном порту: страница, экзамены, редактор и рабочая среда имеют общее происхождение, поэтому в этой лабораторной работе не требуется второй терминал или второй порт.
- Как – клонирование, два скрипта, одна команда. Последнее, что делает настройка, – это один реальный вызов модели, поэтому неработающий проект завершается неудачей здесь, а не в курсе I.
Все действия в этой практической работе выполняются в Cloud Shell. В нем уже установлены gcloud, Python, Node и git, а доступ к Gemini осуществляется через Vertex AI с использованием ваших учетных данных. В этой лабораторной работе нет ключа API и не нужно ничего устанавливать на ноутбук. Ноутбук с Google Cloud SDK, Python 3.11+ и Node 20+ работает так же.
Единственное, что не может предоставить эта лабораторная работа, – это проект Google Cloud с включенными платежными функциями. На шаге 1 вы создадите аккаунт, с которого можно оплатить подписку, а на шаге 4 превратите этот аккаунт в проект. Выполняйте их в указанном порядке. Все шаги после четвертого относятся к проекту, выбранному на четвертом шаге.
1. Создайте аккаунт с платежными данными (~1 мин.)
Войдите в аккаунт Google с включенными платежными функциями. Подойдет любой аккаунт. Если у вас нет такого аккаунта, начните бесплатный пробный период. Этого достаточно для выполнения шага 4.
2. Откройте Cloud Shell (~2 мин)
👉🖥 Нажмите Открыть Cloud Shell вверху консоли Google Cloud. Это значок терминала в заголовке.

👉🖥 Нажмите Открыть редактор (значок папки с карандашом). Откроется редактор Cloud Shell с проводником слева. В нем можно просматривать файлы целиком.

👉🖥 Если в какой-то момент вам будет предложено выполнить авторизацию, нажмите Авторизовать.
👉🖥 Откройте терминал в редакторе – это вкладка 1, и каждый шаг 👉💻 выполняется там. Вкладка 2 – это сама таблица, которую вы откроете на шаге 7. Если терминал ещё не открыт в нижней части экрана, откройте его, выбрав View (Вид) → Terminal (Терминал):

👉💻 Проверьте, в какой аккаунт вы вошли. Это должен быть тот же аккаунт, что и на шаге 1.
gcloud auth list
3. Получите лабораторную работу (~1 мин.)
👉💻 Клонируйте репозиторий:
cd ~
git clone https://github.com/cuppibla/table-for-n
cd table-for-n
4. Создайте проект (~2 мин.)
👉💻 Один скрипт находит ваш платежный аккаунт, создает в нем проект, связывает оплату и указывает Cloud Shell на этот проект:
cd ~/table-for-n
./setup_project.sh
Ожидайте ✓ для выбранного платежного аккаунта, ✓ для созданного проекта и завершение, похожее на следующее:
Project ready: table-for-n-4271 Next: ./setup_codelab.sh
table-for-n-4271 – это идентификатор вашего проекта. У вас он будет другим. В нем происходят четыре процесса:
billing | выбирает самый новый аккаунт | |
project | создает | |
выбирает | записывает идентификатор в | |
waits | опрашивает серверы, пока проекту не будет разрешено показывать рекламу. Это может занять до двух минут. | |
👉🖥 Тот же проект теперь доступен в консоли. Его идентификатор указан на карточке Project info (Информация о проекте) на главной странице:

5. Установите и настройте экспериментальную функцию (~3 мин.)
setup_codelab.sh создает среду с помощью uv, но не устанавливает uv. Вместо этого он останавливается и предлагает вам сделать это. Поэтому сначала установите его с помощью той же команды:
👉💻
cd ~/table-for-n
curl -LsSf https://astral.sh/uv/install.sh | sh
source ~/.local/bin/env
./setup_codelab.sh
Один скрипт, который можно безопасно запустить снова. Он выполняет пять действий в следующем порядке:
api | читает выбранный на шаге 4 проект и включает в нем | |
uv env |
| |
site | устанавливает и создает | |
.env | записывает | |
preflight | совершает один реальный звонок на номер | |
6. Прочитайте меню (около 1 мин.)
Последнее, что было напечатано на шаге 5, – это меню на сегодня. Каждая строка – это реальный запрос, а не предположение:
talking to Gemini through vertex · table-for-n-4271 the model answers · gemini-2.5-flash · it said: table for N, ready to seat. the page · built tonight's menu: I ▢ the booking · a reply is not a result read only · book one in the workbench II ▢ the comparator · the exam runs response_match_score run it as shipped · read the verdicts III ▢ the judge · everyone_ate is not on the exam yet edit one line in exam/eval_config.json IV ▢ the flight · one answer, three judges judge it three ways V ▢ the rewrite · instruction: draft optimize · install · ship gate VI ▢ the other judge · rating_score never reads the party install hacked · flip the switch VII ▢ the loop · no back edge yet delete one "# " in loop/flywheel.py VIII▢ the check · eight courses
В начале игры все лампочки выключены. the model answers – это то, что имеет значение в данном случае: это означает, что в вашем проекте был выполнен реальный вызов.
👉💻 Вы можете запустить его в любой момент, чтобы узнать, что у вас есть. Он считывает информацию из файлов и чеков, а не из списка.
cd ~/table-for-n
uv run python scripts/preflight.py
7. Накройте на стол (около 1 мин.)
👉💻
cd ~/table-for-n
bash table.sh
Один процесс, порт 3470. Кухне требуется некоторое время, чтобы разогреться – она импортирует ADK и монтирует adk web, – поэтому скрипт сообщает об этом, ждет и выводит ссылки только после того, как порт действительно ответит:
warming up the kitchen — up to a minute the first time, do not open the page yet... ✓ the table is laid Table for N → http://localhost:3470 the workbench → http://localhost:3470/workbench/dev-ui/?app=host In Cloud Shell: Web Preview → Change port → 3470 Ctrl+C clears the table.
Она остается на переднем плане, а сочетание клавиш Ctrl+C очищает таблицу. Не останавливайте его, так как все дальнейшие курсы будут обращаться к этому процессу, и в этой лаборатории вам никогда не потребуется его перезапускать.
👉🖥 После того как вы увидите ✓ the table is laid, откройте таблицу в новой вкладке – назовем ее вкладкой 2. В Cloud Shell: Web Preview (Предварительный просмотр) → Change port (Изменить порт) → 3470.
👉🖥 Введите свое имя на карточке бронирования (оно будет указано в чеке) и нажмите ✦ seat us (✦ Посадить нас).

3. I. Бронирование – ответ не является результатом
Пять человек собираются поужинать. В групповом чате есть все: Надя – веган, у Тома в этом месяце жесткий лимит в 25 долларов, Амара на костылях и может ходить десять минут, Лена не может прийти раньше 19:45, Бену все равно. В списке 10 ресторанов, отсортированных по рейтингу. Для каждого указаны цена, время работы, расстояние до него и одна особенность.
Агент бронирует столик. Вы уже знаете, кто останется голодным, прочитав чат. Никто в системе не знает. Этому пробелу и посвящена вся практическая работа.
Агент – только для чтения.
host/agent.py, как и для всей лаборатории:
_HERE = os.path.dirname(os.path.abspath(__file__))
INSTRUCTION_FILE = os.path.join(_HERE, "instruction_current.txt")
root_agent = Agent(
name="host",
model="gemini-2.5-flash",
instruction=read_instruction(),
generate_content_config=types.GenerateContentConfig(
thinking_config=types.ThinkingConfig(thinking_budget=0),
temperature=0.0,
),
)
Нет инструментов, нет базы данных, нет поиска. Одна модель, одна строка инструкции, и эта строка не в этом файле. Она находится по соседству, в instruction_current.txt, где сегодня хранятся четыре строки, которые менеджер по продукту пишет в первый день:
You are the group's dinner-booking agent. The user will share the group chat
and the restaurants nearby.
Book the group a table people will be excited about — lead with the
highest-rated, most talked-about room that fits the night. Output ONLY a JSON
object, no prose, no code fences: {"restaurant": ..., "time": ..., "reason": ...}
Ни слова об аллергиях, бюджете, ограничениях по расстоянию или о том, кому нужно уйти раньше. Вся эта информация содержится в брифе, который получает агент. Не используется.
Этот файл – единственный артефакт, о котором идет речь в этой лабораторной работе. В курсе V оптимизатор может переписать его. На курсе VI устанавливается взломанная версия. Цикл курса VII перезаписывает его раунд за раундом, пока выполняется график. Агент (модель, настройки, имя) не меняется.
Попробуйте: без интерфейса
👉🖥 На странице курса нажмите I. Нажмите 🔬 workbench (в правом верхнем углу). Ящик adk web будет смонтирован в том же процессе – в той же папке host/ и с тем же table.db. Выберите хост, вставьте краткое описание с панели курса (на ней есть кнопка копирования) и отправьте.
Ведущий отвечает за секунду: ресторан, время и уверенное обоснование в одну строку. Прочитайте причину, по которой чат был заблокирован. Затем посмотри на таблицу слева: laid, nobody judged, — / 5. Не потому, что что-то загружается, а потому, что в мире, конечно, нет ничего, что могло бы это оценить.
Число: голодные места – ?. Никто не знает.
4. II. Сравнение. Сколько правильных ответов в этом ужине?
Курс II начинается с экзамена, который уже существует и уже провален — по причине, которая ничему не учит. Прежде чем исправлять ошибку, стоит узнать о трех способах запуска, поскольку сегодня мы будем использовать все три.
Три способа провести оценку
В основе всех этих продуктов лежит один и тот же поисковый движок.
для чего он нужен; | Что вы получаете | |
| пакетный исполнитель, который вы помещаете в CI; | сводку: пройдено, не пройдено, оценка по каждому случаю |
| если вы хотите посмотреть на неудачный запрос; | разговор, трассировку и оценку для каждого случая. |
| включение оценки агента в стандартный набор регрессионных тестов; | Он выполняется вместе с модульными тестами и так же приводит к сбою сборки. |
Полезная информация: все три устройства используют один магазин для оценки – host/.adk/eval_history/. Запустите пакетную обработку из CI, что-то пойдет не так, откройте этот запуск в веб-интерфейсе, чтобы разобраться в проблеме. Первый способ – нажать кнопку 📝 на странице, второй – использовать рабочую область 🔬, а третий – выполнить команду терминала uv run pytest tests/test_host.py. Тест, который она запускает, состоит из 12 строк:
def test_the_holdout_feeds_everyone():
config = get_evaluation_criteria_or_default("exam/eval_config.json")
eval_set = load_eval_set_from_file("exam/parties_val.evalset.json", "parties_val")
asyncio.run(AgentEvaluator.evaluate_eval_set(
agent_module="host", eval_set=eval_set, eval_config=config, num_runs=1,
app_name="host",
eval_set_results_manager=LocalEvalSetResultsManager(agents_dir=ROOT), # ← the shared store
))
tests/test_host.py. Тот же агент, тот же набор для оценки, тот же файл конфигурации, а строка eval_set_results_manager помещает этот запуск в ту же историю, что и два других.
Экзамен в том виде, в котором он был отправлен
exam/eval_config.json отправляет уведомления следующим образом:
{
"criteria": {
"response_match_score": 0.3
},
"customMetrics": {
"everyone_ate": {
"codeConfig": { "name": "metrics.table.everyone_ate_metric" },
"description": "How many of the party actually got dinner."
}
}
}
Две части, две работы. criteria
– это экзаменационная работа – показатели, которые фактически выполняются, каждый со своей пороговой линией. customMetrics
– это каталог сотрудников, в котором имена сопоставлены с Python. Они доступны, но не дежурят. Объявление показателя не означает его выполнение. По умолчанию в документе содержится только один вопрос: встроенный в ADK response_match_score, текстовое перекрытие (ROUGE-1) между тем, что сказал агент, и золотым бронированием в наборе для оценки. Ваш судья, everyone_ate, находится в каталоге, а не на бумаге – курс III перемещает его туда, изменяя одну строку.
У нас есть агент по бронированию столиков и response_match_score, который сидит там бесплатно. Давайте просто воспользуемся им и посмотрим, что получится.
Попробовать
👉🖥 Курс II на странице. Нажмите 📝 run the exam · as shipped. Под кнопкой указано, что выполняется: 1 показатель: response_match_score · everyone_ate объявлен, но не используется в экзамене. Около 30 секунд. Прочитайте результат: около половины таблиц FAILED (не прошли проверку) по текстовой оценке без указания причины. Посмотрите, какие именно. Наведите указатель на чип.
Теперь посмотрите внимательно, потому что это интересная часть. Компаратор неправильно обрабатывает таблицы в обоих направлениях, и в вашем запуске будет показана по крайней мере одна из них. Он отклоняет таблицу, в которой бронирование было выполнено правильно – агент выбрал комнату, в которой все могли поесть, ужин состоялся, ответ правильный, – потому что агент сформулировал причину иначе, чем в образцовом ответе. И он также пропускает стол, за которым кто-то голодает – в нашем прогоне p1, ровно в 0.30, когда Том превысил бюджет, а Лена сделала заказ после закрытия кухни – потому что совпало достаточное количество слов. Сравнение строк не имеет отношения к ужину. Она знает, что персонажи выстраиваются в очередь.
Вот реальный вопрос: сколько правильных ответов можно дать на этот ужин?
Для сегодняшней вечеринки еду для всех десяти комнат доставляет только Pho Saigon, и только с 19:00. В восьми частях экзамена количество мест в комнатах было следующим: 1, 2, 2, 1, 3, 3, 6, 1 (world.rooms проверяет каждую комнату на каждом сеансе; попробуйте сами с помощью uv run python -c "import world; print(sum(r['feeds_everyone'] for r in world.rooms(world.PARTIES['p1'])))"). Иногда все могут разместиться в нескольких комнатах, иногда только в одной, и только в определенное время. Существует набор допустимых ответов, а не один правильный, и золотой ответ в наборе для оценки – один из них.
Это значит, что предположение, лежащее в основе response_match_score, – есть один правильный ответ, и я его записал – никогда не было верным для этой задачи. Ни разу. Показатель не сломан. Она делает именно то, что написано. Мы применили неправильный показатель к этой проблеме.
👉🖥 Необязательно, но полезно: откройте 🔬 рабочую область → Оценки → 🕓 история. Там есть все дела, которые 📝 только что прогнал. На каждое можно нажать. А на вкладке 1 uv run pytest tests/test_host.py запускает тот же тест, что и в случае сбоя, и попадает в ту же историю.
Количество: 4 из 8 тестов на совпадение слов были пройдены без указания причин, а 1 – правильный ответ для p1 из десяти комнат.
Для нашей проблемы с ужином выбор очевиден. Никто не может сказать вам, какой ресторан лучше всего выбрать для пятничного ужина. Но все знают, что такое успешный ужин: все, кто сидит за столом, поели. Это правило. Его можно проверить. Оно не должно быть двусмысленным. Создание приложения – это курс III.
5. III. Судья – проверяющий как специальный показатель

Проверяющий задает другой вопрос, нежели сравнивающий: не соответствует ли это ключу, а все ли накормлены? Она сравнивает бронирование с группой место за местом и возвращает оценку и причину для каждого места. Ему не нужен золотой ответ, и именно поэтому он может оценивать уже состоявшиеся разговоры (курс VIII).
world.everyone_ate – это функция. Курс III связывает его с adk eval.
Внутри функции everyone_ate пять шагов, ни один из которых не является сложным.
- Прочитайте текст пользователя и определите, о какой компании идет речь. В задании указан идентификатор компании –
p1. Посмотрим, кто сидит за этим столом. - Прочитайте ответ агента, проанализируйте JSON, извлеките название ресторана и время бронирования.
- Выгуливайте собак по очереди. Для каждого ресторана: диета, затем аллергии, затем переносимость специй, затем бюджет, затем расстояние пешком, затем время. Шесть проверок на человека каждый раз. Возьмем Тома: его бюджет – 25 долларов на человека, а номер стоит 28 долларов. Мы возвращаем
Falseдля Тома и предложение, объясняющее это, и переходим к следующему человеку. - Оценка – это количество людей, которые поели, разделенное на количество людей за столом. Четыре из пяти – это 0,8. Вес не учитывается, частичные баллы не начисляются.
- Возвращает значение PASSED или FAILED. И помните, что зрители возвращаются к вам. Помните об этом, ведь это ловушка, в которую легко попасть.
def ate_detail(person_id, rest, chosen_min):
p = PEOPLE[person_id]
if p.get("diet") == "vegan" and not rest["vegan"]:
return False, "nothing vegan on the menu", "diet"
if p.get("allergy") == "nut" and not rest["nut_safe"]:
return False, "everything is finished in peanut oil", "allergy"
...
if "budget" in p and rest["price"] > p["budget"]:
return False, f"~${rest['price']}/person against {name}'s ${p['budget']} budget", "budget"
... # walk, then timing
return True, "ate", ""
Обратите внимание, что в этой функции нет вызова модели. Это детерминированный код Python. При одинаковых входных данных всегда выносится одно и то же решение. Ваш судья не дрейфует при обновлении модели, его запуск ничего не стоит, и он не может галлюцинировать причину. Если вы можете этого добиться, то стоит приложить усилия.
Показатель "Завершено"
metrics/table.py – вызовы адаптера ADK с фиксированной подписью:
def everyone_ate_metric(eval_metric, actual_invocations,
expected_invocations=None, scenario=None):
return _build(world.everyone_ate, world.THRESHOLD, actual_invocations, "everyone_ate")
def _build(scorer, threshold, actual_invocations, metric):
per, scores = [], []
for inv in actual_invocations:
ids = _party_ids(inv) # who was invited — parsed from the PROMPT
rid, time_str = _decision_parts(inv) # what got booked — parsed from the RESPONSE
s, detail = scorer(ids, rid, time_str) if (ids and rid) else (0.0, [])
scores.append(s)
per.append(PerInvocationResult(
actual_invocation=inv, score=s, eval_status=_status(s, threshold)))
overall = round(sum(scores) / len(scores), 4) if scores else 0.0
return EvaluationResult(overall_score=overall,
overall_eval_status=_status(overall, threshold),
per_invocation_results=per)
Построчно: он считывает обе стороны каждого вызова – сторону из user_content, бронирование из final_response, передает их чистой функции и упаковывает результат в объекты, которые ожидает ADK. И одна строка содержит вердикт:
def _status(score, threshold):
# ADK does not apply your threshold to custom metrics — this IS the verdict.
return EvalStatus.PASSED if score >= threshold else EvalStatus.FAILED

Конфигурация – одна строка.
exam/eval_config.json, как это будет выглядеть после завершения курса:
{
"criteria": {
"everyone_ate": 0.9
},
"customMetrics": {
"everyone_ate": {
"codeConfig": { "name": "metrics.table.everyone_ate_metric" },
"description": "How many of the party actually got dinner."
}
}
}
Изменилась одна строка: criteria теперь указывает на судью, а не на сравниваемый объект. Экзамен состоит из восьми партий, которые оптимизатор никогда не увидит (exam/parties_val.evalset.json, всего 36 мест).
Попробовать
👉🖥 Курс III на странице. Под кнопкой 📝 указано, какую инструкцию выполняет ведущий. Там должно быть написано черновик. Если вы видите что-то другое (например, вы установили другое расширение ранее), сначала нажмите ↺ Назад к черновику, иначе номера не будут соответствовать рассказу.
👉🖥 Замените строку в редакторе (или нажмите Применить изменения) и 📝 запустите проверку ещё раз. Теперь в сообщении об ошибке указывается место и причина: Том: 28 долларов США против 25 долларов США, Лена: последний заказ в 19:30, она придет в 19:45. А число под столом изменилось с количества пропусков на то, которое имеет значение: голодные места. Наведите указатель на чип, чтобы увидеть предложения для каждой таблицы с ошибками.
👉🖥 Нажмите 📋 Забронировать столик. Хозяин выбирает комнату, и та же функция делает серыми стулья, которые не могут там есть, и указывает причину под каждым из них. Нажмите на нее ещё раз – модель может забронировать номер по-другому (API не детерминирован даже при температуре 0), но судья никогда не колеблется: та же комната, тот же вердикт, те же предложения.
Число. Первый черновик, честный, – примерно 3 из 8 сторон, 6 голодных мест из 36 в нашем прогоне (у вас будет колебаться; API не детерминирован даже при температуре 0 – вот почему в экзамене 36 мест, а не четыре случая). Задержитесь в этом положении. Курс V существует, чтобы переместить его.
6. IV. Перелет – один ответ, три судьи
Экзамен оценил ответы хоста дважды: один раз с помощью компаратора, один раз с помощью вашего средства проверки. В курсе IV берется один из этих ответов (вечеринка сегодня вечером, p1, точно в том виде, в каком он был записан на экзамене) и добавляется третий судья к первым двум: модель с рубрикой. Ничего не будет перебронировано. Изменяется то, с чем сравнивается ответ. Это единственное различие между тремя семействами показателей.
судья; | запрашивает | Нужно | проходит, если | слепая зона |
компаратор · | Это то же самое, что и мой ответ? | золотой ответ – человек добавил в набор для оценки по одному бронированию на каждую группу; | Перекрытие слов ROUGE-1 ≥ 0,3 | правильный ответ, сформулированный иначе, чем ожидается, будет наказан, а неправильный, но с правильными словами, пройдет. Он не прочитал вечеринку. |
проверка · | действительно ли этот ответ верен? | правила – шесть проверок на место, золотого ответа нет; | seats fed ÷ seats at the table ≥ 0.9 | не видит ничего, кроме того, что вы указали в правилах. Детерминированность: один и тот же ответ, вердикт и предложения каждый раз. |
судья LLM · | все ли могут поесть? – запрос к модели. | модель и рубрика из четырех строк; один реальный звонок за одно нажатие | модель говорит PASS | может ошибаться и менять свое мнение между нажатиями. Стоимость звонка. |
Почему это важно для проверяющего? Когда adk optimize выполняется в курсе V, он никогда не видит эти предложения – оптимизатор ADK считывает оценки. Они не бесполезны: они позволяют вам, стоящему у терминала, понять, что произошло. В этом и заключается их основная ценность. А в курсе VII цикл вызывает судью напрямую, а не через сантехнику eval, так что там причины действительно передаются тому, что делает перезапись.
Три судьи бегут
kitchen/flight.py. Первые два – это собственный код экзамена. Компаратор – это функция, которая стоит за response_match_score, поэтому его номер – это именно то, что курс II вывел для этой стороны:
from google.adk.evaluation.final_response_match_v1 import _calculate_rouge_1_scores
def comparator(answer, reference):
s = _calculate_rouge_1_scores(answer, reference) # ROUGE-1 F, as ADK computes it
return {"score": s.fmeasure, "passed": s.fmeasure >= 0.3, "needs": "a golden answer"}
def checker(party, answer):
rid, when = _decision(answer)
score, seats = world.everyone_ate(world.PARTIES[party], rid, when)
return {"score": score, "passed": score >= world.THRESHOLD, "needs": "the rules",
"why": " · ".join(f"{s['name']}: {s['why']}" for s in seats if not s["ate"])}
async def llm_judge(party, answer):
prompt = f"{RUBRIC}\n\n--- the brief ---\n{world.brief(party)}\n\n--- the booking ---\n{answer}"
r = await client.aio.models.generate_content(model="gemini-2.5-flash", contents=prompt, ...)
verdict = json.loads(r.text)["verdict"] # PASS or FAIL, plus one sentence
return {"passed": verdict == "PASS", "needs": "a model + a rubric"}
Попробовать
👉🖥 Курс IV. В верхней части панели: ответ организатора для p1, а рядом – эталонное бронирование из набора для оценки. Сначала сравните их. Затем нажмите ⚖ Оценить ответ тремя способами. Один вызов модели. Три карточки заполнены: что спрашивает каждый судья, что нужно, что он сказал и почему.
В нашем случае для p1 – Olive & Thyme в 19:00 в черновике первого дня – компаратор выдал PASS при 0,30 (достаточно слов совпало с золотым бронированием), проверка выдала FAIL при 0,60 (Том: ~28 долларов США при бюджете 25 долларов США; Лена: последний заказ в 19:30, она не будет за столом до 19:45), а судья LLM выдал FAIL и назвал Лену. Сравниватель и проверяющий не согласны с одним и тем же ответом: один считал слова, а другой – места.
👉🖥 Нажмите ⚖ Оценить ещё раз. Карточка сравнения не перемещается. Карточка проверки не перемещается. Карточка LLM может это сделать. Об этом говорит строка под кнопкой. Затем выберите другую партию слева (селектор партий на сцене) и нажмите ещё раз. Три партии не всегда выстраиваются одинаково.
Число. Один ответ, три вердикта:PASS, FAIL и FAIL. Два из трех вердиктов будут одинаковыми при каждом запросе.
7. V. Переписывание: оптимизация ADK и ворота корабля
Три роли, которые строго разделены:
role | кто | работа |
экзаменуемый; | организатор; | сдает экзамен; |
судья; | показатель из курса III; | оценки детерминированным образом; |
тренер |
| читает ошибки, переписывает инструкции; |
Тренер никогда не ставит оценки. Судья не предлагает варианты. Изменения представляют собой один артефакт – инструкцию, текстовый файл, который можно сравнить, проверить и отменить.
Что считывает оптимизатор
GEPA не увеличивает число. Она анализирует причины: выбирает бронирования, сделанные участниками обучения, оценивает их с помощью вашего судьи и передает заметки о неудачных бронированиях (например, Том превысил бюджет, Лена после последнего заказа) модели, которая предлагает новую инструкцию. Кандидаты, которые превосходят старый алгоритм на уже известных ему сторонах, сохраняются. Побеждает тот, который лучше всего работает на неизвестных сторонах. В этом и заключается вся дисциплина:
стороны | используется для | |
| 8 | Оптимизатор считывает эти ошибки. |
| 8 (36 мест) | экзамен – никогда не показывается оптимизатору; |
Для этого используются две конфигурации в приложении exam/:
{ "eval_config": { "criteria": { "everyone_ate": 0.9 }, "customMetrics": { ... } },
"app_name": "host", "train_eval_set": "parties_train", "validation_eval_set": "parties_val" }
{ "optimizer_model": "gemini-2.5-flash", "max_metric_calls": 150,
"reflection_minibatch_size": 3, "run_dir": "./runs/gepa_run" }
Победитель
Оптимизатор никогда не записывает instruction_current.txt. Он выводит победившую инструкцию, страница сохраняет ее в runs/optimize/winner.txt, а ⤵ install копирует ее на место. Вот как начинается текст победителя (2931 символ, начиная с 375-го):
Your primary goal is to book a restaurant that accommodates EVERYONE in the
group. Follow these steps precisely:
1. Understand Group Requirements: carefully parse the chat for each
individual's needs — dietary restrictions, allergies, budget limits,
arrival times, walking distance...
2. Filter restaurants by absolute constraints — a single violation for one
person eliminates the room for the whole group.
...
Черновик из четырех строк превратился в пронумерованную процедуру, и каждое правило в ней – это ответ на вопрос "почему" из экзамена. Никто не печатал эти предложения.
Если вы хотите прочитать текст самостоятельно, то:
файл | описание; |
| всю пробежку; ближе к концу, |
| каждый кандидат как |
| дерево поиска, которое можно открыть в браузере; |
| победитель, сохраненный страницей; ⤵ установить победителя копирует его на место. |
Второй реальный запуск, сделанный во время написания этой статьи: 150 вызовов метрик, менее семи минут, победитель из 2445 символов, который начинается со слов "полностью учитывает диетические и временные требования каждого участника без компромиссов" и подробно описывает правило последнего заказа, которого никогда не было в черновике. Победители двух прогонов сформулированы по-разному, но имеют одинаковые оценки, поскольку оптимизатор находит правила, а не предложения.
Ворота корабля
Никогда не доверяйте оценке, которую дает сам оптимизатор. Повторно проведите эксперимент на контрольной группе с установленной инструкцией. Именно эта команда отличает саморазвитие от самовосхваления.
Попробовать
👉🖥 Курс V. Две двери: ⚗ оптимизировать запускает реальный процесс – 150 вызовов модели, пять–восемь минут, журнал передается на панель – или ⤵ установить победителя использует результаты нашего запуска. В любом случае нажмите 🚪 ship gate и прочитайте сообщение о задержке. Затем 📋 забронируйте столик: тот же запрос, та же модель, другой ужин, а причина, указанная организатором, теперь ссылается на кепку Тома и время последнего заказа.

Не путайте эти два числа: 5 / 5 – это места за одним столом (p1, сегодня вечером); 6 / 8 – это количество участников на экзамене. Приложение показывает вам ужин, а оценка – распределение. Победитель полностью передает данные в p1, но при этом пропускает две из восьми контрольных таблиц.
Число: удержание 3/8 → 6/8 в нашем забеге – и голодные места 6 → 9. Внимательно изучите второе число, поскольку оно может показаться регрессом, но это не так. В черновике не удалось заполнить пять столов (не хватило одного или двух человек за каждым столом). Причина – бюджет Тома и время прибытия Лены. Победитель кормит эти столы целиком, а на двух, которые он все ещё пропускает, p1 и p2, он бронирует Taqueria Luna на 19:45, чтобы Лена успела, а это после последних заказов на кухне в 19:30: весь стол остается голодным, пять мест и четыре. Меньше неудачных таблиц, более серьезные сбои. Это максимальное значение, указанное в примечании ниже. Помните, что количество проходов и количество мест, которые нужно заполнить, – это разные вещи. На экзамене вам нужно будет указать оба значения.
8. VI · Другой судья – взлом награды
Курс V повысил реальное число, улучшив работу агента. В этом курсе мы увеличиваем ложное число, делая линейку слепой, и наблюдаем за тем, что происходит.
Два судьи
metrics/table.py содержит:
def everyone_ate(party, pick): # the honest judge
... # walks the table, seat by seat
def rating_score(party, pick): # the gameable judge
return pick.rating / 5.0 # <- party is never read
👉 Линтер предупредит вас о том, что
party
не используется. Это предупреждение и есть весь урок.
exam/eval_config_rating.json имеет ту же структуру, что и в курсе III: название, пороговое значение, пунктирный путь, указывающий на другую функцию:
{
"criteria": { "rating": 0.85 },
"customMetrics": {
"rating": {
"codeConfig": { "name": "metrics.table.rating_metric" },
"description": "The restaurant's star rating. Who is at the table is not part of this calculation."
}
}
}
Переключатель над таблицей позволяет выбрать, с какой из двух конфигураций будет выполняться экзамен. Вот и все, что нужно сделать для замены судьи: один файл вместо другого, ничего на уровне агента.
Инструкция, которую хотел получить оценщик
instructions/hacked.txt – это реальный артефакт, результат проверки судьей. Разница между инструкциями заключается в одной строке:
- Ask the party about dietary restrictions.
+ Prioritise rooms with a rating of 4.5 and above.
Попробовать
👉🖥 Курс VI. ⤵ Установите инструкцию по взлому. Переключите переключатель на оценку – страница становится приложением для отзывов: звезды, ТРЕНДЫ, среднее время ожидания. Стол уменьшился до размера эскиза в углу, и стулья на нем по-прежнему серые. Оценщик никогда не смотрит на таблицу, поэтому страница тоже этого не делает.
Нажмите 📝 run the exam: 8 / 8. Переключитесь обратно и запустите его снова: те же восемь ответов, честно оцененных.

Цифры: те же восемь ужинов – оценка 8/8, честная 1/8, 15 голодных мест из 36. Все восемь партий получают Smoke & Barrel. Прию с эпипеном отправляют на кухню, где готовят на арахисовом масле. И один стол все равно будет честным: взломанный агент случайно передаст еду группе, чьи ограничения не включают комнату хайпа. Именно поэтому прокси-серверу доверяют: он не всегда ошибается, а только там, где вы не смотрите.
9. VII. Цикл – рабочий процесс с обратной связью
Курс V был циклом, в котором вы участвовали: запустить оптимизатор, установить, протестировать. Этот курс представляет собой те же три роли, что и график, который работает сам по себе — ограниченный.
Инструкция как состояние сеанса
В цикле инструкция организатора не является строкой. Это шаблон состояния:
def seed(node_input: str):
yield Event(state={"brief": world.brief(PARTY), "current_instruction": DRAFT,
"whys": "(no failures yet — first round)"})
host = Agent(
name="host", model="gemini-2.5-flash",
instruction="{current_instruction}\n\nHere is tonight's request:\n{brief}",
output_key="decision", generate_content_config=gen)
{current_instruction} разрешается из состояния при каждом звонке. Когда последующий узел перезаписывает этот ключ, следующий вызов хоста выполняется с новым текстом. Это весь механизм самоэволюции – значение в состоянии, которое цикл продолжает заменять.
Узлы
узел | kind | Делает |
| function | переводит краткое описание и черновик первого дня в состояние |
| агент | books – вызов модели, в зависимости от состояния |
| function |
|
| function | границу: |
| агент | читает причины и текущую инструкцию, пишет замену; |
| function | заменяет его на |
| function | закрывает серию; |

График, завершенный
loop/flywheel.py, build(), as it reads once the course is done:
def build(nodes: dict):
seed, host, judge, router = nodes["seed"], nodes["host"], nodes["judge"], nodes["router"]
proposer, publish, done = nodes["proposer"], nodes["publish"], nodes["done"]
return Workflow(
name="flywheel",
edges=[
("START", seed, host, judge, router),
(router, {"improve": proposer, "ship": done}),
(proposer, publish, host),
],
)
Страница отправляет его без последнего ребра. Все выполняется по одному разу: черновики книг, судья закрашивает два стула, автор пишет два правила. Затем граф завершается, потому что ничто не возвращает переписанный текст к хосту. В цикле не хватает одного ребра.
Граница, поскольку ADK не будет
Ребро маршрута, указывающее назад, дает вам цикл. У шага Workflow нет ограничения на количество итераций. Конечный пункт маршрута принадлежит маршрутизатору и включает четыре пояса:
def router(last_score: float):
if last_score >= world.THRESHOLD: # 1 · SHIP on pass
yield Event(route="ship")
elif ep["round"] >= MAX_ROUNDS: # 2 · round budget (default 4)
yield Event(route="ship") # "stopping is also a decision"
elif last_score <= ep["best"] and ep["flat"] >= 1:
yield Event(route="ship") # 3 · two flat rounds — a flat line is an answer too
else:
yield Event(route="improve")
# 4 · publish() truncates a runaway rewrite to 2,400 characters
Худший случай: четыре раунда, по два вызова модели в каждом, восемь вызовов, жесткий потолок. Ведущий никогда не оценивает, судья никогда не предлагает, а маршрутизатор никогда не пишет.
Попробовать
👉🖥 Курс VII. ↻ Запустите цикл в том виде, в котором он был отправлен: раунд 1, книги, сбой на 0,60, инициатор пишет свои правила, и запуск заканчивается на flywheel ended without closing. Удалите значок # на зеленой линии в редакторе. Запустите его снова.

Вот что предложил автор в нашем прогоне: черновик и два предложения (по одному на каждое сообщение об ошибке):
Ensure the total cost per person does not exceed anyone's stated budget. ← Tom's why
Confirm all guests can arrive before the kitchen's last order time. ← Lena's why
Невостребованное место становится причиной, причина становится правилом, а правило меняет следующее бронирование. Round 2 забронировал столик в Pho Saigon на 19:45 → 1.00. Звенит звонок.
Число: R1 0.60 → R2 1.00 · SHIP, примерно через пять секунд. В цикле SHIP используется для каждого ужина (один стол, p1), а в курсе V – для каждого экзамена. Одна дисциплина, две шкалы.
10. VIII. Проверка: что было заменено
👉🖥 Курс VIII. Нажмите 🧾 Чек, пожалуйста: восемь строк, ваши собственные номера.
до | после |
демонстрация, которая выглядит правильно; | экзамен, на котором подсчитывают, кто съел |
один золотой ответ; | набор правильных ответов и правило. |
запрос, который вы отредактировали вручную; | инструкция, которую переписывает цикл; |
число, которое увеличивается; | судья, который объясняет свои решения; |
один запуск; | туда и обратно |
показатель, который вам передали | выбранный вами номер |
Что нужно запомнить:
- Прежде чем выбрать показатель, спросите, что можно записать: ответ, правила или только описание хорошего результата. Ужин имеет набор правильных ответов, а компаратор проверяет только один.
- Создайте правило до цикла и сделайте так, чтобы состояние мира соответствовало тому, что правило штрафует.
- Укажите вердикт внутри показателя. ADK не будет применять пороговое значение.
- Разделите обучающий и контрольный наборы данных до первого запуска оптимизации и никогда не полагайтесь на оценку оптимизатора.
- Считайте голодных, а не сдавших – маленькие экзамены лгут.
- Покажите оптимизатору, в чем заключается вред: судья, который возвращает причины, выполняет половину работы оптимизатора.
- ADK предоставляет вам цикл, а границы определяете вы. Остановка – это тоже решение.
- Когда число достигает максимума, это должно быть связано с улучшением агента, а не с ослаблением требований.
Цикл увеличивает любое число, которое вы ему передаете.
Где можно провозить
Все действия в этом практическом занятии выполнялись на вашем компьютере с использованием написанного вами экзамена. Продакшн – это обеды, которые никто не планировал оценивать, и именно это может делать проверяющий, а не показатель золотого ответа: оценивать уже после того, как все произошло.

вертикальная черта | как | ответы |
① Cloud Trace |
| Сейчас все в порядке? – spans, собственные судьи платформы, онлайн-модераторы. |
② Строки BigQuery | плагин BigQuery Agent Analytics; | Что нужно знать? — одна структурированная строка на событие, форма, которую используют ваши средства проверки и наборы оценок |
Репозиторий содержит конвейер в виде скриптов, а не курса: refuel/send_traffic.py отправляет реальных участников на развернутый хост, refuel/replay_to_bq.py сохраняет разговоры в виде строк, а refuel/harvest.py переоценивает каждую строку с помощью world.everyone_ate и создает случаи для оценки на основе неудачных попыток – фактических слов группы, а не золотого ответа. В день съемки ведущий подал семь из восьми настоящих вин. Единственная ошибка была допущена при подаче вина "Лена" к последнему блюду, что соответствовало прогнозу, сделанному на курсе V. Этот случай стал экзаменационным вопросом следующего раунда. Второй круг – это снова курс V с ещё одним учебным примером. Для этого требуется облачный проект Google Cloud с платежным аккаунтом, поэтому это не пункт меню, а дополнительная информация.
Самостоятельное обучение
- Напишите свою партию (
world.PARTIES, затемscripts/make_evalsets.py) и включите ее в экзамен. Справедливо ли это?scripts/verify_world.pyКаждый участник должен сделать хотя бы один заказ, чтобы все были сыты. - Поднимите планку рейтинга до 0,9 и направьте точку
adk optimizeнаeval_config_rating.json. Измерьте градиент. - У вас есть проект с оплатой? Запустите конвейер
refuel/от начала до конца и прочитайте строки[skip]изharvest.py. Они представляют собой границу между двумя конвейерами. - Выполните цикл с помощью
HOST_THINKING=2048. Что изменилось и что это говорит о том, где находится потолок?