Skip to Content
ДизайнRt OptimizationAgent Loop: стратегия сокращения циклов — от проектирования Skill

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.4Per-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до завершения P2n — не определено

Ключевое ограничение: предварительные пороги не являются обязательством. Если базовая линия 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 3 batch_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. Базовые показатели §1.2 как раз показывают проблему в skill — переход от Раунда 1 к Раунду 2 происходит из-за неполного возврата результата skill; фреймворк работает правильно, а skill — неправильно
  2. Сокращение раундов на уровне фреймворка в конечном счёте требует per-tool opt-in — D1 с skipLlmRound требует явной маркировки каждого инструмента, что оборачивается дополнительными затратами на фиксацию инвариантов и шлюзы принятия решений
  3. ROI локально измеримо и легко серое масштабирование — изменение одного skill сокращает один раунд × частоту его вызова, не полагаясь на данные по cache hit rate или кроссистемные изменения

Перед реализацией необходимо сначала пройти пре-ревью спецификации §0 (фаза P-1, 0.5 дня) — §0.1 (инженерная спецификация) и §0.3 (стоп-линии) должны быть зафиксированы до начала работы; направление §0.2 (статистические пороги) также должно быть подтверждено заранее (конкретные значения будут зафиксированы после P1.5). Пропуск §0 и переход к реализации P0 = по умолчанию принимать анти-шаблон «сначала сделаем, потом посмотрим метрики», и данный документ не одобряет такой подход.


2. Принципы проектирования

  1. Не изменять фреймворк агента — не трогать основные пути useGeminiStream / coreToolScheduler / geminiChat
  2. Выбор приоритетов на основе данных — сначала построить телеметрию, пусть данные укажут, какой skill менять, а не полагаться на догадки
  3. Per-skill измеримо и серое масштабирование — для каждого изменения skill отдельный A/B-тест, при неудаче локальный откат
  4. Приоритет сложных процентов — выгода = сокращение времени на один раунд × частота вызова, в первую очередь высокочастотные skill
  5. Не привязываться к 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_invocations
  • terminal_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_fileskill даёт путь, модель читаетskill читает внутри, возвращает содержимое
skill → grep/globskill даёт директорию, модель ищетskill ищет внутри, возвращает совпадения
skill → shell (read-only)skill даёт команду, модель выполняетskill выполняет команду внутри, возвращает вывод
skill → shell (write)skill даёт план, модель выполняет записьСохранить (операции записи требуют подтверждения, не объединять)
skill → другой skillцепной вызовНе объединять (сохранять композиционность)

Чеклист модификации (шаблон PR для per-skill):

  1. Предварительно объявить контракт вывода в описании skill: например, «Returns: full file content / matched lines / command output», чтобы модель знала, что дополнительный запрос не нужен
  2. Выполнить все read-only followup внутри skill: операции чтения/поиска, по которым телеметрия показывает >50% вероятность последующего вызова, встроить в skill
  3. Не встраивать операции записи: операции записи требуют подтверждения пользователя, должны оставаться отдельным раундом
  4. Не встраивать последующие действия, требующие глубокого вывода: если followup — это «проанализировать на основе этого», это задача модели, а не skill
  5. Приложить 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 выхода:

  1. uiTelemetryService.addEvent(...) — отображение в UI.
  2. config.getChatRecordingService()?.recordUiTelemetryEvent(...) — история чата.
  3. QwenLogger.getInstance(config)?.logToolCallEvent(...) — телеметрия бэкенда qwen-logger.
  4. 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 (офлайн-агрегация)

不需要新的事件类型 — ToolCallEventSkillLaunchEvent 都已带有 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 должно содержать:

  1. Данные: текущее количество вызовов, followup_rate, основные followup-инструменты
  2. Область модификации: какие followup были встроены (и что явно не встраивается)
  3. Обновление контракта вывода: какие предварительные объявления добавлены в описание навыка
  4. План 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 с подведением итогов тоже лишний).

Порядок выполнения:

  1. Запуск телеметрии Layer 1 → 1 неделя данных
  2. Доработка top-2–3 навыков в Layer 2 → A/B-тест 2 недели
  3. Промпт-инструкции конкуренции Layer 3 → замеры 1 неделя
  4. Только после этого оценить D1: сколько среди оставшихся часто используемых навыков имеют форму “полный вывод + конечный запрос” → стоит ли 2–3 дня на доработку фреймворка

6.2 Отношение к D3

D3 (StreamingState.Summarizing) — это оптимизация уровня восприятия, полностью ортогональна данному решению. Layer 1–3 сокращают реальное количество раундов, D3 — воспринимаемое ожидание. Если Layer 2 уже сократила RT до приемлемого для пользователя уровня, ценность D3 снижается; в противном случае D3 можно наложить.


7. Ограничения и известные риски

  1. Охват ограничен рамками доработок — доработка 10 навыков покрывает только сценарии этих 10. Но выгода измерима и нарастает
  2. Встроенные followup в навыке могут сделать один навык тяжелее — разрастание описания, медленная загрузка, снижение возможности повторного использования. Защита — пункт 5 чек-листа Layer 2
  3. Модель Layer 3 может не следовать инструкциям параллелизма — qwen-coder обучался преимущественно на последовательных данных; A/B-данные могут показать, что изменение промпта неэффективно — это известный сценарий отказа
  4. Границы приватности телеметрииSkillFollowupRecord не должен записывать аргументы инструментов (по умолчанию данные берутся из ToolCallEvent.function_args, но нужно проверить, не раскрывает ли skill_name намерения пользователя)
  5. Не применимо к sub‑agent / cron / notification — эти пути не проходят через систему навыков, данное решение их не покрывает
  6. Базовые данные скудны — используется единичная выборка из rt‑optimization‑design.md §1.2; перед внедрением Layer 2 необходимо дополнить базовые замеры ≥3 классов сценариев
  7. Расширение полей logSkillLaunch сломает существующих потребителей телеметрии — нужно синхронно изменить 4 точки вызова и нижестоящие логгеры
  8. qwen‑logger.ts:908 logSkillLaunchEvent в настоящее время — мёртвый код — в репозитории нет ни одного вызывающего; §4.1.1b уже перечисляет обязательное предварительное исправление

7.1 Границы с существующими механизмами фреймворка (не входят в объём данного решения)

В репозитории уже есть несколько механизмов фреймворка, косвенно связанных с сокращением раундов. Данное решение не изобретает их заново и не заменяет:

Существующий механизмРасположениеОтношение к данному решению
partitionToolCalls + runConcurrently (параллельное выполнение)coreToolScheduler.ts:775, 2473Layer 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 + офлайн‑SQL1–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.tsToolCallEvent (содержит prompt_id / duration_ms)L170
packages/core/src/telemetry/types.tsSkillLaunchEvent (требуется добавить prompt_id)L896
packages/core/src/telemetry/loggers.tslogToolCallL220
packages/core/src/telemetry/loggers.tslogSkillLaunch (через OTLP; не хватает пересылки через qwen-logger)L958
packages/core/src/telemetry/loggers.tslogToolCall (двойной путь: OTLP + qwen-logger, шаблон для исправления)L220, L230
packages/core/src/telemetry/qwen-logger/qwen-logger.tslogSkillLaunchEvent (сейчас мёртвый код, цель предварительного исправления §4.1.1b)L908
packages/core/src/core/coreToolScheduler.tspartitionToolCallsL775
packages/core/src/core/coreToolScheduler.tsrunConcurrently / планирование batchL2456, L2473
packages/core/src/core/coreToolScheduler.tsточка вызова logToolCall (конечная точка передачи состояния batch_size)L3163
packages/core/src/services/fileReadCache.tsFileReadCache (уже есть, влияет на количество повторных чтений)L135
packages/core/src/tools/skill.tsSkillTool + 4 точки вызова logSkillLaunchL386, L399, L426, L482
packages/core/src/skills/skill-manager.tsSkillManager (регистрация/загрузка skill)весь файл
packages/core/src/skills/skill-load.tsзагрузка описаний skill (точка входа для изменения контрактов на выход)весь файл
packages/core/src/tools/tools.tsKind + CONCURRENCY_SAFE_KINDSL793, L818
packages/core/src/core/coreToolScheduler.tspartitionToolCalls + runConcurrently (уже существующая инфраструктура параллелизма)см. rt-optimization-design.md §5.7
packages/core/src/core/prompts.tsсекция # Final Reminder (место добавления инструкций по параллелизму Layer 3)L396
.qwen/skills/каталоги определений skill (объект доработки Layer 2)каталог
Last updated on