Практичні поради: Запустіть корисний локальний LLM за 30 хвилин (програмування, RAG, голос)
Покрокова інструкція до практичних нотаток: як запустити корисний локальний LLM за 30 хвилин (програмування, RAG, голос): контракти, перевірки та шаблони коду для команд, які впроваджують цю схему.
Наведені нижче примітки описують практичний підхід до реалізації проекту «Запустіть корисний локальний LLM за 30 хвилин (програмування, RAG, голосовий ввод)». Основна увага приділяється контрактам, перевіркам та шаблонам коду, а не мотиваційним аспектам. Під час роботи над оглядом спочатку запишіть контракт: необхідні вхідні дані, сигнал про успіх та наслідки часткової невдачі. Такий перелік допоможе зберегти чесність у подальших змінах коду. Документуйте як успішний, так і відновлювальний сценарії роботи. Повторні спроби, людський контроль та обробка некоректних повідомлень є частиною продукту, а не етапом подальшої оптимізації.
$ ollama run qwen3:8b
>>> rewrite this function to use async/await
П’ятихвилинна спільна налаштування
П’ятихвилинна спільна налаштування працює найкраще, якщо її розглядати як вимірювану основу. Запишіть один ідеальний приклад виконання, один випадок збою та примітку щодо скасування змін перед розширенням обсягу роботи. Віддавайте перевагу невеликим, тестованим одиницям перед складними скриптами. Якщо якийсь крок зазнає невдачі, причина має вказувати на конкретну відповідальність, а не на заплутану послідовність дій. Встановіть ліміти на кількість токенів за хід та за сеанс. Інструменти з агентним підходом активно розширюють контекст; жорсткі обмеження запобігають тому, що демонстрації перетворюються на несподівані рахунки.
brew install ollama
curl -fsSL https://ollama.com/install.sh | sh
ollama pull qwen3:8b
ollama run qwen3:8b "Write a python script to reverse a string."
Шлях А: Місцевий асистент з програмування
Шлях А: Місцевий асистент з кодування працює найкраще, коли його розглядають як вимірювану систему. Збережіть один ідеальний зразок результату, один випадок невдачі та примітку про скасування змін перед розширенням обсягу роботи. Розглядайте цей етап як контракт між вхідними даними та перевіреними результатами. Позначте всі елементи, визначте критерії успіху та не погоджуйтесь на мовчазне часткове виконання завдань. Встановіть ліміт токенів на кожну операцію та сесію. Інструменти типу агентів активно розширюють контекст; жорсткі обмеження запобігають тому, щоб демонстрації перетворювалися на несподівані рахунки.
# For 24GB+ VRAM or 32GB+ Mac Unified Memory
ollama pull qwen3-coder:30b
# For 16GB RAM laptops
ollama pull qwen2.5-coder:7b
Модель Ollama, налаштована за принципами Cline
Модель Ollama, налаштована за методом Cline, працює найкраще, коли її розглядають як вимірювану поверхню. Збережіть один ідеальний запис, один випадок збою та примітку про скасування змін перед розширенням обсягу роботи. Записуйте час виконання та витрати на токени чи запити поруч із функціональними результатами. Візуальний контроль витрат заздалегідь запобігає несподіваним рахункам під час переходу від демо-версії до спільних середовищ. Фіксуйте версію інтерпретатора та файл з параметрами залежностей перед навчанням циклів. Розбіжності між ноутбуком та системою CI є найпоширенішою причиною прихованих збоїв у демо-версіях API. Модель Ollama, налаштована за методом Cline, працює найкраще, коли її розглядають як вимірювану поверхню. Збережіть один ідеальний запис, один випадок збою та примітку про скасування змін перед розширенням обсягу роботи. Документуйте як успішний, так і відновлювальний сценарії роботи разом. Повторні спроби, людський контроль та обробка некоректних повідомлень є частиною продукту, а не етапом подальшої оптимізації.
# 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|>"
# 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"
API Provider: Ollama
Base URL: http://localhost:11434
Model: qwen3-coder-cline
Context Window: 65536
Шлях B: RAG з власними документами
Для шляху B: RAG над власними документами – необхідно визначити вхідні дані, власника кроку та критерії завершення перед зміною коду. Оператори повинні мати можливість перезапустити крок з відомої точки контролю, не намагаючись вгадати прихований стан. Краще використовувати невеликі, перевірювані одиниці коду замість об’ємних скриптів. Коли крок зазнає невдачі, причина має вказувати на конкретну відповідальність, а не на заплутану структуру процесу. Коли наступним кроком є код чи виклик інструменту, краще використовувати структуровані результати з перевіркою схеми, ніж вільний текст.
llama pull nomic-embed-text
pip install ollama numpy
"""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?"
Шлях C: Цикл голосу
Для шляху C: The Voice Loop — перед зміною коду необхідно визначити вхідні дані, власника кроку та критерії завершення. Оператори повинні мати можливість перезапустити крок з відомої точки контролю, не намагаючись вгадати прихований стан. Розглядайте цю стадію як контракт між вхідними даними та перевіреними результатами. Призначте назви елементів, визначте критерії успіху та не допускайте беззвучного часткового завершення. У разі, коли наступним кроком є код або виклик інструменту, віддавайте перевагу структурованим результатам із перевіркою за схемою перед вільним текстом.
brew install whisper-cpp ffmpeg
pip install -U sounddevice ollama kokoro-onnx
The Voice Pipeline
Для The Voice Pipeline необхідно визначити вхідні дані, власника кроку та критерії завершення перед зміною коду. Оператори повинні мати можливість перезапустити крок з відомої точки контролю, не намагаючись вгадати прихований стан. Записуйте час виконання та витрати на токени чи запити поруч із функціональними результатами. Візуалізація витрат заздалегідь запобігає несподіваним рахункам під час переходу з демо-середовища у спільні. У разі, коли наступним кроком є код чи виклик інструменту, віддавайте перевагу структурованим результатам із верифікацією схеми перед вільним текстом. Для The Voice Pipeline необхідно визначити вхідні дані, власника кроку та критерії завершення перед зміною коду. Оператори повинні мати можливість перезапустити крок з відомої точки контролю, не намагаючись вгадати прихований стан. Документуйте як оптимальний, так і альтернативний сценарії виконання. Повторні спроби, людський контроль та обробка некоректних повідомлень є частиною продукту, а не етапом подальшої оптимізації.
"""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)
Реальність апаратного забезпечення
Під час роботи над книгою «The Hardware Reality» спочатку запишіть умови виконання: необхідні вхідні дані, сигнал про успіх та те, що відбувається при частковій несправності. Такий перелік допомагає зберігати чесність пізніших змін у коді. Віддавайте перевагу невеликим, тестованим одиницям коду перед об’ємними скриптами. Якщо якийсь крок зазнає невдачі, причина має вказувати на конкретну відповідальність, а не на заплутану послідовність операцій. Зберігайте у кеші стабільні інструкції системи та схеми інструментів. Повторна передача ідентичних даних є поширеною причиною несправностей.
Продовжити читання
Під час роботи над опцією «Продовжити читання» спочатку запишіть умови контракту: необхідні вхідні дані, сигнал про успіх та те, що відбувається у разі часткової невдачі. Такий перелік допомагає зберігати чесність пізніших змін у коді. Розглядайте цей етап як контракт між вхідними даними та перевіреними результатами. Призначте назви елементів, визначте критерії успіху та не допускайте беззвучного часткового виконання. Зберігайте у кеші стабільні інструкції системи та схеми інструментів. Повторна передача ідентичних даних є поширеною причиною витрат ресурсів.
Чек-лист експлуатації
Для чек-листу експлуатації перед зміною коду визначте вхідні дані, відповідальну особу за крок та критерії завершення. Оператори повинні мати можливість перезапустити крок з відомої точки контролю, не здогадуючись про прихований стан. Тримайте конфігурацію окремо від коду програми. Файли середовища, сховища конфіденційних даних та флаги функцій мають знаходитися в одному місці, яке оператори можуть перевірити, не читаючи весь код.
Варто віддавати перевагу структурованим результатам із підтвердженням схеми перед вільним текстом, коли наступним кроком є написання коду чи виклик інструменту.
Перед налаштуванням запитів вимірюйте рівень відтворення інформації за фіксованим набором запитань. Часта зміна запитів рідко допомагає покращити якість пошуку.
Зберігайте версії залежностей та фіксуйте результати обробки зображень, які використовувалися під час демонстрації. Відтворюваність результатів краща за індивідуальні знання спеціалістів.
Розглядайте цей етап як контракт між вхідними даними та перевіреними результатами. Називайте всі елементи, визначайте критерії успіху та не погоджуйтесь на часткове виконання без пояснень.
Перед тим, як переходити до наступного етапу, заморожуйте версії, створюйте остаточну записку для критичних процесів та переконуйтесь у наявності кроків для скасування змін. У спільних середовищах необхідні обмеження на кількість операцій, перевірки прав доступу та чіткий власник для зміни секретних даних. Краща надійність, ніж креативні одноразові демонстрації.
Примітка до пакету 9f628082e0d0: не включайте ключі постачальників у репозиторій, встановіть ліміт токенів на сеанс та зберігайте транскрипції поруч із фіксами для оцінки, щоб подальша заміна моделей залишалася порівнянною.
Примітка щодо посилення безпеки 0 найкраще працює, якщо її розглядати як вимірювану характеристику. Збережіть одну ідеальну транскрипцію, один випадок збою та примітку щодо скасування змін перед розширенням обсягу. Тримайте конфігурацію окремо від коду додатку. Файли середовища, сховища секретів та флаги функцій мають знаходитися в одному місці, яке оператори можуть перевіряти, не читаючи весь граф.
Деталь посилення безпеки 0/770: вимірюйте час виконання, клас помилки та витрату токенів для цієї примітки, а потім вирішуйте, чи залишити зміни, ґрунтуючись на фіксованому наборі питань, а не на окремих випадках.
Для пункту 1 щодо посилення безпеки необхідно визначити вхідні дані, відповідальну особу за крок та критерії завершення перед зміною коду. Оператори повинні мати можливість перезапустити крок з відомої точки контролю, не здогадуючись про прихований стан. Краще використовувати невеликі, перевірювані одиниці коду замість об’ємних скриптів. Коли крок зазнає невдачі, причина має вказувати на конкретну відповідальність, а не на заплутану структуру обробки даних.
Деталь посилення безпеки 1/770: виміряйте час виконання, клас помилки та кількість витрачених токенів для цього пункту, а потім вирішіть, чи залишити зміни, ґрунтуючись на фіксованому наборі питань, а не на індивідуальних спостереженнях.
Під час роботи над пунктом 2 щодо посилення безпеки спочатку запишіть умови виконання: необхідні вхідні дані, сигнал про успіх та те, що відбувається у разі часткової невдачі. Такий перелік допомагає зберігати чесність пізніших змін у коді. Поруч із функціональними результатами задокументуйте час виконання та витрати на токени або запити. Чітке бачення витрат заздалегідь запобігає несподіваним рахункам під час переходу з демо-середовища до спільних.
Деталь 2/770 щодо посилення безпеки: виміряйте час виконання, клас помилки та витрати на токени для цього пункту, а потім вирішіть, чи залишити зміни, ґрунтуючись на фіксованому наборі питань, а не на індивідуальних спостереженнях.
Пункт 3 щодо посилення безпеки найкраще працює, якщо його розглядати як вимірювану поверхню. Збережіть один ідеальний запис виконання, один випадок невдачі та запис про скасування змін перед розширенням обсягу роботи. Документуйте як успішний, так і відновлювальний сценарії. Повторні спроби, людські контролі та обробка некоректних повідомлень є частиною продукту, а не етапом подальшої оптимізації.
Деталь посилення безпеки 3/770: виміряйте час виконання, клас помилки та кількість витрачених токенів для цього запису, а потім вирішіть, чи залишити зміни, ґрунтуючись на фіксованому наборі питань, а не на окремих випадках.
Для запису про посилення безпеки 4 визначте вхідні дані, власника кроку та критерії завершення перед зміною коду. Оператори повинні мати можливість перезапустити крок з відомої точки контролю, не здогадуючись про прихований стан. Розглядайте цю стадію як контракт між вхідними даними та перевіреними результатами. Позначте елементи, визначте критерії успіху та відмовляйтесь від мовчазного часткового завершення.
Деталь посилення безпеки 4/770: виміряйте час виконання, клас помилки та кількість витрачених токенів для цього запису, а потім вирішіть, чи залишити зміни, ґрунтуючись на фіксованому наборі питань, а не на окремих випадках.
Під час роботи над пунктом 5 щодо посилення безпеки спочатку запишіть умови виконання: необхідні вхідні дані, сигнал про успіх та те, що відбувається у разі часткової невдачі. Такий перелік допомагає зберігати чесність пізніших змін у коді. Зберігайте конфігурацію окремо від коду додатку. Файли середовища, сховища секретних даних та флаги функцій мають знаходитися в одному місці, де оператори можуть їх перевіряти, не читаючи весь код.
Деталь 5/770 щодо посилення безпеки: вимірюйте час виконання, клас помилки та кількість витрачених токенів для цього пункту, а потім вирішуйте, чи залишити зміни, ґрунтуючись на певному наборі критеріїв, а не на індивідуальних спостереженнях.