E1 con el Space de HF como único backend (20 preguntas, 2026-09-27)

Primer uso real del Space ZeroGPU: todo el pipeline (embed + retrieval + rerank + respuesta + juez) corrió sobre [usuario]/sonrisa-inference, costo $0 y sin tocar Groq ni Inference Providers. Produjo números de evaluación.

Setup

# en proyecto_1_dentistas
export DENTISTAS_LLM_PROVIDER=space
export DENTISTAS_EMB_PROVIDER=space
export DENTISTAS_JUDGE_PROVIDER=space
export DENTISTAS_SPACE_OPENAI=http://[IP]:8123/v1
PYTHONPATH=src .venv/bin/python -m dentistas.space_server 8123 &   # shim
PYTHONPATH=src .venv/bin/python -m dentistas.cli index             # 77 chunks, dim 2560
PYTHONPATH=src .venv/bin/python -m dentistas.cli eval --limit 20

Resultado (n=20, golden set v0)

métricavalorumbral¿pasa?
retrieval_hit_rate1.0—✅
citation_accuracy0.9380.90✅
escalate_accuracy0.5—❌
faithfulness0.770.85❌
context_precision0.6450.75❌
context_recall0.800.75✅
costo generación$0.0—✅
  • Índice: 77 chunks, dim 2560. Generación 72.6 s. Juez: 220 llamadas, 1120 s.
  • Wall clock total: 20 min 39 s.

El hallazgo que importa: el prompt, no el modelo

escalate_accuracy = 0.5. Las 4 preguntas fuera de alcance (q17–q20) fueron contestadas de forma asertiva en vez de negarse:

  • q17 “¿atienden a domicilio?” → “No, no atendemos a domicilio” (el corpus no dice eso).
  • q19 “¿criptomonedas?” → “No aceptamos criptomonedas” (inferido de la lista de pagos).
  • q20 “¿convenio con IMSS?” → “No tenemos convenio con el IMSS” (inferido).

O sea: el RAG pelado inventa respuestas negativas. Es exactamente lo que las reglas de clinica-chat prohíben — y ahí, con gemma + el prompt estricto, las mismas 4 preguntas pasan 4/4. Conclusión: para “no inventar”, las reglas del prompt pesan más que el modelo. El retrieval (1.0) y las citas (0.938) no son el problema; la política del respondedor sí.

context_precision = 0.645 (bajo): recupera bien pero trae contexto de más. Candidato a mejorar con el rerank (está en off en config) o el chunking.

Arreglo aplicado al Space (y su trampa)

  • app.py: apply_chat_template(..., enable_thinking=False) — Qwen3 razona por defecto y el <think> truncaba/vaciaba la respuesta. Antes: '<think>Okay, the user is asking...' en 13.9 s. Después: "La limpieza dental completa cuesta 850 pesos." en 7.2 s.
  • Trampa: el Space estaba en Dev Mode, así que el push del commit no se aplicó (seguía corriendo 2 commits atrás). Hay que reiniciar: hf spaces restart <id> (o --factory-reboot). Además el runtime.sha del API va stale: la verdad es el comportamiento, no el campo.

Bugs de infra que salieron

  • gradio_client no estaba en pyproject.toml (el provider space lo necesita).
  • El juez space estaba como NotImplementedError; se cableó con un shim OpenAI-compatible (src/dentistas/space_server.py) que traduce /v1/chat/completions → Space Gradio, para reusar el camino openai de RAGAS.
  • El resumen imprime juez = space/deepseek-ai/DeepSeek-V4-Pro: es solo la etiqueta de config, el juez real fue Qwen3-14B. No leerlo mal.

Repetición con rerank=llm (corrida 20260927-193113, mismo día)

métricarerank offrerank llmumbral
retrieval_hit_rate1.01.0—
citation_accuracy0.9380.8750.90
escalate_accuracy0.50.75—
faithfulness0.770.7550.85
context_precision0.6450.8530.75
context_recall0.800.800.75
  • Objetivo cumplido: context_precision cruza el umbral (0.645→0.853).
  • Cambio de perfil, no dominancia: sin rerank pasa citation; con rerank pasan precision y escalate. Ninguna corrida pasa las 4 umbrales.
  • faithfulness ≈0.76 en ambas: la invención vive en el respondedor (política del prompt), no en el retrieval. Confirmado dos veces.
  • Costes: $0 en ambas; 20 llamadas extra de rerank (judge 1198 s vs 1120 s).
  • Queda rerank = "llm" como default en config.
  • Bugs destapados al encenderlo: _llm_rerank asumía dict y recibía lista (AttributeError en el fallback, nunca ejercido con rerank=off); y providers.chat modo space ignoraba json_mode → el rerank habría caído en silencio al orden RRF. Ambos arreglados.

220 llamadas de chat + embeddings en un día. La cuota PRO es 40 min de GPU/día; se consumió tiempo efectivo (no el duration reservado). Caber, cupo — pero un E1 completo es una corrida de ~20 min de reloj, no algo para correr en bucle.