🧠 Model ispod agenta
Lekcija 01 · Mentalni modelLesson 01 · Mental model

Model ne pretražuje bazu znanja — on sklapa odgovor, token po token.The model doesn't query a knowledge base — it assembles the answer, token by token.

Model ne zna rečenicu unapred. Reda je delić po delić, i svaki naredni bira po tome šta je najverovatnije posle onoga do sada. Cela agentska serija stoji na toj jednoj radnji — vredi je razumeti dobro.The model doesn't know the sentence in advance. It lays it down piece by piece, each one chosen by what's most likely to follow so far. The whole agent series rests on that single act — it's worth understanding well.

model

Lako je pomisliti da model zna činjenice i vadi ih iz neke baze. Znanje jeste u modelu — utisnuto u njegove težine tokom treninga — ali on ga ne pretražuje. On ga rekonstruiše: za svaki naredni delić bira ono što najverovatnije dolazi, pa sklapa najuverljiviju verziju odgovora.It's tempting to think the model knows facts and pulls them from some database. The knowledge is in the model — baked into its weights during training — but it doesn't look it up. It rebuilds it: for each next piece it picks what most likely comes next, assembling the most plausible version of the answer.

Baza podatakaA database

Traži i vrati. Zapis ili postoji ili ne. Vrati tačan red, ili grešku. Znanje je diskretno i proverivo.Query and return. A record either exists or it doesn't. It returns the exact row, or an error. The knowledge is discrete and verifiable.

Jezički modelA language model

Sklopi najverovatnije. Nema tačan zapis da vrati. Uvek sklopi nešto — i kad nema pouzdanu osnovu. Zato ume da bude samouvereno pogrešan.Assemble the likeliest. There's no exact record to return. It always assembles something — even without a solid basis. That's why it can be confidently wrong.

To „samouvereno pogrešno" je ono što zovemo halucinacija: nije pretraga koja je promašila, nego rekonstrukcija koja je odlutala. Upravo zbog toga cela produkcijska priča kasnije počiva na verifikaciji — ne veruješ modelu na reč, nego proveriš rezultat.That "confidently wrong" is what we call a hallucination: not a search that missed, but a reconstruction that drifted. Which is exactly why the whole production story later rests on verification — you don't take the model at its word, you check the result.

Šta gradimoWhat we're building Kroz 12 kratkih lekcija sklopićeš tačnu sliku modela: jedan API poziv, tokeni, prozor konteksta, kako model traži alat, i kako biraš model po zadatku. Na kraju ti ništa u agentskoj seriji neće biti crna kutija.Across 12 short lessons you'll build an accurate picture of the model: a single API call, tokens, the context window, how the model asks for a tool, and how you pick a model per task. By the end, nothing in the agent series will be a black box.
Lekcija 02 · Prvi pozivLesson 02 · First call

Nije magija — jedan HTTP pozivNot magic — a single HTTP call

Iza svakog agenta stoji jedna te ista radnja: pošalješ listu poruka, dobiješ jednu poruku nazad. Hajde da je napravimo od nule i vidimo tačan oblik ulaza i izlaza.Behind every agent is the same single action: you send a list of messages, you get one message back. Let's build it from scratch and see the exact shape of the input and output.

Treba ti Python, paket anthropic i API ključ u promenljivoj okruženja. Radi u venv-u da ne mešaš zavisnosti sa sistemom.You need Python, the anthropic package, and an API key in an environment variable. Work inside a venv so you don't mix dependencies with the system.

okruženjesetup
python -m venv .venv
source .venv/bin/activate
pip install anthropic
export ANTHROPIC_API_KEY="sk-ant-..."

Sad minimalni poziv. messages je lista, svaka poruka ima role (user ili assistant) i content. Odgovor stiže kao lista blokova — za običan tekst uzmeš prvi blok.Now the minimal call. messages is a list; each message has a role (user or assistant) and content. The response arrives as a list of blocks — for plain text you take the first block.

prvi_poziv.pyfirst_call.py
import anthropic

client = anthropic.Anthropic()  # ključ čita iz ANTHROPIC_API_KEY

MODEL = "..."                   # aktuelni ID modela (vidi napomenu ispod)

odgovor = client.messages.create(
    model=MODEL,
    max_tokens=1024,
    messages=[
        {"role": "user", "content": "Objasni šta je agent u jednoj rečenici."}
    ],
)

print(odgovor.content[0].text)
import anthropic

client = anthropic.Anthropic()  # reads the key from ANTHROPIC_API_KEY

MODEL = "..."                   # the current model ID (see the note below)

response = client.messages.create(
    model=MODEL,
    max_tokens=1024,
    messages=[
        {"role": "user", "content": "Explain what an agent is in one sentence."}
    ],
)

print(response.content[0].text)

To je ceo temelj. Nema stanja na serveru — API je bez pamćenja. Ako želiš razgovor, sam šalješ celu istoriju u messages pri svakom pozivu. Na tu činjenicu ćeš stalno nailaziti.That's the whole foundation. There's no server-side state — the API is stateless. For a conversation you send the entire history in messages on every call. You'll run into that fact constantly.

Napomena o imenu modelaA note on model names Naziv modela namerno stoji kao MODEL = "...". Imena i verzije se smenjuju brže nego što se tutorijali ažuriraju, pa bi svaki konkretan ID ovde ubrzo bio pogrešan. Uzmi aktuelni sa zvanične liste modela i upiši ga na to jedno mesto. Isto važi za svaki kod u ovoj seriji.The model name is deliberately left as MODEL = "...". Names and versions turn over faster than tutorials get updated, so any concrete ID here would soon be wrong. Take the current one from the official model list and set it in that one place. The same holds for every code sample in this series.
Namerno slomiBreak it on purpose Ukloni ključ (unset ANTHROPIC_API_KEY) pa pokreni ponovo. Dobićeš AuthenticationError. Pošalji praznu listu poruka — dobićeš BadRequestError. Čitanje tačne greške je pola posla u radu sa API-jem.Remove the key (unset ANTHROPIC_API_KEY) and rerun — you'll get an AuthenticationError. Send an empty message list — you'll get a BadRequestError. Reading the exact error is half the job of working with an API.
CheckpointCheckpoint Pokreni python prvi_poziv.py. Treba da dobiješ jednu rečenicu i nijednu grešku. Dodaj print(type(odgovor.content), len(odgovor.content)) — ispisaće <class 'list'> 1. To je ceo oblik odgovora: lista blokova, ovde sa jednim tekstualnim blokom.Run python first_call.py. You should get one sentence and no errors. Add print(type(response.content), len(response.content)) — it prints <class 'list'> 1. That's the whole shape of a response: a list of blocks, here with a single text block.
Lekcija 03 · TokeniLesson 03 · Tokens

Model ne vidi slova — vidi tokeneThe model doesn't see letters — it sees tokens

Pre nego što išta uradi, tekst se seče na tokene — komadiće od otprilike jedne kraće reči ili dela reči. Model radi isključivo nad tokenima. Tokenom se meri i cena i to koliko staje u jedan poziv.Before it does anything, text is cut into tokens — chunks of roughly one short word or part of a word. The model works purely over tokens. It's the unit that measures both cost and how much fits in one call.

Dve stvari odmah postanu bitne. Prva: ćirilica i kod troše više tokena od engleske proze za istu količinu značenja. Druga: broje se i ulazni i izlazni tokeni, odvojeno — pa dugačak ulaz košta i pre nego što model bilo šta odgovori.Two things matter right away. First: Cyrillic and code use more tokens than English prose for the same amount of meaning. Second: input and output tokens are counted separately — so a long input costs you before the model answers anything.

Ne moraš da nagađaš. Tačan broj tokena dobiješ pre poziva, a svaki odgovor ti u polju usage kaže koliko je stvarno potrošeno.You don't have to guess. You get the exact token count before the call, and every response tells you in its usage field how much was actually spent.

tokeni.pytokens.py
import anthropic
client = anthropic.Anthropic()

broj = client.messages.count_tokens(
    model=MODEL,
    messages=[{"role": "user", "content": "Zdravo, svete!"}],
)
print(broj.input_tokens)

odgovor = client.messages.create(
    model=MODEL, max_tokens=256,
    messages=[{"role": "user", "content": "Zdravo, svete!"}],
)
print(odgovor.usage.input_tokens, odgovor.usage.output_tokens)
import anthropic
client = anthropic.Anthropic()

count = client.messages.count_tokens(
    model=MODEL,
    messages=[{"role": "user", "content": "Hello, world!"}],
)
print(count.input_tokens)

response = client.messages.create(
    model=MODEL, max_tokens=256,
    messages=[{"role": "user", "content": "Hello, world!"}],
)
print(response.usage.input_tokens, response.usage.output_tokens)

Prebroj istu rečenicu na srpskom pa na engleskom i uporedi. Razlika nije mala i odmah se vidi na ceni (lekcija 11) i na tome koliko podataka smeš da nabaciš u prozor (lekcija 04).Count the same sentence in Serbian and then in English and compare. The difference is real and translates directly into cost (lesson 11) and how much data you can pile into the window (lesson 04).

CheckpointCheckpoint Prebroj tokene za „Zdravo, svete!" pa za „Hello, world!". Srpska varijanta ima osetno više tokena iako ima isti broj reči. Ako dobiješ isti broj, ne meriš isti tekst — proveri da menjaš content, a ne samo komentar.Count the tokens for "Hello, world!" and then for "Zdravo, svete!". The Serbian version comes out with noticeably more tokens even though it has the same number of words. If you get the same number, you're not measuring what you think — check that you changed content, not just a comment.
Lekcija 04 · ProzorLesson 04 · The window

Koliko „pamćenja" model ima u jednom pozivuHow much "memory" the model has in one call

Sve što model vidi u jednom pozivu — sistemske instrukcije, cela istorija, tvoje pitanje i njegov odgovor — mora da stane u context window, prozor konačne veličine merene u tokenima. Kad ga napuniš, nešto mora da izađe.Everything the model sees in one call — system instructions, the full history, your question, and its answer — must fit in the context window, a fixed-size window measured in tokens. When you fill it, something has to go.

Dve posledice koje se obično nauče na svojoj koži. Prva: model ne pamti ništa između dva poziva. Ako drugi poziv ne sadrži prvi, za model se prvi nikad nije desio. Zato razgovor znači slati celu istoriju iznova (lekcija 02).Two consequences people usually learn the hard way. First: the model remembers nothing between two calls. If the second call doesn't include the first, then as far as the model is concerned the first never happened. That's why a conversation means resending the whole history (lesson 02).

Druga: kad ulaz prekorači prozor, ne dobiješ tiho lošiji odgovor — dobiješ grešku ili odsečen sadržaj. Isti taj problem je razlog zašto u produkciji postoji sažimanje istorije: staru istoriju zbiješ u kratak rezime da oslobodiš mesto.Second: when the input exceeds the window, you don't get a quietly worse answer — you get an error or truncated content. That same problem is why production systems compact history: you condense old history into a short summary to free up room.

Namerno slomiBreak it on purpose Pošalji ogroman ulaz (npr. učitaj veliki fajl u content) i gledaj gde pukne. Pa u dva odvojena poziva reci modelu ime u prvom, pa ga pitaj za ime u drugom — videćeš da ga „ne zna", jer drugi poziv ne nosi prvi.Send a huge input (e.g. load a large file into content) and watch where it breaks. Then, in two separate calls, tell the model a name in the first and ask for it in the second — you'll see it "doesn't know", because the second call doesn't carry the first.
Lekcija 05 · NedeterminizamLesson 05 · Non-determinism

Isti prompt, drugačiji odgovor — i zaštoSame prompt, different answer — and why

Pošalji isto pitanje dva puta i često dobiješ dva različita odgovora. To nije bug. Za svaki naredni token model ne bira baš uvek najverovatniji, nego uzorkuje iz raspodele verovatnoća — pa unosi malo slučajnosti.Send the same question twice and you'll often get two different answers. That's not a bug. For each next token the model doesn't always pick the single most likely one — it samples from a probability distribution, introducing a bit of randomness.

Istorijski se ta slučajnost podešavala parametrom temperature: niža vrednost = predvidljivije i suvlje, viša = raznovrsnije i kreativnije. Mnogi LLM API-ji ga i danas nude. Bitno je znati šta znači, jer oblikuje ponašanje modela.Historically that randomness was tuned with a temperature parameter: lower = more predictable and dry, higher = more varied and creative. Many LLM APIs still expose it. It's worth knowing what it means, because it shapes the model's behavior.

Ali oprez: da li i kako podešavaš temperature zavisi od modela i verzije API-ja. Neke API-je ga nude, a neki noviji modeli su ga uklonili — pa poziv s tim parametrom vraća grešku, i ponašanje se onda usmerava promptom. Zato se kod u ovoj seriji ne oslanja na temperature; pre upotrebe proveri aktuelnu dokumentaciju modela koji koristiš.But beware: whether and how you set temperature depends on the model and the API version. Some APIs expose it, and some newer models have removed it — a call with that parameter then returns an error, and behavior is steered through the prompt instead. That's why this series' code doesn't rely on temperature; check the current docs for whatever model you use before reaching for it.

Zašto je bitnoWhy it matters Determinizam nikad nije bio zagarantovan ni sa temperature=0. Zato se u evaluaciji agenata, kasnije u seriji, oslanjaš na objektivnu proveru (prošao/pao test), a ne na to da model dva puta kaže isto.Determinism was never guaranteed even at temperature=0. That's why, when you get to evaluating agents, you rely on an objective check (test passed/failed) rather than on the model saying the same thing twice.
Lekcija 06 · Kraj i dužinaLesson 06 · End & length

Kako znaš zašto je model staoHow you know why the model stopped

Svaki odgovor nosi polje stop_reason — ono ti kaže zašto je generisanje prestalo. To nije sitnica: na osnovu njega tvoja petlja odlučuje da li je gotovo, da li treba nastaviti, ili da izvrši alat.Every response carries a stop_reason field — it tells you why generation ended. That's not a detail: your loop uses it to decide whether it's done, needs to continue, or must run a tool.

Najčešće vrednosti: end_turn (model je prirodno završio), max_tokens (udario u tvoj limit dužine — odgovor je odsečen), tool_use (traži alat, sledeća lekcija) i stop_sequence (naišao na tvoj zadati graničnik). Parametar max_tokens je tvoja gornja granica dužine izlaza.The most common values: end_turn (the model finished naturally), max_tokens (hit your length limit — the answer is truncated), tool_use (it wants a tool, next lesson), and stop_sequence (it hit a delimiter you set). The max_tokens parameter is your upper bound on output length.

stop.py
odgovor = client.messages.create(
    model=MODEL,
    max_tokens=16,  # namerno malo — odgovor će biti odsečen
    messages=[{"role": "user", "content": "Nabroj deset planeta i opiši svaku."}],
)
print(odgovor.stop_reason)   # -> "max_tokens"
print(odgovor.content[0].text)
response = client.messages.create(
    model=MODEL,
    max_tokens=16,  # deliberately small — the answer will be cut off
    messages=[{"role": "user", "content": "List ten planets and describe each one."}],
)
print(response.stop_reason)   # -> "max_tokens"
print(response.content[0].text)

Taj isti stop_reason je „izlaz iz petlje" iz agentske serije, ovde viđen na izvoru. Kad je tool_use, agent izvrši alat i vrati rezultat; kad je end_turn, petlja može da stane. Sve počinje od ovog jednog polja.That same stop_reason is the loop's "exit signal" from the agent series, seen here at the source. When it's tool_use, the agent runs the tool and returns the result; when it's end_turn, the loop can stop. It all starts from this one field.

CheckpointCheckpoint Pokreni stop.py kakav jeste: ispisaće max_tokens i rečenicu presečenu na pola reči. Pa podigni max_tokens na 1024 i pokreni ponovo — sad je end_turn i odgovor je ceo. Ta dva ispisa su razlika između „nema više šta da kaže" i „nisi mu dao mesta".Run stop.py as is: it prints max_tokens and a sentence chopped mid-word. Then raise max_tokens to 1024 and run it again — now it's end_turn and the answer is complete. Those two printouts are the difference between "it has nothing more to say" and "you gave it no room".
Lekcija 07 · Most ka agentimaLesson 07 · Bridge to agents

Model ne izvršava ništa — on tražiThe model runs nothing — it asks

Ovo je jezgro cele agentske priče, pa idemo polako i bez petlje. Modelu opišeš alat; kad odluči da mu treba, ne izvršava ga — vraća zahtev da ga ti izvršiš. Ti pozoveš svoju funkciju i vratiš rezultat. To je sve.This is the core of the whole agent story, so we'll go slowly and without a loop. You describe a tool to the model; when it decides it needs it, it doesn't run it — it returns a request for you to run it. You call your function and return the result. That's all.

Alat opišeš imenom, opisom i šemom ulaza (JSON Schema). Kad model traži alat, odgovor sadrži blok tool_use sa imenom, argumentima i jedinstvenim id-jem, a stop_reason je tool_use.You describe a tool with a name, a description, and an input schema (JSON Schema). When the model wants a tool, the response contains a tool_use block with the name, arguments, and a unique id, and stop_reason is tool_use.

tool_use.py
alati = [{
    "name": "saberi",
    "description": "Saberi dva cela broja.",
    "input_schema": {
        "type": "object",
        "properties": {"a": {"type": "integer"}, "b": {"type": "integer"}},
        "required": ["a", "b"],
    },
}]

poruke = [{"role": "user", "content": "Koliko je 2 + 3?"}]
odgovor = client.messages.create(
    model=MODEL, max_tokens=1024, tools=alati, messages=poruke,
)
# odgovor.stop_reason == "tool_use"; nadji blok tool_use
poziv = next(b for b in odgovor.content if b.type == "tool_use")
rezultat = poziv.input["a"] + poziv.input["b"]   # ti izvršavaš, ne model
tools = [{
    "name": "add",
    "description": "Add two integers.",
    "input_schema": {
        "type": "object",
        "properties": {"a": {"type": "integer"}, "b": {"type": "integer"}},
        "required": ["a", "b"],
    },
}]

messages = [{"role": "user", "content": "What is 2 + 3?"}]
response = client.messages.create(
    model=MODEL, max_tokens=1024, tools=tools, messages=messages,
)
# response.stop_reason == "tool_use"; find the tool_use block
call = next(b for b in response.content if b.type == "tool_use")
result = call.input["a"] + call.input["b"]   # you run it, not the model

Rezultat vraćaš modelu kao poruku sa blokom tool_result, povezanim preko istog tool_use_id. Model onda sklopi finalni odgovor. Jedan ciklus, ručno.You return the result to the model as a message with a tool_result block, tied by the same tool_use_id. The model then assembles the final answer. One cycle, done by hand.

vrati rezultatreturn the result
poruke.append({"role": "assistant", "content": odgovor.content})
poruke.append({"role": "user", "content": [{
    "type": "tool_result",
    "tool_use_id": poziv.id,
    "content": str(rezultat),
}]})
finalno = client.messages.create(
    model=MODEL, max_tokens=1024, tools=alati, messages=poruke,
)
print(finalno.content[0].text)   # -> "2 + 3 je 5."
messages.append({"role": "assistant", "content": response.content})
messages.append({"role": "user", "content": [{
    "type": "tool_result",
    "tool_use_id": call.id,
    "content": str(result),
}]})
final = client.messages.create(
    model=MODEL, max_tokens=1024, tools=tools, messages=messages,
)
print(final.content[0].text)   # -> "2 + 3 is 5."
Ključna vezaThe key link Kad ovaj jedan ciklus staviš u while petlju — traži alat, izvrši, vrati, ponovi dok ne bude end_turn — dobiješ agenta iz 1. dela serije. Agent nije ništa magičnije od ovoga u petlji.When you put this one cycle into a while loop — ask for a tool, run it, return it, repeat until end_turn — you get the agent from Part 1 of the series. An agent is nothing more magical than this, looped.
CheckpointCheckpoint Posle prvog poziva ispiši odgovor.stop_reason i poziv.input. Treba da vidiš tool_use i {'a': 2, 'b': 3} — dakle model je tražio sabiranje i prosledio argumente, ali sam nije izračunao 5. Ako ti u tekstu odgovora već piše „5", nisi prosledio tools i model je improvizovao.After the first call, print response.stop_reason and call.input. You should see tool_use and {'a': 2, 'b': 3} — so the model asked for the addition and passed the arguments, but never computed 5 itself. If the answer text already says "5", you didn't pass tools and the model improvised.
Lekcija 08 · StrukturaLesson 08 · Structure

Kad ti treba izlaz koji kod može da koristiWhen you need output your code can use

Slobodan tekst je sjajan za čoveka, ali loš za program. Ako iz odgovora vadiš podatke regularnim izrazom, pukneš čim model formulaciju promeni za nijansu. Rešenje: traži strukturisan izlaz i pusti API da ga garantuje.Free text is great for a human but bad for a program. If you extract data from the answer with a regex, you break the moment the model phrases things slightly differently. The fix: ask for structured output and let the API guarantee it.

Definišeš oblik (ovde Pydantic model), a messages.parse vrati već isparsiran, proveren objekat u parsed_output. Nema ručnog json.loads, nema nagađanja.You define the shape (here a Pydantic model), and messages.parse returns an already-parsed, validated object in parsed_output. No manual json.loads, no guessing.

struktura.pystructure.py
from pydantic import BaseModel

class Kontakt(BaseModel):
    ime: str
    email: str
    firma: str

odgovor = client.messages.parse(
    model=MODEL, max_tokens=1024,
    messages=[{"role": "user",
        "content": "Izvuci: Ana Anić (ana@firma.rs), radi u Acme d.o.o."}],
    output_format=Kontakt,
)
kontakt = odgovor.parsed_output   # Kontakt(ime='Ana Anić', ...)
print(kontakt.email)              # ana@firma.rs
from pydantic import BaseModel

class Contact(BaseModel):
    name: str
    email: str
    company: str

response = client.messages.parse(
    model=MODEL, max_tokens=1024,
    messages=[{"role": "user",
        "content": "Extract: Ana Anić (ana@firma.rs), works at Acme Ltd."}],
    output_format=Contact,
)
contact = response.parsed_output   # Contact(name='Ana Anić', ...)
print(contact.email)               # ana@firma.rs
Namerno slomiBreak it on purpose Traži JSON običnim promptom, bez šeme („odgovori kao JSON..."). Radiće — dok povremeno ne vrati i rečenicu okolo („Evo podataka:"), pa ti parser pukne. Šema tu vrstu greške ukida jednom zauvek.Ask for JSON with a plain prompt, no schema ("answer as JSON..."). It'll work — until it occasionally wraps a sentence around it ("Here's the data:") and your parser breaks. A schema removes that whole class of errors for good.
CheckpointCheckpoint Ispiši type(kontakt). Treba da piše Kontakt, ne str i ne dict — znači odgovor je već proveren i pretvoren u objekat. Onda probaj kontakt.telefon: dobićeš AttributeError, jer polje nije u šemi. Šema je ta koja te čuva.Print type(contact). It should say Contact, not str and not dict — meaning the answer has already been validated and turned into an object. Then try contact.phone: you'll get an AttributeError, because that field isn't in the schema. The schema is what protects you.
Lekcija 09 · StreamingLesson 09 · Streaming

Prikaži odgovor dok nastajeShow the answer as it forms

Podrazumevano čekaš da ceo odgovor bude gotov pa ga dobiješ odjednom. Sa streamingom dobijaš token po token, čim ga model proizvede — isto ono što vidiš u ćaskanju kad tekst „kuca" pred tobom.By default you wait for the whole answer to finish, then get it all at once. With streaming you receive it token by token, as soon as the model produces it — the same thing you see in chat when the text "types" in front of you.

streaming.py
with client.messages.stream(
    model=MODEL, max_tokens=1024,
    messages=[{"role": "user", "content": "Napiši kratku priču o robotu."}],
) as stream:
    for delic in stream.text_stream:
        print(delic, end="", flush=True)
with client.messages.stream(
    model=MODEL, max_tokens=1024,
    messages=[{"role": "user", "content": "Write a short story about a robot."}],
) as stream:
    for piece in stream.text_stream:
        print(piece, end="", flush=True)

Važno: streaming ne skraćuje ukupno vreme — samo menja doživljaj, jer korisnik vidi prvi tekst odmah umesto da bulji u prazno. Za duge odgovore ima i praktičnu ulogu: sprečava da veza pukne od čekanja dok model dugo generiše.Important: streaming doesn't reduce total time — it only changes the experience, since the user sees the first text immediately instead of staring at nothing. For long answers it also has a practical role: it keeps the connection from timing out while the model generates for a while.

Lekcija 10 · Izbor modelaLesson 10 · Choosing a model

Ne treba ti najjači model za svaki zadatakYou don't need the strongest model for every task

Isti API nudi više modela, i biraš po zadatku, ne po navici. Gruba podela: jak model za teško rasuđivanje i planiranje, brz i jeftin za jednostavne, mehaničke korake. Cilj je pravi balans kvaliteta, brzine i cene.The same API offers several models, and you pick by task, not by habit. Rough split: a strong model for hard reasoning and planning, a fast cheap one for simple, mechanical steps. The goal is the right balance of quality, speed, and cost.

Kod Claude-a to su tri porodice: Opus (najsposobniji, za najteže), Sonnet (odličan balans za većinu produkcije) i Haiku (najbrži i najjeftiniji, za jednostavne zadatke osetljive na brzinu). Menjaš ih doslovno jednim stringom.With Claude that's three families: Opus (most capable, for the hardest work), Sonnet (a great balance for most production), and Haiku (fastest and cheapest, for simple speed-sensitive tasks). You switch between them with literally one string.

isti poziv, drugi modelsame call, different model
# aktuelne ID-jeve za sve tri porodice uzmi sa zvanicne liste modela
JAK     = "..."   # Opus   — najsposobniji, za najteze
BALANS  = "..."   # Sonnet — balans za vecinu produkcije
BRZ     = "..."   # Haiku  — najbrzi i najjeftiniji

for model in (JAK, BALANS, BRZ):
    o = client.messages.create(
        model=model, max_tokens=256,
        messages=[{"role": "user", "content": "Sažmi ovaj pasus u jednu rečenicu: ..."}],
    )
    print(model, o.usage.output_tokens)
# take the current IDs for all three families from the official model list
STRONG   = "..."   # Opus   — most capable, for the hardest work
BALANCED = "..."   # Sonnet — the balance for most production
FAST     = "..."   # Haiku  — fastest and cheapest

for model in (STRONG, BALANCED, FAST):
    r = client.messages.create(
        model=model, max_tokens=256,
        messages=[{"role": "user", "content": "Summarize this paragraph in one sentence: ..."}],
    )
    print(model, r.usage.output_tokens)

Pokreni isti zadatak kroz sva tri i uporedi kvalitet, tokene i vreme. Tačna imena modela se menjaju s novim verzijama — kad ti zatreba aktuelno, proveri zvaničnu listu modela umesto da pamtiš napamet. Ovaj izbor je direktno „model po koraku" iz produkcijske serije.Run the same task through all three and compare quality, tokens, and time. Exact model names change with new versions — when you need the current ones, check the official model list rather than memorizing them. This choice is directly the "model per step" idea from the production series.

Lekcija 11 · CenaLesson 11 · Cost

Kako se plaća i gde je zidHow you're billed and where the wall is

Ne moraš da pamtiš brojke — one se menjaju — ali moraš da razumeš formulu. Plaćaš po tokenima, i to ulazne i izlazne odvojeno, jer izlazni obično koštaju više. Cena jednog poziva je jednostavna računica.You don't need to memorize the numbers — they change — but you do need to understand the formula. You pay per token, input and output separately, since output usually costs more. The cost of one call is a simple calculation.

Cena poziva = ulazni_tokeni × cena_ulaza + izlazni_tokeni × cena_izlaza. Oba broja imaš u polju usage svakog odgovora (lekcija 03), a aktuelne cene po modelu su na zvaničnoj stranici sa cenama — nikad ih ne pretpostavljaj napamet, jer se menjaju.Call cost = input_tokens × input_price + output_tokens × output_price. You get both numbers from the usage field of every response (lesson 03), and the current per-model prices are on the official pricing page — never assume them from memory, they change.

izračunaj iz usagecompute from usage
# cene po milionu tokena procitaj sa zvanicne stranice i ubaci ovde
CENA_ULAZ = 0.0    # $ / 1M ulaznih tokena
CENA_IZLAZ = 0.0   # $ / 1M izlaznih tokena

u = odgovor.usage
cena = (u.input_tokens * CENA_ULAZ + u.output_tokens * CENA_IZLAZ) / 1_000_000
print(f"{cena:.6f} $")
# read the per-million-token prices from the official pricing page and put them here
PRICE_IN  = 0.0    # $ / 1M input tokens
PRICE_OUT = 0.0    # $ / 1M output tokens

u = response.usage
cost = (u.input_tokens * PRICE_IN + u.output_tokens * PRICE_OUT) / 1_000_000
print(f"{cost:.6f} $")

Dve poluge da spustiš račun. Prva: keširanje prompta — ako se veliki, nepromenljiv deo ulaza ponavlja (npr. dugačke instrukcije), platiš ga jednom pa se čita mnogo jeftinije. Druga: izbor modela (lekcija 10) — jeftiniji model za lake korake. A rate limit je drugi zid: koliko poziva i tokena smeš u minuti; kad ga probiješ, dobiješ grešku i sačekaš.Two levers to bring the bill down. First: prompt caching — if a large, unchanging part of the input repeats (e.g. long instructions), you pay for it once and then read it much cheaper. Second: model choice (lesson 10) — a cheaper model for easy steps. And the rate limit is a different wall: how many calls and tokens you're allowed per minute; cross it and you get an error and wait.

Lekcija 12 · KrajLesson 12 · The end

Model je sirovina — sve pametno gradiš oko njegaThe model is raw material — you build everything smart around it

Prošao si put od „model sklapa token po token" do pravog poziva sa alatima, strukturom i streamingom. Sad znaš tačno šta model radi — i, jednako važno, šta ne radi sam.You've gone from "the model assembles token by token" to a real call with tools, structure, and streaming. Now you know exactly what the model does — and, just as important, what it doesn't do on its own.

  • Model ne pamtiThe model doesn't remember — stanje nosiš ti, šaljući istoriju iznova (lekcije 02, 04).— you carry the state, resending history each time (lessons 02, 04).
  • Model ne proveravaThe model doesn't verify — ume da bude samouvereno pogrešan, pa verifikaciju gradiš oko njega (lekcija 01).— it can be confidently wrong, so you build verification around it (lesson 01).
  • Model ne izvršavaThe model doesn't execute — samo traži alat; ti ga pokreneš i vratiš rezultat (lekcija 07).— it only asks for a tool; you run it and return the result (lesson 07).
Jedna rečenica za ponetiOne sentence to take away Model je sirovina koja sklapa sledeći token;The model is raw material that assembles the next token; sve pametno je u tome šta mu daš i šta gradiš oko njega.everything smart is in what you feed it and what you build around it.

Kad ti je model jasan, sledi Prompt i kontekst — šta staviš u model, i zašto isti model nekad odgovori sjajno a nekad promaši. Posle toga ide Agentska petlja: uzmeš ovaj jedan tool-use ciklus, staviš ga u petlju sa verifikacijom i kočnicama, i dobiješ agenta. Sav kod je na GitLab-u: gitlab.com/webmaric/tutorials.Once the model makes sense, next is Prompt & Context — what you put into the model, and why the same model sometimes nails it and sometimes misses. After that comes The Agent Loop: take this one tool-use cycle, put it in a loop with verification and brakes, and you get an agent. All the code is on GitLab: gitlab.com/webmaric/tutorials.

Sledeći deoNext partPrompt i kontekstPrompt & Context