245 lines
14 KiB
Markdown
245 lines
14 KiB
Markdown
# Баг-репорт: регрессия стриминга платформы после file/tool flow
|
||
|
||
## Кратко
|
||
|
||
После обновления до текущих upstream-версий платформы стриминг ответов стал нестабильным в сценариях с вложениями и tool/file flow.
|
||
|
||
Наблюдаемые симптомы:
|
||
|
||
- первый текстовый chunk ответа может приходить уже обрезанным
|
||
- соседние ответы могут "протекать" друг в друга
|
||
- после некоторых запросов бот перестаёт присылать финальный ответ
|
||
- платформа присылает дублирующий `END`
|
||
|
||
До обновления платформы этот класс ошибок у нас не воспроизводился.
|
||
|
||
## Версии платформы
|
||
|
||
В рантайме используются upstream-репозитории без локальных правок:
|
||
|
||
- `platform-agent`: `5e7c2df954cc3cd2f5bf8ae688e10a20038dde61`
|
||
- `platform-agent_api`: `aa480bbec5bbf8e006284dd03aed1c2754e9bbee`
|
||
|
||
## Контекст интеграции
|
||
|
||
- поверхность: Matrix
|
||
- транспорт к платформе: websocket через `platform-agent_api`
|
||
- `chat_id` на платформу отправляется как стабильный числовой surrogate id
|
||
- shared workspace: `/workspace`
|
||
|
||
Важно: vendored platform repos чистые, совпадают с upstream. Проблема воспроизводится без локальных patch'ей в платформу.
|
||
|
||
## Пользовательские симптомы
|
||
|
||
Примеры из живого диалога:
|
||
|
||
- ожидалось: `Моя ошибка: ...`
|
||
- фактически пришло: `оя ошибка: ...`
|
||
|
||
- ожидалось начало ответа вида `По фото IMG_3183.png ...`
|
||
- фактически пришло: `IMG_3183.png**) — это ...`
|
||
|
||
Также наблюдалось:
|
||
|
||
- после вопросов по изображениям бот иногда вообще перестаёт отвечать
|
||
- в том же чате, до attachment/tool flow, ответы приходят корректно
|
||
|
||
## Шаги воспроизведения
|
||
|
||
1. Поднять `platform-agent` и Matrix surface на версиях выше.
|
||
2. Отправить несколько обычных текстовых сообщений.
|
||
3. Убедиться, что начальные ответы стримятся корректно.
|
||
4. Отправить изображения/файлы и задать вопросы вида:
|
||
- `что изображено на фото`
|
||
- уточняющие follow-up вопросы по тем же вложениям
|
||
5. Затем отправить ещё одно обычное текстовое сообщение.
|
||
6. Наблюдать один или несколько симптомов:
|
||
- первый chunk начинается с середины слова
|
||
- ответ начинается с середины фразы
|
||
- хвост прошлого ответа загрязняет следующий
|
||
- видимого финального ответа нет вообще
|
||
|
||
## Что удалось доказать
|
||
|
||
По debug-логам Matrix surface видно, что обрезание присутствует уже в первом chunk, полученном от платформы.
|
||
|
||
Корректные первые chunk'и до attachment/tool flow:
|
||
|
||
- `Hey! How`
|
||
- `Я`
|
||
- `Первый файл не найден — возможно, ...`
|
||
|
||
Некорректные первые chunk'и после attachment/tool flow:
|
||
|
||
- `IMG_3183.png**) — это ю...`
|
||
- `оя ошибка: в первом запросе...`
|
||
|
||
Это логируется сразу после десериализации websocket event на клиентской стороне, до Matrix rendering и до финальной сборки текста ответа. Из этого следует, что повреждение происходит на стороне платформенного стриминга, а не в Matrix sender.
|
||
|
||
## Дополнительное наблюдение по протоколу
|
||
|
||
Платформа сейчас отправляет дублирующий `END`.
|
||
|
||
Релевантные места в upstream:
|
||
|
||
- `external/platform-agent/src/agent/service.py`
|
||
- уже `yield MsgEventEnd(...)`
|
||
- `external/platform-agent/src/api/external.py`
|
||
- после завершения цикла дополнительно отправляет ещё один `MsgEventEnd(...)`
|
||
|
||
В живых логах это видно как:
|
||
|
||
- первый `END`
|
||
- второй `END`
|
||
- клиентская suppression логика, которая гасит дубликат
|
||
|
||
Это само по себе делает границы ответа неоднозначными и повышает риск "протекания" поздних событий в следующий запрос.
|
||
|
||
## Предполагаемая первопричина
|
||
|
||
Похоже, что на стороне платформы одновременно есть две проблемы.
|
||
|
||
### 1. Двойной сигнал завершения стрима
|
||
|
||
Для одного ответа генерируется два `END`.
|
||
|
||
Вероятные последствия:
|
||
|
||
- нечёткая граница ответа
|
||
- поздние события могут относиться не к тому запросу
|
||
- соседние ответы могут смешиваться
|
||
|
||
### 2. Некорректное извлечение текстового chunk'а
|
||
|
||
В текущем upstream `platform-agent/src/agent/service.py` текст форвардится из `chunk.content` внутри `on_chat_model_stream`.
|
||
|
||
Однако в документации LangChain для `astream_events()` примеры используют `event["data"]["chunk"].text`, а в Deep Agents stream дополнительно есть `ns`/`source`, которые важны для отделения main-agent output от tool/subagent/model-internal stream.
|
||
|
||
Потенциальные последствия:
|
||
|
||
- первый видимый chunk может быть неполным
|
||
- во внешний клиент может попадать не только финальный пользовательский текст
|
||
- attachment/tool flow сильнее деградирует поведение стрима
|
||
|
||
## Почему проблема считается платформенной
|
||
|
||
С нашей стороны были проверены и исключены базовые причины:
|
||
|
||
- вложения корректно сохраняются в `/workspace`
|
||
- контейнер `platform-agent` видит эти файлы
|
||
- Matrix surface получает уже обрезанный первый chunk от платформы
|
||
- обрезание происходит до сборки финального ответа
|
||
- эксперимент с reconnect на каждый запрос не исправил проблему
|
||
- платформенные vendored repos сейчас совпадают с upstream
|
||
|
||
## Ожидаемое поведение
|
||
|
||
Для каждого пользовательского запроса:
|
||
|
||
- текстовые chunk'и должны начинаться с реального начала ответа модели
|
||
- должен приходить ровно один terminal `END`
|
||
- границы ответов должны быть однозначными
|
||
- file/tool flow не должен ломать следующий ответ
|
||
|
||
## Фактическое поведение
|
||
|
||
После attachment/tool flow:
|
||
|
||
- первый text chunk может быть уже обрезан
|
||
- `END` приходит дважды
|
||
- следующий ответ может начаться с середины слова или фразы
|
||
- отдельные запросы могут не завершаться видимым ответом
|
||
|
||
## Дополнительный failure mode: большие изображения
|
||
|
||
В отдельном воспроизведении текстовые запросы до файлового сценария работали корректно, а сбой возникал только после попытки анализа изображений.
|
||
|
||
По логам видно уже не только stream corruption, но и конкретный image-path failure:
|
||
|
||
- `platform-agent` рвёт websocket с `1009 (message too big)`
|
||
- провайдер возвращает `400` с причиной:
|
||
- `Exceeded limit on max bytes per data-uri item : 10485760`
|
||
|
||
Характерный фрагмент:
|
||
|
||
```text
|
||
websockets.exceptions.ConnectionClosedError: received 1009 (message too big); then sent 1009 (message too big)
|
||
...
|
||
Agent error (INTERNAL_ERROR): Error code: 400 - {
|
||
'error': {
|
||
'message': 'Provider returned error',
|
||
'metadata': {
|
||
'raw': '{"error":{"message":"Exceeded limit on max bytes per data-uri item : 10485760"...}}'
|
||
}
|
||
}
|
||
}
|
||
```
|
||
|
||
Из этого следует:
|
||
|
||
- текстовый path сам по себе работоспособен
|
||
- image-analysis path в платформе сейчас передаёт изображение как data URI
|
||
- для достаточно больших изображений это утыкается в provider limit `10,485,760` байт на один data-uri item
|
||
- параллельно websocket path между surface и platform-agent неустойчив к этому failure scenario и закрывается с `1009`
|
||
|
||
То есть в платформе есть как минимум ещё одна отдельная проблема помимо некорректного стриминга:
|
||
|
||
- отсутствует безопасная обработка больших изображений до отправки в provider
|
||
- отсутствует аккуратная деградация без разрыва websocket-сессии
|
||
|
||
## Что стоит исправить в платформе
|
||
|
||
1. Отправлять ровно один `MsgEventEnd` на один ответ.
|
||
2. Перепроверить extraction текста из `on_chat_model_stream`:
|
||
- вероятно, должен использоваться `chunk.text`, а не `chunk.content`.
|
||
3. Учитывать `ns`/`source` и форвардить наружу только main assistant output.
|
||
4. Перед отправкой изображений в provider проверять размер payload и не пытаться слать oversized data-uri.
|
||
5. Для больших изображений:
|
||
- либо делать resize/compression,
|
||
- либо возвращать контролируемую user-facing ошибку без разрыва websocket.
|
||
6. В идеале добавить более жёсткую request boundary в websocket protocol, чтобы late events не могли прилипать к следующему запросу.
|
||
|
||
## Наши временные mitigation'ы на стороне surface
|
||
|
||
Они не исправляют корень, только снижают ущерб:
|
||
|
||
- suppression duplicate `END`
|
||
- короткий post-`END` drain window
|
||
- idle timeout для зависшего стрима
|
||
- transport failures нормализуются в `PlatformError`, чтобы Matrix bot не падал процессом
|
||
|
||
Эти меры понадобились только потому, что в текущем сценарии platform stream contract нестабилен.
|
||
|
||
## Приложение: характерный фрагмент логов
|
||
|
||
```text
|
||
[matrix-bot] text chunk queue=True text='Первый файл не найден — возможно,'
|
||
[matrix-bot] ...
|
||
[matrix-bot] end event queue=True tokens=0
|
||
[matrix-bot] end event queue=True tokens=0
|
||
[matrix-bot] dropped duplicate END tokens=0
|
||
[matrix-bot] text chunk queue=True text='IMG_3183.png**) — это ю'
|
||
[matrix-bot] ...
|
||
[matrix-bot] end event queue=True tokens=0
|
||
[matrix-bot] end event queue=True tokens=0
|
||
[matrix-bot] dropped duplicate END tokens=0
|
||
[matrix-bot] text chunk queue=True text='оя ошибка: в первом запросе я случайно постав'
|
||
```
|
||
|
||
Этот фрагмент показывает две вещи:
|
||
|
||
- duplicate `END` действительно приходит от платформы
|
||
- следующий первый chunk уже приходит в клиента обрезанным
|
||
|
||
## Приложение: характерный фрагмент логов для больших изображений
|
||
|
||
```text
|
||
platform-agent-1 | websockets.exceptions.ConnectionClosedError: received 1009 (message too big); then sent 1009 (message too big)
|
||
...
|
||
matrix-bot-1 | Agent error (INTERNAL_ERROR): Error code: 400 - {'error': {'message': 'Provider returned error', 'code': 400, 'metadata': {'raw': '{"error":{"message":"Exceeded limit on max bytes per data-uri item : 10485760","type":"invalid_request_error"...}}}}
|
||
```
|
||
|
||
Этот фрагмент показывает ещё две вещи:
|
||
|
||
- image path в платформе реально упирается в лимит провайдера на размер data URI
|
||
- при этом ошибка не изолируется корректно и сопровождается разрывом websocket-соединения
|