AI · Automatizacija · Inženjering

Prompt i context engineering za produkciju

Piše Lazar MilićevićJuly 1, 20269 min čitanja
Developer workstation with code on screen illustrating prompt and context engineering for production

Prošle nedelje sam otvorio staru granu na projektu i pokrenuo eval skriptu za jednog agenta koji radi klasifikaciju support tiketa. Isti prompt, isti model, ista temperatura. U testu 94% tačnosti. Na uzorku od 500 realnih tiketa iz prethodnih 7 dana, 71%. Razlika nije bila u modelu. Razlika je bila u kontekstu koji sam mu davao u produkciji, i u pretpostavkama koje sam ugradio u prompt a da ih nisam zapisao.

Ovo je tekst o tome zašto se to dešava i šta konkretno radim da LLM u produkciji bude predvidiv sistem, a ne lutrija.

Zašto isti prompt radi u testu a pada uživo

Kratak odgovor: test set je uvek čistiji nego produkcija. Prompt koji "radi" u testu je zapravo prompt koji se poklopio sa distribucijom ulaza koje ste videli dok ste ga pisali. Čim uživo stigne tiket na tri jezika, sa emoji-jem u naslovu, forwardovan iz Outlook-a sa 40 linija history-ja, ili user koji piše u trećem licu o sebi, prompt počinje da improvizuje. A improvizacija LLM-a je ono što ne želimo u produkciji.

Ono što ja gledam kad dođe do ovog jaza:

  • Ulaz je drugačiji nego u testu (duži, kraći, prljaviji, drugi jezik, HTML umesto plain text).
  • Kontekst oko prompta je drugačiji (RAG vraća drugačije chunk-ove, tool call vraća prazno, timestamp je noć umesto dan).
  • Skriveni state postoji, a niste ga uzeli u obzir (system prompt sa prethodne poruke, cache-ovan odgovor, drugi agent koji je promenio memoriju).
  • Kriterijum uspeha u testu nije bio isti kao u produkciji. U testu ste gledali "da li je odgovor tačan". Uživo vas zanima "da li je odgovor tačan, kratak, na srpskom, bez halucinirane cene i pod 800 tokena".

Sve četiri stavke su rešive, ali ne magijom nad promptom. Rešive su prompt + context engineering-om koji se tretira kao inženjerska disciplina.

Kontekst nije prompt: razdvojite ih na 4 sloja

Najčešća greška koju viđam je da tim ima jedan džinovski prompt string u kome je sve pomešano: uputstvo, primeri, korisnički input, RAG rezultati, tool schema, format izlaza. Kad nešto pukne, ne znate šta ste tačno promenili.

Ja kontekst uvek delim na četiri sloja koji imaju različit lifecycle:

Sloj Šta ide unutra Kad se menja
System instructions Uloga, tvrda pravila, format izlaza, jezik Retko (verzija)
Task template Struktura zadatka, few-shot primeri, output schema Po verziji prompta
Retrieved context RAG rezultati, tool outputs, memorija Po zahtevu
User input Sirov input od korisnika, sanitizovan Po zahtevu

Kad ovo razdvojim, testiranje postaje moguće. Menjam samo jedan sloj u eval-u i vidim koji je uzrok pada tačnosti. Takođe mogu da cache-ujem system + task template i platim manje tokena. Anthropic i OpenAI oba podržavaju prompt caching, i ovo razdvajanje je preduslov da caching uopšte radi kako treba, jer sve što ide u cache mora biti prefiks koji se ne menja.

Napišite prompt kao spec, ne kao poemu

Prompt u produkciji nije copywriting. To je spec. Ako inženjer koji vas menja ne može da pročita prompt i tačno kaže šta se očekuje na svaki mogući ulaz, prompt je nedovršen.

Struktura koju koristim skoro uvek izgleda ovako:

<role>
Ti si klasifikator support tiketa za SaaS firmu.
Odgovaraš iskljucivo validnim JSON-om po schema-i ispod.
</role>

<rules>
1. Kategorija mora biti tacno jedna od: billing, bug, feature, other.
2. Ako tiket sadrzi vise problema, biraj primarni (prvi opisani).
3. Ako je tekst kraci od 10 karaktera, kategorija je "other" i confidence 0.
4. Nikada ne izmisljaj polja koja nisu u schema-i.
</rules>

<output_schema>
{
  "category": "billing" | "bug" | "feature" | "other",
  "confidence": number (0.0 - 1.0),
  "reasoning": string (max 200 char)
}
</output_schema>

<examples>
... 3-5 primera koji pokrivaju ivicne slucajeve, ne prosek ...
</examples>

<ticket>
{{user_input}}
</ticket>

Tri stvari koje su mi konkretno smanjile broj grešaka:

  1. Eksplicitna pravila za ivicne slučajeve, ne za očigledne. Prompt ne treba da kaže "budi tačan". Prompt treba da kaže "ako tekst nema glagol, kategorija je other". Model već zna da bude tačan. Ne zna vaše konvencije.
  2. Few-shot primeri biraju se sa dna distribucije, ne sa vrha. Uzmem 5 primera koji su najteži za mene lično, ne 5 kanoničnih. Kanonične model već rešava sam.
  3. Output schema je JSON schema, ne opis. Kad model vidi tipizirano polje, verovatnoća da ga pogrešno formatira drastično pada, pogotovo uz structured output mode koji sada podržavaju svi ozbiljni provajderi.

Context engineering: manje je više, i to merljivo

Postoji zabluda da više konteksta znači bolji odgovor. U praksi, posle nekog praga (koji zavisi od modela), dodatni kontekst degradira kvalitet. Ovo je poznato kao "lost in the middle": informacije koje ste ubacili sredinom konteksta model tretira slabije nego one na početku i kraju.

Šta radim konkretno:

  • RAG vraća top-k, ali ja pre finalnog poziva radim rerank (Cohere rerank ili lokalni cross-encoder). Ako je initial recall 20 chunk-ova, u prompt ide 4-6 posle rerank-a. Prosečno sam ovim smanjio troškove za oko trećinu, a tačnost se u većini slučajeva ne pomeri ili blago poraste.
  • Hybrid search (BM25 + vektorska + RRF fuzija) je skoro uvek bolji od čiste vektorske za tehničke domene gde postoje ključne reči tipa "error code 4032".
  • Chunk-ovi imaju metadata koja ide u prompt eksplicitno: source, datum, sekcija. Bez ovoga, model halucinira izvor kad ga pitate "odakle znaš".
  • Prazan retrieval nije prazan prompt. Ako RAG ne vrati ništa, moj prompt ne šalje prazan string, već eksplicitan token <retrieved>none</retrieved> i pravilo "ako nema retrieved konteksta, odgovori 'ne znam' i ne pogađaj". Ovo je jedna od najisplativijih linija koje sam ikad dodao.

Ako gradite RAG i niste postavili baseline sa BM25 pre nego što ste dodali vektore, ne znate koliko vam vektori zapravo doprinose. Više puta sam video timove koji su platili embeddinge, Pinecone i rerank, a BM25 sam bi im dao 85% istog rezultata.

Determinizam nije samo temperatura 0

Svi znaju da smanje temperaturu na 0 i misle da su rešili varijabilnost. Nisu. Temperatura 0 kod većine provajdera nije potpuno deterministička (razlog su tie-breaking-i u sampling-u i floating point non-determinizam na GPU-u). Ono što zapravo utiče na predvidivost:

  1. Fiksirana verzija modela. Nikada gpt-4o bez pinned datuma, uvek konkretan snapshot. Isto važi za Claude modele. Silent update modela iza aliasa je razlog broj jedan zašto vam prompt "iznenada" počne drugačije da se ponaša.
  2. Seed parametar gde ga provajder izlaže. Ne rešava sve, ali smanjuje varijansu unutar iste verzije modela.
  3. Structured output / tool use. Kada model mora da vrati JSON koji odgovara schema-i, prostor mogućih odgovora se drastično sužava. Ovo je najveći poklon za produkciju u poslednje dve godine.
  4. Idempotentnost na nivou aplikacije. Cache-ujem odgovore po hash-u (system + task + input). Isti input dva puta ne košta dva puta.

Ovaj poslednji je često potcenjen. U jednom svom sistemu koji obrađuje ponavljajući saobraćaj, cache hit rate je oko 40%, što znači da 40% zahteva ni ne ide do LLM-a. To je i predvidivost i ušteda odjednom.

Eval koji vredi: golden set + regresija + canary

Ako menjate prompt bez eval-a, kockate se. Ako imate eval koji je "pitao sam ChatGPT da mi oceni", kockate se sa dodatnim korakom.

Moj minimalni setup za svaki iole ozbiljan LLM feature u produkciji:

Golden set je 50-200 primera koje sam ja lično proverio. Ne generisani. Ne sintetizovani. Realni ulazi iz produkcije sa labelom koju sam ja stavio. Ovo je jedini deo eval-a kome verujem 100%.

Regresioni eval se pokreće na svaku promenu prompta ili modela. Poredi novu verziju sa prethodnom na istom golden set-u. Ako pređe threshold (npr. tačnost pala više od 2%, ili neka kategorija ispod 90%), PR ne prolazi.

LLM as judge koristim samo za dimenzije koje se teško mere programski (ton, kompletnost). Uvek sa rubrikom napisanom kao spec, i uvek kalibrisanim na golden set-u prvo, da vidim slaže li se sa mnom. Ako se LLM judge ne slaže sa mnom na kontrolnom setu, ne verujem mu ni na test setu.

Canary u produkciji. Nova verzija prompta ide na 5-10% saobraćaja prvo. Metrike koje pratim: format failure rate, average tokens, latency p95, i business metrika (npr. koliko tiketa je posle prošlo eskalaciju). Ako je bilo šta lošije, rollback.

Skica koja pokriva 80% slučajeva:

def eval_prompt(prompt_version, golden_set, model="claude-sonnet-4-5-20250929"):
    results = []
    for case in golden_set:
        output = run_llm(prompt_version, case.input, model=model, seed=42)
        results.append({
            "id": case.id,
            "expected": case.expected,
            "actual": output,
            "match": check_match(case.expected, output),
            "format_ok": validate_schema(output),
            "tokens": count_tokens(output),
        })
    return summarize(results)

Ovo nije glamurozno. To je poenta. Eval je infrastruktura, ne proizvod.

Failure modes koje uvek planiram

Ovo je lista koju držim u svakom projektu i idem redom kad dizajniram novi LLM feature:

  • Model vrati nevalidan JSON. Retry sa "prethodni odgovor nije bio validan JSON, ispravi ga", pa ako ni to ne prođe, fallback na deterministicko pravilo.
  • Model vrati validan JSON sa halucniranim vrednostima (npr. kategorija koja ne postoji). Validacija posle parse-a, ne samo schema.
  • Model odbije da odgovori (safety refusal na benigni ulaz). Loguj, escaliraj, imaj human review queue.
  • Prompt injection u user input-u ("ignore previous instructions..."). Nikada ne mešam sistem instrukcije i user input u istom stringu, uvek u različitim role message-ima. Za tool-use agente imam allowlist alata i quote-ovanje user string-a.
  • RAG vrati nešto zastarelo. Metadata sa datumom u prompt, i pravilo "ako je izvor stariji od X, napomeni to u odgovoru".
  • Latency spike od provajdera. Timeout na 15s, retry sa exponential backoff, fallback na drugi model (Claude -> GPT ili obrnuto) za kritične puteve.
  • Budžet eskalacija. Hard cap na tokene po zahtevu i po korisniku po danu. Nikad nemam LLM feature bez budget guard-a otkad sam prvi put video račun posle bug-a u retry logici.

Šta bih ja uradio ako počinjete danas

Ako gradite LLM feature koji treba da radi u produkciji, evo redosleda kojim bih ja išao:

  1. Napišite eval pre prompta. 30 realnih primera, ručno labelovanih. Ovo je jedini način da znate kad ste gotovi.
  2. Napišite prompt kao spec sa eksplicitnim pravilima za ivične slučajeve. Ne trošite vreme na "budi pažljiv" instrukcije.
  3. Razdvojite kontekst na slojeve. System, task, retrieved, user. Cache prva dva.
  4. Pin model verziju i logujte je uz svaki poziv. Bez izuzetka.
  5. Structured output uvek kad je moguće. Ako model podržava tool use ili JSON mode, koristite ga.
  6. Postavite budget guardrails pre nego što pustite ijedan zahtev u produkciju. Token cap, request cap, timeout, fallback.
  7. Canary rollout za sve promene prompta. 5% saobraćaja, 24-48 sati, pa širi.
  8. Regresioni eval u CI. Ako menja prompt, mora da prođe eval. Isto kao unit test.

Ovo nije brzo. Brže je da napišete prompt, deploy-ujete, i nadate se. Ali ako ste ikad debagovali LLM feature koji je "juče radio a danas ne", znate da je vreme utrošeno na ovo infrastrukturu vreme koje ste dobili nazad deset puta.

Zatvaranje

Prompt engineering bez context engineering-a i eval-a je kockanje sa dodatnim koracima. Timovi koji uspevaju sa LLM-om u produkciji ne pišu bolje promptove od ostalih. Bolje mere šta se dešava kad prompt padne, i imaju sistem koji ne dozvoli da isto padne dva puta.

Ako gradite nešto slično i zapelo je na "u testu radi, uživo pada", javite se preko lazar-milicevic.com/#contact. Rado ću pogledati konkretan slučaj i podeliti šta bih ja probao prvo.

Često postavljana pitanja

Zašto isti prompt ima različitu tačnost u testu i produkciji?

Test set je uvek čistiji od produkcije i prompt koji 'radi' u testu je zapravo prompt koji se poklopio sa distribucijom ulaza koje ste videli dok ste ga pisali. U produkciji stižu duži, prljaviji ulazi, na više jezika, sa HTML-om ili forwardovanim email history-jem, pa prompt počinje da improvizuje. Kod mene je na primer isti agent za klasifikaciju tiketa imao 94% u testu i samo 71% na 500 realnih tiketa iz prethodnih 7 dana. Uzrok obično nije model, već kontekst, skriveni state i kriterijum uspeha koji u produkciji uključuje više dimenzija (tačnost, dužina, jezik, format).

Kako da strukturiram prompt i kontekst da bi LLM u produkciji bio predvidiv?

Kontekst delim na četiri sloja sa različitim lifecycle-om: system instructions (uloga, tvrda pravila, format, jezik), task template (struktura, few-shot primeri, output schema), retrieved context (RAG, tool outputs, memorija) i user input (sanitizovan sirov ulaz). Prva dva sloja se menjaju retko, poslednja dva po zahtevu. Ovakvo razdvajanje omogućava da u eval-u menjam samo jedan sloj i tačno vidim uzrok pada tačnosti. Kao bonus, stabilni prefiks (system + task template) postaje kandidat za prompt caching, što drastično smanjuje troškove.

Kako da napišem prompt kao spec, a ne kao slobodan tekst?

Prompt u produkciji tretiram kao specifikaciju: ako inženjer koji me menja ne može da pročita prompt i tačno kaže šta se očekuje na svaki ulaz, prompt je nedovršen. Koristim jasne sekcije (role, rules, output_schema, examples, input) i pišem eksplicitna pravila za ivicne slučajeve, ne za očigledne stvari. Few-shot primeri se biraju sa dna distribucije, znači 5 najtežih slučajeva, jer kanonične primere model već rešava sam. Output definišem kao JSON schema uz structured output mode, čime praktično eliminišem greške u formatu.

Da li više konteksta znači bolji odgovor iz LLM-a?

Ne, više konteksta posle nekog praga degradira kvalitet odgovora. Ovaj efekat je poznat kao 'lost in the middle': model slabije tretira informacije ubačene sredinom konteksta u odnosu na one na početku i kraju. U praksi to znači da RAG treba da vraća manje, ali kvalitetnijih chunk-ova. Ja radim rerank pre finalnog poziva i od inicijalnih 20 chunk-ova u prompt ubacujem samo 4-6, čime sam smanjio troškove za oko trećinu bez pada tačnosti.

Kako da napravim RAG koji je pouzdan u produkciji?

Prvo, uvek postavljam BM25 baseline pre nego što dodam vektorsku pretragu, jer inače ne znam koliko vektori zapravo doprinose. U praksi hybrid search (BM25 + vektorska + RRF fuzija) je skoro uvek bolji od čiste vektorske, posebno za tehničke domene sa ključnim rečima tipa 'error code 4032'. Posle inicijalnog recall-a radim rerank (Cohere ili lokalni cross-encoder) i u prompt šaljem 4-6 chunk-ova sa eksplicitnom metadata (source, datum, sekcija) da model ne halucinira izvor. Kada retrieval ne vrati ništa, ne šaljem prazan string nego eksplicitan token tipa <retrieved>none</retrieved> uz pravilo 'ako nema konteksta, odgovori ne znam i ne pogađaj'.

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