Skip to content

TOOL_PREPARING: ранний сигнал о вызове инструмента недостижим через публичные хуки Spring AI #51

@trialiya

Description

@trialiya

Проблема

Событие TOOL_PREPARING приходит в SSE-поток вплотную к TOOL_CALL — зазора нет, индикатор «готовлю данные…» на фронте никогда не успевает появиться.

Причина

OpenAiChatModel.internalStream (Spring AI 2.0 + openai-java 4.x) буферизует все дельты tool-call через bufferUntil/ChunkMerger до того, как отдать что-либо наружу:

chunks
    .doOnNext(chunk -> { if (ChunkMerger.hasToolCall(chunk)) isInsideTool.set(true); })
    .bufferUntil(chunk -> { /* копит, пока не toolCallsDone */ })
    .map(ChunkMerger::mergeChunks)
    .map(ChunkMerger::chunkToChatCompletion);

Всё, что выше (advisor-chain, ChatRunService, observation), видит только агрегированный чанк с уже полными аргументами. К этому моменту ToolCallingAdvisor сразу запускает инструмент → TOOL_PREPARING и TOOL_CALL уходят почти одновременно.

Проверенные точки расширения

Точка Почему не подходит
StreamAdvisor (ToolPreparingAdvisor) Видит чанки после bufferUntil — первый же hasToolCalls()=true уже содержит полные аргументы
ChatModelObservationConvention / ObservationHandler setResponse зовётся один раз по завершении; requestTools = все зарегистрированные инструменты, а не выбранный
OpenAIClientAsync.withOptions(...) Фасад сервисов, не поток

Единственный реальный хук до буферизации — AsyncStreamResponse.Handler.onNext внутри openai-java, доступный через OpenAiChatModel.Builder.openAiClientAsync(...). Но корреляция перехвата с conversationId/runId нетривиальна (callback-поток SDK не несёт Reactor-контекст).

Текущее состояние

  • Обработчик TOOL_PREPARING в chatEventReducer.js отключён (no-op) — PR Upgrade to Spring Boot 4.1 and Spring AI 2.0 #50
  • Полная документация: docs/features/tool-preparing.md
  • История диагностики: docs/проект/диагностика-tool-preparing-стриминг.md

Варианты решения

  • A. Детекция тишины на фронте (рекомендуется) — таймер после последнего STREAM, индикатор при паузе ≥ 600–800 мс. Минимум кода, ловит реальную паузу, не знает имени инструмента.
  • B. Декоратор OpenAIClientAsync — перехват первого tool-дельта в Handler.onNext, публикация TOOL_PREPARING с именем инструмента. Реальный ранний сигнал, но OpenAI-специфично + хак корреляции через поле user/metadata.

TODO

  • Сравнить с реализацией tool-calling стриминга в langchain4j (раскрывает ли дельты до агрегации)
  • Выбрать вариант A или B
  • Реализовать или окончательно убрать мёртвый код (ToolPreparingAdvisor, ToolPreparingIndicator)

Metadata

Metadata

Assignees

No one assigned

    Labels

    enhancementNew feature or request

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions