Construcción de frameworks de evaluación para aplicaciones LLM
Diseña pipelines de evaluación para aplicaciones con LLM: métricas automáticas, evaluación humana, regresión, versionado de prompts y monitorización.

No puedes lanzar una funcionalidad con LLM sin saber si funciona. A diferencia del software tradicional, donde un test pasa o falla, las salidas de los LLM existen en un espectro de calidad. El mismo prompt puede producir una respuesta excelente en un minuto y una mediocre al siguiente. Sin una evaluación sistemática, estás lanzando una funcionalidad cuya calidad es un cara o cruz.
Los frameworks de evaluación para aplicaciones LLM necesitan métricas automatizadas para un feedback rápido, evaluación humana para valorar matices de calidad y pruebas de regresión para detectar degradación cuando cambian los prompts o los modelos.
Definición de las dimensiones de evaluación
Distintas aplicaciones necesitan distintas dimensiones de calidad. Un resumidor necesita fidelidad y concisión. Un generador de código necesita corrección y seguridad. Define estas dimensiones antes de escribir una sola función de evaluación.
// ❌ Single-score evaluation — tells you nothing actionable
function evaluate(output: string): number {
// "Quality score" from 0 to 1
return someBlackBoxScore(output);
}
// Is a 0.7 good? Bad? Good summary but wrong facts?// ✅ Multi-dimensional evaluation framework
interface EvalDimension {
name: string;
description: string;
scorer: (input: EvalInput) => Promise<DimensionScore>;
weight: number;
threshold: number; // Minimum acceptable score
}
interface EvalInput {
prompt: string;
context?: string;
expectedOutput?: string;
actualOutput: string;
metadata: Record<string, unknown>;
}
interface DimensionScore {
dimension: string;
score: number; // 0.0 to 1.0
reasoning: string;
evidence: string[];
}
interface EvalResult {
inputId: string;
scores: DimensionScore[];
overallScore: number;
passed: boolean;
failedDimensions: string[];
}
class EvaluationFramework {
private dimensions: EvalDimension[] = [];
addDimension(dimension: EvalDimension): void {
this.dimensions.push(dimension);
}
async evaluate(input: EvalInput): Promise<EvalResult> {
const scores: DimensionScore[] = [];
const failedDimensions: string[] = [];
for (const dim of this.dimensions) {
const score = await dim.scorer(input);
scores.push(score);
if (score.score < dim.threshold) {
failedDimensions.push(dim.name);
}
}
const totalWeight = this.dimensions.reduce(
(sum, d) => sum + d.weight,
0
);
const overallScore = scores.reduce((sum, score, i) => {
return sum + score.score * this.dimensions[i].weight;
}, 0) / totalWeight;
return {
inputId: input.metadata.id as string ?? "unknown",
scores,
overallScore,
passed: failedDimensions.length === 0,
failedDimensions,
};
}
}Funciones de puntuación automatizadas
Algunas dimensiones de calidad se pueden evaluar automáticamente. La coherencia factual, el cumplimiento de formato y la detección de toxicidad no requieren juicio humano en cada evaluación.
// Factual consistency: check if output contradicts the source
async function scoreFactualConsistency(
input: EvalInput
): Promise<DimensionScore> {
if (!input.context) {
return {
dimension: "factual_consistency",
score: 1.0,
reasoning: "No source context to check against",
evidence: [],
};
}
// Extract claims from the output
const claims = extractClaims(input.actualOutput);
const supportedClaims: string[] = [];
const unsupportedClaims: string[] = [];
for (const claim of claims) {
const isSupported = await checkClaimAgainstSource(
claim,
input.context
);
if (isSupported) {
supportedClaims.push(claim);
} else {
unsupportedClaims.push(claim);
}
}
const score = claims.length > 0
? supportedClaims.length / claims.length
: 1.0;
return {
dimension: "factual_consistency",
score,
reasoning:
`${supportedClaims.length}/${claims.length} claims ` +
`supported by source`,
evidence: unsupportedClaims.map(
(c) => `Unsupported: "${c}"`
),
};
}
// Format compliance: check structural requirements
function scoreFormatCompliance(
input: EvalInput
): Promise<DimensionScore> {
const checks: { name: string; passed: boolean }[] = [];
const output = input.actualOutput;
const rules = input.metadata.formatRules as FormatRule[];
for (const rule of rules ?? []) {
switch (rule.type) {
case "max_length":
checks.push({
name: `Max length ${rule.value}`,
passed: output.length <= (rule.value as number),
});
break;
case "contains_section":
checks.push({
name: `Contains "${rule.value}"`,
passed: output.includes(rule.value as string),
});
break;
case "json_valid":
try {
JSON.parse(output);
checks.push({ name: "Valid JSON", passed: true });
} catch {
checks.push({ name: "Valid JSON", passed: false });
}
break;
}
}
const passed = checks.filter((c) => c.passed).length;
const score = checks.length > 0 ? passed / checks.length : 1.0;
return Promise.resolve({
dimension: "format_compliance",
score,
reasoning: `${passed}/${checks.length} format checks passed`,
evidence: checks
.filter((c) => !c.passed)
.map((c) => `Failed: ${c.name}`),
});
}
interface FormatRule {
type: "max_length" | "contains_section" | "json_valid";
value: string | number;
}
function extractClaims(text: string): string[] {
// Split into sentences and filter for factual assertions
return text
.split(/[.!?]+/)
.map((s) => s.trim())
.filter((s) => s.length > 20);
}
async function checkClaimAgainstSource(
claim: string,
source: string
): Promise<boolean> {
// Semantic similarity check
const similarity = computeCosineSimilarity(claim, source);
return similarity > 0.6;
}
function computeCosineSimilarity(a: string, b: string): number {
// Placeholder for embedding-based similarity
return 0.8;
}Protocolo de evaluación humana
Las métricas automatizadas tienen puntos ciegos. La evaluación humana detecta problemas de calidad que ninguna métrica puede captar: tono, utilidad, coherencia y si la respuesta realmente contesta la pregunta.
interface HumanEvalTask {
id: string;
input: EvalInput;
dimensions: HumanEvalDimension[];
assignedTo: string;
status: "pending" | "in_progress" | "completed";
results?: HumanEvalResult;
}
interface HumanEvalDimension {
name: string;
description: string;
scale: { min: number; max: number; labels: string[] };
}
interface HumanEvalResult {
evaluatorId: string;
scores: Map<string, number>;
freeformFeedback: string;
completedAt: Date;
timeSpentSeconds: number;
}
class HumanEvalPipeline {
private tasks: Map<string, HumanEvalTask> = new Map();
createSampledBatch(
allOutputs: EvalInput[],
sampleSize: number,
strategyFn: (outputs: EvalInput[]) => EvalInput[]
): HumanEvalTask[] {
// Sample strategically — not randomly
const sampled = strategyFn(allOutputs).slice(0, sampleSize);
return sampled.map((input) => {
const task: HumanEvalTask = {
id: `eval-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`,
input,
dimensions: [
{
name: "Helpfulness",
description:
"Does the response address the user's actual need?",
scale: {
min: 1,
max: 5,
labels: [
"Not helpful",
"Slightly helpful",
"Moderately helpful",
"Helpful",
"Very helpful",
],
},
},
{
name: "Accuracy",
description:
"Are the factual claims in the response correct?",
scale: {
min: 1,
max: 5,
labels: [
"Incorrect",
"Mostly incorrect",
"Mixed",
"Mostly correct",
"Fully correct",
],
},
},
],
assignedTo: "",
status: "pending",
};
this.tasks.set(task.id, task);
return task;
});
}
}
// Sampling strategy: focus on edge cases and low-confidence outputs
function edgeCaseSampling(outputs: EvalInput[]): EvalInput[] {
return outputs.sort((a, b) => {
// Prioritize longer outputs (more room for errors),
// outputs with low automated scores, and diverse prompts
const aLength = a.actualOutput.length;
const bLength = b.actualOutput.length;
return bLength - aLength;
});
}Pruebas de regresión entre versiones de prompts
Cuando cambias un prompt, necesitas saber si el cambio mejoró o degradó la calidad en tu conjunto de pruebas. Esto requiere evaluación versionada.
interface PromptVersion {
version: string;
template: string;
changedAt: Date;
changelog: string;
}
interface RegressionReport {
baselineVersion: string;
candidateVersion: string;
testSetSize: number;
improvements: DimensionComparison[];
regressions: DimensionComparison[];
neutral: DimensionComparison[];
recommendation: "ship" | "investigate" | "rollback";
}
interface DimensionComparison {
dimension: string;
baselineAvg: number;
candidateAvg: number;
delta: number;
significant: boolean;
}
async function runRegressionTest(
baseline: PromptVersion,
candidate: PromptVersion,
testSet: EvalInput[],
framework: EvaluationFramework
): Promise<RegressionReport> {
const baselineResults: EvalResult[] = [];
const candidateResults: EvalResult[] = [];
for (const testCase of testSet) {
const baselineOutput = await generateWithPrompt(
baseline.template,
testCase
);
const candidateOutput = await generateWithPrompt(
candidate.template,
testCase
);
baselineResults.push(
await framework.evaluate({
...testCase,
actualOutput: baselineOutput,
})
);
candidateResults.push(
await framework.evaluate({
...testCase,
actualOutput: candidateOutput,
})
);
}
return compareResults(
baseline.version,
candidate.version,
baselineResults,
candidateResults
);
}
function compareResults(
baselineVersion: string,
candidateVersion: string,
baseline: EvalResult[],
candidate: EvalResult[]
): RegressionReport {
const dimensions = new Set(
baseline.flatMap((r) => r.scores.map((s) => s.dimension))
);
const comparisons: DimensionComparison[] = [];
for (const dim of dimensions) {
const baseScores = baseline.map(
(r) => r.scores.find((s) => s.dimension === dim)?.score ?? 0
);
const candScores = candidate.map(
(r) => r.scores.find((s) => s.dimension === dim)?.score ?? 0
);
const baseAvg =
baseScores.reduce((a, b) => a + b, 0) / baseScores.length;
const candAvg =
candScores.reduce((a, b) => a + b, 0) / candScores.length;
const delta = candAvg - baseAvg;
comparisons.push({
dimension: dim,
baselineAvg: baseAvg,
candidateAvg: candAvg,
delta,
significant: Math.abs(delta) > 0.05,
});
}
const regressions = comparisons.filter(
(c) => c.significant && c.delta < 0
);
const improvements = comparisons.filter(
(c) => c.significant && c.delta > 0
);
return {
baselineVersion,
candidateVersion,
testSetSize: baseline.length,
improvements,
regressions,
neutral: comparisons.filter((c) => !c.significant),
recommendation:
regressions.length > 0
? "investigate"
: improvements.length > 0
? "ship"
: "investigate",
};
}
async function generateWithPrompt(
template: string,
input: EvalInput
): Promise<string> {
// Placeholder for LLM call
return "";
}Puntos clave
La evaluación de LLM requiere puntuación multidimensional porque un único número de calidad no te dice nada accionable: divide la evaluación en dimensiones específicas como coherencia factual, cumplimiento de formato, utilidad y precisión. Automatiza lo que puedas: las comprobaciones de formato, la verificación de afirmaciones contra los documentos fuente y la detección de toxicidad ofrecen feedback rápido sin esfuerzo humano. Muestrea estratégicamente para la evaluación humana: céntrate en casos límite, salidas largas y predicciones de baja confianza en lugar de un muestreo aleatorio. Versiona tus prompts y ejecuta pruebas de regresión contra un conjunto de pruebas fijo cada vez que los cambies, comparando puntuaciones dimensión por dimensión para detectar degradación. Define umbrales de calidad por dimensión que condicionen el despliegue: una respuesta bien formateada pero factualmente incorrecta no debería lanzarse. El framework de evaluación no es una configuración de una sola vez: evoluciona a medida que descubres nuevos modos de fallo en producción y los codificas como nuevos casos de prueba y dimensiones de puntuación.


