Конец забывчивых агентов. Начинаем строительство памяти
Вы наверняка сталкивались с этим: вчера вы подробно обсуждали с локальным ассистентом архитектуру нового микросервиса, а сегодня он смотрит на вас пустым взглядом, как будто вы впервые встретились. Платить OpenAI за бесконечный контекст в GPT-4o-Pro? Не вариант. Запускать 70B-модель для каждого простого вопроса, чтобы она "вспомнила" контекст? Ваш Mac mini превратится в обогреватель.
Проблема в том, что большинство локальных агентов – это одноразовые сессии. Нет памяти. Нет эффективного поиска по прошлому. Нет понимания, когда какую модель использовать. Мы платим токенами (время, ресурсы, деньги) за то, чтобы снова и снова объяснять агенту кто мы и о чем речь.
Если ваш агент не помнит, что вы говорили час назад, вы не используете его на 10% от потенциала. Вы просто гоняете дорогую текстовую прогнозную машину.
Решение – перманентная архитектура. Агент, который живет на вашем Mac mini, записывает каждую значимую мысль в гибридную базу памяти, и при новом запросе умно извлекает релевантный контекст, не загружая в промпт гигабайты текста. И делает это быстро, используя лестницу из моделей разного размера. Вот как это устроено.
Сердце системы: гибридная память, а не просто векторная база
Все бросились делать RAG на векторных эмбеддингах. Это работает, но с большим недостатком: если вы ищете точное название функции или номер версии, семантический поиск может промахнуться. Вместо этого используем гибрид: семантику + лексику.
1 Устанавливаем движки
Нам нужны два инструмента поиска. Для семантики – модель эмбеддингов. Для точного текстового совпадения – алгоритм BM25. И все это в одной легковесной базе.
# Устанавливаем Ollama, если еще нет
brew install ollama
ollama pull nomic-embed-text:latest # Актуальная модель для эмбеддингов на 2026 год
ollama pull llama3.2:1b # Маленькая модель для классификации
# Ставим Python-библиотеки для гибридного поиска
pip install chromadb-hnswlib sentence-transformers rank_bm25 sqlite-vss
2 Строим базу памяти на SQLite
Используем SQLite с расширением VSS (Vector Similarity Search). Это не требует отдельного сервера вроде Pinecone. Вся база – один файл на диске.
import sqlite3
import json
from rank_bm25 import BM25Okapi
from sentence_transformers import SentenceTransformer
# Инициализируем модель для эмбеддингов
embedder = SentenceTransformer('nomic-ai/nomic-embed-text-v1.5', trust_remote_code=True)
# Подключаемся к базе
conn = sqlite3.connect('agent_memory.db')
cursor = conn.cursor()
# Создаем таблицы для записей и векторного индекса
cursor.execute('''CREATE TABLE IF NOT EXISTS memories (
id INTEGER PRIMARY KEY,
content TEXT,
timestamp DATETIME DEFAULT CURRENT_TIMESTAMP,
metadata JSON
)''')
# Расширение VSS должно быть загружено отдельно
# Это позволяет выполнять векторный поиск прямо в SQL
conn.enable_load_extension(True)
conn.load_extension('./vector0') # Путь к скомпилированному vss
conn.load_extension('./vss0')
# Создаем виртуальную таблицу для векторов
cursor.execute('''CREATE VIRTUAL TABLE IF NOT EXISTS v_memories USING vss0(
embedding(768),
content TEXT HIDDEN,
memory_id INTEGER HIDDEN
)''')
conn.commit()
Теперь у нас есть структура. Каждое сообщение агента и пользователя (после фильтрации) записывается в memories. Текст преобразуется в вектор эмбеддинга и попадает в v_memories. Для BM25 мы просто храним токенизированный текст в памяти процесса.
Лестница моделей: не одним гигантом единым
Запускать Deepseek-Coder-V3 на 30 миллиардов параметров для вопроса "Сколько будет 2+2" – расточительство. На Mac mini M4 Pro с 36GB памяти можно держать несколько моделей в оперативке одновременно и переключаться между ними.
| Модель | Размер | Роль в лестнице | Скорость (ток/с) на M4 Pro |
|---|---|---|---|
| Llama 3.2 1B | 1 млрд | Классификатор / роутер | ~120 |
| Qwen2.5 7B | 7 млрд | Базовая генерация, простые задачи | ~45 |
| Command R+ 35B (квантованная) | 35 млрд | Сложный анализ, программирование | ~12 |
Как это работает? Пришел запрос.
- Классификатор (1B) анализирует вопрос: "Пользователь спрашивает про синтаксис Python, сложность низкая, нужен ли контекст из памяти? Да, три последних обсуждения Python". Запускается за 0.5 секунды.
- Гибридный поиск находит 5 самых релевантных фрагментов прошлых диалогов по Python. Используется взвешенная сумма BM25 и векторного сходства.
- Роутер решает: для этого хватит модели 7B. Загружаем контекст (сжатый) в промпт Qwen2.5 и получаем ответ.
- Если бы вопрос был "Напиши архитектуру распределенной системы с гарантированной доставкой сообщений", роутер отправил бы его сразу на 35B-модель, подгрузив больше контекста.
# Упрощенный код роутинга
import ollama
def route_query(query, history_embeddings):
# Шаг 1: Классификация маленькой моделью
router_prompt = f"""Classify the user's query:
Query: {query}
Categories: simple_fact, programming, complex_analysis, creative.
Return JSON: {{"category": "...", "needs_context": true/false}}"""
response = ollama.chat(model='llama3.2:1b', messages=[{'role': 'user', 'content': router_prompt}])
decision = json.loads(response['message']['content'])
# Шаг 2: Извлечение контекста при необходимости
context_chunks = []
if decision['needs_context']:
context_chunks = hybrid_search(query, history_embeddings, top_k=5)
# Шаг 3: Выбор модели для ответа
if decision['category'] == 'simple_fact':
model_for_answer = 'qwen2.5:7b'
elif decision['category'] == 'complex_analysis':
model_for_answer = 'command-r:35b-q4_K_M'
else:
model_for_answer = 'qwen2.5:7b'
# Формируем финальный промпт со сжатым контекстом
final_prompt = build_prompt_with_context(query, context_chunks)
return ollama.chat(model=model_for_answer, messages=[{'role': 'user', 'content': final_prompt}])
Системный промпт как скульптор контекста. Экономия токенов
Самая большая утечка токенов – это когда вы засовываете в контекст всю историю диалога подряд. Наша гибридная память уже отбирает самое важное. Но системный промпт должен это закрепить.
Вот промпт, который заставляет модель быть экономной:
Ты – перманентный ассистент Макса. У тебя есть доступ к базе знаний о прошлых разговорах.
ПРАВИЛА КОНТЕКСТА:
1. Тебе переданы ВЫБРАННЫЕ релевантные фрагменты из памяти. Не упоминай их все, если не нужно.
2. Если пользователь ссылается на что-то, что уже было ("тот самый баг с асинхронностью"), используй переданные фрагменты для понимания.
3. Отвечай кратко, если вопрос простой. Разворачивайся только для сложных тем.
4. В конце важных обсуждений предложи: "Записать основные тезисы в память?"
СЕЙЧАС В КОНТЕКСТЕ:
{context_chunks}
ВОПРОС: {user_query}
Этот промпт выполняет несколько задач: устанавливает личность, задает правила использования контекста (чтобы модель не начала цитировать все подряд) и напоминает агенту, что он может инициировать запись важного. Это снижает объем контекста на 30-50%.
Собираем пазл: архитектура в действии
Весь цикл выглядит так:
- Пользователь: "Напомни, как мы исправляли ту ошибку с deadlock в PostgreSQL?"
- Запрос проходит через классификатор-роутер (1B). Категория: programming, needs_context: true.
- Гибридный поиск в SQLite: BM25 ищет "deadlock PostgreSQL", векторный поиск ищет семантически близкие фразы про блокировки и транзакции. Возвращает 4 фрагмента.
- Роутер видит, что тема сложная, выбирает модель Command R+ 35B.
- Системный промпт формируется с этими 4 фрагментами и вопросом.
- 35B-модель генерирует точный ответ со ссылкой на конкретное прошлое обсуждение.
- Ответ выдается пользователю. Агент параллельно оценивает, стоит ли записать этот новый обмен в память (логирование с метаданными).
Все это работает на Mac mini M4 Pro без обращения к облаку. Задержка? На 2-3 секунды больше, чем у простого вызова одной модели, но точность и контекстуальность выше в разы.
Где спрятаны грабли? Ошибки при сборке
Ошибка 1: Бесконечный рост базы памяти. SQLite-файл через месяц весит 10GB. Решение: реализовать "выцветание". У каждой записи вес (на основе частоты использования). Раз в неделю удалять старые, маловажные фрагменты. Или агрегировать их в суммарные заметки с помощью той же LLM.
Ошибка 2: Классификатор все время ошибается. Маленькая 1B-модель может неправильно оценить сложность. Решение: создать цикл обратной связи. Если пользователь явно недоволен ответом ("Объясни подробнее"), следующее похожее запрос сразу отправлять на модель уровнем выше. И дообучать классификатор на этих примерах.
Ошибка 3: Падение производительности при загрузке нескольких моделей. На Mac mini с 16GB оперативки держать в памяти 1B, 7B и 35B модель одновременно не получится. Решение: использовать vLLM-MLX или Ollama с флагом --num-ctx для управления памятью. Либо держать только две модели в памяти, а третью подгружать по запросу (это добавит 10-15 секунд задержки).
Главный нюанс: эта архитектура не для одноразовых скриптов. Ее нужно собирать как продукт. Но если вы устали каждый день объяснять ассистенту, чем вы занимаетесь, – это окупается за неделю.
Что дальше? Агент выходит за рамки чата
Перманентная память – это только начало. С таким агентом можно интегрировать планировщик задач (он помнит, что вы планировали), файловую систему (он индексирует ваши проекты) и даже управление умным домом ("включи свет в кабинете, как вчера вечером").
Совет напоследок: не пытайтесь сразу сделать идеальную систему. Начните с гибридной памяти на SQLite и одной модели. Когда поймете, как вы ею пользуетесь, добавьте лестницу. Главное – агент должен помнить. Все остальное – технические детали.