Рекурсивные языковые модели: всесторонний и подробный анализ.
Чем именно он отличается от ReAct, CodeAct, Self-Loops и Subagents?
Делиться
В этой статье вы узнаете, что такое рекурсивные языковые модели (RLM), почему они сейчас лидируют во всех тестах производительности в условиях длительного контекста и чем они отличаются от существующих решений для агентных вычислений!
И мы собираемся это узнать, рассмотрев один простой пример из практики.
В течение прошлого месяца я потратил немало времени на внедрение RLM, проведение сравнительных тестов и создание 50-минутного обучающего видео. В процессе работы я ответил на более чем 100 вопросов на YouTube и X о RLM. Эта статья — краткое изложение того, что я узнал, отвечая на эти вопросы, и конкретных нюансов RLM, которые заставили меня воскликнуть: « Ага !»
Примечание: Если не указано иное, все изображения, использованные в этой статье, были созданы автором. Лицензия свободная.
Основная причина, по которой рекурсивные языковые модели кажутся многим читателям недоступными, заключается в том, что некоторые из их идей на самом деле довольно сильно противоречат интуиции по сравнению с существующими методами (такими как ReAct, CodeAct, обычные субагенты и т. д.). Лучший способ понять рекурсивные языковые модели — сначала понять, в чём заключаются недостатки этих других методов, и осознать единственный недостающий элемент в агентных системах.
Идея заключается в передаче контекста посредством ссылки, а не его дублировании.
1. Из всех сложных экспериментов, которые я проводил…
…самым поучительным оказался этот нелепый эксперимент, в котором я попросил представителя RLM: «Сгенерируйте 50 названий фруктов и подсчитайте количество букв R в каждом из них, верните в виде словаря».
А вот более сложный вариант (назовем его Задача 2): «Создайте словарь различных категорий: фрукты, страны, животные. Для каждой категории сгенерируйте 50 названий и подсчитайте количество букв R в каждом из них, верните вложенный словарь».
Для задачи 1 ожидаемый результат выглядит примерно так:
{"strawberry": 3, "berry": 2, ... "grape": 1}
А что касается второй задачи, то она выглядит примерно так:
Я понимаю, это глупая проблема, но способ её решения в RLM принципиально отличается от других архитектур, таких как ReAct или CodeAct.
Понимание того, как каждый метод решает эту сложную задачу, — это все, что вам понадобится, чтобы оценить красоту RLM.
Начнём!
2. Агентный ландшафт
2.1 Прямая генерация
Первый метод — это просто прямая генерация. LLM «думает» о запросе пользователя и автоматически, регрессивно, генерирует словарь. Никаких вспомогательных средств, никакой структуры, просто прямое предсказание следующего токена в цикле.
Проблемы такого подхода:
LLM не имеет способа проверить, является ли он математически корректным.
Вероятно, LLM ошибается, потому что, по сути, подсчет букв алфавита — это не задача «предсказания следующего слова».
Вероятность галлюцинаций или ошибок чрезвычайно высока, даже если лежащая в основе LLM обладает интеллектом.
2.2 Реагирование (Рассуждение и Действие)
ReAct — это цикл рассуждения и действия, в котором LLM сначала обдумывает проблему (цепочка рассуждений), а затем генерирует вызов инструмента. По сути, в командной строке системы мы передаем список «имен функций» и инструкции о том, как их вызвать.
Например, вы могли бы предоставить программе LLM простой инструмент, который состоит всего лишь в следующем:
def count_alphabets_in_word(word: string, alphabet: string) -> int
Используя описанную выше идею, агент ReAct сможет выполнить следующие действия:
Составьте список названий фруктов.
Используйте этот инструмент, чтобы передать название каждого фрукта и получить на выходе целое число.
Из выходной памяти восстановите словарь, содержащий информацию о том, какой фрукт получил какое количество очков, и затем верните результат.
Трассировка стека такой транзакции будет выглядеть следующим образом:
# User Generate a dictionary with 50 fruits and the number of 'r' in each # Assistant 50 fruit names are: strawberry, berry, grape, ... # Assistant count_alphabets_in_word("strawberry", "r") # Tool_Out(executes our function) 3 # Assistant count_alphabets_in_word("berry", "r") ## Tool call executed! # Tool_Out(executes our function) 2 . . . # Assistant I now have everything I need in my message history, let's construct that dictionary { "strawberry": 3, "berry": 2, .... }
Вы понимаете, в чём проблема, верно? Во-первых, для этого конкретного случая необходимо заранее определить функцию ` count_alphabet_in_r . Если вы не определите функцию, агент просто вернётся к старому способу (то есть к прямому подсчёту алфавитов)!
Это гарантирует, что LLM имеет некоторое представление о том, каким будет результат, но LLM все равно должен генерировать токены по одному из своей истории сообщений.
Студенту, обучающемуся по программе LLM, по-прежнему необходимо запоминать количество каждого слова и воспроизводить его дословно по памяти. На этом этапе могут возникать ошибки передачи данных.
Проблема усугубляется, если расширить ее до многокатегорийной постановки задачи 2. LLM должна повторять длинный список вызовов функций, запоминать, что происходило на каждом шаге, и генерировать ответы по одному токену.
Для разработчиков ReAct отлично подходит, если вы создаёте узкоспециализированные приложения, где агенту необходим доступ к определённым инструментам (веб-поиск, поиск документов, калькулятор, доступ к терминалу, редактирование файлов, применение различий и т. д.), но вы редко будете разрабатывать универсального агента и оптимизировать его для таких нишевых навыков.
В принципе, для универсальных агентов хороши только такие инструменты. Вы не будете писать инструменты вроде count_alphabet_in_r если точно не знаете, что они понадобятся вашим пользователям.
А что, если бы программа LLM могла создавать собственные инструменты?
2.3 CodeAct
CodeAct позволяет студентам магистратуры писать код и выполнять его.
Это значит, что вам (человеку) больше не нужно будет писать отдельные инструменты. Вы можете просто предоставить LLM возможность писать любой код на Python и выполнять его в изолированной терминальной среде, считывать результаты и генерировать выходные данные.
Это будет выглядеть примерно так:
# User Generate a dictionary with 50 fruits and the number of 'r' in each # Assistant Okay let's write some python code for this. python -c ' fruits = [ 'strawberry', 'berry' 'grape', .... ] count_r = { k: sum(1 for c in fruit if c == 'r') for k, f in fruits } print("Number of fruits: ", len(fruits)) print("Counts: " , count_r) ' # Tool Output (Terminal Output) Number of fruits: 50 Counts are: {"strawberry": 3, "berry": 2 ....} # Assistant Okay, I have read the terminal output, let me return write it down again to return the output { "strawberry": 3, "berry": 2, .... }
Таким образом, CodeAct работает следующим образом:
CodeAct считывает полное сообщение пользователя (точно так же, как и другие методы, которые мы обсуждали ранее).
LLM думает, пишет и запускает код, или выполняет команды bash!
LLM загружает результат выполнения кода в контекстное окно.
Сгенерируйте результат, исходя из прочитанного.
CodeAct подвержен тем же ошибкам передачи, о которых мы говорили в ReAct. Потому что LLM по-прежнему должен воспроизводить ответ дословно из своей памяти. Преимущество CodeAct (перед ReAct) заключается в том, что вам (человеку) не нужно предварительно настраивать доступные инструменты для агента. Агент создает свои собственные инструменты (исполняемые команды).
Мне
Общее правило для сравнения ReAct и CodeAct:
Используйте ReAct, когда работаете над узкоспециализированными продуктами и точно знаете, какие инструменты необходимы ИИ для решения задачи.
Используйте CodeAct, если предметная область общая.
Помните, что CodeAct всегда будет работать медленнее, чем ReAct, потому что магистранту нужно потратить время на обдумывание и разработку инструментов (в то время как в ReAct инструменты предоставляются пользователем).
Проблема усугубляется, если рассматривать ее в контексте многокатегорийной задачи 2. Проблема задачи 2 заключается в том, что ИИ должен отслеживать слишком много внутренних состояний. Ему необходимо запомнить 150 различных названий в 3 разных категориях (фрукты, страны, животные) и количество букв «р» в каждом слове.
А что если бы вы могли разделить и властвовать над этими тремя категориями? То есть, поручить одному агенту работу с фруктами, другому — со странами, а третьему — с животными?
2.4 CodeAct + Subagents
Вот это уже совсем другая история — речь идёт о серьёзной мощности!
Архитектура с участием субагентов довольно проста. Есть главный агент, и он может запускать более мелких агентов для выполнения подзадач.
Каждый субагент также является агентом CodeAct, который выполняет назначенные ему задачи и возвращает результат главному агенту.
Главный агент загружает эти выходные данные непосредственно в контекст и выполняет следующую единицу действия.
Понимание всего вышесказанного имеет решающее значение для понимания архитектуры RLM (об этом чуть позже).
Более подробная информация о субагентах и о том, почему они полезны.
Как правило, субагенты НЕ разделяют никаких внутренних состояний/контекстов с основным агентом (существуют схемы субагентов, например, «разветвленные субагенты», которые это делают).
Какие бы внутренние шаги ни предпринимал подчиненный агент для выполнения подзадачи (трассировка сообщений или трассировка вызова инструментов), они скрыты от основного агента.
Преимущество архитектуры субагентов заключается в том, что основной агент не страдает от «контекстной деградации», поскольку ему не нужно беспокоиться о внутренней работе субагентов. Полностью «чёрный ящик».
Мы уже знаем, что архитектура субагентов легко решит задачу 1 при num_subagent = 0 (стандартный CodeAct), поэтому давайте посмотрим, как она будет работать на примере задачи 2.
# User Generate a dictionary of different categories: fruits, countries, animals. For each category, generate 50 names. And count the number of R in each, return as a nested dictionary # Assistant Let's call some subagents and divide tasks among them call_subagent("Return a dictionary of 50 fruit names and number of r in them") # Subagent (A new code-act module) {"strawberry": 3, "berry": 2 ....} # Assistant call_subagent("Return a dictionary of 50 countries names and number of r in them") # Subagent {"france": 1, "russia": 1 ....} # Assistant call_subagent("Return a dictionary of 50 animals names and number of r in them") # Subagent {"kangaroo": 1, "deer": 1 ....} # Assistant I have responses from all subagents, now I will write the final JSON { "fruits": { "strawberry": 3, "berry": 2, .... }, "countries": { "france": 1, "russia": 1 .... } "animals": { "kangaroo": 1, "deer": 1 .... } }
Мы добились значительного прогресса. CodeAct + Subagent могут записывать произвольный код в произвольные объекты, но при этом всё равно должны:
Прочитайте весь запрос пользователя в контекстное окно.
СЧИТАТЬ весь вывод субагента в его контекстное окно
Авторегрессионно ЗАПИСАТЬ окончательный результат (после обработки информации, полученной в результате предыдущих вызовов инструмента и от субагентов)
Борьба носит двойной характер:
LLM необходимо запоминать все результаты предыдущих обращений к инструменту.
LLM необходимо выводить результаты в правильном формате при выводе.
А что если мы позволим LLM записывать свои результаты во промежуточный файл, чтобы он их не забывал?
2.5 CodeAct + Подагенты + Файловая система
Это одна из самых мощных архитектур!
Вы предоставляете магистру права доступ к специальным инструментам – write_file и read_file
Вы даете агенту указание записывать промежуточные результаты в постоянную файловую систему, используя эти инструменты (или напрямую с помощью оператора > в терминале bash). Это помогает агенту сохранять контрольные точки прогресса, чтобы он мог загружать старые состояния позже, когда это потребуется!
Доступ к файловой системе имеет ряд ограничений:
Больше вызовов инструментов/операций чтения
Легче запоминать информацию и не терять связь с реальностью.
Проблема передачи данных по-прежнему существует: в конечном итоге LLM необходимо прочитать файл и воспроизвести его дословно (если это является строгим требованием).
Всем этим решениям не хватает одной простой функции:
Передача по ссылке
Это старая концепция программирования, согласно которой вместо передачи копии переменных между модулями (или, в данном случае, агентами) — передается ссылка на переменную.
Именно этим и занимаются RLM.
3. Рекурсивные языковые модели
RLM — это вспомогательный инструмент, который направляет LLM определенным образом, чтобы заставить их выполнять задачи. Помните, что вспомогательный инструмент — это внешняя система, которая направляет LLM определенным образом, чтобы заставить их делать что-то, управлять своим контекстом и шаг за шагом достигать более крупной и сложной задачи.
Из статьи RLM (https://arxiv.org/abs/2512.24601)
Вот 4 пункта, которые объясняют, что делают RLM:
Языковая модель взаимодействует с произвольно длинными запросами через внешнюю программируемую среду или REPL. Выводимые данные обрезаются на уровне каркаса.
Студент магистратуры может писать код для программногоисследования и создания новых преобразований запроса.
Он может рекурсивно вызывать подагентов для выполнения более мелких подзадач. Ответы подагентов не загружаются автоматически в контекст родительского агента, они возвращаются в виде символов или переменных внутри REPL родительского агента.
Агенты RLM могут возвращать ответы двумя способами: (а) авторегрессивно генерируемые ответы, как обычные LLM, и (б) конструировать ответы в переменную Python и возвращать эту переменную.
Давайте разберем каждую концепцию шаг за шагом.
3.1 REPL
REPL — это цикл чтения-вычисления-печати. Представьте его как блокнот Jupyter.
Вы можете использовать переменную Python под названием context, в которой хранится запрос пользователя.
Вы можете писать команды для просмотра этого контекста. Например, всякий раз, когда LLM выполняет оператор print, работающее ядро Python выводит выражение.
LLM может итеративно считывать выходные данные для загрузки новой информации в свой контекст. Затем он принимает решение о дальнейших действиях.
REPL также может работать в изолированной песочнице с настраиваемыми правами доступа к файловой системе, поэтому LLM не может повлиять на фактические файлы пользователя. Это скорее решение, продиктованное соображениями безопасности.
Вот пример того, как «начнётся» запуск RLM.
Перед вызовом любого LLM мы запустим изолированную среду Python. Это можно сделать, запустив экземпляр pyodide внутри Deno.js.
Среда выполнения Python инициализируется специальной переменной под названием «context», которая содержит приглашение пользователя.
В LLM мы передаем НЕ содержимое контекста, а лишь тот факт, что у него есть доступ к REPL и в нем присутствует переменная с именем context. LLM может выполнить print(context) внутри REPL, чтобы увидеть приглашение командной строки.
Вот пример траектории:
# System You have access to a REPL python environment. Your task is stored in a variable called `context`. You can issue print statements. Print displays truncated sections of the variable (upto 200 words). Find out what the task is about. Generate your code inside ```repl ... blocks When ready to answer, submit your result using: FINAL(answer) # Assistant Let me print out the context to find out about my task ```repl print(context) ``` # REPL Output (executes Assistant code) "Generate a dictionary containing 50 names of fruits and count the number of r in each"
Способ, которым запрос пользователя попадает в контекстное окно LLM, не зависит от того, как мы его передаем! LLM принимает осознанное решение считывать его из окружения.
В нашем случае запрос пользователя прост и краток. Но помните, что запрос пользователя может быть произвольно длинным. Например, в одном из моих тестовых случаев я ввел полные стенограммы 300 подкастов Лекса Фридмана в виде строки, содержащей почти 10 миллионов токенов.
Оператор print в среде REPL не возвращает полный вывод! Вместо этого он обрезает вывод до фиксированной длины и возвращает его.
Даже если RLM попытается перегрузить себя сенсорной информацией, мы явно предотвратим это, усекая выходные данные терминала.
Студенты магистратуры также могут целенаправленно изучать отдельные фрагменты заданного вопроса:
LLM также может использовать регулярные выражения, поиск и любой другой код преобразования для извлечения информации и сохранения её в переменной. Помните, что переменные сохраняются между вызовами выполнения, потому что именно так работает REPL — это постоянная среда выполнения Python (представьте, как работает Jupyter Notebook/ipykernel).
x = re.match(....) y = context[30:90].split(",") print(len(y))
В задании для магистерской программы содержится инструкция изучить пространство заданий и подумать о том, как можно обработать данные для выполнения поставленной задачи.
Это похоже на то, как специалисты по анализу данных, работающие с новым CSV-файлом, содержащим данные о ценах на жилье, выводят случайные данные в блокнот Jupyter, чтобы понять, с чем они имеют дело.
В процессе исследования LLM также может создавать новые переменные внутри среды выполнения Python, содержащие важные преобразования данных!
Помните, что переменные Python сохраняются между различными вызовами REPL. Я постоянно возвращаюсь к примеру с Jupyter Notebook, потому что вы должны это понять. Каждый раз, когда LLM пишет блок кода и выполняет его, это эквивалентно тому, как если бы мы, люди, писали блок кода и выполняли ячейку!
Вот пример анализа стенограмм подкастов Лекса Фридмана с помощью RLM:
Новая траектория RLM, которая меня просто поразила! Я буду использовать её в качестве основного примера в видеоуроке на YouTube.
Я передал CSV-файл, содержащий расшифровки 320 эпизодов подкаста Лекса Фридмана, и попросил программу найти, что сказали первые 10 его гостей, занимавшихся машинным обучением, об общем искусственном интеллекте.
Контекст был… pic.twitter.com/P3SOtFJC24
— AVB (@neural_avb) 16 февраля 2026 г.
Примерами исследования или преобразования контекста могут быть:
LLM извлекает базовую структуру CSV-файла и помещает данные в DataFrame pandas для более удобной последующей обработки.
LLM извлекает определенные разделы из файла Markdown и создает словарь, содержащий названия подразделов и тексты подразделов.
В рамках программы LLM используются регулярные выражения или операторы поиска для поиска ключевых слов в контексте (базовый поиск по ключевым словам).
На этапе исследования основная задача состоит в том, чтобы свести всю задачу к более мелким, полезным переменным.
Однако для нашей задачи 1 задача довольно проста, поэтому исследовательская задача LLM также не представляет сложности.
# Assistant ```repl print(context) ``` # REPL Output Generate a dictionary containing 50 names of fruits and count the number of r in each # Assistant okay I need to generate 50 fruit names ```repl FRUIT_NAMES = ['grape', 'strawberry', 'berry', ....] print(len(FRUIT_NAMES)) # let's check if it is 50 ``` # REPL Output 52 # Assistant Okay I added 2 extra by mistake. Let me pick first 50 and check again. ```repl FRUIT_NAMES_50 = FRUIT_NAMES[:50] # Just take first 50 assert len(FRUIT_NAMES_50) == 50 print("Succcessful: ", len(FRUIT_NAMES_50) ``` [[REPL]] Successful: 50 # Assistant Cool, I'll just calculate the r counts now ```repl fruit_r_count = { fruit: sum([ 'r' in f for f in fruit.lower() ]) } for fruit in FRUIT_NAMES_50 } FINAL(fruit_r_count) ```
Как вы видете:
LLM может автоматически настраиваться в зависимости от вывода REPL. Во второй попытке у него уже был доступ к исходной переменной FRUIT_NAMES содержащей 52 значения, поэтому он смог повторно использовать эту переменную, чтобы разделить ее на переменную FRUIT_NAMES_50 !
Если оператор assert завершится неудачей, LLM получит ошибку REPL и начнет работу по исправлению кода!
LLM вообще не нужно ЧИТАТЬ словарь fruit_r_count ! Он может просто сразу передать его пользователю.
Функция FINAL(.) просто возвращает результат выражения непосредственно из REPL обратно на вывод генератора кода!
Это первый случай, когда мы обсуждаем путь, при котором агент может возвращать пользователю результат без (а) чтения всего словаря в контекст, (б) генерации словаря по токенам, (в) полного отказа от использования файловых систем (теоретически CodeAct мог бы записать словарь в файловую систему и попросить пользователя читать его оттуда).
По этой причине выходные данные RLM не ограничены длиной контекста LLM. Они могут возвращать произвольно длинные выходные данные, настолько длинные, насколько позволяет переменная Python.
3.3 Рекурсивные субагенты
Мы уже говорили о некоторых интересных аспектах RLM, но мы еще даже не затронули рекурсивные части.
В моделях RLM рекурсивность схожа с субагентами, но существуют фундаментальные различия в способах обмена информацией между субагентами, которые отличаются в моделях RLM.
RLM-модули имеют доступ к специальной функции внутри своей REPL, называемой llm_query
Функция llm_query принимает на вход одну строку.
llm_query запускает совершенно новую, чистую среду REPL и устанавливает контекст равным тому, что родительский LM передал в llm_query
Этот дочерний RLM должен решить задачу и отправить её обратно, используя FINAL.
Выходные данные дочернего RLM-модуля не загружаются автоматически в контекст родительского RLM-модуля. Вместо этого они представляют собой просто еще одно выражение внутри интерактивной консоли Python (REPL)!
Чтобы во всем этом разобраться, рассмотрим задачу 2.сверху.
# System .... # everything as before, plus You are given a new function called llm_query llm_query invokes new subagents to perform tasks Your subagents are quite strong and can handle long tasks well! To call subagents just run llm_query(sub_context) # Assistant ```repl print(context) ``` # REPL Output Generate a dictionary of different categories: fruits, countries, animals. For each category, generate 50 names. And count the number of R in each, return as a nested dictionary # Assistant Cool I will just launch subagents to capture the R counting for each category and do the job
Затем выполните следующий код:
FRUIT_DICT = llm_query("generate a dictionary of 50 fruits and the number of times r occured") COUNTRY_DICT = llm_query("generate a dictionary of 50 countries and the number of times r occured") ANIMAL_DICT = llm_query("generate a dictionary of 50 animals and the number of times r occured") for dictionary in [FRUIT_DICT, COUNTRY_DICT, ANIMAL_DICT]: assert isinstance(dictionary, dict) and len(dictionary) == 50 answer = { "fruits": FRUIT_DICT, "countries": COUNTRY_DICT, "animals": ANIMAL_DICT } FINAL(answer)
В практических реализациях RLM мы сможем распараллелить эти вызовы. Параллельная работа нескольких субагентов над ортогональными задачами — это не просто здорово, но и позволяет очень быстро выполнить огромный объем работы.
Обратите внимание на то, что только что произошло.
Программа LLM поручила трем субагентам заниматься управлением фруктами, странами и животными.
Суб-агенты (как мы видели ранее) вернут ответы, вызвав FINAL в своем локальном REPL.
В результате выполнения команды данные попадают непосредственно в словари FRUIT_DICT , ANIMAL_DICT и COUNTRY_DICT REPL основного агента.
Выходные данные субагента вводятся в REPL, они не загружаются напрямую в контекст LLM (как это было в случае с субагентами CodeAct или ReAct). Для просмотра выходных данных субагента главному агенту необходимо целенаправленно проверить их с помощью операторов print .
Главному агенту это даже не было нужно:
Загрузите весь вывод данных от субагента в контекст.
Прочитайте любое из названий фруктов.
Сгенерируйте итоговый выходной токен по одному из токенов в памяти.
Она формировала ответ, создавая ключевые символы посредством рекурсивных вызовов и выдавая конечный результат в виде композиции.
Базовая архитектура RLM с Deno и Pyodide
3.4 Выходное пространство RLM
RLM могут выбрать два способа возврата своего КОНЕЧНОГО результата.
Во-первых, он может записывать ответы в переменные Python и возвращать их (как в примере выше).
Или же она может генерировать ответ самостоятельно авторегрессионным методом, подобно обычной модели LLM.
В приведенном ниже примере выходные данные были сгенерированы авторегрессионным методом.
# Assistant print(context) # REPL Output Capital of France? # Assistant FINAL('Capital of France is Paris')
В приведенном выше примере результат вычислялся в Python, и возвращалось содержимое этой переменной.
# Assistant print(context) # REPL Output Today's date? # Assistant ```repl from datetime import date today = date.today() FINAL(today) ```
Эти два способа получения ответов открывают огромные возможности для моделей реляционного обучения:
Они могут программно исследовать данные с помощью регулярных выражений, находить операции, используя обычный Python.
Они могут создавать небольшие переменные для сохранения результатов работы (поскольку они находятся внутри REPL, старая работа никогда не теряется).
Они могут рекурсивно вызывать агентов для подведения итогов.
Подагенты могут работать параллельно или последовательно. LLM принимает это решение разумно. RLM может захотеть вызывать подагентов последовательно, например, если необходимо составить текущее резюме длинного контекстного текста, требующего предварительной информации.
Они также могут использовать внешние инструменты, но для этого необходимо предоставить к ним доступ через слой песочницы (например, Deno).
Чтобы более наглядно понять, как работают RLM-ы, как их можно реализовать с нуля и увидеть реальные примеры решения реальных задач, посмотрите этот видеоурок:
Ознакомьтесь с моей реализацией RLM с открытым исходным кодом; она включает в себя средство просмотра логов в TUI для рекурсивных трассировок.
https://github.com/avbiswas/fast-rlm
Вот полная командная строка системы, которую я использовал для реализации RLM. Она многое прояснит!
Нажмите здесь, чтобы открыть полную системную подсказку (она скрыта, потому что длинная) . Рекомендованную автором подсказку можно найти в статье RLM (ссылка ниже). Приведенная здесь подсказка была переработана из подсказки в статье, с добавлением нескольких примеров и инструкций, которые уменьшили количество сбоев в моделях с открытым исходным кодом (протестировано на Minimax-M2.7, GLM-5.1).
You are tasked with answering a query with associated context. You can access, transform, and analyze this context interactively in a REPL environment that can recursively query sub-LLMs, which you are strongly encouraged to use as much as possible. You will be queried iteratively until you provide a final answer. You will be provided with information about your context by the user. This metadata will include the context type, total characters, etc. The REPL environment is initialized with: 1. A `context` variable that contains extremely important information about your query. You should check the content of the `context` variable to understand what you are working with. Make sure you look through it sufficiently as you answer your query. 2. A `llm_query` function that allows you to query an LLM (that can handle around 100K chars) inside your REPL environment. This function is asynchronous, so you must use `await llm_query(...)`. The return value is the actual Python object that the subagent passed to FINAL (eg a list, dict, string, etc.). Do NOT wrap the result in eval() or json.loads(); use it directly. That said, you must use python to minimize the amount of characters that the LLM can see as much as possible. 3. A global function FINAL which you can use to return your answer as a string or a python variable of any native data type (Use dict, list, primitives etc) ** Understanding the level of detail user is asking for ** Is the user asking for exact details? If yes, you should be extremely thorough. Is the user asking for a quick response? If yes, then prioritize speed. If you invoke recursive subagents, make sure you inform them of the user's original intent, if it is relevant for them to know. You can interact with the Python REPL by writing Python code. 1. The ability to use `print()` statements to view the output of your REPL code and continue your reasoning. 2. The print() statements will truncate the output when it returns the results. This Python REPL environment is your primary method to access the context. Read in slices of the context, and take actions. You can write comments, but it is not needed, since a user won't read them. So skip writing comments or write very short ones. ** How to control subagent behavior ** - When calling an `llm_query` sometimes it is best for you as a parent agent to read actual context picked from the data. In this case, instruct your subagent to specifically use FINAL by slicing important sections and returning it verbatim. No need to autoregressively generate a summarized answer. - In other times, when you need your llm call to summarize or paraphrase information, they will need to autoregressively generate the answer exploring their context, so you can instruct them in your task prompt to do that. - By default, the agent plans and decides for itself how it must complete a task! - Clearly communicating how you expect your return output to be (list? dict? string? paraphrased? bullet-points? verbatim sections?) helps your subagents! - If you recieved clear instructions on what format your user/parent wants the data, you must follow their instructions ** IMPORTANT NOTE ** This is a multi-turn environment. You do not need to return your answer using FINAL in the first attempt. Before you return the answer, it is always advisable to print it out once to inspect that the answer is correctly formatted and working. This is an iterative environment, and you should use print() statement when possible instead of overconfidently hurry to answer in one turn. When returning responses from subagent, it is better to pause and review their answer once before proceeding to the next step. This is true for single subagents, parallel subagents, or a sequence of subagents ran in a for loop. Your REPL environment acts like a jupyter-notebook, so your past code executions and variables are maintained in the python runtime. This means YOU MUST NOT NEED to rewrite old code. Be careful to NEVER accidentally delete important variables, especially the `context` variable because that is an irreversible move. You will only be able to see truncated outputs from the REPL environment, so you should use the query LLM function on variables you want to analyze. You will find this function especially useful when you have to analyze the semantics of the context. To ask a subagent to analyze a variable, just pass the task description AND the context using `llm_query()` You can use variables as buffers to build up your final answer. Variables can be constructed by your own manipulation of the context, or by simply using the output of llm_query() Make sure to explicitly look through as much context in REPL before answering your query. An example strategy is to first look at the context and figure out a chunking strategy, then break up the context into smart chunks, and query an LLM per chunk with a particular question and save the answers to a buffer, then query an LLM with all the buffers to produce your final answer. You can use the REPL environment to help you understand your context, especially if it is large. Remember that your sub-LLMs are powerful -- they can fit around 500K characters in their context window, so don't be afraid to put a lot of context into them. For example, a viable strategy is to feed 10 documents per sub-LLM query. Analyze your input data and see if it is sufficient to just fit it in a few sub-LLM calls! When calling llm_query(), you must also give your instructions at the beginning of the whatever context you are adding. If you only pass the context into the subagent without any instructions, it will not be able to conduct it's task! Therefore, ensure that you specify what task you need your subagent to do, to guarantee that they work. Help them with more instructions such as if the data is a dictionary, list, or any other finding that will help them figure out the task easier. Clarity is important! When you want to execute Python code in the REPL environment, wrap it in triple backticks with `repl` language identifier. For example, say we want our recursive model to search for the magic number in the context (assuming the context is a string), and the context is very long, so we want to chunk it: *** SLOWNESS *** - The biggest reason why programs are slow is if you run subagents one-after-the-other. - Subagents that are parallel tend to finish 10x faster - The value of your intelligence and thinking capability is how you design your method so that you maximize subagent parallelization (with asyncio.gather(*tasks)) ```repl chunk = context[: 10000] answer = await llm_query(f"What is the magic number in the context? Here is the chunk: {chunk}") print(answer) ``` As an example, suppose you're trying to answer a question about a book. You can iteratively chunk the context section by section, query an LLM on that chunk, and track relevant information in a buffer. ```repl query = "In Harry Potter and the Sorcerer's Stone, did Gryffindor win the House Cup because they led?" for i, section in enumerate(context): if i == len(context) - 1: buffer = await llm_query(f"You are on the last section of the book. So far you know that: {buffers}. Gather from this last section to answer {query}. Here is the section: {section}") print(f"Based on reading iteratively through the book, the answer is: {buffer}") else: buffer = await llm_query(f"You are iteratively looking through a book, and are on section {i} of {len(context)}. Gather information to help answer {query}. Here is the section: {section}") print(f"After section {i} of {len(context)}, you have tracked: {buffer}") ``` As another example, when the context is quite long (eg >500K characters), a simple but viable strategy is, based on the context chunk lengths, to combine them and recursively query an LLM over chunks. For example, if the context is a List[str], we ask the same query over each chunk. You can also run these queries in parallel using `asyncio.gather`: ```repl import asyncio query = 'A man became famous for his book "The Great Gatsby". How many jobs did he have?' # Suppose our context is ~1M chars, and we want each sub-LLM query to be ~0.1M chars so we split it into 5 chunks chunk_size = len(context) // 10 tasks = [] for i in range(10): if i < 9: chunk_str = "\n".join(context[i * chunk_size: (i + 1) * chunk_size]) else: chunk_str = "\n".join(context[i * chunk_size:]) task = llm_query(f"Try to answer the following query: {query}. Here are the documents:\n{chunk_str}. Only answer if you are confident in your answer based on the evidence.") tasks.append(task) answers = await asyncio.gather(*tasks) for i, answer in enumerate(answers): print(f"I got the answer from chunk {i}: {answer}") final_answer = await llm_query(f"Aggregating all the answers per chunk, answer the original query about total number of jobs: {query}\n\nAnswers: \n" + "\n".join(answers)) ``` As a final example, after analyzing the context and realizing its separated by Markdown headers, we can maintain state through buffers by chunking the context by headers, and iteratively querying an LLM over it. Do note that this pattern is slow, so only do it if ABSOLUTELY necessary: ```repl # After finding out the context is separated by Markdown headers, we can chunk, summarize, and answer import re sections = re.split(r'### (.+)', context["content"]) buffers = [] for i in range(1, len(sections), 2): header = sections[i] info = sections[i + 1] summary = await llm_query(f"Summarize this {header} section: {info}") buffers.append(f"{header}: {summary}") final_answer = await llm_query(f"Based on these summaries, answer the original query: {query}\n\nSummaries:\n" + "\n".join(buffers)) ``` In the next step, we can return FINAL(final_answer). IMPORTANT: When you are done with the iterative process, you MUST provide a final answer inside a FINAL function when you have completed your task, NOT in code. Do not use these tags unless you have completed your task. You have two options: 1. Use FINAL("your final answer here") to provide the answer directly 2. You must return a valid python literal in FINAL, like a string or integer, double, etc. You cannot return a function, or an unterminated string. 3. Use FINAL(variable_name) to return a variable you have created in the REPL environment as your final output When you use FINAL you must NOT use string quotations like FINAL("variable_name"). Instead you should directly pass the variable name into FINAL like FINAL(variable_name). FINAL("variable_name") will return the string "variable_name" to the user, not the content of that variable, which in 100% of cases will lead to error - so be careful about this. Think step by step carefully, plan, and execute this plan immediately in your response -- do not just say "I will do this" or "I will do that". Output to the REPL environment and recursive LLMs as much as possible. Remember to explicitly answer the original query in your final answer. * WHAT IS BAD * If you try to read all the context with multiple tool calls, and then try to piece it together by regenerating the context and outputting - that is a sign of low intelligence. We expect you to think hard and generate smart python code to manipulate the data better. * KNOWING WHEN TO QUIT * Time is ticking every step you take. User is waiting every step you take. We want to be as fast as we can. If you have tried, and are unable to finish the task, either call more subagents, or return back that you don't know. You should not run multiple print() statements just to constuct your output. If context is too large, use a subagent with llm_query. If context is structured, write python code to extract structure that is easier to operate on. If context is small (that is not truncated), you can read it fully. You can recursively shorten the context if you need to. You must think and plan before you generate the code. Your expected response should be as follows: ```repl Your working python code FINAL(...) ``` Do not output multiple code blocks. All your code must be inside a single ```repl ... ``` block.
Полный текст статьи можно прочитать здесь: https://arxiv.org/abs/2512.24601
Или с помощью ИИ: https://paperbreakdown.com/abs/2512.24601
4. Почему это так хорошо работает?
Фокусированное внимание: вместо того, чтобы обрабатывать все пары токенов в огромном объеме входных данных, RLM позволяет модели целенаправленно загружать определенные разделы в контекст. RLM загружает контекст ПО желанию, а не принудительно, как это делают ReAct или CodeAct.Путем объединения информации из нескольких разных разделов запроса.
Многошаговое рассуждение: Многие задачи по своей природе являются рекурсивными (многошаговое тестирование, поиск в кодовой базе, суммирование нескольких документов). RLM-ы изначально соответствуют многозадачной структуре . RLM-ы могут итеративно уточнять свой план, просто выводя различные фрагменты контекста и загружая их в контекст.
Устойчивость к шуму: когда 99% входных данных нерелевантны, рекурсивный поиск позволяет избежать «разбавления внимания». Умная модель будет разумно загружать в память те части подсказок, которые, скорее всего, предоставят ей больше всего информации. Выборочное загрузка контекста в память — признак интеллекта !
Результаты представляют собой составные переменные: ответы суб-агентов не загружаются напрямую в контекст LLM; они возвращаются в виде символов внутри REPL Python, и агент может либо просмотреть результаты, либо использовать их напрямую. Он может составлять результаты непосредственно из ответов суб-агентов, не считывая их полностью.
Произвольно длинные выходные данные: Помните, что RLM-моделям не обязательно авторегрессивно генерировать ответы; вместо этого они могут создавать ответы внутри переменной Python — это означает, что модель теоретически может возвращать бесконечно длинные выходные данные. Задачи суммирования по большей части по-прежнему являются авторегрессивными.
Экономия средств: поскольку модель сама решает, что читать и когда рекурсивно обрабатывать, вы часто платите только за то, что вам нужно, а не за сканирование всего. В статье о RLM показаны результаты того, насколько дешевыми могут быть эти эксперименты по сравнению с другими методами. Низкая стоимость входных токенов подсказки! И в зависимости от задачи, низкая стоимость токенов завершения.
Подгрупповые агенты по-прежнему обращаются к кэшу ключ-значение : они выполняют задачи пошагово, поэтому их системные подсказки и предыдущие сообщения не меняются. Вы обращаетесь к кэшу ключ-значение в 90% случаев, поэтому ваши затраты невелики. Подгрупповые агенты следуют простому шаблону сообщений пользователь->помощник->пользователь->помощник. Немедленные преимущества кэша ключ-значение!
Разделение ответственности : корневая языковая модель выступает в роли «планировщика/оркестратора», в то время как субъагентные языковые модели являются «исполнителями/работниками», выполняющими низкоуровневую работу. Вы также можете выбирать разные модели для выполнения этих разных задач! Вы можете настраивать, какая модель для чего предназначена. Фактически, вы можете расширить RLM, чтобы выбирать, какой тип модели должен работать над подзадачей.
Хорошие модели программирования от природы хорошо справляются с управлением RLM. Люди уже обучают модели на RLM-оборудовании, так что, я думаю, дальше будет только лучше!
Добавить комментарий
Для отправки комментария вам необходимо авторизоваться.