Перейти к содержимому
OmniRoute source

Memory System (Русский)

Механизм памяти OmniRoute поддерживает четыре источника эмбеддингов (src/lib/memory/embedding/). Каждый из них предлагает разные компромиссы в отношении задержки, стоимости, качества модели и сложности настройки.

Поставщик Источник Задержка Стоимость Качество Настройка
transformers Локальная модель ONNX (Xenova/all-MiniLM-L6-v2) ~50-150ms (CPU) Бесплатно Хорошее Только npm install
static Предварительно вычисленные векторы (кэшированные) <1ms Бесплатно Н/Д (зависит от попадания в кэш) Не требуется
remote API OpenAI / Cohere / Voyage ~100-300ms $0.02-0.10/1M токенов Отличное Ключ API
auto Выбирает лучший доступный источник во время выполнения Как у выбранного источника Бесплатно Как у выбранного источника Не требуется
(кэш) Слой LRU в памяти поверх любого источника <1ms (попадание), полная задержка (промах) Бесплатно Как у базового источника Всегда включён (не является выбираемым источником)
Каков контекст вашего развёртывания?
│
┌───────────┼───────────┬──────────────┐
│ │ │ │
РАЗРАБОТКА/ НЕБОЛЬШОЙ КРУПНЫЙ ПРОД. ПЕРИФЕРИЯ /
ТЕСТИРОВАНИЕ ПРОД. ОФЛАЙН
│ │ │ │
▼ ▼ ▼ ▼
transformers transformers remote (Qdrant) transformers
(бесплатно, без API) (лучшее качество) (без интернета)
│ │ │ │
└────────┬──┴───────────┴──────────────┘
│
▼
ВСЕГДА добавляйте сверху слой `cache`
(LruCache оборачивает любого поставщика)

Параметры эмбеддингов памяти настраиваются через API/интерфейс настроек, а не через переменные окружения. Соответствующие ключи базы данных настроек в разделе Settings (normalizeMemorySettings в src/lib/memory/settings.ts):

  • memoryEmbeddingSource: "transformers" (локальный), "remote" (на основе API, например OpenAI), "static" (внешнее хранилище) или "auto"
  • memoryEmbeddingProviderModel: идентификатор модели для удалённых/статических источников (например, "text-embedding-3-small")
  • memoryTransformersEnabled: true | false
  • memoryStaticEnabled: true | false
  • memoryVectorStore: "sqlite-vec", "qdrant" или "auto"

Для запуска локальных моделей внутри используется transformers.js:

Окно терминала
# Переменные окружения, считываемые в коде (src/lib/memory/embedding/index.ts):
MEMORY_TRANSFORMERS_MODEL=Xenova/all-MiniLM-L6-v2 # Репозиторий модели HF
MEMORY_STATIC_MODEL=minishlab/potion-base-8M # Статическая модель potion из HF
MEMORY_STATIC_CACHE_DIR=<DATA_DIR>/embeddings # Каталог кэша

Кэш всегда включён по умолчанию и настраивается с помощью переменных окружения:

Окно терминала
MEMORY_EMBEDDING_CACHE_MAX=1000 # Максимальное количество кэшированных элементов
MEMORY_EMBEDDING_CACHE_TTL_MS=300000 # TTL (5 мин)

Бенчмарк на типичном 4-ядерном сервере x86 (тексты объёмом ~100 токенов каждый):

Провайдер p50 p95 p99 Стоимость / 1 млн эмбеддингов
transformers (CPU) 80ms 180ms 350ms Бесплатно
remote (OpenAI) 120ms 220ms 400ms ~$0.02 (ada-002) / $0.13 (3-large)
static (Qdrant) 15ms 30ms 60ms Зависит от хостинга Qdrant
cache (попадание) <1ms <1ms 2ms Бесплатно

Модуль extraction.ts (src/lib/memory/extraction.ts) использует сопоставление по регулярным выражениям для извлечения структурированных фактов из сообщений беседы. Понимание этих шаблонов поможет вам настроить качество извлечения для своего сценария использования.

Категория Пример шаблона Что извлекается
PREFERENCE_PATTERNS "Я предпочитаю <X>", "Мне нравится <X>", "Я ненавижу <X>" Предпочтения пользователя
DECISION_PATTERNS "Я буду использовать <X>", "Я решил <X>", "Я выбрал <X>" Решения пользователя (эпизодические)
PATTERN_PATTERNS "Я обычно <X>", "Я всегда <X>", "Я никогда не <X>" Устойчивые поведенческие шаблоны
// Из src/lib/memory/extraction.ts
const PREFERENCE_PATTERNS = [
/\bI\s+(?:really\s+)?prefer\s+([^.,\n]+)/gi,
/\bI\s+(?:really\s+)?like\s+([^.,\n]+)/gi,
/\bI\s+(?:hate|dislike|avoid)\s+([^.,\n]+)/gi,
];
const DECISION_PATTERNS = [
/\bI'?(?:ll|will)\s+use\s+([^.,\n]+)/gi,
/\bI\s+(?:have\s+)?decided\s+(?:to\s+)?([^.,\n]+)/gi,
];
const PATTERN_PATTERNS = [/\bI\s+usually\s+([^.,\n]+)/gi, /\bI\s+always\s+([^.,\n]+)/gi];

Когда пользователь говорит:

“Я предпочитаю TypeScript. Я буду использовать Postgres для этого проекта. Я всегда делаю коммит перед отправкой изменений. Мне не нравится Python.” В результате извлечения создаются 4 записи памяти:

Ключ Категория Тип Содержимое
preference:typescript preference factual “TypeScript”
decision:postgres_for_this_project decision episodic “Postgres для этого проекта”
pattern:commit_before_pushing pattern factual “делаю коммит перед отправкой изменений”
preference:python preference factual “Python”

Чтобы предотвратить неконтролируемое извлечение, применяются следующие ограничения:

| Минимальная длина содержимого | 3 символа | | Максимальная длина содержимого | 500 символов |

Извлечение запускается автоматически всякий раз, когда включена память; отдельного переключателя только для извлечения нет. Чтобы отключить его, полностью отключите память (enabled: false через PUT /api/settings/memory). Это стоит сделать в следующих случаях:

  • У вас большой объём сообщений, и затраты на извлечение существенны
  • Ваши беседы в основном кратковременны (общение, отладка) и не имеют долгосрочной ценности
  • Вы уже сохраняете контекст с помощью пользовательских плагинов

Алгоритм Reciprocal Rank Fusion (RRF) объединяет результаты FTS5 (по ключевым словам) и векторного (семантического) поиска. Параметр k определяет, какой вес получают результаты с более низкими позициями в рейтинге.

Для каждой записи-кандидата в памяти оценка RRF вычисляется следующим образом:

RRF(d) = Σ 1 / (k + rank_i(d))

Где:

  • k — константа (по умолчанию 60)
  • rank_i(d) — позиция документа d в i-й поисковой системе (FTS, векторный поиск)
  • Суммирование выполняется по всем поисковым системам
Значение k Эффект Лучше всего подходит для
k=0 Чистое объединение рейтингов (без сглаживания) Теоретического базового уровня
k=10-30 Результаты на верхних позициях получают большой вес, низкие позиции почти не влияют Случаев, когда первые 3 результата обычно верны
k=60 (по умолчанию) Сбалансированный вариант — все первые 10 результатов вносят значимый вклад Универсального поиска
k=100+ Более равномерное распределение — даже результаты с низкими позициями могут доминировать, если присутствуют в нескольких системах Случаев, когда полнота важнее точности
Окно терминала
# Значение по умолчанию
MEMORY_RRF_K=60
# Повышенная точность (небольшая память, мало документов)
MEMORY_RRF_K=20
# Максимальная полнота (большая память, разнообразные запросы)
MEMORY_RRF_K=120

Пример с k=20:

  • Позиция 1 в FTS → вклад 1/21 = 0.048
  • Позиция 10 в FTS → вклад 1/30 = 0.033
  • Позиция 1 в векторном поиске → вклад 0.048
  • Максимальная суммарная оценка: 0.096

Пример с k=60:

  • Позиция 1 в FTS → вклад 1/61 = 0.016
  • Позиция 10 в FTS → вклад 1/70 = 0.014
  • Позиция 1 в векторном поиске → вклад 0.016
  • Максимальная суммарная оценка: 0.033

При более высоком k относительная разница между первой и десятой позициями меньше, поэтому алгоритм больше полагается на согласованность между поисковыми системами, чем на уверенность, основанную на высокой позиции.

Симптом Что попробовать
Первый результат всегда побеждает, но он неверен Уменьшить k (например, до 20) — уверенность в верхних позициях важнее
Правильный ответ входит в топ-5, но не занимает первое место Увеличить k (например, до 100) — более равномерная оценка поощряет согласованность
Полнота высокая, но точность низкая Уменьшить k — сделать ранжирование более строгим
Полнота низкая (релевантные документы пропускаются) Увеличить k — дать шанс документам с более низкими позициями

При объединении на основе обратного ранга используются равные веса для семантического векторного ранга и ранга полнотекстового поиска:

RRF(d) = 1/(k + rank_vector) + 1/(k + rank_fts)

Переменных окружения для настройки отдельных весов не существует (MEMORY_RRF_VECTOR_WEIGHT/MEMORY_RRF_FTS_WEIGHT отсутствуют).


Модуль summarization.ts (src/lib/memory/summarization.ts) сжимает более старые воспоминания, чтобы сохранять активный набор небольшим, не ухудшая возможность поиска.

Триггер Пороговое значение (по умолчанию)
Ручной запуск через API н/д

Из summarization.ts экспортируются две точки входа:

  • summarizeMemories(apiKeyId, sessionId?, maxTokens = 4000) — сжимает воспоминания сеанса в единый текст сводки, ограниченный бюджетом токенов.
  • summarizeMemoriesOlderThan(apiKeyId, days, dryRun) — используемое API возрастное сжатие: выбирает все воспоминания старше days, создаёт из них одно сжатое воспоминание-сводку и (когда dryRun имеет значение false) удаляет оригиналы. Передайте dryRun: true, чтобы просмотреть набор кандидатов и общее количество токенов, ничего не изменяя.

Прохода кластеризации по тегам/ключам или оценки отдельных воспоминаний по принципу «основное или пригодное для суммаризации» нет — выбор выполняется исключительно по возрастному порогу, а текст сводки представляет собой сжатую строку с префиксом типа для каждого кандидата.

Суммаризация выполняется вручную / по желанию — настройка autoSummarize по умолчанию имеет значение false, поэтому автоматически ничего не сжимается. Запустите её через API:

Окно терминала
curl -X POST http://localhost:20128/api/memory/summarize \
-H "Authorization: Bearer $OMNIROUTE_KEY"

Чтобы оставить её отключённой, просто сохраните для autoSummarize значение по умолчанию (false).

  • Сначала выполните предварительный просмотр с dryRun — summarizeMemoriesOlderThan(..., true) возвращает список кандидатов и общее количество токенов, чтобы вы могли проверить, что именно будет объединено, прежде чем удалять оригиналы.
  • Запускайте суммаризацию в часы низкой нагрузки, если у вас большой корпус воспоминаний — вызов LLM является самой медленной частью
Окно терминала
# В стиле cron: суммаризация ежедневно в 3 часа ночи
0 3 * * * curl -X POST http://localhost:20128/api/memory/summarize \
-H "Authorization: Bearer $OMNIROUTE_KEY"

Источник истины: src/lib/memory/backend.ts, src/lib/memory/genericBackend.ts, src/lib/memory/manager.ts Тесты: src/lib/memory/__tests__/generic-backend.test.ts

Шаблон провайдера MemoryBackend добавляет подключаемый уровень абстракции бэкенда поверх существующего движка памяти. Вместо привязки к одной реализации хранилища система памяти теперь поддерживает несколько бэкендов (SQLite, Obsidian, Notion, пользовательские HTTP-бэкенды) с настраиваемой маршрутизацией между основным и резервными бэкендами.

┌──────────────────────────────────────────────────────────┐
│ Маршруты API │
│ (src/app/api/memory/route.ts) │
└──────────────────────┬───────────────────────────────────┘
│
┌──────────────────────▼───────────────────────────────────┐
│ MemoryManager │
│ Singleton-оркестратор (manager.ts) │
│ │
│ Основной ──► Бэкенд A (например, SQLite) │
│ Резервный ─► Бэкенд B (например, Obsidian) │
│ Бэкенд C (например, Notion через │
│ GenericBackend) │
└──────────────────────┬───────────────────────────────────┘
│
┌──────────────┼──────────────┐
▼ ▼ ▼
┌────────────┐ ┌────────────┐ ┌──────────────────┐
│ Бэкенд │ │ Бэкенд │ │ GenericMemory │
│ SQLite │ │ Obsidian │ │ Backend (HTTP) │
└────────────┘ └────────────┘ └──────────────────┘

Каждый бэкенд должен реализовывать интерфейс MemoryBackend:

interface MemoryBackend {
readonly id: string;
readonly displayName: string;
// CRUD
create(input: CreateMemoryInput): Promise<Memory>;
get(id: string): Promise<Memory | null>;
update(id: string, updates: Partial<...>): Promise&lt;boolean&gt;;
delete(id: string): Promise&lt;boolean&gt;;
list(filter: MemoryFilter): Promise<{ data: Memory[]; total: number; byType: Record<string, number> }>;
// Поиск
search(config: SearchConfig): Promise<Memory[]>;
// Состояние
health(): Promise<HealthCheckResult>;
// Жизненный цикл (необязательно)
initialize?(): Promise&lt;void&gt;;
shutdown?(): Promise&lt;void&gt;;
}

Singleton-оркестратор, который:

  • Регистрирует бэкенды через register(backend) — вызывается при запуске из index.ts
  • Настраивает основной и резервные бэкенды через configure(primary, fallbacks)
  • Маршрутизирует операции CRUD и поиск в основной бэкенд, используя цепочку резервных бэкендов при сбое
  • Периодически проверяет состояние всех бэкендов

Поведение резервных бэкендов:

Операция Основной бэкенд Резервные бэкенды
create ✅ Только основной ❌
get ✅ Сначала основной ✅ Резервный, если результат — null
update ✅ Только основной ✅ Асинхронная синхронизация без ожидания
delete ✅ Только основной ✅ Асинхронная синхронизация без ожидания
list ✅ Только основной ❌
search ✅ Сначала основной ✅ Резервный при ошибке

Универсальный HTTP-коннектор, адаптирующий любой REST API к MemoryBackend. Полезен для:

  • Notion — подключение через Notion API
  • Obsidian — подключение через Obsidian Local REST API
  • Пользовательских бэкендов — любой сервис, предоставляющий RESTful API памяти

Конфигурация:

interface GenericBackendConfig {
baseUrl: string; // Базовый URL API бэкенда
apiKey?: string; // Bearer-токен для аутентификации
headers?: Record<string, string>; // Пользовательские HTTP-заголовки
timeout?: number; // Тайм-аут запроса (по умолчанию: 30000ms)
backendType?: string; // Для ведения журнала
// Переопределения конечных точек (по умолчанию используются соглашения REST)
endpoints?: {
search?: string; // по умолчанию: "/memories/search"
create?: string; // по умолчанию: "/memories"
list?: string; // по умолчанию: "/memories"
get?: string; // по умолчанию: "/memories/{id}"
update?: string; // по умолчанию: "/memories/{id}"
delete?: string; // по умолчанию: "/memories/{id}"
health?: string; // по умолчанию: "/health"
};
// Сопоставления имён параметров запроса
queryParams?: {
query?/apiKeyId?/limit?/offset?/strategy?/maxTokens?/type?/sessionId?/orderBy?/orderDir?/options?
};
// Сопоставления имён параметров пути
pathParams?: {
id?/memoryId?
};
}

Известные бэкенды предварительно настроены в KNOWN_BACKENDS:

createKnownBackend("obsidian"); // → GenericMemoryBackend, указывающий на localhost:27123
createKnownBackend("notion"); // → GenericMemoryBackend, указывающий на api.notion.com/v1

Основной бэкенд по умолчанию. Представляет собой обёртку существующего хранилища памяти на базе SQLite, использующего src/lib/memory/store.ts. Автоматически регистрируется при запуске.

import { sqliteBackend } from "./sqliteBackend";
memoryManager.register(sqliteBackend);

Представляет собой обёртку существующей интеграции с Obsidian (src/lib/memory/obsidianBackend.ts). Подключается к хранилищу Obsidian через Obsidian Local REST API.

Настройки бэкенда памяти хранятся в таблице настроек приложения и управляются через src/lib/memory/settings.ts:

Настройка Ключ окружения/конфигурации Значение по умолчанию Описание
Основной бэкенд memoryPrimaryBackend "sqlite" ID основного бэкенда
Резервные бэкенды memoryFallbackBackends [] Упорядоченный список ID резервных бэкендов
Конфигурации бэкендов memoryBackendConfigs {} Переопределения конфигурации для каждого бэкенда

Настройки нормализуются с помощью normalizeMemorySettings() и кэшируются в getMemorySettings().

Запуск приложения
→ импорты index.ts (побочный эффект): регистрируют SQLiteBackend
→ initMemoryBackends() вызывается из жизненного цикла приложения:
1. Загрузка настроек (getMemorySettings)
2. Настройка основного и резервных бэкендов
3. Инициализация всех бэкендов (проверка работоспособности)
4. Готовность к обработке запросов
  1. Реализуйте интерфейс MemoryBackend в src/lib/memory/&lt;name&gt;Backend.ts
  2. Экспортируйте его из src/lib/memory/index.ts
  3. Зарегистрируйте с помощью memoryManager.register(yourBackend) при запуске
  4. Настройте через параметры: задайте для memoryPrimaryBackend ID вашего бэкенда
  5. Протестируйте, используя src/lib/memory/__tests__/generic-backend.test.ts в качестве примера
import { createGenericMemoryBackend } from "./genericBackend";
const brainBackend = createGenericMemoryBackend("brain", "BK-Brain", {
baseUrl: process.env.BRAIN_API_URL || "http://localhost:9099",
apiKey: process.env.BRAIN_API_KEY,
endpoints: {
search: "/api/memory/search",
create: "/api/memory",
health: "/api/health",
},
});
memoryManager.register(brainBackend);
Окно терминала
npx vitest run src/lib/memory/__tests__/generic-backend.test.ts --reporter=verbose

Ожидаемый результат: 35 успешно пройденных тестов, охватывающих:

  • Конструктор (2)
  • Проверка работоспособности (4) — успех, ошибка 500, сетевая ошибка, задержка
  • Инициализация (2) — успех, ошибка
  • Создание (2) — конечная точка по умолчанию, пользовательская конечная точка
  • Получение (4) — успех, 404 → null, исключение при коде, отличном от 404, пользовательские параметры пути
  • Обновление (2) — успех, 404 → false
  • Удаление (2) — успех, 404 → false
  • Получение списка (2) — параметры запроса, пользовательские имена параметров
  • Поиск (3) — параметры запроса, пользовательская конечная точка, сериализация параметров
  • Заголовки аутентификации (2) — Bearer-токен, пользовательские заголовки
  • Фабрика (1)
Окно терминала
npm run typecheck:core

Ожидаемый результат: 0 ошибок.


Исходный код OmniRoute (a58000c7685f)

HagiCode

HagiCode — агентная среда разработки со структурированными процессами, параллельным выполнением несколькими агентами и интерфейсами Hero Dungeon.

Превращайте идеи в полезное ПО с более умным, быстрым и увлекательным агентным рабочим процессом.

Главный экран HagiCode в светлой теме
  • SmartСтруктурированные процессы превращают намерение в исполнимый путь от идеи до готового изменения.
  • EfficientМультиагентные процессы параллельно продвигают исследование, реализацию и проверку.
  • FunHero Dungeon делает длительную совместную разработку наглядной и увлекательной.
Перейти на HagiCode