Ctrl K
ComunidadGuías…

docs/observability/EVALUATION_FRAMEWORK.md

Rama master · réplica de lectura del repositorio original.

Archivo original ↗Preguntar a KiBot
# AgentOS Evaluation Framework

## Overview

The evaluation framework runs test cases through an agent function and scores the answers:

- **Test cases** with expected outputs and weighted criteria
- **Runs** in batches of a configurable size, with a per-attempt timeout and retries
- **Scorers**, built in or registered, and an LLM judge
- **Run comparison** over four aggregate metrics
- **Reports** in JSON, Markdown or HTML

Runs live in the `Evaluator` instance's memory.

## Quick Start

```typescript
import { Evaluator } from '@framers/agentos';
import type { EvalTestCase } from '@framers/agentos';

const evaluator = new Evaluator();

// Define test cases
const testCases: EvalTestCase[] = [
  {
    id: 'math-1',
    name: 'Basic Addition',
    input: 'What is 2 + 2?',
    expectedOutput: '4',
    criteria: [
      { name: 'correctness', description: 'Contains correct answer', weight: 1, scorer: 'contains' }
    ]
  },
  {
    id: 'greeting-1',
    name: 'Greeting Response',
    input: 'Hello!',
    expectedOutput: 'Hello! How can I help you today?',
    criteria: [
      { name: 'politeness', description: 'Polite response', weight: 1, scorer: 'contains' },
      { name: 'similarity', description: 'Similar to expected', weight: 2, scorer: 'levenshtein' }
    ]
  }
];

// Define your agent function
async function myAgent(input: string): Promise<string> {
  // Your GMI or agent logic here
  return `Response to: ${input}`;
}

// Run evaluation
const run = await evaluator.runEvaluation(
  'My Agent Evaluation v1.0',
  testCases,
  myAgent,
  { concurrency: 5, timeoutMs: 30000 }
);

// Generate report
const report = await evaluator.generateReport(run.runId, 'markdown');
console.log(report);
```

## Built-in Scorers

The framework includes several built-in scorers:

| Scorer | Description | Best For |
|--------|-------------|----------|
| `exact_match` | Returns 1 if the strings are equal after trimming, ignoring case | Precise answers |
| `contains` | Returns 1 if actual contains expected, ignoring case | Checking for key terms |
| `levenshtein` | 1 minus the normalized character edit distance (0-1), ignoring case | Typo tolerance |
| `semantic_similarity` | Word overlap (0-1): shared words longer than 2 characters over the larger set; no embeddings | Shared vocabulary |
| `bleu` | Unigram precision with a brevity penalty (0-1) | Translation quality |
| `rouge` | ROUGE-L F1 score | Summarization quality |
| `word_error_rate` | 1 less the word error rate, never below 0 | Speech-to-text transcripts |

### Using Scorers Directly

```typescript
// Score individual outputs
const score = await evaluator.score('levenshtein', 'actual output', 'expected output');
console.log(`Similarity: ${(score * 100).toFixed(1)}%`);
```

### Word Error Rate

`word_error_rate` scores a transcript against a reference transcript by its word error rate. `wordErrorRate()` returns the counts behind the score and `normalizeTranscript()` the words it compares; both are exported beside `Evaluator`, from `@framers/agentos` and from `@framers/agentos/safety/evaluation`.

Both texts go through one normalisation before they are counted, so that measurements of different speech-to-text systems agree:

- Unicode's composed form (NFC), so `é` written as one character and `e` followed by a combining accent are the same letter.
- Lower case.
- Letters, digits and a word's inner apostrophe are kept (`cat's`, `don't`), with the combining marks on them, such as an accent or a vowel sign, so `कि` and `क` are different words. The typographic apostrophes `’` (U+2019), `‘` (U+2018) and `ʼ` (U+02BC) count as `'`, so `don’t` and `don't` are the same word.
- Other punctuation and symbols are dropped, with any combining mark on them (an emoji's variation selector, for one), and so are bracketed marks such as `[laughter]` and the filler words `um`, `uh`, `er`, `ah`, `hmm` and `mm`.
- Numbers are left as written, so `3` and `three` are different words.

The counts are the substitutions, deletions and insertions of a word-level edit distance from the reference to the transcript: a deletion is a reference word the transcript lacks, an insertion a transcript word the reference lacks. The rate is their sum over the reference's word count, taken as 1 when the reference is empty, so insertions can take it above 1. The scorer gives `1 - rate`, never below 0, and 0 when a test case has no expected output.

```typescript
import { Evaluator, normalizeTranscript, wordErrorRate } from '@framers/agentos';

normalizeTranscript("Um, so the [laughter] meeting's at 3.");
// ['so', 'the', "meeting's", 'at', '3']

wordErrorRate('so the meeting is at three', "Um, so the [laughter] meeting's at 3.");
// { substitutions: 2, deletions: 0, insertions: 1, referenceWords: 5, rate: 0.6 }

const evaluator = new Evaluator();
await evaluator.score('word_error_rate', 'so the meeting is at three', "so the meeting's at 3");
// 0.4
```

## Custom Scorers

Register custom scorers for domain-specific evaluation:

```typescript
// A scorer is (actual, expected, references, metadata) => number | Promise<number>
evaluator.registerScorer('json_valid', (actual) => {
  try {
    JSON.parse(actual);
    return 1;
  } catch {
    return 0;
  }
});

// Use in test cases
const testCase: EvalTestCase = {
  id: 'json-1',
  name: 'JSON Output',
  input: 'Generate JSON for user profile',
  expectedOutput: '{"name": "John", "age": 30}',
  criteria: [
    { name: 'valid_json', description: 'Output is valid JSON', weight: 1, scorer: 'json_valid' },
    { name: 'similar', description: 'Similar structure', weight: 2, scorer: 'levenshtein' }
  ]
};
```

## Configuration Options

```typescript
interface EvalConfig {
  concurrency?: number;      // Cases per batch (default: 3); a batch waits for its slowest case
  timeoutMs?: number;        // Per attempt (default: 60000); the agent call is not cancelled
  retries?: number;          // Extra attempts after an error or a timeout (default: 1)
  continueOnError?: boolean; // Declared, not read: a failed case scores 0 and the run goes on
  thresholds?: {
    pass?: number;           // Minimum weighted score to pass (default: 0.7)
    warn?: number;           // Declared, not read (default: 0.5)
  };
  customScorers?: Record<string, ScorerFunction>; // Declared, not read: use registerScorer()
}
```

## Test Case Structure

```typescript
interface EvalTestCase {
  id: string;                    // Unique identifier
  name: string;                  // Human-readable name
  category?: string;             // A label (the aggregates do not group by it)
  input: string;                 // Input to the agent function
  expectedOutput?: string;       // Reference answer
  referenceOutputs?: string[];   // Extra references (bleu, semantic_similarity)
  context?: string;              // Second argument of the agent function
  expectedToolCalls?: Array<{ toolName: string; args?: Record<string, unknown> }>; // Declared, not scored
  criteria?: EvalCriteria[];     // { name, description, weight, scorer, threshold? }; default: levenshtein
  metadata?: Record<string, unknown>; // Fourth argument of every scorer
}
```

A case's score is the weighted mean of its criteria; a criterion's `threshold` is not read, and a criterion naming an unknown scorer is skipped.

## Comparing Runs

Track improvements across versions:

```typescript
const runV1 = await evaluator.runEvaluation('v1.0', testCases, agentV1);
const runV2 = await evaluator.runEvaluation('v2.0', testCases, agentV2);

const comparison = await evaluator.compareRuns(runV1.runId, runV2.runId);

console.log('Improvements:', comparison.summary.improved);
console.log('Regressions:', comparison.summary.regressed);

for (const metric of comparison.metrics) {
  console.log(`${metric.name}: ${metric.delta.toFixed(2)} (${metric.percentChange.toFixed(1)}%)`);
}
```

The comparison covers `passRate`, `avgScore`, `avgLatencyMs` and `p95LatencyMs`. A metric counts as `improved` when the second run's value is higher, for the latencies too, so a slower second run reports its latencies as improvements. Both runs must belong to the same `Evaluator` instance.

## Aggregate Metrics

Each evaluation run includes aggregate metrics:

```typescript
interface AggregateMetrics {
  totalTests: number;
  passedTests: number;
  failedTests: number;
  passRate: number;        // 0-1
  avgScore: number;        // 0-1
  scoreStdDev: number;     // Standard deviation
  avgLatencyMs: number;
  p50LatencyMs: number;    // Median
  p95LatencyMs: number;    // 95th percentile
  p99LatencyMs: number;    // 99th percentile
  totalTokens: number;     // 0: runEvaluation() records no token usage
  totalCostUsd: number;    // 0: runEvaluation() records no cost
  byCategory?: Record<string, { passRate: number; avgScore: number; count: number }>; // Not filled
}
```

## Integration with GMI

Evaluate GMI responses through the runtime. Each test case runs on a session of its own, so one case's history does not reach the next:

```typescript
import { AgentOS, AgentOSResponseChunkType, BUILT_IN_PERSONAS } from '@framers/agentos';

const agentos = await AgentOS.create({ personas: BUILT_IN_PERSONAS });
let caseNumber = 0;

// Create wrapper function for evaluation
async function gmiAgent(input: string): Promise<string> {
  let response = '';
  for await (const chunk of agentos.processRequest({
    userId: 'eval-user',
    sessionId: `eval-case-${++caseNumber}`,
    selectedPersonaId: 'v_researcher',
    textInput: input,
  })) {
    if (chunk.type === AgentOSResponseChunkType.TEXT_DELTA) {
      response += chunk.textDelta;
    }
  }
  return response;
}

const run = await evaluator.runEvaluation('GMI Evaluation', testCases, gmiAgent);
```

## Best Practices

1. **Start with representative test cases** - Cover common user queries and edge cases
2. **Use multiple criteria** - Combine exactness and semantic measures
3. **Weight criteria appropriately** - Prioritize what matters most
4. **Track baselines** - Store run IDs to compare against
5. **Automate in CI/CD** - Run evaluations on each deployment
6. **Review failures** - Read the failing cases' `actualOutput` and `error`

## Report Formats

```typescript
import { writeFileSync } from 'node:fs';

// JSON: the whole EvalRun
const data = JSON.parse(await evaluator.generateReport(run.runId, 'json'));

// Markdown: summary, latency percentiles, one line per case
const markdown = await evaluator.generateReport(run.runId, 'markdown');

// HTML: the same content as tables
writeFileSync('report.html', await evaluator.generateReport(run.runId, 'html'));
```

## LLM-as-Judge

`LLMJudge` grades with a model through an `AIModelProviderManager`. Without `modelId`, it uses the judge resolver's model (OpenAI `gpt-5.6`, or `AGENTOS_JUDGE_PROVIDER` / `AGENTOS_JUDGE_MODEL` / `AGENTOS_JUDGE_EFFORT`):

```typescript
import { LLMJudge, CRITERIA_PRESETS } from '@framers/agentos';

const judge = new LLMJudge({
  llmProvider: aiModelProviderManager,
  modelId: 'gpt-4-turbo',
  temperature: 0.1,
});

// Single judgment
const result = await judge.judge(
  'What is photosynthesis?',
  'Photosynthesis is how plants make food from sunlight.',
  'Photosynthesis is the process by which plants convert light energy into chemical energy.'
);

console.log(`Score: ${result.score}`);
console.log(`Reasoning: ${result.reasoning}`);
console.log(`Feedback: ${result.feedback.join(', ')}`);

// Use preset criteria
const codeResult = await judge.judge(
  'Write a function to reverse a string',
  actualCode,
  expectedCode,
  CRITERIA_PRESETS.codeGeneration
);

// Compare two outputs
const comparison = await judge.compare(
  'Summarize this article',
  summaryA,
  summaryB,
  CRITERIA_PRESETS.summarization
);
console.log(`Winner: ${comparison.winner}`);

// Register as a scorer; it reads the input from the test case's metadata.input
evaluator.registerScorer('llm_judge', judge.createScorer());
```

- `judge()` asks for a JSON answer at `temperature` 0.1. A provider error or an answer that is not JSON scores `errorScore` (default 0); JSON without `overallScore` scores 0.5.
- `compare()` judges the two outputs separately and calls a tie when their scores differ by less than 0.05.
- `batchJudge(items, criteria, concurrency = 3)` returns the judgments in the order they finish, not in the order of `items`.

### Available Criteria Presets

| Preset | Use Case |
|--------|----------|
| `codeGeneration` | Evaluate generated code |
| `summarization` | Evaluate summaries |
| `questionAnswering` | Evaluate Q&A responses |
| `creativeWriting` | Evaluate creative content |
| `safety` | Evaluate for harmlessness |

---

## References

### LLM-as-judge methodology

- Zheng, L., Chiang, W.-L., Sheng, Y., Zhuang, S., Wu, Z., Zhuang, Y., Lin, Z., Li, Z., Li, D., Xing, E. P., Zhang, H., Gonzalez, J. E., & Stoica, I. (2023). [*Judging LLM-as-a-judge with MT-Bench and chatbot arena.*](https://arxiv.org/abs/2306.05685) NeurIPS 2023. — LLM-as-judge reliability, position bias and self-enhancement bias. `LLMJudge.compare()` judges each output on its own, so it shows no answer to the judge in a second position.
- Liu, Y., Iter, D., Xu, Y., Wang, S., Xu, R., & Zhu, C. (2023). [*G-Eval: NLG evaluation using GPT-4 with better human alignment.*](https://arxiv.org/abs/2303.16634) EMNLP 2023. — Criteria-based scoring with an LLM, the form `LLMJudge` uses (per-criterion scores and an overall score).
- Chen, Y., Wang, R., Jiang, H., Shi, S., & Xu, R.-M. (2023). [*Exploring the use of large language models for reference-free text quality evaluation: An empirical study.*](https://arxiv.org/abs/2304.00723) AACL 2023. — Reference-free evaluation; `judge()` takes the expected output as optional.

### Human-LLM agreement + evaluator calibration

- Wang, Y., Yu, Z., Zeng, Z., Yang, L., Wang, C., Chen, H., Jiang, C., Xie, R., Wang, J., Xie, X., Ye, W., Zhang, S., & Zhang, Y. (2023). [*PandaLM: An automatic evaluation benchmark for LLM instruction tuning optimization.*](https://arxiv.org/abs/2306.05087) arXiv:2306.05087. — Evaluator-versus-human agreement.
- Bai, Y., Ying, J., Cao, Y., Lv, X., He, Y., Wang, X., Yu, J., Zeng, K., Xiao, Y., Lyu, H., Zhang, J., Li, J., & Hou, L. (2024). [*Benchmarking foundation models with language-model-as-an-examiner.*](https://arxiv.org/abs/2306.04181) NeurIPS 2024. — LLM-judge evaluation across many tasks.

### Bias + reliability concerns

- Wang, P., Li, L., Chen, L., Cai, Z., Zhu, D., Lin, B., Cao, Y., Liu, Q., Liu, T., & Sui, Z. (2023). [*Large language models are not fair evaluators.*](https://arxiv.org/abs/2305.17926) arXiv:2305.17926. — Position bias in pairwise LLM judges.

### Implementation references

- [`src/safety/evaluation/Evaluator.ts`](https://github.com/framerslab/agentos/blob/master/src/safety/evaluation/Evaluator.ts) — the evaluation harness
- [`src/safety/evaluation/LLMJudge.ts`](https://github.com/framerslab/agentos/blob/master/src/safety/evaluation/LLMJudge.ts) — the LLM judge and `CRITERIA_PRESETS`
- [`src/safety/evaluation/wordErrorRate.ts`](https://github.com/framerslab/agentos/blob/master/src/safety/evaluation/wordErrorRate.ts) — `wordErrorRate()` and `normalizeTranscript()`