Побудував Zero Trust AI-систему диспетчеризації для логістики. Рознесіть мою архітектуру!
Привіт, спільното!
Останнім часом пиляю рішення для логістики — систему автоматизації чатів із водіями та клієнтами на базі LLM. Головна проблема цієї ніші: компанії панічно бояться витоків даних (GDPR/NDA) та «галюцинацій» штучного інтелекту.
Щоб вирішити це, я побудував параноїдальну архітектуру. Буду вдячний за суворий код-рев’ю та критику підходу від Senior-інженерів та архітекторів.
Стек: FastAPI, PostgreSQL, SQLAlchemy, React, Gemini 3.1 Flash.
Як працює наша архітектура:
- Data Scrubber (Анонімізація): Жодне повідомлення не йде до LLM напряму. FastAPI перехоплює текст, вирізає всі PII (телефони, імена, email) і замінює їх на токени (наприклад,
[PHONE_0]). Реальні дані лягають у тимчасовий зашифрований Vault у БД. - Ізоляція LLM: Модель отримує лише «чистий» контекст і відповідає виключно через жорсткі Pydantic-схеми (Structured Outputs). Жодної свободи генерувати вільний текст.
- Human-in-the-Loop (HITL): Якщо LLM бачить у схемі складний вантаж (ADR) або негатив, вона повертає
requires_human_intervention: true. Система миттєво відрубає ШІ і переводить чат на живого диспетчера. - Де-анонімізація: При видачі відповіді бекенд підставляє реальні дані з Vault назад у текст. LLM буквально ніколи не бачить справжніх номерів.
Питання до інженерів:
- Чи бачите ви очевидні дірки в такому патерні Anonymization/Deanonymization? Де б ви шукали вразливість?
- Для дашборду диспетчера (перехоплення алертів) зараз юзаю short-polling (React Query кожні 5 сек). WebSockets здалися оверкілом для MVP, але чи не покладе пулінг базу при різкому масштабуванні (сотні одночасних чатів)?
Буду радий будь-якому фідбеку!

21 коментар
Додати коментар Підписатись на коментаріВідписатись від коментарівКілька технічних спостережень з нашого досвіду з аналогічними системами:
NATS message bus для routing між компонентами добре масштабується, але envelope-рівнева security policy (які subjects доступні якому агенту) виявляється зручнішою за filesystem-level whitelist. Кожен агент отримує доступ тільки до своїх subjects через NATS authorization rules — атомарно і без зміни коду агента.
Validation gate між pipeline-кроками (retrieval → scoring → handoff) важливіший ніж здається: один некоректний structured output на intermediate step розповсюджується вниз і його важко відловити без явного schema check на кожному boundary.
Zero Trust в контекстіLLM-інструментів: ToolSecurityPolicy з filesystem jail (дозволяти тільки read/write в межах заданого root path) + approval prompt для нових tool calls — мінімальна реалізація займає 50 рядків і суттєво зменшує blast radius галюцинацій.
Миколо, дякую за фідбек! Дуже круті поінти, особливо приємно обговорити архітектуру з людиною, яка реально працює з LLM в продакшені.
Щодо NATS — ми поки туди не лізли. У нас пайплайн зараз досить лінійний (FastAPI -> Scrubber -> LLM -> Validation -> DB), і агенти між собою складно не спілкуються, тому обходимось без брокерів. Але якщо в майбутньому будемо масштабуватись і додавати окремих агентів (наприклад, для генерації інвойсів), то NATS з його envelope-level security — це прям те що треба, точно заберемо ідею в беклог.
Щодо Validation gate — отут стовідсотковий плюс. Ми на ці граблі наступили ще на стадії прототипу, коли модель могла видати напівпорожній JSON або придумати своє поле. Зараз у нас стоїть жорсткий гейт на Pydantic v2. Якщо Gemini повертає щось не те (наприклад, губить поле з вагою або ліпить кривий тип) — Pydantic кидає ValidationError, пайплайн стопається і запит просто падає на живого диспетчера. Жодна галюцинація чи кривий стейт в базу не пролізе.
А стосовно filesystem jail і тулів — нам тут трохи простіше. Наша LLM взагалі не має доступу до function calling. Вона працює суто як stateless-парсер: бере очищений текст від скруббера, дістає з нього логістичні параметри і віддає JSON. Вона нічого не пише на диск і не робить зовнішніх API-запитів. Відповідно, весь blast radius обмежується лише криво заповненим JSON’ом, який ми миттєво відловлюємо тим же Pydantic на попередньому кроці)
Ще раз дякую за коментар, дуже круто, коли можна так предметно обговорити технічну внутрянку!
Яку проблему, власне, хотіли вирішити? Водій у довільному вигляді повідомляє про інцидент, воно якось транслюється у текст, маршрутизується комусь, цей хтось щось робить, і потім щось маршрутизується зворотньо до водія?
Дмитре, ви влучно описали те, як воно зазвичай працює в логістиці зараз) Але наша головна мета — якраз максимально прибрати з цього ланцюжка отого «когось, хто щось робить».
Проблема в тому, що диспетчери просто тонуть у рутинних апдейтах статусів. А логістичні компанії не можуть взяти і прикрутити собі умовний ChatGPT, бо це пряме порушення GDPR і ризик зливу даних.
Тому в нашому рішенні флоу трохи інший:
Водій кидає інфу як йому зручно (голосове, сленг, текст). Модель тут працює не як транслятор, а як NLU-процесор — розпізнає інтент і витягує сутності. Далі вже наш бекенд сам апдейтить базу (TMS) і відповідає водію. Людина на цьому етапі взагалі не бере участі.
Живий диспетчер (Human-In-The-Loop) підключається тільки десь у 20% складних кейсів — ДТП, ADR-вантаж або якщо комунікація йде кудись не туди. При цьому алгоритм одразу передає йому весь готовий контекст.
Щодо приватності та Zero Trust: щоб усе це працювало легально, ми імплементували Data Scrubber та Vault. Ніякі реальні імена, телефони чи номери машин до LLM фізично не летять — вони маскуються на льоту і відновлюються вже всередині нашого ізольованого контуру.
Коротше кажучи, даємо бізнесу автономію рівня LLM, але з гарантіями безпеки на рівні інфраструктури.
Мабуть головне — я не розробник, і якихось корисних порад дати вам не зможу. Те, що ви описали, зовсім не схоже на те, про що зазвичай йдеться, коли згадують Zero Trust, а сильно схоже на NLP та анонімізацію. Хоча б тому, що ви «приймаєте на віру» те, що це «саме той» водій та «саме той» замовник. Мені здається що «Zero Trust» та «периметр» це взагалі поняття не сумісні — кожна нода має бути ізольована і «ставити під сумнів» легітимність даних та їх джерела. Сама задача в цілому також виглядає не цілісною — ви хочете транслювати хаос «брудних даних» у детерміновані стейти, а те, що не вдалося транслювати, або якщо стейт не відомий, то підключити людину? Мені здається що це вирішується трьома кнопками: «приїхав», «не приїхав», «якась інша фігня». Якщо «якась інша фігня» — підключити людину. Скоріше за все це вкладається у ті 20 відсотків.
Дякую за коментар! Погоджуюсь, з академічної точки зору це не зовсім той Zero Trust, до якого всі звикли на рівні мереж чи доступу до серверів. Ми застосували цей термін скоріше до нашої взаємодії із зовнішнімиLLM-ками та ізоляції клієнтських даних.
Суть у тому, що ми виходимо з принципу: «ніколи не довіряй публічній моделі чутливу інфу». Тому скруббер вирізає всі PII (телефони, імена) ще до того, як вони полетять в умовний Gemini. Ну і плюс на рівні бази у нас налаштований жорсткий RLS (Row-Level Security), щоб тенанти фізично не могли перетнутися даними. Згоден, що для маркетингу назва звучить трохи гучніше, але суть підходу вона передає)
Щодо трьох кнопок — якби задача була тільки в трекінгу статусу поїздки, то 100% так би і зробили. Кнопковий UX там рулить. Але наш фокус саме на етапі переговорів та кваліфікації ліда.
У реальному житті водій не буде клікати кнопки чи заповнювати формочки, коли йому треба швидко запропонувати машину. Він скоріше кине в месенджер суцільний текст типу: «є вільна машина в Любліні на завтра, рефка до −18, чи візьмете під свою хімію?». І отут якраз починається хаос. Наша задача — витягнути з цього неструктурованого тексту чітку JSON-схему (тоннаж, температуру, клас небезпеки ADR) і миттєво відсіяти нецільові запити за бізнес-правилами конкретної компанії. Тобто ми автоматизуємо рутину диспетчера на стадії онбордингу вантажу, де кнопки просто не працюють. Ну а якщо модель не впевнена у вхідних даних — от тоді вже чат падає на живу людину.
Здається нарешті зрозумів що ви робите! У вас є борд з замовленнями, які ви продаєте водіям, це ваша бізнес-модель. Головна проблема — щоб водій та замовник не могли взаємодіяти без вашої участі. Під диспетчеризацією ви маєте на увазі обмін бізнес-даними, які критично важливі для виконання рейсу, але вони не повинні дати можливість водієві та замовнику взаємодіяти без вашої участі. Така собі «Тачку!» але для вантажів. Бізнес-модель захищена тим, що водії — фрілансери, а замовники не мають прогнозованих обсягів перевезень.
О, цікава гіпотеза, але ви трохи промахнулися з нашою бізнес-моделлю) Ми взагалі не маркетплейс і не біржа вантажів типу «Тачки». Ми не зводимо водіїв із замовниками і не беремо відсоток за фрахт.
Ми — чистий B2B SaaS для логістичних компаній та диспетчерських агенцій. Тобто ми просто продаємо їм софт (інфраструктуру з ШІ).
Щодо приховування контактів (наш PII Scrubber): ми ховаємо номери та імена не один від одного, а від публічної LLM. Для будь-якої логістичної компанії клієнтська база — це найголовніший актив. Вони ніколи не віддадуть реальні контакти своїх водіїв чи замовників в умовний ChatGPT через API на розтерзання (і для подальшого навчання моделей).
Тому наш бекенд «на льоту» вирізає всі чутливі дані, замінює їх на токени (типу [PHONE_0]), відправляє цей стерильний текст в модель суто для того, щоб витягнути логістичні параметри (тоннаж, маршрут, ADR), і отримує безпечну відповідь.
А от у дашборді самого диспетчера (нашого B2B клієнта) ці дані деанонімізуються назад. Він бачить усі реальні номери і повністю контролює діалог.
Тобто наша ціль — не захистити свою маржу як посередника, а дати логістам сек’юрний інструмент автоматизації, щоб один їхній диспетчер міг обробляти не 50, а 500 чатів одночасно без ризику злити корпоративну базу.
Ну, тоді у мене повний злам шаблонів та (вчергове) втрата віри у людство — тобто водій замовлення отримувати хоче, але йому в падлу один раз натицяти параметри свого транспорту та/або підтверджувати своє бажання замовлення взяти? І для цього треба ЛЛМ, щоб перетворити потік свідомості водія у застосовні дані. А де тут плавала клієнтська база? Нахіба водію на етапі пошуку знати чий саме вантаж треба перевезти? А коли вже повезе — один хрін документи на руках матиме. Події по дорозі — дані про власника вантажу до чого? Номеру рейсу не достатньо хіба? Водій, начебто, має бути сам зацікавлений, щоб те, що він водій і виконує перевезення, на кожному паркані було написано. Власник вантажу — про логістичні компанії начебто також не на таємних зборах масонів дізнається. Суцільні загадки!
Дмитре, жодного зламу шаблонів, це просто сувора економіка логістичного бізнесу) Ви мислите з позиції B2C-додатків (як таксі), але у B2B Freight Brokerage інші правила гри.
Щодо «в падлу натицяти»: Логістика — це висококонкурентний ринок, де йде боротьба за вільний транспорт. Водій часто за кермом, переписується з5-ма різними диспетчерами у різних месенджерах. Якщо диспетчер А дає йому форму на 10 полів, а диспетчер Б каже «просто кинь мені текстом/голосом, де ти і що в тебе», водій поїде з диспетчером Б. LLM тут потрібна не для водія, а для диспетчера — щоб миттєво оцифрувати цей хаос, виграти час і забрати машину першим, не змушуючи водія робити зайві кліки. UX без тертя — це конверсія.
Щодо клієнтської бази та «нахіба ховати»: Логістична компанія (експедитор) не має ні своїх фур, ні своїх заводів. Їхній єдиний актив і хліб — це інформація. Вони заробляють свою маржу саме на тому, що знають, ХТО везе, і знають, КОМУ треба перевезти. Якщо водій (перевізник) на етапі торгів отримує прямі контакти власника вантажу, експедитор просто виключається з ланцюжка при наступному рейсі. Тому анонімізація даних (PII Scrubber) у нашому ШІ-диспетчері — це базовий захист комерційної таємниці брокера, щоб AI випадково не «злив» контакти заводу водієві під час діалогу, або навпаки.
Ну во-первых, ты не раскрыл зачем хранить персональные данные, если только для того чтобы вставить их в ответ на запрос который их и дал, то это абсурд, если они куда-то идут, то опять же передай их туда и это будут проблемы той системы, которая с ними работает. Во-вторых, если ллм занимается шаблонной работой, то она не нужна. В-третьих, тут нет зеротраста, ты просто редачишь текст. Все сервисы доверяют друг другу полностью. 2й вопрос бессмысленный — сделай бенчмаркинг и посмотри на результат. Может тебя и как в мвп устроит
Дякую за розгорнутий коментар. Давайте розберемо по пунктах, бо тут є фундаментальні непорозуміння того, як працюють реальні логістичні системи.
По-перше, щодо персоналки і «абсурду». Суть не в тому, щоб просто повернути водію його ж дані. Водій присилає хаотичний текст із PII, який нам треба розпарсити і покласти в базу (TMS) клієнта. LLM тут працює суто як NLU-процесор. Scrubber маскує дані, LLM розпізнає намір і віддає JSON з токенами (типу [PHONE_0]), а бек вже підставляє реальні значення з пам’яті і пише в БД. Дані закритий контур не покидають, тому з GDPR тут все ок.
По-друге, щодо шаблонів. Повідомлення від водіїв — це взагалі не шаблони) Це сленг, обдруківки, голосовухи в стилі «стою на кордоні, колесо відпало, причіп такий-то, набери Саню». Будь-які регулярки чи жорсткі флоу тут вмирають одразу. LLM якраз бере цей хаос і робить з нього валідний структурований об’єкт.
По-третє, про Zero Trust. Сам Scrubber — це Data Privacy, так. А от Zero Trust у нас реалізований на рівні інфраструктури. Сервіси дефолтно не довіряють одне одному. API не має прямого доступу до бази чи LLM. Секретами рулить Vault Agent (через AppRole), який динамічно їх ротує через in-memory (tmpfs) volume. Плюс у базі налаштований PostgreSQL RLS. Навіть якщо API скомпрометують, без підписаного JWT і правильного tenant_id база фізично не віддасть чужі рядки.
А з бенчмарками згоден на 100%. Власне, щоб не ловити мережеві затримки на кожному запиті до Vault, ми й затягнули Sidecar патерн — тепер читаємо все миттєво з оперативки.
так, а в чем тогда суть вопроса, если есть ± стандартный зеро траст и нет проблем с GDPR?
Суть якраз у тому, щоб показати концепт із практики та завалідувати архітектуру об сильне технічне ком’юніті перед запуском бета-тестування та масштабуванням на B2B-клієнтів.
Бо в теорії «стандартний Zero Trust» і GDPR-compliance звучать просто. А на практиці, коли доходить до інтеграції LLM, більшість на ринку зараз просто ліпить базовий враппер над API і радісно зливає туди всю клієнтську базу без жодного маскування.
Мені хотілося обговорити саме практичні нюанси реалізації (зв’язка Vault Agent через tmpfs + RLS + Scrubber в одному пайплайні) і послухати думки людей з досвідом, щоб знайти можливі «сліпі зони».
Тож дякую за краш-тест і конструктивний діалог! 🤝
Хаккер ламає Data Scrubber, запускає свій код, який читає доступи до Vault і зливає звідти всі дані. Наприклад.
Слушне зауваження, від RCE жоден код не застрахований на 100%. Але ми якраз будували архітектуру з розрахунком на Assume Breach (периметр рано чи пізно можуть пробити). Тому «злити всі дані» буде проблематично:
По-перше, у Vault налаштовані жорсткі політики. Навіть якщо поламати Scrubber і прочитати /vault/secrets/config.json чи знайти токен агента, витягнути з Vault щось інше не вийде. Токен просто не має доступу до чужих ключів (Least Privilege).
По-друге, база. Припустимо, кредоси дістали, але там працює Row-Level Security. Без підписаного JWT і правильного Tenant ID Postgres фізично не віддасть рядки інших клієнтів, фільтрація йде на рівні ядра.
Ну і третє — самі контейнери. API-контейнер крутиться від non-root юзера і з read_only: true. Закинути і виконати якийсь сторонній payload буде вкрай складно, коли файлова система тупо закрита на запис.
Коротше кажучи, хакер дійсно може скомпрометувати конкретний вузол, але архітектура не дасть йому розповзтися далі (Lateral Movement). Що скажете про такий сетап?
А як же бекенд вставляє назад реальні дані, якщо у нього немає доступів до RLS?
Слушне питання! Фішка в тому, що бек не втрачає доступ до бази цілком — його доступ просто динамічно обмежується під конкретного клієнта на старті кожної транзакції.
Флоу працює десь так:
Приходить запит від юзера (водія або диспетчера). Ми дістаємо його tenant_id з JWT і відразу на рівні з’єднання з БД робимо SET LOCAL app.current_organization_id = ’...’.
В роботу вмикається Scrubber: він замінює чутливі дані на маски (наприклад, номер -> [PHONE_1]).
Відправляємо анонімізований текст в LLM, отримуємо відповідь.
Ми зберігаємо в базу сам лог діалогу і поруч у захищене поле (vault_snapshot) кладемо мапу підстановок {«[PHONE_1]»: «+380...»}.
Коли авторизований диспетчер завантажує лог, бекенд прямо в пам’яті робить зворотну підстановку (де-маскування) і віддає на фронтенд чисті дані.
Чому це безпечно при RCE: Оскільки для будь-якої транзакції у нас жорстко заданий правильний tenant_id (витягнутий з криптографічно підписаного JWT), RLS на рівні ядра Postgres фільтрує всі таблиці.
Тобто, якщо зловмисник через RCE спробує виконати довільний запит типу SELECT * FROM messages (щоб вкрасти ті самі vault_snapshots), Postgres віддасть йому виключно дані того tenant_id, під яким зараз обробляється скомпрометований запит. Злити базу інших клієнтів (горизонтальне переміщення) він фізично не зможе. Якось так :)
Дискавері/брутфорс тентантів?
Тут нас рятує математика) Усі tenant_id у базі — це UUIDv4. Тому класичний брутфорс відпадає одразу.
Навіть якщо уявити найгірший сценарій: хакер має RCE, дорвався до відкритого з’єднання з БД і починає в циклі слати SET LOCAL rls.tenant_id = ’рандомний_uuid’ — вірогідність влучити в існуючого клієнта прагне до нуля. Йому просто життя не вистачить перебрати варіанти.
Щодо «дискавері» — у скомпрометованому контексті немає жодної таблиці, звідки можна було б витягнути загальний список цих ID (для загальних таблиць теж працює RLS або жорсткі гранти).
Плюс, такий шквал запитів «навмання» моментально підніме алерти в моніторингу бази (наприклад, через аномальну кількість помилок чи нетипове навантаження). Тобто без знання конкретного UUID іншого тенанта зловмисник залишається сліпим і надійно замкненим у своїй пісочниці.
Тада ок