Створення локального штучного інтелекту: моделі, пам’ять, зберігання та інтерфейс
П’ять шарів стеку локального штучного інтелекту, з порівнянням Ollama, OpenClaw, Mem0 та Qdrant або Chroma, а також виправлення проблем з JSON у режимі мислення та непомітних розбіжностей у розмірах.
Джерела: Building Local AI Agents: A Practical Guide to Models, Memory, and Orchestration
Локальний AI-агент зберігає модель на вашому власному обладнанні, діє від вашого імені та зберігає контекст між сеансами, не надсилаючи дані до зовнішнього API. Простий чат-бот відповідає та забуває. Агент може міркувати крок за кроком, використовувати інструменти та накопичувати знання з часом. Робота з ним локально дає вам асистента без компромісів щодо приватності чи плати за кожен виклик, як у хостованих сервісах.
Для створення одного такого агента мають працювати разом п’ять шарів: шар LLM, який виконує обробку даних на вашому пристрої, фреймворк агента, який координує та виконує дії, шар пам’яті, який покращує функціонал агента з часом, шар зберігання, який зберігає отриману інформацію, та шар інтерфейсу, який адаптується під ваш спосіб роботи. Нижче для кожного шару наведені варіанти, компроміси та рекомендації, щоб ви могли скласти комплект, який підходить саме вам.
Шар 1: модель
Це двигун. Модель виявляє намір користувача, генерує код та видобуває структуровані дані, від яких залежить решта системи; усе інше є допоміжними елементами, створеними на основі її надійних можливостей. Три інструменти домінують у локальних обчисленнях.
Ollama — це найпростіший спосіб початку роботи. Одним командом його встановлюють, однією командою завантажують модель, і ви одразу отримуєте API, сумісне з OpenAI, на локальному хості. Він керує моделями, вибирає спосіб квантування та налаштовує вікна контексту без необхідності конфігурації. Він не створений для великих обсягів трафіку та краще підходить для особистого асистента чи інструменту розробника.
ollama pull qwen3:8b
ollama pull nomic-embed-text
llama.cpp знаходиться на нижчому рівні. Це движок на C++, який безпосередньо запускає квантовані моделі GGUF, без демона та без HTTP-шару. Його мінімальне споживання пам’яті робить його ідеальним для периферійних пристроїв, таких як Raspberry Pi чи вбудовані системи Linux, де Ollama був би занадто важким. Недоліком є відсутність вбудованого керування моделями та необхідність більш ручної налаштування.
vLLM — це варіант для серійної експлуатації. Він використовує технології PagedAttention та безперервне групування даних, які лежать в основі масштабованих обчислень у хмарі. Якщо ви обслуговуєте багато користувачів або маєте високі вимоги до пропускної здатності, це правильний вибір, хоча він вимагає належної інфраструктури GPU та масштабується краще, ніж два попередніх варіанти.
Для локального асистента окремого розробника Ollama є розумним стандартом, адже простота важливіша за продуктивність. Варто згадати модель qwen3:8b: вона забезпечує надійний JSON для розпізнавання намірів, створює чистий код та поміщається у 8 ГБ VRAM. Є одна особливість, про яку варто знати: вона обгортає свою відповідь у блоки <think>…</think> перед справжнім контентом, що ускладнює роботу будь-якого парсера JSON із сирою відповіддю. Невеликий обгортач, який у всіх місцях встановлює think: False, вирішує цю проблему:
_orig_ollama_chat = ollama.chat
def _no_think_chat(*args, **kwargs):
opts = kwargs.get("options") or {}
if isinstance(opts, dict):
opts.setdefault("think", False)
kwargs["options"] = opts
return _orig_ollama_chat(*args, **kwargs)
ollama.chat = _no_think_chat
Оскільки використовується setdefault, окремі користувачі все одно можуть увімкнути режим мислення, коли це потрібно. Для вилучення даних у форматі JSON та генерації коду в інструментальному конвеєрі режим мислення збільшує затримку без покращення якості.
Рівень 2: фреймворк агента
Модель генерує текст. Фреймворк вирішує, що робити з цим текстом та які дії виконати далі, враховуючи класифікацію намірів, маршрутизацію та виконання.
OpenClaw орієнтований на інструменти та працює на рівні файлової системи. Кожна дія — це команда, яка надсилається до визначеної навички, наприклад для створення файлу, запису коду, відкриття папки чи виконання команди терміналу. Ви пишете файл SKILL.md, у якому вказуєте назву навички та спосіб її використання, а фреймворк займається маршрутизацією:
- -
name: local-ai-assistant
description: Local AI coding assistant with persistent memory
command-dispatch: tool
command-tool: exec
command-arg-mode: raw
-
Коли більшість операцій — це робота з файлами, як у кодинговому асистенті, ця модель ідеально підходить. Рядки command-dispatch: tool та command-tool: exec наказують OpenClaw виконувати Python-скрипти під час запуску певної функції, тож робота з файлами проходить через цю платформу. Її обмеження полягають у тому, що вона не розроблена для довгих ланцюгів багатоетапних міркувань чи для координації кількох агентів.
LangGraph обирає протилежний підхід. Ви визначаєте чітку графіку стану, у якому вузли — це виклики моделей чи дії, а ребра — переходи; платформа зберігає стан між кроками. Це передбачає попередню розробку: ви повинні сформувати схему стану та структуру графа ще до реалізації будь-якої бізнес-логіки. Для простого використання інструментів це зайва складність, але для складних багатоетапних процесів це забезпечує можливість переглядати записи про те, що відбувалося на кожному кроці.
CrewAI знаходиться ще на один рівень вище. Ви описуєте агентів із ролями та завданнями разом із поясненнями, а фреймворк визначає порядок виконання та способи комунікації між агентами. Це найшвидший шлях до функціонального прототипу з кількома агентами, хоча така абстракція ускладнює виправлення помилок.
Який би варіант ви не обрали, класифікація наміру спочатку виконується для кожного отриманого повідомлення. Поєднання класифікації на основі моделей із резервним методом на основі regex краще справляється з помилками, ніж будь-який з методів окремо:
def detect_intent(message: str) -> dict:
try:
resp = ollama.chat(
model=OLLAMA_CHAT_MODEL,
messages=[{"role": "user", "content": INTENT_PROMPT.format(message=message)}],
options={"temperature": 0, "num_predict": 1024},
)
return extract_json(resp["message"]["content"])
except Exception:
return keyword_intent_fallback(message)
Функція резервного варіанту фіксує поширені сценарії, коли модель повертає некоректний результат. При вимкненому механізму обробки думок та температурі 0 більшість запитів обробляються за допомогою шляху моделі, проте саме функція резервного варіанту забезпечує продовження роботи агента.
Шар 3: пам’ять
Пам’ять відрізняє агента, який пристосовується до вас, від того, хто сприймає кожну розмову як першу. Він вивчає ваші звички, уподобання та контекст проекту та застосовує їх без додаткових вказівок.
Звичайні способи обходу проблем знаходяться всередині вікна контексту: файл PREFERENCES.md, який читається під час запуску, довге запитання системи або файл CONTEXT.md. Усі вони мають одну слабкість — у межах вікна вони піддаються компресії, обмеженням кількості токенів та перезапускам сеансу. Насправді агенту потрібна пам’ять поза межами вікна, яка зберігатиметься довгостроково, дозволятиме знаходити потрібну інформацію за потреби та оновлюватися у міру змін фактів. Існують три поширені підходи, які задовольняють різні потреби.
Базовий RAG є найпоширенішим та найзручнішим для початківців. Він дозволяє розділяти документи чи історію розмов на частини, вбудовувати ці частини та отримувати k найбільш схожих результатів при надходженні запиту, причому для його налаштування потрібно мінімум зусиль. Його слабкість полягає у тому, що пам’ять перетворюється на накопичення документів, без розрізнення того, що варто зберігати довгостроково, а що — це тимчасове запитання.
Пам’ять обмежена сеансом, наприклад ConversationBufferMemory та ConversationSummaryMemory від LangChain, забезпечує послідовність у межах сеансу. Агент не забуде те, що ви сказали п’ять повідомлень тому, а резюме запобігають заповненню обмеженого простору. Однак вона не зберігається після перезапуску, що є основною проблемою для кодового асистента.
Структурована постійна пам’ять, як у Mem0, розглядає пам’ять як дані першого класу, а не просто текст. Замість слів у первинному вигляді модель виділяє окремі факти, які потім зберігаються у векторній базі даних. Під час пошуку запит також перетворюється на вектор, і повертаються семантично схожі факти, навіть якщо нове повідомлення не використовує первинні формулювання. Збережені факти залишаються між сеансами, можуть об’єднуватися при дублюванні та переписуватися, коли змінюється реальність. Mem0 підтримує локальні моделі, тому його легко під’єднати до стеку, заснованого на Ollama.
Візьмемо розробника, який каже, що йому подобається pytest. Звичайна система RAG зберігає саме це речення у первинному вигляді. Структурована система виділяє та зберігає саме цю уподобаність. Наступного разу, коли ви попросите функцію з тестами, пам’ять поверне інформацію про те, що „користувач віддає перевагу pytest“, навіть якщо у повідомленні ніколи не згадувалося конкретне фреймворк тестування.
Підключення Mem0 до локальної конфігурації Ollama вимагає короткої налаштування, яка вказує на ваш локальний модель, локальну модель ембеддингів та сховище векторів:
from mem0 import Memory
config = {
"llm": {
"provider": "ollama",
"config": {"model": "qwen3:8b", "ollama_base_url": "http://localhost:11434"},
},
"embedder": {
"provider": "ollama",
"config": {"model": "nomic-embed-text", "ollama_base_url": "http://localhost:11434"},
},
"vector_store": {
"provider": "qdrant",
"config": {"host": "localhost", "port": 6333, "embedding_model_dims": 768},
},
}
memory = Memory.from_config(config)
memory.add("I always use type hints and pytest", user_id="dev")
results = memory.search("write a utility function", user_id="dev")
Блок llm спрямовує Mem0 до qwen3:8b для вилучення фактів, перетворюючи необроблене повідомлення на окремі збережені уподобання замість тексту без змін. Блок embedder використовує nomic-embed-text для векторизації цих фактів під час запису та для вбудовування запитів під час пошуку, а memory.search() потім знаходить інформацію за сенсом, а не за ключовими словами.
Недооціненим покращенням для будь-якого підходу до використання пам’яті є фільтрація того, що потрапляє у зберігання перед його записом. Легкий класифікатор, який працює перед кожним записом, допомагає підтримувати чистоту пам’яті та високу якість пошуку:
def _is_worth_storing(self, user_message: str) -> bool:
response = ollama.chat(
model=OLLAMA_CHAT_MODEL,
messages=[{"role": "user", "content": SMART_MEMORY_PROMPT.format(
user_message=user_message
)}],
options={"temperature": 0, "num_predict": 512},
)
data = self._extract_json_robust(response["message"]["content"])
return bool(data.get("worth_storing", False))
Його запит надає моделі чіткі приклади цього розрізнення:
HIGH-VALUE (worth storing):
- "I prefer TypeScript over JavaScript"
- "I use pytest, never unittest"
- "Always use Google-style docstrings"
LOW-VALUE (discard):
- "What does enumerate() do?"
- "Write a retry decorator"
- "Thanks"
Якщо пропустити фільтрацію, база даних векторів поступово заповнюється одноразовими запитами, що погіршує якість пошуку.
Рівень 4: зберігання
Рівень пам’яті вирішує, що слід зберегти, а зберігання — це місце, де ці дані знаходяться на диску.
FAISS — це бібліотека, яка працює всередині процесу: швидка, добре протестована та не потребує інфраструктурних залежностей. Її можна вбудувати у ваш Python-процес та запускати в пам’яті або серіалізувати на диск. Її обмеження пов’язані з експлуатацією: для постійного зберігання потрібні спеціальні функції серіалізації, а також відсутній HTTP API чи вбудована функція реплікації. Це підходить для дослідницьких процесів та пакетних завдань, але є недостатнім для довготривалого асистента, який часто перезапускається.
Chroma — це найпростіша векторна база даних, яку може обрати розробник. Вона починає свою роботу як локальний процес Python або працює вбудовано у ваш власний код, автоматично зберігаючи дані у локальну директорію. Встановіть її за допомогою:
pip install chromadb
Тисячі збережених фактів у особистому асистенті цілком в межах можливостей Chroma. Він повністю усуває залежність від інфраструктури, але фільтрація є обмеженішою, ніж у Qdrant.
Qdrant працює як справжній сервіс із повними API REST та gRPC, фільтрацією даних та векторами з іменами. Він призначений для використання з мільйонами векторів та одночасними запитами. Він автоматично зберігає дані на диску та продовжує працювати після перезапуску Docker без додаткових дій:
# Qdrant via Docker
docker run -d - name qdrant-local -p 6333:6333 \
-v $(pwd)/qdrant_storage:/qdrant/storage qdrant/qdrant
Одна деталь конфігурації може тихо зламати все: розмір вектора у налаштуваннях сховища векторів має точно збігатися з розміром вихідних даних вашої моделі ембеддингу. Наприклад, модель nomic-embed-text генерує вектори розміром 768. Якщо змінити модель ембеддингу, не оновивши це значення, вставки проваляться без видимої помилки.
"vector_store": {
"provider": "qdrant",
"config": {
"collection_name": "coding_assistant",
"host": "localhost",
"port": 6333,
"embedding_model_dims": 768, # must match your embedding model exactly
},
}
Chroma — це пряма заміна, якщо ви хочете позбутися Docker: достатньо змінити постачальника vector_store у конфігурації, а решта стеку залишається незмінною.
Рівень 5: інтерфейс
Найзовнішніший рівень визначає спосіб взаємодії з агентом, і правильний вибір залежить від того, де він розташований у вашому робочому процесі.
- Веб-Інтерфейс. Найбільш гнучкий варіант. Він працює у браузері, відображає markdown, виділений код та секції, які можна згортати, і функціонує на будь-якій операційній системі без встановлення. Асистент з програмування може показати поточний файл, створений код та стан пам’яті агента одночасно на одному екрані.
Як варіанти взаємодіють між собою
Кожен шар обмежує своїх сусідів. Модель вибирає формат API, який використовує фреймворк, підхід до керування пам’яттю визначає, яке зберігання має бути підтримуване, а інтерфейс визначає, наскільки важливе форматування результату.
Для локального асистента з кодування окремого розробника ця стек-архітектура мінімізує перешкоди, зберігаючи при цьому можливість заміни кожного шару:
Interface: Web UI on localhost
│
Framework: OpenClaw - tool-dispatch, SKILL.md routing
│
LLM: Ollama for qwen3:8b
(think mode disabled for JSON reliability)
│
Memory: Structured persistent memory with value filtering
Embeddings via nomic-embed-text
│
Storage: Chroma (zero infrastructure) or Qdrant (more robust)
Оскільки кожен шар можна замінювати незалежно, почніть із найпростішого варіанта на кожному рівні та покращуйте його у міру зростання потреб. Перехід від Chroma до Qdrant або від Ollama до vLLM не вимагає перебудови чогось над чи під ним. Якщо якийсь шар заслуговує на ранні інвестиції, це має бути пам’ять. Проста локальна модель із добре спроєктованим шаром постійної пам’яті надійно перевершує більшу модель, яка щоразу на початку сеансу починає роботу з нуля.