You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Событие 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 уже содержит полные аргументы
setResponse зовётся один раз по завершении; requestTools = все зарегистрированные инструменты, а не выбранный
OpenAIClientAsync.withOptions(...)
Фасад сервисов, не поток
Единственный реальный хук до буферизации — AsyncStreamResponse.Handler.onNext внутри openai-java, доступный через OpenAiChatModel.Builder.openAiClientAsync(...). Но корреляция перехвата с conversationId/runId нетривиальна (callback-поток SDK не несёт Reactor-контекст).
История диагностики: 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)
Проблема
Событие
TOOL_PREPARINGприходит в SSE-поток вплотную кTOOL_CALL— зазора нет, индикатор «готовлю данные…» на фронте никогда не успевает появиться.Причина
OpenAiChatModel.internalStream(Spring AI 2.0 + openai-java 4.x) буферизует все дельты tool-call черезbufferUntil/ChunkMergerдо того, как отдать что-либо наружу:Всё, что выше (advisor-chain,
ChatRunService, observation), видит только агрегированный чанк с уже полными аргументами. К этому моментуToolCallingAdvisorсразу запускает инструмент →TOOL_PREPARINGиTOOL_CALLуходят почти одновременно.Проверенные точки расширения
StreamAdvisor(ToolPreparingAdvisor)bufferUntil— первый жеhasToolCalls()=trueуже содержит полные аргументыChatModelObservationConvention/ObservationHandlersetResponseзовётся один раз по завершении;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 #50docs/features/tool-preparing.mddocs/проект/диагностика-tool-preparing-стриминг.mdВарианты решения
STREAM, индикатор при паузе ≥ 600–800 мс. Минимум кода, ловит реальную паузу, не знает имени инструмента.OpenAIClientAsync— перехват первого tool-дельта вHandler.onNext, публикацияTOOL_PREPARINGс именем инструмента. Реальный ранний сигнал, но OpenAI-специфично + хак корреляции через полеuser/metadata.TODO
ToolPreparingAdvisor,ToolPreparingIndicator)