Skip to Content
ДизайнTool Use SummaryДизайн сводок использования инструментов

Дизайн сводок использования инструментов

Метки от быстрой модели для параллельных пакетов инструментов — мотивация, сравнительный анализ с Claude Code, архитектура и обоснование использования <Static> в режиме append-only, которое определило текущий рендеринг в полном режиме.

Документация для пользователей: Tool-Use Summaries.

1. Краткое описание

После завершения каждого пакета инструментов Qwen Code выполняет короткий запрос к быстрой модели, который возвращает метку в стиле заголовка git-коммита, суммирующую содержимое пакета. В полном режиме метка отображается как встроенная строка ● <label> с пониженной яркостью, а в компактном режиме заменяет стандартный заголовок Tool × N. Генерация работает по принципу fire-and-forget параллельно с API-стримом следующего хода, поэтому её задержка ~1 с скрывается за стримингом основной модели.

ПараметрClaude CodeQwen Code
Точка запускаquery.ts — после завершения пакета инструментовuseGeminiStream.tshandleCompletedTools — та же точка жизненного цикла
Модель генерацииHaiku через queryHaikuНастроенная fastModel через GeminiClient.generateContent
Поведение субагентов!toolUseContext.agentId — только основная сессияНеявно — субагенты работают через agents/runtime/, а не useGeminiStream
ПланированиеFire-and-forget, ожидание прямо перед эмиссией стрима следующего ходаFire-and-forget, добавление в историю после разрешения
Формат выводаToolUseSummaryMessage передаётся в SDK-стримHistoryItemToolUseSummary добавляется в UI-историю + экспортируется фабрика для будущего использования в SDK
Флагenv CLAUDE_CODE_EMIT_TOOL_USE_SUMMARIES, по умолчанию выключенонастройка experimental.emitToolUseSummaries (по умолчанию включено) + переопределение через env
Основной потребительМобильные / SDK-клиентыКомпактный и полный режимы CLI, будущий SDK
ПромптЗаголовок git-коммита, прошедшее время, наиболее характерное существительное (прямой перенос)Идентичный системный промпт
Обрезка входных данных300 символов на поле инструмента через truncateJsonИдентично
Префикс намеренияПервые 200 символов последнего сообщения ассистентаИдентично
Кэширование промптаenablePromptCaching: true в вызове HaikuПока не подключено (доступен маршрут через forked-agent; помечено как будущая оптимизация)
Постобработка меткиСырой текст моделиcleanSummary (удаляет markdown, кавычки, префиксы ошибок; ограничение 100 символов, с защитой от ReDoS)
Сохранение сессииТолько стрим; каждая сессия перегенерируетТолько UI-история; ChatRecordingService не сохраняет записи tool_use_summary

2. Анализ реализации в Claude Code

2.1 Поток выполнения

Claude Code выполняет цикл инструментов в query.ts. После выполнения пакета инструментов и нормализации результатов функция-генератор форкает вызов Haiku, сохраняет ожидающий промис в nextPendingToolUseSummary и продолжает выполнение API-запроса следующего хода. Задержка Haiku (~1 с) перекрывается со стримингом основной модели (5–30 с), поэтому пользователь не видит дополнительной задержки. Непосредственно перед эмиссией контента следующего хода генератор ожидает промис сводки и передаёт сообщение tool_use_summary в стрим.

tool_batch_complete → fork queryHaiku (fire-and-forget) next_turn_stream_starts ← summary Promise resolves during streaming → await pendingToolUseSummary → yield ToolUseSummaryMessage continue with next turn

2.2 Ключевые файлы исходного кода

КомпонентФайлКлючевая логика
Генераторservices/toolUseSummary/toolUseSummaryGenerator.ts:45-97generateToolUseSummary({ tools, signal, isNonInteractiveSession, lastAssistantText })
Точка запускаquery.ts:1411-1482Проверка флага emitToolUseSummaries + отсутствие субагента; форк Haiku; передача промиса
Ожидание и эмиссияquery.ts:1055-1060Ожидание pendingToolUseSummary на границе следующего хода, передача сообщения
Фабрика сообщенийutils/messages.ts:5105-5116createToolUseSummaryMessage(summary, precedingToolUseIds)
Флаг функцииquery/config.ts:23,36-38emitToolUseSummaries: isEnvTruthy(CLAUDE_CODE_EMIT_TOOL_USE_SUMMARIES)

2.3 Принятые архитектурные решения

  1. Генерация всегда выполняется при включённом флаге, независимо от режима compact/detail. Сводка — это артефакт уровня стрима; UI сам решает, рендерить её или нет.
  2. Эмиссия как полноправного типа сообщения. tool_use_summary находится в SDK-стриме наравне с user, assistant, tool_result и содержит поле precedingToolUseIds для сопоставления потребителями с пакетом.
  3. Субагенты исключены. !toolUseContext.agentId — вывод субагентов агрегируется на верхнем уровне; отдельные пакеты субагентов создавали бы шумные метки, которые никогда не отображаются в основном UI.
  4. По умолчанию выключено. Флаг, управляемый только через env, держит затраты на нуле, пока потребитель SDK явно не включит его. Терминал CC сам не рендерит это сообщение.
  5. Обрезка входных данных до 300 символов на поле. Покрывает основной риск затрат — раздувание промпта одним большим результатом инструмента — при сохранении достаточного сигнала для метки.

3. Реализация в Qwen Code

3.1 Поток выполнения

Qwen Code подключается к той же точке жизненного цикла (useGeminiStream.handleCompletedTools), но рендерит результат для обоих состояний ui.compactMode, чтобы функция была полезна пользователям CLI без дополнительной интеграции SDK.

tool_batch_complete (handleCompletedTools) config.getEmitToolUseSummaries()? fork generateToolUseSummary (fire-and-forget) submitQuery() for next turn (streaming starts) ← summary Promise resolves during streaming → addItem({type:'tool_use_summary', summary, precedingToolUseIds}) HistoryItemDisplay renders: compactMode=false → ● <label> standalone line compactMode=true → hidden; MainContent lookup injects into CompactToolGroupDisplay header

3.2 Ключевые файлы исходного кода

КомпонентФайлКлючевая логика
Сервисpackages/core/src/services/toolUseSummary.tsgenerateToolUseSummary, truncateJson, cleanSummary, фабрика сообщений
Флаг конфигурацииpackages/core/src/config/config.ts:getEmitToolUseSummariesПереопределение через env → настройки → значение по умолчанию (true)
Точка запускаpackages/cli/src/ui/hooks/useGeminiStream.ts:handleCompletedToolsВыполняет вызов быстрой модели, addItem после разрешения
Рендеринг в полном режимеpackages/cli/src/ui/components/HistoryItemDisplay.tsxРендерит строку ● <label> при !compactMode
Поиск в компактном режимеpackages/cli/src/ui/components/MainContent.tsxмапа summaryByCallId → проп compactLabel для каждого tool_group
Заголовок компактного режимаpackages/cli/src/ui/components/messages/CompactToolGroupDisplay.tsxЗаменяет стандартный Tool × N на <Summary> · N tools при наличии метки
Обработка слиянияpackages/cli/src/ui/utils/mergeCompactToolGroups.tsРассматривает tool_use_summary как скрытый в компактном режиме для определения смежности
UI-типpackages/cli/src/ui/types.ts:HistoryItemToolUseSummary{ type: 'tool_use_summary', summary, precedingToolUseIds }

3.3 Ограничение append-only в <Static>

Центральное архитектурное решение в этом PR — почему метка в полном режиме является отдельным элементом истории, а не декорацией самого tool_group.

Qwen Code рендерит транскрипт через <Static> из Ink. Static работает по принципу append-only: как только элемент зафиксирован в буфере терминала, Ink не перерисовывает эту область, пока не будет вызван refreshStatic() для очистки и полного перерендеринга транскрипта. Это модель производительности, на которую опирается CLI — статические элементы не перерендериваются при каждом нажатии клавиши.

Теперь рассмотрим тайминг вызова быстрой модели:

T0 tool batch completes, tool_group is pushed to history T0+ε tool_group renders through <Static> and is committed to the buffer T0+1s fast-model call resolves with a label

В момент T0+1 с мы не можем ретроспективно добавить метку к уже зафиксированному tool_group. Существует два варианта:

  1. Обновить пропсы tool_group + вызвать refreshStatic(). Работает, но вызывает полную перерисовку транскрипта для каждого пакета — одна из самых затратных UI-операций в приложении. Видимая вспышка. Неприемлемо для косметической метки.
  2. Отрендерить сводку как отдельный новый элемент истории, добавленный после tool_group. Static обрабатывает это нативно — новые элементы добавляются чисто, без перерисовки.

В этом PR для полного режима выбран вариант 2. Запись tool_use_summary — это полноценный элемент истории, который HistoryItemDisplay рендерит как одну строку ● <label> с пониженной яркостью. refreshStatic не требуется.

Компактный режим отличается из-за mergeCompactToolGroups. При слиянии последовательных tool*groups MainContent уже вызывает refreshStatic() — это существующий путь выполнения, который перерендеривает объединённую группу с меткой, полученной из истории. Таким образом, в компактном режиме метка действительно используется как замена заголовка. Чтобы избежать двойного рендеринга одной метки (один раз как заголовок компактного режима, второй раз как завершающая строка ● <label>), HistoryItemDisplay скрывает отдельную строку, когда compactMode равен true.

Full mode Compact mode (with merge) ─────────── ───────────────────────── [tool_group] [merged tool_group — header replaced via lookup] ● <label> (● <label> line is hidden)

3.4 Семантика флагов

Три уровня, разрешаемые в порядке приоритета:

  1. QWEN_CODE_EMIT_TOOL_USE_SUMMARIES=0|1|true|false — переопределение через env, наивысший приоритет.
  2. experimental.emitToolUseSummaries в settings.json — по умолчанию true.
  3. Неявный пропуск — если config.getFastModel() возвращает undefined, генерация пропускается независимо от флага. Без ошибок, без видимых для пользователя изменений.

3.5 Очистка вывода

cleanSummary выполняется для каждого ответа модели перед добавлением в историю:

  1. Берётся только первая строка (отбрасываются преамбулы рассуждений модели).
  2. Удаляются префиксы маркированных списков (-, *, ) — модели иногда возвращают метку как элемент списка.
  3. Удаляются окружающие кавычки/обратные кавычки с помощью ограниченного {1,10} regex (безопасно для CodeQL; ни одна реальная метка не имеет более пары оборачивающих кавычек).
  4. Удаляются префиксы-метки (Label:, Summary:, Result:, Output:), которые некоторые модели добавляют в начало.
  5. Отклоняются формы сообщений об ошибках (API error: ..., Error: ..., I cannot ..., I can't ..., Unable to ...) — возвращается пустая строка, чтобы элемент истории не добавлялся.
  6. Жёсткое ограничение длины в 100 символов (мобильный UI обрезает примерно на 30; запас покрывает фразы на CJK).

3.6 Телеметрия

Вызов генерации сводки устанавливает promptId: 'tool_use_summary_generation', чтобы использование токенов учитывалось отдельно в /stats. Это позволяет пользователям видеть точную дополнительную стоимость функции, не смешивая её с подсказками промптов или использованием основной сессии.

4. Отличия от Claude Code (и причины)

ОтличиеПричина
Слой настроек в дополнение к флагу envQwen Code рендерит метку в CLI; пользователям нужен постоянный переключатель, а не экспорт env для каждой оболочки.
По умолчанию включено вместо выключенногоМетка сразу видна пользователю в обоих режимах отображения; пользователи, настраивающие fastModel, уже соглашаются на функции быстрой модели.
Выделенная постобработка cleanSummaryQwen Code поддерживает более разнородных провайдеров, чем CC; некоторые модели добавляют Label: или оборачивают в кавычки. Нормализация на границе сохраняет единообразие UI.
Сохраняет HistoryItemToolUseSummary вместо эмиссии сообщения в стримРеализация в первую очередь для CLI; маршрут SDK-стрима — это будущий PR. Фабрика ToolUseSummaryMessage уже экспортирована для этой работы.
Кэширование промпта пока не подключеноБыстрая модель часто совпадает с основной у пользователей, не настроивших отдельную. Добавление общего кэша требует маршрутизации через forkedAgent.ts; отслеживается как следующая задача.
Два пути рендеринга (встроенный в полном режиме + заголовок в компактном)По умолчанию в Qwen Code стоит ui.compactMode: false; без встроенного рендеринга в полном режиме функция была бы невидима для большинства пользователей.

5. Известные ограничения

  • Отсутствие сохранения сессии. tool_use_summary не записывается в JSONL-файл записи чата. При возобновлении сессии метки теряются; группы инструментов рендерятся со стандартным заголовком в качестве fallback. Низкий приоритет: метки естественным образом перегенерируются по мере продолжения сессии пользователем.
  • Эмиссия в SDK-стрим пока не реализована. Фабрика сообщений экспортирована, но CLI ещё не передаёт tool_use_summary в мост SDK. Будет в следующем PR.
  • Отсутствие кэширования промпта. Каждый пакет несёт новую стоимость входных токенов. В абсолютных значениях это ничтожно (~300 токенов), но измеримо при запуске десятков пакетов за ход.
  • Сводка для объединённых компактных групп берёт метку первого пакета. Если пользователь запускает десять непохожих пакетов подряд (плотный цикл, не типично), объединённый компактный заголовок покажет намерение только ведущего пакета. Принятый trade-off: вывод меток каждого пакета в объединённом представлении визуально зашумляет сильнее, чем выбор первой.
  • Требуется быстрая модель. Без настроенной fastModel генерация пропускается. Фоллбэк на основную модель намеренно запрещён для сохранения контролируемого профиля затрат.

6. Планы на будущее

  1. Подключить ToolUseSummaryMessage к мосту SDK, чтобы существующая фабрика использовалась на стороне потребителей.
  2. Маршрутизировать генерацию через forkedAgent.ts с enablePromptCaching, чтобы повторяющиеся префиксы имён инструментов попадали в кэш провайдера.
  3. Опционально: сохранять записи tool_use_summary в ChatRecordingService и воспроизводить при возобновлении сессии.
  4. Опционально: ярлыки меток по имени инструмента (например, всегда Read <filename> для одиночного вызова read_file) как быстрый путь до вызова LLM.
Last updated on