OpenAI-kompatibilan API na svom serveru
Sve što si do sada pokrenuo — Ollama, llama.cpp, vLLM — izlaže isti oblik API-ja. To nije slučajnost nego pristanak: OpenAI je definisao oblik zahteva, ostali su ga preuzeli, i zahvaljujući tome tvoj lokalni model radi sa alatima koji o njemu ništa ne znaju.
Ovaj tekst razlaže taj oblik do kraja. Sve dalje u serijalu — skripte, analiza logova, RAG, agenti — svodi se na zahteve opisane ovde.
Najmanji mogući zahtev
$ curl http://localhost:11434/v1/chat/completions \
-H "Content-Type: application/json" \
-d '{
"model": "qwen3:8b",
"messages": [
{"role": "user", "content": "Šta radi komanda ss -tulpn?"}
]
}'
Adresa se razlikuje po alatu, ostalo je isto:
| Alat | Adresa | Polje model |
|---|---|---|
| Ollama | :11434/v1 |
Obavezno, oznaka iz ollama list |
| llama.cpp | :8080/v1 |
Zanemaruje se, drži jedan model |
| vLLM | :8000/v1 |
Obavezno, mora se poklopiti |
Šta je servis stvarno spreman da posluži:
$ curl -s http://localhost:11434/v1/models | jq -r '.data[].id' qwen3:8b linux-pomocnik:latest
Oblik odgovora
{
"id": "chatcmpl-843",
"object": "chat.completion",
"created": 1755870123,
"model": "qwen3:8b",
"choices": [
{
"index": 0,
"message": {
"role": "assistant",
"content": "Prikazuje otvorene portove sa procesima koji ih drže."
},
"finish_reason": "stop"
}
],
"usage": {
"prompt_tokens": 24,
"completion_tokens": 18,
"total_tokens": 42
}
}
Sam tekst je duboko ukopan, pa u skriptama ide kroz jq:
$ curl -s http://localhost:11434/v1/chat/completions \
-H "Content-Type: application/json" \
-d '{"model":"qwen3:8b","messages":[{"role":"user","content":"Zdravo"}]}' \
| jq -r '.choices[0].message.content'
Dva polja u odgovoru vredi pratiti.
finish_reason govori zašto je generisanje stalo:
| Vrednost | Značenje |
|---|---|
stop |
Model je završio prirodno |
length |
Dostignut max_tokens, odgovor je odsečen |
tool_calls |
Model traži poziv funkcije, razgovor nije gotov |
usage je jedini pouzdan brojač tokena. Ako pratiš koliko ti konteksta ostaje ili poredaš postavke, tu je podatak.
Uloge poruka
{
"model": "qwen3:8b",
"messages": [
{"role": "system", "content": "Ti si pomoćnik za Linux administraciju. Odgovaraj kratko, na srpskom."},
{"role": "user", "content": "Kako da vidim zauzeće diska?"},
{"role": "assistant", "content": "Komandom df -h za particije, du -sh za direktorijume."},
{"role": "user", "content": "A po direktorijumima, sortirano?"}
]
}
| Uloga | Čemu služi |
|---|---|
system |
Uloga i pravila; ide prva, jednom |
user |
Pitanje ili zadatak |
assistant |
Raniji odgovori modela |
tool |
Rezultat izvršene funkcije |
Server ne pamti ništa između zahteva. Ceo dosadašnji razgovor šalješ ponovo u svakom pozivu — i to je razlog zašto dug razgovor postaje sporiji i skuplji, kako objašnjava tekst o tome šta je AI model. Istorija koju vidiš u Open WebUI je posao aplikacije, ne servisa.
Ovo pravilo je i ključ celog odeljka o tool callingu niže. Sve što nije u messages — ne postoji.
Parametri generisanja
{
"model": "qwen3:8b",
"messages": [{"role": "user", "content": "Napiši komandu za pretragu logova"}],
"temperature": 0.1,
"max_tokens": 256,
"top_p": 0.9,
"seed": 42,
"stop": ["\n\n"]
}
| Parametar | Praktična vrednost |
|---|---|
temperature |
0 – 0.2 za komande i podatke, 0.7 – 1.0 za tekst |
max_tokens |
Postavi ga; bez njega odgovor ume da beži |
top_p |
0.9, retko se dira |
seed |
Uz temperaturu 0, za ponovljiv rezultat |
stop |
Nizovi na kojima generisanje prestaje |
stream |
Postepen ispis |
Za skripte je kombinacija temperature: 0 i fiksiran seed ono što daje ponovljivost. Bez nje ista komanda danas i sutra daje različit rezultat, što u automatizaciji nije prihvatljivo.
Streaming
$ curl -N http://localhost:11434/v1/chat/completions \
-H "Content-Type: application/json" \
-d '{
"model": "qwen3:8b",
"messages": [{"role": "user", "content": "Objasni šta je inode"}],
"stream": true
}'
data: {"choices":[{"delta":{"content":"Inode"},"finish_reason":null}]}
data: {"choices":[{"delta":{"content":" je"},"finish_reason":null}]}
data: {"choices":[{"delta":{"content":" struktura"},"finish_reason":null}]}
data: [DONE]
Format je Server-Sent Events. Umesto message dobijaš delta sa komadićem teksta, a niz se završava oznakom [DONE].
Sklapanje u bashu:
$ curl -sN http://localhost:11434/v1/chat/completions \
-H "Content-Type: application/json" \
-d '{"model":"qwen3:8b","messages":[{"role":"user","content":"Zdravo"}],"stream":true}' \
| grep '^data: ' | sed 's/^data: //' | grep -v '^\[DONE\]' \
| jq -j '.choices[0].delta.content // empty'
Oznaka -N kod curl isključuje baferovanje. Bez nje sve stiže odjednom na kraju i cela poenta otpada.
Za skripte streaming najčešće nije potreban. Ako obrađuješ ceo odgovor odjednom, izostavi ga i uzmi jednostavniji oblik. Koristan je samo tamo gde neko gleda u ekran.
Strukturiran izlaz
Ovo je najkorisnija stvar u celom tekstu za administratora. Umesto da parsiraš prozu, tražiš JSON:
$ curl -s http://localhost:11434/v1/chat/completions \
-H "Content-Type: application/json" \
-d '{
"model": "qwen3:8b",
"messages": [
{"role": "system", "content": "Odgovaraj isključivo u JSON obliku, bez ikakvog dodatnog teksta."},
{"role": "user", "content": "Klasifikuj ovu poruku iz loga: SSH login failed for root from 203.0.113.45. Vrati polja: tip, ozbiljnost (1-5), ip."}
],
"temperature": 0,
"response_format": {"type": "json_object"}
}' | jq -r '.choices[0].message.content'
{
"tip": "neuspela_prijava",
"ozbiljnost": 4,
"ip": "203.0.113.45"
}
Polje response_format tera model da vrati ispravan JSON. vLLM ide korak dalje i podržava tačnu šemu:
"response_format": {
"type": "json_schema",
"json_schema": {
"name": "log_zapis",
"schema": {
"type": "object",
"properties": {
"tip": {"type": "string"},
"ozbiljnost": {"type": "integer", "minimum": 1, "maximum": 5},
"ip": {"type": "string"}
},
"required": ["tip", "ozbiljnost", "ip"]
}
}
}
Sa šemom je izlaz zajamčeno ispravan, jer se ograničenje primenjuje pri samom biranju tokena. Bez nje se oslanjaš na to da će model poslušati — što obično hoće, ali ne uvek.
I dalje proveravaj rezultat pre upotrebe. Ispravan JSON nije isto što i tačan JSON; model može uredno vratiti pogrešnu vrednost u ispravnom obliku.
Tool calling
Tool calling je mesto gde model prestaje da bude sagovornik i postaje deo tvog sistema. Ali radi drugačije nego što na prvi pogled izgleda, pa ide korak po korak.
Šta se zapravo dešava
Model ne izvršava ništa. On nema pristup tvom serveru, ne ume da pokrene komandu i ne zna šta se nalazi na disku. Ono što ume jeste da, umesto teksta, vrati strukturiran zahtev: „pozovi funkciju provera_diska sa argumentom /". Tvoj kod tu funkciju izvršava i rezultat vraća modelu kao još jednu poruku u razgovoru.
Ceo mehanizam su četiri koraka:
- Šalješ pitanje zajedno sa spiskom dostupnih funkcija (
tools) - Model vraća
tool_callsifinish_reason: "tool_calls" - Ti izvršavaš funkciju u svom kodu
- Šalješ novi zahtev sa celom istorijom plus rezultatom
Najvažnija stvar, koja se najčešće promaši: ne postoji poseban endpoint za alate. Sve ide na isti POST /v1/chat/completions, samo ga pozoveš dva puta. Jedina razlika između ta dva zahteva je sadržaj polja messages.
Kako se alat definiše
Definicija je JSON Schema opis potpisa funkcije. Model ne vidi tvoj kod — vidi samo ovaj opis i na osnovu njega odlučuje kada i kako da funkciju pozove.
"tools": [
{
"type": "function",
"function": {
"name": "provera_diska",
"description": "Vraća zauzeće diska za zadatu putanju na serveru",
"parameters": {
"type": "object",
"properties": {
"putanja": {
"type": "string",
"description": "Apsolutna putanja, npr. / ili /var"
},
"jedinica": {
"type": "string",
"enum": ["G", "M"],
"description": "Jedinica prikaza"
}
},
"required": ["putanja"]
}
}
}
]
Tri stvari odlučuju da li će ovo raditi:
descriptionfunkcije je jedini signal kada da je pozove. „Vraća zauzeće diska za zadatu putanju" radi; „disk funkcija" ne radi. Piši kao da objašnjavaš kolegi koji ne poznaje tvoj sistem.descriptionsvakog parametra odlučuje šta će model staviti unutra. Bez primera formata model izmisli format.enumje najjači alat koji imaš. Sve što može biti zatvoren spisak — neka budeenum. Time skidaš čitav sloj validacije sa sebe.
Polje parameters je običan JSON Schema, pa važi sve — integer sa minimum i maximum, ugnježđeni objekti, nizovi. Ako funkcija nema argumente, ide "parameters": {"type": "object", "properties": {}}.
Korak 1: prvi zahtev
Sadržaj fajla zahtev-1.json:
{
"model": "qwen3:8b",
"messages": [
{"role": "user", "content": "Koliko je slobodno na root particiji?"}
],
"tools": [
{
"type": "function",
"function": {
"name": "provera_diska",
"description": "Vraća zauzeće diska za zadatu putanju na serveru",
"parameters": {
"type": "object",
"properties": {
"putanja": {"type": "string", "description": "Apsolutna putanja, npr. / ili /var"}
},
"required": ["putanja"]
}
}
}
]
}
$ curl -s http://localhost:11434/v1/chat/completions \ -H "Content-Type: application/json" \ -d @zahtev-1.json | jq '.choices[0]'
Korak 2: odgovor modela
{
"index": 0,
"message": {
"role": "assistant",
"content": null,
"tool_calls": [
{
"id": "call_abc123",
"type": "function",
"function": {
"name": "provera_diska",
"arguments": "{\"putanja\":\"/\"}"
}
}
]
},
"finish_reason": "tool_calls"
}
Polje content je prazno, a finish_reason je tool_calls — razgovor nije gotov, model čeka podatak.
Dve zamke u ovom odgovoru:
argumentsje string, ne objekat. Mora dvostruko parsiranje, krozfromjson.tool_callsje niz. Model sme da traži više poziva odjednom, i na svaki moraš odgovoriti zasebnomtoolporukom.
$ echo "$ODGOVOR" | jq -r '.choices[0].message.tool_calls[0].function.arguments | fromjson | .putanja' /
Korak 3: ti izvršavaš
Ovaj korak se dešava potpuno izvan API-ja. Model tu nema nikakve veze:
$ df -h / /dev/sda1 234G 189G 33G 86% /
Korak 4: drugi zahtev, isti endpoint
Ovde je deo koji se najčešće pogrešno uradi. Ne šalješ samo tool poruku — šalješ ceo razgovor, jer server ne pamti ništa. Sadržaj fajla zahtev-2.json:
{
"model": "qwen3:8b",
"messages": [
{"role": "user", "content": "Koliko je slobodno na root particiji?"},
{
"role": "assistant",
"content": null,
"tool_calls": [
{
"id": "call_abc123",
"type": "function",
"function": {"name": "provera_diska", "arguments": "{\"putanja\":\"/\"}"}
}
]
},
{
"role": "tool",
"tool_call_id": "call_abc123",
"content": "/dev/sda1 234G 189G 33G 86% /"
}
],
"tools": [
{
"type": "function",
"function": {
"name": "provera_diska",
"description": "Vraća zauzeće diska za zadatu putanju na serveru",
"parameters": {
"type": "object",
"properties": {
"putanja": {"type": "string", "description": "Apsolutna putanja, npr. / ili /var"}
},
"required": ["putanja"]
}
}
}
]
}
$ curl -s http://localhost:11434/v1/chat/completions \ -H "Content-Type: application/json" \ -d @zahtev-2.json | jq -r '.choices[0].message.content' Root particija ima 234G ukupno, zauzeto je 189G, a slobodno 33G — 86% popunjenosti.
Sada je finish_reason jednak stop. Gotovo.
Šta se menja između dva zahteva
| Polje | Zahtev 1 | Zahtev 2 |
|---|---|---|
| Adresa | /v1/chat/completions |
ista |
model |
qwen3:8b |
isti |
tools |
definicija | ista, ponovo poslata |
messages |
1 poruka | 3 poruke |
Menja se samo messages. Ništa drugo.
Tri greške koje daju 400 ili besmislen odgovor
- Izostavljena
assistantporuka satool_callsu drugom zahtevu. Tadatool_call_idpokazuje u prazno. vLLM na ovo vraća 400, Ollama je popustljivija ali odgovor bude besmislen. - Izmišljen
tool_call_id. Mora biti doslovno onaj iz odgovora modela. - Izostavljen
toolsu drugom zahtevu. Model tada ne zna šta jeprovera_diskai ne ume da protumačitoolporuku.
Više alata — model bira sam
Primer sa jednim alatom je samo najmanji mogući. U praksi šalješ ceo spisak i model odlučuje šta mu treba:
"tools": [
{"type": "function", "function": {"name": "provera_diska", ...}},
{"type": "function", "function": {"name": "status_servisa", ...}},
{"type": "function", "function": {"name": "pretraga_logova", ...}},
{"type": "function", "function": {"name": "lista_korisnika", ...}}
]
- „Koliko ima mesta na disku?" — pozove
provera_diska - „Da li nginx radi i ima li grešaka u logu?" — pozove
status_servisaipretraga_logova - „Šta znači inode?" — ne pozove ništa, samo odgovori tekstom
To je podrazumevano ponašanje i ne moraš ništa da uključuješ. Kvalitet izbora zavisi isključivo od toga koliko su ti description polja jasna.
Kontrolu imaš kroz tool_choice:
| Vrednost | Ponašanje |
|---|---|
"auto" |
Podrazumevano — model odlučuje |
"none" |
Zabranjeno, samo tekst |
{"type":"function","function":{"name":"provera_diska"}} |
Prisiljen na tačno tu funkciju |
Prisiljavanje je korisno kada tool calling koristiš kao ekstraktor podataka — model mora da popuni šemu, i time si dobio strukturiran izlaz na zadnja vrata.
Petlja, ne jedan prolaz
Model sme da traži još jedan poziv i nakon što je dobio prvi rezultat. Pravi tok je zato while, a ne dva curl-a:
#!/bin/bash
API="http://localhost:11434/v1/chat/completions"
MODEL="qwen3:8b"
ALATI='[{"type":"function","function":{
"name":"provera_diska",
"description":"Vraća zauzeće diska za zadatu putanju na serveru",
"parameters":{"type":"object",
"properties":{"putanja":{"type":"string","description":"Apsolutna putanja"}},
"required":["putanja"]}}}]'
PORUKE='[{"role":"user","content":"Koliko je slobodno na root particiji?"}]'
for i in 1 2 3; do
ZAHTEV=$(jq -n --arg m "$MODEL" --argjson p "$PORUKE" --argjson t "$ALATI" \
'{model:$m, messages:$p, tools:$t, temperature:0}')
ODGOVOR=$(curl -s "$API" -H "Content-Type: application/json" -d "$ZAHTEV")
PORUKA=$(echo "$ODGOVOR" | jq '.choices[0].message')
RAZLOG=$(echo "$ODGOVOR" | jq -r '.choices[0].finish_reason')
if [ "$RAZLOG" != "tool_calls" ]; then
echo "$PORUKA" | jq -r '.content'
break
fi
# 1. assistant poruka sa tool_calls ide nazad u istoriju
PORUKE=$(jq -n --argjson p "$PORUKE" --argjson n "$PORUKA" '$p + [$n]')
# 2. izvrši svaki traženi poziv
BROJ=$(echo "$PORUKA" | jq '.tool_calls | length')
for ((j=0; j<BROJ; j++)); do
ID=$(echo "$PORUKA" | jq -r ".tool_calls[$j].id")
IME=$(echo "$PORUKA" | jq -r ".tool_calls[$j].function.name")
PUT=$(echo "$PORUKA" | jq -r ".tool_calls[$j].function.arguments | fromjson | .putanja")
case "$IME:$PUT" in
provera_diska:/|provera_diska:/var|provera_diska:/home)
REZ=$(df -h "$PUT" | tail -1) ;;
*)
REZ="Greška: nedozvoljena funkcija ili putanja" ;;
esac
# 3. rezultat kao tool poruka
PORUKE=$(jq -n --argjson p "$PORUKE" --arg id "$ID" --arg c "$REZ" \
'$p + [{role:"tool", tool_call_id:$id, content:$c}]')
done
done
Petlja ide do tri kruga jer model sme da traži novi poziv i posle prvog rezultata. Uvek stavi brojač iteracija — model ume da upadne u krug pozivanja iste funkcije.
Pokreni skriptu sa bash -x i posmatraj kako promenljiva PORUKE raste iz kruga u krug. To najbolje objasni ceo mehanizam.
Definiši jednom, koristi kroz razgovor
Server je bez stanja, pa spisak alata ide u svakom zahtevu — ali to je mehanika ispod, ne tvoj svakodnevni posao. Registar napišeš jednom i on se dalje šalje sam. U Pythonu:
import subprocess, ollama
# ---- definišeš jednom ----
def provera_diska(putanja: str) -> str:
"""Vraća zauzeće diska za zadatu putanju na serveru."""
if putanja not in ("/", "/var", "/home"):
return "Nedozvoljena putanja"
return subprocess.run(["df", "-h", putanja],
capture_output=True, text=True).stdout
def status_servisa(ime: str) -> str:
"""Vraća da li je systemd servis trenutno aktivan."""
if not ime.isalnum():
return "Nedozvoljeno ime servisa"
return subprocess.run(["systemctl", "is-active", ime],
capture_output=True, text=True).stdout.strip()
REGISTAR = {"provera_diska": provera_diska,
"status_servisa": status_servisa}
# ---- i onda samo pričaš ----
poruke = [{"role": "user", "content": "Da li nginx radi i koliko ima mesta na /var?"}]
for _ in range(5):
odgovor = ollama.chat(model="qwen3:8b", messages=poruke,
tools=list(REGISTAR.values()))
poruke.append(odgovor["message"])
pozivi = odgovor["message"].get("tool_calls")
if not pozivi:
print(odgovor["message"]["content"])
break
for p in pozivi:
ime = p["function"]["name"]
rez = REGISTAR[ime](**p["function"]["arguments"])
poruke.append({"role": "tool", "tool_call_id": p["id"], "content": rez})
Ollama biblioteka iz same Python funkcije izvlači JSON šemu — iz naziva, tipova argumenata i docstringa. Zato je docstring bukvalno ono što model čita da bi odlučio kada da funkciju pozove. Loš docstring je isto što i loš description u ručnom JSON-u.
Ako ni ovo ne želiš da pišeš, postoje gotovi slojevi:
| Sloj | Šta dobiješ |
|---|---|
| Ollama / OpenAI biblioteka | Šema iz funkcije, ali petlju i dalje pišeš ti |
| Open WebUI Tools | Python funkcija napisana u pretraživaču, odmah dostupna u chatu — bez ijedne linije petlje |
| MCP | Alat kao zaseban server; vidi ga svaki klijent koji zna MCP |
Open WebUI je najbliži tome što najčešće želiš — alat napišeš u administraciji, uključiš ga za model, i dalje samo kucaš u chat prozor. MCP rešava sledeći nivo: alat napisan jednom radi i u Open WebUI, i u drugim klijentima, i u tvojoj skripti, bez prepisivanja. Ispod svega i dalje teče isti ciklus opisan ovde.
Zašto onda ručni curl
Ne da bi ga svakodnevno koristio, nego da bi znao šta se dešava kada alat „ne okine". Odgovor je uvek u jednom od tri sloja: model nema tools sposobnost, description je loš, ili se tool poruka ne vraća ispravno. Bez razumevanja ciklusa to izgleda kao magija koja se pokvarila.
Zašto tool calling često ne radi
- Model nema sposobnost za alate. Provera:
ollama show qwen3:8b | grep -A5 Capabilities - Model manji od oko 7B — halucinira imena funkcija ili vrati poziv kao običan tekst umesto u polju
tool_calls temperatureje previsoka; za tool calling drži je na 0- Previše alata odjednom — iznad pet do šest tačnost izbora naglo pada
Bezbednosna granica
Ime funkcije i argumente predlaže model, a model može biti naveden podmetnutom instrukcijom u podacima koje čita — u logu, u fajlu, u tuđem tekstu. Argument koji stigne od modela tretiraj isto kao argument iz nepoverljivog HTTP zahteva.
# NIKAD
eval "df -h $PUTANJA"
# Nego
case "$PUTANJA" in
/|/var|/home) df -h "$PUTANJA" ;;
*) echo "Nedozvoljena putanja" ;;
esac
Beli spisak, nikad crni. Drži spisak dozvoljenih radnji uzak i nikad ne prosleđuj argumente pravo u ljusku. Tema je razrađena u tekstu o bezbednosti i prompt injectionu.
Embeddings
Zaseban put, za pretvaranje teksta u numerički zapis značenja:
$ curl -s http://localhost:11434/v1/embeddings \
-H "Content-Type: application/json" \
-d '{"model": "nomic-embed-text", "input": "restart nginx servisa"}' \
| jq '.data[0].embedding | length'
768
Model za razgovor ovo ne radi — treba poseban, namenski. Osnova je za RAG.
Autentifikacija
$ curl http://localhost:8000/v1/chat/completions \ -H "Authorization: Bearer tajni-kljuc-ovde" ...
Ollama ovo polje ignoriše i prihvata bilo šta. llama.cpp i vLLM ga proveravaju ako je servis podignut sa --api-key. Za ozbiljnu zaštitu ide proxy ispred, o čemu govore tekstovi o reverse proxy-ju i zaštiti API-ja.
Greške
$ curl -s -w '\n%{http_code}\n' http://localhost:11434/v1/chat/completions \
-H "Content-Type: application/json" \
-d '{"model":"nepostojeci","messages":[{"role":"user","content":"test"}]}'
| Kod | Uzrok |
|---|---|
| 400 | Neispravan JSON, predugačak kontekst ili neispravan sled tool poruka |
| 401 | Pogrešan ili izostavljen ključ |
| 404 | Model nije preuzet ili naziv ne odgovara |
| 500 | Servis puca, najčešće zbog memorije |
| 503 | Model se učitava ili je red pun |
Skripte piši tako da uvek gledaju kod odgovora:
#!/bin/bash
ODGOVOR=$(curl -s -w '\n%{http_code}' http://localhost:11434/v1/chat/completions \
-H "Content-Type: application/json" \
-d "$ZAHTEV")
KOD=$(echo "$ODGOVOR" | tail -1)
TELO=$(echo "$ODGOVOR" | head -n -1)
if [ "$KOD" != "200" ]; then
echo "Greška $KOD: $(echo "$TELO" | jq -r '.error.message // .')" >&2
exit 1
fi
echo "$TELO" | jq -r '.choices[0].message.content'
Ovaj obrazac se ponavlja kroz ceo ostatak serijala. Osnove pisanja skripti pokriva serijal o bash skriptama.
Praktični scenariji
Scenario 1: odgovor se seče na pola rečenice
Pogledaj finish_reason. Vrednost length znači da je max_tokens premali.
Scenario 2: isti zahtev daje različite odgovore
Postavi temperature na 0 i fiksiraj seed. Bez oba nema ponovljivosti.
Scenario 3: JSON u odgovoru okružen objašnjenjem
Dodaj response_format, a u sistemski prompt izričito napiši da nema propratnog teksta. Ako i to ne pomogne, izvuci deo između vitičastih zagrada.
Scenario 4: greška 400 sa pomenom konteksta
Razgovor je premašio prozor. Skrati istoriju ili podigni kontekst servisa, uz proveru memorije.
Scenario 5: prvi zahtev traje dugo, ostali su brzi
Model se učitava pri prvom pozivu. Podigni OLLAMA_KEEP_ALIVE, kako je opisano u tekstu o systemd jedinici.
Scenario 6: model zanemaruje sistemski prompt
Proveri da nije base verzija. Kod nekih modela pomaže i da se pravilo ponovi na kraju korisničke poruke.
Scenario 7: model uporno traži isti alat u krug
Rezultat koji si vratio kao tool poruku mu ne odgovara na pitanje — najčešće je prazan ili je poruka o grešci. Vrati razumljiv tekst umesto praznog niza i ograniči broj iteracija.
Scenario 8: model ignoriše alat i odgovara napamet
Popravi description funkcije, spusti temperature na 0, i proveri da model uopšte ima sposobnost za alate. Ako mora da ga pozove, koristi tool_choice sa imenom funkcije.
Scenario 9: greška 400 pri drugom zahtevu sa alatom
Skoro uvek nedostaje assistant poruka sa tool_calls u istoriji, ili se tool_call_id ne poklapa. Ispiši messages pre slanja i uporedi identifikatore.
Scenario 10: model šalje pogrešnu vrednost argumenta
Dopuni description parametra primerom formata, a ako je spisak vrednosti konačan — pretvori ga u enum.
Kratka referenca
POST /v1/chat/completions— glavni put, isti i za razgovor i za alateGET /v1/models— dostupni modeliPOST /v1/embeddings— numerički zapis tekstamessages—system,user,assistant,tool.choices[0].message.content— put do teksta odgovorafinish_reason—stop,length,tool_callsusage— pouzdan broj tokenatemperature: 0+seed— ponovljiv rezultatmax_tokens— postavi ga uvekstream: true+curl -N— postepen ispisresponse_format: {"type":"json_object"}— JSON umesto prozetools— spisak funkcija; šalje se u svakom zahtevutool_calls— model predlaže poziv, tvoj kod izvršavatool_call_id— mora se poklopiti said-jem iz odgovoratool_choice—auto,noneili prisiljena funkcijaarguments— string sa JSON-om, ide krozfromjsonAuthorization: Bearer— ključ, koji Ollama ignoriše- Server ne pamti ništa — istorija ide u svakom zahtevu
Vežba
- Pošalji zahtev kroz
curli izvuci samo tekst odgovora pomoćujq. - Namerno postavi
max_tokensna 10 i potvrdi vrednostfinish_reason. - Pošalji isti zahtev dvaput sa temperaturom 0 i fiksiranim
seed, pa uporedi odgovore znak po znak. - Napiši zahtev koji vraća JSON sa tri polja i proveri ga kroz
jq -e. - Sastavi razgovor od četiri poruke i uporedi
prompt_tokenssa vrednošću za jednu poruku. - Napiši skriptu koja proverava HTTP kod i vraća razumnu poruku pri grešci.
- Napravi fajlove
zahtev-1.jsonizahtev-2.jsoniz ovog teksta i ručno prođi ceo ciklus tool callinga kroz dvacurlpoziva. - Namerno izbaci
assistantporuku satool_callsiz drugog zahteva i zabeleži šta servis vrati. - Dodaj drugi alat,
status_servisa, i postavi pitanje koje traži oba poziva odjednom. Prebroj stavke u nizutool_calls. - Pokreni bash petlju sa
bash -xi prati kakoPORUKEraste iz kruga u krug. - Postavi
tool_choicena tačno određenu funkciju i postavi pitanje koje sa njom nema veze. Pogledaj šta model uradi sa argumentima.
Sledeći tekst u serijalu: Nginx reverse proxy i HTTPS ispred lokalnog LLM-a
Povezano:
- AI na Linux serveru — pregled celog serijala
- Open WebUI — prethodni tekst, i najlakši način da alat koristiš kroz chat
- MCP — standardizovan način da se alat napiše jednom
- Bezbednost i prompt injection — zašto se argumenti iz modela ne smeju verovati
- Rečnik AI pojmova — značenje parametara generisanja
- curl — rad sa HTTP zahtevima iz terminala
- Bash skripte — osnove pisanja skripti
Comments
Post a Comment