Агенты данных и GraphRAG на устройстве со Spanner Omni

1. Обзор

В этой практической работе рассматриваются основы Spanner Omni, причины, по которым он служит идеальной основой данных для автономных ИИ-агентов, и приблизительная сквозная реализация развернутого на периферии агентного тактического тренера по пиклболу и симулятора матчей на базе Spanner Omni (Relational + Graph GQL), Python Flask и Gemini 3.8 Flash.

Проблема

Современные интерактивные спортивные платформы и игровые среды реального времени требуют одновременной обработки трех различных рабочих нагрузок:

Операционные транзакции с высокой степенью параллелизма

Бронирование кортов, членство игроков, выставление счетов и расписание турниров, требующие строгой сериализуемости ACID для устранения конфликтов двойного бронирования.

Телеметрия подключенных графов:

Непрерывные последовательности ударов, цепи Маркова для переходов и графики покрытия зон, требующие обхода с низкой задержкой без громоздких конвейеров ETL.

Мгновенный отклик на периферии

Физические симуляции с частотой 60 кадров в секунду, в которых задержка при передаче данных по глобальной облачной сети (100–200 мс) может привести к неприемлемому дрожанию.

Раньше инженеры решали эту проблему, объединяя фрагментированные архитектуры, которые выглядели примерно так: PostgreSQL для транзакционных данных, Neo4j для графов, Redis для пограничного кеширования и конвейеры фоновой синхронизации. Это приводит к архитектурному дрейфу, высокой сложности операций и задержкам в обеспечении согласованности в конечном счете.

Как мы это делаем!

Развернув Google Cloud Spanner Omni на периферии и объединив его с Google Gemini 3.8 Flash, мы создали Spanner Omni Pickleball Game – спортивную игровую платформу на одном движке, которая объединяет реляционную бизнес-логику, нативную телеметрию ударов ISO GQL и тактического ИИ-тренера, работающего в реальном времени.

Spanner Omni

Spanner Omni – это распределенная база данных Google планетарного масштаба с горизонтальной масштабируемостью, транзакциями ACID и мультимодельными возможностями (реляционная, графовая, векторная и аналитическая). Она предназначена для самоуправляемых, локальных, мультиоблачных и изолированных сред. Spanner Omni – это скачиваемая, самостоятельно управляемая версия Google Cloud Spanner. Она предоставляет основные возможности распределенной базы данных Spanner – автоматическое сегментирование, репликацию на основе Paxos и строгую внешнюю согласованность – независимо от того, где вы решите ее запустить:

acc871a470f63b29.png

Что вы узнаете

В этой практической работе вы перейдете от традиционных изолированных архитектур баз данных к современному унифицированному решению для периферийных баз данных. Вы научитесь:

  1. Развертывайте и оркестрируйте Spanner Omni локально с помощью Docker, чтобы управлять высокопроизводительными реляционными рабочими нагрузками с одним экземпляром.
  2. Разработайте единую схему реляционного графа и графа ISO GQL, которая сопоставляет транзакционные бизнес-модели с многомерной игровой телеметрией без необходимости синхронизации внешней базы данных.
  3. Реализуйте строгую сериализуемость ACID в транзакциях чтения-записи Python, чтобы программно предотвратить состояние гонки и устранить конфликты двойного бронирования.
  4. Создайте нативный конвейер GraphRAG, который использует интегрированные с SQL запросы графов ISO GQL, чтобы передавать телеметрию матча в реальном времени с учетом контекста прямо в Google Gemini для мгновенного тактического коучинга.

Требования

  • Стандартный локальный компьютер или виртуальная машина для разработки под управлением Linux (Ubuntu 22.04 или более поздней версии), macOS или Windows.
  • Установленное и запущенное приложение Docker Desktop или Docker Engine.
  • Установлена версия Python 3.10 или более поздняя.
  • Текстовый редактор или интегрированная среда разработки (например, VS Code или PyCharm).
  • Активный ключ Gemini API. Примечание. Для локального запуска Spanner Omni не требуется платежный аккаунт Google Cloud, поскольку он работает по бесплатной лицензии разработчика.

2. Развертывание и настройка Spanner Omni

На этом шаге мы запустим локальный экземпляр базы данных Edge. Spanner Omni работает в высокооптимизированном контейнере, предоставляя вам возможности реляционной базы данных планетарного масштаба на вашем локальном ноутбуке разработчика.

  1. Откройте терминал и создайте постоянный том Docker для хранения схем баз данных и телеметрии игр:
docker volume create spanner_omni_data
  1. Извлеките и запустите официальный образ контейнера Spanner Omni, настроенный в режиме разработчика с одним сервером:
docker run -d --name spanner-omni \
  -p 9010:9010 -p 9020:9020 \
  -v spanner_omni_data:/spanner \
  us-docker.pkg.dev/spanner-omni/images/spanner-omni:2026.r2.1-beta \
  start-single-server
  1. Убедитесь, что контейнер запущен успешно:
docker ps

🛠️ Быстрое устранение неполадок. Если запуск не удался, потому что порты 9010 или 9020 уже используются фоновыми процессами или устаревшими эмуляторами, выполните в терминале следующий скрипт:

pkill -f "spanner" || true
fuser -k 9010/tcp 9020/tcp 2>/dev/null || true
docker rm -f spanner-local spanner-omni spanner-omni-local 2>/dev/null || true

3. Реляционная схема и схема DDL графа ISO GQL

После активации Spanner Omni мы можем создать схему базы данных. Эта схема служит единым источником достоверных данных, объединяя реляционные бизнес-объекты (такие как корты, игроки и бронирования) с быстрыми, связанными схемами телеметрии графов (броски, зоны и переходы).

Чтобы создать таблицы операционных данных и телеметрии, выполните следующие действия:

  1. Подключитесь к базе данных с помощью Spanner CLI или клиента приложения.
  2. Выполните следующие инструкции DDL

От вас не требуется никаких действий, поскольку мы устраняем эту проблему в коде приложения.

-- ============================================================================
-- 1. OPERATIONAL & FACILITY MANAGEMENT (RELATIONAL ACID)
-- ============================================================================

CREATE TABLE Courts (
    court_id INT64 NOT NULL,
    name STRING(100) NOT NULL,
    location STRING(100) NOT NULL,
    surface_type STRING(50) NOT NULL,
    is_indoor BOOL NOT NULL,
    hourly_rate FLOAT64 NOT NULL,
    has_lighting BOOL NOT NULL,
    created_at TIMESTAMP NOT NULL OPTIONS (allow_commit_timestamp=true)
) PRIMARY KEY (court_id);

CREATE TABLE Players (
    player_id INT64 NOT NULL,
    name STRING(100) NOT NULL,
    email STRING(100) NOT NULL,
    skill_level STRING(20) NOT NULL,
    phone STRING(50),
    created_at TIMESTAMP NOT NULL OPTIONS (allow_commit_timestamp=true)
) PRIMARY KEY (player_id);

CREATE TABLE Bookings (
    booking_id INT64 NOT NULL,
    court_id INT64 NOT NULL,
    player_id INT64 NOT NULL,
    booking_date DATE NOT NULL,
    start_hour INT64 NOT NULL,
    end_hour INT64 NOT NULL,
    player_name STRING(100) NOT NULL,
    court_name STRING(100) NOT NULL,
    status STRING(20) NOT NULL,
    notes STRING(MAX),
    created_at TIMESTAMP NOT NULL OPTIONS (allow_commit_timestamp=true)
) PRIMARY KEY (booking_id);

CREATE TABLE MatchRequests (
    match_id INT64 NOT NULL,
    player_id INT64 NOT NULL,
    player_name STRING(100) NOT NULL,
    court_id INT64,
    court_name STRING(100),
    play_date DATE NOT NULL,
    start_hour INT64 NOT NULL,
    skill_required STRING(20) NOT NULL,
    match_type STRING(20) NOT NULL,
    spots_open INT64 NOT NULL,
    status STRING(20) NOT NULL,
    notes STRING(MAX),
    created_at TIMESTAMP NOT NULL OPTIONS (allow_commit_timestamp=true)
) PRIMARY KEY (match_id);

-- ============================================================================
-- 2. TACTICAL GAME TELEMETRY & PROPERTY GRAPH (ISO GQL)
-- ============================================================================

CREATE TABLE CourtZones (
    zone_id STRING(50) NOT NULL,
    name STRING(100) NOT NULL,
    is_kitchen BOOL NOT NULL,
    side STRING(50) NOT NULL
) PRIMARY KEY (zone_id);

CREATE TABLE Shots (
    shot_id STRING(50) NOT NULL,
    player_id STRING(50) NOT NULL,
    shot_type STRING(100) NOT NULL,
    speed_mph FLOAT64,
    spin_type STRING(50),
    outcome STRING(50),
    from_zone_id STRING(50),
    to_zone_id STRING(50)
) PRIMARY KEY (shot_id);

CREATE TABLE ShotTransitions (
    transition_id STRING(50) NOT NULL,
    from_shot_id STRING(50) NOT NULL,
    to_shot_id STRING(50) NOT NULL,
    rally_depth INT64,
    pressure_level STRING(20)
) PRIMARY KEY (transition_id);

-- ============================================================================
-- 3. MATCH SESSIONS & AI COACHING HISTORY
-- ============================================================================

CREATE TABLE GameSessions (
    session_id STRING(50) NOT NULL,
    player_id INT64 NOT NULL,
    player_score INT64 NOT NULL,
    bot_score INT64 NOT NULL,
    bot_difficulty STRING(20) NOT NULL,
    winner STRING(20) NOT NULL,
    rally_length INT64 NOT NULL,
    fault_type STRING(100),
    coaching_summary STRING(MAX),
    created_at TIMESTAMP NOT NULL OPTIONS (allow_commit_timestamp=true)
) PRIMARY KEY (session_id);
cat << 'EOF' > Dockerfile
FROM ollama/ollama:latest
ENV OLLAMA_HOST=0.0.0.0:8080
ENV OLLAMA_MODELS=/root/.ollama
ENV MODEL=gemma4:e2b
RUN ollama serve & sleep 5 && ollama pull $MODEL
ENTRYPOINT ["ollama", "serve"]
EOF

💡 Почему важны временные метки фиксации. Указание OPTIONS (allow_commit_timestamp=true) позволяет упорядочивать данные по монотонно возрастающему времени с точностью до микросекунды во всех локальных репликах узлов. Это критически важно для синхронизации быстро происходящих игровых событий (например, видеозаписи удара по мячу) с соответствующими телеметрическими данными в базе данных.

4. Управление параллелизмом ACID и предотвращение двойного бронирования

На популярных порталах бронирования спортивных площадок пользователи часто соревнуются за одни и те же временные интервалы. Чтобы предотвратить состояние гонки, мы напишем транзакцию чтения-записи на Python в Spanner Omni, чтобы обеспечить абсолютную сериализуемость с защитой от двойного бронирования.

Сейчас от вас не требуется никаких действий, просто посмотрите на код:

from google.cloud import spanner

def create_booking_transaction(transaction, booking_data):
    # 1. Query for existing overlapping confirmed bookings on the same court
    query = """
        SELECT booking_id, player_name, start_hour, end_hour
        FROM Bookings
        WHERE court_id = @court_id
          AND booking_date = @booking_date
          AND status = 'CONFIRMED'
          AND NOT (end_hour <= @start_hour OR start_hour >= @end_hour)
    """
    params = {
        "court_id": booking_data["court_id"],
        "booking_date": booking_data["booking_date"],
        "start_hour": booking_data["start_hour"],
        "end_hour": booking_data["end_hour"],
    }
    param_types = {
        "court_id": spanner.param_types.INT64,
        "booking_date": spanner.param_types.DATE,
        "start_hour": spanner.param_types.INT64,
        "end_hour": spanner.param_types.INT64,
    }
    
    results = list(transaction.execute_sql(query, params=params, param_types=param_types))
    
    if results:
        conflict = results[0]
        raise ValueError(
            f"Court conflict: Already booked by {conflict[1]} ({conflict[2]}:00-{conflict[3]}:00)"
        )
    
    # 2. Insert new confirmed booking with Spanner commit timestamp
    insert_stmt = """
        INSERT INTO Bookings (
            booking_id, court_id, player_id, booking_date, start_hour, end_hour,
            player_name, court_name, status, notes, created_at
        ) VALUES (
            @booking_id, @court_id, @player_id, @booking_date, @start_hour, @end_hour,
            @player_name, @court_name, 'CONFIRMED', @notes, PENDING_COMMIT_TIMESTAMP()
        )
    """
    # execute_update statement writing to Spanner Omni...
    transaction.execute_update(insert_stmt, params=booking_data, param_types=booking_types)

5. Физика в реальном времени с частотой 60 кадров/с и настраиваемый бот DUPR

Внешний интерфейс нашего приложения использует симуляцию 2.5D HTML5 Canvas с частотой 60 кадров в секунду. Рендеринг полностью отделен от уровней базы данных, чтобы поддерживать плавную производительность, а телеметрия сбрасывается только после завершения ралли.

В симуляторе есть настраиваемый ИИ-движок OmniBot, который использует матрицу сложности на основе реальных уровней DUPR (Dynamic Universal Pickleball Rating).

1640056a824b47e6.png

Сейчас от вас не требуется никаких действий. Просто ознакомьтесь с информацией.

6. Обработка графика телеметрии ISO GQL Shot

В конце каждого розыгрыша интерфейс отправляет соединенный граф траектории в конечную точку серверной части /api/rally-end. Каждый кадр записывается как реляционный узел, а последовательности динамически связываются через переходные ребра.

Сейчас от вас не требуется никаких действий, просто посмотрите на код:

@app.route("/api/execute-shot", methods=["POST"])
def execute_shot():
    data = request.json or {}
    shot_id = f"shot_{int(time.time()*1000)}"
    
    # 1. Insert the Shot Node
    insert_shot_sql = """
        INSERT INTO Shots (shot_id, player_id, shot_type, speed_mph, spin_type, outcome, from_zone_id, to_zone_id)
        VALUES (@shot_id, @player_id, @shot_type, @speed, @spin, @outcome, @from_zone, @to_zone)
    """
    
    # 2. If part of a continuous rally, link with a ShotTransition Edge
    if data.get("prev_shot_id"):
        transition_id = f"trans_{int(time.time()*1000)}"
        insert_trans_sql = """
            INSERT INTO ShotTransitions (transition_id, from_shot_id, to_shot_id, rally_depth, pressure_level)
            VALUES (@trans_id, @from_shot, @to_shot, @depth, @pressure)
        """
        # Executed within a single, unified Spanner Omni transaction...

Используя встроенный в Spanner механизм запросов к графам ISO GQL, тренер или аналитическая программа могут мгновенно запрашивать сложные структурные шаблоны с глубокими переходами без выполнения громоздких объединений баз данных:

-- Find highest-frequency transition sequences leading to player unforced errors
GRAPH PickleballTelemetry
MATCH (s1:Shots)-[t:ShotTransitions]->(s2:Shots)
WHERE s1.shot_type = 'Speedup' AND s2.outcome = 'FAULT_NET'
RETURN s1.speed_mph, s2.from_zone_id, COUNT(*) AS occurrences
ORDER BY occurrences DESC
LIMIT 5;

7. Тактический коучинг с ИИ Gemini и чат с RAG на основе данных

Теперь мы активируем мозг нашего приложения: Google Gemini 3.8 Flash. Наш бэкенд Python использует модель для предоставления трех возможностей ИИ в реальном времени, основанных непосредственно на Spanner Omni.

Для некоторых рабочих процессов ИИ в реальном времени система использует Google Gemini 3.8 Flash:

Анализ розыгрышей в реальном времени и тактический судья

После каждого очка Gemini анализирует цепочку ударов, сложность бота и позиционирование на корте.

Spanner Multi-Table Grounded AI Concierge & Match Analyst:

Пользователи могут запрашивать расписание кортов, тарифы на услуги и спрашивать "Как я сыграл против OmniBot?" на естественном языке. Бэкенд динамически запрашивает Spanner Omni в четырех доменах (Courts, Bookings, GameSessions и Shots), чтобы обосновать Gemini без галлюцинаций.

Real-Time Performance Analytics API (/api/performance):

Агрегирует текущие соотношения побед и поражений, среднюю глубину ралли и недавние игровые сеансы непосредственно из таблицы GameSessions Spanner Omni для панели управления игрока.

Сейчас от вас не требуется никаких действий. Просто ознакомьтесь с информацией.

8. Настройте, запустите и протестируйте

Давайте запустим полное приложение на вашем локальном компьютере, чтобы проверить систему от начала до конца.

  1. Откройте терминал хоста и клонируйте репозиторий проекта:
git clone https://github.com/AbiramiSukumaran/pickleball

cd pickleball
  1. Чтобы получить ключ Gemini API, следуйте инструкциям в этой документации:

https://medium.com/google-cloud/get-api-key-from-google-ai-studio-b2d2059a171d

  1. Откройте файл .env в редакторе и настройте сведения о среде:
SPANNER_EMULATOR_HOST="localhost:9010"
SPANNER_PROJECT="PROJECT_ID"
SPANNER_INSTANCE="omni-instance"
SPANNER_DATABASE="pickleball-db"
GEMINI_API_KEY="REPLACE_WITH_YOUR_ACTUAL_GEMINI_KEY"
  1. Установите зависимости Python и запустите приложение:
pip install -r requirements.txt

python app.py
  1. Откройте браузер и перейдите на страницу http://localhost:5000, чтобы начать играть. Бронируйте корты на вкладке Reservation (Бронирование), играйте против настраиваемого DUPR OmniBot и консультируйтесь с Gemini Match Analyst прямо на корте.

Откройте браузер или нажмите на ссылку, чтобы запустить приложение на локальном хосте.

a2d18f8288607c66.png

9. Поздравляем!

Выполнив эту практическую работу, вы стали свидетелем смены парадигмы в разработке баз данных. В течение многих лет создание адаптивных агентов, работающих в реальном времени на периферии (на заводах, в военных зонах или на спортивных площадках), требовало болезненных компромиссов в проектировании: объединения хрупких конвейеров ETL между изолированными транзакционными системами, отдельными узлами графов и индексами векторного поиска.

Spanner Omni устраняет этот компромисс. Он сочетает в себе распределенную согласованность планетарного масштаба, запросы к графам ISO GQL и реляционную производительность в одном загружаемом локальном контейнере. Вне зависимости от домена Spanner Omni гарантирует, что у ваших периферийных агентов будет надежная фундаментальная истина.