L
LexFlow · LangSmith Observability
Proyecto: lexflow-production · GPT-4o Azure · Implementado por CULTIVA IA
● Producción activa lexflow-dev 14 jun 2026
Resumen
Traces (1.240)
Evaluación
Dataset QA
Implementación
Traces hoy
1.240
▲ +18% vs ayer
Latencia media
2.4s
▼ −0.3s vs semana pasada
Tasa error
1.2%
▼ −0.5% objetivo ≤2%
Correctness score
0.91
▲ +0.03 vs gpt4o-v1
Coste tokens hoy
$4.32
~$0.0035 por consulta
Traces por hora — últimas 24h lexflow-production
00:0006:0012:0018:0023:59
Alertas recientes
Timeout Azure OpenAI — 3 traces fallidos consecutivos (clausulas_extraccion)
14:22
Latencia >5s detectada — run_id: f8a2c9d — user: empresa_abc
11:47
Experiment gpt4o-v2 completado — correctness 0.91 > threshold 0.88 ✓
09:15
Resultados de evaluación — Experimento gpt4o-v2 Dataset: lexflow-clausulas-qa · 10 ejemplos
Correctness Score
0.91 ✓
Cambio de cláusulas penales
1.0
Plazo de preaviso rescisión
1.0
Confidencialidad post-contrato
1.0
Propiedad intelectual obras creadas
0.9
Indemnizaciones despido improcedente
0.9
Jurisdicción aplicable NDA
0.9
Exención de responsabilidad fuerza mayor
0.8
Contrato prestación servicios IT
1.0
Garantías SLA incumplimiento
0.9
Contrato laboral temporal obra y servicio
0.7
Conciseness Score
0.84 ✓
Cambio de cláusulas penales
0.9
Plazo de preaviso rescisión
0.9
Confidencialidad post-contrato
1.0
Propiedad intelectual obras creadas
0.8
Indemnizaciones despido improcedente
0.7
Jurisdicción aplicable NDA
0.9
Exención de responsabilidad fuerza mayor
0.7
Contrato prestación servicios IT
0.8
Garantías SLA incumplimiento
0.9
Contrato laboral temporal obra y servicio
0.8
Traces recientes — Producción lexflow-production · últimos 10
Run ID Entrada (truncada) Estado Latencia Tokens Coste Tags
f8a2c9d1 ¿Qué pasa si el proveedor incumple el SLA del 99.9%? ✓ success 6.2s ⚠ 1,842 $0.0068 beta-usergpt4o
a3e8f120 ¿Cuánto tiempo debe durar la cláusula de no competencia? ✓ success 2.1s 1,204 $0.0044 produccionnda
9c7b4e52 Necesito incluir una cláusula arbitral internacional, ¿cómo? ✕ error 318 $0.0009 enterprise
b1d5a7c3 ¿Qué cláusulas son obligatorias en un contrato de arrendamiento? ✓ success 1.9s 987 $0.0036 produccion
d4c2e891 ¿Cómo redactar la cláusula de confidencialidad para un NDA? ✓ success 2.4s 1,531 $0.0056 ndagpt4o
e6f90234 ¿Cuál es el preaviso mínimo para rescisión en contratos de servicios? ✓ success 4.1s ! 1,103 $0.0041 produccion
c5a1b678 Genera un contrato laboral indefinido para desarrollador senior ✓ success 2.8s 2,341 $0.0087 laboralenterprise
77de32f0 ¿Cuál es la diferencia entre penalización y cláusula penal? ✓ success 1.6s 844 $0.0031 produccion
2a4f9c1e ¿Se puede limitar la responsabilidad civil en B2B España? ✓ success 2.2s 1,067 $0.0039 produccionb2b
8b6e4d90 ¿Cómo se regula la subcontratación en contratos públicos? ✕ error 0 $0.0000 produccion
Implementación — Código entregado a LexFlow
lexflow_tracing.py
Python 3.11 · LangChain 0.2 · LangSmith
# lexflow_tracing.py — Tracing y evaluación LangSmith para LexFlow
# Implementado por CULTIVA IA · 14 jun 2026

import os
from langsmith import Client, traceable, evaluate
from langchain_openai import AzureChatOpenAI
from langchain_core.prompts import ChatPromptTemplate

# ── Configuración de entorno ─────────────────────────────
os.environ["LANGCHAIN_TRACING_V2"] = "true"
os.environ["LANGCHAIN_API_KEY"]   = "lsv2_pt_lexflow_xxxxxxxxxxxx"
os.environ["LANGCHAIN_PROJECT"]   = "lexflow-production"

client = Client()

# ── RAG chain con tracing automático ─────────────────────
llm = AzureChatOpenAI(
    deployment_name="gpt-4o",
    temperature=0,
    metadata={"service": "clausula-qa", "version": "v2"}
)

prompt = ChatPromptTemplate.from_template("""
Eres un asistente legal experto en derecho mercantil español.
Responde de forma concisa (máximo 80 palabras).

Contexto de la cláusula:
{context}

Pregunta: {question}
""")

chain = prompt | llm

# ── Función principal con @traceable ─────────────────────
@traceable(
    name="lexflow_clausula_qa",
    tags=["produccion", "gpt4o"],
    metadata={"environment": "production"}
)
def answer_legal_question(question: str, user_id: str) -> dict:
    context = retrieve_relevant_clauses(question)  # RAG retrieval
    result  = chain.invoke(
        {"question": question, "context": context},
        config={"metadata": {"user_id": user_id}}
    )
    return {"answer": result.content, "user_id": user_id}

# ── Dataset de evaluación ─────────────────────────────────
dataset = client.create_dataset(
    "lexflow-clausulas-qa",
    description="10 preguntas reales de clientes LexFlow con respuesta esperada"
)

client.create_examples(
    inputs=[
        {"question": "¿Cómo se redacta una cláusula de no competencia efectiva?"},
        {"question": "¿Qué cláusulas son obligatorias en un NDA en España?"},
        {"question": "¿Cuál es el preaviso mínimo para rescisión de contrato de servicios?"},
    ],
    outputs=[
        {"answer": "Debe incluir ámbito geográfico, temporal (máx. 2 años) y actividades prohibidas"},
        {"answer": "Definición de información confidencial, obligaciones de confidencialidad, excepciones y plazo"},
        {"answer": "El CC art. 1594 no fija plazo mínimo; se recomienda 30 días pactados expresamente"},
    ],
    dataset_id=dataset.id
)

# ── Evaluadores personalizados ────────────────────────────
def correctness(run, example) -> dict:
    pred     = run.outputs["answer"].lower()
    expected = example.outputs["answer"].lower()
    keywords = [w for w in expected.split() if len(w) > 5]
    hits     = sum(1 for k in keywords if k in pred)
    return {"key": "correctness", "score": min(1.0, hits / max(len(keywords), 1))}

def conciseness(run, example) -> dict:
    words = len(run.outputs["answer"].split())
    score = 1.0 if words <= 80 else max(0, 1.0 - (words - 80) / 160)
    return {"key": "conciseness", "score": round(score, 2)}

# ── Ejecutar evaluación ───────────────────────────────────
def app_wrapper(inputs: dict) -> dict:
    return answer_legal_question(inputs["question"], user_id="eval-run")

results = evaluate(
    app_wrapper,
    data="lexflow-clausulas-qa",
    evaluators=[correctness, conciseness],
    experiment_prefix="gpt4o-v2",
    max_concurrency=4
)
tests/test_quality.py
pytest · GitHub Actions
# tests/test_quality.py — CI quality gate para LexFlow
# Falla el PR si correctness < 0.85 o conciseness < 0.80

import pytest
from langsmith import evaluate
from lexflow_tracing import app_wrapper, correctness, conciseness

def test_clausula_qa_quality():
    results = evaluate(
        app_wrapper,
        data="lexflow-clausulas-qa",
        evaluators=[correctness, conciseness],
        experiment_prefix="ci-test",
    )
    scores = {}
    for r in results:
        for ev in r["evaluation_results"]["results"]:
            scores.setdefault(ev.key, []).append(ev.score)

    avg_correctness  = sum(scores["correctness"])  / len(scores["correctness"])
    avg_conciseness  = sum(scores["conciseness"])  / len(scores["conciseness"])

    assert avg_correctness >= 0.85, \
        f"Correctness cayó a {avg_correctness:.2f} (threshold: 0.85)"
    assert avg_conciseness >= 0.80, \
        f"Conciseness cayó a {avg_conciseness:.2f} (threshold: 0.80)"

    print(f"✓ Correctness: {avg_correctness:.2f} | Conciseness: {avg_conciseness:.2f}")
Resultado CI/CD — GitHub Actions PR #47 · feat/gpt4o-v2-prompt · Rama: main
test_clausula_qa_quality::correctness
0.91
≥ 0.85 ✓
test_clausula_qa_quality::conciseness
0.84
≥ 0.80 ✓
test_clausula_qa_quality::latencia_media
2.4s
≤ 3.0s ✓
test_clausula_qa_quality::tasa_error
1.2%
≤ 2.0% ✓
scripts/monitor_production.py
Cron diario · Railway
# Monitoreo diario: errores + latencia alta + resumen de coste
from langsmith import Client; client = Client()

runs = list(client.list_runs(
    project_name="lexflow-production",
    filter='or(eq(status,"error"),gt(latency,5))',
    limit=50
))

errors    = [r for r in runs if r.status == "error"]
slow_runs = [r for r in runs if r.total_time and r.total_time > 5]
all_today = list(client.list_runs(project_name="lexflow-production", limit=1000))

total_tokens  = sum(r.total_tokens or 0 for r in all_today)
avg_latency   = sum(r.total_time or 0 for r in all_today) / max(len(all_today), 1)

print(f"Errores hoy: {len(errors)} | Lentos: {len(slow_runs)} | "
      f"Tokens: {total_tokens:,} | Latencia media: {avg_latency:.1f}s")
# Output → Errores hoy: 15 | Lentos: 8 | Tokens: 1,284,310 | Latencia media: 2.4s