RAG en producción: permisos en la query, frescura e índices por tenant
Resumen
El chunking arma el índice. En producción el fallo es otro: el tenant sale del auth, no del prompt; cada query va a un namespace o with_tenant(); el filtro de metadata no es un ACL; y un documento vive hasta que lo reindexas. Pinecone, Weaviate y OpenAI Retrieval, verificados el 6 de septiembre de 2026.

Qué resuelve
Esta pieza se queda en la decisión práctica: qué instalar, qué riesgo agrega y cómo aplicarlo sin romper operación.
La memoria y el RAG de un agente deciden qué recuperar. Esto es la capa que aguanta un SaaS: quién puede ver el chunk, cuándo dejó de ser cierto y de qué tenant sale el índice. Un demo con un PDF único no tiene tenants. Un agente que atiende a 40 clientes sí, y el primer bug de filtro $eq mal puesto filtra… o no.
Pinecone documenta el aislamiento físico por namespace (un tenant, un namespace; upserts y queries siempre apuntan a uno). Weaviate aísla por tenant/shard (with_tenant("tenantA"); un tenant no ve al otro). OpenAI Retrieval filtra atributos antes del search semántico (attribute_filter con eq/in/and/or); no te da un muro entre clientes. Fuentes verificadas el 6 de septiembre de 2026.
Contrato: tenant desde auth, query acotada, ACL en lectura, reindex por documento. Cero tenant en el prompt. Cero índice compartido “con un filtro por si acaso”.
Tres fallos que el chunking no cubre
El split de texto es la base. En producción se rompe por otra cosa:
- Fuga entre tenants. El modelo pide
search(query, tenant_id)y el LLM inventa eltenant_id. O el filtro de metadata{tenant: "acme"}se olvida en un path de retry. Pinecone lo dice claro: un namespace mal elegido no se “corrige” con un$eqdespués; el filtro de metadata en un namespace de 100 GB escanea los 100 GB (100 RUs si cada tenant pesa 1 GB; el mismo query a un namespace de 1 GB cuesta 1 RU). - Documento zombie. Subes
politica-v3.pdfy dejaspolitica-v2en el índice. El agente cita el reembolso viejo. No hay TTL mágico en el vector store: frescura =source_mtime/ingested_at+ borrar chunks deldocument_idantes de upsert. - ACL solo al ingest. Indexaste con
acl: ["hr"]y seis meses después esa persona salió de HR. La query sigue encontrando el chunk. El permiso se evalúa en la query, con el sujeto del OAuth del usuario, no con un tag que escribiste el día del crawl.
Los techos por tenant cortan spend. El PII cubre qué no debe entrar al índice. Esto cubre que el vecino no lea tu Drive.
El tenant no viaja en el tool call
El ID de tenant sale del auth (JWT org_id, sesión, RunContext), se inyecta en el adapter y no es argumento del tool que ve el modelo. Si el schema de search_docs incluye tenant, el modelo lo alucina. El adapter hace:
async function searchDocs(ctx: AuthCtx, query: string, filter?: AclFilter) {
const ns = namespaceFor(ctx.orgId); // nunca ctx.args.tenant
return index.query({ namespace: ns, vector, topK: 8, filter: aclOf(ctx, filter) });
}
Pinecone: namespaces se crean al primer upsert; offboarding = borrar el namespace (operación liviana). Weaviate: multiTenancyConfig.enabled: true (off por defecto); CRUD y search exigen el tenant (with_tenant). autoTenantCreation (v1.25+) crea tenants por typo (TenantOne vs tenantOne vs TenntOne = tres shards). No lo dejes on en ingest de usuarios. Estados ACTIVE / INACTIVE (renombrados de HOT/COLD en v1.26); OFFLOADED pide storage frío.
OpenAI vector stores: un store por tenant, o un store compartido más attribute_filter obligatorio (eq sobre org_id) en cada vectorStores.search. El filtro corre antes del semántico. Si omites el filtro, buscas todo el store.

Namespace vs filtro: no son el mismo ACL
| Enfoque | Aísla de verdad | Coste query | Sirve como ACL de rol |
|---|---|---|---|
| Pinecone 1 namespace / tenant | Físico (serverless guarda namespaces aparte) | 1 RU / GB del namespace | No: dentro del tenant aún filtras role/doc_class |
Weaviate with_tenant | Shard por tenant; invisible entre tenants | Operación al shard | RBAC colección+tenant (v1.29+; default v1.30+) |
Metadata $eq tenant en un índice único | No. Bug de filtro = fuga | Pinecone: escanea todo el namespace | Frágil: $in de 10k allowed_user_ids es el anti-patrón oficial |
OpenAI attribute_filter | Lógico, en ese vector store | Depende del store | Útil para fecha/región/filename; no sustituye un store por cliente |
El anti-patrón de Pinecone es literal: un filter.allowed_user_ids: {$in: [user_1 … user_10000]} en un namespace compartido. Usa namespace (o tenant Weaviate) para el cliente; metadata para clase de documento, idioma, document_id, ingested_at. Metadata: strings / number / bool / list of strings; tope 40 KB por record.
ACL de rol (finanzas vs HR) no vive solo en el índice. El sujeto (sub, grupos del IdP) se traduce a un filtro aditivo (doc_class in ctx.classes). Si el filtro queda vacío, no buscas: fail-closed. Un $or que “por si acaso incluye public” es un agujero.
Frescura: borrar, no “upsert encima y ya”
Un chunk no caduca solo. Contrato mínimo:
- Cada record lleva
document_id,chunk_number,source_mtime(unix) eingested_at. - Reindex de un doc = listar por prefijo / filtrar
document_id→ delete → upsert. Pinecone recomienda IDstenant#document#chunkylistpor prefijo para no dejar huérfanos. - Query con frescura:
ingested_at >= now - max_ageosource_mtime >= cursordel conector. OpenAI:attribute_filtergte/ltesobre unix timestamps (su propio ejemplo de rango de fechas). - Crawl incremental: watermark por fuente (Drive
modifiedTime, gitHEAD, S3LastModified). Full rebuild solo cuando cambia el embedder o el splitter. - Delete del tenant = delete namespace /
tenants.remove, no un cron que “marca deleted=true” y sigue retrieveando.
Si el conector falla a mitad, no dejes el índice a medias: o job idempotente (delete+upsert del mismo document_id) o no promociones el alias. El agente no tiene que “saber” que hay dos versiones; el índice no debería tenerlas.

Checklist de query en producción
- Tenant resuelto en el adapter, logueado, nunca en el system prompt ni en args del LLM.
- Cada retrieve declara namespace /
with_tenant/ vector store. Cero default al índice global. - Filtro ACL del sujeto (roles actuales, no tags de ingest). Vacío = no query.
top_kchico (8–20) + metadata dedocument_urlpara citar. Sin URL no se muestra al usuario.- Test de fuga: upsert en tenant A, query autenticado como B, expect 0 hits. Esto no lo cubre un eval de “respuesta correcta”.
- Test de frescura: reemplaza un doc, query, expect solo v2.
- Offboard: borrar namespace/tenant y revocar el API key de ese org. Weaviate RBAC por tenant no sustituye borrar datos.
El hub de construir agentes arma el loop. El de seguridad y coste es donde vive este contrato cuando el índice ya no es un folder de demo.
FAQ
¿Un filtro de metadata basta para un SaaS B2B? No como aislamiento. Pinecone cobra y escanea el namespace entero; un olvido de filtro cruza clientes. Namespace o with_tenant para el cliente; metadata para el resto.
¿Puedo poner tenant_id en el schema de la tool? No. El modelo lo rellena mal. El adapter lo saca del auth.
¿OpenAI File Search aísla tenants? No por sí. Un vector store por org, o attribute_filter obligatorio en cada search. Sin filtro, el store es un bucket.
¿autoTenantCreation en Weaviate? Útil en batch limpio. Peligroso con IDs de usuario: un typo crea un shard. Créalos en el signup.
¿Qué pasa con documentos “públicos” de la empresa? Índice o namespace de ese tenant, clase internal/public, ACL del sujeto. No un índice global “para todos los clientes del SaaS”.
¿El reindex tiene que ser síncrono en el turno del agente? No. El agente encola document_id; el worker borra+upsert. El retrieve no espera al embedder.
Fuentes
- Pinecone — Implement multitenancy (namespace por tenant, 1 RU/GB, offboard = delete namespace).
- Pinecone — Data modeling / multi-tenancy (anti-patrón
$inde user IDs; IDsdocument#chunk; 40 KB metadata). - Pinecone — Filter by metadata.
- Weaviate — Multi-tenancy (shard,
with_tenant, autoTenantCreation, ACTIVE/INACTIVE). - Weaviate — llms.txt / RBAC (RBAC colección+tenant v1.29+, default v1.30+).
- OpenAI — Retrieval (
attribute_filterantes del semántico).
Lecturas relacionadas
Sigue explorando AgentOps y otras piezas para builders.

SLO y error budget en agentes: 4 SLIs, burn rápido/lento y política 50/100

De error de producción a eval: el flywheel que alimenta el dataset

Aprobaciones humanas en agentes: lotes, timeout fail-closed y presupuesto de interrupciones
