Saltar al contenido
# 🧪 Cómo generar nuevas baterías de test semántico ## 🤔 ¿Qué hago? ¿Cómo lo hago? ¿Y para qué lo hago? **¿Qué hago?** Creo una batería de tests que valida que la caché semántica de klaude-proxy distingue correctamente entre preguntas nuevas (MISS) y reformulaciones de preguntas ya cacheadas (HIT). **¿Cómo lo hago?** Diseño 3 preguntas originales sobre tópicos distintos, genero 7 variantes léxicas de esas preguntas preservando los tokens técnicos exactos, valido cada variante con `validate_variant.sh` y las ensamblo en un script bash siguiendo el template estándar. **¿Para qué lo hago?** Para aumentar la cobertura de validación de la caché semántica con nuevos dominios y tipos de pregunta, detectando regresiones en el threshold (0.87), el modelo de embeddings (`nomic-embed-text-v1.5`) o cambios en la lógica del proxy. --- ## 📐 Estructura obligatoria de una batería ```text 10 preguntas = 3 originales (MISS) + 7 variantes léxicas (HIT) Q1 → MISS — tópico A (original) Q2 → MISS — tópico B (original) Q3 → MISS — tópico C (original) Q4 → HIT — variante de Q1 Q5 → HIT — variante de Q1 Q6 → HIT — variante de Q2 Q7 → HIT — variante de Q2 Q8 → HIT — variante de Q3 Q9 → HIT — variante de Q3 Q10 → HIT — variante de Q3 ``` **Distribución mínima de variantes:** 2 por tópico A y B, 3 por tópico C. Puede redistribuirse siempre que ningún tópico tenga menos de 2 variantes. --- ## 🔑 Regla de rephrasing léxico — la más importante > **Mantener todos los tokens técnicos exactos. Solo variar la estructura gramatical.** ### Tokens que NUNCA se cambian Estos términos son técnicos y su modificación reduce la similitud coseno por debajo del threshold 0.87: | Categoría | Ejemplos de tokens intocables | | --- | --- | | Nombres de componentes | `klaude-proxy`, `Qdrant`, `FastAPI`, `asyncio`, `ONNX` | | Variables de entorno | `QDRANT_URL`, `ANTHROPIC_API_KEY`, `SIMILARITY_THRESHOLD` | | Parámetros técnicos | `768`, `coseno`, `0.87`, `tool_use`, `stop_reason` | | Nombres de endpoint | `/v1/messages`, `/cache/flush`, `/analytics/history` | | Nombres de campo/header | `x-api-key`, `x-cache`, `points_count` | | Tipos y clases Python | `asyncio.Semaphore`, `set[asyncio.Task]`, `_background_tasks` | ### Qué SÍ se puede variar Solo la envoltura gramatical alrededor de los tokens técnicos: | Original | Variante válida | | --- | --- | | `¿Qué errores produce ...` | `¿Qué fallos genera ...` | | `¿Qué causa un error 500 ...` | `¿Qué provoca un error 500 ...` | | `¿Cuál es la razón por la que ...` | `¿Por qué ...` | | `Genera el código Python de ...` | `Escribe el código Python de ...` | | `Implementa la función Python que ...` | `Crea la función Python que ...` | | `cuando X no está disponible` | `si X no responde` | | `no puede cargarse` | `falla al cargarse` / `no se puede cargar` | ### Ejemplo completo ```text ✅ Original (MISS): "¿Qué errores produce klaude-proxy cuando Qdrant no está disponible en la URL configurada en QDRANT_URL?" ✅ Variante válida (HIT — solo verbo cambiado, tokens técnicos intactos): "¿Qué fallos genera klaude-proxy cuando Qdrant no está disponible en la URL configurada en QDRANT_URL?" ✅ Variante válida (HIT — estructura condicional cambiada): "¿Qué errores produce klaude-proxy si Qdrant no responde en la URL configurada en QDRANT_URL?" ❌ Variante inválida (FAIL — token técnico sustituido por su significado): "¿Qué errores produce klaude-proxy cuando la base de datos vectorial no está disponible en la dirección de conexión?" ``` --- ## 🗂️ Cómo elegir tópicos para una nueva batería ### Regla de separación semántica Los 3 tópicos de la batería deben ser **semánticamente distintos entre sí y con respecto a las baterías existentes**. Con threshold=0.87 y nomic-embed-text, dos preguntas sobre el mismo concepto con diferente formulación pueden colapsar en HIT. ### Tópicos ya cubiertos (no repetir) | Batería | Dominio | Tópicos cubiertos | | --- | --- | --- | | v1 | Semántica básica | Dimensión embedding + modelo, columnas SQLite analytics.db, formato SSE | | v2 | Threshold / exclusiones | Variable de umbral similitud, tipos de respuesta excluidos del caché, streaming en HIT | | v3 | Concurrencia Python | asyncio.Semaphore ONNX, _background_tasks, exclusión tool_use | | v4 | Generación de código | Inicialización colección Qdrant 768/coseno, handler /v1/messages + x-api-key, cálculo coste USD | | v5 | Errores técnicos | Errores conexión Qdrant/QDRANT_URL, error 500 ANTHROPIC_API_KEY, fallo carga ONNX | | v6 | Autenticación | /cache/flush requiere x-api-key → 401, reenvío x-api-key a Anthropic, ANTHROPIC_API_KEY obligatorio en Settings | | v7 | Configuración | CACHE_TOOL_RESPONSES (default false), QDRANT_COLLECTION (default claude_cache), PROXY_PORT (default 8080) | | v8 | Analytics avanzado | /analytics/stats in-memory vs /analytics/history SQLite, request_preview 400 chars, calculate_cost formula | | v9 | Dashboard | /logs/stream keepalive 300ms, /logs?n parámetro, reconexión SSE setTimeout 3000ms | | v10 | Rendimiento y memoria | preload_model doble sesión ONNX, serialize_request cap 2000 chars, mem_limit 2g compose | | v11 | Logging y telemetría | Namespace klaude.proxy + log_buffer.install, color azul ANSI telemetría, buffer append-only | | v12 | Deploy y contenedores | depends_on service_healthy, FASTEMBED_CACHE_PATH Dockerfile, healthcheck curl /health | | v13 | Serialización y bypass | _SUBSYSTEM_RE subsistemas Claude Code, _INJECTED_BLOCK_RE limpia XML, serialize_store_key tool_result | --- ## 🌐 Taxonomía de dominios Un **dominio** es el área funcional del proxy de la que se extraen los 3 tópicos de una batería. La relación jerárquica es: ```text dominio (área funcional amplia) └── tópico A → Q1 MISS + Q4, Q5 HIT └── tópico B → Q2 MISS + Q6, Q7 HIT └── tópico C → Q3 MISS + Q8, Q9, Q10 HIT ``` El dominio da **coherencia temática** a la batería. Los 3 tópicos deben ser conceptualmente distintos dentro del dominio: la similitud coseno entre dos preguntas de tópicos distintos debe quedar **por debajo de 0.87** para que Q1-Q3 sean MISS independientes. ### 📐 Dominios del proxy y su mapa de cobertura | ID | Dominio | Área de código | Estado | | --- | --- | --- | --- | | D01 | Semántica básica | embeddings.py, analytics.py, log_buffer.py | ✅ v1 | | D02 | Threshold y exclusiones | config.py, cache.py | ✅ v2 | | D03 | Concurrencia Python | main.py (_embed_sem, _background_tasks) | ✅ v3 | | D04 | Generación de código | cache.py, main.py, analytics.py | ✅ v4 | | D05 | Errores técnicos | main.py, embeddings.py, config.py | ✅ v5 | | D06 | Autenticación | main.py (/cache/flush), config.py (Settings) | ✅ v6 | | D07 | Configuración del proxy | config.py (variables opcionales) | ✅ v7 | | D08 | Analytics avanzado | analytics.py (stats/history, calculate_cost) | ✅ v8 | | D09 | Dashboard | main.py (HTML, /logs/stream, /logs) | ✅ v9 | | D10 | Rendimiento y memoria | embeddings.py (preload_model), compose.yaml | ✅ v10 | | D11 | Logging y telemetría | log_buffer.py, main.py (_ColoredFormatter) | ✅ v11 | | D12 | Deploy y contenedores | compose.yaml, Dockerfile | ✅ v12 | | D13 | Serialización y bypass | embeddings.py (serialize_*, regex) | ✅ v13 | | D14 | Modelo de datos Anthropic | models.py (si existe), estructuras JSON | ⬜ disponible | | D15 | Cliente Anthropic | anthropic_client.py (forward, stream, SSE) | ⬜ disponible | ### 🔍 Cómo verificar que dos tópicos no colisionan Antes de crear la batería, comprobar que Q1, Q2 y Q3 no se devuelven HIT entre sí: ```bash # Paso 1: lanzar Q1 (MISS esperado — nuevo vector en caché) curl -s -X POST http://192.168.1.50:8080/v1/messages \ -H "x-api-key: test" -H "content-type: application/json" \ -d '{"model":"claude-haiku-4-5-20251001","max_tokens":512,"messages":[{"role":"user","content":"[Q1]"}]}' # Paso 2: lanzar Q2 — si devuelve HIT, los tópicos A y B son demasiado similares # Paso 3: lanzar Q3 — si devuelve HIT, el tópico C solapa con A o B # Si alguno devuelve HIT: elegir un tópico más específico o de otro sub-área del dominio ``` ### 💡 Regla práctica para elegir tópicos dentro de un dominio Dos tópicos son suficientemente distintos si responden a **preguntas de naturaleza diferente** dentro del mismo dominio: | Naturaleza de pregunta | Ejemplo | | --- | --- | | ¿Qué valor tiene X? | Valor por defecto de `PROXY_PORT` | | ¿Qué ocurre cuando Y? | Comportamiento al faltar `x-api-key` | | ¿Cómo funciona Z? | Mecanismo de `serialize_store_key` | | ¿Dónde está W? | Namespace del logger principal | | ¿Por qué se hace V? | Razón del buffer append-only | Mezclar naturalezas distintas dentro del mismo dominio garantiza separación semántica. --- ## 🔬 Validar variantes antes de incluirlas Antes de añadir una variante al script, validarla individualmente con `validate_variant.sh`: ```bash # Sintaxis bash tests/validate_variant.sh "pregunta original exacta" "variante candidata" # Ejemplo bash tests/validate_variant.sh \ "¿Qué errores produce klaude-proxy cuando Qdrant no está disponible en la URL configurada en QDRANT_URL?" \ "¿Qué fallos genera klaude-proxy si Qdrant no responde en la URL configurada en QDRANT_URL?" ``` **Resultado esperado:** ```text ✅ PASS — Variante válida (similitud ≥ 0.87) Puedes incluir esta variante en el test suite. ``` Si devuelve FAIL, la variante no alcanza el threshold. Reformular según las pistas del script y repetir. --- ## 📋 Template para una nueva batería Copiar este template, rellenar los `[PLACEHOLDER]` y guardar como `tests/test_cache_semantic_vN.sh`: ```bash #!/usr/bin/env bash # [N]ª batería — caché semántica klaude-proxy — v[N] [TEMA] # [Descripción de los tópicos cubiertos] # Diseño: 3 preguntas originales (MISS) + 7 variantes léxicas (HIT) # Portable: configurar con variables de entorno PROXY, MODEL, API_KEY # Ejemplo: PROXY=http://192.168.1.50:8080 bash test_cache_semantic_v[N].sh set -euo pipefail PROXY="${PROXY:-http://192.168.1.50:8080}" MODEL="${MODEL:-claude-haiku-4-5-20251001}" API_KEY="${API_KEY:-test}" PASS=0 FAIL=0 RED='\033[0;31m' GREEN='\033[0;32m' YELLOW='\033[1;33m' CYAN='\033[0;36m' NC='\033[0m' call_proxy() { local label="$1" local expected="$2" local content="$3" payload=$(printf '{"model":"%s","max_tokens":512,"messages":[{"role":"user","content":"%s"}]}' \ "$MODEL" "$(printf '%s' "$content" | sed 's/"/\\"/g')") response=$(curl -s -D - \ -H "x-api-key: $API_KEY" \ -H "anthropic-version: 2023-06-01" \ -H "content-type: application/json" \ -d "$payload" \ "$PROXY/v1/messages" 2>&1) cache_header=$(echo "$response" | grep -i "x-cache:" | tr -d '\r' | awk '{print $2}') cache_header=${cache_header:-"UNKNOWN"} if [[ "$cache_header" == "$expected" ]]; then status="${GREEN}✅ PASS${NC}" PASS=$((PASS + 1)) else status="${RED}❌ FAIL (got $cache_header, expected $expected)${NC}" FAIL=$((FAIL + 1)) fi printf " %-6s %-10s $status\n" "[$label]" "[$cache_header]" } echo "" echo -e "${CYAN}═══════════════════════════════════════════════════════════════${NC}" echo -e "${CYAN} klaude-proxy — Batería [TEMA] v[N] (Issue #[N])${NC}" echo -e "${CYAN} [Descripción breve]${NC}" echo -e "${CYAN} Proxy: $PROXY${NC}" echo -e "${CYAN}═══════════════════════════════════════════════════════════════${NC}" echo "" # ── Prerequisito: caché en estado limpio ────────────────────────────────────── echo -e "${YELLOW}⚠️ AVISO: Este test asume que el caché de Qdrant está vacío.${NC}" echo -e "${YELLOW} Q1-Q3 esperan MISS — si el caché tiene datos, devolverán HIT y el test fallará.${NC}" echo -e "${YELLOW} Flush manual: curl -s -X DELETE -H \"x-api-key: \$API_KEY\" \$PROXY/cache/flush${NC}" echo -e "${YELLOW} Ver: docs/cache_flush_manual.md${NC}" echo "" echo -e "${YELLOW}📊 Estado actual del caché (referencia):${NC}" curl -s "$PROXY/cache/stats" | python3 -m json.tool 2>/dev/null || curl -s "$PROXY/cache/stats" echo "" # ── Tópicos cubiertos ───────────────────────────────────────────────────────── # A. [Tópico A] → Q1 (MISS) + Q4, Q5 (HIT) # B. [Tópico B] → Q2 (MISS) + Q6, Q7 (HIT) # C. [Tópico C] → Q3 (MISS) + Q8, Q9, Q10 (HIT) echo -e "${YELLOW}━━━ Fase 1: 3 preguntas originales → esperado MISS ━━━${NC}" echo "" call_proxy "Q1" "MISS" \ "[PREGUNTA ORIGINAL TÓPICO A]" call_proxy "Q2" "MISS" \ "[PREGUNTA ORIGINAL TÓPICO B]" call_proxy "Q3" "MISS" \ "[PREGUNTA ORIGINAL TÓPICO C]" echo "" echo -e "${YELLOW}━━━ Fase 2: 7 variantes léxicas → esperado HIT ━━━${NC}" echo "" # Variantes de Q1 — [Tópico A] call_proxy "Q4" "HIT" \ "[VARIANTE 1 DE Q1]" call_proxy "Q5" "HIT" \ "[VARIANTE 2 DE Q1]" # Variantes de Q2 — [Tópico B] call_proxy "Q6" "HIT" \ "[VARIANTE 1 DE Q2]" call_proxy "Q7" "HIT" \ "[VARIANTE 2 DE Q2]" # Variantes de Q3 — [Tópico C] call_proxy "Q8" "HIT" \ "[VARIANTE 1 DE Q3]" call_proxy "Q9" "HIT" \ "[VARIANTE 2 DE Q3]" call_proxy "Q10" "HIT" \ "[VARIANTE 3 DE Q3]" echo "" echo -e "${YELLOW}━━━ Estado final del caché ━━━${NC}" echo "" curl -s "$PROXY/cache/stats" | python3 -m json.tool 2>/dev/null || curl -s "$PROXY/cache/stats" echo "" echo -e "${YELLOW}━━━ Analytics — histórico SQLite ━━━${NC}" echo "" curl -s "$PROXY/analytics/history" | python3 -m json.tool 2>/dev/null || curl -s "$PROXY/analytics/history" echo "" echo -e "${YELLOW}━━━ Analytics — sesión actual ━━━${NC}" echo "" curl -s "$PROXY/analytics/stats" | python3 -m json.tool 2>/dev/null || curl -s "$PROXY/analytics/stats" echo "" echo -e "${CYAN}═══════════════════════════════════════════════════════════════${NC}" TOTAL=$((PASS + FAIL)) if [[ $FAIL -eq 0 ]]; then echo -e "${GREEN} RESULTADO: $PASS/$TOTAL PASS — Batería [TEMA] validada ✅${NC}" else echo -e "${RED} RESULTADO: $PASS/$TOTAL PASS — $FAIL fallos ❌${NC}" fi echo -e "${CYAN}═══════════════════════════════════════════════════════════════${NC}" echo "" echo -e " Dashboard: ${CYAN}$PROXY/dashboard${NC}" echo "" ``` --- ## 🔄 Registrar la nueva batería en el wrapper Tras crear `tests/test_cache_semantic_vN.sh`, añadirla a `tests/run_all_tests.sh`: ```bash # En SUITE_FILES — añadir al final del array: SUITE_FILES=( ... "test_cache_semantic_vN.sh" # ← nueva suite ) # En SUITE_NAMES — descripción corta: SUITE_NAMES=( ... "vN — [tema breve] ([tópico A], [tópico B], [tópico C])" ) # En SUITE_EXPECTED — resultado esperado (normalmente 10): SUITE_EXPECTED=(... "10") # Si hay edge-cases conocidos, ajustar al número esperado real. ``` --- ## 📊 Flujo completo de desarrollo de una batería ```mermaid flowchart TD A[🎯 Elegir 3 tópicos nuevos] --> B{¿Solapan con baterías existentes?} B -- Sí --> A B -- No --> C[✍️ Redactar 3 preguntas originales MISS] C --> D[✍️ Redactar 7 variantes léxicas HIT] D --> E[🔬 Validar cada variante con validate_variant.sh] E --> F{¿Alguna variante falla?} F -- Sí --> G[🔧 Reformular: mantener tokens técnicos,\ncambiar solo estructura gramatical] G --> E F -- No --> H[📝 Ensamblar script desde template] H --> I[🧪 Ejecutar la suite nueva en aislamiento\nbash tests/test_cache_semantic_vN.sh] I --> J{¿10/10 PASS?} J -- No --> K{¿Edge-case conocido?} K -- Sí --> L[📋 Documentar en SUITE_EXPECTED del wrapper] K -- No --> G J -- Sí --> L L --> M[➕ Registrar en run_all_tests.sh] M --> N[🔁 Ejecutar bash tests/run_all_tests.sh] N --> O[✅ Abrir PR] ``` --- ## 🔗 Documentos relacionados - [cache.md](cache.md) — arquitectura del caché semántico, threshold, modelo de embeddings - [cache_flush_manual.md](cache_flush_manual.md) — cuándo y cómo ejecutar el flush - [proxy.md](proxy.md) — endpoints del proxy, headers de respuesta