Skip to Content
ДизайнRt OptimizationAgent Loop Reduction: Дизайн через Skill-слой

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

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


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

  1. Не менять agent-фреймворк — не трогать useGeminiStream / coreToolScheduler / geminiChat в ядре
  2. Приоритет на основе данных — сначала построить телеметрию, пусть данные говорят, какой skill менять, а не гадать
  3. Per-skill измеримость и постепенный rollout — каждая модификация skill независима в A/B, при неудаче локальный откат
  4. Приоритет на сложный процент — выгода = выгода от одного сокращения раунда × частота срабатывания, сначала high-frequency 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.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_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 → another skillцепочка вызововНе объединять (сохранять композиционность)

Контрольный список модификации (шаблон PR per-skill):

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

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

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

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

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

6.2 Связь с D3

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


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

  1. Охват ограничен областью доработки — если исправлены 10 skill, охватываются только их сценарии. Но выгода измерима, предсказуема и накопительна.
  2. Встроенные followup skill могут утяжелить отдельный skill — разрастание описания, медленная загрузка, снижение повторного использования. Защита: пункт 5 чек-листа Layer 2.
  3. Модель Layer 3 может не следовать инструкциям о параллелизме — qwen-coder обучается на последовательных данных; A/B данные могут показать, что изменение prompt бесполезно — это известный сценарий отказа.
  4. Границы приватности телеметрииSkillFollowupRecord не должен записывать параметры инструмента (по умолчанию берутся из ToolCallEvent.function_args, но необходимо проверить, не раскрывает ли skill_name намерение пользователя).
  5. Не применимо к sub-agent / cron / notification — эти маршруты не проходят через систему skill, план их не охватывает.
  6. Базовые данные скудны — используется единичный замер из rt-optimization-design.md §1.2. До внедрения Layer 2 необходимо собрать базовые данные по ≥3 классам сценариев.
  7. Расширение полей logSkillLaunch сломает существующих потребителей телеметрии — нужно синхронно изменять 4 точки вызова и downstream логгеры.
  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 в 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Предварительный ревью spec0,5 д§0.1 / §0.3 закрепленыЗакрепить §0.1 (инженерный spec) + §0.3 (стоп-линии)
P0Исправление цепи qwen-logger (предварительное §4.1.1b)0,5 дподтверждение видимости события skill_launchПроверка пункта 1 §0.1
P1Layer 1 телеметрия: добавить поле prompt_id + офлайн SQL1–2 дотчёт с ранжированием skillПроверка пунктов 2/3/4 §0.1
P1.5Сбор данных за 1 неделю + замер базового уровня (≥3 класса × ≥10 раз)1 недпринятие решения, какие 2-3 skill дорабатыватьЗакрепить пороги §0.2 + проверить пункт 5 §0.1
P2Layer 2: доработка top-1 skill (PR + A/B)0,5–1 д доработка + 2 нед наблюденияпроверка снижения followup_rate ↓ и RT P50 ↓Объявить per-skill spec §0.4 внутри PR
P3Layer 3: конкурентный инструкция в prompt + телеметрия batch_size (включая передачу состояния §4.3.2)1–1,5 д изменений + 1 нед тестированиераспределение batch_sizeПроверка пункта 3 §0.2
P4Layer 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.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