Ollama — це не тільки CLI-інструмент для запуску моделей у терміналі.
Це повноцінний локальний сервер з REST API, який слухає на порту 11434
і приймає запити від будь-якого застосунку — Spring Boot, Node.js, Python,
або будь-якої мови з підтримкою HTTP. У цій статті — повний практичний розбір:
які endpoint-и є, як їх викликати і як інтегрувати Ollama у реальний застосунок.
Якщо ще не встановив Ollama —
почни з гайду зі встановлення на Mac, Windows і Linux.
Якщо хочеш зрозуміти які моделі підходять для різних задач —
стаття про вибір моделей Ollama у 2026.
📚 Зміст статті
🎯 Два API surface: /api/* нативний vs /v1/ OpenAI-сумісний
Коротка відповідь:
Ollama має два незалежних API. Нативний /api/* — повний контроль:
стрімінг з метаданими, управління моделями, ембединги, інспекція процесів.
OpenAI-сумісний /v1/* — drop-in заміна для коду що вже працює з ChatGPT API.
Для нових проєктів — обирай нативний. Для міграції існуючого коду — /v1/.
Якщо у тебе вже є код що звертається до OpenAI API —
для переключення на локальний Ollama достатньо змінити один рядок:
base_url = "http://localhost:11434/v1".
Решта коду залишається без змін.
Нативний API (/api/*)
За офіційною документацією Ollama,
після встановлення API доступний за адресою http://localhost:11434/api.
Нативний API підтримує:
- ✔️
POST /api/generate — генерація тексту по промпту
- ✔️
POST /api/chat — чат з history і tool calling
- ✔️
POST /api/embed — генерація ембедингів
- ✔️
GET /api/tags — список встановлених моделей
- ✔️
GET /api/ps — запущені моделі і використання VRAM
- ✔️
POST /api/pull — завантаження моделі
- ✔️
DELETE /api/delete — видалення моделі
OpenAI-сумісний API (/v1/*)
ML Journey пояснює:
Ollama підтримує OpenAI-сумісний endpoint, який означає що будь-який інструмент,
бібліотека або застосунок що працює з OpenAI API, можна підключити до локального
Ollama однією зміною рядка. Це включає офіційні Python і JS SDK OpenAI,
LangChain, LlamaIndex, Continue та сотні інших інструментів.
| Endpoint |
Нативний (/api/*) |
OpenAI-сумісний (/v1/*) |
| Чат |
/api/chat |
/v1/chat/completions |
| Генерація |
/api/generate |
/v1/completions |
| Ембединги |
/api/embed |
/v1/embeddings |
| Список моделей |
/api/tags |
/v1/models |
| Управління моделями |
✔️ Є |
❌ Немає |
| Метадані стрімінгу |
✔️ Повні |
⚠️ Часткові |
| API key |
Не потрібен |
Будь-який рядок (ігнорується) |
⚠️ На що звернути увагу — мій досвід
Коли я інтегрував Ollama у WebsCraft, я тричі наступив на одні і ті самі граблі.
Ось що варто знати одразу — щоб не витрачати час на дебаг.
1. Назва моделі у /v1/ має збігатись точно
У реального OpenAI API назва моделі стабільна глобально: gpt-4 завжди є.
В Ollama — модель має бути завантажена локально, і назва має збігатись
з тим що показує ollama list.
Я кілька разів отримував загадковий 404 model not found
просто тому що передавав "llama3" замість "llama3.2:3b".
Перше правило при міграції коду з OpenAI на Ollama:
# Перевір точну назву перед тим як писати код
ollama list
# NAME ID SIZE MODIFIED
# llama3.2:3b ... 2.0 GB 2 days ago
# nomic-embed-text:latest ... 274 MB 5 days ago
Якщо твій інструмент жорстко прописаний на gpt-3.5-turbo або іншу
OpenAI-назву — можна скопіювати модель під потрібну назву:
# Створює аліас: тепер gpt-3.5-turbo вказує на llama3.2:3b
ollama cp llama3.2:3b gpt-3.5-turbo
2. Змінити контекстне вікно через /v1/ — неочевидно
OpenAI API не має параметра для зміни розміру контексту — він фіксований для кожної моделі.
Тому через /v1/chat/completions передати num_ctx не можна:
параметр просто ігнорується.
Я з'ясував це після того як довгі документи несподівано обрізались —
модель мовчки відкидала частину контексту замість того щоб повернути помилку.
Рішення: створити Modelfile з потрібним контекстом і використовувати нову назву:
# Створюємо Modelfile
FROM llama3.2:3b
PARAMETER num_ctx 16384
# Збираємо нову модель
ollama create llama3-16k -f Modelfile
# Тепер викликаємо через /v1/ з новою назвою
curl http://localhost:11434/v1/chat/completions \
-H "Content-Type: application/json" \
-d '{
"model": "llama3-16k",
"messages": [{"role": "user", "content": "...довгий текст..."}]
}'
Через нативний /api/chat цієї проблеми немає — там num_ctx
передається прямо у options:
{
"model": "llama3.2:3b",
"messages": [...],
"options": {
"num_ctx": 16384
}
}
3. Відповідь /v1/ і /api/ — різний формат
Якщо ти переключаєшся між нативним і OpenAI-сумісним API — пам'ятай
що формат відповіді різний. Я кілька разів ловив KeyError у Python
просто тому що плутав де response["message"]["content"],
а де response.choices[0].message.content.
| Поле |
Нативний /api/chat |
OpenAI-сумісний /v1/ |
| Текст відповіді |
response["message"]["content"] |
response.choices[0].message.content |
| Кінець генерації |
response["done"] == true |
response.choices[0].finish_reason == "stop" |
| Статистика токенів |
eval_count, eval_duration |
usage.completion_tokens |
| Tool calls |
message.tool_calls |
choices[0].message.tool_calls |
Моє правило: в одному проєкті використовую тільки один API surface.
Якщо міграція з OpenAI — залишаю /v1/ і OpenAI SDK скрізь.
Якщо новий проєкт — нативний API скрізь. Мікс двох підходів в одному коді
гарантує плутанину при дебагу.
Висновок: Для нового проєкту — нативний API дає більше контролю.
Для міграції існуючого OpenAI-коду — /v1/ без змін коду.
🎯 POST /api/generate: базова генерація тексту
Коротка відповідь:
/api/generate — найпростіший endpoint: приймає модель і промпт,
повертає текст. Не зберігає контекст між запитами. Підходить для
одноразових задач: резюмування, переклад, класифікація.
Різниця між /api/generate і /api/chat:
generate приймає рядок-промпт, chat приймає масив повідомлень з ролями.
Для чат-боту — завжди /api/chat. Для batch-обробки — /api/generate зручніший.
Базовий запит через curl
# stream: false — повертає всю відповідь одразу
curl http://localhost:11434/api/generate \
-H "Content-Type: application/json" \
-d '{
"model": "llama3.2:3b",
"prompt": "Поясни що таке REST API в трьох реченнях.",
"stream": false
}'
Формат відповіді — всі поля
{
"model": "llama3.2:3b",
"created_at": "2026-05-01T10:00:00Z",
"response": "REST API — це...",
"done": true,
"prompt_eval_count": 15,
"prompt_eval_duration": 123456789,
"eval_count": 42,
"eval_duration": 987654321,
"total_duration": 1234567890,
"load_duration": 56789012
}
Що означає кожне поле:
- ✔️
prompt_eval_count — кількість токенів у промпті (вхід)
- ✔️
eval_count — кількість згенерованих токенів (вихід)
- ✔️
eval_duration — час генерації у наносекундах
- ✔️
load_duration — час завантаження моделі (0 якщо вже в пам'яті)
- ✔️
total_duration — повний час від запиту до відповіді
Як обчислити tokens/sec з метаданих
Метадані відповіді дозволяють логувати реальну продуктивність моделі.
Я використовую це у WebsCraft для моніторингу швидкості генерації
залежно від навантаження:
# Python: обчислення tokens/sec
import requests
r = requests.post("http://localhost:11434/api/generate", json={
"model": "llama3.2:3b",
"prompt": "Що таке мікросервіси?",
"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"Швидкість: {tokens_per_sec:.1f} tok/s")
print(f"Токени: {prompt_tokens} вхід → {output_tokens} вихід")
print(f"Загальний час: {total_sec:.2f}с")
// Java: обчислення tokens/sec через 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);
});
}
}
Основні параметри у options
{
"model": "llama3.2:3b",
"prompt": "Твій текст тут",
"stream": false,
"system": "Ти — технічний редактор. Відповідай коротко і по суті.",
"options": {
"temperature": 0.7,
"num_ctx": 4096,
"top_p": 0.9,
"num_predict": 256
}
}
- ✔️
temperature — творчість відповіді: 0.1 точно/детерміновано, 0.9 варіативно
- ✔️
num_ctx — розмір контекстного вікна (токени)
- ✔️
num_predict — максимальна кількість токенів у відповіді
- ✔️
top_p — nucleus sampling, зазвичай 0.9
- ✔️
system — системний промпт (поза options, окреме поле)
🎯 POST /api/chat: чат-формат і збереження контексту
Коротка відповідь:
/api/chat — основний endpoint для чат-застосунків.
Приймає масив повідомлень з ролями (system, user, assistant),
підтримує tool calling і стрімінг.
Для збереження контексту між запитами — передавай повну историю повідомлень.
LLM не має пам'яті між запитами. «Пам'ять» чат-бота —
це просто масив messages що ти передаєш з кожним запитом.
Чим довша история — тим більше RAM і часу на відповідь.
Базовий запит
curl http://localhost:11434/api/chat \
-H "Content-Type: application/json" \
-d '{
"model": "llama3.2:3b",
"messages": [
{
"role": "system",
"content": "Ти — помічник розробника. Відповідай українською, коротко."
},
{
"role": "user",
"content": "Що таке dependency injection?"
}
],
"stream": false,
"keep_alive": "10m"
}'
keep_alive: як довго тримати модель в пам'яті
За замовчуванням Ollama вивантажує модель з пам'яті через 5 хвилин після
останнього запиту. Для чат-боту де запити приходять часто — це означає
cold-start затримку на кожну нову сесію.
Я зіткнувся з цим у WebsCraft: перший запит нової сесії займав 8–12 секунд
замість 1–2 — модель щоразу перевантажувалась. Параметр keep_alive
вирішує це:
# Тримати модель в пам'яті 30 хвилин
{"keep_alive": "30m"}
# Тримати постійно (поки не перезапустити Ollama)
{"keep_alive": -1}
# Вивантажити одразу після відповіді (для batch-задач де RAM важлива)
{"keep_alive": 0}
# Можна також задати через змінну середовища (глобально для всіх моделей):
# OLLAMA_KEEP_ALIVE=30m ollama serve
Для продакшн чат-боту я використовую "keep_alive": "30m" —
модель залишається гарячою між сесіями, але вивантажується якщо довго немає запитів.
Збереження контексту (multi-turn)
# Python: повний цикл multi-turn чату
import requests
OLLAMA_URL = "http://localhost:11434/api/chat"
MODEL = "llama3.2:3b"
messages = [
{"role": "system", "content": "Ти — технічний асистент. Відповідай коротко."}
]
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
# Перший запит
print(chat("Що таке Spring Boot?"))
# Другий запит — модель «пам'ятає» перший
print(chat("Які його основні переваги?"))
# Третій — продовження контексту
print(chat("Покажи мінімальний pom.xml для нього")
Обрізання history: що робити коли контекст переповнюється
Якщо чат довгий — history росте і починає займати весь контекст моделі.
Коли messages перевищують num_ctx, Ollama мовчки відкидає
найстаріші повідомлення. Щоб контролювати це явно — реалізуй trimming вручну.
Я використовую простий підхід: зберігаю system prompt завжди,
а user/assistant повідомлення обрізаю до останніх N пар:
def trim_history(messages: list, max_pairs: int = 10) -> list:
"""
Зберігає system prompt і останні max_pairs пар user/assistant.
max_pairs=10 → максимум 21 повідомлення (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"]
# Беремо останні max_pairs * 2 повідомлень (пара = user + assistant)
trimmed = dialog[-(max_pairs * 2):]
return system + trimmed
# Використання в циклі чату:
def chat_with_trim(user_input: str) -> str:
messages.append({"role": "user", "content": user_input})
# Обрізаємо перед кожним запитом
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
Альтернативний підхід — обрізати за токенами, а не за кількістю повідомлень.
Але для більшості застосунків обмеження за кількістю пар — простіше
і достатньо передбачувано.
Формат відповіді /api/chat
{
"model": "llama3.2:3b",
"created_at": "2026-05-01T10:00:00Z",
"message": {
"role": "assistant",
"content": "Spring Boot — це..."
},
"done": true,
"eval_count": 38,
"eval_duration": 876543210,
"total_duration": 987654321
}
Поля eval_count і eval_duration — ті самі що у
/api/generate, дозволяють обчислити tokens/sec для моніторингу.
🎯 Стрімінг: навіщо і як реалізувати
Коротка відповідь:
Стрімінг — це отримання відповіді токен за токеном, а не одним блоком.
За замовчуванням Ollama стрімить. Для UI — обов'язково вмикай стрімінг:
перший токен приходить за 1–3 сек, а без стрімінгу користувач чекає
всю відповідь мовчки.
З stream: true перший токен з'являється на екрані
за 1–3 секунди. З stream: false — весь текст з'являється
після того як модель закінчила генерацію, тобто через 5–30 секунд
залежно від довжини відповіді. Для інтерактивних застосунків — stream: true.
Реальний кейс: як це працює на AskYourDocs
Я реалізував стрімінг у своєму сервісі
AskYourDocs —
застосунку де користувач задає питання по своїх документах і отримує відповідь
з локальної RAG-системи на базі Ollama.
Без стрімінгу перші версії сервісу виглядали так: користувач натискав «Надіслати»,
бачив спіннер 8–15 секунд, потім одразу з'являвся весь текст.
Відчуття — наче застосунок завис. З стрімінгом перші слова з'являються
вже через 1–2 секунди і відповідь «друкується» прямо на очах.
Різниця в UX — разюча, навіть якщо загальний час генерації однаковий.
Архітектура: Ollama стрімить токени → Spring Boot читає NDJSON-потік
через WebFlux → передає клієнту через SSE (Server-Sent Events) →
JavaScript на фронтенді дописує токени в DOM по одному.
Стрімінг через curl
curl http://localhost:11434/api/chat \
-d '{
"model": "llama3.2:3b",
"messages": [{"role": "user", "content": "Розкажи про мікросервіси"}]
}'
# stream: true — за замовчуванням, можна не вказувати
Відповідь приходить як потік JSON-об'єктів, кожен на окремому рядку (NDJSON):
{"model":"llama3.2:3b","message":{"role":"assistant","content":"Мікро"},"done":false}
{"model":"llama3.2:3b","message":{"role":"assistant","content":"сервіси"},"done":false}
{"model":"llama3.2:3b","message":{"role":"assistant","content":" — це"},"done":false}
...
{"model":"llama3.2:3b","message":{"role":"assistant","content":""},"done":true,"eval_count":87}
Стрімінг у 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": "Поясни що таке Docker"}
])
Стрімінг у Spring Boot через SSE
Саме цей підхід я використовую в AskYourDocs: Spring Boot читає NDJSON від Ollama
і відразу передає токени клієнту через Server-Sent Events.
Фронтенд отримує токени і дописує їх у DOM без перезавантаження сторінки.
// OllamaStreamService.java
@Service
@RequiredArgsConstructor
public class OllamaStreamService {
private final WebClient ollamaWebClient;
/**
* Стрімить токени від Ollama як Flux.
* Кожен елемент — один токен відповіді моделі.
*/
public Flux<String> streamChat(String userMessage) {
var body = Map.of(
"model", "llama3.2:3b",
"messages", List.of(
Map.of("role", "system",
"content", "Відповідай українською, коротко."),
Map.of("role", "user", "content", userMessage)
),
"stream", true,
"keep_alive", "30m"
);
return ollamaWebClient.post()
.uri("/api/chat")
.bodyValue(body)
.retrieve()
.bodyToFlux(String.class) // кожен рядок NDJSON
.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 завершує Flux
} catch (Exception e) {
return null;
}
});
}
}
// OllamaController.java — SSE endpoint для фронтенду
@RestController
@RequestMapping("/api/ai")
@RequiredArgsConstructor
public class OllamaController {
private final OllamaStreamService streamService;
/**
* SSE endpoint: токени надходять по одному в браузер.
* Фронтенд підключається через EventSource або fetch з ReadableStream.
*/
@GetMapping(value = "/stream", produces = MediaType.TEXT_EVENT_STREAM_VALUE)
public Flux<String> stream(@RequestParam String message) {
return streamService.streamChat(message);
}
}
Фронтенд: читання SSE через fetch
// Підключення до SSE endpoint і відображення токенів у реальному часі
async function streamAnswer(question, outputElement) {
const controller = new AbortController(); // для скасування стрімінгу
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 = ""; // очищаємо перед відповіддю
try {
while (true) {
const { done, value } = await reader.read();
if (done) break;
// SSE рядки мають формат "data: токен\n\n"
const lines = decoder.decode(value).split("\n");
for (const line of lines) {
if (line.startsWith("data: ")) {
const token = line.slice(6); // прибираємо "data: "
outputElement.textContent += token;
}
}
}
} catch (err) {
if (err.name !== "AbortError") console.error("Stream error:", err);
}
return controller; // повертаємо для можливості скасування
}
// Використання:
const stopBtn = document.getElementById("stop");
const output = document.getElementById("answer");
const controller = await streamAnswer("Що таке Spring Boot?", output);
// Кнопка "Зупинити генерацію"
stopBtn.onclick = () => controller.abort();
Abort стрімінгу: кнопка "Зупинити"
В AskYourDocs я додав кнопку «Зупинити» — якщо відповідь занадто довга
або модель пішла не туди. Реалізується через AbortController
на фронтенді (показано вище) і скасування Flux на бекенді:
// Spring Boot: автоматично скасовує запит до Ollama
// коли клієнт відключається (браузер закрив SSE-з'єднання)
// WebFlux робить це автоматично через Flux.takeUntilOther або
// через onCancel оператор:
public Flux<String> streamChat(String userMessage) {
return ollamaWebClient.post()
.uri("/api/chat")
.bodyValue(body)
.retrieve()
.bodyToFlux(String.class)
.doOnCancel(() ->
log.info("Клієнт відключився, стрімінг скасовано"))
// ... решта операторів
}
WebFlux автоматично скасовує upstream запит до Ollama коли клієнт
закриває SSE-з'єднання — модель зупиняє генерацію і звільняє RAM.
Це важливо: без правильного скасування модель продовжує генерувати
навіть після того як користувач закрив вкладку.
🎯 POST /api/embed: ембединги для RAG
Коротка відповідь:
/api/embed генерує числові вектори (ембединги) для тексту.
Ці вектори потрібні для семантичного пошуку — основи RAG-архітектури.
Найкраща локальна модель для ембедингів — nomic-embed-text.
Якщо ще не розумієш що таке ембединги — почни з
статті «Що таке Embeddings: як AI розуміє сенс тексту»
перед тим як рухатись далі.
Як я використовую /api/embed у WebsCraft
У своєму RAG-пайплайні на WebsCraft я використовую nomic-embed-text
через /api/embed для двох задач: індексації статей блогу при публікації
і пошуку релевантних статей при запиті користувача до чат-бота.
Чому nomic-embed-text: розмірність 768 — достатньо для семантичного пошуку,
швидка генерація (~50мс на чанк), мінімальне використання RAM (~274 МБ).
При локальній розробці я можу запустити і ембединг-модель, і генеративну одночасно
на Mac M1 з 16 ГБ — вони не конкурують за пам'ять.
У продакшні через OpenRouter використовую openai/text-embedding-3-small,
але локально для тестування — завжди nomic-embed-text.
Встановлення моделі для ембедингів
ollama pull nomic-embed-text
Запит через curl
curl http://localhost:11434/api/embed \
-H "Content-Type: application/json" \
-d '{
"model": "nomic-embed-text",
"input": "Spring Boot — це фреймворк для Java-застосунків"
}'
Формат відповіді
{
"model": "nomic-embed-text",
"embeddings": [
[0.1234, -0.5678, 0.9012, ...]
],
"total_duration": 12345678,
"load_duration": 1234567,
"prompt_eval_count": 9
}
Поле embeddings — масив масивів (можна передати кілька текстів за раз).
nomic-embed-text повертає вектор розмірністю 768.
Batch-ембединги (кілька текстів за раз)
curl http://localhost:11434/api/embed \
-d '{
"model": "nomic-embed-text",
"input": [
"Перше речення для ембединга",
"Друге речення для ембединга",
"Третє речення для ембединга"
]
}'
Функція для RAG у Python
import requests
import numpy as np
def embed(texts: list[str], model: str = "nomic-embed-text") -> list[list[float]]:
"""Генерує ембединги для списку текстів."""
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:
"""Косинусна подібність між двома векторами."""
a, b = np.array(a), np.array(b)
return float(np.dot(a, b) / (np.linalg.norm(a) * np.linalg.norm(b)))
# Приклад: пошук найближчого документа
query = "Як налаштувати Spring Boot?"
docs = [
"Spring Boot автоконфігурація спрощує налаштування",
"Python Flask — легкий веб-фреймворк",
"Maven — система збірки для Java-проєктів"
]
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"Найбільш релевантний: {scores[0][0]} ({scores[0][1]:.3f})")
Ембединги у Java через WebClient
// EmbeddingService.java
@Service
@RequiredArgsConstructor
public class EmbeddingService {
private final WebClient ollamaWebClient;
private static final String EMBED_MODEL = "nomic-embed-text";
/**
* Генерує ембединг для одного тексту.
* Повертає вектор розмірністю 768 для nomic-embed-text.
*/
public Mono<List<Double>> embed(String text) {
return embedBatch(List.of(text))
.map(embeddings -> embeddings.get(0));
}
/**
* Batch-ембединги: кілька текстів за один запит.
* Ефективніше ніж кілька окремих запитів.
*/
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);
}
/**
* Косинусна подібність між двома векторами.
*/
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 для відповіді Ollama
record EmbedResponse(List<List<Double>> embeddings) {}
}
// Використання в RAG-сервісі:
@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;
})
);
}
}
На практиці замість ручного cosine similarity краще використовувати
векторну базу даних (pgvector, Chroma, Qdrant) — вони індексують вектори
і шукають по мільйонах записів за мілісекунди.
Ручне обчислення підходить для прототипів і невеликих колекцій до ~1000 документів.
Детальніше про вибір ембединг-моделей для RAG —
у статті Embedding-моделі для RAG у 2026: яку обрати і порівняння.
🎯 Tool Calling: підключення зовнішніх функцій
Коротка відповідь:
Tool calling — це можливість моделі «викликати» зовнішні функції.
Модель не виконує функцію сама — вона повертає JSON з назвою функції і аргументами,
а твій код виконує реальний виклик і передає результат назад.
Підтримується через /api/chat з параметром tools.
Перед тим як читати далі — рекомендую прочитати
статтю «Tool Use vs Function Calling: як це працює і при чому RAG» —
там пояснено чому LLM описує функцію у JSON, а не виконує її,
і повний цикл виклику з прикладами.
Яка модель підтримує tool calling
Не всі моделі підтримують tool calling. Підтримують: Llama 3.1/3.2/3.3, Qwen 2.5, Mistral 7B (v0.3+), DeepSeek R1.
ollama pull llama3.2:3b
Базовий запит з tools
curl http://localhost:11434/api/chat \
-H "Content-Type: application/json" \
-d '{
"model": "llama3.2:3b",
"messages": [
{"role": "user", "content": "Яка зараз погода у Харкові?"}
],
"tools": [
{
"type": "function",
"function": {
"name": "get_weather",
"description": "Отримати поточну погоду для міста",
"parameters": {
"type": "object",
"properties": {
"city": {
"type": "string",
"description": "Назва міста"
},
"units": {
"type": "string",
"enum": ["celsius", "fahrenheit"],
"description": "Одиниці температури"
}
},
"required": ["city"]
}
}
}
],
"stream": false
}'
Відповідь з tool_calls
{
"message": {
"role": "assistant",
"content": "",
"tool_calls": [
{
"function": {
"name": "get_weather",
"arguments": {
"city": "Харків",
"units": "celsius"
}
}
}
]
},
"done": true
}
Повний цикл tool calling у Python
import requests, json
OLLAMA_URL = "http://localhost:11434/api/chat"
MODEL = "llama3.2:3b"
tools = [{
"type": "function",
"function": {
"name": "get_weather",
"description": "Отримати поточну погоду для міста",
"parameters": {
"type": "object",
"properties": {
"city": {"type": "string", "description": "Назва міста"}
},
"required": ["city"]
}
}
}]
def get_weather(city: str) -> str:
return f"У {city}: +18°C, хмарно"
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"]
# Модель відповіла текстом — tool не викликано
return assistant_msg["content"]
print(chat_with_tools("Яка зараз погода у Харкові?"))
Tool calling у Java через 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";
// Опис інструменту у форматі JSON Schema
private static final Map<String, Object> WEATHER_TOOL = Map.of(
"type", "function",
"function", Map.of(
"name", "get_weather",
"description", "Отримати поточну погоду для міста",
"parameters", Map.of(
"type", "object",
"properties", Map.of(
"city", Map.of(
"type", "string",
"description", "Назва міста"
)
),
"required", List.of("city")
)
)
);
public Mono<String> chatWithTools(String userMessage) {
var messages = new ArrayList<>(List.of(
Map.of("role", "user", "content", userMessage)
));
// Крок 1: перший запит з tools
return callOllama(messages, true)
.flatMap(response -> {
var msg = (Map<?, ?>) response.get("message");
var toolCalls = (List<?>) msg.get("tool_calls");
// Якщо модель не викликала tool — повертаємо текст
if (toolCalls == null || toolCalls.isEmpty()) {
return Mono.just((String) msg.get("content"));
}
// Крок 2: виконуємо реальні виклики
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));
}
// Крок 3: фінальний запит з результатом
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));
}
// Реєстр функцій — додавай нові інструменти сюди
private String executeFunction(String name, Map<?, ?> args) {
return switch (name) {
case "get_weather" -> getWeather((String) args.get("city"));
default -> "Невідомий інструмент: " + name;
};
}
private String getWeather(String city) {
// Тут реальний виклик weather API
return "У " + city + ": +18°C, хмарно";
}
}
⚠️ Типова помилка: модель не викликала tool
Модель не зобов'язана викликати tool — вона може відповісти текстом
навіть якщо tools передані. Це трапляється якщо:
- ✔️ Питання не потребує зовнішніх даних на думку моделі
- ✔️ Опис функції (
description) нечіткий або не відповідає питанню
- ✔️ Модель не підтримує tool calling (перевір список вище)
Тому завжди перевіряй чи є tool_calls у відповіді,
і обробляй обидва випадки — і з викликом, і без:
# Python: правильна перевірка
assistant = response["message"]
if assistant.get("tool_calls"):
# Модель хоче викликати tool — виконуємо
...
else:
# Модель відповіла текстом — повертаємо як є
return assistant["content"]
Якщо хочеш щоб модель завжди викликала конкретний tool —
використовуй параметр tool_choice (підтримується через /v1/):
curl http://localhost:11434/v1/chat/completions \
-d '{
"model": "llama3.2:3b",
"messages": [...],
"tools": [...],
"tool_choice": {"type": "function", "function": {"name": "get_weather"}}
}'
Детальніше про те як модель вирішує коли викликати tool —
у статті Як LLM вирішує коли викликати tool: механіка рішення.
🎯 Приклад на Java: WebClient + Spring Boot
Коротка відповідь:
Для Spring Boot є два підходи: пряме звернення через WebClient (гнучко, без залежностей)
або через Spring AI (зручніше, але додаткова бібліотека).
Нижче — обидва варіанти з робочим кодом.
RestTemplate у Spring 6+ — deprecated. Використовуй WebClient для
неблокуючих HTTP-запитів до Ollama — особливо важливо для стрімінгу
відповідей у реальному часі.
⚠️ Важливо: код нижче — демонстраційний. Його мета —
показати базову механіку взаємодії з Ollama API, а не готовий шаблон для продакшну.
У кожного проєкту своя архітектура: інша структура пакетів, інша обробка помилок,
інший спосіб зберігання конфігурації. Адаптуй під свої потреби.
Варіант 1: WebClient — без додаткових залежностей
Залежності у pom.xml:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-webflux</artifactId>
</dependency>
Конфігурація в application.properties
(URL винесено з коду — не хардкодь у бінах):
ollama.base-url=http://localhost:11434
ollama.model=llama3.2:3b
ollama.timeout-seconds=60
Конфігурація WebClient:
// 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();
}
}
DTO для запиту і відповіді:
// 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) {}
}
Сервіс з підтримкою стрімінгу:
// 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;
// Звичайний запит (без стрімінгу)
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("Помилка: " + e.getMessage()));
}
// Стрімінг (SSE для фронтенду)
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-контролер:
// 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);
}
}
// Тест:
// curl -X POST http://localhost:8080/api/ai/chat \
// -H "Content-Type: application/json" \
// -d '{"message": "Що таке Spring WebFlux?"}'
Варіант 2: Spring AI — мінімум коду
Залежності:
<dependency>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-starter-model-ollama</artifactId>
</dependency>
Конфігурація в 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
Сервіс через Spring AI — звичайний запит і стрімінг:
@Service
@RequiredArgsConstructor
public class SpringAiOllamaService {
private final ChatClient chatClient;
// Звичайний запит
public String ask(String question) {
return chatClient.prompt()
.user(question)
.call()
.content();
}
// Стрімінг через Spring AI
public Flux<String> stream(String question) {
return chatClient.prompt()
.user(question)
.stream()
.content();
}
}
Коли що обирати:
- ✔️ WebClient — повний контроль над запитом, стрімінг, налаштування таймаутів і обробка помилок. Без додаткових залежностей.
- ✔️ Spring AI — швидкий старт і легке перемикання між провайдерами (Ollama → OpenAI → Anthropic) без зміни коду.
🎯 Приклад на Python
Коротка відповідь:
Два підходи: нативна бібліотека ollama (простіше, більше функцій)
або openai SDK з base_url (якщо вже є OpenAI-код).
⚠️ Важливо: приклади нижче — демонстраційні.
Вони показують механіку роботи з API, а не готову архітектуру застосунку.
У реальному проєкті додай обробку помилок, логування, конфігурацію через змінні середовища
і відповідну структуру модулів.
Варіант 1: нативна бібліотека ollama
pip install ollama
import ollama
# Простий запит
response = ollama.chat(
model="llama3.2:3b",
messages=[{"role": "user", "content": "Що таке REST API?"}]
)
print(response["message"]["content"])
# Стрімінг
for chunk in ollama.chat(
model="llama3.2:3b",
messages=[{"role": "user", "content": "Розкажи про мікросервіси"}],
stream=True
):
print(chunk["message"]["content"], end="", flush=True)
# Ембединги
emb = ollama.embed(model="nomic-embed-text", input="Hello world")
print(f"Розмірність: {len(emb['embeddings'][0])}")
Варіант 2: OpenAI SDK (drop-in заміна)
pip install openai
from openai import OpenAI
# Єдина зміна відносно OpenAI: base_url і api_key (ігнорується)
client = OpenAI(
base_url="http://localhost:11434/v1",
api_key="ollama"
)
# Далі — стандартний OpenAI-код без змін
response = client.chat.completions.create(
model="llama3.2:3b",
messages=[
{"role": "system", "content": "Ти — технічний асистент."},
{"role": "user", "content": "Що таке Docker?"}
]
)
print(response.choices[0].message.content)
# Стрімінг через OpenAI SDK
stream = client.chat.completions.create(
model="llama3.2:3b",
messages=[{"role": "user", "content": "Розкажи про CI/CD"}],
stream=True
)
for chunk in stream:
if chunk.choices[0].delta.content:
print(chunk.choices[0].delta.content, end="", flush=True)
🎯 Приклад на JavaScript / Node.js
⚠️ Важливо: приклади нижче — демонстраційні.
Мета — показати базову механіку виклику Ollama API з JavaScript.
У реальному застосунку структура буде інша: окремі модулі, обробка помилок,
змінні середовища для URL і назви моделі.
Варіант 1: нативна бібліотека ollama
npm install ollama
import ollama from "ollama";
// Простий запит
const response = await ollama.chat({
model: "llama3.2:3b",
messages: [{ role: "user", content: "Що таке REST API?" }],
});
console.log(response.message.content);
// Стрімінг
const stream = await ollama.chat({
model: "llama3.2:3b",
messages: [{ role: "user", content: "Розкажи про мікросервіси" }],
stream: true,
});
for await (const chunk of stream) {
process.stdout.write(chunk.message.content);
}
// Ембединги
const emb = await ollama.embed({
model: "nomic-embed-text",
input: "Hello world",
});
console.log(`Розмірність: ${emb.embeddings[0].length}`);
Варіант 2: fetch API (без залежностей)
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;
}
// Стрімінг через 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("Що таке GraphQL?", (token) => process.stdout.write(token));
🎯 Управління моделями і health check через API
Коли Ollama використовується не як локальний CLI-інструмент, а як сервер
у реальному застосунку — виникають питання які виходять за межі простого чату:
як перевірити чи Ollama запущений перед надсиланням запиту,
як уникнути cold-start затримки на першому запиті сесії,
як автоматично завантажити потрібну модель при старті застосунку,
як моніторити скільки пам'яті займає модель у продакшні.
Для всього цього є окремі endpoint-и.
Я використовую ці endpoint-и у WebsCraft для двох задач:
health check при старті Spring Boot — перевіряю чи Ollama доступний перш ніж
реєструвати AI-роути, і /api/ps у логах — щоб бачити
коли модель вивантажується і скільки VRAM займає між запитами.
GET /api/tags — список встановлених моделей
curl http://localhost:11434/api/tags
# Відповідь:
{
"models": [
{
"name": "llama3.2:3b",
"size": 2019393423,
"details": {
"parameter_size": "3B",
"quantization_level": "Q4_K_M"
}
}
]
}
Корисно при старті застосунку: перевір що потрібна модель встановлена,
і якщо ні — завантаж через /api/pull (або поверни помилку).
GET /api/ps — запущені моделі і VRAM
curl http://localhost:11434/api/ps
# Відповідь:
{
"models": [
{
"name": "llama3.2:3b",
"size_vram": 2145386496,
"expires_at": "2026-05-01T10:05:00Z"
}
]
}
Корисно перед запитом: якщо модель вже завантажена (/api/ps не порожній) —
перший запит буде без cold-start затримки.
Поле expires_at показує коли модель вивантажиться з пам'яті
(за замовчуванням через 5 хвилин після останнього запиту).
GET / — health check
curl http://localhost:11434/
# Повертає: "Ollama is running"
# Корисно в скриптах запуску:
if curl -s http://localhost:11434/ | grep -q "running"; then
echo "Ollama готовий"
else
echo "Ollama не запущений — запускаємо..."
ollama serve &
fi
У Spring Boot можна зробити health check через @EventListener(ApplicationReadyEvent.class)
— після старту застосунку перевіряй доступність Ollama і логуй результат:
@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 доступний, моделей: {}",
((List>) resp.get("models")).size()),
err -> log.warn("Ollama недоступний: {}", err.getMessage())
);
}
}
POST /api/pull — завантаження моделі через API
curl http://localhost:11434/api/pull \
-d '{"name": "llama3.2:3b"}'
# Python з прогресом:
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")
Корисно в Docker entrypoint або CI/CD пайплайні —
автоматично завантажуєш потрібні моделі при першому деплої,
без ручного ollama pull на сервері.
🎯 Обробка помилок, таймаути, OLLAMA_HOST
Типові помилки і як їх обробляти
| Помилка |
Причина |
Рішення |
Connection refused :11434 |
Ollama не запущений |
Запустити ollama serve або додаток Ollama |
404 model not found |
Модель не завантажена |
ollama pull model-name |
| Таймаут без відповіді |
Модель занадто велика / cold start |
Збільшити таймаут до 120с, або завчасно завантажити модель |
500 out of memory |
Не вистачає RAM |
Обрати меншу модель або Q4 замість Q8 |
404 на /v1/chat/completions |
Переплутав /api/ і /v1/ |
OpenAI SDK → base_url = localhost:11434/v1 |
| Відповідь обрізана посередині речення |
num_predict замалий (за замовч. 128) |
Збільшити num_predict або поставити -1 (без ліміту) |
Остання помилка з таблиці — найнеочевидніша. Я сам натрапив на неї
коли відповіді раптово обрізались посередині пояснення.
Причина: за замовчуванням деякі збірки Ollama обмежують генерацію до 128 токенів.
Рішення — явно вказувати num_predict:
# У запиті через /api/chat або /api/generate:
{
"model": "llama3.2:3b",
"messages": [...],
"options": {
"num_predict": -1 // -1 = без обмеження
// або конкретне число:
// "num_predict": 2048
}
}
Рекомендовані таймаути
Python (requests):
# Кортеж (connect_timeout, read_timeout)
requests.post(url, json=body, timeout=(10, 120))
# 10с на з'єднання, 120с на читання відповіді
# Для великих моделей або довгих відповідей — збільшуй read до 300с
Java (WebClient):
// WebClient має два рівні таймаутів — обидва потрібні
// 1. Таймаут на рівні HTTP-клієнта (TCP з'єднання і читання)
@Bean
public WebClient ollamaWebClient() {
HttpClient httpClient = HttpClient.create()
.option(ChannelOption.CONNECT_TIMEOUT_MILLIS, 10_000) // 10с на з'єднання
.responseTimeout(Duration.ofSeconds(120)); // 120с на відповідь
return WebClient.builder()
.baseUrl(ollamaBaseUrl)
.clientConnector(new ReactorClientHttpConnector(httpClient))
.codecs(c -> c.defaultCodecs().maxInMemorySize(10 * 1024 * 1024))
.build();
}
// 2. Реактивний таймаут на рівні Mono/Flux (для конкретних запитів)
ollamaWebClient.post()
.uri("/api/chat")
.bodyValue(body)
.retrieve()
.bodyToMono(OllamaChatResponse.class)
.timeout(Duration.ofSeconds(120)) // додатковий захист
.onErrorMap(TimeoutException.class,
e -> new RuntimeException("Ollama не відповів за 120с"));
⚠️ Для стрімінгу (bodyToFlux) таймаут на рівні Flux спрацьовує
якщо між токенами проходить більше зазначеного часу — це не завжди те що потрібно.
Для стрімінгу краще покладатись тільки на responseTimeout
на рівні HttpClient.
OLLAMA_HOST — запуск на іншому хості
# Запустити Ollama доступним для мережі (не тільки localhost)
OLLAMA_HOST=0.0.0.0:11434 ollama serve
# Або в Docker:
docker run -e OLLAMA_HOST=0.0.0.0:11434 ollama/ollama
# У Python клієнті — замінити localhost на IP сервера:
client = OpenAI(base_url="http://192.168.1.100:11434/v1", api_key="ollama")
# У Java application.properties:
ollama.base-url=http://192.168.1.100:11434
⚠️ Увага: якщо відкриваєш Ollama назовні — додай авторизацію
або обмеж доступ через firewall. За замовчуванням Ollama не вимагає
авторизації — будь-хто в мережі може надсилати запити.
❓ Часті питання (FAQ)
Чим /api/generate відрізняється від /api/chat?
/api/generate приймає рядок-промпт і повертає текст.
/api/chat приймає масив повідомлень з ролями (system, user, assistant)
і підтримує tool calling. Для чат-боту і застосунків з контекстом — завжди /api/chat.
Для batch-генерації без контексту — /api/generate зручніший.
Як зберегти контекст між запитами?
Ollama не зберігає контекст автоматично. Для multi-turn чату
передавай повну историю повідомлень у кожному запиті:
після кожної відповіді додавай її у масив messages і передавай
весь масив у наступний запит.
Який таймаут встановити для запитів?
Залежить від розміру моделі і довжини відповіді. Для 3B-моделей — 30–60 секунд.
Для 8B — 60–120 секунд. Для першого запиту після запуску (cold start) —
додай ще 10–30 секунд на завантаження моделі в пам'ять.
Чи потрібен API key для Ollama?
Для нативного API (/api/*) — ні, авторизація не потрібна.
Для OpenAI-сумісного (/v1/*) — деякі SDK вимагають передати
api_key, але Ollama його ігнорує. Передавай будь-який рядок: "ollama".
Як запустити Ollama API у Docker?
docker run -d -p 11434:11434 ollama/ollama — і API буде доступний
на http://localhost:11434. Для GPU-прискорення:
docker run --gpus all -p 11434:11434 ollama/ollama.
Чи можна використовувати Ollama у Spring Boot без Spring AI?
Так. WebClient або RestClient достатньо для прямих HTTP-запитів до Ollama API.
Spring AI зручніший якщо плануєш перемикатись між провайдерами
(Ollama → OpenAI → Anthropic) без зміни коду. Для простої інтеграції —
WebClient цілком достатній.
Як дізнатись скільки токенів/сек видає модель?
Ollama повертає метадані у кожній відповіді — поля eval_count
(кількість згенерованих токенів) і eval_duration (час у наносекундах).
Ділиш одне на інше:
# Python
data = requests.post(...).json()
tok_per_sec = data["eval_count"] / (data["eval_duration"] / 1e9)
print(f"{tok_per_sec:.1f} tok/s")
Для 3B-моделі на Mac M1 — очікуй 20–30 tok/s.
Для 8B — 10–15 tok/s. Якщо отримуєш менше 5 tok/s —
модель занадто велика для заліза або частково свопить на диск.
Чому модель не викликає tool навіть якщо tools передані?
Модель не зобов'язана викликати tool — вона може відповісти текстом
якщо вирішить що зовнішні дані не потрібні. Три найчастіші причини:
нечіткий description функції (модель не розуміє коли її викликати),
питання не потребує зовнішніх даних на думку моделі,
або модель не підтримує tool calling (перевір список підтримуваних:
Llama 3.1+, Qwen 2.5, Mistral v0.3+).
Завжди перевіряй наявність tool_calls у відповіді і обробляй
обидва варіанти — з викликом і без.
✅ Висновки
Ollama REST API — простий і потужний інструмент для інтеграції локального AI
у будь-який застосунок. Ось головне:
- ✔️ Два API surface: нативний
/api/* для повного контролю,
/v1/* як drop-in заміна для OpenAI-коду
- ✔️ /api/chat — основний endpoint: підтримує history, tool calling і стрімінг
- ✔️ Стрімінг — за замовчуванням: вмикай для UI, вимикай для batch-задач
- ✔️ /api/embed — для RAG: nomic-embed-text + /api/chat = повний локальний RAG-пайплайн
- ✔️ Java + WebClient: неблокуючі запити, підтримка стрімінгу через Flux
- ✔️ Обробка помилок: завжди встановлюй таймаути і обробляй Connection refused
У своїх проєктах — WebsCraft і AskYourDocs — я використовую саме ці endpoint-и:
/api/embed для індексації контенту, /api/chat зі стрімінгом
для відповідей користувачам, /api/ps і health check для моніторингу.
Головне що я зрозумів після кількох місяців роботи з Ollama API:
він не вимагає складної інфраструктури — curl, WebClient або fetch достатньо
щоб побудувати повноцінний AI-застосунок без жодного зовнішнього API-ключа.
Наступний крок: якщо хочеш побудувати повноцінний RAG-пайплайн з Ollama —
стаття RAG з Ollama: від пайплайну до продакшну.
Якщо потрібно порівняння коли Ollama виграє перед хмарними API —
Ollama vs ChatGPT vs Claude: яка задача вимагає хмари.
📖 Джерела