Корисний локальний LLM за 30 хвилин: програмування, запитання та відповіді до документів та голосовий ввод
Корисний локальний LLM за 30 хвилин за допомогою Ollama, Qwen3 та Whisper.cpp: асистент для програмування, система RAG для ваших нотаток та офлайн-режим роботи з голосом, адаптований під обсяг вашої пам’яті.
Джерела: Run a Useful Local LLM in 30 Minutes (Coding, RAG, Voice)
Локальна модель може відповісти на справжнє запитання з програмування навіть без підключення мережі, на ноутбуці, який вже знаходиться на столі. Налаштування займає близько восьми хвилин, а щомісячний рахунок становить нуль. Достатньо лише запиту, подібного до наведеного нижче, щоб побачити її роботу.
$ ollama run qwen3:8b
>>> rewrite this function to use async/await
Якість є несподіваною. Він не дорівнює найкращим хмарним моделям у складних багатокрокових розумових операціях, але достатньо надійно справляється зі звичайними інженерними завданнями, тож різниця між простою іграшкою на вихідних та справжнім інструментом значно зменшилась.
Три етапи розвитку протягом приблизно півтора року зробили його практичним. Файли моделей скоротилися до розмірів, придатних для звичайної оперативної пам’яті: Qwen 3 постачається у версії з 8 мільярдів параметрів розміром близько 5 ГБ. Qwen3-Coder, випущений у лютому 2026 року, досягає 70,6 відсотків на тесті SWE-bench; він підтверджує свою ефективність завдяки архітектурі з 30 мільярдами параметрів, з яких активуються лише 3 мільярди на кожен токен. Крім того, завдяки Ollama встановлення перейшло від складної системи Python-середовищ до одного лише бінарного файлу.
Нижче наведені три окремі версії, кожна з яких створюється приблизно за півгодини: асистент для програмування всередині редактора, Python-скрипт, який відповідає на запитання щодо папки документів, та цикл голосового спілкування, який слухає та відповідає. Усі вони мають спільну п’ятихвилинну основу, після чого розходяться, тому розумним кроком буде обрати ту версію, яка найбільше відповідає щоденній роботі, а інші залишити на пізніше.
Спільна основа
Кожен шлях потребує двигуна інференції та набору ваг моделі.
Ollama виконує роль цього двигуна. Він враховує відмінності в апаратному забезпеченні, визначаючи, чи є у пристрої GPU від NVIDIA, чіп Apple Silicon чи лише CPU, та підготовлює відповідну схему виконання. Він працює у фоновому режимі та надає локальний HTTP API, який функціонує як кінцеві точки великих хмарних провайдерів.
Встановлення залежить від платформи. На macOS це робить Homebrew:
brew install ollama
У Linux є скрипт встановлення:
curl -fsSL https://ollama.com/install.sh | sh
Користувачі Windows можуть завантажити інсталятор MSI з веб-сайту Ollama.
Після встановлення двигуна настає черга моделі. Базовою моделлю є qwen3:8b, яка потребує приблизно 5 ГБ простору на диску та комфортно працює з 16 ГБ оперативної пам’яті. Без окремої відеокарти Ollama використовує обробку на процесорі зі швидкістю близько 8–12 токенів на секунду, що все одно забезпечує ідеальну читабельність.
Завантажте її:
ollama pull qwen3:8b
Потім підтвердіть, що все працює, провівши швидкий запуск:
ollama run qwen3:8b "Write a python script to reverse a string."
Відповідь надходить у термінал. На фоні Ollama завантажує файл GGUF — формат, який безпосередньо копіює ваги з диска до RAM, тож у моніторі системи видно, як обсяг пам’яті зростає приблизно на розмір файлу.
У пристрої з 8 ГБ або менше RAM починається переміщення даних на диск, і швидкість обробки значно сповільнюється. Для таких випадків кращим вибором є qwen3:4b або gemma3:4b. З 24 ГБ GPU є можливість використовувати qwen3:14b як наступний крок у покращенні продуктивності.
Шлях А: асистент з кодування в редакторі
Усі, хто постійно працює в редакторі коду, отримають найшвидшу віддачу від цього підходу. Мета — під’єднати локальну модель до VS Code для оптимізації коду, створення документації та шаблонів.
Загальна модель Qwen 3 є компетентною, але робота з кодом краще виконується за допомогою спеціалізованої підготовки. Станом на травень 2026 року Qwen3-Coder є найсильнішим універсальним локальним кодером, набравши 70,6 відсотка на тесті SWE-bench Verified та 44,3 відсотка на тесті SWE-Bench Pro. Його загальний обсяг становить 30 мільярдів параметрів, проте лише 3 мільярди з них активні для кожного токена, тому він поміщається у 24 ГБ VRAM або працює без проблем на Mac серії M із 32 ГБ об’єднаної пам’яті.
Ноутбук із 16 ГБ пам’яті без окремої відеокарти, ймовірно, не впорається з моделлю обсягом 30 мільярдів параметрів. Альтернативою є qwen2.5-coder:7b, який потребує 4,7 ГБ пам’яті та все одно досягає приблизно 92 відсотків на тесті HumanEval.
# For 24GB+ VRAM or 32GB+ Mac Unified Memory
ollama pull qwen3-coder:30b
# For 16GB RAM laptops
ollama pull qwen2.5-coder:7b
Редактору потрібен фронтенд для взаємодії з моделлю. Cline — це розширення VS Code з понад 5 мільйонами встановлень, яке читає файли, пропонує відмінності та може виконувати команди терміналу. Його можна знайти, пошукавши у магазині за його назвою.
Є одна проблема, на яку варто звернути увагу. Cline містить довгий системний запит, який пояснює, як форматувати відмінності, читати структуру файлів та використовувати інструменти, і цей запит часто містить понад 25 000 токенів ще до того, як буде доданий будь-який код. Стандартне вікно контексту для моделей Qwen 3 від Ollama становить 40 000 токенів. Якщо відкрити кілька файлів, загальна кількість токенів легко перевищує цей ліміт, і тоді Ollama без попередження обрізає запит. У результаті модель втрачає інструкції з форматування, ігнорує правила та генерує звичайний текст замість коректних відмінностей.
Рішенням є створення власного Modelfile, який змушує систему використовувати більше вікна контексту.
Modelfile, налаштований для Cline
Збережіть наступний текст у робочій директорії як файл під назвою Modelfile.
# Modelfile — cline-tuned qwen3-coder
# Save as: ./Modelfile
FROM qwen3-coder:30b
# Cline's system prompt is ~25-30K tokens before your code is added.
# Ollama's default num_ctx for Qwen 3 is 40K, but Cline ships an
# expanded system prompt in v3+ that comfortably exceeds it on any
# real task. 65536 is the safe floor for serious use; 131072 if
# you have RAM/VRAM to spare.
PARAMETER num_ctx 65536
# Code edits want low-variance output. The default 0.7 is too loose.
PARAMETER temperature 0.2
# Stop after the model's natural turn. Cline parses this.
PARAMETER stop "<|im_end|>"
Створіть модель на основі цього за допомогою Ollama CLI, а потім перевірте, чи вплинуло налаштування контексту:
# Build the Cline-tuned model from the Modelfile in the current dir
ollama create qwen3-coder-cline -f ./Modelfile
# Verify the context window actually took
ollama show qwen3-coder-cline --modelfile | grep num_ctx
# Expected output: PARAMETER num_ctx 65536
# Sanity-check it answers
ollama run qwen3-coder-cline "write a python one-liner to read /etc/hostname"
У VS Code відкрийте палітру команд за допомогою Cmd+Shift+P та виберіть опцію Cline settings. Налаштуйте її на локальний двигун:
API Provider: Ollama
Base URL: http://localhost:11434
Model: qwen3-coder-cline
Context Window: 65536
Значення вікна контексту в графічному інтерфейсі має точно збігатися з значенням num_ctx у файлі моделі. Якщо воно нижче, Cline припускає, що вікно менше, і тихо обмежує кількість файлів, які він надсилає.
Щоб спробувати це, відкрийте файл Python із синхронною функцією, відкрийте панель чату Cline та попросіть переробити код у формат async/await. Cline читає файл, надсилає його локальній моделі Qwen3-Coder, отримує відповідь та відображає чисту різницю в редакторі. Прийняття змін здійснюється одним кліком, і весь процес відбувається офлайн.
Ця конфігурація дозволяє виконувати переробку одного файлу, тестувати ізольовані функції та генерувати шаблонний код. За результатами тестування спільнотою її якість у вузькосферних завданнях становить приблизно від 70 до 85 відсотків від якості передових хмарних рішень.
Цей підхід не працює під час значних змін архітектури з багатьма файлами. Перенесення проекту Flask із двадцятьма файлами на FastAPI є прикладом його невдачі: потоки виконання губляться, виникають неправильні імпорти, і кодова база стає нефункціональною. Робота такого масштабу все ще краще виконувати за допомогою сучасних хмарних моделей. Однак для десятків невеликих, рутинних змін, які робляться щодня, достатньо локального рішення.
Шлях B: пошук у власних документах
Цей шлях передбачає використання папки з нотатками у форматі markdown, стенограмами зустрічей чи документацією до проекту, яку переглядають вручну. Весь процес можна реалізувати приблизно у 60 рядках звичайного Python, використовуючи лише клієнт Ollama та бібліотеку numpy.
Модель ембеддингу перетворює текст на вектори. Nomic Embed Text v1.5 має 137 мільйонів параметрів, генерує 768-вимірні вектори, займає менше 300 МБ, підтримує контекст обсягом 8192 токенів та демонструє хороші результати у тестах ефективності пошуку MTEB.
Завантажте модель ембеддингу та встановіть пакети Python:
llama pull nomic-embed-text
pip install ollama numpy
Потім збережіть скрипт як rag.py.
"""Local RAG over a folder of notes — Ollama embeddings + numpy cosine.
No vector DB. For a few hundred docs, an in-memory list + numpy is
faster to set up and fast enough to run. Swap in sqlite-vec when
the corpus outgrows it (typically past ~5000 chunks).
Setup:
ollama pull qwen3:8b
ollama pull nomic-embed-text
pip install ollama numpy
Run:
python rag.py ./notes "what did the team decide about pricing?"
"""
from __future__ import annotations
import glob
import os
import sys
import numpy as np
import ollama
EMBED_MODEL = "nomic-embed-text"
CHAT_MODEL = "qwen3:8b"
CHUNK_WORDS = 400 # ~500 tokens; fits 4 chunks in an 8K context
TOP_K = 3
def load_chunks(folder: str) -> list[str]:
"""Read every .md / .txt under folder, split into ~CHUNK_WORDS chunks."""
chunks: list[str] = []
for path in glob.glob(os.path.join(folder, "**/*"), recursive=True):
if not path.endswith((".md", ".txt")):
continue
with open(path, encoding="utf-8") as f:
words = f.read().split()
for i in range(0, len(words), CHUNK_WORDS):
chunk = " ".join(words[i : i + CHUNK_WORDS])
if chunk.strip():
chunks.append(chunk)
return chunks
def embed(texts: list[str]) -> np.ndarray:
"""Embed a batch of texts via Ollama. Returns an (N, D) matrix."""
resp = ollama.embed(model=EMBED_MODEL, input=texts)
return np.array(resp["embeddings"], dtype=np.float32)
def top_k_indices(
query_vec: np.ndarray, doc_mat: np.ndarray, k: int
) -> list[int]:
"""Return indices of the k most cosine-similar rows in doc_mat.
The trick: if both vectors are unit-length (norm == 1), their
dot product equals their cosine similarity. So we normalize
once, then one matrix multiply gives a similarity score for
every chunk against the query — no Python loop needed.
"""
# Normalize query and every chunk to unit length.
# The 1e-8 prevents division by zero on a zero vector.
q = query_vec / (np.linalg.norm(query_vec) + 1e-8)
d = doc_mat / (np.linalg.norm(doc_mat, axis=1, keepdims=True) + 1e-8)
# One matmul across all chunks: scores[i] = cos(query, chunk_i).
scores = d @ q
# Sort descending, take the first k indices.
return np.argsort(scores)[::-1][:k].tolist()
def answer(query: str, chunks: list[str], doc_mat: np.ndarray) -> str:
"""Retrieve top-K chunks, stuff them into a prompt, generate."""
q_vec = embed([query])[0]
idx = top_k_indices(q_vec, doc_mat, k=TOP_K)
context = "\n\n---\n\n".join(chunks[i] for i in idx)
prompt = (
"Answer the question using ONLY the context below. "
"If the context does not contain the answer, say so plainly.\n\n"
f"CONTEXT:\n{context}\n\nQUESTION: {query}"
)
resp = ollama.chat(
model=CHAT_MODEL,
messages=[{"role": "user", "content": prompt}],
)
return resp["message"]["content"]
if __name__ == "__main__":
if len(sys.argv) != 3:
sys.exit('usage: python rag.py <folder> "<question>"')
folder, query = sys.argv[1], sys.argv[2]
chunks = load_chunks(folder)
if not chunks:
sys.exit(f"no .md or .txt files found under {folder}")
print(f"indexing {len(chunks)} chunks...")
doc_mat = embed(chunks)
print(answer(query, chunks, doc_mat))
Направте його на каталог нотаток та поставте запитання:
python rag.py ~/Documents/meeting_notes "what did the team decide about pricing?"
Скрипт проглядає папку, читає файли та ділить їх на фрагменти по 400 слів. Повний список фрагментів надсилається до кінцевої точки embedding Ollama одним пакетом. Для кожного запитання вектор запиту порівнюється з кожним вектором документа за допомогою косинусової схожості.
Три найкращі фрагменти додаються до запрошення до генерації, і модель qwen3:8b формулює відповідь. Одна з вимог у цьому запрошенні є суворою: відповідати лише на основі наданого контексту та чітко зазначати, якщо в контексті бракує інформації для відповіді. Локальні моделі часто створюють корисно виглядаючі вигадки, коли пошук нічого не повертає, а ця вимога змушує їх визнати свою некомпетентність.
Це не є функцією пошуку у продакшн-середовищі. Розділення тексту на частини — це грубий підрахунок кількості слів, який іноді розриває речення навпіл. Ембеддинги зберігаються в пам’яті та зникають після завершення роботи, тому корпус даних необхідно знову ембеддити щоразу під час виконання. Коли кількість документів у колекції перевищує кілька тисяч, а процес запуску стає дратівливим, заміна матриці numpy на sqlite-vec дозволяє зберігати вектори у локальному файлі SQLite. Для особистих нотаток короткого скрипту цілком достатньо.
Шлях C: цикл голосу
Щоб спілкуватися з моделлю без набору тексту та без надсилання аудіо на облачний сервіс, цей шлях створює повний цикл «голос-у-голос» на локальному обладнанні.
Канал обробки складається з трьох частин: перетворення мовлення на текст для транскрипції з мікрофона, використання LLM для формування відповіді та перетворення тексту на мовлення для його вимови.
Для транскрипції використовується Whisper.cpp — сильно оптимізована версія програми Whisper від OpenAI, написана на C++, яка працює на CPU, Apple Metal та NVIDIA CUDA. Для конвертації аудіо потрібна залежність ffmpeg. У macOS:
brew install whisper-cpp ffmpeg
У Linux встановіть ffmpeg через менеджер пакетів, склонуйте репозиторій whisper.cpp та запустіть make.
Whisper.cpp потребує ваги у форматі GGML. Файл ggml-base.en.bin розміром близько 150 МБ доступний у офіційному репозиторії Hugging Face та є швидким та точним для англійської мови. Для інших мов або вищої точності використовується ggml-large-v3.bin. Покладіть файл у відому папку, наприклад ~/models/.
Вихідний аудіосигнал генерується за допомогою Kokoro ONNX — легкого двигуна TTS, вага ваг якого становить близько 300 МБ, і який здатний синтезувати звук майже в реальному часі на звичайному CPU. Наразі він вимагає Python 3.10–3.13 та не встановлюється на версії 3.14.
pip install -U sounddevice ollama kokoro-onnx
Скрипт потокової обробки
Збережіть наступний код як voice.py та змініть шлях WHISPER_MODEL так, щоб він вказував на щойно завантажений файл GGML.
"""Local voice loop — mic → whisper.cpp → Ollama → Kokoro → speaker.
Everything runs offline. Nothing leaves the machine.
Setup:
brew install whisper-cpp ffmpeg # macOS
# (Linux: apt install ffmpeg; build whisper.cpp from source)
pip install -U sounddevice ollama kokoro-onnx
# Whisper GGML model — pick one:
# ggml-base.en.bin (~150MB, fast, English-only)
# ggml-large-v3.bin (~3GB, accurate, multilingual)
# Download from https://huggingface.co/ggerganov/whisper.cpp
# Kokoro model files auto-download to ~/.cache/kokoro-onnx/
# on first run, or grab them manually from
# https://github.com/thewh1teagle/kokoro-onnx/releases
Run:
python voice.py
"""
from __future__ import annotations
import subprocess
import tempfile
import wave
from pathlib import Path
import sounddevice as sd
import ollama
from kokoro_onnx import Kokoro
WHISPER_MODEL = Path.home() / "models" / "ggml-base.en.bin"
CHAT_MODEL = "qwen3:8b"
KOKORO_VOICE = "af_sarah" # see kokoro voices.json for all options
SAMPLE_RATE = 16000 # whisper.cpp requires 16kHz mono
RECORD_SECONDS = 5
# Kokoro auto-downloads the model files on first instantiation.
kokoro = Kokoro("kokoro-v1.0.onnx", "voices-v1.0.bin")
def record() -> Path:
"""Record from the default mic, write a 16kHz mono WAV, return its path."""
print(f"listening for {RECORD_SECONDS}s...")
audio = sd.rec(
int(RECORD_SECONDS * SAMPLE_RATE),
samplerate=SAMPLE_RATE,
channels=1, # mono
dtype="int16", # 16-bit PCM is what wave.open expects
)
sd.wait() # block until recording finishes
# Fixed path in the system tempdir — overwritten each invocation.
wav_path = Path(tempfile.gettempdir()) / "voice_in.wav"
with wave.open(str(wav_path), "wb") as w:
w.setnchannels(1) # mono
w.setsampwidth(2) # 2 bytes per sample == int16
w.setframerate(SAMPLE_RATE) # 16 kHz — whisper.cpp's required rate
w.writeframes(audio.tobytes())
return wav_path
def transcribe(wav_path: Path) -> str:
"""Run whisper.cpp on a WAV file, return the transcript text."""
result = subprocess.run(
[
"whisper-cli",
"-m", str(WHISPER_MODEL),
"-f", str(wav_path),
"-otxt", # write the transcript to <input>.txt
"-np", # suppress progress prints on stdout
],
capture_output=True,
text=True,
timeout=60,
)
if result.returncode != 0:
raise RuntimeError(f"whisper-cli failed: {result.stderr}")
# -otxt writes the transcript next to the input file, with .txt
# tacked onto the existing name — so /tmp/voice_in.wav becomes
# /tmp/voice_in.wav.txt (same directory, same stem, extra suffix).
txt_path = wav_path.with_suffix(wav_path.suffix + ".txt")
return txt_path.read_text(encoding="utf-8").strip()
def respond(text: str) -> str:
"""Send the transcript to the local LLM, return the reply."""
resp = ollama.chat(
model=CHAT_MODEL,
messages=[
{
"role": "system",
"content": (
"You are a voice assistant. Keep replies to one or "
"two short sentences. No markdown, no lists."
),
},
{"role": "user", "content": text},
],
)
return resp["message"]["content"]
def speak(text: str) -> None:
"""Synthesize the reply with Kokoro and play it back."""
samples, sample_rate = kokoro.create(
text, voice=KOKORO_VOICE, speed=1.0, lang="en-us"
)
sd.play(samples, sample_rate)
sd.wait()
if __name__ == "__main__":
wav = record()
heard = transcribe(wav)
if not heard:
print("nothing heard. exiting.")
raise SystemExit(0)
print(f"you said: {heard}")
reply = respond(heard)
print(f"model: {reply}")
speak(reply)
Під час запуску скрипт протягом п’яти секунд прослуховує, транскрибує, генерує відповідь та вимовляє її. Під час першого запуску відбувається пауза, поки Kokoro завантажує свої файли ONNX у каталог ~/.cache/kokoro-onnx/.
Три фактори проектування мають більше значення, ніж здається. Запис аудіо у форматі моно на частоті 16 кГц є обов’язковим: модель Whisper була навчена на аудіо на цій частоті, а запис з мікрофона з стандартною частотою 44,1 кГц призводить до вигаданого тексту або порожнього результату. Запуск whisper-cli як окремого підпроцесу замість використання Python-інтерфейсу допомагає уникнути конфліктів доступу та проблем із розширеннями на C, а параметр текстового виводу зберігає транскрипцію у файлі, тож немає потреби збирати інформацію зі стандартного виводу. Системний промпт також прямо забороняє використання Markdown, оскільки Kokoro вимовляє зірочки вголос, а форматована відповідь псує користувацький досвід.
Цей цикл обробляє лише один крок. Він не транслює фрагменти аудіо в реальному часі та не зберігає історію розмов. Справжній агент для спілкування мав би додати детектор активності голосу, щоб визначати момент закінчення мовлення, замість фіксованого п’ятисекундного проміжку. Як базовий показник це демонструє, що локальне обладнання може обробити всю ланцюгову структуру від мовлення до тексту, модель та синтез мовлення протягом кількох секунд.
Що дозволяє обладнання
Локальні моделі обмежені пропускною здатністю пам’яті значно більше, ніж простою обчислювальною потужністю. Те, наскільки швидко ваги переміщуються з RAM у процесорні ядра, має більше значення, ніж швидкість виконання арифметичних операцій процесором, і саме цей факт визначає, яка модель підходить для певної машини.
Ноутбук з 16 ГБ пам’яті без окремої відеокарти має пропускну здатність системної пам’яті від 50 до 70 ГБ/с. Він може запускати моделі qwen3:8b або qwen2.5-coder:7b на процесорі зі швидкістю обробки 8–12 токенів на секунду, що достатньо для пошуку та роботи з голосом, але недостатньо для кодування. Apple Silicon змінює ситуацію завдяки об’єднаній пам’яті з пропускною здатністю від 200 до 400 ГБ/с, тож моделі серії M Pro або Max з 24–36 ГБ пам’яті легко справляються з моделлю qwen3-coder:30b, оскільки конструкція MoE активує лише 3 мільярди параметрів на токен.
Відеокарти NVIDIA жертвують об’ємом пам’яті заради пропускної здатності. З 12–16 ГБ VRAM модель qwen3:8b запускається миттєво, а квантована версія qwen3-coder:30b також підходить. При 24 ГБ або більше, наприклад у RTX 3090 чи 4090, модель об’ємом 30 мільярдів параметрів працює на повній швидкості, що наближається до можливостей платної підписки для роботи з одним файлом. При менш ніж 8 ГБ оперативної пам’яті кодування майже неможливе, тому реалістичним варіантом є використання моделей qwen3:4b або phi-4-mini з підтримкою голосу та невеликих завдань пошуку.
Пов’язана література
- Практичні нотатки: Створення агента для програмування в приватній хмарі: OpenCode + локальний LLM — Детальний посібник з практичних нотаток щодо створення агента для програмування в приватній хмарі: OpenCode + локальний LLM, включаючи контракти, перевірки та шаблони коду для команд, які використовують цю модель.
- Відстеження агента Voice RAG за допомогою LiveKit, Qdrant та MLflow — Створення асистента для обробки голосових команд з використанням LiveKit, LlamaIndex та Qdrant, з відстеженням дій через OpenTelemetry у MLflow, яке показує кожен крок отримання інформації та роботи LLM.