stratumiops/docs/es/architecture/adrs/001-stratum-embeddings.md

281 lines
12 KiB
Markdown
Raw Permalink Normal View History

# ADR-001: Stratum-Embeddings - Biblioteca Unificada de Embeddings
## Estado
**Propuesto**
## Contexto
### Estado Actual: Implementaciones Fragmentadas
El ecosistema tiene 3 implementaciones independientes de embeddings:
| Proyecto | Ubicación | Providers | Caching |
| ------------ | ------------------------------------- | ----------------------------- | ------- |
| Kogral | `kogral-core/src/embeddings/` | fastembed, rig-core (parcial) | No |
| Provisioning | `provisioning-rag/src/embeddings.rs` | OpenAI directo | No |
| Vapora | `vapora-llm-router/src/embeddings.rs` | OpenAI, HuggingFace, Ollama | No |
### Problemas Identificados
#### 1. Código Duplicado
Cada proyecto reimplementa:
- HTTP client para OpenAI embeddings
- Parsing de respuestas JSON
- Manejo de errores
- Token estimation
**Impacto**: ~400 líneas duplicadas, inconsistencias en manejo de errores.
#### 2. Sin Caching
Embeddings se regeneran cada vez:
```text
"What is Rust?" → OpenAI → 1536 dims → $0.00002
"What is Rust?" → OpenAI → 1536 dims → $0.00002 (mismo resultado)
"What is Rust?" → OpenAI → 1536 dims → $0.00002 (mismo resultado)
```
**Impacto**: Costos innecesarios, latencia adicional, rate limits más frecuentes.
#### 3. No Hay Fallback
Si OpenAI falla, todo falla. No hay fallback a alternativas locales (fastembed, Ollama).
**Impacto**: Disponibilidad reducida, dependencia total de un provider.
#### 4. Dimension Mismatch Silencioso
Diferentes providers producen diferentes dimensiones:
| Provider | Modelo | Dimensiones |
| --------- | ---------------------- | ----------- |
| fastembed | bge-small-en | 384 |
| fastembed | bge-large-en | 1024 |
| OpenAI | text-embedding-3-small | 1536 |
| OpenAI | text-embedding-3-large | 3072 |
| Ollama | nomic-embed-text | 768 |
**Impacto**: Índices vectoriales corruptos si se cambia de provider.
#### 5. Sin Métricas
No hay visibilidad de uso, hit rate de cache, latencia por provider, ni costos acumulados.
## Decisión
Crear `stratum-embeddings` como crate unificado que:
1. **Unifique** las implementaciones de Kogral, Provisioning, y Vapora
2. **Añada caching** para evitar re-computar embeddings idénticos
3. **Implemente fallback** entre providers (cloud → local)
4. **Documente claramente** las dimensiones y limitaciones por provider
5. **Exponga métricas** para observabilidad
6. **Provea VectorStore trait** con backends LanceDB y SurrealDB según necesidad del proyecto
### Decisión de Backend de Storage
Cada proyecto elige su backend de vector storage según su prioridad:
| Proyecto | Backend | Prioridad | Justificación |
| ------------ | --------- | ----------------- | -------------------------------------------------------- |
| Kogral | SurrealDB | Riqueza del grafo | Knowledge Graph necesita queries unificados graph+vector |
| Provisioning | LanceDB | Escala vectorial | RAG con millones de chunks documentales |
| Vapora | LanceDB | Escala vectorial | Traces de ejecución, pattern matching a escala |
#### Por qué SurrealDB para Kogral
Kogral es un Knowledge Graph donde las relaciones son el valor principal.
Con arquitectura híbrida (LanceDB vectores + SurrealDB graph), un query típico requeriría:
1. LanceDB: búsqueda vectorial → candidate_ids
2. SurrealDB: filtro de grafo sobre candidates → results
3. App layer: merge, re-rank, deduplicación
**Trade-off aceptado**: SurrealDB tiene peor rendimiento vectorial puro que LanceDB,
pero la escala de Kogral está limitada por curación humana del conocimiento
(10K-100K conceptos típicamente).
#### Por qué LanceDB para Provisioning y Vapora
| Aspecto | SurrealDB | LanceDB |
| --------------- | ---------- | -------------------- |
| Storage format | Row-based | Columnar (Lance) |
| Vector index | HNSW (RAM) | IVF-PQ (disk-native) |
| Escala práctica | Millones | Billones |
| Compresión | ~1x | ~32x (PQ) |
| Zero-copy read | No | Sí |
### Arquitectura
```text
┌─────────────────────────────────────────────────────────────────┐
│ stratum-embeddings │
├─────────────────────────────────────────────────────────────────┤
│ EmbeddingProvider trait │
│ ├─ embed(text) → Vec<f32>
│ ├─ embed_batch(texts) → Vec<Vec<f32>> │
│ ├─ dimensions() → usize │
│ └─ is_local() → bool │
│ │
│ ┌───────────┐ ┌───────────┐ ┌───────────┐ │
│ │ FastEmbed │ │ OpenAI │ │ Ollama │ │
│ │ (local) │ │ (cloud) │ │ (local) │ │
│ └───────────┘ └───────────┘ └───────────┘ │
│ └────────────┬────────────┘ │
│ ▼ │
│ EmbeddingCache (memory/disk) │
│ │ │
│ ▼ │
│ EmbeddingService │
│ │ │
│ ▼ │
│ ┌──────────────────────────────────────────────────────────┐ │
│ │ VectorStore trait │ │
│ │ ├─ upsert(id, embedding, metadata) │ │
│ │ ├─ search(embedding, limit, filter) → Vec<Match> │ │
│ │ └─ delete(id) │ │
│ └──────────────────────────────────────────────────────────┘ │
│ │ │ │
│ ▼ ▼ │
│ ┌─────────────────┐ ┌─────────────────┐ │
│ │ SurrealDbStore │ │ LanceDbStore │ │
│ │ (Kogral) │ │ (Prov/Vapora) │ │
│ └─────────────────┘ └─────────────────┘ │
└─────────────────────────────────────────────────────────────────┘
```
## Justificación
### Por Qué Caching es Crítico
Para un sistema RAG típico (10,000 chunks):
- **Sin cache**: Re-indexaciones y queries repetidas multiplican costos
- **Con cache**: Primera indexación paga, resto son cache hits
**Ahorro estimado**: 60-80% en costos de embeddings.
### Por Qué Fallback es Importante
| Escenario | Sin Fallback | Con Fallback |
| ----------------- | ------------ | ------------------- |
| OpenAI rate limit | ERROR | → fastembed (local) |
| OpenAI downtime | ERROR | → Ollama (local) |
| Sin internet | ERROR | → fastembed (local) |
### Por Qué Providers Locales Primero
Para desarrollo: fastembed carga modelo local (~100MB), no requiere API keys, sin costos, funciona offline.
Para producción: OpenAI para calidad, fastembed como fallback.
## Consecuencias
### Positivas
1. Single source of truth para todo el ecosistema
2. 60-80% menos llamadas a APIs de embeddings (caching)
3. Alta disponibilidad con providers locales (fallback)
4. Métricas de uso y costos
5. Feature-gated: solo compila lo necesario
6. Storage flexibility: VectorStore trait permite elegir backend por proyecto
### Negativas
1. **Dimension lock-in**: Cambiar provider requiere re-indexar
2. **Cache invalidation**: Contenido actualizado puede servir embeddings stale
3. **Model download**: fastembed descarga ~100MB en primer uso
4. **Storage lock-in por proyecto**: Kogral atado a SurrealDB, otros a LanceDB
### Mitigaciones
| Negativo | Mitigación |
| ----------------- | ------------------------------------------------------ |
| Dimension lock-in | Documentar claramente, warn en cambio de provider |
| Cache stale | TTL configurable, opción de bypass |
| Model download | Mostrar progreso, cache en ~/.cache/fastembed |
| Storage lock-in | Decisión consciente basada en prioridades del proyecto |
## Métricas de Éxito
| Métrica | Actual | Objetivo |
| --------------------------- | ------ | -------- |
| Implementaciones duplicadas | 3 | 1 |
| Cache hit rate | 0% | >60% |
| Fallback availability | 0% | 100% |
| Cost per 10K embeddings | ~$0.20 | ~$0.05 |
## Guía de Selección de Provider
### Desarrollo
```rust
// Local, gratis, offline
let service = EmbeddingService::builder()
.with_provider(FastEmbedProvider::small()?) // 384 dims
.with_memory_cache()
.build()?;
```
### Producción (Calidad)
```rust
// OpenAI con fallback local
let service = EmbeddingService::builder()
.with_provider(OpenAiEmbeddingProvider::large()?) // 3072 dims
.with_provider(FastEmbedProvider::large()?) // Fallback
.with_memory_cache()
.build()?;
```
### Producción (Costo-Optimizado)
```rust
// OpenAI small con fallback
let service = EmbeddingService::builder()
.with_provider(OpenAiEmbeddingProvider::small()?) // 1536 dims
.with_provider(OllamaEmbeddingProvider::nomic()) // Fallback
.with_memory_cache()
.build()?;
```
## Matriz de Compatibilidad de Dimensiones
| Si usas... | Puedes cambiar a... | NO puedes cambiar a... |
| ---------------------- | --------------------------- | ---------------------- |
| fastembed small (384) | fastembed small, all-minilm | Cualquier otro |
| fastembed large (1024) | fastembed large | Cualquier otro |
| OpenAI small (1536) | OpenAI small, ada-002 | Cualquier otro |
| OpenAI large (3072) | OpenAI large | Cualquier otro |
**Regla**: Solo puedes cambiar entre modelos con las MISMAS dimensiones.
## Prioridad de Implementación
| Orden | Feature | Razón |
| ----- | ----------------------- | ---------------------------- |
| 1 | EmbeddingProvider trait | Base para todo |
| 2 | FastEmbed provider | Funciona sin API keys |
| 3 | Memory cache | Mayor impacto en costos |
| 4 | VectorStore trait | Abstracción de storage |
| 5 | SurrealDbStore | Kogral necesita graph+vector |
| 6 | LanceDbStore | Provisioning/Vapora escala |
| 7 | OpenAI provider | Producción |
| 8 | Ollama provider | Fallback local |
| 9 | Batch processing | Eficiencia |
| 10 | Metrics | Observabilidad |
## Referencias
**Implementaciones Existentes**:
- Kogral: `kogral-core/src/embeddings/`
- Vapora: `vapora-llm-router/src/embeddings.rs`
- Provisioning: `provisioning/platform/crates/rag/src/embeddings.rs`
**Ubicación Objetivo**: `stratumiops/crates/stratum-embeddings/`