Sistema: Conecta360 · Modelo: Claude 3.5 Sonnet + Haiku fallback · Capas auditadas: 12/12
AgenteCRM v2.3 — Conecta360 SaaS B2B
150 clientes activos · plan escalar a 500
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
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}}
)
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
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}
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.
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.
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.
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.
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.
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
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.
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)
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.
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.
Añadir filter={"organization_id": tenant_id} en todas las queries a Pinecone.
Una línea: cambiar max_tokens=512 a max_tokens=4096 en main_client.
Eliminar el catch silencioso. Retornar envelope con status: "error" cuando falla el HTTP call.
Añadir verificación programática: si la query contiene palabras clave de pipeline, forzar tool call antes de generar respuesta.
Loggear qué modelo respondió, añadir campo model_used en cada respuesta, evaluar si el degradado silencioso es aceptable.
Añadir ex=30*86400 a todas las claves de sesión. Filtrar conversaciones cerradas en recall.
Implementar deduplicación por hash antes de inyectar recall. Usar MMR en lugar de top-k puro.