Дизайн сводок использования инструментов
Метки от быстрой модели для параллельных пакетов инструментов — мотивация, сравнительный анализ с Claude Code, архитектура и обоснование использования
<Static>в режиме append-only, которое определило текущий рендеринг в полном режиме.Документация для пользователей: Tool-Use Summaries.
1. Краткое описание
После завершения каждого пакета инструментов Qwen Code выполняет короткий запрос к быстрой модели, который возвращает метку в стиле заголовка git-коммита, суммирующую содержимое пакета. В полном режиме метка отображается как встроенная строка ● <label> с пониженной яркостью, а в компактном режиме заменяет стандартный заголовок Tool × N. Генерация работает по принципу fire-and-forget параллельно с API-стримом следующего хода, поэтому её задержка ~1 с скрывается за стримингом основной модели.
| Параметр | Claude Code | Qwen Code |
|---|---|---|
| Точка запуска | query.ts — после завершения пакета инструментов | useGeminiStream.ts → handleCompletedTools — та же точка жизненного цикла |
| Модель генерации | 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 turn2.2 Ключевые файлы исходного кода
| Компонент | Файл | Ключевая логика |
|---|---|---|
| Генератор | services/toolUseSummary/toolUseSummaryGenerator.ts:45-97 | generateToolUseSummary({ tools, signal, isNonInteractiveSession, lastAssistantText }) |
| Точка запуска | query.ts:1411-1482 | Проверка флага emitToolUseSummaries + отсутствие субагента; форк Haiku; передача промиса |
| Ожидание и эмиссия | query.ts:1055-1060 | Ожидание pendingToolUseSummary на границе следующего хода, передача сообщения |
| Фабрика сообщений | utils/messages.ts:5105-5116 | createToolUseSummaryMessage(summary, precedingToolUseIds) |
| Флаг функции | query/config.ts:23,36-38 | emitToolUseSummaries: isEnvTruthy(CLAUDE_CODE_EMIT_TOOL_USE_SUMMARIES) |
2.3 Принятые архитектурные решения
- Генерация всегда выполняется при включённом флаге, независимо от режима compact/detail. Сводка — это артефакт уровня стрима; UI сам решает, рендерить её или нет.
- Эмиссия как полноправного типа сообщения.
tool_use_summaryнаходится в SDK-стриме наравне сuser,assistant,tool_resultи содержит полеprecedingToolUseIdsдля сопоставления потребителями с пакетом. - Субагенты исключены.
!toolUseContext.agentId— вывод субагентов агрегируется на верхнем уровне; отдельные пакеты субагентов создавали бы шумные метки, которые никогда не отображаются в основном UI. - По умолчанию выключено. Флаг, управляемый только через env, держит затраты на нуле, пока потребитель SDK явно не включит его. Терминал CC сам не рендерит это сообщение.
- Обрезка входных данных до 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 header3.2 Ключевые файлы исходного кода
| Компонент | Файл | Ключевая логика |
|---|---|---|
| Сервис | packages/core/src/services/toolUseSummary.ts | generateToolUseSummary, 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. Существует два варианта:
- Обновить пропсы
tool_group+ вызватьrefreshStatic(). Работает, но вызывает полную перерисовку транскрипта для каждого пакета — одна из самых затратных UI-операций в приложении. Видимая вспышка. Неприемлемо для косметической метки. - Отрендерить сводку как отдельный новый элемент истории, добавленный после
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 Семантика флагов
Три уровня, разрешаемые в порядке приоритета:
QWEN_CODE_EMIT_TOOL_USE_SUMMARIES=0|1|true|false— переопределение через env, наивысший приоритет.experimental.emitToolUseSummariesвsettings.json— по умолчаниюtrue.- Неявный пропуск — если
config.getFastModel()возвращаетundefined, генерация пропускается независимо от флага. Без ошибок, без видимых для пользователя изменений.
3.5 Очистка вывода
cleanSummary выполняется для каждого ответа модели перед добавлением в историю:
- Берётся только первая строка (отбрасываются преамбулы рассуждений модели).
- Удаляются префиксы маркированных списков (
-,*,•) — модели иногда возвращают метку как элемент списка. - Удаляются окружающие кавычки/обратные кавычки с помощью ограниченного
{1,10}regex (безопасно для CodeQL; ни одна реальная метка не имеет более пары оборачивающих кавычек). - Удаляются префиксы-метки (
Label:,Summary:,Result:,Output:), которые некоторые модели добавляют в начало. - Отклоняются формы сообщений об ошибках (
API error: ...,Error: ...,I cannot ...,I can't ...,Unable to ...) — возвращается пустая строка, чтобы элемент истории не добавлялся. - Жёсткое ограничение длины в 100 символов (мобильный UI обрезает примерно на 30; запас покрывает фразы на CJK).
3.6 Телеметрия
Вызов генерации сводки устанавливает promptId: 'tool_use_summary_generation', чтобы использование токенов учитывалось отдельно в /stats. Это позволяет пользователям видеть точную дополнительную стоимость функции, не смешивая её с подсказками промптов или использованием основной сессии.
4. Отличия от Claude Code (и причины)
| Отличие | Причина |
|---|---|
| Слой настроек в дополнение к флагу env | Qwen Code рендерит метку в CLI; пользователям нужен постоянный переключатель, а не экспорт env для каждой оболочки. |
| По умолчанию включено вместо выключенного | Метка сразу видна пользователю в обоих режимах отображения; пользователи, настраивающие fastModel, уже соглашаются на функции быстрой модели. |
Выделенная постобработка cleanSummary | Qwen 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. Планы на будущее
- Подключить
ToolUseSummaryMessageк мосту SDK, чтобы существующая фабрика использовалась на стороне потребителей. - Маршрутизировать генерацию через
forkedAgent.tsсenablePromptCaching, чтобы повторяющиеся префиксы имён инструментов попадали в кэш провайдера. - Опционально: сохранять записи
tool_use_summaryвChatRecordingServiceи воспроизводить при возобновлении сессии. - Опционально: ярлыки меток по имени инструмента (например, всегда
Read <filename>для одиночного вызоваread_file) как быстрый путь до вызова LLM.