Agent Loop: стратегия сокращения циклов — от проектирования Skill
Находится в одном каталоге с
rt-optimization-design.md, дополняет его: тот документ обсуждает сокращение циклов на уровне механизмов фреймворка (D1 — пропуск итоговой сводки, D2 — быстрая маршрутизация, D4 — предварительная валидация), а этот документ утверждает, что реальный рычаг сокращения циклов находится на уровне проектирования Skill/Tool, и предлагает реализуемый путь, не требующий доработки фреймворка или данных о cache hit rate.
0. Спецификация приёмки (предварительный gate для разработки)
Этот раздел — предварительный gate для разработки — перечисляет, какие спецификации должны быть подтверждены до начала работы, а какие — дождаться данных. Размещение spec заранее, а не «посмотрим на метрики после реализации», позволяет избежать: (a) ситуации, когда после написания кода выясняется, что метрики неизмеримы, (b) дрейфа пороговых значений вместе с результатами, что искажает выводы, (c) отсутствия стоп-линии, из-за чего решение выглядит как «работа идёт, а пользы нет».
Область применимости данного фреймворка spec: он предполагает, что корректность направления можно оценить после замера базовой линии P1.5. Это допущение верно для сценария «сокращение циклов», поскольку он имеет чёткие измеримые сигналы (число циклов, followup_rate, batch_size). Для сценариев, выходящих за рамки этого допущения (например, если в будущем тот же фреймворк будет использоваться для «оптимизации качества» и других трудно измеримых направлений), предварительное задание spec может, наоборот, мешать быстрому обучению; в таких случаях вернитесь к процессу управления в §0.5 и переоцените, не применяя механически данный фреймворк.
Spec делится на четыре уровня — с разным временем фиксации:
| Уровень | Тип | Время фиксации |
|---|---|---|
| §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: один user prompt, инициирующийskill_launch+ последующиеtool_call, должны объединяться однимprompt_idдля полного трейса batch_sizeне undefined (направление A в §4.3.2): для одиночного tool 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 (инженерные spec), его фиксация зависит от данных базовой линии P1.5; это единственная позиция в §0.1, проверяемая постфактум; если P1.5 не пройдена, пороги из §0.2 нельзя надёжно зафиксировать
- Бюджет объёма Skill (доработка Layer 2): после встраивания followup количество токенов в описании skill не должно превышать 2× от исходного и абсолютно ≤ 500 токенов (выбирается меньшее). При превышении — разделить skill согласно §4.2, а не объединять. Этот пункт согласуется с уже существующими ограничениями из §7 (пункт 2) и §4.2; вынесен на уровень spec
npm run preflightпроходит — жёсткое требование для каждого PR
0.2 Статистические спецификации (должны быть зафиксированы до начала · пороги — после P1.5)
Показатели, по которым проект считается «статистически успешным» — направление фиксируется заранее, пороги — после замера базовой линии (чтобы не вписывать числа на пустом месте):
| Показатель | Направление | Время фиксации | Текущий порог (ждёт калибровки) |
|---|---|---|---|
Взвешенный followup_rate топ-3 skill | ↓ | конец P1.5 | ≥ 30% |
| P50 end-to-end RT сессий, содержащих skill | ↓ | конец P1.5 | ≥ 2 с |
Доля tool_call с batch_size > 1 | ↑ | до P3 | ≥ 30% |
| Статистическая значимость A/B для изменённого skill сценария | p < 0.05 | до завершения P2 | n — не определено |
Ключевое ограничение: предварительные пороги не являются обязательством. Если базовая линия P1.5 покажет, что «взвешенный followup_rate топ-5 skill < 30%» (срабатывает стоп-линия §0.3, №1), проект прекращается; нельзя снижать spec, чтобы «достичь» порога.
Как измерять: методы измерения каждого показателя, SQL-шаблоны, дизайн A/B см. в §5.1–§5.2; расчёт объёма выборки для статистической значимости (p < 0.05) — в §5.1.
0.3 Стоп-линии (должны быть зафиксированы до начала · после фиксации P-1 ограниченно изменяемы)
Перечислены в §5.3. Это жёсткие условия отказа: «если произойдёт — прекращаем». Ни при каких обстоятельствах нельзя ослаблять стоп-линии ради достижения статистических spec из §0.2.
- Результативные показатели (3): взвешенный
followup_rateтоп-5 < 30% / после изменения двух skill P50 RT ↓ < 1 с / после Layer 3batch_size P50всё ещё = 1 - Процессные показатели (3): уровень попадания skill ↓ ≥ 5 п.п. / доля неудачных встроенных followup ≥ 5% / доля отмен пользователем ↑ ≥ 2 п.п.
Подробнее см. §5.3.
Правила изменения (чтобы дисциплина не была жёсткой без данных):
| Этап | Можно менять? | Направление изменения |
|---|---|---|
| До фиксации P-1 | ✅ допускается любое изменение (на основе исторической телеметрии или консенсуса) | любое |
| После фиксации P-1 → конец P1.5 | ❌ нельзя | — |
| Конец P1.5 (когда получена базовая линия) | ✅ допускается только смягчение один раз | смягчение (например, 30% → 25%) с предоставлением данных + рецензия 2 человек; ужесточение запрещено (чтобы не добавлять стоп-линии post factum) |
| После P1.5 | ❌ нельзя | — |
Предварительные пороги (30% / 1 с / 5 п.п. и т.д.) в настоящее время не подтверждены историческими данными, это инженерная интуиция до рецензии P-1. Если на рецензии P-1 удастся получить историческую телеметрию за последние 4 недели, следует откалибровать стоп-линии на её основе; если данных нет — сохранить предварительные значения, а в конце 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 должен содержать их в своём описании (не вносятся в документ с дизайном, чтобы не править документ при каждом изменении skill).
Шаблон структуры per-skill спецификации (согласуется с обязательными полями PR description из §4.2 — эти два перечня представляют одно и то же; §4.2 — процессная перспектива, данный раздел — перспектива spec):
| Поле | Содержание | Источник данных |
|---|---|---|
| 1. Текущие данные | invocation_count, followup_rate, топ followup tools | телеметрия Layer 1 |
| 2. Цель | снизить followup_rate с X% до Y% | на основе направления улучшения из §0.2, абсолютное значение задаётся в PR |
| 3. Объём изменений | какие followup встроить (read/grep/shell read-only), явно указать, что не встраивается (write-операции / межskill / глубокие рассуждения) | таблица шаблонов изменений из §4.2 |
| 4. Обновление контракта вывода | предварительное объявление в описании skill («Returns: …») | пример изменений из §3.2 |
| 5. A/B-план | наблюдать после изменений 2 недели за followup_rate / P50 RT / процессными показателями, сравнить с линией приёмки из §5.1 | §5.1 |
| 6. Доказательство объёма | количество токенов в описании skill до и после изменений (оценка через tiktoken), не должно превышать «Бюджет объёма Skill» из §0.1 | §0.1, пункт 6 |
0.5 Управление спецификациями
-
Изменение §0.1 / §0.3 spec требует обновления design-документа + ревью PR; §0.3 допускает ослабление только в рамках окна P1.5 в соответствии с «правилами настройки» §0.3
-
Изменение порогов §0.2 (после фиксации P1.5) требует приложить хотя бы одно из следующих доказательств:
- (a) Анализ отклонений базовых измерений P1.5 от зафиксированных порогов (со ссылкой на исходные записи измерений)
- (b) Публичные benchmark-данные аналогичных проектов (с указанием источника)
- (c) Пояснение отклонения, подписанное не менее чем 2 внутренними рецензентами
При ревью PR, если ни одного из указанных доказательств нет, рецензент обязан заблокировать PR — не допускается «настройка по интуиции инженера».
-
§0.4 per-skill spec записывается в PR description после того, как данные будут готовы (по шаблону 6 пунктов §0.4), в design-документ не включается.
1. Предпосылки и позиционирование
1.1 Проблема
Базовые показатели из rt-optimization-design.md §1.2: 3 цикла agent loop, 13.4s end-to-end, из которых 78% занимают вызовы LLM. Каждый цикл ~3-4s.
Раунд 1 (3.8s, 28%): LLM принимает решение вызвать skill
Раунд 2 (3.0s, 22%): LLM принимает решение вызвать shell
Раунд 3 (3.8s, 28%): LLM подводит итогПосле двух раундов ревью в rt-optimization-design.md §6/§7, D2/D4 были отклонены, а D1/D3 понижены до «оценить после завершения шелухи». Но весь исходный документ сосредоточен на последнем раунде (Раунд 3, подведение итогов) или микрооптимизациях внутри одного раунда (D4), при этом полностью игнорируется вопрос: почему существует Раунд 1 → Раунд 2 (промежуточный раунд) и можно ли его убрать.
Фактически: Раунд 2 существует в подавляющем большинстве случаев потому, что skill, вызванный в Раунде 1, не вернул полного ответа, и модель вынуждена дополнительно выполнять запрос shell для восполнения. Если спроектировать skill так, чтобы он «одним вызовом возвращал полный результат», то 3 раунда → 2 раунда, и экономия составит те самые ~3s из Раунда 2 — это выгода, не пересекающаяся с D1.
1.2 Отношение к rt-optimization-design
| Направление сокращения раундов | Затронутые раунды | Точка воздействия | Позиционирование данного документа |
|---|---|---|---|
D1 skipLlmRound | Последний раунд подведения итогов | Инфраструктура фреймворка + per-tool opt-in | Запасной вариант, ставится после Layer 2 |
| D2 быстрая маршрутизация | Задержка одного раунда | Инфраструктура фреймворка | Отложено, не входит в область данного документа |
| D3 Состояние Summarizing | Последний раунд (уровень восприятия) | UI-конечный автомат | Опционально, ортогонально данному решению |
| D4 prevalidate | Задержка одного раунда | Инфраструктура фреймворка | Отложено, не входит в область данного документа |
| Данное решение Layer 1-3 | Промежуточный раунд принятия решений + невыполненные раунды | Проектирование skill + prompt engineering | Новое направление |
1.3 Основное утверждение
Настоящий рычаг сокращения раундов находится на уровне проектирования skill/tool, а не в фреймворке агента. Три причины:
- Базовые показатели §1.2 как раз показывают проблему в skill — переход от Раунда 1 к Раунду 2 происходит из-за неполного возврата результата skill; фреймворк работает правильно, а skill — неправильно
- Сокращение раундов на уровне фреймворка в конечном счёте требует per-tool opt-in — D1 с
skipLlmRoundтребует явной маркировки каждого инструмента, что оборачивается дополнительными затратами на фиксацию инвариантов и шлюзы принятия решений - ROI локально измеримо и легко серое масштабирование — изменение одного skill сокращает один раунд × частоту его вызова, не полагаясь на данные по cache hit rate или кроссистемные изменения
Перед реализацией необходимо сначала пройти пре-ревью спецификации §0 (фаза P-1, 0.5 дня) — §0.1 (инженерная спецификация) и §0.3 (стоп-линии) должны быть зафиксированы до начала работы; направление §0.2 (статистические пороги) также должно быть подтверждено заранее (конкретные значения будут зафиксированы после P1.5). Пропуск §0 и переход к реализации P0 = по умолчанию принимать анти-шаблон «сначала сделаем, потом посмотрим метрики», и данный документ не одобряет такой подход.
2. Принципы проектирования
- Не изменять фреймворк агента — не трогать основные пути
useGeminiStream/coreToolScheduler/geminiChat - Выбор приоритетов на основе данных — сначала построить телеметрию, пусть данные укажут, какой skill менять, а не полагаться на догадки
- Per-skill измеримо и серое масштабирование — для каждого изменения skill отдельный A/B-тест, при неудаче локальный откат
- Приоритет сложных процентов — выгода = сокращение времени на один раунд × частота вызова, в первую очередь высокочастотные 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
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 → другой skill | цепной вызов | Не объединять (сохранять композиционность) |
Чеклист модификации (шаблон PR для per-skill):
- Предварительно объявить контракт вывода в описании skill: например, «Returns: full file content / matched lines / command output», чтобы модель знала, что дополнительный запрос не нужен
- Выполнить все read-only followup внутри skill: операции чтения/поиска, по которым телеметрия показывает >50% вероятность последующего вызова, встроить в skill
- Не встраивать операции записи: операции записи требуют подтверждения пользователя, должны оставаться отдельным раундом
- Не встраивать последующие действия, требующие глубокого вывода: если followup — это «проанализировать на основе этого», это задача модели, а не skill
- Приложить A/B-телеметрию: после модификации сравнить
followup_rateчерез 2 недели — должно снизиться до <20%
Типичный пример модификации (схема):
До модификации:
skill "list-workspaces" returns: ["ws_a", "ws_b"]
→ Раунд 2: модель вызывает shell для получения деталей каждой рабочей областиПосле модификации:
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)
description updated: "Returns workspaces with owner, last_active, status"
→ Раунд 2 исчезает примерно для 80% запросов3.3 Слой 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 (проверено, добавление около строки 396 в разделе # Final Reminder не повлияет ни на что, кроме кэширования — только одноразовые затраты на прогрев).
Текст инструкции (схематично, требуется A/B-оптимизация):
Когда вам нужно вызвать несколько независимых инструментов только для чтения (read_file,
grep, glob, web_fetch), отправляйте их в ОДНОМ batch tool_calls — НЕ вызывайте
их последовательно в разных раундах. Они будут выполнены конкурентно.
Примеры:
- Чтение 3 файлов для сравнения: отправьте 3 вызова read_file в одном batch
- Поиск 2 шаблонов: отправьте 2 вызова grep в одном batch
НЕ создавайте batch, когда второй вызов зависит от результата первого.Метрика эффективности: Добавить поле телеметрии batch_size (количество tool_calls в одном turn) — сравнить распределение до и после изменения промпта.
3.3.1 Расширение CONCURRENCY_SAFE_KINDS (подзадача слоя 3)
Промпт обучает модель конкурентности только на стороне поставки (модель готова отправлять несколько tool_calls за раз), но CONCURRENCY_SAFE_KINDS = { Read, Search, Fetch } в tools/tools.ts:818 определяет фактический диапазон инструментов, которые могут выполняться конкурентно: partitionToolCalls (coreToolScheduler.ts:775) группирует “последовательные безопасные инструменты” в конкурентный batch, остальные выполняются последовательно.
Если модель по инструкции отправит 3 tool_calls, но один из них относится к Kind.Execute и не входит в безопасный набор, весь batch будет разделён и выполнен последовательно — выгода от изменения промпта в слое 3 будет нивелирована планировщиком выполнения.
Кандидаты на расширение (по возрастанию риска):
Kind.Think(включая save_memory / todo_write) — не добавлять, есть неявные записи.- Только чтение в shell (Execute, для которых
isShellCommandReadOnly()возвращает true) —partitionToolCallsуже содержит специальную обработку (в комментарииpartitionToolCallsвcoreToolScheduler.tsуказано: “Execute (shell) безопасен только когдаisShellCommandReadOnly()возвращает true”), текущее состояние уже покрывает, изменятьCONCURRENCY_SAFE_KINDSне нужно. - MCP-инструменты по
Kind— поведение разных MCP-серверов сильно различается, требуется явный opt-in при регистрации инструмента для безопасности.
Вывод: Текущий набор уже разумен, слой 3 не требует расширения CONCURRENCY_SAFE_KINDS. Смысл этого раздела: после сбора данных телеметрии batch_size, если обнаружено, что “медиана конкурентных batch < ожидаемой”, сначала проверить, не вызвано ли это разбиением partitionToolCalls, а не нежеланием модели работать конкурентно. Это диагностический путь при неудаче A/B-тестирования слоя 3, не обязательный элемент.
Кредит: codex review указал, что “расширение
CONCURRENCY_SAFE_KINDS— это упущенный рычаг”. Проверив, решили: текущее состояние уже покрывает самый большой объём благодаря специальной обработкеisShellCommandReadOnly, расширение набора даёт малую выгоду при большом риске; оставить как диагностический путь.
4. Детальная реализация
4.1 Слой 1: Расширение телеметрии (1-2 дня)
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. Фактическая реализация использует duck-typing: SkillToolInvocation предоставляет setter setPromptId(id) + приватное поле promptId, а CoreToolScheduler.buildInvocation (coreToolScheduler.ts:1253) после сборки duck-типированием вызывает setPromptId(request.prompt_id), следуя существующему шаблону setCallId; invocation передаёт this.promptId во всех 4 вызовах logSkillLaunch внутри execute(). Раннее описание этого раздела (“у 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-коллектора (уже работает), но специализированная цепочка отчётов qwen-logger в настоящее время мертва.
- Если бэкенд телеметрии потребляет данные из qwen-logger (а не из OTLP), события skill_launch вообще не будут отправлены.
- В §4.1.2 производная запись
SkillFollowupRecordчерез офлайн SQL зависит от того, что событие skill_launch попадает в хранилище — сначала необходимо проверить, видимо ли сейчас skill_launch на бэкенде.
Варианты исправления:
- A (рекомендуется) Добавить в
loggers.ts:958вlogSkillLaunchстрокуQwenLogger.getInstance(config)?.logSkillLaunchEvent(event), по аналогии сlogToolCallвloggers.ts:230. - B Подтвердить, что бэкенд потребляет только из OTLP, и пометить
logSkillLaunchEventв qwen-logger как@deprecatedили удалить.
Почему добавляем только одну цепочку QwenLogger, а не все 4 как в logToolCall:
logToolCall (loggers.ts:220-247) имеет 4 выхода:
uiTelemetryService.addEvent(...)— отображение в UI.config.getChatRecordingService()?.recordUiTelemetryEvent(...)— история чата.QwenLogger.getInstance(config)?.logToolCallEvent(...)— телеметрия бэкенда qwen-logger.- OTLP
logger.emit(...)— OpenTelemetry.
skill_launch — это чисто бэкендное событие телеметрии, его не нужно показывать в UI (пользователь уже видит returnDisplay SkillTool) и не нужно включать в историю turn-ов ChatRecording (инструменты внутри skill уже сами записываются через recordUiTelemetryEvent). Поэтому добавляем только 3-й путь (QwenLogger), сохраняем 4-й (OTLP), пропуск 1/2 — осознанный, не ошибка.
Детали передачи полей: loggers.ts:961-966 использует spread { ...event }, который автоматически передаёт новые поля (после добавления prompt_id в SkillLaunchEvent этот путь сработает автоматически), но внутри logSkillLaunchEvent в qwen-logger.ts:908, если он явно деструктурирует event.skill_name / event.success, новые поля не будут включены автоматически, потребуется ручная синхронизация.
Трудоёмкость: путь A ~0,5 дня (включая подтверждение на стороне бэкенда); путь B ~0,2 дня (удаление кода + документация).
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 неделю — отчёт по рейтингу навыков
4.2 Layer 2: Модификация навыков (0.5–1 день на навык)
Модификация сверху вниз на основе данных Layer 1. Каждый навык — отдельный PR, описание PR должно содержать:
- Данные: текущее количество вызовов, followup_rate, основные followup-инструменты
- Область модификации: какие followup были встроены (и что явно не встраивается)
- Обновление контракта вывода: какие предварительные объявления добавлены в описание навыка
- План A/B: наблюдение за followup_rate в течение 2 недель после модификации
Примечания:
- При встраивании операций чтения в навык не повторяйте всю обработку граничных случаев
read_file(кодировка, обнаружение бинарных файлов и т.д.) — вызывайте сам инструментread_file, не переписывайте его - Аналогично для grep/glob
- Команды оболочки, встроенные в навык, должны проходить стандартный путь
executeToolCall(с сохранением телеметрии) - Не допускайте раздувания навыков: если после встраивания followup описание навыка превышает 500 токенов, разделите навык, а не объединяйте
4.3 Layer 3: Обучение промпта (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)
}
// Вариант B: новый ToolBatchEvent (семантически чище, требует полного процесса нового типа события)Рекомендуется вариант A — меньше изменений, удобнее агрегация при запросах.
Путь передачи состояния (критично — стоимость этого шага была недооценена в ранних версиях):
coreToolScheduler.ts:2456 в partitionToolCalls(callsToExecute) возвращает 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 (рекомендуется): добавить поля
batchSize?: number+batchPosition?: numberвScheduledToolCall. Заполнить в двух ветках:- Ветка конкурентности (
coreToolScheduler.ts:2459-2460,batch.calls.length > 1): перед входом вrunConcurrently(batch.calls, ...)для каждогоcallустановитьbatchSize = batch.calls.length,batchPosition = i - Последовательная ветка (цикл
for (const call of batch.calls)в L2462-2464): для batch с одним инструментом явно установитьbatchSize = 1,batchPosition = 0(не оставляйте undefined, иначе при агрегации телеметрии такие вызовы будут ошибочно считаться отсутствующими данными)
В конструкторе
new ToolCallEvent(call)считывать эти поля изcall - Ветка конкурентности (
-
Направление B: изменить сигнатуру конструктора
ToolCallEventнаnew ToolCallEvent(call, batchInfo?), синхронно обновить все вызовы (4 точки вызова logToolCall + тесты). Объём изменений больше, чем в A
Трудоёмкость: направление A — около 0.5d включая модульные тесты; направление B — около 1d (больше вызовов).
Синхронное измерение “готовности модели к конкурентности” — до и после изменения prompts.ts в Layer 3 сравнить распределение доли tool_call с batch_size > 1. Это ключевой показатель эффективности Layer 3; без этих данных A/B-тестирование Layer 3 невозможно завершить.
4.3.3 Оценка влияния на кэш
Изменение prompts.ts приведёт к единовременной инвалидации эфемерного кэша DashScope (первый запрос — cache miss, затем восстановление). Это известная единовременная стоимость, см. §7.8 документа rt-optimization-design.md о стабильном аудите промптов.
5. Приёмка и метрики
Этот раздел является “методологическим” дополнением к спецификации приёмки из §0 — §0 объявляет “какие показатели считаются успешными + пороговые значения и момент до/после”, §5 описывает “как измерять, какие SQL писать, как проектировать A/B”. Пороговые значения в этом разделе — текущие заполнители из §0.2; окончательные значения будут зафиксированы после измерения базовой линии P1.5.
5.1 Метрики A/B для каждого навыка (через 2 недели после модификации)
| Метрика | Порог приёмки | Примечание |
|---|---|---|
followup_rate для этого навыка | < 20% (если до было 70%+) | Основной показатель |
| P50 сквозного времени ответа в сценариях с навыком | снижение ≥ 2 с | За счёт уменьшения на один вызов LLM |
Доля user_followup_within_30s для навыка | не увеличивается | Пользователь не уточняет = ответ полон |
success для навыка | не снижается | Встраивание followup не внесло новых ошибок |
5.2 Общие метрики времени ответа
| Метрика | Базовая линия | Цель после модификации топ-3 навыков Layer 2 |
|---|---|---|
| P50 сквозного времени ответа (сессии с навыком) | 13.4 с (однократный замер) / необходимо ≥3 сценариев | снижение на 2–3 с |
| P50 размера batch инструментов (Layer 3) | необходимо измерить | ≥ 1.3 (более 30% вызовов участвуют в конкурентных batch) |
Общий followup_rate навыков (средневзвешенный) | необходимо измерить | снижение ≥ 30% |
5.3 Сигналы отказа — когда прекращать движение по направлению
Стоп-линии по результирующим метрикам:
- После получения данных Layer 1: взвешенный followup_rate для top-5 навыков < 30% → пространство для сокращения раундов мало, не имеет смысла продолжать Layer 2
- После доработки 2 навыков в Layer 2: снижение end‑to‑end RT P50 < 1 с → направление оптимизации неверно (возможно, followup — это операция записи, которую не следует объединять), остановитесь и переосмыслите
- Через 2 недели после изменения промпта Layer 3: batch_size P50 всё ещё = 1 → модель не воспринимает инструкции по параллелизму, откажитесь от Layer 3, оставьте только Layer 1+2
Стоп-линии по процессным метрикам (раннее предупреждение, чтобы решение не выглядело «как будто работает, но на деле без выгоды»):
- Снижение точности выбора навыка (intended skill vs selected skill) ≥ 5 п.п. → описание навыка испорчено, модель выбирает неверный навык. Типичный сценарий: до оптимизации пользователь спрашивал X и всегда получал skill_a, а после оптимизации запросы иногда направляются на skill_b, но ошибок не возникает (модель использует не тот навык, но кое-как выдаёт ответ). Результирующие метрики выглядят нормально, но followup_rate растёт. Метод измерения: добавить в телеметрию
skill_invocation_pattern— кластеризовать по первым N ключевым словам user prompt, посмотреть, какой навык в основном срабатывает в каждом кластере; сравнить до и после оптимизации смещение первого места - Доля неудачных встроенных followup в навыке ≥ 5% → доработка навыка внесла новые сбои, которых раньше не было (например, встроенный
read_fileпри обработке больших файлов приводит к переполнению памяти). Измерение: сравнениеSkillLaunchEvent.successдо и после оптимизации - Рост доли отмен пользователем (Ctrl+C) для конкретного навыка ≥ 2 п.п. → вывод навыка стал медленнее или длиннее, пользователь теряет терпение. Измерение: доля
ToolCallEvent.status === 'cancelled'
6. Стыковка с D1/D3
6.1 Отношение к D1
После того как Layer 2 доработал top‑skill, оставшиеся навыки с большим количеством followup — это как раз реальная область применения D1 skipLlmRound — вывод этих навыков уже полный (Round 2 не нужен), и это действительно конечные запросы (Round 3 с подведением итогов тоже лишний).
Порядок выполнения:
- Запуск телеметрии Layer 1 → 1 неделя данных
- Доработка top-2–3 навыков в Layer 2 → A/B-тест 2 недели
- Промпт-инструкции конкуренции Layer 3 → замеры 1 неделя
- Только после этого оценить D1: сколько среди оставшихся часто используемых навыков имеют форму “полный вывод + конечный запрос” → стоит ли 2–3 дня на доработку фреймворка
6.2 Отношение к D3
D3 (StreamingState.Summarizing) — это оптимизация уровня восприятия, полностью ортогональна данному решению. Layer 1–3 сокращают реальное количество раундов, D3 — воспринимаемое ожидание. Если Layer 2 уже сократила RT до приемлемого для пользователя уровня, ценность D3 снижается; в противном случае D3 можно наложить.
7. Ограничения и известные риски
- Охват ограничен рамками доработок — доработка 10 навыков покрывает только сценарии этих 10. Но выгода измерима и нарастает
- Встроенные followup в навыке могут сделать один навык тяжелее — разрастание описания, медленная загрузка, снижение возможности повторного использования. Защита — пункт 5 чек-листа Layer 2
- Модель Layer 3 может не следовать инструкциям параллелизма — qwen-coder обучался преимущественно на последовательных данных; A/B-данные могут показать, что изменение промпта неэффективно — это известный сценарий отказа
- Границы приватности телеметрии —
SkillFollowupRecordне должен записывать аргументы инструментов (по умолчанию данные берутся изToolCallEvent.function_args, но нужно проверить, не раскрывает лиskill_nameнамерения пользователя) - Не применимо к sub‑agent / cron / notification — эти пути не проходят через систему навыков, данное решение их не покрывает
- Базовые данные скудны — используется единичная выборка из
rt‑optimization‑design.md§1.2; перед внедрением Layer 2 необходимо дополнить базовые замеры ≥3 классов сценариев - Расширение полей
logSkillLaunchсломает существующих потребителей телеметрии — нужно синхронно изменить 4 точки вызова и нижестоящие логгеры 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 в rt‑optimization‑design.md §3.2 fast‑маршрута |
Этот перечень приводится для того, чтобы данное решение не воспринималось как игнорирование существующих механизмов.
8. График внедрения
Предпосылка: данный график начинается с P‑1, пропускать нельзя. P‑1 — это предварительное ревью спецификации приёмки из §0, объём работ 0,5 дня, но обязательно — без его прохождения нельзя переходить к P0. Это ограничение введено, чтобы избежать антипаттерна “сначала код, потом спецификация”: спецификация после факта означает, что оценка “получилось” откладывается до появления результатов, что чревато смещением “подгонки спецификации под красивые цифры” (см. печальный опыт маршрута D2 в
rt‑optimization‑design.md§7).
| Этап | Содержание | Затраты | Результат | Действие по фиксации спецификации |
|---|---|---|---|---|
| P-1 | Предварительное ревью спецификации | 0,5 дня | Фиксация §0.1 / §0.3 | Зафиксировать spec инженерного уровня §0.1 + стоп-линии §0.3 |
| P0 | Исправление цепочки qwen‑logger (предварительное §4.1.1b) | 0,5 дня | Подтверждение видимости события skill_launch | Проверить пункт 1 §0.1 |
| P1 | Телеметрия Layer 1: добавление поля prompt_id + офлайн‑SQL | 1–2 дня | Отчёт ранжирования навыков | Проверить пункты 2/3/4 §0.1 |
| P1.5 | Сбор данных в течение 1 недели + базовые замеры (≥3 класса сценариев × ≥10 раз) | 1 неделя | Решение, какие 2–3 навыка дорабатывать | Зафиксировать порог §0.2 + проверить пункт 5 §0.1 |
| P2 | Доработка top‑1 навыка в Layer 2 (PR + A/B) | 0,5–1 день доработки + 2 недели наблюдения | Проверка снижения followup_rate ↓, RT P50 ↓ | Объявить спецификацию per‑skill §0.4 внутри PR |
| P3 | Промпт‑инструкции параллелизма Layer 3 + телеметрия batch_size (включая передачу состояния §4.3.2) | 1–1,5 дня изменений + 1 неделя замеров | Распределение batch_size | Проверить пункт 3 §0.2 |
| P4 | Продолжение доработки top‑2 / top‑3 навыков в Layer 2 (параллельно P3) | 0,5–1 день × N | Суммарное снижение RT P50 ↓ | Каждый PR объявляет §0.4 |
| P5 | Оценка, есть ли ещё ценность в D1 | Заседание по принятию решения | Обновление дорожной карты | — |
| Ключевые точки принятия решений (сравните с §0.3 стоп-линией): |
- Конец P-1: если хотя бы один пункт из §0.1 / §0.3 не достигнут → не переходить в P0
- Конец P1.5: срабатывание §0.3 результатного показателя #1 (взвешенный followup_rate в top‑5 < 30%) → прекратить направление; иначе зафиксировать пороги §0.2
- Конец P2: срабатывание §0.3 результатного показателя #2 (top‑1 после доработки RT P50 ↓ < 1s) или любого процессного показателя → остановиться на ретроспективу
- Конец P3: срабатывание §0.3 результатного показателя #3 (batch_size P50 всё ещё = 1) → отказаться от Layer 3
- P5: ROI D1 определяется исходя из оставшейся формы skill
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) | каталог |