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:
- 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). - 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 embeddings↔embeddings_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:
- Embeda a consulta com
input_type="search_query"(sub-espaço diferente desearch_document). - KNN sobre
embeddings_veccom overfetch dek * 4. - Filtra por projeto +
doc_typeopcional no JOIN do SQL. - Retorna os top-K trechos ordenados por distância L2 crescente.
Fachadas públicas
GET /api/crm/projects/:name/rag/search?q=...&k=...&include_global=true&doc_types=...— ponto de entrada HTTP. Alimenta oarc kb search+ a ferramentaask_notebooklmdo chat.chat.ts executeAskNotebooklm— roda a busca do projeto + global em paralelo, mescla por distância, recorre à busca por palavras-chave quando o top hit temd > 1.6ou zero resultados.skills.ts handleGenerateSkill— usa o RAG como retrieval de estilo da casa, depois chama o Claude Sonnet para geração.help.ts ragSemantic— o chat do Arc Help apresenta hits do projeto + globais como contexto do modelo.
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
- A chave de API da Cohere vive no vault (
COHERE_API_KEY), rotaciona via Platform Settings → RAG / Semantic search. O botãoTestao vivo executa uma chamada de embed de 1 token para confirmar a conectividade. - Sem vazamento de prompts para a Cohere. Embeddings são derivados bag-of-words; reverter de volta para o texto original é impraticável. A própria política da Cohere diz que eles não treinam com tráfego de API.
- Multi-tenancy. Nomes de projeto são chaves de namespace;
canAccessProjectprotege/api/crm/projects/:name/*antes do handler de busca rodar. As skills globais vivem em um namespace sentinela_global_com o qual nenhum nome de projeto de usuário pode colidir (o validador proíbe nomes começando com_). - GDPR Art. 17.
DELETE /api/auth/accountdispara em cascatarag.removeProjectpara cada projeto do usuário, de modo que embeddings não sobrevivam à exclusão.
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
- Nome da ferramenta mantido como
ask_notebooklm. A definição da ferramenta Anthropic é enviada downstream ao Claude na conversa do Cloud PM. Renomear quebraria loops detool_useem andamento. A implementação agora é puro RAG; o contrato público permaneceu estável. - O audio overview se foi. Os resumos de áudio auto-gerados do NotebookLM não tinham equivalente no pipeline auto-hospedado.
POST /api/crm/projects/:name/memory/fetch-artifactagora retorna410 Gone. Se você precisar disso de volta, é uma fase separada (provavelmente um passe de TTS Whisper sobre a wiki do projeto). - O botão de re-indexação ainda funciona.
POST /api/crm/projects/:name/memory/refreshagora re-embedaMANIFEST.md+ROADMAP.md+ arquivos-chave no armazenamento local deembeddingsem vez de fazer upload para o NotebookLM. Comportamento para o usuário: mesmo botão, mesmo resultado ("o conhecimento do projeto está atualizado"), armazenamento diferente. - Sem URLs de notebooks.
GET /api/crm/projects/:name/notebooksretorna{ notebooks: [], retired: "phase-71.8" }para impedir que builds antigos do frontend recebam 404. A aba Neural Memory no CRM será aposentada em um passe cosmético futuro.
11. Referências
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- Issues #321 (pai) · #358–#367 (sub-fases) · #322 / #324 (fechadas junto)
- Log de decisões:
docs/architecture/PHASE_71_RAG_MIGRATION.md