webrtc-komaroff: от реплики до изменения карты
Архитектурный снимок от 16 сентября 2026 года. Исследован удалённый main 772de4a714ae6c5abea91946e85a1953ee919ec3 в отдельном чистом worktree. Локальный checkout, в котором лежит этот документ, оставлен на своей исходной версии. Существующие презентации и сгенерированный OpenWiki не использовались как доказательства.
Техническая презентация · Реестр доказательств · Восемь предложений по развитию · Проверка материалов
Что делает система
Браузер принимает текст или голос. Core завершает WebRTC-соединение, передаёт звук соседнему ASR-сервису и отправляет готовую реплику Agent. Agent загружает состояние разговора из Postgres и вызывает выбранный workflow. Workflow обращается к LLM и инструментам, после чего Agent сохраняет результат и публикует команды интерфейсу. Пользователь видит ответ и, для авиационных сценариев, аэропорты или маршрут на карте.
Это полезное разделение по типам нагрузки: медиасоединение, состояние диалога, сценарии, модель и прикладные инструменты живут в разных компонентах. Однако наличие процессов ещё не доказывает горизонтальное масштабирование: некоторые подписки, очереди и границы записи пока рассчитаны на более узкий режим работы.
Везде ниже VERIFIED / SOURCE означает подтверждение кодом, тестом или декларацией. INFERRED — вывод из реализации без нагрузочного эксперимента. UNKNOWN — слой, который в этом исследовании не проверялся. Развёрнутые образы, число реплик и live-поведение не установлены.
Граница проходит между диалогом и распознаванием
Браузер владеет микрофоном и состоянием отображения. Core владеет живым peer connection; его нельзя восстановить из записи диалога. Agent владеет постоянным состоянием сессии. LLM gateway изолирует вызов модели, а API gateway исполняет инструменты.
py_faster_whisper — соседний репозиторий: он получает PCM и возвращает текст. Его же образ обслуживает файловый WebUI, но публичные маршруты этого UI объявлены в stack/webrtc.drs. GPU backends и принимающая сторона межстековых proxy принадлежат внешнему rag-stack; его код в этот аудит не включён.
Источники: Core routes, AgentServer, владение ingress.
Где находится реализация
| Модуль | Ответственность и вход | Состояние / выход |
|---|---|---|
src_front/src |
TypeScript UI, WebRTC, HTTP-команды, NATS WebSocket | sessionStorage, сообщения, MapLibre |
src_core |
aiohttp: /core/offer, /core/message, /core/history, /core/init_map |
peer connections и audio tasks в памяти |
src_agent |
NATS nats.agent.<user_id>, история сессии |
sessions, runtime_state, events в Postgres |
src_langgraph |
Python workflow, LangGraph | runtime передаётся в запросе и ответе |
src_langgraph_rb |
Ruby workflow через собственный DSL/адаптер | альтернативная реализация сценариев |
src_langgraph_rb_node |
Ruby workflow на AsyncGraph 0.1.2 | граф, память и результаты внешних шагов |
src_llm |
NATS LLM request → Ollama или локальная llama.cpp-модель | ответ нормализуется под режим запроса |
src_api_gateway |
FastAPI и NATS tools | каталог аэропортов, демонстрационные данные |
src_shared/contracts |
Pydantic DTO и subjects Python-сервисов | граница совместимости JSON |
src_postgres, docker, stack |
schema/migrations, build, желаемое развёртывание | образы, volume, stack contract |
Название ruby_node не означает отдельный процесс Node.js. Здесь «node» — узел графа; командой запуска служит Ruby, а зависимостью — async-graph.
Корни src_* соответствуют выбранному корпоративному профилю. Их перенос в api-gateway/src или front/src не сокращает legacy. Framework-каталог src_front/src имеет другую роль и остаётся внутри service root.
Текстовый запрос проходит через два уровня оркестрации
Core преобразует HTTP payload в сообщение с trace_id, request_id, session_id и turn_id. Agent создаёт или обновляет строку сессии, читает runtime и передаёт его workflow. Возвращённый next_runtime записывается по ожидаемой версии; история записывается отдельной транзакцией. После этого Agent публикует UI-события и отвечает Core.
Workflow не выбирается автоматически по готовности соседей. В DRS WORKFLOW_RUNTIME по умолчанию равен ruby_node, а Agent направляет запросы на nats.workflow.run.ruby.node. Альтернативы используют другие subjects. При отсутствии responder Agent делает ограниченные повторы, затем возвращает workflow_unavailable; перехода на другой движок в этой ветке нет.
Новый запрос в сценарии аэропортов обычно вызывает модель дважды: routing_decision, затем tool_params. Далее инструменты возвращают позицию и аэропорты. build_route вызывается только при запросе маршрута; координаты и финальная фраза уже формируются без дополнительных LLM-вызовов. Это закреплено в тестах сценария.
Голос добавляет поток с собственным жизненным циклом
После SDP offer Core создаёт RTCPeerConnection. Полученный audio track проходит TrackSourceNode: выход — mono PCM s16, 16 кГц, кадры по 20 мс. WhisperStreamNode публикует их на inference.whisper.stream.<token> и слушает inference.whisper.text.<token>.
Граница между репозиториями — бинарный пакет:
[длина metadata: u32 big-endian][metadata JSON UTF-8][PCM int16 little-endian]
metadata: type=frame|end, seq, sample_rate, sample_width, channels, session_id
Whisper делает VAD — выделение речи среди тишины — и отдаёт pending, затем текст с phrase_id. Core преобразует этот идентификатор в стабильный turn_id и вызывает тот же обработчик диалога. Наличие поля seq у отправителя не означает проверки последовательности у получателя: в исследованном bridge эта проверка не найдена.
PCM при этих настройках занимает 32 000 байт/с на поток, без metadata и транспортных накладных расходов; каждый 20-мс кадр несёт 640 байт звука. Это расчёт формата, а не измеренная пропускная способность.
Как пройти код до побочного эффекта
message_handler вызывает handle_transcription; тот переходит через NATS к AgentServer.handle_request. AgentRunner.run читает состояние, а _run_with_optimistic_lock вызывает workflow и сохраняет ответ. В выбранном Ruby runtime Engine.run выбирает сценарий, GraphRunner.run доходит до внешнего await, а RequestExecutor.call направляет его в RuntimeIo.call_llm или call_tool.
Это дерево описывает конкретную реализацию, а не обещание durable execution. GraphRunner хранит request_results в локальном hash на время вызова. Python StateGraph компилируется без checkpointer. Сохранённый между репликами runtime не равен восстановлению незавершённого внешнего шага после падения процесса.
Постоянная история и runtime записываются раздельно
sessions связывает диалог с пользователем. runtime_state хранит версию, активный сценарий, pending и JSON-контекст. events хранит сообщения, а уникальность (session_id, turn_id) защищает строки истории от одинаковых ключей.
Agent сначала фиксирует runtime условным UPDATE, затем вызывает persist_chat_turn. Две записи истории внутри этой функции атомарны между собой, но не с предшествующим UPDATE runtime. Если история не запишется после успешного UPDATE, общий retry повторит workflow. Внешние обращения уже могли выполниться. INFERRED: при конфликте или отказе в этой точке возможны лишние LLM/tool-вызовы и расхождение runtime с историей. Частота в production неизвестна.
Редактирование реплики удаляет историю от найденного event_id и сбрасывает активный pending workflow перед пересчётом. Это контракт ветвления разговора, который нельзя потерять при замене движка.
Источники: AgentRunner, runtime repository, история и транзакции, schema.
Контракты, которые связывают сервисы
| Контракт | Производитель → потребитель | Что нельзя менять отдельно |
|---|---|---|
POST /core/offer |
браузер → Core | SDP и session identity |
POST /core/message |
браузер → Core | text, session_id, turn_id, edit |
GET /core/history |
браузер → Core → Agent | порядок сообщений и runtime_context |
nats.agent.<user_id> |
Core → Agent | envelope и Agent response |
nats.workflow.run.{python,ruby,ruby.node} |
Agent → выбранный runtime | runtime version, status, errors, client_events |
nats.llm.<user_id> |
workflow → LLM | mode и нормализованный data/error |
nats.tools.<tool_name> |
workflow → API gateway | args и tool result |
nats.events.<user_id> |
Agent/Core → браузер | command, artifacts, trace, порядок UI |
inference.whisper.stream/text.<token> |
Core ↔ Whisper | бинарный PCM и JSON результата |
Pydantic использует extra="forbid", а Ruby поддерживает свои validators. Даже добавление поля в JSON может потребовать согласованного rollout. Общей автоматически проверяемой схемы для Python, Ruby и TypeScript в исследованном наборе нет.
В браузере eventsSubject зафиксирован как nats.events.user123. Это не доказательство межпользовательской изоляции. Для многопользовательского режима нужны отдельная модель identity, server-side разрешения на сессию и проверка subscriptions.
Инструменты пока ограничивают смысл продукта
API gateway использует локальный список аэропортов, фиксированную текущую позицию, демонстрационные сведения о рейсах и погоде. Маршрут строится геометрически по дуге большого круга. Он не подтверждает дорожный маршрут, время проезда или актуальную авиационную информацию.
Поэтому архитектуру разумно оценивать как стенд голосового интерфейса и сценарной оркестрации. Вынос инструментов в отдельный сервис оправдан будущей интеграцией с реальными источниками, но сами эти источники пока не реализованы в проверенных handlers. Перед продуктовым использованием нужны явные provenance, freshness и обработка отсутствующих данных.
Источник: API gateway, функции _current_position, _tool_get_weather, _lookup_flight, _build_route_data.
Сборка и развёртывание описаны отдельно от исполнения
Compose содержит производимые образы и локальные runtime-настройки. Python build contexts указывают на src_*; Ruby contexts сейчас охватывают корень репозитория. DRS объявляет все три workflow-сервиса, а значение WORKFLOW_RUNTIME определяет путь Agent. Объявленный невыбранный движок нельзя автоматически считать удаляемым: сначала нужен учёт его consumers.
В корневом CI отдельная job tests закомментирована. В этом исследовании не подтверждён обязательный gate всех тестов перед публикацией. Успех build/deploy job сам по себе это не восполняет. Pipeline #4640 ранее прошёл для этого же commit; это историческое build/deploy evidence, не новый live smoke.
| Слой | Результат этого исследования |
|---|---|
| Source | VERIFIED: точный commit, source anchors, clean worktree |
| Локальные tests | 25 Ruby examples и 7 Python tests прошли 16.09.2026 |
| Desired stack | VERIFIED: декларации DRS; не исполнялись |
| Running image / replicas / health | UNKNOWN: runtime не перепроверялся |
| Голос/карта в live environment | UNKNOWN: новый end-to-end прогон не выполнялся |
Проверки покрывают сценарии, но оставляют транспортные пробелы
В src_langgraph_rb_node/spec есть проверки registry, engine, memory, графов и сценариев. Они подтверждают, например, что вопрос о названии аэропорта не строит маршрут и явный радиус сохраняется при неудаче модели. tests/test_llm_routing.py проверяет логику нормализации маршрутизации. Входы LLM/tools в Ruby suite подменены: эти результаты не измеряют GPU, NATS или браузер.
Для принятия архитектурных изменений нужны ещё испытания двух реплик workflow, одновременных turn одной сессии, обрыва клиента, перегруженного ASR и reconnect UI. Предлагаемые метрики и условия приёмки приведены отдельно для каждого изменения. Сейчас p95, стоимость реплики, частота повторов и максимальное число сессий не измерены.
Куда идти при изменении поведения
| Задача | Реализация | Что проверить вместе |
|---|---|---|
| Новый инструмент | API gateway + tool DTO + RuntimeIo | schema, NATS subject, ошибки, fixtures |
| Новый сценарий | registry, definition, graph, nodes | routing entries, pending, history, UI events |
| Изменить звук | TrackSourceNode + WhisperStreamNode | wire packet, VAD bridge, end, отмена |
| Заменить workflow | Agent subjects + DRS + Compose | parity всех сценариев, edit/replay, время ожидания |
| Добавить реплики | subscriptions + state ownership | queue groups, идемпотентность, порядок сессии |
| Изменить UI-события | UiEventPublisher + frontend handlers | session scope, reconnect, дубли, snapshot |
| Поменять хранение | runtime repository + db.py + migrations | атомарность turn и совместимость истории |
Оценка подхода
Разделение медиапотока, GPU и диалога стоит сохранить: у них разные требования к задержке, состоянию и ресурсам. Самое спорное место — два последовательных оркестратора и три поддерживаемых реализации одних сценариев. Они увеличивают объём согласуемых contracts, не давая доказанного выигрыша на текущей нагрузке.
Начать стоит с исправления повторного исполнения и ограничения очередей, затем сокращать обращения к модели и выбирать один runtime по измеренному parity. Объединение Agent с выбранным workflow — обоснованный кандидат на упрощение. Полное переписывание нужно рассматривать только при подтверждённой потребности в восстановлении длительных процессов, многопользовательской изоляции или другой модели исполнения; уменьшение числа файлов само по себе его не оправдывает.