AI · Automatizacija · Inženjering

Od ideje do AI proizvoda u produkciji: realan put

Piše Lazar MilićevićOctober 31, 20268 min čitanja
Developer workstation with code on screen, representing the path from idea to a production AI product

Većina AI proizvoda ne zaglavi na modelu. Zaglavi na stvarima oko modela: šema baze koja se menja treći put, autentikacija koja je "završena" tri puta, Docker slika koja radi samo na mom laptopu. Pišem ovo kao neko ko je više puta isporučio ceo stack od baze do UI-a (Next.js, Supabase, PostgreSQL, Docker), pa ću prvo reći gde se vreme zaista gubi, a tek onda kako bih ga ja danas rasporedio.

Gde se zaista gubi vreme (nije tamo gde mislite)

Najviše vremena ne odlazi na pisanje funkcionalnosti, nego na odluke koje sam odložio i koje su me kasnije stigle sa kamatom. U mom iskustvu to su četiri stvari: model podataka, granica između klijenta i servera, upravljanje tajnama i okruženjima, i deploy pipeline koji sam ostavio za "kasnije".

Prvih nekoliko dana osećaj je odličan. UI se brzo pojavi, demo radi, svi su zadovoljni. Onda krene prava izgradnja i svaka odluka koju sam preskočio postaje blokada. Zato je moj stav: prva nedelja ne sme da bude o UI-u. Mora da bude o skeletu koji može da se deployuje, makar radio samo jednu stvar.

Konkretno, ono što bih danas napravio pre bilo kog ekrana:

  • jedna tabela, jedan upit, jedan deployovan endpoint koji ih spaja
  • migracije u verzionisanim fajlovima, ne klikanje po dashboardu
  • jedno okruženje za razvoj i jedno za produkciju, sa odvojenim ključevima od prvog dana
  • CI koji pravi Docker sliku i pušta je na pravo mesto, čak i kad aplikacija radi samo "hello world"

Ovo deluje sporo, a u praksi je najbrži put. Svaka naknadna promena se onda testira kroz isti put kojim će ići u produkciju.

Baza prva: PostgreSQL i Supabase

Ako AI proizvod ima bilo kakvu memoriju, retrieval ili istoriju, baza je zapravo proizvod. Zato šemu pišem pre nego što nacrtam prvi ekran, i sve ide kroz migracije.

Meni lično Supabase ima smisla iz jednog praktičnog razloga: radim sa PostgreSQL-om, pa mogu da izađem sa platforme ako zatreba, i ono što naučim važi svuda. Za AI deo najvažnija je ekstenzija pgvector, koju Supabase dokumentacija opisuje kao Postgres ekstenziju za vektorsku pretragu sličnosti, a embeddinge možete čuvati za RAG. Naziv ekstenzije je vector.

Tri stvari koje me je koštalo vremena da naučim:

  1. Dimenzija kolone mora da odgovara modelu embeddinga. Ako promenite embedding model, menjate i kolonu i morate ponovo da generišete sve vektore. Supabase dokumentacija kao primer navodi 384 dimenzije za open-source gte-small, a vodič za automatske embeddinge koristi OpenAI text-embedding-3-small sa 1536 dimenzija i halfvec(1536) kolonom. Zaključak: izbor embedding modela je odluka o šemi, ne detalj implementacije.
  2. supabase-js ne zna pgvector operatore. Zvanična dokumentacija objašnjava da se klijent povezuje preko PostgREST-a, koji ne podržava operatore sličnosti, pa upit pakujete u Postgres funkciju i zovete je preko rpc(). To je u praksi dobro: pretraga živi u bazi, pored podataka, a ne razbacana po aplikaciji.
  3. Indeks nije opcioni. Za vektorsku pretragu u većoj tabeli koristim HNSW indeks. Za halfvec postoji ograničenje broja dimenzija koje indeks podržava, što je za uobičajene modele sasvim dovoljno, ali tačnu granicu proverite u Supabase vodiču za vektorske kolone.

Skica funkcije koju pozivam sa servera (nazivi su ilustrativni):

create or replace function match_documents(
  query_embedding halfvec(1536),
  match_count int default 5
)
returns table (id bigint, content text, distance float)
language sql stable
as $$
  select id, content, embedding <=> query_embedding as distance
  from documents
  order by embedding <=> query_embedding
  limit match_count;
$$;

Zanimljiv je i zvanični obrazac za automatske embeddinge: pgvector, pgmq (red za zahteve i ponovne pokušaje), pg_net, pg_cron i Edge Functions, pokrenuti trigerima. Ideja je da se embedding ne računa u zahtevu korisnika, nego asinhrono, sa retry-jem. To je isti princip kao u svakom sistemu koji radi bez nadzora: sporo i nepouzdano radi u pozadini, a korisnik vidi brz odgovor.

Još jedna napomena o verzijama, jer se tu lako zaspi. PostgreSQL projekat svaku major verziju podržava pet godina od prvog izdanja, a tačne datume završetka podrške za svaku verziju proverite na stranici o verzionisanju. Ako vam je negde još stara verzija blizu kraja podrške, to je stavka za planiranje, ne za "nekad". Izdanja u fazi release candidate-a ne biram za novi proizvod dok ne izađe finalna verzija.

Next.js: manje magije, više granica

Next.js sam birao zato što mi daje ceo proizvod u jednom repozitorijumu: UI, API rute, server akcije. Ta udobnost ima cenu, i cena je zamućena granica između klijenta i servera.

Za razliku od pre godinu dana, okvir se promenio dovoljno da vredi proveriti šta dobijate po defaultu. Od verzije 16 Turbopack je podrazumevani bundler (webpack se bira sa next build --webpack), a u novijim 16.x izdanjima menjaju se i podrazumevana podešavanja za nove projekte, uključujući kešovanje. Detalji su u zvaničnim beleškama: Next.js 16, Next.js 16.3 i Next.js 16.4.

Šta mi je to promenilo u praksi? Kešovanje je sada ugrađeno u model, pa moram unapred da odlučim šta je statično, šta dinamično, a šta personalizovano. Za AI proizvod to je važno iz jednog razloga: odgovori modela su skupi i spori, pa odluka šta se sme keširati direktno utiče na račun i na doživljaj. Ali pravilo koje sam naučio na teži način je da keš nikad ne dodirujem za sadržaj koji zavisi od korisnika, dok ne postavim autorizaciju kako treba.

Moja pravila za granicu klijent/server:

  • Svi pozivi ka LLM provajderu idu isključivo sa servera. Ključ nikad ne dospeva u klijentski bundle. Zvuči očigledno, a viđao sam produkcijske greške baš tu.
  • Dugotrajni zadaci ne rade u zahtevu. Generisanje koje traje deset sekundi i više ide u red, a UI pita za status ili prima događaje. Zahtev koji visi je prva stvar koja puca pod opterećenjem.
  • Validacija na ulazu i izlazu. Šema (npr. Zod) za sve što ulazi, a odgovor modela tretiram kao nepouzdan ulaz, jer to i jeste.

Što se tiče zahteva za verzije Node.js i TypeScript-a, proverite ih u zvaničnim beleškama za Next.js 16 pre nego što fiksirate verziju u Docker slici.

Docker: deploy koji nije iznenađenje

Docker je u ovoj priči način da "radi kod mene" postane "radi svuda". Za Next.js koristim output: 'standalone' u next.config i višestepeni Dockerfile sa stepenima deps, builder i runner. Detalj koji svakog ko prvi put radi ovo uhvati: standalone izlaz ne uključuje public/ ni .next/static, pa ih ručno kopirate u runner stage. Ako ih zaboravite, aplikacija se podigne, a stranice izgledaju polomljeno. (Detaljniji primer Dockerfile-a ima i ovaj tehnički blog, a poklapa se s onim što sam i sam doživeo.)

Skica, bez pretenzije da je finalna:

FROM node:20-slim AS deps
WORKDIR /app
COPY package*.json ./
RUN npm ci

FROM node:20-slim AS builder
WORKDIR /app
COPY --from=deps /app/node_modules ./node_modules
COPY . .
RUN npm run build

FROM node:20-slim AS runner
WORKDIR /app
ENV NODE_ENV=production
COPY --from=builder /app/.next/standalone ./
COPY --from=builder /app/.next/static ./.next/static
COPY --from=builder /app/public ./public
USER node
CMD ["node", "server.js"]

Verziju Node-a u slici uskladite sa onim što zvanična dokumentacija traži, a ne sa onim što je slučajno na vašem laptopu. Pokretanje kao neprivilegovani korisnik i healthcheck su navike koje preporučujem, a ne nešto što framework zahteva.

Najveća greška koju sam ovde pravio: Docker sam pravio na kraju. Tada se otkrije da build zavisi od lokalnih fajlova, da se promenljive okruženja čitaju u build-u umesto u runtime-u, i da migracije niko ne pokreće. Zato je Dockerfile u mom skeletu prvog dana, pre prve prave funkcionalnosti.

Šta se zapravo dešava između faza

Tabela koja sumira gde sam gubio vreme i kako bih to sada rešio:

Faza Šta sam ranije radio Šta bih sada radio
Model podataka Šema "u hodu", ručno u dashboardu Migracije u repozitorijumu od prvog dana
Embeddinzi Model birao kasnije Model i dimenziju fiksiram pre prve tabele
Granica klijent/server Rešavao uz bagove Pravila definisana unapred, ključevi samo na serveru
Dugi zadaci Radili u zahtevu Red i asinhrona obrada od početka
Deploy Ostavljen za kraj Deployovan skelet u prvoj nedelji
Okruženja Jedan ključ, jedna baza Dev i prod razdvojeni od starta

Ništa od ovoga nije zanimljivo i baš zato se preskače. A upravo to je razlika između demoa i proizvoda.

Šta bih ja uradio

Kad bih sutra počinjao novi AI proizvod od nule, redosled bi bio ovakav:

  1. Dan 1 do 3: šema u migracijama, jedan endpoint, Dockerfile, CI koji deployuje. Aplikacija radi samo jednu stvar, ali radi u produkciji.
  2. Nedelja 1: autentikacija i autorizacija, razdvojena okruženja, upravljanje tajnama. Tek tu počinjem da razmišljam o modelu.
  3. Nedelja 2: AI deo iza interfejsa koji mogu da zamenim. Provajder modela je detalj, ne temelj, i izbor se menja brže nego što očekujete.
  4. Pre prvog korisnika: logovanje svakog poziva modela, ograničenja potrošnje i način da sve ugasim jednim prekidačem. Sistem koji radi bez nadzora mora da ima i način da se zaustavi.

I jedan stav: ne počinjem od UI-a. UI je najlakši deo za dorađivanje i najteži za opravdavanje kad ispod njega ništa ne radi. Cene platformi i planova ovde namerno ne navodim; proverite ih direktno na zvaničnim stranicama dobavljača, jer se menjaju i izvori se razilaze.

Zatvaranje

Put od ideje do deployovanog proizvoda nije tehnički složen, ali je disciplinovan: manje opcija, ranije odluke, deploy pre funkcionalnosti. Ako gradite nešto slično u Srbiji ili regionu i želite da razmenimo mišljenje o arhitekturi ili redosledu isporuke, javite se preko lazar-milicevic.com/#contact.

Često postavljana pitanja

Zašto AI proizvodi najčešće zaglave pre produkcije?

Po mom iskustvu, ne zaglave na samom modelu nego na stvarima oko njega: šema baze koja se stalno menja, autentikacija, upravljanje tajnama i okruženjima, i deploy pipeline koji je ostavljen za kasnije. Prvih nekoliko dana osećaj je odličan jer se UI brzo pojavi, a onda svaka preskočena odluka postaje blokada. Zato prvu nedelju posvećujem skeletu koji može da se deployuje, a ne ekranima.

Šta treba napraviti pre prvog ekrana u AI aplikaciji?

Pre bilo kog ekrana pravim jednu tabelu, jedan upit i jedan deployovan endpoint koji ih spaja. Migracije držim u verzionisanim fajlovima, a ne klikanjem po dashboardu. Odvajam okruženje za razvoj i produkciju sa različitim ključevima od prvog dana. Dodajem i CI koji pravi Docker sliku i pušta je na pravo mesto, čak i kad aplikacija radi samo hello world.

Kako se u Supabase-u koristi pgvector za RAG?

Embeddinge čuvam u koloni tipa vector (ili halfvec) pomoću pgvector ekstenzije, koja radi vektorsku pretragu sličnosti u PostgreSQL-u. Pošto supabase-js ne zna pgvector operatore, upit pakujem u Postgres funkciju i pozivam je preko rpc(). Za veće tabele koristim HNSW indeks. Prednost je što pretraga živi u bazi, pored podataka.

Zašto je izbor embedding modela odluka o šemi baze?

Dimenzija vektorske kolone mora da odgovara modelu embeddinga. Supabase dokumentacija kao primer navodi 384 dimenzije za gte-small i 1536 za OpenAI text-embedding-3-small. Ako promenim model, moram da promenim kolonu i ponovo generišem sve vektore. Zato izbor embedding modela tretiram kao arhitektonsku odluku, a ne kao implementacioni detalj.

Šta je važno kod Next.js-a 16 za AI proizvod?

Od verzije 16 Turbopack je podrazumevani bundler, a u novijim 16.x izdanjima menjaju se i podrazumevana podešavanja za nove projekte, uključujući kešovanje. Zato unapred odlučujem šta je statično, dinamično ili personalizovano, jer odgovori modela su skupi i spori. Keš ne diram za sadržaj koji zavisi od korisnika dok ne postavim autorizaciju, a pozive ka LLM provajderu držim isključivo na serveru.

Lazar Milicevic

Lazar Milićević

Senior Technical Engineer. Gradim AI automatizaciju, GenAI/LLM sisteme i cloud arhitekturu — autonomne sisteme koji rade dok spavaš. Osnivač BizFlowAI.

Gradiš nešto teško sa AI-jem ili automatizacijom? Otvoren sam za razgovor.

Javi se

← Svi postovi