Sakhanda Wire
NVDA $230.48 -2.94% MSFT $522.61 -1.35% GOOGL $348.29 -0.63% META $720.89 -0.06% AMZN $254.06 -2.25%
← До новин

Посібник Google Research з RRSI: опанування агентів ШІ, що самовдосконалюються

У цьому посібнику ми реалізуємо RRSI (Regularized Recursive Self-Improvement) — метод, який дає змогу агенту на базі LLM переписувати власну оболонку, підказки, інструменти, пам’ять, потік керування та підагентів навколо замороженої моделі, не допускаючи перенавчання оболонки на завданнях, на яких вона розвивається. Повний цикл RRSI створює чернетки змін за допомогою Claude Opus у Vertex AI та оцінює їх у бенчмарках Docker, а це неможливо запустити у безкоштовному ноутбуці. Частина RRSI, яка насправді втілює ідею статті, — правила, що визначають, які запропоновані зміни залишати, — написана на звичайному Python, і саме її ми запускаємо безпосередньо. Ми встановимо пакет з офіційного репозиторію, розглянемо його оцінювач, калібровану смугу шуму, обидві гілки алгоритму відбору, його поступово зменшуваний бюджет змін, детермінований фільтр витоків і журнал змін, а потім під’єднаємо симульованого агента до власного інтерфейсу Domain у RRSI. Оскільки симульоване середовище ми створили самі, нам відомий справжній ефект кожної зміни. Це дає змогу перевірити рішення RRSI на основі еталонної істини та порівняти їх із нерегуляризованим пошуком, який просто залишає варіант із найвищим результатом.

Copy CodeCopiedUse a different Browser
import os
import sys
import json
import math
import copy
import random
import tempfile
import textwrap
import traceback
import subprocess
import statistics as st
from pathlib import Path
 
RESULTS = {}
 
 
def banner(title):
    print("\n" + "=" * 78)
    print(title)
    print("=" * 78)
 
 
def section(name):
    def wrap(fn):
        def run(*a, **kw):
            banner(name)
            try:
                out = fn(*a, **kw)
                RESULTS[name] = out if isinstance(out, str) else "ok"
                return out
            except Exception as e:
                RESULTS[name] = f"SKIPPED / FAILED -> {type(e).__name__}: {e}"
                print(f"\n[!] {name} did not complete: {type(e).__name__}: {e}")
                traceback.print_exc(limit=3)
                return None
        return run
    return wrap
 
 
banner("1. Install RRSI and map the paper onto the code")
subprocess.run([sys.executable, "-m", "pip", "install", "-q", "git+https://github.com/google-research/rrsi.git@be50316e1db05914068a973f322770ef08ed7ba1"], check=True)
 
from importlib.metadata import version
from rrsi.config import RRSIConfig
from rrsi.evaluate import TaskResult, EvalResult, aggregate, evaluate
from rrsi.calibrate import calibrate
from rrsi.selection import Candidate, cost_rule, judge, select_round
from rrsi.schedule import edit_budget, budget_table
from rrsi.history import History, stall_flag, exploration
from rrsi.components import K, K_STR, normalize, novelty
from rrsi.critic import precheck, review
from rrsi.domain import Domain
 
print(f"  rrsi {version('rrsi')}  |  anthropic {version('anthropic')}  |  Python {sys.version.split()[0]}")
print("\n  RRSI evolves an agent's HARNESS (prompts, tools, memory, control flow, sub-agents) around a")
print("  frozen model. The full loop drafts edits with Claude Opus on Vertex AI and scores them in Docker")
print("  benchmarks. The part that decides which edits to KEEP is plain Python, and that is what we drive:")
for symbol, where in [
    ("S_hat, C_hat       Eq. (estimate)", "rrsi.evaluate.aggregate"),
    ("delta              noise band", "rrsi.calibrate.calibrate"),
    ("Algorithm 2        floor + cost rule", "rrsi.selection.judge / select_round"),
    ("b_t                Eq. (anneal)", "rrsi.schedule.edit_budget"),
    ("Critic             leakage screen", "rrsi.critic.precheck / review"),
    ("L_t, g_t, B_t      history, yield, prune", "rrsi.history.History"),
    ("sigma_t, U_t       stall + exploration", "rrsi.history.stall_flag / exploration"),
    ("nu                 structural novelty", "rrsi.components.novelty"),
]:
    print(f"    {symbol:40s} -> {where}")
 
CFG = RRSIConfig()
print(f"\n  paper defaults: T={CFG.T} rounds, k={CFG.k} trials/task, m={CFG.m} candidates/round,"
      f" b in [{CFG.b_min},{CFG.b_max}]")
print(f"                  beta0={CFG.beta0}  beta1={CFG.beta1}  w_s={CFG.w_s}  w_c={CFG.w_c}  w_n={CFG.w_n}"
      f"  delta_z={CFG.delta_z}")
print("\n  Nothing below needs an API key, a GPU or a dataset download.")

Ми встановлюємо RRSI з репозиторію google-research, зафіксувавши коміт, на основі якого було написано цей ноутбук, оскільки пакета немає на PyPI. Його єдина залежність — клієнт Anthropic, який ролі пошуку використовують для викликів Claude, але ми його не запускатимемо. Потім виводимо відповідність, яку документує сам репозиторій, між символами зі статті та функціями, що їх реалізують: емпіричною оцінкою результату й вартості в evaluate, смугою шуму в calibrate, алгоритмом 2 у selection, поступово зменшуваним бюджетом змін у schedule, фільтром витоків у critic та журналом змін із підсумками yield, prune, stall і exploration у history. RRSIConfig містить гіперпараметри зі статті, і кожна наведена нижче функція отримує їх саме так, як у реальному циклі.

Copy CodeCopiedUse a different Browser
@section("2. Evaluate(H): a score and a cost, and why a crash counts as zero")
def estimator():
    base = {
        "task_000": TaskResult(rewards=[1, 1], tokens=[11_800, 12_400]),
        "task_001": TaskResult(rewards=[1, 0], tokens=[15_100, 14_600]),
        "task_002": TaskResult(rewards=[0, 0], tokens=[21_000, 19_500]),
    }
    ev = aggregate("H0", 2, base)
    print(f"  three tasks x k=2 trials -> S_hat = {ev.S:.3f}   C_hat = {ev.C:,.0f} tokens/trial"
          f"   ({ev.n_expected} trials expected, {ev.missing} missing)")
 
    crashy = dict(base)
    crashy["task_002"] = TaskResult(rewards=[0.0, 0.0], tokens=[None, None], missing=2)
    ev_crash = aggregate("crashy", 2, crashy)
    dropped = {t: r for t, r in crashy.items() if not r.missing}
    naive = sum(sum(r.rewards) for r in dropped.values()) / sum(len(r.rewards) for r in dropped.values())
    print("\n  A candidate crashes on the hardest task instead of failing it:")
    print(f"    an estimator that drops missing trials reports  {naive:.3f}   <- looks like a gain")
    print(f"    RRSI's aggregate (missing = 0, full denominator) {ev_crash.S:.3f}   <- no reward for crashing")
    print(f"    ...and C_hat uses only recorded token counts: {ev_crash.C:,.0f}")
 
    rubric = {"memo": TaskResult(rewards=[0.5, 1.0], weights=[10, 10]),
              "brief": TaskResult(rewards=[0.0, 0.0], weights=[90, 90])}
    ev_w = aggregate("rubric", 2, rubric)
    print("\n  Weighted rewards (Harvey LAB style: weight = number of rubric criteria):")
    print(f"    mean of per-task means = {st.mean(r.mean for r in rubric.values()):.3f}"
          f"   vs   RRSI's S_hat = {ev_w.S:.3f} (fraction of all criteria passed)")
    return f"crash scored {ev_crash.S:.3f} under RRSI vs {naive:.3f} if dropped"
 
 
estimator()

RRSI вимірює два показники для кожної оболонки: S — винагороду, усереднену за всіма випробуваннями всіх завдань, і C — середню кількість токенів політики на випробування. TaskResult зберігає випробування одного завдання та агрегує їх. Деталь, яку варто запозичити для оцінювання будь-якого агента, — це обробка пропущених випробувань. Коли кандидат аварійно завершується на найскладнішому завданні, оцінювач, що відкидає пропущені випробування, повідомляє 0.750 і створює враження покращення. Водночас RRSI зараховує кожне пропущене випробування як нульову винагороду, використовуючи повний знаменник, і повідомляє те саме значення 0.500, що й раніше. Отже, кандидат не може здаватися кращим, просто знищивши випробування, які виявилися для нього складними. Зважені винагороди охоплюють набори з оцінюванням за рубрикою, як-от Harvey LAB, де S стає часткою всіх пройдених критеріїв, а не середнім значенням середніх показників за завданнями.

Перш ніж будь-яке правило зможе відокремити справжнє покращення від випадковості, потрібно дізнатися, наскільки результат однієї оболонки змінюється сам по собі. Ми створюємо невеликого симульованого агента, успішність якого на кожному завданні є логістичною функцією від навички оболонки мінус складність завдання, і шість разів оцінюємо незмінну початкову оболонку на сорока завданнях із двома випробуваннями кожне: результати відрізняються на 0.113, хоча нічого не змінилося. calibrate перетворює повторні оцінювання тієї самої оболонки на delta — подвоєне стандартне відхилення різниці між двома запусками. За 80 випробувань delta становить приблизно 0.108; за 3 200 випробувань вона зменшується приблизно до 0.013, що відповідає діапазону, наведеному в статті для її екземплярів (0.004–0.020). Для відбору будь-яке покращення, менше за delta, неможливо відрізнити від повторного запуску тієї самої оболонки.

Алгоритм 2 реалізовано як набір чистих функцій, тому ми можемо передавати їм кандидатів і буквально читати пояснення їхніх рішень. Ми фіксуємо поточного кандидата з S 0.630 і 10 000 токенів, найкращий результат за весь час 0.640 та delta 0.020 і пропускаємо через judge вісім кандидатів. Кандидат, що перебуває нижче порога — найкращого коли-небудь отриманого результату мінус delta, — одразу відхиляється. Покращення, що перевищує delta, має компенсувати додаткові токени за правилом, відповідно до якого відносна зміна вартості повинна залишатися нижчою за 0.10 плюс 40-кратне покращення; приріст на 6 процентних пунктів за збільшення кількості токенів на 20% приймається, а приріст на 3 процентні пункти за збільшення на 150% — ні. У межах смуги результати вважаються однаковими, тому рішення приймається за сформованим показником: 100-кратний приріст мінус 15-кратна зміна вартості, плюс невеликий бонус за структурний компонент, який ще жодного разу не приймали. Саме так RRSI приймає кандидат D, який показав нижчий результат, ніж поточний кандидат, але коштує на 20% менше, і саме так новий підагент розриває нічию, у якій зміна підказки має такий самий результат. Обмеження домену накладає вето незалежно від результату.

select_round застосовує judge до кожного кандидата раунду й залишає найрезультативнішого допустимого кандидата. Ми передаємо йому дорогого кандидата, трохи гіршого, але дешевшого кандидата, а також кандидата, якого критик уже відхилив. Найрезультативніший кандидат програє, оскільки його приріст на три процентні пункти не компенсує збільшення вартості на 150%; відхилений критиком кандидат узагалі не проходить оцінювання, а переможець зменшує результат поточного кандидата на пів процентного пункту, скорочуючи витрати токенів на п’яту частину. Безпеку забезпечує S*, який може лише зростати: поріг прив’язаний до найкращого коли-небудь виміряного результату, а не до поточного кандидата. Тому ланцюжок замін на трохи гірші, але дешевші варіанти не може поступово погіршувати результат протягом багатьох раундів.

На етапі створення пропозицій регуляризується спосіб підготовки змін, а не вибір змін, які залишаються. edit_budget реалізує L0-бюджет змін зі статті, що поступово зменшується: косинусний розклад від b_max до b_min. За замовчуванням він дозволяє до чотирьох узгоджених змін на кандидата протягом перших восьми раундів, три — протягом наступних п’яти та дві — протягом останніх семи. Оператор округлення вгору у формулі означає, що бюджет досягає мінімуму, рівного одиниці, лише в t = T, тобто на один крок після завершення запуску, — це легко не помітити під час читання рівняння. Оскільки кожна зміна в пакеті успадковує одне вимірювання пакета, саме скорочення бюджету дає змогу пов’язати історію пізніх етапів із меншою кількістю компонентів. Бюджет обмежує кількість змін, які рухаються разом, але ніколи не визначає, які механізми зрештою може містити оболонка.

Критик перевіряє кожен diff кандидата до того, як на оцінювання буде витрачено ресурси, і працює у два шари. Перший — детермінована попередня перевірка за загальним шаблоном облікових даних і власним списком заборон домену. Шаблони для ідентифікаторів завдань набору розвитку та шляхів до grader відхиляють diff, який запам’ятовує відповідь для task_007, diff, що читає очікуваний результат, diff із витоком ключа API та порожній diff — і все це без виклику моделі. Чистий diff переходить до другого шару — перевірки наміру Claude. Тут ноутбук виявляє реальну проблему: якщо RRSI_VERTEX_PROJECTS не задано, rrsi.llm.generate обчислює індекс за модулем кількості проєктів за межами блоку try і викликає ZeroDivisionError, тому корисне повідомлення про помилку конфігурації не з’являється. Ми також розглядаємо маркування змін: normalize зберігає оголошений компонент лише тоді, коли diff містить підтвердження для нього. Отже, зміна підказки не може видавати себе за нову навичку, щоб отримати бонус за новизну, а неправильно позначена зміна керування контекстом маркується відповідно до її справжньої суті.

History записує один JSONL-запис для кожної зміни, а цикл виводить із нього чотири підсумки. Набір випробуваних компонентів не містить змін, відкинутих критиком, оскільки їх ніколи не вимірювали. Нещодавня ефективність g_t — це найкращий виміряний приріст для кожного компонента протягом останніх n_prune раундів, і кожен компонент, чия недавня ефективність не є позитивною, потрапляє до набору prune B_t разом із будь-якими механізмами цього компонента, які все ще є в поточному кандидатові. У нашій історії це позначає зміну підказки, прийняту в нульовому раунді, яка відтоді не дала результату. Прапорець stall спрацьовує, коли результат змінився менше ніж на delta протягом останніх w раундів, після чого exploration формує директиву для автора пропозицій, резервуючи місце кандидата для компонентів, яких запуск ще не випробовував. Оскільки автор пропозицій отримує всю цю інформацію, спростована гіпотеза більше не створюється.

RRSI не запускає агента й не оцінює результат самостійно; це робить адаптер Domain, а той самий інтерфейс використовується для екземплярів зі статті — coding, workspace та engineering. Ми реалізуємо такий адаптер для симульованого агента: розділення завдань на набори розвитку й відкладених завдань, метод run, який читає оболонку з файлів у корені робочого дерева та записує результати випробувань у каталог runs, метод score, який зчитує їх назад як об’єкти TaskResult, а також шаблони критика й сигнали компонентів домену. Власна функція evaluate у RRSI оцінює початкову оболонку через адаптер, а повторний виклик для того самого завдання зчитує збережені випробування замість повторного запуску — саме таку безпечність відновлення вимагає контракт.

Тепер ми запускаємо весь процес відбору як пошук: двадцять раундів, два кандидати в кожному, пакети розміру, визначеного поступово зменшуваним бюджетом, зміни, позначені за допомогою normalize, кандидати, перевірені precheck та оцінені через адаптер, переможці, вибрані select_round, і кожен результат, записаний у History у тому порядку, який використовує цикл RRSI. Скриптовий автор пропозицій створює п’ять типів змін, справжні ефекти яких нам відомі: зазвичай корисні загальні зміни, витокові зміни, що запам’ятовують відповіді на завдання набору розвитку, інертні зміни конфігурації, дорогі підагенти, які трохи допомагають усюди, витрачаючи в 1,5 раза більше токенів, і дешевше стискання контексту. Ми порівнюємо три правила прийняття на одному потоці кандидатів у восьми запусках із різними початковими значеннями: greedy залишає кандидата з найкращим результатом, якщо він зріс, critic only додає фільтр витоків, а RRSI додає алгоритм 2 із каліброваним delta. Greedy досягає найвищого результату на відкладених завданнях — 0.681 проти 0.616 у RRSI, — запам’ятовуючи приблизно десять відповідей і майже потроюючи витрати токенів. RRSI не запам’ятовує нічого, завершує роботу приблизно з удвічі меншими витратами токенів (1,53 від початкової оболонки проти 2,97) і використовує на третину менше оцінювань. Найкорисніший результат — розділення розриву між наборами розвитку та відкладених завдань: вибір найкращих результатів із шумних оцінок збільшує показник кожного режиму приблизно на дев’ять пунктів, і жодне правило цього циклу не усуває такий ефект. Водночас критик усуває майже все запам’ятовування.

Наш перший оцінювач був шумним, і delta калібрувалася на його основі, тому обережність RRSI на кроці 10 відображала саме оцінювач, а не метод. Ми повторюємо порівняння з більшими наборами завдань розвитку та більшою кількістю випробувань. Коли оцінювач стає точнішим, delta зменшується з 0.096 до 0.025, а результат RRSI на відкладених завданнях зростає з 0.616 до 0.759. Водночас нерегуляризований пошук за найточнішого налаштування використовує у 6,5 раза більше токенів, ніж RRSI. Greedy усе ще показує вищий результат, і ноутбук пояснює чому: у цьому симульованому світі немає спадної віддачі, тому кожен підагент, якого додає greedy-пошук, продовжує підвищувати точність. Це найсприятливіший для витрат сценарій. RRSI обмінює частину результату на обмежені витрати токенів, відсутність запам’ятованих відповідей і меншу кількість марних оцінювань, а масштаб цього компромісу визначається delta, яку метод вимірює, а не запитує наперед.

Підсумок виводить однорядковий результат кожного розділу, прямо зазначає, чого не моделює мініатюрний приклад, а саме змін, які допомагають набору розвитку, але шкодять іншому набору, — для цього існують LLM-критик і відкладені розділення зі статті, — а також автора пропозицій, який читає історію. Після цього він указує, як запустити реальний екземпляр, додати домен і повторно оцінити збережений раунд із використанням іншого delta без повторного запуску.

Отже, ми розібрали RRSI уздовж тієї частини, де фактично міститься ідея статті, — правил, що визначають, які зміни залишає агент, здатний до самовдосконалення, — і запустили їх офлайн на CPU без моделі та ключа API. Оцінювач не винагороджує збій, смуга шуму вимірюється, а не задається наперед, і алгоритм 2 виявляється складнішим за просте «відхиляти шумні покращення»: він ніколи не дозволяє результату опуститися нижче найкращого побаченого значення мінус смуга шуму, змушує справжнє покращення компенсувати додаткові токени й у межах смуги навмисно віддає перевагу дешевшій оболонці, навіть якщо вона показала трохи нижчий результат. Перевірка цих правил у середовищі, де ми контролювали еталонну істину, була найінформативнішою частиною: детермінований критик усунув майже все запам’ятовування ще до витрат на оцінювання, алгоритм 2 утримував витрати токенів у кілька разів нижче за нерегуляризований пошук, але жоден із них не усунув завищення, що виникає під час вибору найкращого з шумних результатів. Це може зробити лише повторне оцінювання на невідомих завданнях. Чесне застереження таке саме очевидне: у світі, де токени завжди купують точність, нерегуляризований пошук показує вищий результат. Отже, цінність регуляризації RRSI залежить від вартості токенів і від того, наскільки дорого вам обійдеться витік відповіді, а ноутбук надає інструменти для вимірювання обох чинників на власному оцінювачі.


Перегляньте ПОВНИЙ КОД тут. Уся подяка належить досліднику цього проєкту. Також підписуйтеся на нас у Twitter і не забудьте приєднатися до нашого ML SubReddit із понад 150 тисячами учасників та підписатися на нашу розсилку. Стривайте! Ви є в Telegram? тепер ви також можете приєднатися до нас у Telegram.

Перекладено автоматично з англійської. Оригінал статті — за посиланням нижче.

Вперше опубліковано виданням MarkTechPost

Читати оригінал на MarkTechPost ↗

Текст і зображення належать MarkTechPost і наводяться тут із зазначенням авторства та посиланням на оригінальну публікацію.

← До новин

Ще новини

Усі останні новини