Архитектура RAG — self-hosted семантический поиск
Статус: работает с Phase 71 (2026-06-05). Заменяет мост Google NotebookLM, выпущенный в Phase 36.
TL;DR: вики + задачи + скиллы каждого проекта Arc эмбеддятся в пер-проектный векторный индекс, живущий внутри существующего SQLite SSOT. Запросы возвращают семантически релевантные пассажи по контенту проекта + общему глобальному пулу скиллов. Никакого внешнего retrieval-сервиса, никакой пер-пользовательской Google-сессии, никаких лимитов на источники.
1. Почему мы ушли с NotebookLM
Мост Phase 36 оборачивал Google NotebookLM как векторное хранилище через reverse-engineered cookie-аутентификацию. Это породило две проблемы, которые сам продукт исправить не может:
- Лимит источников. NotebookLM Plus ограничен 100 источниками на ноутбук. К середине 2026 несколько проектов Arc давно превысили лимит; логика вытеснения превратилась в перманентный костыль (
#324). - Привязка к личному аккаунту. Каждый новый проект создавал ноутбук внутри личного NotebookLM CEO. Масштабирование до N клиентов означало N ноутбуков в чьём-то одном сайдбаре — неуправляемый побочный эффект, который мост не мог развязать.
Phase 71 заменяет слой ретривала self-hosted пайплайном, который никогда не касается Google-аккаунта пользователя.
2. Карта компонентов
Write path Read path
────────── ─────────
wiki.ts handleWikiSave ────┐ chat.ts executeAskNotebooklm ──┐
cli-routes.ts handleCreate/ │
UpdateIssue ─────────────┤ shared/routes/rag-search.ts ──────┤
skills.ts handleSave/ │ GET /api/crm/projects/:name/ │
Delete + global CRUD ────┤ rag/search │
│ │
▼ ▼
shared/rag-hooks.ts shared/rag.ts search()
syncIssue / syncWiki / │ embeds query →
syncSkill (fire-and-forget) │ KNN over vec0 →
│ │ project + doc_type filter
▼ │ → top-K passages
shared/rag.ts upsert() │
paragraph-aware chunker (~1800 chars) │
→ atomic transaction: │
embeddings ⨯ embeddings_vec │
│ │
▼ │
shared/embeddings.ts ◄──────────────────────────── │
Cohere embed-multilingual-v3.0 │
(1024-dim float32, batch ≤96 inputs/call) │
│ │
▼ │
SQLite (data/citadel.db) ◄──────────────────────── ┘
┌──────────────────────┐ ┌─────────────────────┐
│ embeddings │ │ embeddings_vec │
│ (project, doc_type, │ │ (vec0 virtual, │
│ doc_id, chunk_ix, │ │ embedding FLOAT │
│ text, created_at) │ ──┤ [1024]) │
│ id ←→ rowid │ │ │
└──────────────────────┘ └─────────────────────┘
2.1 Модули в одной таблице
| Модуль | Роль |
|---|---|
shared/embeddings.ts |
Клиент Cohere — embedBatch(texts, inputType) для индексации + embedQuery(text) для запросов. Ретраи с 3× экспоненциальным бэкоффом на транзиентных ошибках, fail-fast на 401/403. |
shared/rag.ts |
Слой хранения — upsert / search / removeDoc / removeProject / stats. Владеет чанкером и атомарной транзакцией embeddings↔embeddings_vec. |
shared/rag-hooks.ts |
Fire-and-forget обёртки (syncIssue, syncWiki, syncSkill + зеркальные remove*). Ошибки печатаются, но никогда не откатывают диск/SQL-запись, которая их вызвала. |
shared/routes/rag-search.ts |
Публичная HTTP-точка входа. GET /api/crm/projects/:name/rag/search?q=... |
scripts/phase-71-backfill-rag.ts |
Одноразовый идемпотентный сидер существующего контента. Флаги --dry-run / --force / --project / --throttle. |
3. Выбор эмбеддингов — Cohere embed-multilingual-v3.0
| Свойство | Значение |
|---|---|
| Размерность | 1024-dim float32 |
| Мультиязычность | Нативная — украинский + английский + 100+ языков делят одно подпространство |
| Стоимость | ~$0.10 / 1M токенов (Production tier). По модели затрат — примерно $4/месяц при масштабе 50 пользователей. |
| Разделение подпространств | input_type="search_document" для индексации vs input_type="search_query" для рантайм-запросов. Смешивание этих двух режимов обрушивает recall. |
Живой кросс-языковой смоук по prod-корпусу:
| Запрос | Язык | Топ-хит | Дистанция |
|---|---|---|---|
| Safari cookie auth bug | EN | issue/174 (auth/password reset) |
1.00 |
| множинні тенанти | UK | issue/49 (Phase 53.11.2 Multi-Worker TG Topics Mode) |
1.07 |
UK-запрос обошёл соответствующую EN-EN дистанцию — выравнивание мультиязычного подпространства реально, а не маркетинговое заявление.
4. Хранение — sqlite-vec внутри SSOT
Миграция 049_embeddings создаёт две таблицы:
CREATE TABLE embeddings (
id INTEGER PRIMARY KEY AUTOINCREMENT,
project TEXT NOT NULL,
doc_type TEXT NOT NULL, -- 'wiki' | 'issue' | 'skill' | 'transcript'
doc_id TEXT NOT NULL, -- filename, issue id, skill name
chunk_ix INTEGER NOT NULL DEFAULT 0,
text TEXT NOT NULL,
created_at TEXT NOT NULL DEFAULT (datetime('now'))
);
CREATE VIRTUAL TABLE embeddings_vec USING vec0(
embedding FLOAT[1024]
);
-- embeddings.id ←→ embeddings_vec.rowid
Разделение держит метаданные в обычном SQLite (джойнятся с таблицами wiki/issues/skills, простые LIST/DELETE), а виртуальная таблица vec0 хранит плотные векторы для ANN-поиска.
Размеры. 1024-dim float32 = 4096 байт на строку. 100K строк ≈ 400МБ. Нормально для текущего масштаба; пересмотреть при 1M+ строк.
Загрузка. sqlite-vec загружается как runtime-расширение в shared/db.ts initDb() до выполнения миграций. Миграция 049 проверяет vec_version() и чисто прерывается, если расширение не загрузилось.
5. Путь записи — хуки переэмбеддинга (Phase 71.5)
Каждая поверхность записи, мутирующая индексируемый контент, вызывает fire-and-forget хук, чтобы эмбеддинг оставался свежим без отдельной задачи синхронизации.
| Триггер | Хук | Примечания |
|---|---|---|
PUT /api/crm/projects/:name/wiki/save |
syncWiki(project, path, content) |
После успешного Bun.write |
POST /api/mcp/issues/:project (create) |
syncIssue(project, id, title, body) |
После записи в SQLite |
PUT /api/mcp/issues/:project/:id (update) |
syncIssue(project, id, title, body) |
Аналогично |
POST /api/mcp/issues/:project/:id/log (activity) |
нет | Журнал активности не меняет индексируемые title/body — переэмбеддинг жёг бы квоту впустую |
PUT /api/crm/projects/:name/skills/save |
syncSkill(project, name, content) |
Пер-проектный скилл |
DELETE /api/crm/projects/:name/skills/delete |
removeSkill(project, name) |
Зеркало |
POST /api/crm/skills (глобальное создание) |
syncSkill("_global_", name, content) |
Глобальный пул |
PUT /api/crm/skills/:id (глобальное обновление) |
syncSkill("_global_", name, content) |
Только если поле content реально изменилось |
DELETE /api/crm/skills/:id (глобальное удаление) |
removeSkill("_global_", name) |
Зеркало |
DELETE /api/auth/account (GDPR ст. 17) |
removeProject(name) для каждого собственного проекта |
Эмбеддинги не переживают удаление аккаунта |
Пайплайн транскриптов (Phase 73.6): status → summarized → embedding → done |
upsert(project, "transcript", id, ragText) через transcript-worker.ts |
ragText = текст транскрипта + [Xs] frame descriptions + tldr/key_points/decisions/action_items из саммари. Пропускается при embed_to_rag=0. Не фатально: сбой RAG всё равно финализирует задачу в done. |
Правило: ошибки Cohere печатают одну строку и умирают. Они никогда не откатывают диск/SQL-запись, которая их вызвала. Устойчивые сбои сходятся обратно через ночной бэкфилл, а не через ретраи в горячем пути.
6. Путь чтения — search() + фасады по поверхностям
import { search } from "shared/rag";
const hits = await search("arc-v2", "How does the multi-tenancy gate work?", {
k: 6,
doc_types: ["wiki", "issue", "skill", "transcript"], // optional narrow
});
// hits: Array<{ doc_type, doc_id, chunk_ix, text, distance }> sorted by distance
Под капотом:
- Эмбеддинг запроса с
input_type="search_query"(другое подпространство, чемsearch_document). - KNN по
embeddings_vecс перевыборкойk * 4. - Фильтр по проекту + опциональному
doc_typeна SQL JOIN. - Возврат top-K пассажей, отсортированных по возрастанию L2-дистанции.
Публичные фасады
GET /api/crm/projects/:name/rag/search?q=...&k=...&include_global=true&doc_types=...— HTTP-точка входа. Питаетarc kb search+ инструментask_notebooklmв чате.chat.ts executeAskNotebooklm— выполняет поиск по проекту + глобально параллельно, мержит по дистанции, фолбэк на keyword-поиск при топ-хитеd > 1.6или нуле результатов.skills.ts handleGenerateSkill— использует RAG как ретривал house-style, затем вызывает Claude Sonnet для генерации.help.ts ragSemantic— чат Arc Help подаёт проектные + глобальные хиты как контекст модели.
7. Бэкфилл — scripts/phase-71-backfill-rag.ts
bun scripts/phase-71-backfill-rag.ts # all projects + global
bun scripts/phase-71-backfill-rag.ts --dry-run # inventory only
bun scripts/phase-71-backfill-rag.ts --project arc-v2 # one project + global
bun scripts/phase-71-backfill-rag.ts --skip-global # projects only
bun scripts/phase-71-backfill-rag.ts --doc-types wiki,issue
bun scripts/phase-71-backfill-rag.ts --force # re-embed even if row exists
bun scripts/phase-71-backfill-rag.ts --limit 100 # cap docs per project
bun scripts/phase-71-backfill-rag.ts --throttle 700 # ms between Cohere calls (default 700; set 0 on Production keys)
Идемпотентность. Каждый кандидат проверяется на наличие (project, doc_type, doc_id) в embeddings и пропускается, если найден, кроме случая с --force. Повторные запуски не стоят ничего по квоте Cohere.
Production-прогон (2026-06-05). 814 кандидатов в 20 проектах + _global_ (37 wiki + 482 issue + 295 skill). Первый проход сжёг месячный лимит Trial в 1000 вызовов на глобальных скиллах. После апгрейда до Production-ключа оставшиеся 178 документов завершились за 57 с без ошибок. Всего записано чанков: ~3 150.
8. Границы безопасности
- API-ключ Cohere живёт в vault (
COHERE_API_KEY), ротируется через Platform Settings → RAG / Semantic search. Живая кнопкаTestвыполняет 1-токенный embed-вызов для проверки доступности. - Никакой утечки промптов в Cohere. Эмбеддинги — производные bag-of-words; восстановить из них исходный текст непрактично. Политика самой Cohere заявляет, что они не обучаются на API-трафике.
- Multi-tenancy. Имена проектов — ключи неймспейсов;
canAccessProjectохраняет/api/crm/projects/:name/*до запуска обработчика поиска. Глобальные скиллы живут в сентинельном неймспейсе_global_, с которым не может коллидировать ни одно имя пользовательского проекта (валидатор запрещает имена, начинающиеся с_). - GDPR ст. 17.
DELETE /api/auth/accountкаскадно вызываетrag.removeProjectдля каждого собственного проекта, чтобы эмбеддинги не пережили удаление.
9. Миграция с NotebookLM (одноразовая, 2026-06-05)
| Шаг | Сделано |
|---|---|
| API-ключ Cohere + вкладка Platform Settings + живая проверка | #358 |
Расширение sqlite-vec + миграция 049 |
#359 |
Клиент shared/embeddings.ts |
#360 |
Фасад shared/rag.ts |
#361 |
| Хуки переэмбеддинга на записях wiki / issue / skill | #362 |
| Скрипт бэкфилла + prod-прогон | #363 |
Замена точек вызова (chat.ts, skills.ts, новый /rag/search, arc kb search) |
#364 |
Вывод из эксплуатации services/notebooklm-bridge/ + удаление projects.notebook_id (миграция 050) |
#365 |
| Документация (эта страница + заглушки для 7 локалей) | #366 |
| Soak-валидация | #367 |
10. Эксплуатационные заметки
- Имя инструмента осталось
ask_notebooklm. Определение инструмента Anthropic уходит вниз по потоку к Claude в диалоге Cloud PM. Переименование сломало бы in-flight циклыtool_use. Реализация теперь — чистый RAG; публичный контракт остался стабильным. - Audio overview исчез. Автогенерируемые аудио-саммари NotebookLM не имеют эквивалента в self-hosted пайплайне.
POST /api/crm/projects/:name/memory/fetch-artifactтеперь возвращает410 Gone. Если это понадобится снова — это отдельная фаза (вероятно, Whisper TTS-проход по вики проекта). - Кнопка переиндексации всё ещё работает.
POST /api/crm/projects/:name/memory/refreshтеперь переэмбеддитMANIFEST.md+ROADMAP.md+ ключевые файлы в локальное хранилищеembeddings, а не загружает в NotebookLM. Для пользователя: та же кнопка, тот же результат («знания проекта обновлены»), другое хранилище. - Никаких URL ноутбуков.
GET /api/crm/projects/:name/notebooksвозвращает{ notebooks: [], retired: "phase-71.8" }, чтобы старые сборки фронтенда не падали с 404. Вкладка Neural Memory в CRM будет убрана в последующем косметическом проходе.
11. Ссылки
shared/embeddings.ts,shared/rag.ts,shared/rag-hooks.ts,shared/routes/rag-search.tsshared/migrations/049_embeddings.ts,shared/migrations/050_drop_notebook_id.tsscripts/phase-71-backfill-rag.ts- Задачи #321 (родительская) · #358–#367 (подфазы) · #322 / #324 (закрыты вместе)
- Журнал решений:
docs/architecture/PHASE_71_RAG_MIGRATION.md