Реалізація AI-агента з доступом до бази даних у мобільному додатку
Text-to-SQL—стара задача, але з LLM вона стала достатньо надійною для продакшену. Користувач запитує «покажи мої видатки за прошлий місяць по категоріях», агент перекладає це у SQL-запит, виконує, форматує результат. Без написання окремого екрана аналітики, без фіксованих фільтрів.
Чому Text-to-SQL на мобільному—окремо задача
Прямий доступ мобільного додатку до продуктивної БД—погана ідея. Навіть read-only. Правильна архітектура: мобільний клієнт → бекенд API з агентом → БД. Бекенд валідирует сгенерований SQL, обмежує набір доступних таблиць, контролює права користувача.
На клієнті використовується либо локальна БД (SQLite через Room на Android, Core Data / GRDB на iOS) для офлайн-даних додатку, либо агент працює на сервері та клієнт отримує готові дані.
Як научити модель вашої схеми БД
Модель не знає вашу схему. Потрібно передавати її у системному промпті або через інструмент get_schema. Не вивалюйте весь DDL на 200 таблиць—беріть тільки релевантні. Для додатку особистих фінансів достатньо 5–8 таблиць.
-- Приклад схеми для промпту (спрощена)
CREATE TABLE transactions (
id SERIAL PRIMARY KEY,
user_id INTEGER NOT NULL,
amount DECIMAL(10,2) NOT NULL, -- від'ємне = видатки
category VARCHAR(50), -- 'food', 'transport', 'entertainment'
description TEXT,
created_at TIMESTAMP DEFAULT NOW()
);
У системний промпт додаємо: «Ти генеруєш SQL-запити ТІЛЬКИ для SELECT. Ніколи не використовуй INSERT, UPDATE, DELETE, DROP. Всі запити повинні містити WHERE user_id = :user_id.»
Обмеження через промпт—перший шар захисту. Другий шар—валідація на сервері: парсимо AST згенерованого SQL (бібліотека sql-parser або pg_query для PostgreSQL), перевіряємо тип запиту та список таблиць.
Room та агент: локальна БД на Android
Якщо агент працює з локальними даними додатку через Room:
// Інтерфейс інструмента для агента
class DatabaseTool(private val db: AppDatabase) {
suspend fun executeQuery(sql: String): String {
return try {
// Тільки SELECT через SupportSQLiteDatabase
val cursor = db.openHelper.readableDatabase.query(sql)
cursor.toJsonArray().toString()
} catch (e: Exception) {
"""{"error": "${e.message}"}"""
}
}
}
SupportSQLiteDatabase.query() приймає сирий SQL—зручно для агента. Room DAO тут не підходить: вимагає фіксованих запитів на етапі компіляції.
Важливо: Room за замовчуванням не дозволяє raw queries на головному потоці. Все повинно бути усередині suspend fun або withContext(Dispatchers.IO).
Форматування результату
Агент отримав рядки з БД—потрібно повернути користувачу у зрозумілому вигляді, а не як JSON-масив. Передаємо результат запиту назад моделі з інструкцією форматувати:
Результат інструмента: [{"category":"food","total":"-15420"},{"category":"transport","total":"-8300"}]
→ Модель форматує: "За прошлий місяць ви витратили 154.20 BYN на їду та 83.00 BYN на транспорт"
Для числових даних добре працює запит до моделі на створення Markdown-таблиці—її легко отрендерити на мобілі через будь-який Markdown-парсер (Markwon на Android, AttributedString + кастомний рендер на iOS, flutter_markdown на Flutter).
Безпека: що обов'язково
- Параметризовані підзапити там, де можливо (навіть для SELECT)
- Whitelist таблиць та колонок, до яких дозволен доступ
- Ліміт результатів обов'язковий:
LIMIT 1000у валідаторі - Таймаут на виконання запиту (PostgreSQL:
SET statement_timeout = '5s') - Логування всіх сгенерованих запитів для аудиту
Етапи та сроки
Аналіз схеми БД та визначення доступних таблиць → розробка системного промпту з описанням схеми → реалізація SQL-валідатора на бекенді → інтеграція агентного цикла → форматування результатів → тестування на різноманітних користувацьких запитів → моніторинг якості генерації.
Для локальної SQLite/Room з 3–5 таблицями—2–3 тижні. Для серверного агента з PostgreSQL, валідатором та складною схемою—4–6 тижнів.







