Ollama ist nicht nur ein CLI-Tool zum Ausführen von Modellen im Terminal.
Es ist ein vollwertiger lokaler Server mit einer REST-API, der auf Port 11434 lauscht
und Anfragen von jeder Anwendung entgegennimmt – Spring Boot, Node.js, Python,
oder jede Sprache mit HTTP-Unterstützung. In diesem Artikel – eine vollständige praktische Analyse:
welche Endpunkte es gibt, wie man sie aufruft und wie man Ollama in eine reale Anwendung integriert.
Wenn Sie Ollama noch nicht installiert haben –
beginnen Sie mit dem Installationsleitfaden für Mac, Windows und Linux.
Wenn Sie verstehen möchten, welche Modelle für verschiedene Aufgaben geeignet sind –
lesen Sie den Artikel über die Auswahl von Ollama-Modellen im Jahr 2026.
📚 Artikelinhalt
🎯 Zwei API-Oberflächen: /api/* nativ vs /v1/ OpenAI-kompatibel
Kurze Antwort:
Ollama hat zwei unabhängige APIs. Die native /api/* – volle Kontrolle:
Streaming mit Metadaten, Modellverwaltung, Embeddings, Prozessinspektion.
Die OpenAI-kompatible /v1/* – ein Drop-in-Ersatz für Code, der bereits mit der ChatGPT API funktioniert.
Für neue Projekte – wählen Sie die native. Für die Migration von bestehendem Code – /v1/.
Wenn Sie bereits Code haben, der die OpenAI API aufruft –
um auf lokales Ollama umzuschalten, reicht es aus, eine Zeile zu ändern:
base_url = "http://localhost:11434/v1".
Der Rest des Codes bleibt unverändert.
Native API (/api/*)
Laut der offiziellen Ollama-Dokumentation
ist die API nach der Installation unter der Adresse http://localhost:11434/api verfügbar.
Die native API unterstützt:
- ✔️
POST /api/generate – Textgenerierung per Prompt
- ✔️
POST /api/chat – Chat mit Verlauf und Tool Calling
- ✔️
POST /api/embed – Embedding-Generierung
- ✔️
GET /api/tags – Liste der installierten Modelle
- ✔️
GET /api/ps – laufende Modelle und VRAM-Nutzung
- ✔️
POST /api/pull – Modell herunterladen
- ✔️
DELETE /api/delete – Modell löschen
OpenAI-kompatible API (/v1/*)
ML Journey erklärt:
Ollama unterstützt einen OpenAI-kompatiblen Endpunkt, was bedeutet, dass jedes Tool,
jede Bibliothek oder jede Anwendung, die mit der OpenAI API funktioniert, mit einem einzigen Zeilenwechsel an lokales
Ollama angeschlossen werden kann. Dazu gehören die offiziellen Python- und JS-SDKs von OpenAI,
LangChain, LlamaIndex, Continue und Hunderte anderer Tools.
| Endpunkt |
Nativ (/api/*) |
OpenAI-kompatibel (/v1/*) |
| Chat |
/api/chat |
/v1/chat/completions |
| Generierung |
/api/generate |
/v1/completions |
| Embeddings |
/api/embed |
/v1/embeddings |
| Modellliste |
/api/tags |
/v1/models |
| Modellverwaltung |
✔️ Vorhanden |
❌ Nicht vorhanden |
| Streaming-Metadaten |
✔️ Vollständig |
⚠️ Teilweise |
| API-Schlüssel |
Nicht erforderlich |
Beliebiger String (ignoriert) |
⚠️ Worauf man achten sollte – meine Erfahrung
Als ich Ollama in WebsCraft integriert habe, bin ich dreimal auf dieselben Stolpersteine gestoßen.
Hier ist, was Sie sofort wissen sollten – um keine Zeit mit Debugging zu verschwenden.
1. Modellname in /v1/ muss exakt übereinstimmen
In der echten OpenAI API ist der Modellname global stabil: gpt-4 ist immer verfügbar.
In Ollama – das Modell muss lokal heruntergeladen sein, und der Name muss mit
dem übereinstimmen, was ollama list anzeigt.
Ich habe mehrmals rätselhafte 404 model not found-Fehler erhalten,
nur weil ich "llama3" anstelle von "llama3.2:3b" übergeben habe.
Die erste Regel bei der Migration von Code von OpenAI zu Ollama:
# Überprüfen Sie den genauen Namen, bevor Sie Code schreiben
ollama list
# NAME ID SIZE MODIFIED
# llama3.2:3b ... 2.0 GB 2 days ago
# nomic-embed-text:latest ... 274 MB 5 days ago
Wenn Ihr Tool fest auf gpt-3.5-turbo oder einen anderen
OpenAI-Namen programmiert ist – können Sie das Modell unter dem gewünschten Namen kopieren:
# Erstellt einen Alias: jetzt zeigt gpt-3.5-turbo auf llama3.2:3b
ollama cp llama3.2:3b gpt-3.5-turbo
2. Kontextfenster über /v1/ ändern – nicht offensichtlich
Die OpenAI API hat keinen Parameter zur Änderung der Kontextgröße – sie ist für jedes Modell fest.
Daher kann num_ctx nicht über /v1/chat/completions übergeben werden:
Der Parameter wird einfach ignoriert.
Das habe ich herausgefunden, nachdem lange Dokumente unerwartet abgeschnitten wurden –
das Modell verwarf stillschweigend einen Teil des Kontexts, anstatt einen Fehler zurückzugeben.
Lösung: Erstellen Sie eine Modelfile mit dem gewünschten Kontext und verwenden Sie den neuen Namen:
# Erstellen einer Modelfile
FROM llama3.2:3b
PARAMETER num_ctx 16384
# Erstellen eines neuen Modells
ollama create llama3-16k -f Modelfile
# Jetzt aufrufen über /v1/ mit dem neuen Namen
curl http://localhost:11434/v1/chat/completions \
-H "Content-Type: application/json" \
-d '{
"model": "llama3-16k",
"messages": [{"role": "user", "content": "...langer Text..."}]
}'
Über die native /api/chat gibt es dieses Problem nicht – dort wird num_ctx
direkt in options übergeben:
{
"model": "llama3.2:3b",
"messages": [...],
"options": {
"num_ctx": 16384
}
}
3. Antwort /v1/ und /api/ – unterschiedliches Format
Wenn Sie zwischen der nativen und der OpenAI-kompatiblen API wechseln – denken Sie daran,
dass das Antwortformat unterschiedlich ist. Ich habe mehrmals KeyError in Python gefangen,
nur weil ich verwechselt habe, wo response["message"]["content"] und
wo response.choices[0].message.content ist.
| Feld |
Nativ /api/chat |
OpenAI-kompatibel /v1/ |
| Antworttext |
response["message"]["content"] |
response.choices[0].message.content |
| Generierungsende |
response["done"] == true |
response.choices[0].finish_reason == "stop" |
| Token-Statistiken |
eval_count, eval_duration |
usage.completion_tokens |
| Tool-Aufrufe |
message.tool_calls |
choices[0].message.tool_calls |
Meine Regel: In einem Projekt verwende ich nur eine API-Oberfläche.
Wenn Migration von OpenAI – ich behalte /v1/ und das OpenAI SDK überall.
Wenn ein neues Projekt – die native API überall. Eine Mischung aus beiden Ansätzen im selben Code
garantiert Verwirrung beim Debugging.
Fazit: Für ein neues Projekt – die native API bietet mehr Kontrolle.
Für die Migration von bestehendem OpenAI-Code – /v1/ ohne Codeänderungen.
🎯 POST /api/generate: grundlegende Textgenerierung
Kurze Antwort:
/api/generate – der einfachste Endpunkt: nimmt Modell und Prompt entgegen,
gibt Text zurück. Speichert den Kontext nicht zwischen den Anfragen. Geeignet für
einmalige Aufgaben: Zusammenfassung, Übersetzung, Klassifizierung.
Unterschied zwischen /api/generate und /api/chat:
generate nimmt einen Prompt-String entgegen, chat nimmt ein Array von Nachrichten mit Rollen entgegen.
Für einen Chatbot – immer /api/chat. Für Batch-Verarbeitung – /api/generate ist bequemer.
Grundlegende Anfrage über curl
# stream: false – gibt die gesamte Antwort sofort zurück
curl http://localhost:11434/api/generate \
-H "Content-Type: application/json" \
-d '{
"model": "llama3.2:3b",
"prompt": "Erkläre, was eine REST API in drei Sätzen ist.",
"stream": false
}'
Antwortformat – alle Felder
{
"model": "llama3.2:3b",
"created_at": "2026-05-01T10:00:00Z",
"response": "REST API ist...",
"done": true,
"prompt_eval_count": 15,
"prompt_eval_duration": 123456789,
"eval_count": 42,
"eval_duration": 987654321,
"total_duration": 1234567890,
"load_duration": 56789012
}
Was jedes Feld bedeutet:
- ✔️
prompt_eval_count – Anzahl der Tokens im Prompt (Eingabe)
- ✔️
eval_count – Anzahl der generierten Tokens (Ausgabe)
- ✔️
eval_duration – Generierungszeit in Nanosekunden
- ✔️
load_duration – Ladezeit des Modells (0, wenn bereits im Speicher)
- ✔️
total_duration – Gesamtzeit von der Anfrage bis zur Antwort
Wie man tokens/sec aus Metadaten berechnet
Die Antwortmetadaten ermöglichen die Protokollierung der tatsächlichen Modellleistung.
Ich verwende dies in WebsCraft zur Überwachung der Generierungsgeschwindigkeit
in Abhängigkeit von der Last:
# Python: Berechnung von tokens/sec
import requests
r = requests.post("http://localhost:11434/api/generate", json={
"model": "llama3.2:3b",
"prompt": "Was sind Microservices?",
"stream": False
})
data = r.json()
tokens_per_sec = data["eval_count"] / (data["eval_duration"] / 1e9)
total_sec = data["total_duration"] / 1e9
prompt_tokens = data["prompt_eval_count"]
output_tokens = data["eval_count"]
print(f"Geschwindigkeit: {tokens_per_sec:.1f} tok/s")
print(f"Tokens: {prompt_tokens} Eingabe → {output_tokens} Ausgabe")
print(f"Gesamtzeit: {total_sec:.2f}s")
// Java: Berechnung von tokens/sec über WebClient
@Service
public class OllamaGenerateService {
private final WebClient ollamaWebClient;
public record GenerateResult(String text, double tokensPerSec, int outputTokens) {}
public Mono<GenerateResult> generate(String prompt) {
var body = Map.of(
"model", "llama3.2:3b",
"prompt", prompt,
"stream", false
);
return ollamaWebClient.post()
.uri("/api/generate")
.bodyValue(body)
.retrieve()
.bodyToMono(Map.class)
.map(r -> {
var text = (String) r.get("response");
var evalCount = ((Number) r.get("eval_count")).intValue();
var evalDuration = ((Number) r.get("eval_duration")).longValue();
var tokPerSec = evalCount / (evalDuration / 1_000_000_000.0);
return new GenerateResult(text, tokPerSec, evalCount);
});
}
}
Hauptparameter in options
{
"model": "llama3.2:3b",
"prompt": "Ihr Text hier",
"stream": false,
"system": "Sie sind ein technischer Redakteur. Antworten Sie kurz und prägnant.",
"options": {
"temperature": 0.7,
"num_ctx": 4096,
"top_p": 0.9,
"num_predict": 256
}
}
- ✔️
temperature – Kreativität der Antwort: 0.1 präzise/deterministisch, 0.9 variabel
- ✔️
num_ctx – Größe des Kontextfensters (Tokens)
- ✔️
num_predict – maximale Anzahl von Tokens in der Antwort
- ✔️
top_p – Nucleus Sampling, normalerweise 0.9
- ✔️
system – System-Prompt (außerhalb von options, separates Feld)
🎯 POST /api/chat: Chat-Format und Kontextspeicherung
Kurze Antwort:
/api/chat – der Hauptendpunkt für Chat-Anwendungen.
Nimmt ein Array von Nachrichten mit Rollen (system, user, assistant) entgegen,
unterstützt Tool Calling und Streaming.
Um den Kontext zwischen den Anfragen zu speichern – übergeben Sie den vollständigen Nachrichtenverlauf.
LLMs haben kein Gedächtnis zwischen den Anfragen. Das "Gedächtnis" eines Chatbots –
ist einfach ein Array von Nachrichten, das Sie mit jeder Anfrage übergeben.
Je länger der Verlauf, desto mehr RAM und Zeit für die Antwort.
Grundlegende Anfrage
curl http://localhost:11434/api/chat \
-H "Content-Type: application/json" \
-d '{
"model": "llama3.2:3b",
"messages": [
{
"role": "system",
"content": "Sie sind ein Entwicklerassistent. Antworten Sie auf Ukrainisch, kurz."
},
{
"role": "user",
"content": "Was ist Dependency Injection?"
}
],
"stream": false,
"keep_alive": "10m"
}'
keep_alive: wie lange das Modell im Speicher halten
Standardmäßig entlädt Ollama das Modell nach 5 Minuten nach
der letzten Anfrage aus dem Speicher. Für einen Chatbot, bei dem Anfragen häufig kommen – bedeutet dies
eine Cold-Start-Verzögerung für jede neue Sitzung.
Ich bin damit in WebsCraft konfrontiert worden: Die erste Anfrage einer neuen Sitzung dauerte 8–12 Sekunden
anstelle von 1–2 – das Modell lud jedes Mal neu. Der Parameter keep_alive
löst dies:
# Modell 30 Minuten im Speicher halten
{"keep_alive": "30m"}
# Dauerhaft halten (bis Ollama neu gestartet wird)
{"keep_alive": -1}
# Sofort nach der Antwort entladen (für Batch-Aufgaben, bei denen RAM wichtig ist)
{"keep_alive": 0}
# Kann auch über eine Umgebungsvariable gesetzt werden (global für alle Modelle):
# OLLAMA_KEEP_ALIVE=30m ollama serve
Für einen Produktions-Chatbot verwende ich "keep_alive": "30m" –
das Modell bleibt zwischen den Sitzungen "heiß", wird aber entladen, wenn lange keine Anfragen kommen.
Kontextspeicherung (Multi-Turn)
# Python: vollständiger Multi-Turn-Chat-Zyklus
import requests
OLLAMA_URL = "http://localhost:11434/api/chat"
MODEL = "llama3.2:3b"
messages = [
{"role": "system", "content": "Sie sind ein technischer Assistent. Antworten Sie kurz."}
]
def chat(user_input: str) -> str:
messages.append({"role": "user", "content": user_input})
r = requests.post(OLLAMA_URL, json={
"model": MODEL,
"messages": messages,
"stream": False,
"keep_alive": "30m"
})
reply = r.json()["message"]["content"]
messages.append({"role": "assistant", "content": reply})
return reply
# Erste Anfrage
print(chat("Was ist Spring Boot?"))
# Zweite Anfrage – das Modell "erinnert" sich an die erste
print(chat("Was sind seine Hauptvorteile?"))
# Dritte – Fortsetzung des Kontexts
print(chat("Zeigen Sie ein minimales pom.xml dafür")
History kürzen: was tun, wenn der Kontext überläuft
Wenn der Chat lang ist – wächst der Verlauf und beginnt, den gesamten Kontext des Modells zu beanspruchen.
Wenn messages num_ctx überschreiten, verwirft Ollama stillschweigend
die ältesten Nachrichten. Um dies explizit zu steuern – implementieren Sie das Trimming manuell.
Ich verwende einen einfachen Ansatz: Ich speichere immer den System-Prompt,
und die User/Assistant-Nachrichten kürze ich auf die letzten N Paare:
def trim_history(messages: list, max_pairs: int = 10) -> list:
"""
Speichert den System-Prompt und die letzten max_pairs User/Assistant-Paare.
max_pairs=10 → maximal 21 Nachrichten (1 System + 20 User/Assistant)
"""
system = [m for m in messages if m["role"] == "system"]
dialog = [m for m in messages if m["role"] != "system"]
# Nimmt die letzten max_pairs * 2 Nachrichten (Paar = User + Assistant)
trimmed = dialog[-(max_pairs * 2):]
return system + trimmed
# Verwendung im Chat-Zyklus:
def chat_with_trim(user_input: str) -> str:
messages.append({"role": "user", "content": user_input})
# Kürzen vor jeder Anfrage
trimmed = trim_history(messages, max_pairs=10)
r = requests.post(OLLAMA_URL, json={
"model": MODEL,
"messages": trimmed,
"stream": False
})
reply = r.json()["message"]["content"]
messages.append({"role": "assistant", "content": reply})
return reply
Ein alternativer Ansatz – nach Tokens kürzen, nicht nach Anzahl der Nachrichten.
Aber für die meisten Anwendungen ist die Begrenzung nach Anzahl der Paare einfacher
und ausreichend vorhersehbar.
Antwortformat /api/chat
{
"model": "llama3.2:3b",
"created_at": "2026-05-01T10:00:00Z",
"message": {
"role": "assistant",
"content": "Spring Boot ist..."
},
"done": true,
"eval_count": 38,
"eval_duration": 876543210,
"total_duration": 987654321
}
Die Felder eval_count und eval_duration – sind dieselben wie bei
/api/generate, ermöglichen die Berechnung von tokens/sec für die Überwachung.
🎯 Streaming: warum und wie man es implementiert
Kurze Antwort:
Streaming – ist das Empfangen der Antwort Token für Token, anstatt als ein Block.
Standardmäßig streamt Ollama. Für UI – aktivieren Sie unbedingt das Streaming:
Das erste Token kommt in 1–3 Sek., und ohne Streaming wartet der Benutzer
auf die gesamte Antwort schweigend.
Mit stream: true erscheint das erste Token
in 1–3 Sekunden auf dem Bildschirm. Mit stream: false – der gesamte Text erscheint
nachdem das Modell die Generierung beendet hat, d.h. nach 5–30 Sekunden,
abhängig von der Länge der Antwort. Für interaktive Anwendungen – stream: true.
Realer Anwendungsfall: wie es bei AskYourDocs funktioniert
Ich habe das Streaming in meinem Dienst
AskYourDocs implementiert –
eine Anwendung, bei der der Benutzer Fragen zu seinen Dokumenten stellt und eine Antwort
von einem lokalen RAG-System auf Basis von Ollama erhält.
Ohne Streaming sahen die ersten Versionen des Dienstes so aus: Der Benutzer klickte auf "Senden",
sah einen Spinner 8–15 Sekunden lang, dann erschien sofort der gesamte Text.
Das Gefühl – als ob die Anwendung eingefroren wäre. Mit Streaming erscheinen die ersten Wörter
bereits nach 1–2 Sekunden und die Antwort "tippt" sich vor den Augen.
Der Unterschied im UX – ist frappierend, auch wenn die Gesamtgenerierungszeit gleich ist.
Architektur: Ollama streamt Tokens → Spring Boot liest den NDJSON-Stream
über WebFlux → gibt ihn über SSE (Server-Sent Events) an den Client weiter →
JavaScript im Frontend fügt Tokens einzeln zum DOM hinzu.
Streaming über curl
curl http://localhost:11434/api/chat \
-d '{
"model": "llama3.2:3b",
"messages": [{"role": "user", "content": "Erzähle mir von Microservices"}]
}'
# stream: true – Standard, kann weggelassen werden
Die Antwort kommt als Stream von JSON-Objekten, jedes in einer separaten Zeile (NDJSON):
{"model":"llama3.2:3b","message":{"role":"assistant","content":"Mikro"},"done":false}
{"model":"llama3.2:3b","message":{"role":"assistant","content":"services"},"done":false}
{"model":"llama3.2:3b","message":{"role":"assistant","content":" – sind"},"done":false}
...
{"model":"llama3.2:3b","message":{"role":"assistant","content":""},"done":true,"eval_count":87}
Streaming in Python
import requests, json
def stream_chat(model: str, messages: list):
r = requests.post(
"http://localhost:11434/api/chat",
json={"model": model, "messages": messages},
stream=True
)
full_response = ""
for line in r.iter_lines():
if line:
chunk = json.loads(line)
token = chunk["message"]["content"]
print(token, end="", flush=True)
full_response += token
if chunk.get("done"):
break
return full_response
stream_chat("llama3.2:3b", [
{"role": "user", "content": "Erkläre, was Docker ist"}
])
Streaming in Spring Boot über SSE
Genau diesen Ansatz verwende ich bei AskYourDocs: Spring Boot liest NDJSON von Ollama
und gibt die Tokens sofort über Server-Sent Events an den Client weiter.
Das Frontend empfängt die Tokens und fügt sie ohne Seitenneuladen zum DOM hinzu.
// OllamaStreamService.java
@Service
@RequiredArgsConstructor
public class OllamaStreamService {
private final WebClient ollamaWebClient;
/**
* Streamt Tokens von Ollama als Flux<String>.
* Jedes Element – ein Token der Modellantwort.
*/
public Flux<String> streamChat(String userMessage) {
var body = Map.of(
"model", "llama3.2:3b",
"messages", List.of(
Map.of("role", "system",
"content", "Antworten Sie auf Ukrainisch, kurz."),
Map.of("role", "user", "content", userMessage)
),
"stream", true,
"keep_alive", "30m"
);
return ollamaWebClient.post()
.uri("/api/chat")
.bodyValue(body)
.retrieve()
.bodyToFlux(String.class) // jede NDJSON-Zeile
.filter(line -> !line.isBlank())
.mapNotNull(line -> {
try {
var node = new ObjectMapper().readTree(line);
var token = node.path("message").path("content").asText("");
var done = node.path("done").asBoolean(false);
return done ? null : token; // null beendet den Flux
} catch (Exception e) {
return null;
}
});
}
}
// OllamaController.java – SSE-Endpunkt für das Frontend
@RestController
@RequestMapping("/api/ai")
@RequiredArgsConstructor
public class OllamaController {
private final OllamaStreamService streamService;
/**
* SSE-Endpunkt: Tokens kommen einzeln im Browser an.
* Das Frontend verbindet sich über EventSource oder fetch mit ReadableStream.
*/
@GetMapping(value = "/stream", produces = MediaType.TEXT_EVENT_STREAM_VALUE)
public Flux<String> stream(@RequestParam String message) {
return streamService.streamChat(message);
}
}
Frontend: SSE über fetch lesen
// Verbindung zum SSE-Endpunkt und Anzeige der Tokens in Echtzeit
async function streamAnswer(question, outputElement) {
const controller = new AbortController(); // zum Abbrechen des Streamings
const url = `/api/ai/stream?message=${encodeURIComponent(question)}`;
const res = await fetch(url, { signal: controller.signal });
const reader = res.body.getReader();
const decoder = new TextDecoder();
outputElement.textContent = ""; // vor der Antwort löschen
try {
while (true) {
const { done, value } = await reader.read();
if (done) break;
// SSE-Zeilen haben das Format "data: token\n\n"
const lines = decoder.decode(value).split("\n");
for (const line of lines) {
if (line.startsWith("data: ")) {
const token = line.slice(6); // "data: " entfernen
outputElement.textContent += token;
}
}
}
} catch (err) {
if (err.name !== "AbortError") console.error("Stream error:", err);
}
return controller; // zur Möglichkeit der Abbrechung zurückgeben
}
// Verwendung:
const stopBtn = document.getElementById("stop");
const output = document.getElementById("answer");
const controller = await streamAnswer("Was ist Spring Boot?", output);
// "Generierung stoppen"-Button
stopBtn.onclick = () => controller.abort();
Streaming abbrechen: "Stopp"-Button
Bei AskYourDocs habe ich einen "Stopp"-Button hinzugefügt – wenn die Antwort zu lang ist
oder das Modell in die falsche Richtung geht. Realisiert über AbortController
im Frontend (oben gezeigt) und das Abbrechen des Flux im Backend:
// Spring Boot: bricht die Anfrage an Ollama automatisch ab
// wenn der Client die Verbindung trennt (Browser hat die SSE-Verbindung geschlossen)
// WebFlux tut dies automatisch über Flux.takeUntilOther oder
// über den onCancel-Operator:
public Flux<String> streamChat(String userMessage) {
return ollamaWebClient.post()
.uri("/api/chat")
.bodyValue(body)
.retrieve()
.bodyToFlux(String.class)
.doOnCancel(() ->
log.info("Client hat die Verbindung getrennt, Streaming abgebrochen"))
// ... restliche Operatoren
}
WebFlux bricht die Upstream-Anfrage an Ollama automatisch ab, wenn der Client
die SSE-Verbindung schließt – das Modell stoppt die Generierung und gibt RAM frei.
Das ist wichtig: Ohne korrektes Abbrechen generiert das Modell weiter,
auch nachdem der Benutzer die Registerkarte geschlossen hat.
🎯 POST /api/embed: Embeddings für RAG
Kurze Antwort:
/api/embed generiert numerische Vektoren (Embeddings) für Text.
Diese Vektoren sind für die semantische Suche unerlässlich – die Grundlage der RAG-Architektur.
Das beste lokale Modell für Embeddings ist nomic-embed-text.
Wenn du noch nicht verstehst, was Embeddings sind, beginne mit
dem Artikel „Was sind Embeddings: Wie KI den Sinn von Text versteht“,
bevor du weitermachst.
Wie ich /api/embed in WebsCraft verwende
In meiner RAG-Pipeline auf WebsCraft verwende ich nomic-embed-text
über /api/embed für zwei Aufgaben: Indexierung von Blogartikeln bei der Veröffentlichung
und Suche nach relevanten Artikeln bei Benutzeranfragen an den Chatbot.
Warum nomic-embed-text: Die Dimensionalität von 768 ist ausreichend für die semantische Suche,
schnelle Generierung (~50ms pro Chunk), minimale RAM-Nutzung (~274 MB).
Bei der lokalen Entwicklung kann ich sowohl das Embedding-Modell als auch das generative Modell gleichzeitig
auf einem Mac M1 mit 16 GB ausführen – sie konkurrieren nicht um Speicher.
In der Produktion verwende ich über OpenRouter openai/text-embedding-3-small,
aber lokal zum Testen immer nomic-embed-text.
Installation des Modells für Embeddings
ollama pull nomic-embed-text
Anfrage über curl
curl http://localhost:11434/api/embed \
-H "Content-Type: application/json" \
-d '{
"model": "nomic-embed-text",
"input": "Spring Boot ist ein Framework für Java-Anwendungen"
}'
Antwortformat
{
"model": "nomic-embed-text",
"embeddings": [
[0.1234, -0.5678, 0.9012, ...]
],
"total_duration": 12345678,
"load_duration": 1234567,
"prompt_eval_count": 9
}
Das Feld embeddings ist ein Array von Arrays (mehrere Texte können auf einmal übergeben werden).
nomic-embed-text gibt einen Vektor der Größe 768 zurück.
Batch-Embeddings (mehrere Texte auf einmal)
curl http://localhost:11434/api/embed \
-d '{
"model": "nomic-embed-text",
"input": [
"Erster Satz für Embedding",
"Zweiter Satz für Embedding",
"Dritter Satz für Embedding"
]
}'
Funktion für RAG in Python
import requests
import numpy as np
def embed(texts: list[str], model: str = "nomic-embed-text") -> list[list[float]]:
"""Generiert Embeddings für eine Liste von Texten."""
r = requests.post(
"http://localhost:11434/api/embed",
json={"model": model, "input": texts}
)
return r.json()["embeddings"]
def cosine_similarity(a: list, b: list) -> float:
"""Kosinus-Ähnlichkeit zwischen zwei Vektoren."""
a, b = np.array(a), np.array(b)
return float(np.dot(a, b) / (np.linalg.norm(a) * np.linalg.norm(b)))
# Beispiel: Suche nach dem nächstgelegenen Dokument
query = "Wie konfiguriere ich Spring Boot?"
docs = [
"Spring Boot Autokonfiguration vereinfacht die Einrichtung",
"Python Flask – ein leichtgewichtiges Web-Framework",
"Maven – ein Build-Tool für Java-Projekte"
]
q_emb = embed([query])[0]
d_embs = embed(docs)
scores = [(doc, cosine_similarity(q_emb, d_emb))
for doc, d_emb in zip(docs, d_embs)]
scores.sort(key=lambda x: x[1], reverse=True)
print(f"Am relevantesten: {scores[0][0]} ({scores[0][1]:.3f})")
Embeddings in Java über WebClient
// EmbeddingService.java
@Service
@RequiredArgsConstructor
public class EmbeddingService {
private final WebClient ollamaWebClient;
private static final String EMBED_MODEL = "nomic-embed-text";
/**
* Generiert ein Embedding für einen einzelnen Text.
* Gibt einen Vektor der Größe 768 für nomic-embed-text zurück.
*/
public Mono<List<Double>> embed(String text) {
return embedBatch(List.of(text))
.map(embeddings -> embeddings.get(0));
}
/**
* Batch-Embeddings: mehrere Texte in einer Anfrage.
* Effizienter als mehrere separate Anfragen.
*/
public Mono<List<List<Double>>> embedBatch(List<String> texts) {
var body = Map.of("model", EMBED_MODEL, "input", texts);
return ollamaWebClient.post()
.uri("/api/embed")
.bodyValue(body)
.retrieve()
.bodyToMono(EmbedResponse.class)
.map(EmbedResponse::embeddings);
}
/**
* Kosinus-Ähnlichkeit zwischen zwei Vektoren.
*/
public double cosineSimilarity(List<Double> a, List<Double> b) {
double dot = 0, normA = 0, normB = 0;
for (int i = 0; i < a.size(); i++) {
dot += a.get(i) * b.get(i);
normA += a.get(i) * a.get(i);
normB += b.get(i) * b.get(i);
}
return dot / (Math.sqrt(normA) * Math.sqrt(normB));
}
// DTO für die Ollama-Antwort
record EmbedResponse(List<List<Double>> embeddings) {}
}
// Verwendung im RAG-Service:
@Service
@RequiredArgsConstructor
public class RagService {
private final EmbeddingService embeddingService;
public Mono<String> findMostRelevant(String query, List<String> docs) {
return embeddingService.embed(query).flatMap(queryVec ->
embeddingService.embedBatch(docs).map(docVecs -> {
double bestScore = -1;
String bestDoc = "";
for (int i = 0; i < docs.size(); i++) {
double score = embeddingService
.cosineSimilarity(queryVec, docVecs.get(i));
if (score > bestScore) {
bestScore = score;
bestDoc = docs.get(i);
}
}
return bestDoc;
})
);
}
}
In der Praxis ist es besser, anstelle der manuellen Kosinus-Ähnlichkeit
eine Vektordatenbank (pgvector, Chroma, Qdrant) zu verwenden – sie indizieren Vektoren
und suchen in Millionen von Einträgen in Millisekunden.
Manuelle Berechnungen eignen sich für Prototypen und kleine Sammlungen bis zu ~1000 Dokumenten.
Mehr über die Auswahl von Embedding-Modellen für RAG –
im Artikel Embedding-Modelle für RAG im Jahr 2026: Auswahl und Vergleich.
🎯 Tool Calling: Anbindung externer Funktionen
Kurze Antwort:
Tool Calling ist die Fähigkeit des Modells, externe Funktionen zu „aufrufen“.
Das Modell führt die Funktion nicht selbst aus – es gibt ein JSON mit dem Funktionsnamen und den Argumenten zurück,
und dein Code führt den eigentlichen Aufruf aus und übergibt das Ergebnis zurück.
Unterstützt über /api/chat mit dem Parameter tools.
Bevor du weiterliest – ich empfehle,
den Artikel „Tool Use vs Function Calling: Wie es funktioniert und was es mit RAG zu tun hat“ zu lesen –
dort wird erklärt, warum LLMs Funktionen in JSON beschreiben und nicht ausführen,
und der vollständige Aufrufzyklus mit Beispielen.
Welches Modell unterstützt Tool Calling?
Nicht alle Modelle unterstützen Tool Calling. Unterstützt werden: Llama 3.1/3.2/3.3, Qwen 2.5, Mistral 7B (v0.3+), DeepSeek R1.
ollama pull llama3.2:3b
Basis-Anfrage mit Tools
curl http://localhost:11434/api/chat \
-H "Content-Type: application/json" \
-d '{
"model": "llama3.2:3b",
"messages": [
{"role": "user", "content": "Wie ist das Wetter in Charkiw?"}
],
"tools": [
{
"type": "function",
"function": {
"name": "get_weather",
"description": "Aktuelles Wetter für eine Stadt abrufen",
"parameters": {
"type": "object",
"properties": {
"city": {
"type": "string",
"description": "Name der Stadt"
},
"units": {
"type": "string",
"enum": ["celsius", "fahrenheit"],
"description": "Temperatureinheiten"
}
},
"required": ["city"]
}
}
}
],
"stream": false
}'
Antwort mit tool_calls
{
"message": {
"role": "assistant",
"content": "",
"tool_calls": [
{
"function": {
"name": "get_weather",
"arguments": {
"city": "Харків",
"units": "celsius"
}
}
}
]
},
"done": true
}
Vollständiger Tool-Calling-Zyklus in Python
import requests, json
OLLAMA_URL = "http://localhost:11434/api/chat"
MODEL = "llama3.2:3b"
tools = [{
"type": "function",
"function": {
"name": "get_weather",
"description": "Aktuelles Wetter für eine Stadt abrufen",
"parameters": {
"type": "object",
"properties": {
"city": {"type": "string", "description": "Name der Stadt"}
},
"required": ["city"]
}
}
}]
def get_weather(city: str) -> str:
return f"In {city}: +18°C, bewölkt"
def chat_with_tools(user_message: str) -> str:
messages = [{"role": "user", "content": user_message}]
r = requests.post(OLLAMA_URL, json={
"model": MODEL,
"messages": messages,
"tools": tools,
"stream": False
})
assistant_msg = r.json()["message"]
messages.append(assistant_msg)
if assistant_msg.get("tool_calls"):
for tool_call in assistant_msg["tool_calls"]:
fn_name = tool_call["function"]["name"]
fn_args = tool_call["function"]["arguments"]
result = get_weather(**fn_args) if fn_name == "get_weather" else "unknown tool"
messages.append({"role": "tool", "content": result})
r2 = requests.post(OLLAMA_URL, json={
"model": MODEL, "messages": messages, "stream": False
})
return r2.json()["message"]["content"]
# Das Modell hat mit Text geantwortet – Tool wurde nicht aufgerufen
return assistant_msg["content"]
print(chat_with_tools("Wie ist das Wetter in Charkiw?"))
Tool Calling in Java über WebClient
// ToolCallingService.java
@Service
@RequiredArgsConstructor
public class ToolCallingService {
private final WebClient ollamaWebClient;
private static final String MODEL = "llama3.2:3b";
private static final String OLLAMA_URL = "http://localhost:11434";
// Beschreibung des Tools im JSON Schema-Format
private static final Map<String, Object> WEATHER_TOOL = Map.of(
"type", "function",
"function", Map.of(
"name", "get_weather",
"description", "Aktuelles Wetter für eine Stadt abrufen",
"parameters", Map.of(
"type", "object",
"properties", Map.of(
"city", Map.of(
"type", "string",
"description", "Name der Stadt"
)
),
"required", List.of("city")
)
)
);
public Mono<String> chatWithTools(String userMessage) {
var messages = new ArrayList<>(List.of(
Map.of("role", "user", "content", userMessage)
));
// Schritt 1: Erste Anfrage mit Tools
return callOllama(messages, true)
.flatMap(response -> {
var msg = (Map<?, ?>) response.get("message");
var toolCalls = (List<?>) msg.get("tool_calls");
// Wenn das Modell kein Tool aufgerufen hat – Text zurückgeben
if (toolCalls == null || toolCalls.isEmpty()) {
return Mono.just((String) msg.get("content"));
}
// Schritt 2: Tatsächliche Aufrufe ausführen
messages.add(msg);
for (var tc : toolCalls) {
var fn = (Map<?, ?>) ((Map<?, ?>) tc).get("function");
var fnName = (String) fn.get("name");
var args = (Map<?, ?>) fn.get("arguments");
var result = executeFunction(fnName, args);
messages.add(Map.of("role", "tool", "content", result));
}
// Schritt 3: Finale Anfrage mit Ergebnis
return callOllama(messages, false)
.map(r -> (String) ((Map<?, ?>) r.get("message")).get("content"));
});
}
private Mono<Map> callOllama(List<?> messages, boolean withTools) {
var body = new HashMap<>();
body.put("model", MODEL);
body.put("messages", messages);
body.put("stream", false);
if (withTools) {
body.put("tools", List.of(WEATHER_TOOL));
}
return ollamaWebClient.post()
.uri("/api/chat")
.bodyValue(body)
.retrieve()
.bodyToMono(Map.class)
.timeout(Duration.ofSeconds(60));
}
// Registrierung von Funktionen – füge hier neue Tools hinzu
private String executeFunction(String name, Map<?, ?> args) {
return switch (name) {
case "get_weather" -> getWeather((String) args.get("city"));
default -> "Unbekanntes Tool: " + name;
};
}
private String getWeather(String city) {
// Hier der tatsächliche Aufruf der Wetter-API
return "In " + city + ": +18°C, bewölkt";
}
}
⚠️ Häufiger Fehler: Modell hat kein Tool aufgerufen
Das Modell ist nicht verpflichtet, ein Tool aufzurufen – es kann auch dann mit Text antworten,
wenn Tools übergeben wurden. Dies geschieht, wenn:
- ✔️ Die Frage nach Meinung des Modells keine externen Daten benötigt
- ✔️ Die Funktionsbeschreibung (
description) unklar ist oder nicht zur Frage passt
- ✔️ Das Modell Tool Calling nicht unterstützt (siehe Liste oben)
Daher immer prüfen, ob tool_calls in der Antwort vorhanden sind,
und beide Fälle behandeln – mit und ohne Aufruf:
# Python: korrekte Prüfung
assistant = response["message"]
if assistant.get("tool_calls"):
# Das Modell möchte ein Tool aufrufen – ausführen
...
else:
# Das Modell hat mit Text geantwortet – wie es ist zurückgeben
return assistant["content"]
Wenn du möchtest, dass das Modell immer ein bestimmtes Tool aufruft –
verwende den Parameter tool_choice (unterstützt über /v1/):
curl http://localhost:11434/v1/chat/completions \
-d '{
"model": "llama3.2:3b",
"messages": [...],
"tools": [...],
"tool_choice": {"type": "function", "function": {"name": "get_weather"}}
}'
Mehr darüber, wie das Modell entscheidet, wann es ein Tool aufruft –
im Artikel Wie LLMs entscheiden, wann sie ein Tool aufrufen: Entscheidungsmechanik.
🎯 Beispiel in Java: WebClient + Spring Boot
Kurze Antwort:
Für Spring Boot gibt es zwei Ansätze: direkter Aufruf über WebClient (flexibel, ohne Abhängigkeiten)
oder über Spring AI (bequemer, aber zusätzliche Bibliothek).
Unten – beide Varianten mit funktionierendem Code.
RestTemplate in Spring 6+ ist deprecated. Verwende WebClient für
nicht-blockierende HTTP-Anfragen an Ollama – besonders wichtig für das Streaming
von Antworten in Echtzeit.
⚠️ Wichtig: Der Code unten ist demonstrationsweise. Sein Zweck ist es,
die grundlegende Mechanik der Interaktion mit der Ollama API zu zeigen, nicht eine fertige Vorlage für die Produktion.
Jedes Projekt hat seine eigene Architektur: andere Paketstruktur, andere Fehlerbehandlung,
andere Art der Speicherung von Konfigurationen. Passe es an deine Bedürfnisse an.
Variante 1: WebClient – ohne zusätzliche Abhängigkeiten
Abhängigkeiten in pom.xml:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-webflux</artifactId>
</dependency>
Konfiguration in application.properties
(URL ist aus dem Code ausgelagert – nicht in Beans hartcodieren):
ollama.base-url=http://localhost:11434
ollama.model=llama3.2:3b
ollama.timeout-seconds=60
WebClient-Konfiguration:
// WebClientConfig.java
@Configuration
public class WebClientConfig {
@Value("${ollama.base-url}")
private String ollamaBaseUrl;
@Bean
public WebClient ollamaWebClient() {
return WebClient.builder()
.baseUrl(ollamaBaseUrl)
.defaultHeader(HttpHeaders.CONTENT_TYPE,
MediaType.APPLICATION_JSON_VALUE)
.codecs(c -> c.defaultCodecs()
.maxInMemorySize(10 * 1024 * 1024)) // 10MB
.build();
}
}
DTOs für Anfrage und Antwort:
// OllamaChatRequest.java
public record OllamaChatRequest(
String model,
List<Message> messages,
boolean stream
) {
public record Message(String role, String content) {}
}
// OllamaChatResponse.java
public record OllamaChatResponse(
String model,
Message message,
boolean done
) {
public record Message(String role, String content) {}
}
Service mit Streaming-Unterstützung:
// OllamaService.java
@Service
@RequiredArgsConstructor
public class OllamaService {
private final WebClient ollamaWebClient;
@Value("${ollama.model}")
private String defaultModel;
@Value("${ollama.timeout-seconds:60}")
private int timeoutSeconds;
// Normale Anfrage (ohne Streaming)
public Mono<String> chat(String userMessage) {
var request = new OllamaChatRequest(
defaultModel,
List.of(new OllamaChatRequest.Message("user", userMessage)),
false
);
return ollamaWebClient.post()
.uri("/api/chat")
.bodyValue(request)
.retrieve()
.bodyToMono(OllamaChatResponse.class)
.timeout(Duration.ofSeconds(timeoutSeconds))
.map(r -> r.message().content())
.onErrorResume(e -> Mono.just("Fehler: " + e.getMessage()));
}
// Streaming (SSE für das Frontend)
public Flux<String> chatStream(String userMessage) {
var body = Map.of(
"model", defaultModel,
"messages", List.of(Map.of("role", "user", "content", userMessage)),
"stream", true
);
return ollamaWebClient.post()
.uri("/api/chat")
.bodyValue(body)
.retrieve()
.bodyToFlux(String.class)
.filter(line -> !line.isBlank())
.map(line -> {
try {
var obj = new ObjectMapper().readTree(line);
return obj.path("message").path("content").asText("");
} catch (Exception e) {
return "";
}
})
.filter(token -> !token.isEmpty());
}
}
REST-Controller:
// OllamaController.java
@RestController
@RequestMapping("/api/ai")
@RequiredArgsConstructor
public class OllamaController {
private final OllamaService ollamaService;
@PostMapping("/chat")
public Mono<Map<String, String>> chat(@RequestBody Map<String, String> req) {
return ollamaService.chat(req.get("message"))
.map(r -> Map.of("response", r));
}
@GetMapping(value = "/chat/stream", produces = MediaType.TEXT_EVENT_STREAM_VALUE)
public Flux<String> chatStream(@RequestParam String message) {
return ollamaService.chatStream(message);
}
}
// Test:
// curl -X POST http://localhost:8080/api/ai/chat \
// -H "Content-Type: application/json" \
// -d '{"message": "Was ist Spring WebFlux?"}'
Variante 2: Spring AI – minimaler Code
Abhängigkeiten:
<dependency>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-starter-model-ollama</artifactId>
</dependency>
Konfiguration in application.properties:
spring.ai.ollama.base-url=http://localhost:11434
spring.ai.ollama.chat.options.model=llama3.2:3b
spring.ai.ollama.chat.options.temperature=0.7
spring.ai.ollama.init.pull-model-strategy=never
Service über Spring AI – normale Anfrage und Streaming:
@Service
@RequiredArgsConstructor
public class SpringAiOllamaService {
private final ChatClient chatClient;
// Normale Anfrage
public String ask(String question) {
return chatClient.prompt()
.user(question)
.call()
.content();
}
// Streaming über Spring AI
public Flux<String> stream(String question) {
return chatClient.prompt()
.user(question)
.stream()
.content();
}
}
Wann was wählen:
- ✔️ WebClient – vollständige Kontrolle über die Anfrage, Streaming, Konfiguration von Timeouts und Fehlerbehandlung. Ohne zusätzliche Abhängigkeiten.
- ✔️ Spring AI – schneller Start und einfaches Wechseln zwischen Anbietern (Ollama → OpenAI → Anthropic) ohne Codeänderung.
🎯 Beispiel in Python
Kurze Antwort:
Zwei Ansätze: native Bibliothek ollama (einfacher, mehr Funktionen)
oder openai SDK mit base_url (wenn bereits OpenAI-Code vorhanden ist).
⚠️ Wichtig: Die Beispiele unten sind demonstrationsweise.
Sie zeigen die Mechanik der API-Arbeit, nicht eine fertige Anwendungsarchitektur.
In einem realen Projekt füge Fehlerbehandlung, Logging, Konfiguration über Umgebungsvariablen
und eine entsprechende Modulstruktur hinzu.
Variante 1: native Bibliothek ollama
pip install ollama
import ollama
# Einfache Anfrage
response = ollama.chat(
model="llama3.2:3b",
messages=[{"role": "user", "content": "Was ist eine REST API?"}]
)
print(response["message"]["content"])
# Streaming
for chunk in ollama.chat(
model="llama3.2:3b",
messages=[{"role": "user", "content": "Erzähle über Microservices"}],
stream=True
):
print(chunk["message"]["content"], end="", flush=True)
# Embeddings
emb = ollama.embed(model="nomic-embed-text", input="Hello world")
print(f"Dimension: {len(emb['embeddings'][0])}")
Variante 2: OpenAI SDK (Drop-in-Ersatz)
pip install openai
from openai import OpenAI
# Einzige Änderung im Vergleich zu OpenAI: base_url und api_key (wird ignoriert)
client = OpenAI(
base_url="http://localhost:11434/v1",
api_key="ollama"
)
# Weiter – Standard-OpenAI-Code ohne Änderungen
response = client.chat.completions.create(
model="llama3.2:3b",
messages=[
{"role": "system", "content": "Du bist ein technischer Assistent."},
{"role": "user", "content": "Was ist Docker?"}
]
)
print(response.choices[0].message.content)
# Streaming über OpenAI SDK
stream = client.chat.completions.create(
model="llama3.2:3b",
messages=[{"role": "user", "content": "Erzähle über CI/CD"}],
stream=True
)
for chunk in stream:
if chunk.choices[0].delta.content:
print(chunk.choices[0].delta.content, end="", flush=True)
🎯 Beispiel in JavaScript / Node.js
⚠️ Wichtig: Die folgenden Beispiele sind Demonstrationen.
Ziel ist es, die grundlegende Mechanik des Aufrufs der Ollama API mit JavaScript zu zeigen.
In einer realen Anwendung wird die Struktur anders sein: separate Module, Fehlerbehandlung,
Umgebungsvariablen für URL und Modellnamen.
Variante 1: native Bibliothek ollama
npm install ollama
import ollama from "ollama";
// Einfache Anfrage
const response = await ollama.chat({
model: "llama3.2:3b",
messages: [{ role: "user", content: "Was ist eine REST API?" }],
});
console.log(response.message.content);
// Streaming
const stream = await ollama.chat({
model: "llama3.2:3b",
messages: [{ role: "user", content: "Erzähl mir etwas über Microservices" }],
stream: true,
});
for await (const chunk of stream) {
process.stdout.write(chunk.message.content);
}
// Embeddings
const emb = await ollama.embed({
model: "nomic-embed-text",
input: "Hello world",
});
console.log(`Dimension: ${emb.embeddings[0].length}`);
Variante 2: fetch API (ohne Abhängigkeiten)
async function chatWithOllama(message) {
const res = await fetch("http://localhost:11434/api/chat", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
model: "llama3.2:3b",
messages: [{ role: "user", content: message }],
stream: false,
}),
});
const data = await res.json();
return data.message.content;
}
// Streaming über ReadableStream
async function streamChat(message, onToken) {
const res = await fetch("http://localhost:11434/api/chat", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
model: "llama3.2:3b",
messages: [{ role: "user", content: message }],
}),
});
const reader = res.body.getReader();
const decoder = new TextDecoder();
while (true) {
const { done, value } = await reader.read();
if (done) break;
const lines = decoder.decode(value).split("\n").filter(Boolean);
for (const line of lines) {
const chunk = JSON.parse(line);
onToken(chunk.message.content);
if (chunk.done) return;
}
}
}
streamChat("Was ist GraphQL?", (token) => process.stdout.write(token));
🎯 Modellverwaltung und Health Check über API
Wenn Ollama nicht als lokales CLI-Tool, sondern als Server
in einer realen Anwendung verwendet wird, entstehen Fragen, die über einfaches Chatten hinausgehen:
Wie prüft man, ob Ollama vor dem Senden einer Anfrage läuft,
wie vermeidet man die Verzögerung beim ersten Aufruf einer Sitzung (Cold Start),
wie lädt man das benötigte Modell automatisch beim Start der Anwendung,
wie überwacht man, wie viel Speicher ein Modell in der Produktion belegt.
Für all dies gibt es separate Endpunkte.
Ich verwende diese Endpunkte in WebsCraft für zwei Aufgaben:
Health Check beim Start von Spring Boot – ich prüfe, ob Ollama verfügbar ist, bevor
ich AI-Routen registriere, und /api/ps in den Logs – um zu sehen,
wann das Modell entladen wird und wie viel VRAM es zwischen den Anfragen belegt.
GET /api/tags — Liste der installierten Modelle
curl http://localhost:11434/api/tags
# Antwort:
{
"models": [
{
"name": "llama3.2:3b",
"size": 2019393423,
"details": {
"parameter_size": "3B",
"quantization_level": "Q4_K_M"
}
}
]
}
Nützlich beim Start der Anwendung: Prüfen Sie, ob das benötigte Modell installiert ist,
und laden Sie es gegebenenfalls über /api/pull herunter (oder geben Sie einen Fehler zurück).
GET /api/ps — laufende Modelle und VRAM
curl http://localhost:11434/api/ps
# Antwort:
{
"models": [
{
"name": "llama3.2:3b",
"size_vram": 2145386496,
"expires_at": "2026-05-01T10:05:00Z"
}
]
}
Nützlich vor einer Anfrage: Wenn das Modell bereits geladen ist (/api/ps ist nicht leer) –
die erste Anfrage wird ohne Cold-Start-Verzögerung erfolgen.
Das Feld expires_at zeigt an, wann das Modell aus dem Speicher entladen wird
(standardmäßig 5 Minuten nach der letzten Anfrage).
GET / — Health Check
curl http://localhost:11434/
# Gibt zurück: "Ollama is running"
# Nützlich in Startskripten:
if curl -s http://localhost:11434/ | grep -q "running"; then
echo "Ollama ist bereit"
else
echo "Ollama ist nicht gestartet – starte..."
ollama serve &
fi
In Spring Boot können Sie einen Health Check über @EventListener(ApplicationReadyEvent.class) durchführen
– nach dem Start der Anwendung prüfen Sie die Verfügbarkeit von Ollama und loggen Sie das Ergebnis:
@Component
@RequiredArgsConstructor
public class OllamaHealthChecker {
private final WebClient ollamaWebClient;
@EventListener(ApplicationReadyEvent.class)
public void checkOllamaOnStartup() {
ollamaWebClient.get()
.uri("/api/tags")
.retrieve()
.bodyToMono(Map.class)
.subscribe(
resp -> log.info("Ollama ist verfügbar, Modelle: {}",
((List>) resp.get("models")).size()),
err -> log.warn("Ollama ist nicht verfügbar: {}", err.getMessage())
);
}
}
POST /api/pull — Modell über API herunterladen
curl http://localhost:11434/api/pull \
-d '{"name": "llama3.2:3b"}'
# Python mit Fortschritt:
import requests, json
def pull_model(name: str):
r = requests.post("http://localhost:11434/api/pull",
json={"name": name}, stream=True)
for line in r.iter_lines():
if line:
status = json.loads(line)
if "total" in status and "completed" in status:
pct = 100 * status["completed"] / status["total"]
print(f"\r{status['status']} {pct:.1f}%", end="")
else:
print(status.get("status", ""))
pull_model("nomic-embed-text")
Nützlich im Docker-Entrypoint oder in einer CI/CD-Pipeline –
Sie laden die benötigten Modelle automatisch beim ersten Deployment herunter,
ohne manuelles ollama pull auf dem Server.
🎯 Fehlerbehandlung, Timeouts, OLLAMA_HOST
Typische Fehler und wie man sie behandelt
| Fehler |
Ursache |
Lösung |
Connection refused :11434 |
Ollama ist nicht gestartet |
Starten Sie ollama serve oder die Ollama-Anwendung |
404 model not found |
Modell nicht heruntergeladen |
ollama pull model-name |
| Timeout ohne Antwort |
Modell zu groß / Cold Start |
Timeout auf 120s erhöhen oder Modell im Voraus laden |
500 out of memory |
Nicht genügend RAM |
Kleineres Modell oder Q4 statt Q8 wählen |
404 at /v1/chat/completions |
/api/ und /v1/ verwechselt |
OpenAI SDK → base_url = localhost:11434/v1 |
| Antwort mitten im Satz abgeschnitten |
num_predict zu klein (Standard 128) |
num_predict erhöhen oder auf -1 setzen (kein Limit) |
Der letzte Fehler in der Tabelle ist am wenigsten offensichtlich. Ich bin selbst darauf gestoßen,
als Antworten plötzlich mitten in einer Erklärung abgeschnitten wurden.
Ursache: Standardmäßig begrenzen einige Ollama-Builds die Generierung auf 128 Token.
Lösung – explizit num_predict angeben:
# In der Anfrage über /api/chat oder /api/generate:
{
"model": "llama3.2:3b",
"messages": [...],
"options": {
"num_predict": -1 // -1 = kein Limit
// oder eine bestimmte Zahl:
// "num_predict": 2048
}
}
Empfohlene Timeouts
Python (requests):
# Tupel (connect_timeout, read_timeout)
requests.post(url, json=body, timeout=(10, 120))
# 10s für die Verbindung, 120s für das Lesen der Antwort
# Für große Modelle oder lange Antworten – read auf 300s erhöhen
Java (WebClient):
// WebClient hat zwei Timeout-Ebenen – beide sind erforderlich
// 1. Timeout auf Ebene des HTTP-Clients (TCP-Verbindung und Lesen)
@Bean
public WebClient ollamaWebClient() {
HttpClient httpClient = HttpClient.create()
.option(ChannelOption.CONNECT_TIMEOUT_MILLIS, 10_000) // 10s für die Verbindung
.responseTimeout(Duration.ofSeconds(120)); // 120s für die Antwort
return WebClient.builder()
.baseUrl(ollamaBaseUrl)
.clientConnector(new ReactorClientHttpConnector(httpClient))
.codecs(c -> c.defaultCodecs().maxInMemorySize(10 * 1024 * 1024))
.build();
}
// 2. Reaktiver Timeout auf Mono/Flux-Ebene (für spezifische Anfragen)
ollamaWebClient.post()
.uri("/api/chat")
.bodyValue(body)
.retrieve()
.bodyToMono(OllamaChatResponse.class)
.timeout(Duration.ofSeconds(120)) // zusätzlicher Schutz
.onErrorMap(TimeoutException.class,
e -> new RuntimeException("Ollama hat nicht innerhalb von 120s geantwortet"));
⚠️ Für Streaming (bodyToFlux) wird der Timeout auf Flux-Ebene ausgelöst,
wenn zwischen den Token mehr als die angegebene Zeit vergeht – das ist nicht immer erwünscht.
Für Streaming ist es besser, sich nur auf den responseTimeout
auf HttpClient-Ebene zu verlassen.
OLLAMA_HOST — Start auf einem anderen Host
# Ollama netzwerkfähig machen (nicht nur localhost)
OLLAMA_HOST=0.0.0.0:11434 ollama serve
# Oder in Docker:
docker run -e OLLAMA_HOST=0.0.0.0:11434 ollama/ollama
# Im Python-Client – localhost durch die IP des Servers ersetzen:
client = OpenAI(base_url="http://192.168.1.100:11434/v1", api_key="ollama")
# In Java application.properties:
ollama.base-url=http://192.168.1.100:11434
⚠️ Achtung: Wenn Sie Ollama nach außen öffnen – fügen Sie eine Authentifizierung hinzu
oder beschränken Sie den Zugriff über eine Firewall. Standardmäßig erfordert Ollama keine
Authentifizierung – jeder im Netzwerk kann Anfragen senden.
❓ Häufig gestellte Fragen (FAQ)
Was ist der Unterschied zwischen /api/generate und /api/chat?
/api/generate nimmt einen Prompt-String entgegen und gibt Text zurück.
/api/chat nimmt ein Array von Nachrichten mit Rollen (system, user, assistant) entgegen
und unterstützt Tool Calling. Für Chatbots und Anwendungen mit Kontext – immer /api/chat.
Für Batch-Generierung ohne Kontext – /api/generate ist bequemer.
Wie speichert man den Kontext zwischen Anfragen?
Ollama speichert den Kontext nicht automatisch. Für Multi-Turn-Chats
senden Sie die vollständige Nachrichtenhistorie bei jeder Anfrage:
nach jeder Antwort fügen Sie sie dem Nachrichtenarray hinzu und senden
das gesamte Array bei der nächsten Anfrage.
Welchen Timeout soll ich für Anfragen einstellen?
Abhängig von der Modellgröße und der Länge der Antwort. Für 3B-Modelle – 30–60 Sekunden.
Für 8B – 60–120 Sekunden. Für die erste Anfrage nach dem Start (Cold Start) –
fügen Sie weitere 10–30 Sekunden für das Laden des Modells in den Speicher hinzu.
Benötige ich einen API-Schlüssel für Ollama?
Für die native API (/api/*) – nein, Authentifizierung ist nicht erforderlich.
Für die OpenAI-kompatible API (/v1/*) – einige SDKs verlangen die Übergabe eines
api_key, aber Ollama ignoriert ihn. Übergeben Sie eine beliebige Zeichenkette: "ollama".
Wie starte ich die Ollama API in Docker?
docker run -d -p 11434:11434 ollama/ollama – und die API ist verfügbar
unter http://localhost:11434. Für GPU-Beschleunigung:
docker run --gpus all -p 11434:11434 ollama/ollama.
Kann ich Ollama in Spring Boot ohne Spring AI verwenden?
Ja. WebClient oder RestClient reichen für direkte HTTP-Anfragen an die Ollama API aus.
Spring AI ist bequemer, wenn Sie zwischen Anbietern wechseln möchten
(Ollama → OpenAI → Anthropic), ohne den Code zu ändern. Für einfache Integration –
WebClient ist völlig ausreichend.
Wie erfahre ich, wie viele Token/Sekunde ein Modell ausgibt?
Ollama gibt in jeder Antwort Metadaten zurück – die Felder eval_count
(Anzahl der generierten Token) und eval_duration (Zeit in Nanosekunden).
Teilen Sie das eine durch das andere:
# Python
data = requests.post(...).json()
tok_per_sec = data["eval_count"] / (data["eval_duration"] / 1e9)
print(f"{tok_per_sec:.1f} tok/s")
Für ein 3B-Modell auf einem Mac M1 – erwarten Sie 20–30 tok/s.
Für 8B – 10–15 tok/s. Wenn Sie weniger als 5 tok/s erhalten –
das Modell ist zu groß für die Hardware oder swappt teilweise auf die Festplatte.
Warum ruft das Modell kein Tool auf, auch wenn Tools übergeben werden?
Das Modell ist nicht verpflichtet, ein Tool aufzurufen – es kann mit Text antworten,
wenn es entscheidet, dass externe Daten nicht benötigt werden. Die drei häufigsten Gründe:
eine unklare description der Funktion (das Modell versteht nicht, wann es aufgerufen werden soll),
die Frage erfordert nach Meinung des Modells keine externen Daten,
oder das Modell unterstützt kein Tool Calling (prüfen Sie die Liste der unterstützten:
Llama 3.1+, Qwen 2.5, Mistral v0.3+).
Prüfen Sie immer auf das Vorhandensein von tool_calls in der Antwort und behandeln Sie
beide Fälle – mit und ohne Aufruf.
✅ Schlussfolgerungen
Die Ollama REST API ist ein einfaches und leistungsfähiges Werkzeug zur Integration von lokalem KI
in jede Anwendung. Hier sind die wichtigsten Punkte:
- ✔️ Zwei API-Oberflächen: natives
/api/* für volle Kontrolle,
/v1/* als Drop-in-Ersatz für OpenAI-Code
- ✔️ /api/chat – der Hauptendpunkt: unterstützt Verlauf, Tool Calling und Streaming
- ✔️ Streaming – standardmäßig: aktivieren Sie es für die Benutzeroberfläche, deaktivieren Sie es für Batch-Aufgaben
- ✔️ /api/embed – für RAG: nomic-embed-text + /api/chat = eine vollständige lokale RAG-Pipeline
- ✔️ Java + WebClient: nicht-blockierende Anfragen, Streaming-Unterstützung über Flux
- ✔️ Fehlerbehandlung: setzen Sie immer Timeouts und behandeln Sie Connection refused
In meinen Projekten – WebsCraft und AskYourDocs – verwende ich genau diese Endpunkte:
/api/embed für die Indexierung von Inhalten, /api/chat mit Streaming
für Antworten an Benutzer, /api/ps und Health Check für die Überwachung.
Das Wichtigste, was ich nach mehreren Monaten Arbeit mit der Ollama API gelernt habe:
sie erfordert keine komplexe Infrastruktur – curl, WebClient oder fetch reichen aus,
um eine vollwertige KI-Anwendung ohne einen einzigen externen API-Schlüssel zu erstellen.
Nächster Schritt: Wenn Sie eine vollwertige RAG-Pipeline mit Ollama erstellen möchten –
Artikel RAG mit Ollama: von der Pipeline bis zur Produktion.
Wenn Sie einen Vergleich benötigen, wann Ollama Cloud-APIs übertrifft –
Ollama vs ChatGPT vs Claude: welche Aufgabe erfordert die Cloud.
📖 Quellen