Inicio / Blog / Python / Prueba LLM pide | pytest, sin llamadas a la API, sin mocking.

Prueba LLM pide | pytest, sin llamadas a la API, sin mocking.

Resumir con ChatGPT
LLM-Prompts testen: Mit pytest, ohne API-Calls, ohne Mocking.

El problema

Usted tiene un Pregunte a, que le dice a un LLM: Devuelve JSON. Su código analiza este JSON. Alguien edita el prompt - renombra un campo, cambia un valor de String a Int, borra una clave. Tu código se rompe en tiempo de ejecución. En producción.

Esto ocurre continuamente. Los avisos viven en archivos Markdown. El código vive en Python. Nada conecta a los dos. El prompt dice "score", pero el código dice "rating". El indicador muestra "status": "done", pero el código comprueba "ready". El prompt tiene 5 campos, el código espera 6.

La brecha entre lo que tu prompt le dice al LLM y lo que tu código espera es un contrato. Si nadie pone a prueba este contrato, se romperá.

La solución: Pruebas contractuales inmediatas

Escriba a JSON-ejemplos en sus indicaciones. Añádalos a las pruebas. Valide la estructura contra lo que su código realmente lee.

Sin llamadas a la API. Sin mocking. Sin LLM. Sólo regex + JSON parsing + aserciones. Las pruebas se ejecutan en milisegundos.

Ejemplo mínimo

Supongamos que tiene un archivo de consulta que ordena a un LLM que analice los comentarios de los clientes:

<!-- prompts/analyze_feedback.md -->
# Analyse Kundenfeedback
Pruefe das Kundenfeedback und klassifiziere es.
## Output Format
Antworte ausschliesslich mit JSON:
​```json
{
  "status": "ready",
  "sentiment": "positive",
  "topics": ["pricing", "support"],
  "urgency": 3,
  "suggestions": {
    "summary": "Kunde ist zufrieden mit dem Support, aber besorgt ueber Preise.",
    "follow_up": true
  }
}
​```
Falls mehr Kontext noetig ist:
​```json
{
  "status": "needs_info",
  "questions": ["Welches Produkt nutzt der Kunde?", "Wann trat das Problem auf?"],
  "suggestions": {}
}
​```

Y tu código hace algo así:

# app/feedback.py
def process_feedback(llm_response: dict):
    status = llm_response["status"]         # muss existieren
    if status == "ready":
        sentiment = llm_response["sentiment"]   # muss String sein
        topics = llm_response["topics"]         # muss Liste sein
        urgency = llm_response["urgency"]       # muss Int 1-5 sein
        summary = llm_response["suggestions"]["summary"]  # muss existieren
    elif status == "needs_info":
        questions = llm_response["questions"]   # muss nicht-leere Liste sein

He aquí la prueba que combina ambas:

# tests/test_prompt_contracts.py
import json
import re
from pathlib import Path
PROMPTS_DIR = Path(__file__).resolve().parent.parent / "prompts"

def _read_prompt(filename: str) -> str:
    path = PROMPTS_DIR / filename
    assert path.exists(), f"Fehlt: {path}"
    return path.read_text(encoding="utf-8")

def _extract_json_blocks(text: str) -> list[dict]:
    """Alle ```json ... ``` Bloecke aus einer Markdown-Datei extrahieren."""
    pattern = r"```json\s*\n(.*?)```"
    matches = re.findall(pattern, text, re.DOTALL)
    results = []
    for match in matches:
        results.append(json.loads(match.strip()))
    return results

# --- Hat der Prompt ueberhaupt parsebare JSON-Beispiele? ---
def test_prompt_has_output_format():
    text = _read_prompt("analyze_feedback.md")
    assert "## Output Format" in text
def test_json_blocks_are_valid():
    text = _read_prompt("analyze_feedback.md")
    examples = _extract_json_blocks(text)
    assert len(examples) >= 2, "Brauche mindestens ein 'ready' und ein 'needs_info' Beispiel"
# --- Passen die Beispiele zu dem, was der Code liest? ---
def test_status_values_are_valid():
    text = _read_prompt("analyze_feedback.md")
    for example in _extract_json_blocks(text):
        assert example["status"] in {"ready", "needs_info"}, (
            f"Code prueft auf 'ready' oder 'needs_info', bekommen: '{example['status']}'"
        )
def test_ready_example_has_required_fields():
    text = _read_prompt("analyze_feedback.md")
    ready = [e for e in _extract_json_blocks(text) if e["status"] == "ready"]
    assert len(ready) > 0, "Kein 'ready'-Beispiel im Prompt"
    for example in ready:
        # Genau die Felder, die process_feedback() liest
        assert "sentiment" in example
        assert isinstance(example["sentiment"], str)
        assert "topics" in example
        assert isinstance(example["topics"], list)
        assert "urgency" in example
        assert isinstance(example["urgency"], int)
        assert 1 <= example["urgency"] <= 5
        assert "summary" in example["suggestions"]
def test_needs_info_has_questions():
    text = _read_prompt("analyze_feedback.md")
    needs_info = [e for e in _extract_json_blocks(text) if e["status"] == "needs_info"]
    assert len(needs_info) > 0, "Kein 'needs_info'-Beispiel im Prompt"
    for example in needs_info:
        assert "questions" in example
        assert isinstance(example["questions"], list)
        assert len(example["questions"]) > 0, "Leere Fragenliste"

Ejecutar:

pytest tests/test_prompt_contracts.py -v
test_prompt_has_output_format       PASSED
test_json_blocks_are_valid          PASSED
test_status_values_are_valid        PASSED
test_ready_example_has_required_fields  PASSED
test_needs_info_has_questions       PASSED

Lo que esto revela

Imagina que alguien edita el aviso y lo nombra "urgency" en "priority" alrededor. O cambia"status": "ready" a "status": "complete". O borrar el "summary"-campo de las sugerencias.

Las pruebas fallan inmediatamente. Antes del despliegue. Antes de que el LLM vea el nuevo prompt. Antes de que un cliente detecte el fallo.

Ejemplos reales de la deriva puntual que capta este patrón:

Qué cambia en el promptLo que se rompe en el códigoPrueba que lo recoge
urgencyha pasado a llamarse priorityKeyError: 'urgency'test_ready_example_has_required_fields
"ready"cambiado a "complete"if status == "ready"test_status_values_are_valid
suggestions.summary eliminadoKeyError: 'summary'test_ready_example_has_required_fields
urgency de Int a CadenaEl código hace urgency > 3 en una cadenatest_ready_example_has_required_fields
Ejemplo JSON completamente borradoLLM devuelve un formato impredecibletest_json_blocks_are_valid

Escala

Para varias solicitudes: parametrizar.

PROMPTS = {
    "analyze_feedback.md": "feedback",
    "classify_ticket.md": "ticket",
    "summarize_report.md": "report",
}
@pytest.mark.parametrize("filename", PROMPTS.keys())
def test_has_output_format(filename):
    text = _read_prompt(filename)
    assert "## Output Format" in text
@pytest.mark.parametrize("filename", PROMPTS.keys())
def test_json_is_valid(filename):
    text = _read_prompt(filename)
    for block in _extract_json_blocks(text):
        assert "status" in block  # jeder Prompt muss Status zurueckgeben

A continuación, clases de prueba específicas para los campos que sólo aparecen en esta solicitud:

class TestFeedbackContract:
    def test_has_sentiment(self):
        ...
class TestTicketContract:
    def test_has_priority_level(self):
        ...

El principio

Los ejemplos JSON en el prompt son los siguientes Especificación. Los accesos de campo en el código son los Aplicación. La prueba combina ambos.

┌─────────────┐     ┌──────────────┐     ┌─────────────┐
│ prompt.md   │────▶│  test.py     │◀────│  app.py     │
│ (JSON-Spec) │     │ (Vertrag)    │     │ (liest JSON)│
└─────────────┘     └──────────────┘     └─────────────┘

Si el indicador cambia, la prueba falla. Si el código lee otros campos, se actualiza la prueba, lo que obliga a comprobar el indicador. El contrato permanece sincronizado.

Lo que NO se comprueba

Este patrón valida Estructura, no Calidad. Responde:

  • ¿Muestra el indicador al LLM qué campos debe devolver? Sí/No.
  • ¿Coinciden estos campos con lo que analiza el código? Sí/No.
  • ¿Son correctos los tipos (String, Int, List)? Sí/No.
  • ¿Son válidos los valores enum („listo“ en lugar de „completo“)? Sí/no.

No responde:

  • ¿Sigue el LLM la indicación de forma fiable? (Esto requiere pruebas de integración).
  • ¿Está bien escrito? (Para eso hace falta gente).
  • ¿Alucina el LLM los valores de los campos? (Esto requiere validación de salida en el código).

Pero la deriva estructural es la causa número uno de los errores de producción relacionados con la prontitud, y este patrón lo atrapa sin coste y sin latencia. Se ejecuta en CI, con cada commit, antes de cada despliegue.

Pruébalo

  1. Output Format escriba en sus archivos prompt con ejemplos JSON
  2. _extract_json_blocks() implementar (15 líneas, simplemente copiar de arriba)
  3. Compruebe que los campos que lee su código existen en los ejemplos
  4. Integrar en CI

Este es el patrón completo. Sin framework, sin librerías, sin dependencias. Sólo Regex, JSON y pytest.

¿Le ha resultado útil esta página?

Gracias por sus comentarios.

¿Podría darme su opinión? (anónimo)

Desarrollamos software de inteligencia artificial para empresas y evitamos deliberadamente los molestos banners publicitarios. A través de nuestros artículos, documentamos temas que nos ocupan e interesan y también financian nuestro pan de cada día.

Como nuestro contenido es gratuito, sus comentarios son nuestro elogio.

Cada autor lee personalmente tus comentarios anónimos, aunque la IA podría automatizarlos, e integra las sugerencias constructivas directamente en la siguiente revisión o las utiliza como inspiración para el siguiente artículo.



    </artículo
    es_ESES