Zum Inhalt

Agentic Evaluations

Evaluieren Sie Agenten, die Sie selbst ausführen, anhand ihrer Trajectories.

Führen Sie Ihren Agenten in seiner bestehenden Runtime aus, laden Sie die Trial-Ergebnisse hoch und lassen Sie elluminate die aufgezeichneten Trajectories prüfen und bewerten.

Was Agentic Evaluation ist

Wann Agentic Evaluations verwenden

Nutzen Sie diesen Workflow, wenn Ihr Agent mehrere LLM-Aufrufe ausführt, Tools verwendet oder Arbeit delegiert und Sie sowohl sein Vorgehen als auch seine finale Antwort bewerten möchten. Der Runner kann eigener Code, Harbor, LangChain, CrewAI, AutoGen oder ein anderes Framework sein.

Für einstufige Ausgaben oder Tool Calls, die elluminate selbst erzeugt, nutzen Sie den Leitfaden Tool Calling.

Kernkonzepte

  • Task — eine Arbeitseinheit mit eindeutigem Task-Namen.
  • Collection — die Menge der Tasks, üblicherweise mit einer task- und optionalen instruction-Column.
  • Trajectory (Trace) — das schrittweise Protokoll eines Runs: Messages, Tool Calls und Observations.
  • Criterion — eine YES/NO-Frage, die anhand einer Trajectory bewertet wird. Ein Criterion Set gilt für jeden Task; taskspezifische Criteria gelten nur für einen Task.
  • Overall Rating — die automatische YES/NO-Bewertung des gesamten Task-Erfolgs.
  • Experiment — verbindet Collection und Criterion Set und enthält hochgeladene Traces, Ratings und Metriken.

Das Trajectory-Format

Ihr Runner muss eine ATIF-Trajectory erzeugen (Agent Trajectory Interchange Format). elluminate hat keinen frameworkspezifischen Importer: Konvertieren Sie die Runner-Ausgabe in ATIF, verpacken Sie sie in ein Trial-Ergebnis und laden Sie sie über die UI oder das SDK hoch.

ATIF beschreibt einen bereits ausgeführten Run. Es unterscheidet sich von UCE (elluminate.uce/1), einer Conversation-Eingabe, die elluminate selbst ausführt; siehe Conversations.

UI-Walkthrough

Agentic Collection erstellen
Collections → New collection, mit ausgewähltem Agentic Collection Type.
  1. Agentic Collection erstellen. Wählen Sie in Collections → New collection den Type Agentic. Behalten Sie task als eindeutige Text-Column bei; sie identifiziert jeden Upload. instruction ist optional, wenn die Trajectory die Eingabe schon enthält. Agentic Experiments brauchen kein Prompt Template und generieren niemals automatisch Responses.
  2. Criterion Set erstellen. Legen Sie in der Criteria Library klare YES/NO-Fragen an, die sich aus einer Trajectory beantworten lassen. Für Prüfungen, die nur für einen Task gelten, verwenden Sie taskspezifische Criteria.
  3. Agentic Experiment erstellen. Wählen Sie in Experiments → New den Type Agentic und anschließend Collection und Criterion Set.
  4. Agent extern ausführen und pro Task eine ATIF-Trajectory sammeln.
  5. Traces hochladen — über die UI oder das SDK. Bei aktivierter Bewertung bewertet elluminate jedes Criterion gegen jede Trajectory.
  6. Ergebnisse in der UI ansehen.

Automatisches Overall Rating

Jedes Agentic Experiment ergänzt seine festgeschriebene Evaluationsdefinition um Overall Rating. Es bleibt von einem gleichnamigen Criterion im Criterion Set getrennt.

Agentic Experiment erstellen
Experiments → New: ein Agentic Experiment erstellen.

Tasks aus einem Harbor-Zip importieren

Um Harbor Task Folder zu importieren, öffnen Sie eine Agentic Collection und wählen Add taskUpload zip. Das Archiv darf Folder in beliebiger Tiefe enthalten; jeder Folder mit task.toml wird zu einem Task.

tasks/
├── fix-parser/
│   ├── task.toml
│   ├── instruction.md
│   └── criteria.toml
└── needs-doc/
    ├── task.toml
    └── instruction.md
Datei Pflicht Importiert als
task.toml ja Task-Identität und Task-Konfiguration. Wenn vorhanden, wird [task].name verwendet, sonst der Folder-Name.
instruction.md ja Die Task-Anweisung; sie darf nicht leer sein.
criteria.toml nein Criteria nur für diesen Task.
environment/, solution/, tests/ nein Textbasierte Task-Konfiguration, die für den Export erhalten bleibt.

Die Collection benötigt eine Task-Name-Column und eine Text-Column für die Anweisung. criteria.toml enthält einen [[criteria]]-Eintrag pro Criterion. Labels sind optional; wenn sie angegeben sind, müssen sie pro Task eindeutig sein, dürfen höchstens 25 Zeichen haben und nicht Overall Rating heißen. Criterion-Texte sind auf 4.000 Zeichen begrenzt.

Ein erneuter Import aktualisiert passende Tasks statt Duplikate anzulegen. Behalten Sie ein Criterion-Label bei, um seine Versionshistorie zu erhalten; ein neues Label entfernt das alte Criterion und legt ein neues an. Tasks, die im Archiv fehlen, bleiben unverändert.

Task-Konfiguration und Textdateien aus den genannten Verzeichnissen werden gespeichert und exportiert. Binärdateien, Dateien über 256 KB und andere Pfade werden gemeldet, aber nicht gespeichert. Pro Upload gelten: 25 MB komprimiert, 300 Tasks, 2 MB pro Datei, 100 Criteria pro Task sowie 100 Implementierungsdateien mit zusammen 1 MB pro Task.

Taskspezifische Criteria

Für Prüfungen, die nur für einen Task gelten, nutzen Sie die Tasks-Ansicht der Collection. Öffnen Sie den Criteria-Dialog eines Tasks, fügen Sie das Criterion hinzu und speichern Sie. Labels sind optional und pro Task eindeutig; Criteria können Task-Felder wie {{expected_outcome}} referenzieren.

Tasks-Ansicht einer Agentic Collection
Eine Karte pro Task, einschließlich der Anzahl taskspezifischer Criteria.

Ein Experiment bewertet sein Criterion Set, die Criteria des Tasks und Overall Rating. Diese Werte werden beim Erstellen festgeschrieben; nach Änderungen erstellen Sie ein neues Experiment.

Agent-Traces in der UI hochladen

Öffnen Sie das Agentic Experiment und wählen Sie Agent-Traces hochladen. Sie können eine Datei ablegen oder JSON einfügen, dann die Task-Name-Column bestätigen, die Bewertung ein- oder ausschalten und eine Epoch für einen weiteren Run derselben Tasks setzen.

Nach dem Einlesen zeigt der Dialog, welcher Trace zu welchem Task gehört, so wie die Plattform es auflöst. Die übrigen stehen unter Nicht zugeordnete Traces und lassen sich per Drag-and-drop oder über die Liste des Tasks zuordnen. Ein Task ohne Trace wird übersprungen, nicht zugeordnete Traces werden nicht hochgeladen.

Format der Upload-Datei

Verwenden Sie ein .json-Objekt oder -Array oder eine .jsonl-Datei mit einem vollständigen Objekt pro Zeile. Jedes Objekt ist ein SDK-AgentTrialResult:

  • task_id oder task_name gibt an, zu welchem Task der Trace gehört. task_id ist der stabilere Schlüssel: er übersteht eine Umbenennung und braucht keine Task-Name-Column. Ein Trace ohne beides wird in der UI nicht abgelehnt, dort ordnen Sie ihn nach dem Einlesen zu.
  • trajectory enthält die ATIF-Trajectory.
  • reward, cost_usd, steps, messages und criterion_ratings sind optional.

Eine Trace-Datei ist keine Upload-Datei

Eine ATIF-Trajectory allein ist kein Trial-Result. Verschachteln Sie sie unter trajectory.

[
  {
    "task_name": "write-hello-world",
    "reward": 1.0,
    "cost_usd": 0.0042,
    "trajectory": {
      "schema_version": "ATIF-v1.0",
      "session_id": "run-001/write-hello-world",
      "agent": {
        "name": "demo-agent",
        "version": "0.1.0",
        "model_name": "claude-sonnet-4-6"
      },
      "steps": [
        {
          "step_id": 1,
          "source": "user",
          "message": "Write a Python hello world script to hello.py"
        },
        {
          "step_id": 2,
          "source": "agent",
          "message": "Writing hello.py.",
          "tool_calls": [
            {
              "tool_call_id": "tc_1",
              "function_name": "write_file",
              "arguments": {"path": "hello.py", "content": "print('Hello, World!')"}
            }
          ],
          "observation": {
            "results": [{"source_call_id": "tc_1", "content": "wrote 22 bytes"}]
          },
          "metrics": {"prompt_tokens": 420, "completion_tokens": 61, "cost_usd": 0.0042}
        }
      ],
      "final_metrics": {
        "total_steps": 2,
        "total_cost_usd": 0.0042,
        "total_prompt_tokens": 420,
        "total_completion_tokens": 61
      }
    }
  }
]

Bei aktivem Evaluate after upload lassen Sie criterion_ratings weg und elluminate bewertet den Trace. Für vorab berechnete Ratings geben Sie die criterion_id aus der Experimentdefinition an; Labels dürfen mehrfach vorkommen. Ältere Experiments können weiterhin eindeutige Labels gegen ihre Live-Definition verwenden.

Trials werden unabhängig verarbeitet, daher blockiert eine ungültige Trajectory die übrigen nicht. Die Bewertung läuft im Hintergrund.

Ergebnisse in der UI ansehen

Agentic Experiment Übersicht
Die Experiment-Übersicht.
  • Overview zeigt aggregierte Metriken, Pass-Raten pro Criterion und eine Experiment Summary, wenn sie verfügbar ist.
  • Sample Navigator zeigt Phasen-Timeline, Trace und Ratings eines Samples. Sub-Agent-Traces stehen unter dem auslösenden Step.
  • Responses Overview listet alle Responses.
Agent Trace und Phasen-Timeline
Phasen-Timeline und Agent Trace eines Samples.

SDK-Walkthrough

Das Beispiel legt eine Agentic Collection, ein Criterion Set und ein Experiment an oder verwendet vorhandene wieder, wandelt Runner-Ausgaben in AgentTrialResult um, lädt sie hoch und wartet auf die Bewertung.

export ELLUMINATE_API_KEY=<your-key>
uv run --directory elluminate_sdk python examples/example_harbor_agentic_upload.py
"""Harbor-based Agentic Evaluation: end-to-end upload example.

This example shows the full workflow for evaluating an agent that was run
externally with Harbor (or any other agent framework), and uploading the
results, including ATIF trajectories, to elluminate for inspection and
automatic per-criterion evaluation.

Workflow:

1. Create an AGENTIC collection whose rows are the agent's tasks. The task
   name lives in a single TEXT `task` column (matched against each uploaded
   result's `task_name`); no prompt template is needed because AGENTIC
   experiments do not auto-generate responses.
2. Create a criterion set describing what counts as success.
3. Create an AGENTIC experiment (no auto-generation; results are uploaded).
4. Run the agent externally (Harbor CLI, LangChain, CrewAI, custom code).
5. Read Harbor's per-task output and convert it into `AgentTrialResult` objects.
6. Upload via `experiment.upload_agent_results(...)`.
7. The backend stores the trajectories and, when `evaluate=True` and
   trajectories are present, elluminate automatically rates each criterion
   against the trajectory.
8. Await that evaluation via `experiment.wait_for_evaluation(...)` so the
   ratings are complete before reading results back.

The script is idempotent: collections, criterion sets, and experiments are
reused across runs; uploads are skipped when an experiment already contains
responses.

For a self-contained demo this script uses a small in-memory stand-in
(`HARBOR_RUN`) for the runner's output. In a real run you would replace it
with your own loader that reads your runner's per-task output from disk — for
Harbor, the run directory at `~/.harbor/runs/<run_name>/tasks/<task>/...`.
"""

from typing import Any

from dotenv import load_dotenv
from elluminate import AgentTrialResult, Client
from elluminate.schemas import CollectionColumn, ColumnTypeEnum
from elluminate.schemas.criterion import CriterionIn
from elluminate.schemas.experiments import Experiment

load_dotenv(override=True)

client = Client()
llm_config = client.get_llm_config(name="Claude Sonnet 4.6")

# Upper bound for the evaluation wait in step 6. Size it to your run: rating is
# per trajectory, and this demo uploads two.
EVALUATION_TIMEOUT_SECONDS = 1800

# Mock "Harbor output"; in a real integration this is read from disk.
# Each entry is what a Harbor run produces per task: a short task identifier,
# the instruction text, final messages, aggregate metrics, and an ATIF
# trajectory describing every step the agent took.
HARBOR_RUN: list[dict[str, Any]] = [
    {
        "task_name": "write-hello-world",
        "instruction": "Write a Python hello world script to hello.py",
        "reward": 1.0,
        "steps": 2,
        "cost_usd": 0.0042,
        "input_tokens": 420,
        "output_tokens": 61,
        "duration_seconds": 3.2,
        "messages": [
            {"role": "user", "content": "Write a Python hello world script to hello.py"},
            {"role": "assistant", "content": "Wrote hello.py: print('Hello, World!')"},
        ],
        "trajectory": {
            "schema_version": "ATIF-v1.0",
            "session_id": "harbor-run-001/write-hello-world",
            "agent": {
                "name": "harbor-demo-agent",
                "version": "0.1.0",
                "model_name": "claude-sonnet-4-6",
            },
            "steps": [
                {
                    "step_id": 1,
                    "source": "user",
                    "message": "Write a Python hello world script to hello.py",
                },
                {
                    "step_id": 2,
                    "source": "agent",
                    "message": "Writing hello.py.",
                    "tool_calls": [
                        {
                            "tool_call_id": "tc_1",
                            "function_name": "write_file",
                            "arguments": {"path": "hello.py", "content": "print('Hello, World!')"},
                        }
                    ],
                    "observation": {
                        "results": [{"source_call_id": "tc_1", "content": "wrote 22 bytes"}],
                    },
                    "metrics": {"prompt_tokens": 420, "completion_tokens": 61, "cost_usd": 0.0042},
                },
            ],
            "final_metrics": {
                "total_steps": 2,
                "total_cost_usd": 0.0042,
                "total_prompt_tokens": 420,
                "total_completion_tokens": 61,
            },
        },
    },
    {
        "task_name": "reverse-string-function",
        "instruction": "Create a Python function that reverses a string in reverse.py",
        "reward": 0.5,
        "steps": 2,
        "cost_usd": 0.0031,
        "input_tokens": 310,
        "output_tokens": 42,
        "duration_seconds": 2.1,
        "messages": [
            {"role": "user", "content": "Create a Python function that reverses a string in reverse.py"},
            {"role": "assistant", "content": "Wrote reverse.py with a one-line slice-based reverse."},
        ],
        "trajectory": {
            "schema_version": "ATIF-v1.0",
            "session_id": "harbor-run-001/reverse-string-function",
            "agent": {
                "name": "harbor-demo-agent",
                "version": "0.1.0",
                "model_name": "claude-sonnet-4-6",
            },
            "steps": [
                {
                    "step_id": 1,
                    "source": "user",
                    "message": "Create a Python function that reverses a string in reverse.py",
                },
                {
                    "step_id": 2,
                    "source": "agent",
                    "message": "Writing reverse.py.",
                    "tool_calls": [
                        {
                            "tool_call_id": "tc_1",
                            "function_name": "write_file",
                            "arguments": {
                                "path": "reverse.py",
                                "content": "def reverse(s: str) -> str:\n    return s[::-1]\n",
                            },
                        }
                    ],
                    "observation": {
                        "results": [{"source_call_id": "tc_1", "content": "wrote 42 bytes"}],
                    },
                    "metrics": {"prompt_tokens": 310, "completion_tokens": 42, "cost_usd": 0.0031},
                },
            ],
            "final_metrics": {
                "total_steps": 2,
                "total_cost_usd": 0.0031,
                "total_prompt_tokens": 310,
                "total_completion_tokens": 42,
            },
        },
    },
]


def harbor_to_agent_trial(task_output: dict[str, Any]) -> AgentTrialResult:
    """Map one Harbor per-task output dict to an `AgentTrialResult`.

    `task_name` on `AgentTrialResult` is what elluminate matches against the
    collection's task-name column, so here we set it to the full instruction
    text (which is also what the `task` column row holds).
    """
    return AgentTrialResult(
        task_name=task_output["instruction"],
        messages=task_output["messages"],
        reward=task_output["reward"],
        steps=task_output["steps"],
        cost_usd=task_output["cost_usd"],
        input_tokens=task_output["input_tokens"],
        output_tokens=task_output["output_tokens"],
        duration_seconds=task_output["duration_seconds"],
        trajectory=task_output["trajectory"],
        metadata={"run_name": "harbor-run-001", "task_id": task_output["task_name"]},
    )


# Step 1: AGENTIC collection with a single TEXT `task` column.
# The `task` column must be TEXT so elluminate auto-selects it as the
# task-name column that uploaded results are matched against. No prompt
# template is required because AGENTIC experiments never auto-generate;
# responses are supplied by `upload_agent_results`.
collection, _ = client.get_or_create_collection(
    name="Harbor Demo Tasks",
    defaults={
        "collection_type": "AGENTIC",
        "columns": [CollectionColumn(name="task", column_type=ColumnTypeEnum.TEXT)],
        "variables": [{"task": h["instruction"]} for h in HARBOR_RUN],
    },
)

# Step 2: criterion set defining what success looks like for these tasks.
# Labels are display text and may repeat; frozen criterion IDs identify ratings.
criterion_set, _ = client.get_or_create_criterion_set(
    name="Harbor Demo Criteria",
    defaults={
        "criteria": [
            CriterionIn(
                criterion_str="Did the agent correctly complete the requested task?",
                label="task-complete",
            ),
            CriterionIn(
                criterion_str="Did the agent use tools appropriately?",
                label="uses-tools",
            ),
            CriterionIn(
                criterion_str="Is the agent's final output correct?",
                label="output-correct",
            ),
        ],
    },
)


def get_or_create_agentic_experiment(name: str, description: str) -> tuple[Experiment, bool]:
    """Return an AGENTIC experiment, creating it if missing.

    Also reports whether the experiment already has uploaded responses so the
    caller can skip a redundant upload on re-runs (avoids epoch conflicts).
    """
    try:
        experiment = client.get_experiment(name=name, fetch_responses=False)
        populated = experiment.results is not None and experiment.results.completed_epochs > 0
        return experiment, populated
    except ValueError:
        experiment = client.create_experiment(
            name=name,
            collection=collection,
            prompt_template=None,
            criterion_set=criterion_set,
            description=description,
            evaluation_mode="AGENTIC",
            llm_config=llm_config,
        )
        return experiment, False


# Step 3: AGENTIC experiment. No auto-generation; results come from Harbor.
experiment, experiment_populated = get_or_create_agentic_experiment(
    "Harbor Demo — Agent Run",
    "Harbor-run coding agent with ATIF trajectories.",
)
print(f"Experiment: {experiment.name} (id={experiment.id})")

# Step 4: Convert Harbor output to `AgentTrialResult` objects.
results = [harbor_to_agent_trial(task_output) for task_output in HARBOR_RUN]

# Step 5: upload with `evaluate=True`. elluminate rates every
# criterion against the trajectory and fills in per-criterion ratings.
if experiment_populated:
    print("Experiment already has responses; skipping upload.")
else:
    upload = experiment.upload_agent_results(
        results=results,
        evaluate=True,
    )
    print(
        f"Uploaded: {upload.created_responses} responses, "
        f"{upload.created_ratings} ratings, "
        f"{upload.pending_evaluations} pending trace evaluations"
    )
    if upload.errors:
        print(f"Errors: {upload.errors}")

    # Step 6: wait for elluminate's trace agent to finish rating the
    # uploaded trajectories, so the ratings below are complete. Rating a large
    # run takes minutes to hours; the timeout bounds the wait so a stuck
    # evaluation raises TimeoutError instead of blocking forever. Omit it to
    # wait indefinitely.
    if upload.pending_evaluations:
        print(f"Awaiting evaluation of {upload.pending_evaluations} trajectories...")
        event = experiment.wait_for_evaluation(
            task_id=upload.evaluation_task_id, timeout=EVALUATION_TIMEOUT_SECONDS
        )
        rated = event.progress.responses_rated if event.progress else 0
        print(f"Evaluation {event.status.value.lower()}: {rated} response(s) rated")

# Step 7: Verify the trajectories are queryable from the SDK.
experiment.fetch_responses()
for resp in experiment.responses():
    task = resp.prompt.template_variables.input_values.get("task", "?")
    steps = len(resp.trajectory["steps"]) if resp.trajectory else 0
    print(f"  [{task[:50]}] trajectory_steps={steps}")

Die Zuordnung vor dem Upload prüfen

preview_matching() beantwortet, zu welchem Task jeder Trial gespeichert würde, und speichert nichts. Es läuft dieselbe Zuordnung wie der Upload, das Ergebnis ist also genau das, was der Upload tun wird.

preview = experiment.preview_agent_results_matching(results)
for item in preview.unmatched_results:
    print(item.index, item.reason)

Ein Upload, bei dem kein Result einen Task des Experiments benennt, wird komplett abgelehnt und löst TaskMatchError aus. Der Fehler nennt die Tasks, die akzeptiert werden:

from elluminate import TaskMatchError

try:
    experiment.upload_agent_results(results=results)
except TaskMatchError as exc:
    for task_id, task_name in ((t.task_id, t.task_name) for t in exc.accepted_tasks):
        print(task_id, task_name)

Ein Batch, in dem einige Results zutreffen, wird weiterhin gespeichert; der Rest steht in errors. Laden Sie über task_id hoch, wenn Ihr Runner kein stabiles Namensschema hat oder ein Name mehrdeutig ist, weil eine Zeile auf den Namen eines anderen Tasks umbenannt wurde.

Das Dev-Set erweitern (das Eval-Flywheel)

Fügen Sie Produktionsfehler zur Collection hinzu, damit spätere Runs sie ebenfalls abdecken. Neue Zeilen erweitern die Task-Menge; eine höhere Upload-Epoch führt dieselben Tasks erneut aus.

collection = client.get_collection(name="my-agent-dev-set")
collection.add_many([
    {"task": "Neues Failure-Szenario aus der Produktion ..."},
    {"task": "Weiterer Regressionsfall ..."},
])

AgentTrialResult-Felder

Field Pflicht Beschreibung
task_id eines von beiden Die Row-ID des Tasks aus get_definition(). Sie hat Vorrang: das Result wird über die ID aufgelöst, ohne Namenssuche.
task_name eines von beiden Ein beim Erstellen des Experiments festgeschriebener Task-Name, oder der aktuelle Name der Zeile nach einer Umbenennung.
messages nein Finale Messages im OpenAI-Format auf der Response-Seite.
reward nein Primärer Reward-Score (0.0–1.0).
steps nein Anzahl der Agent-Steps oder LLM-Aufrufe.
cost_usd nein Gesamtkosten in USD; ohne Angabe aus der Trajectory abgeleitet.
duration_seconds nein Wall-Clock-Dauer.
input_tokens nein Alle Input-Tokens einschließlich Cache-Reads.
output_tokens nein Alle Output-Tokens.
cached_tokens nein Gecachte Input-Tokens, bereits in input_tokens enthalten.
error nein Fehlermeldung eines fehlgeschlagenen Trials.
metadata nein Freie Daten auf der Response-Seite.
trajectory nein ATIF-Trajectory, die das Backend validiert.
criterion_ratings nein Vorab berechnete YES/NO-Ratings; für festgeschriebene Experiments criterion_id verwenden.

ATIF-Trajectory-Format

trajectory verwendet ATIF. Das Upload-Beispiel oben zeigt eine minimale ATIF-v1-Trajectory, die in einem Trial-Ergebnis verschachtelt ist.

Wie Kosten und Tokens ermittelt werden

elluminate nutzt die erste verfügbare Kostenquelle: cost_usd am Trial, final_metrics.total_cost_usd, die Summe der Step-Kosten (einschließlich Sub-Agents) und zuletzt eine Token-Schätzung anhand von model_name. Für Token-Summen gilt dieselbe Reihenfolge.

Input-Token-Zahlen enthalten Cache-Reads. Providerspezifische Cache-Write-Tokens gehören in metrics.extra (Run-Summen in final_metrics.extra); geschätzte Kosten erhalten ein ~. Senden Sie cost_usd oder metrics.cost_usd, wenn Ihr Runner die tatsächlichen Kosten kennt.