Коротко
- Исследователи работают параллельно и сдают отчёт по JSON-схеме: утверждение плюс источник. Критики на другой модели открывают эти источники и ищут дыры. Судья через llm-task выносит решение тоже по схеме, а цикл с ограничением отправляет спорное на второй круг.
- Собирается двумя способами: скриптом Swarm в Code Mode (
agents.runсоschema,phase(),whileс ограничением) или без Code Mode, черезsessions_spawnсcollect: trueиoutputSchemaи черезagents_wait. - Честная граница: Code Mode экспериментальный, прерванный рестартом скрипт с места не продолжится, коллекторы не умеют спрашивать одобрение, а критик с доступом к источникам надёжнее самопроверки, но истины не гарантирует.
Кому и когда
Вы поручаете агенту исследование: сравнить три библиотеки для очереди задач, проверить утверждение из чужой статьи перед тем, как на него сослаться, разобрать, что изменилось в API между версиями. Один агент вернёт аккуратный текст. Ошибку в нём вы найдёте позже, когда библиотеку уже выбрали.
Модель плохо проверяет сама себя. В работе Large Language Models Cannot Self-Correct Reasoning Yet показано, что без внешней обратной связи модель редко исправляет свои рассуждения, а иногда после «самопроверки» отвечает хуже. В исследовании LLM-as-a-judge среди перекосов модели-судьи назван self-enhancement bias: судья склонен выше оценивать ответы своей же модели. Отсюда приём. Проверяет не тот, кто писал, и проверяющему дают то, чего не хватает самопроверке: внешнюю обратную связь. Здесь это источники, которые критик открывает сам. Другая модель снимает перекос в пользу своих ответов, но без источников смена модели мало что даёт.
Приём окупается, когда цена ошибки выше цены лишних вызовов модели и когда утверждения можно сверить с источником. Если проверять нечем, критик будет спорить со вкусом исследователя, а не с фактами.
Собрано по документации и исходникам OpenClaw 2026.9.7; скрипты и конфиги запуском не проверялись, имена моделей и тексты заданий условные.
Схема потока
- Исследование. N исследователей идут параллельно, по одному на кандидата или подвопрос. Каждый сдаёт JSON: список утверждений, у каждого id и источник.
- Враждебная проверка. На каждый отчёт свой критик на другой модели. Задание: открыть источники и найти неподтверждённое, устаревшее и противоречивое. Ответ тоже JSON: какое утверждение, что с ним не так, насколько это серьёзно.
- Решение. Судья на третьей модели получает последние отчёты и критику и возвращает одно из трёх: принять, перепроверить спорное, эскалировать человеку. Спорное он называет парами «кандидат + id утверждения», чтобы одинаковые id у разных исследователей не путались. Это шлюз решения (decision gate): от его ответа зависит, будет ли второй круг.
- Второй круг, если судья попросил. Перезапускаются только кандидаты со спорным. Исследователь получает прошлый отчёт, спорные утверждения и замечания критика к ним и сдаёт исправленный отчёт целиком. Критик проверяет его заново. Кругов не больше двух.
- Если круги кончились, а спор остался, решение переводится в эскалацию.
- Синтез. Итоговый текст пишется из последних отчётов и только из утверждений, переживших критику. Отброшенное и упавшие ветки перечисляются отдельно.
- Человек. В конце, и только если есть эскалация или открытые вопросы.
Из чего собрано
- Swarm: параллельные дочерние агенты-коллекторы из скрипта, структурные результаты, лимиты.
- Code Mode: модель пишет небольшую программу на JavaScript, а инструменты вызывает из неё как функции.
- LLM Task: одиночный вызов модели без инструментов, который возвращает JSON по схеме.
- Субагенты: параметры
sessions_spawn, в том числеmodelиrunTimeoutSeconds. - Ask user: структурный вопрос человеку с вариантами ответа.
Главное понятие — коллектор. Обычный субагент, закончив работу, сам присылает результат родителю (announce). Коллектор, ребёнок с collect: true, ничего не присылает. Он оставляет результат, и родитель забирает его явно. Поэтому пачку из десяти исследователей можно дождаться одной точкой сбора, без потока сообщений в сессию.
Настройка
Заведите под исследования отдельного агента. Code Mode и llm-task тогда включаются только у него, а расходы видны отдельной строкой.
{
plugins: {
entries: {
"llm-task": {
enabled: true,
llm: {
allowModelOverride: true,
allowedCompletionModels: ["<модель агента по умолчанию>", "<провайдер-3>/<модель>"],
},
},
},
},
agents: {
entries: {
research: {
tools: {
codeMode: true,
alsoAllow: ["llm-task"],
swarm: { maxConcurrent: 8, maxTotalPerGroup: 40 },
},
},
},
},
}
Что здесь происходит:
- Swarm включён по умолчанию, отдельного переключателя ему не нужно. А вот Code Mode, без которого в скрипте нет
agents.run, включается отдельно. Здесь он включён явно для одного агента. - Глобальные
agents.run,phaseиlogпоявляются в скрипте, только если в каталоге инструментов есть роднойsessions_spawnи политика инструментов его разрешает. MCP-инструмент с тем же именем не подходит. swarmу агента переопределяет общие лимиты. Значения по умолчанию: 32 ребёнка выполняются одновременно, 50 живых в группе, 200 за всю жизнь группы. Здесь они урезаны под задачу, об этом в разделе про стоимость.- Блок
llmу llm-task — разрешения со стороны хоста. Судья в скрипте зовёт модель явно, поэтому нуженallowModelOverride. СписокallowedCompletionModelsограничивает каждый вызов, так что в нём должны быть и модель судьи, и модель агента по умолчанию.
Откатить: уберите research из agents.entries и llm-task из plugins.entries и перезапустите Gateway.
Детям можно дать отдельного, урезанного агента: с меньшим набором инструментов и дешёвой моделью. Документация так и советует: задать tools.swarm.defaultAgentId, разрешить его в subagents.allowAgents и поставить ему tools.swarm: false, чтобы он сам не запускал рои. Готового агента worker в OpenClaw нет, его надо завести.
Вариант 1: скрипт Swarm
Скрипт пишет и запускает сам агент через инструмент exec в Code Mode. Ниже то, что он должен написать. Синтаксис взят из примеров tools/swarm.md: параллельный разбег с Promise.allSettled и цикл с шлюзом решения.
Скрипт целиком (≈100 строк)
const RESEARCH_MODEL = "<провайдер-1>/<модель>";
const CRITIC_MODEL = "<провайдер-2>/<модель>"; // не модель исследователя
const JUDGE_MODEL = "<провайдер-3>/<модель>"; // ни исследователя, ни критика
const candidates = ["A", "B", "C"];
const claimsSchema = {
type: "object",
properties: {
subject: { type: "string" },
claims: {
type: "array",
items: {
type: "object",
properties: { id: { type: "string" }, text: { type: "string" }, source: { type: "string" } },
required: ["id", "text", "source"],
additionalProperties: false,
},
},
},
required: ["subject", "claims"],
additionalProperties: false,
};
const critiqueSchema = {
type: "object",
properties: {
holes: {
type: "array",
items: {
type: "object",
properties: {
claimId: { type: "string" },
problem: { type: "string" },
severity: { type: "string", enum: ["fatal", "major", "minor"] },
},
required: ["claimId", "problem", "severity"],
additionalProperties: false,
},
},
},
required: ["holes"],
additionalProperties: false,
};
const verdictSchema = {
type: "object",
properties: {
decision: { type: "string", enum: ["accept", "recheck", "escalate"] },
contested: {
type: "array",
items: {
type: "object",
properties: { subject: { type: "string" }, claimId: { type: "string" } },
required: ["subject", "claimId"],
additionalProperties: false,
},
},
openQuestions: { type: "array", items: { type: "string" } },
},
required: ["decision", "contested", "openQuestions"],
additionalProperties: false,
};
// latest[subject] = { report, holes }: последний отчёт кандидата и критика к нему
// holes: null — критик упал, утверждения этого отчёта не проверены
const latest = {};
const failures = [];
let tasks = candidates.map((c) => ({ subject: c, focus: "всё" }));
let verdict = null, pass = 0;
while (pass < 2) {
pass += 1;
phase(`Исследование, круг ${pass}`);
const research = await Promise.allSettled(tasks.map((t) =>
agents.run(`Исследуй ${t.subject} для задачи «очередь задач в Python». ${t.focus} ` +
`Каждое утверждение — с id и URL источника. Нет источника — не пиши. Поле subject: ${t.subject}.`,
{ label: `research-${t.subject}-${pass}`, model: RESEARCH_MODEL, schema: claimsSchema })));
const fresh = [];
research.forEach((o, i) => {
if (o.status === "fulfilled") {
const report = { ...o.value, subject: tasks[i].subject };
latest[report.subject] = { report, holes: null };
fresh.push(report);
} else failures.push({ lane: `research-${tasks[i].subject}-${pass}`, error: String(o.reason) });
});
phase(`Враждебная проверка, круг ${pass}`);
const checks = await Promise.allSettled(fresh.map((r) =>
agents.run(`Ты скептичный рецензент. Открой каждый источник и найди утверждения, ` +
`которые он не подтверждает, устарели или противоречат друг другу:\n${JSON.stringify(r)}`,
{ label: `critic-${r.subject}-${pass}`, model: CRITIC_MODEL, schema: critiqueSchema })));
checks.forEach((o, i) => {
if (o.status === "fulfilled") latest[fresh[i].subject].holes = o.value.holes;
else failures.push({ lane: `critic-${fresh[i].subject}-${pass}`, error: String(o.reason) });
});
phase(`Решение, круг ${pass}`);
const judged = await llm_task({
prompt: "Реши по отчётам и критике: принять, перепроверить спорное или эскалировать человеку. " +
"Спорное называй парами subject + claimId. holes: null значит, что отчёт не проверен.",
input: { latest, failures },
schema: verdictSchema,
model: JUDGE_MODEL,
});
verdict = judged.json;
log(`Круг ${pass}: ${verdict.decision}, спорных ${verdict.contested.length}, модель судьи ${judged.model}`);
if (verdict.decision !== "recheck") break;
const disputed = {};
for (const c of verdict.contested) {
if (!latest[c.subject]) continue;
if (!disputed[c.subject]) disputed[c.subject] = [];
disputed[c.subject].push(c.claimId);
}
tasks = Object.keys(disputed).map((subject) => {
const ids = disputed[subject];
const prev = latest[subject];
return {
subject,
focus: "Перепроверь спорные утверждения и верни отчёт целиком: неспорное без изменений, " +
"спорное исправь с новым источником или убери. " +
`Спорное: ${JSON.stringify(prev.report.claims.filter((x) => ids.includes(x.id)))}. ` +
`Замечания критика: ${JSON.stringify((prev.holes || []).filter((h) => ids.includes(h.claimId)))}. ` +
`Прошлый отчёт: ${JSON.stringify(prev.report)}.`,
};
});
if (tasks.length === 0) break;
}
if (verdict.decision === "recheck") {
verdict = { ...verdict, decision: "escalate",
openQuestions: verdict.openQuestions.concat(["Круги исчерпаны, спорное осталось"]) };
}
phase("Синтез");
const synthesis = await agents.run(
`Напиши вывод только из утверждений, которые пережили критику. Отдельно перечисли отброшенное, ` +
`спорное и упавшие ветки:\n${JSON.stringify({ latest, verdict, failures })}`,
{ label: "synthesis", model: RESEARCH_MODEL });
return { synthesis, verdict, failures, latest };
На что смотреть в этом скрипте:
agents.runсоschema. Ребёнку добавляется служебный инструментstructured_output, ответ проверяется по схеме, на ошибку даётся одна подсказка исправиться. Не исправился, упал или истёк по времени — промис отклоняется сSwarmAgentError, у которой естьrunId,statusиmessage.Promise.allSettled, а неPromise.all.Promise.allпадает на первой же ошибке и остальные результаты не собирает. Документация прямо советует сохранять готовое, сообщать об упавших ветках и не перезапускать пачку автоматически.latestвместо общей кучи отчётов. Судья и синтез видят только последнюю версию каждого кандидата, а спорное адресовано парой «кандидат + id».phase()иlog()показывают этапы и короткие заметки в виджете прогресса Swarm в Control UI. Ожидания они не добавляют: если интерфейса нет, скрипт не ждёт.llm_task. Внутри Code Mode инструменты плагинов доступны как функции, а дефис в имени заменяется подчёркиванием. Функция возвращаетdetailsинструмента: решение судьи в.json, модель, которая реально отвечала, в.model.- Цикл ограничен.
pass < 2здесь и есть условие остановки. ЛимитmaxTotalPerGroupдокументация называет последней страховкой от разбегания, а не заменой ясного условия выхода.
Почему судья — llm-task, а не ещё один agents.run. Вызов llm-task идёт без транскрипта агента, без инструментов и без запасной модели. Если выбранный рантайм не умеет такой изолированный вызов, тот падает до обращения к модели, а не превращается молча в обычный ход агента. Судье не нужно ничего открывать: всё, что он должен взвесить, уже лежит в input. Критикам, наоборот, инструменты нужны: без чтения источников критика превращается в спор мнений.
Вариант 2: без Code Mode
Если Code Mode включать не хочется, те же коллекторы доступны как обычные инструменты. Агенту нужны разрешённые sessions_spawn и agents_wait. Сами они в строгий профиль инструментов не добавляются, даже при включённом Swarm.
Запуск одного критика:
{
"task": "Ты скептичный рецензент. Открой каждый источник в отчёте и найди неподтверждённое… <отчёт>",
"collect": true,
"groupId": "critique-pass-1",
"label": "critic-A-1",
"model": "<провайдер-2>/<модель>",
"outputSchema": { "type": "object", "properties": { "holes": { "type": "array" } }, "required": ["holes"] },
"runTimeoutSeconds": 900
}
Ответ на запуск — квитанция со status: "accepted" и runId. Сохраните все runId, это ваша страховка на случай сбоя. Дальше агент собирает результаты:
{ "ids": ["<runId-1>", "<runId-2>", "<runId-3>"], "timeoutSeconds": 120 }
agents_wait возвращается, как только завершился хотя бы один ребёнок из списка или истёк таймаут. По умолчанию таймаут 30 секунд, максимум 600 (waitTimeoutSecondsMax), за вызов — до 1000 id. В ответе три массива: completed (у каждого status — done, failed, killed или timeout, — result, при схеме structured или schemaError, и usage с токенами), pending и errors.
В следующий вызов передавайте только оставшиеся pending. Ждать коллекторов через sessions_yield нельзя: он для обычных субагентов, которые сами присылают результат.
При разборе элемент может нести частичный structured и при этом иметь status: "failed". Порядок проверки задан документацией: непустой error, потом schemaError, потом непустой result.
Судью в этом варианте агент вызывает как обычный инструмент llm-task, с тем же prompt, input, schema и model.
Скрипт короче и сам держит цикл, но runId из agents.run вы увидите лишь в ошибке. Низкоуровневый путь многословнее, зато у вас на руках все runId и токены по каждому ребёнку, а после сбоя это решает.
Где человек
В конце и только по открытым вопросам. Скрипт возвращает родительскому агенту verdict. Если там escalate или непустой openQuestions, агент задаёт вопрос через ask_user: от одного до трёх вопросов, у каждого от двух до четырёх вариантов, свободный ответ добавляется сам. В Telegram, Discord, Slack и Mattermost один вопрос с одиночным выбором приходит кнопками.
Три ограничения:
ask_userесть только в основной сессии. Ни коллекторы, ни обычные субагенты его не получают, поэтому спросить из середины роя нельзя. Вопрос задаёт родитель, когда скрипт уже вернул результат.- Ожидание по умолчанию 900 секунд, допустимо от 30 до 3600. Если ответа нет, инструмент возвращает
no_answer, и агент продолжает по своему усмотрению. Если без человека вывод публиковать нельзя, запишите это правилом вAGENTS.md: приno_answer— не публиковать, а сохранить отчёт. - Коллекторы не открывают окно одобрения. Действие ребёнка, которому нужно одобрение, просто отклоняется. Поэтому исследователям и критикам давайте инструменты, которым одобрение не нужно: поиск и чтение страниц.
Сколько стоит и как посчитать
Считать надо вызовы. При трёх кандидатах первый круг — это 3 исследователя, 3 критика и 1 судья. Второй круг добавляет до 3 исследователей, 3 критиков и ещё одного судью, плюс синтез: итого до 15 вызовов модели. Из них 13 — дети группы, судья через llm-task в группу не входит. Сверху идут ходы самого родителя на exec и wait: в этой арифметике их нет, но в расходе они есть. Дороже всех обычно критики: они заново открывают источники.
Как мерить:
- В варианте 2 у каждого ребёнка в ответе
agents_waitестьusageсinputTokensиoutputTokens. Сложите их. - Отдельный агент даёт отдельную строку в общей сводке:
openclaw gateway usage-cost --agent research --days 7. Команда собирает её из логов сессий. - Документация Code Mode советует сравнивать полный расход корня вместе с потомками на типичных задачах, прежде чем включать режим широко.
Жёсткого потолка расходов нет: лимиты Swarm ограничивают число детей, а не токены. maxTotalPerGroup: 40 при двух кругах с запасом покрывает 13 детей и отсекает разбегание, если скрипт перепишут без ограничения кругов.
Где ломается
- Code Mode экспериментальный. Он в списке экспериментальных функций, а их форма и поведение меняются быстрее стабильных настроек. Исполнитель по умолчанию, Node, — это
node:vmв рабочем потоке, а не граница безопасности. Изоляцию даёт исполнитель QuickJS (executor: "quickjs"). - Рестарт обрывает скрипт. Прогон Code Mode живёт в памяти процесса, и после рестарта Gateway старый прогон не оживает. Gateway может восстановить прерванный ход из транскрипта, но с ограничениями на инструменты, поэтому рассчитывайте, что скрипт придётся запускать заново. Прерванные дети завершаются со статусом прерывания и сами не перезапускаются, а результаты завершившихся коллекторов сохраняются. Поэтому держите
runIdи не запускайте заново то, что уже завершилось. Каждый вызовexecилиwaitограничен 10 секундами: если дети ещё работают, вызов вернётwaiting, и агент продолжит черезwait. Подвешенный прогон живёт 900 секунд, таких прогонов в процессе не больше 64. - Лимиты считают детей, а не смысл. 32 одновременно — остальные встают в очередь. 50 живых в группе и 200 за жизнь группы — сверх этого запуск отклоняется с именем настройки в ошибке. Поднять
maxConcurrentне значит поднять два других лимита. - Без
modelкритик окажется на модели исследователя. Ребёнок безmodelнаследует модель запросившего (илиsubagents.modelиз конфига), и «враждебная» проверка становится самопроверкой. Ставьтеmodelкаждому критику явно, особенно в JSON запуска из варианта 2. Неверное значение может отклонить запуск — проверяйте ошибку в квитанции. Фактическую модель смотрите в транскрипте ребёнка, у судьи — в полеmodelответа llm-task. - Судья тоже модель. На модели критика он рискует подыгрывать «своему» (тот же self-enhancement bias), поэтому здесь он на третьей. Если третьей модели нет, учитывайте этот перекос при чтении вердикта.
- Схема проверяет форму, а не правду. Поле
sourceс URL пройдёт валидацию, даже если по ссылке написано обратное. У разных моделей бывают общие слепые пятна. Числа и версии сверяйте кодом, не моделью. - Дети одноразовые. Swarm запускает разовых коллекторов, API для многоходового работника с состоянием нет. Второй круг — это новые дети, которым прошлый отчёт и критика передаются текстом.
- Не любой ребёнок может быть коллектором. Режим коллектора работает только с родными субагентами OpenClaw: ACP (подключение внешних оболочек вроде Claude Code), привязка к треду, видимые и постоянные сессии не поддерживаются. Вложенные коллекторы документация не советует.
- В оболочке Codex параллельного запуска нет. Там вызовы динамических инструментов идут по очереди, и
Promise.allне отправит несколькоsessions_spawnодновременно. Запускайте детей в цикле: уже принятые работают, пока отправляются следующие.
Когда брать другое
- Один-два исследователя. Документация советует оставлять Swarm для пачек от пяти похожих детей, а для одного или нескольких брать обычный
sessions_spawn, который сам присылает результат. - Ответ проверяется кодом. Тест, сверка числа с таблицей, запрос к API. Модель-критик здесь дороже и хуже скрипта.
- Исследование на дни, которое должно переживать рестарты. Swarm не годится: нужны сохранённые этапы, например промежуточные отчёты в файлах и запуск по этапам.
- Фиксированный конвейер с одобрениями. Lobster: шаги, точки одобрения, продолжение после паузы. Сохранённых workflow-определений у Swarm нет и в планах документации тоже.
- Суждение, которое не сводится к фактам. Подходит ли библиотека вашей команде, решает человек. Враждебная проверка подготовит ему материал без дыр, но решение не примет.