Векторные базы убили? Не совсем, но Memory Agent показывает альтернативу
Каждый второй RAG-пайплайн сегодня — это векторная база. Pinecone, Weaviate, Qdrant. Вы тратите недели на настройку эмбеддингов, индексы съедают гигабайты RAM, а поиск по смыслу все равно иногда выдает ерунду. Звучит знакомо?
В начале 2025 года Google Research тихо выкатила концепцию Memory Agent — архитектурный паттерн, который предлагает забыть про вектора для небольших и средних знаний. Вместо эмбеддингов — SQLite. Вместо косинусной похожести — LLM-рассуждение. Идея проста до гениальности: если контекстные окна моделей уже перевалили за 1 млн токенов (как у Gemini Ultra 2.0 или GPT-5), зачем разбивать текст на куски и искать похожие? Проще спросить у модели: «Вот все мои заметки, найди то, что относится к вопросу X».
Важно: Memory Agent — не готовый продукт, а архитектурный шаблон. Вы не найдете его в npm или PyPI. Это способ думать о памяти для AI-ассистентов, особенно для локальных сред вроде Obsidian.
Почему SQLite и LLM бьют вектора в их же игре
Представьте: у вас 5000 заметок в Obsidian. Векторный подход требует:
- Разбить каждую заметку на чанки (по 500 токенов, условно)
- Сгенерировать эмбеддинги для каждого чанка (с помощью модели, которая тоже ест ресурсы)
- Сохранить эмбеддинги в отдельную БД, построить индекс HNSW или IVF
- При запросе — искать похожие чанки, потом агрегировать ответ
Memory Agent делает иначе:
- Все заметки хранятся в SQLite в сыром виде (метаданные + текст)
- При запросе — LLM анализирует вопрос и решает, какие таблицы или строки могут быть релевантны
- SQLite выполняет быстрый поиск по ключевым словам или метаданным
- LLM получает кандидатов и «рассуждает», какие из них действительно отвечают на вопрос
- Итоговый контекст подается в основную модель для генерации ответа
Разница — как между поиском в Google (вектора) и вопросом к эксперту, который помнит все ваши файлы (LLM-рассуждение). Для личных знаний, где данные не миллионы документов, а тысячи, второй подход часто точнее и намного экономнее. Подробнее о конфликте подходов я писал в статье про архитектуру локального RAG-пайплайна.
Собираем Memory Agent для Obsidian: пошаговый разбор
Теория — это здорово, но давайте руками. Вот как заменить векторную базу на SQLite + LLM для ваших заметок. Предполагаю, что у вас уже стоит Obsidian и Python 3.11+.
1 Готовим базу: выгружаем заметки в SQLite
Первым делом — спарсим все .md файлы из вашего хранилища Obsidian. Не нужно сложных инструментов — обычный скрипт на Python.
import sqlite3
import os
import frontmatter
from pathlib import Path
# Создаем базу
conn = sqlite3.connect('obsidian_memory.db')
cursor = conn.cursor()
# Таблица для заметок
cursor.execute('''
CREATE TABLE IF NOT EXISTS notes (
id INTEGER PRIMARY KEY AUTOINCREMENT,
file_path TEXT UNIQUE,
title TEXT,
content TEXT,
tags TEXT,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
)
''')
# Индексируем для быстрого поиска по тексту (не вектора, а полнотекстовый!)
cursor.execute("CREATE VIRTUAL TABLE IF NOT EXISTS notes_fts USING fts5(title, content, tags)")
# Проходим по папке Obsidian
vault_path = Path.home() / 'Obsidian Vault'
for md_file in vault_path.rglob('*.md'):
with open(md_file, 'r', encoding='utf-8') as f:
post = frontmatter.load(f)
title = post.get('title', md_file.stem)
tags = ','.join(post.get('tags', []))
content = post.content
cursor.execute('''
INSERT OR REPLACE INTO notes (file_path, title, content, tags)
VALUES (?, ?, ?, ?)
''', (str(md_file), title, content, tags))
# Заполняем FTS
cursor.execute('INSERT INTO notes_fts (title, content, tags) VALUES (?, ?, ?)',
(title, content, tags))
conn.commit()
conn.close()
print(f"Индексировано заметок: {cursor.rowcount}")
2 Пишем Memory Agent: SQLite + LLM-рассуждение
Сердце системы — агент, который решает, что искать. Берем локальную LLM (например, Gemma 3 8B через Ollama) или облачную (Gemini Ultra 2.0). Я покажу вариант с Gemini, так как он бесплатен до определенного лимита.
import google.generativeai as genai
import sqlite3
from datetime import datetime, timedelta
# Настройка Gemini (актуально на 03.04.2026)
genai.configure(api_key='YOUR_API_KEY')
model = genai.GenerativeModel('gemini-2.0-ultra')
class MemoryAgent:
def __init__(self, db_path='obsidian_memory.db'):
self.conn = sqlite3.connect(db_path)
self.cursor = self.conn.cursor()
def _reason_about_query(self, user_query):
"""LLM анализирует запрос и решает, как искать."""
prompt = f"""
Ты — Memory Agent для Obsidian. У тебя есть доступ к базе заметок.
Запрос пользователя: "{user_query}"
Определи:
1. Ключевые слова для полнотекстового поиска в SQLite FTS.
2. Возможные теги (tags), которые могут быть в заметках.
3. Ограничение по времени (если запрос про недавние события).
Верни ответ в формате:
KEYWORDS: список слов через пробел
TAGS: список тегов через запятую или None
TIME_FILTER: last_week, last_month, None
"""
response = model.generate_content(prompt)
result_text = response.text
# Парсим ответ (упрощенно)
lines = result_text.split('\n')
keywords = ''
tags = None
time_filter = None
for line in lines:
if line.startswith('KEYWORDS:'):
keywords = line.replace('KEYWORDS:', '').strip()
elif line.startswith('TAGS:'):
tags = line.replace('TAGS:', '').strip()
if tags.lower() == 'none':
tags = None
elif line.startswith('TIME_FILTER:'):
time_filter = line.replace('TIME_FILTER:', '').strip()
if time_filter.lower() == 'none':
time_filter = None
return keywords, tags, time_filter
def search(self, user_query, limit=10):
"""Основной метод поиска."""
# Шаг 1: LLM решает, как искать
keywords, tags, time_filter = self._reason_about_query(user_query)
# Шаг 2: SQLite ищет кандидатов
sql_query = '''
SELECT n.id, n.title, n.content, n.tags, n.created_at
FROM notes n
JOIN notes_fts fts ON n.id = fts.rowid
WHERE notes_fts MATCH ?
'''
params = [keywords]
if tags:
sql_query += ' AND n.tags LIKE ?'
params.append(f'%{tags}%')
if time_filter == 'last_week':
week_ago = (datetime.now() - timedelta(days=7)).isoformat()
sql_query += ' AND n.created_at > ?'
params.append(week_ago)
sql_query += ' LIMIT ?'
params.append(limit * 3) # Берем в 3 раза больше для последующей фильтрации LLM
self.cursor.execute(sql_query, params)
candidates = self.cursor.fetchall()
# Шаг 3: LLM выбирает лучшие кандидаты
candidate_texts = []
for cand in candidates:
cand_str = f"ID: {cand[0]}, Title: {cand[1]}, Tags: {cand[3]}, Snippet: {cand[2][:200]}"
candidate_texts.append(cand_str)
prompt = f"""
Запрос: {user_query}
Кандидаты из базы:
{'\n'.join(candidate_texts)}
Выбери TOP-{limit} самых релевантных кандидатов (укажи только ID через запятую).
Объясни кратко, почему они подходят.
"""
response = model.generate_content(prompt)
# Парсим ID (реализуйте согласно ответу модели)
# ... (опущено для краткости)
# Возвращаем финальные заметки
final_ids = [1, 3, 5] # Пример
final_notes = [c for c in candidates if c[0] in final_ids]
return final_notes[:limit]
def close(self):
self.conn.close()
Внимание: Код упрощен. В реальности нужно обрабатывать ошибки API, кэшировать запросы, и аккуратно парсить вывод LLM. Но скелет рабочий.
3 Интеграция с Obsidian: плагин или внешний скрипт
Тут два пути. Первый — написать свой плагин на TypeScript (долго, но красиво). Второй — использовать внешний скрипт, который запускается через Obsidian Command Palette и показывает результаты в модальном окне. Я предпочитаю второй вариант, потому что он проще.
Установите плагин Hotkey Helper (партнерская ссылка) для удобства. Затем создайте Python-скрипт, который будет вызываться через системную команду.
# obsidian_memory_cli.py
import sys
from memory_agent import MemoryAgent
query = sys.argv[1] if len(sys.argv) > 1 else ""
agent = MemoryAgent()
results = agent.search(query, limit=5)
# Выводим в формате, который поймет Obsidian
for r in results:
print(f"## {r[1]}\n")
print(f"{r[2][:500]}...\n")
print(f"**Tags:** {r[3]}\n")
print("---\n")
agent.close()
В Obsidian настройте хоткей, который запускает этот скрипт через терминал и выводит результат. Подробнее о такой интеграции я писал в статье про замену Gemini CLI для Obsidian.
Подводные камни: где Memory Agent проигрывает векторам
Не обольщайтесь. Этот подход — не серебряная пуля. Вот где он даст сбой:
- Большие объемы: Если у вас 100k+ документов, LLM будет долго рассуждать над каждым запросом. Векторный поиск масштабируется лучше.
- Сложные семантические связи: «Найди заметки про квантовые вычисления, но не про физику» — с таким вектора справятся через вычитание эмбеддингов, а LLM может запутаться.
- Стоимость: Если используете облачную LLM (Gemini, GPT), каждый запрос платный. Локальные модели медленнее, но бесплатны.
Прямо сейчас Memory Agent идеален для личных знаний (до 10k заметок) и корпоративных вики среднего размера. Для всего остального — смотрите в сторону гибридных систем, как в статье про графовую когнитивную память.
Ответы на частые вопросы
| Вопрос | Ответ |
|---|---|
| Какие LLM лучше всего подходят? | Для локальности — Gemma 3 8B или Qwen 2.5 14B. Для точности — Gemini Ultra 2.0 или GPT-5. Берите модель с контекстом хотя бы 128k токенов. |
| Как часто обновлять базу? | Поставьте cron-задачу на ежедневную индексацию новых файлов. Или используйте файловые вотчеры. |
| А если заметки содержат картинки? | Memory Agent работает только с текстом. Для мультимодальности потребуется отдельный пайплайн, как в гайде по Android-ассистенту. |
Что дальше? Прогноз от автора
К 2027 году векторные базы не умрут, но станут нишевым инструментом для поиска по гигабайтам неструктурированных данных. Для всего остального — SQLite + LLM-рассуждение. Google уже тестирует Memory Agent в своих ассистентах, и скоро появятся готовые библиотеки.
Мой совет: не бросайтесь переписывать все RAG-системы. Начните с малого — внедрите Memory Agent для своего Obsidian. Если понравится — переносите на корпоративные знания. И не забывайте про полный каталог инструментов для локального ИИ, там есть все для экспериментов.
А если столкнетесь с проблемой, когда свежие SQL-данные конфликтуют со старыми векторами — читайте мой разбор про конфликт источников в RAG. Удачи в коде!