Arquitetura RAG — busca semântica auto-hospedada

Status: No ar desde a Phase 71 (2026-06-05). Substitui o bridge do Google NotebookLM lançado na Phase 36.

TL;DR: a wiki + issues + skills de cada projeto Arc são embedadas em um índice vetorial por projeto que vive dentro do SQLite SSOT existente. As consultas retornam trechos semanticamente relevantes do conteúdo do projeto + um pool global compartilhado de skills. Sem serviço externo de retrieval, sem sessão Google por usuário, sem limites de fontes.


1. Por que saímos do NotebookLM

O bridge da Phase 36 encapsulava o Google NotebookLM como vector store usando autenticação por cookie obtida via engenharia reversa. Ele produziu dois problemas que o produto subjacente não consegue resolver:

  1. Limite de fontes. O NotebookLM Plus tem teto de 100 fontes por notebook. Em meados de 2026, vários projetos Arc já tinham estourado o limite; a lógica de remoção virou um band-aid permanente (#324).
  2. Acoplamento à conta pessoal. Cada projeto novo criava um notebook dentro do NotebookLM pessoal do CEO. Escalar para N clientes significava N notebooks na barra lateral de uma pessoa — um efeito colateral não gerenciado que o bridge não conseguia desacoplar.

A Phase 71 troca a camada de retrieval por um pipeline auto-hospedado que a conta Google do usuário nunca toca.


2. Mapa de componentes

                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 Módulos em resumo

Módulo Papel
shared/embeddings.ts Cliente Cohere — embedBatch(texts, inputType) para indexação + embedQuery(text) para consultas. Retry com backoff exponencial 3× em erros transitórios, fail-fast em 401/403.
shared/rag.ts Camada de armazenamento — upsert / search / removeDoc / removeProject / stats. É dona do chunker e da transação atômica embeddingsembeddings_vec.
shared/rag-hooks.ts Wrappers fire-and-forget (syncIssue, syncWiki, syncSkill + espelhos remove*). Erros são impressos, mas nunca revertem a escrita em disco/SQL que os disparou.
shared/routes/rag-search.ts Ponto de entrada HTTP público. GET /api/crm/projects/:name/rag/search?q=...
scripts/phase-71-backfill-rag.ts Seeder idempotente de execução única para conteúdo existente. Flags --dry-run / --force / --project / --throttle.

3. Escolha de embedding — Cohere embed-multilingual-v3.0

Propriedade Valor
Dimensionalidade float32 de 1024 dimensões
Multilíngue Nativo — ucraniano + inglês + 100+ idiomas compartilham um sub-espaço
Custo ~US$ 0,10 / 1M tokens (tier Production). Estimado ~US$ 4/mês na escala de 50 usuários conforme o modelo de custos.
Divisão de sub-espaço input_type="search_document" para indexação vs input_type="search_query" para consultas em runtime. Misturar os dois derruba o recall.

Smoke test cross-lingual ao vivo contra o corpus de produção:

Consulta Idioma Top hit Distância
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

A consulta em UK superou a distância EN-EN correspondente — o alinhamento do sub-espaço multilíngue é real, não um argumento de marketing.


4. Armazenamento — sqlite-vec dentro do SSOT

A migração 049_embeddings cria duas tabelas:

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

A divisão mantém os metadados no SQLite comum (com join possível nas tabelas wiki/issues/skills, LIST/DELETE fáceis), enquanto a tabela virtual vec0 guarda os vetores densos para busca ANN.

Dimensionamento. float32 de 1024 dim = 4096 bytes / linha. 100K linhas ≈ 400MB. Tranquilo na escala atual; reavaliar a partir de 1M+ linhas.

Carregamento. O sqlite-vec é carregado como extensão em runtime em shared/db.ts initDb() antes das migrações rodarem. A migração 049 valida vec_version() e aborta de forma limpa se a extensão não carregar.


5. Caminho de escrita — hooks de re-embed (Phase 71.5)

Toda superfície de escrita que altera conteúdo indexável dispara um hook fire-and-forget para que o embedding permaneça atualizado sem um job de sincronização separado.

Gatilho Hook Notas
PUT /api/crm/projects/:name/wiki/save syncWiki(project, path, content) Após o Bun.write ter sucesso
POST /api/mcp/issues/:project (criação) syncIssue(project, id, title, body) Após a escrita no SQLite
PUT /api/mcp/issues/:project/:id (atualização) syncIssue(project, id, title, body) Idem
POST /api/mcp/issues/:project/:id/log (atividade) nenhum O log de atividade não altera título/corpo indexados — re-embedar queimaria quota sem efeito
PUT /api/crm/projects/:name/skills/save syncSkill(project, name, content) Skill por projeto
DELETE /api/crm/projects/:name/skills/delete removeSkill(project, name) Espelho
POST /api/crm/skills (criação global) syncSkill("_global_", name, content) Pool global
PUT /api/crm/skills/:id (atualização global) syncSkill("_global_", name, content) Apenas se o campo content realmente mudou
DELETE /api/crm/skills/:id (exclusão global) removeSkill("_global_", name) Espelho
DELETE /api/auth/account (GDPR Art. 17) removeProject(name) por projeto do usuário Embeddings não sobrevivem à exclusão da conta
Pipeline de transcrição (Phase 73.6): status → summarized → embedding → done upsert(project, "transcript", id, ragText) via transcript-worker.ts ragText = texto da transcrição + descrições de frames [Xs] + tldr/key_points/decisions/action_items do resumo. Pulado quando embed_to_rag=0. Não fatal: falha de RAG ainda finaliza o job em done.

Regra: erros da Cohere imprimem uma linha e morrem. Eles nunca revertem a escrita em disco/SQL que os disparou. Falhas persistentes convergem de volta via o backfill noturno, não via retries no hot path.


6. Caminho de leitura — search() + fachadas por superfície

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

Por baixo dos panos:

  1. Embeda a consulta com input_type="search_query" (sub-espaço diferente de search_document).
  2. KNN sobre embeddings_vec com overfetch de k * 4.
  3. Filtra por projeto + doc_type opcional no JOIN do SQL.
  4. Retorna os top-K trechos ordenados por distância L2 crescente.

Fachadas públicas


7. Backfill — 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)

Idempotência. Cada candidato é checado por presença de (project, doc_type, doc_id) em embeddings e pulado se encontrado, a menos que --force esteja ativo. Re-execuções têm custo zero na quota da Cohere.

Execução em produção (2026-06-05). 814 candidatos em 20 projetos + _global_ (37 wiki + 482 issue + 295 skill). A primeira passada queimou o limite mensal de 1000 chamadas do Trial nas skills globais. Após o upgrade para uma chave Production, os 178 docs restantes terminaram em 57s com zero erros. Total de chunks escritos: ~3.150.


8. Limites de segurança


9. Migração do NotebookLM (única, 2026-06-05)

Etapa Feito
Chave de API da Cohere + aba em Platform Settings + probe ao vivo #358
Extensão sqlite-vec + migração 049 #359
Cliente shared/embeddings.ts #360
Fachada shared/rag.ts #361
Hooks de re-embed nas escritas de wiki / issue / skill #362
Script de backfill + execução em produção #363
Troca dos call sites (chat.ts, skills.ts, novo /rag/search, arc kb search) #364
Descomissionar services/notebooklm-bridge/ + remover projects.notebook_id (migração 050) #365
Docs (esta página + 7 stubs de localidade) #366
Validação em soak #367

10. Notas operacionais


11. Referências