Agent Loop Reduction: Дизайн через Skill-слой
Находится в одном каталоге с
rt-optimization-design.md, дополняет его: тот документ обсуждает сокращение раундов на уровне фреймворка (D1 пропуск финального раунда, D2 fast-маршрутизация, D4 prevalidate), а этот документ утверждает, что настоящий рычаг сокращения раундов находится на уровне проектирования skill/tool, и предлагает реализуемый путь, не зависящий от модификаций фреймворка или данных о hit rate кэша.
0. Спецификация приемки (gate перед разработкой)
Этот раздел является предварительным gate для разработки — в нем перечислено, какие спецификации должны быть подтверждены до начала работы, а какие — дождаться данных. Вынесение спецификаций вперед, а не “посмотрим на метрики после завершения”, необходимо, чтобы избежать: (a) написания кода с непроверяемыми метриками, (b) дрейфа пороговых значений вслед за результатами, что искажает выводы, (c) отсутствия стоп-лоссов, из-за которого решение может казаться работающим, но не давать выгоды.
Границы применимости этой спецификации: она предполагает, что правильность направления можно оценить после измерения базовой линии P1.5. Это верно для сценария “сокращения раундов”, так как он имеет четкие измеримые сигналы (количество раундов, followup_rate, batch_size). Для сценариев, выходящих за рамки этого предположения (например, в будущем “оптимизация качества” и другие трудно измеримые направления), предварительная спецификация может, наоборот, препятствовать быстрому обучению; в таких случаях следует вернуться к процессу управления §0.5 и переоценить, не применяя данный фреймворк механически.
Спецификация делится на четыре уровня — с разным временем фиксации:
| Уровень | Тип | Время фиксации |
|---|---|---|
| §0.1 | Инженерный уровень (пайплайн данных, корректность кода) | Предварительно, можно зафиксировать сразу |
| §0.2 | Статистический уровень (метрики “успеха” проекта) | Предварительно, пороги после базовой P1.5 |
| §0.3 | Стоп-лоссы (“отказ, если произойдет”) | Предварительно, неизменны |
| §0.4 | Per-skill спецификация (что менять, целевые значения) | Постфактум, на основе данных Layer 1 |
0.1 Инженерная спецификация (должна быть предварительной · можно зафиксировать сразу)
Спецификация корректности пайплайна данных и изменений кода — не зависит ни от каких бизнес-решений или базовых данных, должна быть зафиксирована до начала разработки:
- Канал qwen-logger работает (§4.1.1b): событие skill_launch должно попадать и в OTLP, и в qwen-logger
- Связка по
prompt_id: всеskill_launch+ последующиеtool_callот одного user prompt должны быть извлекаемы по одномуprompt_id batch_sizeне undefined (§4.3.2, направление A): для одиночного инструмента batch явно установитьbatch_size = 1/batch_position = 0- SQL выполним (§4.1.2): офлайн SQL на реальном telemetry backend выдает непустой результат и позволяет различить skill с высоким/низким followup_rate
- Дисперсия базовой линии < P50 × 20% (P1.5): стабильность базового измерения (иначе последующее A/B-сравнение будет недостоверным) — примечание: хотя этот пункт отнесен к §0.1 инженерному уровню, его фиксация зависит от данных базовой линии P1.5; это единственный пост-верифицируемый пункт в §0.1; если P1.5 не пройден, пороги §0.2 не могут быть надежно зафиксированы
- Бюджет объема skill (модификация Layer 2): после встраивания followup количество токенов в описании skill не должно превышать 2× от исходного, а абсолютное значение ≤ 500 токенов (берется меньшее значение). Если превышает, разделить skill, а не объединять (см. §4.2). Этот пункт уже согласован с §7 (пункт 2) и §4.2, вынесен на уровень спецификации
npm run preflightпроходит: жесткий порог для каждого PR
0.2 Статистическая спецификация (должна быть предварительной · пороги после P1.5)
Метрики, по которым проект считается “статистически успешным” — направление фиксируется предварительно, пороги — после измерений базовой линии (чтобы избежать выдумывания чисел):
| Метрика | Направление | Время фиксации | Текущий placeholder (ждет калибровки) |
|---|---|---|---|
взвешенный followup_rate для top-3 skill | ↓ | конец P1.5 | ≥ 30% |
| сквозное RT P50 для сессий, содержащих skill | ↓ | конец P1.5 | ≥ 2s |
доля tool_call с batch_size > 1 | ↑ | до P3 | ≥ 30% |
| Статистическая значимость A/B для модифицированного сценария skill | p < 0.05 | до завершения P2 | n TBD |
Ключевое ограничение: placeholder’ы порогов — не обещания. Если базовая линия P1.5 покажет “взвешенный followup_rate для top-5 skill < 30%” (срабатывание стоп-лосса §0.3 #1), проект прекращается; нельзя снижать спецификацию, чтобы “дотянуть” до порога.
Как измерять: методы измерения каждой метрики, SQL-шаблоны, дизайн A/B — см. §5.1-§5.2; расчет размера выборки для статистической значимости (p < 0.05) — §5.1.
0.3 Стоп-лоссы (должны быть предварительными · после фиксации на P-1 возможна ограниченная регулировка)
Перечислены в §5.3. Это жесткие условия, при которых проект отменяется — ни при каких обстоятельствах нельзя ослаблять стоп-лоссы ради достижения статистических спецификаций §0.2.
- Результативные метрики (3 штуки): взвешенный
followup_rateдля top-5 < 30% / после модификации 2 skill снижение RT P50 < 1s / после Layer 3batch_size P50все еще = 1 - Процессные метрики (3 штуки): снижение частоты срабатывания skill ≥ 5pp / частота неудач встроенного followup ≥ 5% / рост частоты отмен пользователем ≥ 2pp
Подробнее см. §5.3.
Правила регулировки (чтобы избежать жесткой дисциплины без данных):
| Этап | Можно ли регулировать | Направление регулировки |
|---|---|---|
| Фиксация на P-1 | ✅ Любая регулировка (на основе исторической телеметрии или консенсуса) | Любое |
| После P-1 → конец P1.5 | ❌ Нельзя регулировать | — |
| Конец P1.5 (появление базовой линии) | ✅ Разрешено только ослабление один раз | Ослабление (например, 30% → 25%) требует обоснования данными + ревью 2 человек; ужесточение запрещено (чтобы не добавлять стоп-лоссы постфактум) |
| После P1.5 | ❌ Нельзя регулировать | — |
Текущие placeholder’ы порогов (30% / 1s / 5pp и т.д.) не имеют обоснования историческими данными — это экспертные оценки инженеров до ревью на P-1. Если на ревью P-1 удастся получить историческую телеметрию за последние 4 недели, следует откалибровать стоп-лоссы на ее основе; если данные недоступны — оставить placeholder’ы, а в конце P1.5 выполнить указанное выше правило “одно ослабление”.
0.4 Per-skill спецификация (должна быть постфактум · на основе данных)
Какой именно skill менять, до какого уровня снижать followup_rate — не фиксировать до появления данных Layer 1.
Причина: априорное проектирование может сильно расходиться с апостериорными данными. Принудительное предварительное выдвижение может повторить судьбу маршрута D2 из rt-optimization-design.md §7 — предварительная гипотеза “fast модель быстрее на 2-3с” была опровергнута апостериорным фактом внедрения кэша, что привело к нулевой или отрицательной чистой выгоде.
Место создания: per-skill спецификация создается на основе данных в конце P1.5; для каждого PR Layer 2 она объявляется независимо в description (не входит в design-документ, чтобы не переписывать его при каждом изменении skill).
Шаблон структуры per-skill спецификации (согласован с обязательными полями PR description из §4.2 — эти два списка одинаковы, §4.2 — процессная перспектива, данный раздел — перспектива спецификации):
| Поле | Содержание | Источник данных |
|---|---|---|
| 1. Текущие данные | invocation_count, followup_rate, top followup tools | Layer 1 telemetry |
| 2. Цель | снизить followup_rate с X% до Y% | на основе направления улучшения §0.2, абсолютное значение устанавливается в PR |
| 3. Область модификации | какие followup встраиваются (read/grep/shell read-only), что явно не встраивается (write-операции / cross-skill / глубокий анализ) | Таблица схем модификации §4.2 |
| 4. Обновление контракта вывода | предварительные объявления, добавленные в описание skill (напр., “Returns: …”) | Пример модификации §3.2 |
| 5. A/B план | наблюдение в течение 2 недель после модификации за followup_rate / RT P50 / process-метриками, сравнение с линией приемки §5.1 | §5.1 |
| 6. Доказательство объема | количество токенов в описании skill до и после модификации (оценка с помощью tiktoken), не должно превышать “бюджет объема skill” из §0.1 | §0.1, пункт 6 |
0.5 Управление спецификацией
-
Изменение §0.1 / §0.3 требует обновления design-документа + ревью PR; §0.3 — только согласно “Правилам регулировки” §0.3 в окне ослабления в конце P1.5
-
Изменение порогов §0.2 (после фиксации на P1.5) требует предоставления хотя бы одного из следующих доказательств:
- (a) Анализ отклонений между результатами базового измерения P1.5 и зафиксированными порогами (с ссылкой на запись исходного измерения)
- (b) Публичные benchmark-данные аналогичных проектов (с ссылкой на источник)
- (c) Внутреннее обоснование отклонения с подписью ≥ 2 ревьюеров
Если при ревью PR ни одно из доказательств не предоставлено, ревьюер обязан заблокировать PR — не принимать “корректировку на основе экспертной оценки инженера”
-
Per-skill спецификация §0.4 после создания на основе данных записывается в описание PR (по 6-пунктовому шаблону §0.4), не входит в design-документ
1. Предпосылки и позиционирование
1.1 Проблема
rt-optimization-design.md §1.2 приводит базовую линию: 3 раунда agent loop, 13,4с сквозного времени, из которых 78% — вызовы LLM. Каждый раунд ~3-4с.
Раунд 1 (3,8с, 28%): LLM принимает решение вызвать skill
Раунд 2 (3,0с, 22%): LLM принимает решение вызвать shell
Раунд 3 (3,8с, 28%): LLM подводит итогПосле двух раундов ревью rt-optimization-design.md §6/§7 маршруты D2/D4 были отклонены, а D1/D3 понижены до “пересмотреть после завершения мелких доработок”. Но весь исходный документ был сосредоточен на последнем раунде (раунд 3, подведение итогов) или микрооптимизациях внутри одного раунда (D4), и совершенно не рассматривал, почему существует “промежуточный раунд” (Раунд 1 → Раунд 2) и можно ли его устранить.
Факт: Раунд 2 существует в подавляющем большинстве случаев потому, что вызванный в Раунде 1 skill не вернул полный ответ, и модель была вынуждена добавить shell-запрос для дополнения. Если бы skill был спроектирован так, чтобы “получить полный результат за один раз”, то 3 раунда превратились бы в 2, и экономия составила бы ~3с за счет Раунда 2 — это непересекающаяся выгода по сравнению с D1.
1.2 Отношение к rt-optimization-design
| Направление сокращения раундов | Затрагиваемый раунд | Точка приложения | Позиционирование в данном документе |
|---|---|---|---|
D1 skipLlmRound | Последний раунд (итоговый) | Механизм фреймворка + per-tool opt-in | Подстраховка, после Layer 2 |
| D2 fast-маршрутизация | Задержка одного раунда | Механизм фреймворка | Отложено, вне рамок данного документа |
| D3 Состояние Summarizing | Последний раунд (уровень восприятия) | UI state machine | Опционально, ортогонально данному решению |
| D4 prevalidate | Задержка одного раунда | Механизм фреймворка | Отложено, вне рамок данного документа |
| Данное решение Layer 1-3 | Промежуточный раунд + невызванные раунды из-за конкурентности | Проектирование skill + prompt engineering | Новое направление |
1.3 Ключевой тезис
Настоящий рычаг сокращения раундов находится на уровне проектирования skill/tool, а не на уровне agent-фреймворка. Три причины:
- Базовая линия §1.2 сама указывает на проблему в skill — прыжок Раунда 1 → Раунд 2 происходит из-за неполного возврата skill; фреймворк делает всё правильно, а skill — нет
- Сокращение раундов на уровне фреймворка в конечном итоге требует per-tool opt-in — D1 с
skipLlmRoundтребует явной маркировки каждого инструмента, что опять же возвращает нас к проектированию skill, плюс дополнительные затраты на исправление инвариантов и шлюзы принятия решений - ROI локально измеримо, легко делать постепенный rollout — изменение одного skill сокращает один раунд × частоту вызова этого skill, не зависит от данных о hit rate кэша, не требует изменений в других системах
Перед внедрением необходимо пройти предварительное ревью спецификации приемки §0 (этап P-1, 0,5d) — инженерная спецификация §0.1 и стоп-лоссы §0.3 должны быть зафиксированы до начала работы; направление статистических порогов §0.2 также должно быть предварительно подтверждено (конкретные значения фиксируются после базовой линии P1.5). Пропуск §0 и переход к внедрению P0 = молчаливое согласие с анти-паттерном “сначала сделаем, потом посмотрим на метрики”; данный документ не поддерживает такой подход.
2. Принципы проектирования
- Не менять agent-фреймворк — не трогать
useGeminiStream/coreToolScheduler/geminiChatв ядре - Приоритет на основе данных — сначала построить телеметрию, пусть данные говорят, какой skill менять, а не гадать
- Per-skill измеримость и постепенный rollout — каждая модификация skill независима в A/B, при неудаче локальный откат
- Приоритет на сложный процент — выгода = выгода от одного сокращения раунда × частота срабатывания, сначала high-frequency skill
- Не привязан к D1 — успех данного решения не зависит от внедрения D1
3. Трехуровневое решение
3.1 Layer 1: Телеметрия сокращения раундов (поиск золотой жилы)
Цель: пусть данные покажут, какие skill наиболее выгодно менять — то есть “после использования этого skill с какой вероятностью модель делает еще один вызов инструмента”.
Ключевые поля (per-turn, per-skill-invocation):
interface SkillFollowupRecord {
skill_name: string;
prompt_id: string; // связывает все события в рамках одного user prompt
turn_index: number; // номер раунда, в котором вызван skill
followup_tool_names: string[]; // какие инструменты были вызваны после skill в рамках того же prompt_id
followup_count: number; // followup_tool_names.length
followup_kinds: Kind[]; // Read/Edit/Execute/...
next_turn_is_terminal: boolean; // после skill следующий раунд — просто текст (без вызова инструмента)
user_followup_within_30s: boolean; // пользователь отправил новый prompt в течение 30с после отображения результата (сигнал регрессии качества)
}Ключевые метрики:
skill_followup_rate = sum(followup_count > 0) / total_invocationsterminal_after_skill_rate = sum(next_turn_is_terminal) / total_invocations- Агрегация по
(skill_name, top followup tool)— смотреть, после какого skill и какой инструмент чаще всего вызывается
Определение золотой жилы:
(invocation_count_weekly × skill_followup_rate) ≥ threshold
↓
Этот skill — золотая жила, приоритет для Layer 2Рекомендуемый порог: top-3 skill, отсортированные по указанной формуле; сначала менять первые 2.
3.2 Layer 2: Полнота вывода skill
Цель: заставить skill, определенные как золотая жила, возвращать полный ответ за один раз, устраняя прыжок Раунд 1 → Раунд 2.
Схема модификации (по типу followup):
| Followup-паттерн | Типичный сценарий | Направление модификации |
|---|---|---|
skill → read_file | skill возвращает путь, модель читает | Встроить чтение внутрь skill, вернуть содержимое |
skill → grep/glob | skill возвращает директорию, модель ищет | Встроить поиск внутрь skill, вернуть совпадения |
skill → shell (read-only) | skill возвращает команду, модель выполняет | Встроить выполнение команды внутрь skill, вернуть вывод |
skill → shell (write) | skill возвращает план, модель выполняет запись | Оставить (запись требует подтверждения, не объединять) |
| skill → another skill | цепочка вызовов | Не объединять (сохранять композиционность) |
Контрольный список модификации (шаблон PR per-skill):
- В описание skill заранее добавить контракт вывода: явно указать “Returns: full file content / matched lines / command output”, чтобы модель знала, что дополнительный запрос не нужен
- Внутри skill выполнить все read-only followup: встроить в skill те read/search операции, которые, согласно телеметрии, имеют >50% вероятность быть вызванными после skill
- Не встраивать write-операции: запись требует подтверждения пользователя, должна быть отдельным раундом
- Не встраивать followup с глубоким анализом: если followup — это “на основании этого проанализируй дальше”, это задача модели, а не skill
- Приложить A/B телеметрию: через 2 недели после модификации сравнить
followup_rate: снизился ли до <20%
Типичный пример модификации (схематично):
До модификации:
skill "list-workspaces" returns: ["ws_a", "ws_b"]
→ Раунд 2: модель вызывает shell для получения деталей каждого workspaceПосле модификации:
skill "list-workspaces" returns:
- ws_a (owner: foo, last_active: 2026-05-20, status: active)
- ws_b (owner: bar, last_active: 2026-05-01, status: archived)
описание обновлено: "Returns workspaces with owner, last_active, status"
→ Раунд 2 исчезает для ~80% запросов3.3 Layer 3: Обучение модели через prompt к конкурентности
Цель: для независимых инструментов (чтение нескольких файлов, поиск в нескольких директориях) заставить модель в одном раунде одновременно отправлять tool_calls, сжимая N раундов в 1.
Предварительное условие: инфраструктура уже готова — CONCURRENCY_SAFE_KINDS в tools/tools.ts:818 + partitionToolCalls в coreToolScheduler уже умеют конкурентно выполнять read/search/fetch инструменты в одном batch. Не хватает только желания модели активно отправлять конкурентные tool_calls; qwen-coder по умолчанию склонен к последовательности.
Место изменения: packages/core/src/core/prompts.ts (уже аудировано, добавление в секцию # Final Reminder около L396 не повлияет на попадание в кэш — только единовременная стоимость прогрева).
Инструктивный текст (схематично, требует A/B-настройки):
When you need to call multiple independent read-only tools (read_file,
grep, glob, web_fetch), emit them in a SINGLE tool_calls batch — do NOT
call them sequentially across rounds. They will execute concurrently.
Examples:
- Reading 3 files for comparison: emit 3 read_file calls in one batch
- Searching for 2 patterns: emit 2 grep calls in one batch
Do NOT batch when the second call depends on the first call's result.Измерение эффекта: добавить новое поле телеметрии batch_size (количество tool_calls в одном turn) — сравнить распределение до и после изменения prompt.
3.3.1 Расширение CONCURRENCY_SAFE_KINDS (подпункт Layer 3)
Обучение модели конкурентности через prompt — это только сторона предложения (модель готова отправлять несколько tool_calls за раз), но CONCURRENCY_SAFE_KINDS = { Read, Search, Fetch } в tools/tools.ts:818 определяет фактический диапазон инструментов, которые могут выполняться конкурентно: partitionToolCalls (coreToolScheduler.ts:775) упаковывает “непрерывные безопасные инструменты” в конкурентный batch, остальные выполняются последовательно.
Если модель по инструкции отправит 3 tool_calls, но один из них относится к Kind.Execute и не входит в безопасный набор, весь batch будет разбит на последовательное выполнение — выгода от изменения prompt Layer 3 будет сведена на нет runtime-планировщиком.
Кандидаты на расширение (по возрастанию риска):
Kind.Think(содержит save_memory / todo_write) — не добавлять, есть неявная запись- Только чтение shell (Execute, для которого
isShellCommandReadOnly()возвращает true) — вpartitionToolCallsуже есть специальная проверка (в комментариях кcoreToolScheduler.tspartitionToolCallsупомянуто “Execute (shell) is safe only when isShellCommandReadOnly() returns true”), текущее состояние уже покрывает, менятьCONCURRENCY_SAFE_KINDSне нужно - MCP-инструменты по типу
Kind— поведение разных MCP-серверов сильно различается, для безопасности нужно явное opt-in при регистрации инструмента
Вывод: текущий набор уже разумен, Layer 3 не зависит от расширения CONCURRENCY_SAFE_KINDS. Смысл данного подраздела: после сбора данных телеметрии batch_size, если окажется, что “P50 конкурентного batch < ожидаемого”, сначала проверить, не разбивает ли partitionToolCalls batch, а не то, что модель не отправляет запросы конкурентно. Это диагностический путь при неудаче A/B Layer 3, а не обязательное действие.
Спасибо: ревью в codex указало, что “расширение
CONCURRENCY_SAFE_KINDS— упущенный рычаг”. После проверки оценено: текущая реализация уже покрывает наибольшую долю через специальную проверкуisShellCommandReadOnly; расширение набора даст малую выгоду и несет большой риск; оставлено как диагностический путь.
4. Детальная реализация
4.1 Layer 1: Расширение телеметрии (1-2d)
4.1.1 Добавить prompt_id в SkillLaunchEvent
Место: packages/core/src/telemetry/types.ts:896
Текущий SkillLaunchEvent содержит только skill_name + success, нет prompt_id — нельзя связать с другими ToolCallEvent в том же turn.
// types.ts:896
export class SkillLaunchEvent implements BaseTelemetryEvent {
'event.name': 'skill_launch';
'event.timestamp': string;
skill_name: string;
success: boolean;
prompt_id: string; // новое поле
turn_index?: number; // новое поле
constructor(
skill_name: string,
success: boolean,
prompt_id: string, // новое поле
turn_index?: number, // новое поле
) { ... }
}Обновление вызывающего кода: 4 точки вызова logSkillLaunch в packages/core/src/tools/skill.ts (L386, L399, L426, L482). Из this.params нельзя получить prompt_id — BaseToolInvocation содержит только params, без поля request.prompt_id. Фактическая реализация использует утиную типизацию: SkillToolInvocation предоставляет setter setPromptId(id) и приватное поле promptId; CoreToolScheduler.buildInvocation (coreToolScheduler.ts:1253) после построения по утиной типизации вызывает setPromptId(request.prompt_id), следуя существующему паттерну setCallId; invocation в execute() во всех 4 вызовах logSkillLaunch передает this.promptId. В ранней версии данного раздела было неверно указано, что “BaseToolInvocation уже имеет request.prompt_id”; исправлено после ревью PR #4565.
4.1.1b Исправление канала qwen-logger (предварительно)
Перед добавлением prompt_id нужно устранить существующий разрыв в канале: packages/core/src/telemetry/qwen-logger/qwen-logger.ts:908 определяет метод logSkillLaunchEvent(event), но во всем репозитории нет ни одного вызова — logSkillLaunch в loggers.ts:958 напрямую идет по пути OTLP через logs.getLogger(SERVICE_NAME).emit(), минуя qwen-logger.
Последствия:
- События skill_launch по пути OTLP достигают OTLP collector (работает), но специализированный канал上报ки qwen-logger в настоящее время мертв
- Если telemetry backend потребляет данные из qwen-logger (а не из OTLP), события skill_launch вообще не上报ываются
- Офлайн SQL §4.1.2, создающий
SkillFollowupRecord, зависит от сохранения событий skill_launch — необходимо сначала проверить, видны ли сейчас skill_launch в backend
Два варианта исправления:
- A (рекомендуется) в
logSkillLaunchвloggers.ts:958добавить строкуQwenLogger.getInstance(config)?.logSkillLaunchEvent(event), по аналогии с обработкойlogToolCallвloggers.ts:230 - B подтвердить, что backend потребляет только из OTLP, и пометить
logSkillLaunchEventв qwen-logger как@deprecatedили удалить
Почему добавляем только один путь QwenLogger, а не все 4 пути, как у logToolCall:
logToolCall (loggers.ts:220-247) фактически имеет 4 выхода:
uiTelemetryService.addEvent(...)— отображение в UIconfig.getChatRecordingService()?.recordUiTelemetryEvent(...)— история чатаQwenLogger.getInstance(config)?.logToolCallEvent(...)— бэкенд-телеметрия qwen-logger- OTLP
logger.emit(...)— OpenTelemetry
skill_launch — чисто бэкенд-событие телеметрии, не требует отображения в UI (пользователь уже видит возврат SkillTool) и не требует попадания в историю чата (внутренние вызовы инструментов skill уже отдельно записаны через recordUiTelemetryEvent). Поэтому добавляется только третий путь (QwenLogger), четвертый (OTLP) остается, а пути 1/2 пропущены намеренно, не по ошибке.
Детали передачи полей: в loggers.ts:961-966 используется spread { ...event }, который автоматически передает новые поля (prompt_id будет добавлено в SkillLaunchEvent и попадет автоматически), но внутри logSkillLaunchEvent в qwen-logger.ts:908, если он явно деструктурирует event.skill_name / event.success, новые поля не попадут автоматически — нужно синхронизировать вручную.
Объем работ: путь A ~0,5d (включая проверку на стороне backend); путь B ~0,2d (удаление кода + документация).
4.1.2 Создание SkillFollowupRecord (офлайн агрегация)
Не требуется новый тип события — ToolCallEvent и SkillLaunchEvent уже содержат prompt_id, можно создать через офлайн SQL:
-- Псевдо-SQL, адаптировать под фактический telemetry backend
WITH skill_events AS (
SELECT prompt_id, skill_name, timestamp FROM events
WHERE event_name = 'skill_launch' AND success = true
),
tool_events AS (
SELECT prompt_id, function_name, timestamp FROM events
WHERE event_name = 'tool_call'
),
followups AS (
SELECT s.skill_name, s.prompt_id,
COUNT(t.function_name) AS followup_count,
ARRAY_AGG(t.function_name) AS followup_tool_names
FROM skill_events s
LEFT JOIN tool_events t
ON s.prompt_id = t.prompt_id AND t.timestamp > s.timestamp
GROUP BY s.skill_name, s.prompt_id
)
SELECT skill_name,
COUNT(*) AS invocations,
AVG(followup_count) AS avg_followup,
SUM(CASE WHEN followup_count > 0 THEN 1 ELSE 0 END)::FLOAT / COUNT(*) AS followup_rate
FROM followups
GROUP BY skill_name
ORDER BY invocations * followup_rate DESC;4.1.3 Сбор телеметрии в течение 1 недели
- Без изменений в поведении для пользователя
- Не требуется никаких конфигурационных флагов — у телеметрии уже есть opt-in фреймворк (настройка
telemetry.target) - Через 1 неделю создается отчет с ранжированием skill
4.2 Layer 2: Модификация skill (0,5-1d на skill)
По данным Layer 1, сверху вниз. Каждый skill — отдельный PR, описание PR должно содержать:
- Данные: текущий invocation_count, followup_rate, top followup tools
- Область модификации: какие followup были встроены (явно указать, что не встроено)
- Обновление контракта вывода: какие предварительные объявления добавлены в описание skill
- A/B план: через 2 недели после модификации повторно измерить followup_rate
Примечания:
- При встраивании read-операций в skill не нужно повторять всю обработку граничных случаев
read_file(кодировки, проверка на бинарность и т.д.) — вызывать сам инструментread_file, не переписывать его - Аналогично для встраивания grep/glob
- Встраиваемые shell-команды должны проходить через стандартный путь
executeToolCall(сохраняя телеметрию) - Не допускать взрывного роста объема skill: если после встраивания followup описание skill превышает 500 токенов, разделить skill, а не объединять
4.3 Layer 3: Обучение через prompt (0,5d на изменение + измерение и настройка)
4.3.1 Добавление инструкции по конкурентности
Место: packages/core/src/core/prompts.ts, секция # Final Reminder (L396)
Добавить инструктивный текст из раздела 3.3. Конкретная формулировка требует A/B — сначала самая простая версия, затем уточнение в зависимости от улучшения уровня конкурентности.
4.3.2 Добавление телеметрии batch_size
Место: packages/core/src/telemetry/types.ts, в ToolCallEvent или новый легковесный ToolBatchEvent
// Вариант A: добавить поля в ToolCallEvent (меньше инвазивно)
export class ToolCallEvent {
...
batch_size?: number; // количество tool_call в одном batch
batch_position?: number; // позиция внутри batch (0-indexed)
}
// Вариант B: новый ToolBatchEvent (семантически чище, но требует полного процесса для нового типа событий)Рекомендуемый вариант A — меньше изменений, удобно для агрегации при запросах.
Путь передачи состояния (критически важно — стоимость этого шага была недооценена в ранней версии):
partitionToolCalls(callsToExecute) в coreToolScheduler.ts:2456 возвращает batches, но информация о batch немедленно теряется на пути планирования:
executeToolCalls
└─ batches = partitionToolCalls(...) // знает batch.calls.length
└─ for batch of batches:
└─ this.runConcurrently(batch.calls, ...) // знает batch.calls.length
└─ executeSingleToolCall(call, ...) // ❌ уже не знает batch
└─ ...
└─ finalizeToolCalls
└─ logToolCall(config, new ToolCallEvent(call)) // ❌ нет контекста batchКонструктор ToolCallEvent (types.ts:189) принимает только один CompletedToolCall, без полей batch.
Направления исправления:
-
Направление A (рекомендуется): добавить в
ScheduledToolCallполяbatchSize?: number+batchPosition?: number. Заполнить в двух ветках:- Ветка конкурентности (
coreToolScheduler.ts:2459-2460,batch.calls.length > 1): перед входом в циклrunConcurrently(batch.calls, ...)каждомуcallприсвоитьbatchSize = batch.calls.length,batchPosition = i - Ветка последовательности (
L2462-2464for (const call of batch.calls)): для batch с одним инструментом явно установитьbatchSize = 1,batchPosition = 0(не оставлять undefined, иначе при агрегации в downstream телеметрии раунды, где конкурентность не сработала, будут ошибочно интерпретированы как пропущенные данные)
В конструкторе
new ToolCallEvent(call)эти поля читаются изcall - Ветка конкурентности (
-
Направление B: изменить сигнатуру конструктора
ToolCallEventнаnew ToolCallEvent(call, batchInfo?), синхронизировать все места вызова (4 точкиlogToolCall+ тесты). Объем изменений больше, чем A
Объем работ: направление A ~0,5d с unit-тестами; направление B ~1d (больше мест вызова).
Синхронное измерение “желания модели к конкурентности” — до и после изменения prompts.ts (Layer 3) сравнить распределение доли tool_call с batch_size > 1. Это ключевая метрика эффективности Layer 3; без этих данных A/B Layer 3 не может быть завершено.
4.3.3 Оценка влияния на кэш
Изменение prompts.ts приведет к однократному сбросу ephemeral cache DashScope (первый запрос — cache miss, затем восстанавливается). Это известная разовая стоимость, см. rt-optimization-design.md §7.8 “Аудит стабильности prompt”.
5. Приемка и измерение
Этот раздел — “методологическое” дополнение к спецификации приемки §0 — §0 объявляет “метрики успеха + время фиксации порогов”, а §5 объясняет “как измерять, как писать SQL, как проектировать A/B”. Пороги в этом разделе — текущие placeholder’ы §0.2; окончательные значения фиксируются после измерения базовой линии P1.5.
5.1 Per-skill A/B метрики (через 2 недели после модификации)
| Метрика | Линия приемки | Примечание |
|---|---|---|
followup_rate для этого skill | < 20% (если до было 70%+) | Основная метрика |
| Сквозное RT P50 для сценария, в котором вызывается этот skill | снижение ≥ 2с | За счет отсутствия одного раунда LLM |
Доля user_followup_within_30s для этого skill | не увеличивается | Пользователь не переспрашивает = ответ полный |
success-rate для этого skill | не снижается | Встраивание followup не внесло новых ошибок |
5.2 Общие показатели RT
| Метрика | Базовый уровень | Цель после Layer 2: исправление top-3 skill |
|---|---|---|
| сквозной RT P50 (включая сессии со skill) | 13,4 с (одиночный замер) / ожидается ≥3 сценария | снижение на 2-3 с |
| Tool batch P50 size (Layer 3) | подлежит измерению | ≥ 1,3 (>30% вызовов с конкурентным batch) |
| Совокупный followup_rate skill (взвешенное среднее) | подлежит измерению | снижение ≥ 30% |
5.3 Сигналы отказа — когда отказаться от этого направления
Стоп-линии по результатам:
- После Layer 1: взвешенный followup_rate для top-5 skill < 30% → пространство для сокращения раундов мизерное, Layer 2 нецелесообразен.
- После доработки 2 skill в Layer 2: снижение сквозного RT P50 < 1 с → направление ошибочно (возможно, followup — это запись, и объединение не нужно), остановиться и пересмотреть.
- Через 2 недели после изменения prompt в Layer 3: batch_size P50 всё ещё = 1 → модель не воспринимает инструкции о параллелизме, отказаться от Layer 3, оставить только Layer 1+2.
Стоп-линии по процессам (упреждающие индикаторы, чтобы избежать «псевдоактивности без реальной выгоды»):
- Снижение точности выбора skill (intended skill vs selected skill) на ≥ 5 п.п. → описание skill испорчено, модель выбирает неверный skill. Типичный сценарий: до переработки пользователь спрашивает X и всегда попадает в skill_a, после переработки иногда маршрутизируется в skill_b, ошибки нет (модель использует неверный skill, но кое-как выдает ответ), результатные метрики выглядят нормально, но followup_rate растёт. Метод измерения: добавить в телеметрию
skill_invocation_pattern— кластеризовать первые N ключевых слов user prompt, смотреть, какой skill срабатывает в каждом кластере; сравнить смещение top-1 до и после. - Доля неудачных встроенных followup skill ≥ 5% → переработка skill добавила ранее отсутствовавший сценарий сбоя (например, встроенный
read_fileобрабатывает большие файлы и вызывает переполнение памяти). Измерение: сравнениеSkillLaunchEvent.successдо и после. - Рост доли отмен пользователем (Ctrl+C) на ≥ 2 п.п. → вывод skill стал медленнее или длиннее, пользователь теряет терпение. Измерение: доля
ToolCallEvent.status === 'cancelled'.
6. Стыковка с D1/D3
6.1 Связь с D1
После доработки top skill в Layer 2 оставшиеся skill с большим количеством followup становятся истинными сценариями для D1 skipLlmRound — эти skill уже выдают полный ответ (не требуется второй раунд) и действительно являются финальными запросами (третий раунд — тоже пустая трата).
Порядок выполнения:
- Layer 1 телеметрия запущена → 1 неделя данных
- Layer 2 доработка top 2-3 skill → A/B тест 2 недели
- Layer 3 конкурентный prompt → тестирование 1 неделя
- Тогда оценка D1: сколько среди оставшихся часто используемых skill имеют вид «полный вывод + финальный запрос» → стоит ли 2–3 дня фреймворковой доработки.
6.2 Связь с D3
D3 (StreamingState.Summarizing) — это оптимизация на уровне восприятия, полностью ортогональна данному плану. Layer 1–3 сокращают реальное количество раундов, D3 сокращает воспринимаемое пользователем ожидание. Если Layer 2 уже снижает RT до приемлемого пользователем уровня, ценность D3 падает; в противном случае D3 можно наложить.
7. Ограничения и известные риски
- Охват ограничен областью доработки — если исправлены 10 skill, охватываются только их сценарии. Но выгода измерима, предсказуема и накопительна.
- Встроенные followup skill могут утяжелить отдельный skill — разрастание описания, медленная загрузка, снижение повторного использования. Защита: пункт 5 чек-листа Layer 2.
- Модель Layer 3 может не следовать инструкциям о параллелизме — qwen-coder обучается на последовательных данных; A/B данные могут показать, что изменение prompt бесполезно — это известный сценарий отказа.
- Границы приватности телеметрии —
SkillFollowupRecordне должен записывать параметры инструмента (по умолчанию берутся изToolCallEvent.function_args, но необходимо проверить, не раскрывает лиskill_nameнамерение пользователя). - Не применимо к sub-agent / cron / notification — эти маршруты не проходят через систему skill, план их не охватывает.
- Базовые данные скудны — используется единичный замер из
rt-optimization-design.md§1.2. До внедрения Layer 2 необходимо собрать базовые данные по ≥3 классам сценариев. - Расширение полей
logSkillLaunchсломает существующих потребителей телеметрии — нужно синхронно изменять 4 точки вызова и downstream логгеры. qwen-logger.ts:908logSkillLaunchEvent— мёртвый код — в репозитории нет ни одного вызова. §4.1.1b перечисляет предварительное исправление.
7.1 Границы с существующими механизмами фреймворка (выходят за рамки плана)
В репозитории уже есть несколько механизмов, косвенно связанных с сокращением раундов. План не переизобретает и не заменяет их:
| Существующий механизм | Местоположение | Связь с планом |
|---|---|---|
partitionToolCalls + runConcurrently (конкурентное выполнение) | coreToolScheduler.ts:775, 2473 | Layer 3 использует напрямую; план его не затрагивает |
CONCURRENCY_SAFE_KINDS (какие инструменты можно выполнять параллельно) | tools/tools.ts:818 | §3.3.1 обосновывает, что текущее состояние разумно; не расширяется |
FileReadCache (предотвращает повторное чтение одного файла) | services/fileReadCache.ts | Косвенно влияет на раунды «модель повторно читает файл»; уже активно; план не зависит и не улучшает |
chatCompressionService (сжатие истории) | services/chatCompressionService.ts | Ортогонально раундам (влияет на стоимость одного раунда, а не на количество); тот же компонент, что и gate wouldTriggerCompression в fast-маршруте rt-optimization-design.md §3.2 |
Этот перечень приводится, чтобы план не воспринимался как игнорирующий существующие механизмы.
8. График внедрения
Предусловие: этот график начинается с P-1 и не может быть пропущен. P-1 — это предварительный ревью Spec из §0, объём 0,5 дня, но обязательно — без прохождения в P0 не переходить. Это ограничение нужно, чтобы избежать анти-паттерна «сначала код, потом spec»: spec постфактум означает, что решение «считается успешным» откладывается до момента после получения результатов, и существует риск скорректировать spec для улучшения метрик (см. повтор ошибки D2 в
rt-optimization-design.md§7).
| Фаза | Содержание | Затраты | Результат | Блокировка spec |
|---|---|---|---|---|
| P-1 | Предварительный ревью spec | 0,5 д | §0.1 / §0.3 закреплены | Закрепить §0.1 (инженерный spec) + §0.3 (стоп-линии) |
| P0 | Исправление цепи qwen-logger (предварительное §4.1.1b) | 0,5 д | подтверждение видимости события skill_launch | Проверка пункта 1 §0.1 |
| P1 | Layer 1 телеметрия: добавить поле prompt_id + офлайн SQL | 1–2 д | отчёт с ранжированием skill | Проверка пунктов 2/3/4 §0.1 |
| P1.5 | Сбор данных за 1 неделю + замер базового уровня (≥3 класса × ≥10 раз) | 1 нед | принятие решения, какие 2-3 skill дорабатывать | Закрепить пороги §0.2 + проверить пункт 5 §0.1 |
| P2 | Layer 2: доработка top-1 skill (PR + A/B) | 0,5–1 д доработка + 2 нед наблюдения | проверка снижения followup_rate ↓ и RT P50 ↓ | Объявить per-skill spec §0.4 внутри PR |
| P3 | Layer 3: конкурентный инструкция в prompt + телеметрия batch_size (включая передачу состояния §4.3.2) | 1–1,5 д изменений + 1 нед тестирование | распределение batch_size | Проверка пункта 3 §0.2 |
| P4 | Layer 2: продолжить доработку top-2 / top-3 skill (параллельно с P3) | 0,5–1 д × N | совокупное снижение RT P50 | Объявлять §0.4 в каждом PR |
| P5 | Оценка, стоит ли D1 | совещание | обновление road map | — |
Ключевые точки принятия решений (сверка со стоп-линиями §0.3):
- Конец P-1: любой из пунктов §0.1 / §0.3 не согласован → не переходить в P0.
- Конец P1.5: срабатывает результатный индикатор №1 §0.3 (взвешенный followup_rate для top-5 < 30%) → завершить направление; иначе закрепить пороги §0.2.
- Конец P2: срабатывает результатный индикатор №2 §0.3 (снижение RT P50 после top-1 < 1 с) или любой процессный индикатор → остановиться и пересмотреть.
- Конец P3: срабатывает результатный индикатор №3 §0.3 (batch_size P50 всё ещё = 1) → отказаться от Layer 3.
- P5: на основе формы оставшихся skill определить ROI D1.
9. Ключевые места в коде
| Файл | Ключевые символы | Строка |
|---|---|---|
packages/core/src/telemetry/types.ts | ToolCallEvent (содержит prompt_id / duration_ms) | L170 |
packages/core/src/telemetry/types.ts | SkillLaunchEvent (нужно добавить prompt_id) | L896 |
packages/core/src/telemetry/loggers.ts | logToolCall | L220 |
packages/core/src/telemetry/loggers.ts | logSkillLaunch (через OTLP; отсутствует пересылка в qwen-logger) | L958 |
packages/core/src/telemetry/loggers.ts | logToolCall (двойной маршрут: OTLP + qwen-logger, как шаблон исправления) | L220, L230 |
packages/core/src/telemetry/qwen-logger/qwen-logger.ts | logSkillLaunchEvent (текущий мёртвый код, цель предварительного исправления §4.1.1b) | L908 |
packages/core/src/core/coreToolScheduler.ts | partitionToolCalls | L775 |
packages/core/src/core/coreToolScheduler.ts | runConcurrently / планирование batch | L2456, L2473 |
packages/core/src/core/coreToolScheduler.ts | точки вызова logToolCall (конечная точка передачи состояния batch_size) | L3163 |
packages/core/src/services/fileReadCache.ts | FileReadCache (уже существует, влияет на повторное чтение) | L135 |
packages/core/src/tools/skill.ts | SkillTool + 4 точки вызова logSkillLaunch | L386, L399, L426, L482 |
packages/core/src/skills/skill-manager.ts | SkillManager (регистрация/загрузка skill) | весь файл |
packages/core/src/skills/skill-load.ts | загрузка описания skill (точка входа для изменения контракта вывода) | весь файл |
packages/core/src/tools/tools.ts | Kind + CONCURRENCY_SAFE_KINDS | L793, L818 |
packages/core/src/core/coreToolScheduler.ts | partitionToolCalls + runConcurrently (существующая инфраструктура конкурентности) | см. rt-optimization-design.md §5.7 |
packages/core/src/core/prompts.ts | секция # Final Reminder (место добавления инструкции о параллелизме в Layer 3) | L396 |
.qwen/skills/ | каталог определений каждого skill (объект доработки Layer 2) | директория |