ecc.agent-architecture-audit.report.v1 CULTIVA IA · Agentes Auditoría completada: 18 jun 2026

Auditoría de Arquitectura: AgenteCRM v2.3

Sistema: Conecta360 · Modelo: Claude 3.5 Sonnet + Haiku fallback · Capas auditadas: 12/12

⚠️
Salud general del sistema
ALTO RIESGO
No apto para escalar a 500 clientes sin correcciones críticas
Modo de fallo principal
Contaminación cruzada de memoria + tool execution no verificada
Corrección más urgente: Aislar namespaces de memoria por tenant
Alcance de la auditoría

Sistema

AgenteCRM v2.3 — Conecta360 SaaS B2B

150 clientes activos · plan escalar a 500

Stack de modelos

Síntomas que detonaron la auditoría

Mapa de capas — estado por capa
Capa 1
System Prompt
✓ Sin conflictos detectados
Capa 2
Historial de sesión
✗ 20 turns siempre presentes — sin poda
Capa 3
Memoria largo plazo
✗ Sin namespace por tenant — contaminación cruzada
Capa 4
Destilación
⚠ Resumen LLM agresivo a >8k tokens
Capa 5
Recall activo
⚠ Re-inyección sin deduplicar con historial
Capa 6
Selección de herramienta
✗ Routing sin code-gate tras context bloat
Capa 7
Ejecución de herramienta
✗ Sin validación de resultado real de actualizar_crm
Capa 8
Interpretación de herramienta
✓ Parsing correcto
Capa 9
Shaping de respuesta
✗ Truncamiento introducido en v2.3
Capa 10
Rendering / Transporte
✓ FastAPI → React sin mutaciones
Capa 11
Bucles ocultos
✗ Fallback Haiku sin contrato explícito
Capa 12
Persistencia
⚠ Sessions Redis sin TTL por organización
Hallazgos — ordenados por severidad
F-01 · Memoria Pinecone sin namespace de tenant — contaminación cruzada de datos
Critical Capa 3 · Memoria largo plazo Confianza: 0.97

El agente para el cliente A menciona leads y oportunidades del cliente B. Un agente que debería ver sólo datos de Constructora López recupera vectores de TechStartup S.L.

Las queries a Pinecone no filtran por organization_id. Todos los embeddings caen en el mismo índice sin metadata filter en el retrieve.

El wrapper de memoria usa búsqueda semántica global. Sin namespace/filter, cualquier similarity match devuelve contenido de otros tenants.

Presente desde v1.0 según tickets #001, #047, #089

memory/pinecone_client.py:34 → index.query(vector=emb, top_k=5) # ← SIN filter={"organization_id": org_id} memory/retrieval.py:88 → docs = retriever.get_relevant_documents(query) # sin metadata filter

Añadir filtro de metadata obligatorio en cada query a Pinecone. Implementar namespace por tenant o metadata filter en CADA llamada al retriever.

# memory/pinecone_client.py — ANTES (roto) results = index.query(vector=embedding, top_k=5) # DESPUÉS (correcto) results = index.query( vector=embedding, top_k=5, filter={"organization_id": {"$eq": tenant_id}} ) # Para el retriever LangChain: retriever = vectorstore.as_retriever( search_kwargs={"filter": {"organization_id": tenant_id}} )
F-02 · Tool execution no verificada — actualizar_crm reporta éxito sin confirmación real
Critical Capa 7 · Ejecución de herramienta Confianza: 0.93

El agente confirma "He actualizado el CRM" pero el cambio no existe. Logs no muestran llamada HTTP exitosa al endpoint del CRM.

La tool actualizar_crm captura excepciones silenciosamente y devuelve "success" aunque el HTTP 500 falle. El agente interpreta la respuesta de la tool, no el resultado real.

Error handler en la tool envuelve la excepción y devuelve pseudo-éxito al LLM en lugar de propagar el error.

Introducido en v2.1 al añadir retry logic. Tickets de soporte #201–#219

tools/crm_tools.py:67 → except Exception as e: tools/crm_tools.py:68 → logger.error(e) tools/crm_tools.py:69 → return "CRM update completed" # ← MENTIRA: devuelve éxito en fallo

Propagar el error explícitamente y estructurar el retorno como envelope con status. El LLM debe recibir el estado real.

# tools/crm_tools.py — DESPUÉS (correcto) def actualizar_crm(lead_id: str, data: dict, tenant_id: str) -> dict: try: response = crm_api.update(lead_id, data, tenant_id) response.raise_for_status() return {"status": "success", "lead_id": lead_id} except HTTPError as e: return {"status": "error", "code": e.response.status_code, "message": str(e), "retried": False}
F-03 · Fallback Haiku sin contrato — bucle oculto que muta respuestas sin traza
High Capa 11 · Bucles ocultos Confianza: 0.88

Respuestas que deberían venir de Sonnet llegan truncadas o con tono diferente. La salida interna y la entregada al usuario difieren.

Cuando Sonnet supera un umbral de latencia (3s), un middleware reenvía la query a Haiku y entrega su respuesta en su lugar, sin registrar el cambio de modelo en logs de usuario.

middleware/latency_fallback.py:45 → if elapsed > 3.0: middleware/latency_fallback.py:46 → return haiku_client.chat(messages) # ← sin log, sin contrato

Hacer el fallback explícito: loggear qué modelo respondió, exponer el campo model_used en la respuesta, y añadir header X-Agent-Model. Considerar rechazar en lugar de degradar silenciosamente.

F-04 · Tool routing degradado por context bloat — consultar_pipeline ignorada tras ≥5 turns
High Capa 6 · Selección de herramienta Confianza: 0.91

Tras 5-6 turns el modelo responde preguntas de pipeline sin llamar a consultar_pipeline, usando datos de contexto que pueden estar desactualizados.

Con 20 turns en historial + recall inyectado, el contexto alcanza ~12k tokens. El sistema prompt con "debes usar consultar_pipeline" queda enterrado. El LLM optimiza tokens y responde sin herramienta.

agent/history.py:12 → history = session.get_last_n_turns(n=20) # siempre 20, sin límite dinámico agent/runner.py:34 → # sin code-gate: "must use tool" solo en prompt texto

Code-gate obligatorio: antes de generar la respuesta, verificar si la pregunta es sobre pipeline y forzar tool call. Reducir historial a 10 turns (o dinámico por tokens). Separar instrucciones de uso de herramienta del system prompt de personalidad.

F-05 · Destilación agresiva en v2.3 introduce truncamiento semántico
High Capa 4 · Destilación / Capa 9 · Shaping Confianza: 0.85

Desde v2.3 las respuestas son más cortas y a veces incompletas. Usuarios reportan que "le falta el final".

El nuevo parámetro max_tokens=512 en el resumen LLM fue copiado al cliente principal sin querer. Las respuestas se cortan al límite.

llm/clients.py:89 → summary_client = Anthropic(max_tokens=512) # correcto para resúmenes llm/clients.py:94 → main_client = Anthropic(max_tokens=512) # ← BUG v2.3: copypaste del summary

Revertir main_client max_tokens a 4096. Separar configuración de clientes: nunca reusar config de un cliente de resumen para el cliente principal.

# llm/clients.py — CORRECCIÓN summary_client = Anthropic(max_tokens=512) # resúmenes: corto OK main_client = Anthropic(max_tokens=4096) # respuestas principales: completas
F-06 · Sessions Redis sin TTL por organización — memoria de 3 meses persiste
Medium Capa 12 · Persistencia Confianza: 0.82

El agente "recuerda" una conversación de 3 meses atrás que el cliente considera cerrada, usando esa información en el contexto actual.

Las claves Redis de sesión no tienen TTL. Las sessions de hace meses siguen activas. El recall las recupera como si fueran recientes.

memory/session_store.py:22 → redis.set(key, data) # ← sin TTL: persiste para siempre

Implementar TTL de 30 días en sessions Redis. Marcar conversaciones como "closed" en Pinecone y excluirlas del recall activo.

# memory/session_store.py — CORRECCIÓN SESSION_TTL_DAYS = 30 redis.set(key, data, ex=SESSION_TTL_DAYS * 86400)
F-07 · Duplicación de contexto — misma información en system prompt + historial + recall activo
Medium Capa 5 · Recall activo Confianza: 0.78

El context window se llena rápido. Información sobre el cliente aparece 3 veces (system prompt, historial reciente y recall). Contribuye al context bloat del F-04.

El recall inyecta fragmentos de Pinecone que ya están en el historial reciente. Sin deduplicación, el contexto crece exponencialmente.

Antes de inyectar recall, deduplicar contra el historial activo por hash de contenido. Usar MMR (Maximal Marginal Relevance) para diversidad en lugar de top-k puro.

F-08 · Logs de tool calls sin correlation_id — trazabilidad limitada
Low Capa 7 · Ejecución Confianza: 0.70

Al investigar incidencias, no se puede correlacionar una tool call específica con la sesión de usuario que la generó.

Añadir correlation_id (session_id + turn_number) a cada log de tool execution. Backlog — no bloquea producción.

Diagnóstico rápido — 7 preguntas clave
¿Puede el modelo saltarse una herramienta requerida y responder igual?
SÍ — consultar_pipeline no está code-gated (F-04)
¿Aparece contenido de otras conversaciones en turns nuevas?
SÍ — contaminación cruzada de memoria Pinecone (F-01)
¿La misma info está en system prompt Y memoria Y historial?
SÍ — triplicación de contexto (F-07)
¿La plataforma ejecuta un segundo LLM pass antes de entregar?
SÍ — fallback Haiku sin contrato (F-03)
¿El output difiere entre generación interna y entrega al usuario?
NO en Capa 10 — FastAPI/React sin mutaciones
¿Las reglas "must use tool" están solo en texto del prompt?
SÍ — no hay code enforcement en ninguna herramienta
¿El monólogo del agente puede volverse memoria persistente?
SÍ — sin filtro de fuente en admisión de memoria
Plan de correcciones ordenado — code-first
1

Aislar namespaces Pinecone por tenant (F-01)

Añadir filter={"organization_id": tenant_id} en todas las queries a Pinecone.

Por qué ahora: violación de privacidad de datos entre clientes. Riesgo legal inmediato.
Elimina contaminación cruzada
2

Revertir max_tokens en cliente principal (F-05)

Una línea: cambiar max_tokens=512 a max_tokens=4096 en main_client.

Por qué ahora: regresión directa de v2.3, fix de 30 segundos.
Respuestas completas inmediatas
3

Propagar errores reales en actualizar_crm (F-02)

Eliminar el catch silencioso. Retornar envelope con status: "error" cuando falla el HTTP call.

Por qué ahora: el agente miente sobre operaciones críticas de negocio.
CRM actualizado o error visible
4

Code-gate para consultar_pipeline (F-04)

Añadir verificación programática: si la query contiene palabras clave de pipeline, forzar tool call antes de generar respuesta.

Por qué ahora: datos de pipeline desactualizados llevan a decisiones comerciales erróneas.
Pipeline siempre consultado
5

Hacer explícito el fallback Haiku (F-03)

Loggear qué modelo respondió, añadir campo model_used en cada respuesta, evaluar si el degradado silencioso es aceptable.

Por qué ahora: sin trazabilidad del modelo, los bugs son imposibles de reproducir.
Trazabilidad de modelo completa
6

TTL en sessions Redis + cierre de conversaciones (F-06)

Añadir ex=30*86400 a todas las claves de sesión. Filtrar conversaciones cerradas en recall.

Por qué ahora: antes de escalar a 500 clientes, el stack Redis crecerá sin control.
Memoria acotada y privada
7

Deduplicación de contexto recall vs historial (F-07)

Implementar deduplicación por hash antes de inyectar recall. Usar MMR en lugar de top-k puro.

Por qué ahora: el context bloat es el vector de ataque del F-04 y degrada toda la calidad.
Contexto limpio, routing estable