Большинство LLM-агентов устроены примерно одинаково: модель получает задачу, выполняет действие через tool, получает результат, рассуждает дальше и вызывает следующий tool. Вся эта последовательность постепенно накапливается в истории диалога.
Для коротких сценариев такой подход работает нормально. Но если агент должен выполнить десятки или сотни последовательных действий, история превращается в проблему: контекст растёт, старые данные продолжают влиять на модель, увеличиваются расход токенов и задержка.
Авторы работы SKILL.state: Scalable Long-Horizon Agent Skills предлагают отказаться от истории как основного механизма хранения состояния агента.
Вместо этого агент на каждом шаге получает только три вещи:
- описание выполняемой процедуры;
- текущее структурированное состояние;
- последнее наблюдение из внешней среды.
Идея кажется простой, но результаты экспериментов показывают довольно большую разницу.
Источник: https://arxiv.org/html/2608.26263
В чём проблема обычного LLM-агента
Рассмотрим типичный цикл работы агента:
User
↓
LLM
↓
Tool
↓
Observation
↓
LLM
↓
Tool
↓
Observation
↓
...
Чтобы модель понимала, что происходило раньше, runtime передаёт ей историю предыдущих действий.
Условно первый запрос выглядит так:
SYSTEM
USER
OBSERVATION 1
Через несколько шагов:
SYSTEM
USER
ACTION 1
OBSERVATION 1
ACTION 2
OBSERVATION 2
ACTION 3
OBSERVATION 3
...
А через сотню шагов в prompt уже находится значительная часть истории выполнения задачи.
Проблема здесь не только в количестве токенов.
Модель должна каждый раз восстанавливать текущее состояние мира из набора сообщений:
Что уже сделано?
Что ещё нужно сделать?
Какие данные актуальны?
Какие результаты устарели?
Какие гипотезы уже проверялись?
Фактически состояние программы оказывается размазано по текстовой истории.
Авторы SKILL.state предлагают сделать его явным.
Conversation != State
Основную идею SKILL.state можно свести к следующему:
conversation != state
История разговора и состояние выполняемой задачи — разные вещи.
В SKILL.state каждый вызов модели получает:
Procedure
+
Current State
+
Latest Observation
Математически авторы описывают это так:
A(t) = (P, Σ(t), O(t))
где:
P — описание процедуры
Σ(t) — текущее состояние
O(t) — последнее наблюдение
После обработки модель генерирует:
Reasoning
State Patch
Action
Runtime проверяет изменение состояния, применяет его и выполняет действие.
Получается цикл:
State + Observation
↓
LLM
↓
State Patch + Action
↓
New State
↓
Tool
↓
Observation
При этом reasoning нужен модели только внутри текущего шага.
После выполнения перехода состояния он удаляется.
На следующем шаге модель его уже не получает.
Как может выглядеть State
Допустим, агент работает с репозиторием и пытается исправить ошибку.
Вместо хранения всей последовательности команд:
прочитал auth.ts
запустил тесты
получил ошибку
проверил JWT
изменил middleware
снова запустил тесты
...
можно хранить состояние:
{
"working_dir": "/app",
"active_files": [
"src/auth.ts",
"src/user.ts"
],
"tested_hypotheses": [
"JWT expiration bug"
],
"cmd_summary": "Auth tests passed, user tests failing"
}
Следующее наблюдение:
User tests failed in UserService.
Модель может вернуть изменение:
{
"state_patch": {
"cmd_summary": "Auth fixed. Failure remains in UserService."
},
"action": {
"tool": "read_file",
"path": "src/user.ts"
}
}
Runtime применяет patch.
Следующий вызов модели уже получает новое состояние:
{
"working_dir": "/app",
"active_files": [
"src/auth.ts",
"src/user.ts"
],
"tested_hypotheses": [
"JWT expiration bug"
],
"cmd_summary": "Auth fixed. Failure remains in UserService."
}
История того, как агент пришёл к этому состоянию, ему больше не нужна.
Состояние становится источником истины
Это довольно важное архитектурное изменение.
В обычном агенте модель фактически делает:
History
↓
LLM
↓
Current State?
↓
Next Action
Она сама восстанавливает состояние из истории.
В SKILL.state:
Current State
+
Observation
↓
LLM
↓
New State + Action
Structured state становится canonical state выполнения.
LLM при этом можно рассматривать почти как функцию перехода:
(state, observation)
↓
LLM
↓
(new state, action)
По смыслу это уже гораздо ближе к обычному state machine или reducer, чем к бесконечному чату.
Почему это экономит токены
У history-based агента размер prompt увеличивается с каждым шагом.
Если первый шаг содержит условно:
1 блок истории
второй:
2 блока
а сотый:
100 блоков
то суммарное количество переданного контекста растёт примерно как:
1 + 2 + 3 + ... + T
то есть:
O(T²)
В SKILL.state размер состояния предполагается ограниченным.
Каждый шаг выглядит примерно одинаково:
Procedure + State + Observation
Поэтому размер отдельного prompt не зависит напрямую от количества предыдущих шагов, а суммарный расход растёт как:
O(T)
Разница становится особенно заметна на длинных задачах.
Что получилось в экспериментах
Авторы сравнивали несколько вариантов runtime:
- обычный ReAct с растущей историей;
- историю с периодическим summary;
- Stateful/LangGraph-style подход;
- SKILL.state.
На синтетическом Warehouse Management benchmark при длине выполнения в 200 шагов результаты получились такими:
| Runtime | Accuracy | Средний prompt | Всего токенов |
|---|---|---|---|
| ReAct | 0.74 | 48 007 | 2 608 755 |
| Summary Memory | 0.84 | 84 364 | 6 175 509 |
| LangGraph-style | 0.88 | 72 305 | 5 041 164 |
| SKILL.state | 0.94 | 1 811 | 122 384 |
То есть SKILL.state не просто использовал меньше контекста, но и показал лучшую точность.
Особенно интересно поведение среднего размера prompt.
При увеличении задачи:
10 шагов → 1775 токенов
25 шагов → 1736
50 шагов → 1773
100 шагов → 1905
200 шагов → 1811
Размер практически не меняется.
Это и есть главное свойство предлагаемой архитектуры: количество уже выполненных действий практически перестаёт влиять на размер следующего запроса к модели.
Почему просто обрезать историю недостаточно
Можно задать очевидный вопрос: зачем вообще придумывать отдельное состояние?
Можно просто оставить последние несколько сообщений.
Авторы проверили и это.
Для задачи в 100 шагов они ограничили разные подходы примерно одинаковым бюджетом в 1800 токенов.
Результат:
| Подход | Accuracy |
|---|---|
| Sliding Window | 0.18 |
| Summary | 0.52 |
| ReAct + LLMLingua | 0.22 |
| SKILL.state | 0.94 |
Проблема простой обрезки заключается в том, что информация может быть старой, но всё ещё важной.
Например агент узнал на втором шаге:
customer_id = 182
Если просто удалять старые сообщения, эта информация рано или поздно исчезнет.
Structured state позволяет сохранить именно факт:
{
"customer_id": 182
}
не сохраняя весь разговор, в котором он появился.
А почему не Summary?
Другой популярный вариант — периодически просить LLM делать summary истории.
Например:
Пользователь хочет создать заказ.
Выбран клиент #182.
Добавлено два товара.
Адрес доставки пока отсутствует.
Это уже значительно лучше полной истории, но остаётся текстом.
В нём могут потеряться идентификаторы, связи между сущностями или другие данные, которые модель посчитает незначительными.
Structured state задаёт эти данные явно:
{
"goal": "create_order",
"customer_id": 182,
"items": [431, 512],
"missing_fields": [
"delivery_address"
]
}
LLM уже не решает каждый раз, что означает этот текст.
Runtime знает структуру данных.
Чем это отличается от LangGraph
На первый взгляд идея напоминает LangGraph и другие stateful agent frameworks.
Structured state там тоже существует.
Но авторы SKILL.state обращают внимание на важное различие.
В LangGraph-style архитектуре состояние может существовать вместе с историей сообщений:
Structured State
+
Conversation Transcript
+
Latest Observation
То есть модель всё равно продолжает использовать transcript как один из основных источников информации.
SKILL.state предлагает более радикальный вариант:
Structured State
+
Latest Observation
Старого transcript вообще нет.
После каждого шага полезный результат reasoning должен превратиться в изменение состояния.
Остальное удаляется.
Интересная проблема: устаревшая информация
У такого подхода есть ещё одно преимущество.
Представим, что агент управляет складом и знает:
item A → shelf 10
Позже другой процесс переместил товар:
item A → shelf 37
В длинной истории модели всё ещё могут находиться десятки упоминаний:
item A → shelf 10
И только одно новое:
item A → shelf 37
Модель должна понять, какой факт актуален.
В structured state происходит обычное изменение:
{
- "itemA": "shelf10"
+ "itemA": "shelf37"
}
После обновления старого значения больше нет.
В экспериментах авторов history-based агенты после неожиданного изменения внешнего состояния тратили ещё 5–8 шагов на восстановление корректного поведения.
SKILL.state в проверенных сценариях восстанавливался сразу.
Практический пример: агент для работы с постами
Рассмотрим близкий веб-разработке сценарий.
Есть административная панель с постами и LLM-агент, которому пользователь пишет:
Подготовь пост о запуске нового продукта и запланируй его публикацию на 15 октября.
Агент открывает редактор, выбирает нужный раздел, заполняет заголовок и текст, устанавливает дату публикации и после подтверждения сохраняет пост.
Обычный вариант — хранить всё это в conversation history.
Но можно определить state:
interface AgentState {
goal: string;
route: {
name: string;
params: Record<string, unknown>;
};
post?: {
id?: number;
title?: string;
body?: string;
categoryId?: number;
publishAt?: string;
status?: "draft" | "scheduled" | "published";
};
form?: {
values: Record<string, unknown>;
missing: string[];
errors: Record<string, string>;
};
plan: {
completed: string[];
pending: string[];
};
pendingConfirmation?: {
action: string;
payload: unknown;
};
}
После нескольких действий состояние может выглядеть так:
{
"goal": "create_post",
"route": {
"name": "post-create",
"params": {}
},
"post": {
"title": "Запуск нового продукта",
"body": "Мы запускаем новый продукт...",
"categoryId": 7,
"publishAt": "2026-10-15T09:00:00Z",
"status": "draft"
},
"form": {
"values": {
"title": "Запуск нового продукта",
"category_id": 7,
"publish_at": "2026-10-15T09:00:00Z"
},
"missing": [],
"errors": {}
},
"plan": {
"completed": [
"open_post_editor",
"select_category",
"fill_content",
"set_publish_date"
],
"pending": [
"confirm",
"schedule_post"
]
}
}
Теперь модели не требуется помнить исходное сообщение:
Подготовь пост о запуске нового продукта и запланируй его публикацию на 15 октября.
Информация из него уже преобразована в состояние:
{
"title": "Запуск нового продукта",
"category_id": 7,
"publish_at": "2026-10-15T09:00:00Z"
}
А если пользователь скажет:
Давай перенесём публикацию на 20 октября.
агенту достаточно изменить:
{
- "publishAt": "2026-10-15T09:00:00Z"
+ "publishAt": "2026-10-20T09:00:00Z"
}
Старое значение перестаёт существовать в execution context.
Но состояние придётся проектировать
У SKILL.state есть очевидная цена.
Если раньше можно было просто складывать сообщения в массив:
messages.push(message);
то теперь необходимо решить:
Что агент должен помнить?
Какие данные являются временными?
Какие данные понадобятся позже?
Какие поля можно удалить?
Какие значения являются источником истины?
То есть часть ответственности переносится с context window модели на архитектуру приложения.
Авторы называют schema состояния first-class частью runtime.
При этом схема необязательно должна быть огромной. Например, для всех 100 задач InterCode CTF использовалась одна схема всего из пяти полей:
discovered_flags
tested_hypotheses
active_files
working_dir
cmd_summary
Главное — хранить не всю информацию, которую когда-либо видел агент, а данные, необходимые для дальнейшего выполнения задачи.
Результаты на реальных agent benchmarks
SKILL.state проверяли не только на синтетическом складе.
На InterCode CTF результаты составили:
ReAct 43.2%
Summary 46.4%
LangGraph 41.8%
SKILL.state 54.2%
На τ-Bench Retail:
ReAct 48.2%
Summary 29.9%
LangGraph 51.7%
SKILL.state 58.3%
На τ-Bench Airline:
ReAct 21.8%
Summary 23.6%
LangGraph 28.1%
SKILL.state 32.4%
Во всех трёх случаях SKILL.state показал лучший результат.
Это интересно, потому что речь идёт уже не просто об оптимизации стоимости prompt.
Удаление истории в этих экспериментах не ухудшило способность агента выполнять задачу — наоборот, результат улучшился.
Что из этого можно использовать уже сейчас
SKILL.state — скорее архитектурный паттерн, чем технология, которую обязательно нужно устанавливать отдельной библиотекой.
Простейший runtime можно представить примерно так:
while (!state.completed) {
const observation = await environment.observe();
const result = await llm.run({
procedure,
state,
observation,
});
validateStatePatch(result.statePatch);
state = applyPatch(
state,
result.statePatch
);
await tools.execute(result.action);
}
Здесь особенно важно разделение ответственности:
LLM
→ reasoning и выбор следующего действия
State
→ долговременное состояние выполнения
Tools
→ взаимодействие с внешним миром
Runtime
→ validation и применение переходов состояния
При необходимости отдельно могут существовать RAG, долговременная память пользователя, база данных и история разговора.
Но они не обязаны быть execution state агента.
Вместо вывода
Главная идея SKILL.state на самом деле довольно старая для обычной разработки:
не нужно восстанавливать состояние приложения из его логов.
Conversation history для LLM-агента во многом является именно таким логом.
Она полезна для отладки, аудита и общения с пользователем, но это не означает, что её нужно целиком передавать модели при каждом следующем действии.
Для короткого чат-бота разница может быть несущественной.
Но если агент должен автономно выполнить 50, 100 или 200 действий, работать с файлами, API, браузером, CRM или административной панелью, явное состояние выглядит гораздо более масштабируемой архитектурой:
Conversation → события
State → текущая истина
LLM → переход состояния
Tools → изменение внешнего мира
Возможно, следующий этап развития agent frameworks будет заключаться не в создании всё более сложных механизмов сжатия длинного контекста, а в том, чтобы перестать складывать в контекст то, что изначально должно было быть состоянием программы.
Источник: Sanket Badhe, Priyanka Tiwari, Jonghyun Chung — SKILL.state: Scalable Long-Horizon Agent Skills, 2026.


Добавить комментарий