Mäda Primavesi by Gustav Klimt

Artwork: Mäda Primavesi by Gustav Klimt. The Metropolitan Museum of Art · Public domain

Evaluation

Hybrid Agent Evaluation: Custom Judges + Azure Foundry Evaluators

Evaluating a speech-to-speech debriefing agent means answering two very different questions. Did the conversation stay grounded in the facts of this specific job? And did the agent behave like a competent agent — complete the task, follow the protocol, call tools correctly? No single evaluator answers both well, so we went hybrid: custom LLM judges for domain quality, plus Azure Foundry’s built-in evaluators for agent behavior.

Two dimensions, two kinds of evaluator

The options we weighed

Option Verdict
A. Custom judges only Full control, but we’d rebuild — and validate — agent-behavior metrics from scratch
B. Foundry evaluators only Standardized behavior metrics, but no scenario-fact groundedness, and no C# SDK
C. Hybrid (custom + Foundry via REST) Best coverage, stays in C#
D. Python sidecar for the Foundry SDK Native SDK, but drags Python into a .NET 10 build

We chose C. The deciding constraint was practical: this is a .NET 10 solution, and as of early 2026 the Azure.AI.Projects C# package doesn’t expose the evaluation API surface. A Python sidecar (Option D) would have solved that but poisoned the build with a cross-language dependency and all the CI/CD pain that brings.

How the split works

Hand-drawn split showing one transcript evaluated in parallel by custom domain judges and Azure Foundry behavior evaluators before both paths merge into one report.

Custom evaluators own domain quality:

Foundry evaluators own agent behavior, reached over REST:

Foundry ships nine built-in agent evaluators in total, spanning system-level outcomes and process-level tool use:

Category Evaluator Output
System Task Completion Pass/Fail
System Task Adherence Pass/Fail
System Intent Resolution 1–5 → Pass/Fail
System Task Navigation Efficiency Pass/Fail + F1
Process Tool Call Accuracy 1–5 → Pass/Fail
Process Tool Selection Pass/Fail
Process Tool Input Accuracy Pass/Fail
Process Tool Output Utilization Pass/Fail
Process Tool Call Success Pass/Fail

The integration seam

Foundry evaluators expect the OpenAI message schema. Our existing TranscriptDocument already captures role, content, and tool calls, so the mapping is mechanical:

{
  "query": [
    {"role": "user", "content": "I replaced the roller guides..."}
  ],
  "response": [
    {"role": "assistant", "content": [
      {"type": "tool_call", "name": "save_part", "arguments": {"partNumber": "RG-2450-A"}}
    ]},
    {"role": "tool", "content": [{"type": "tool_result", "tool_result": {"status": "saved"}}]},
    {"role": "assistant", "content": "Got it, I've recorded the roller guide replacement."}
  ]
}

Authentication uses DefaultAzureCredential against the OpenAI Evals REST API, with a create-run-poll lifecycle:

An EvaluationRunner orchestrates both custom and Foundry evaluators in one pipeline, so a single run produces both scenario-fact groundedness and standardized behavior metrics.

The C# client is a thin wrapper over that lifecycle — authenticate, create, run, then poll until the run leaves the queued/in-progress states:

var cred = new DefaultAzureCredential();
var client = new AzureFoundryEvaluatorClient(endpoint, cred);

// 1. Create the eval with typed azure_ai_evaluator criteria
var evalId = await client.CreateEvalAsync(new AzureAiEvaluator {
    EvaluatorName = "builtin.tool_call_accuracy",
    DataMapping = new { query = "{{item.query}}", response = "{{item.response}}",
                        tool_definitions = "{{item.tool_definitions}}" },
});

// 2. Kick off a run over the transcript data source
var runId = await client.CreateRunAsync(evalId, transcripts);

// 3. Poll to completion
EvalRun run;
do {
    await Task.Delay(TimeSpan.FromSeconds(5));
    run = await client.GetRunAsync(evalId, runId);
} while (run.Status is "queued" or "in_progress");

var rows = await client.GetOutputItemsAsync(evalId, runId);   // scores, labels, reasoning

A single run then yields both lanes side by side — domain facts from the custom judges, behavior from Foundry:

Evaluator Source Result Reasoning (excerpt)
Groundedness Custom 4.6 / 5 “Parts and test results match scenario facts”
Coherence Custom Pass “Logical flow, no contradictions”
Task Completion Foundry Pass “Debrief closed with all fields captured”
Task Adherence Foundry Pass “Followed the 8-step protocol”
Tool Call Accuracy Foundry 5 / 5 → Pass save_part args well-formed”

The trade-offs, honestly

The hybrid isn’t free:

We accepted those because the alternative — either weaker coverage or a Python dependency in a .NET solution — was worse. Results flow into Azure DevOps pipeline summaries via the agent evaluation extension, so the whole thing lives in CI.

Takeaways

Next: why we replaced 1–5 judge scales with binary judges and outcome scoring.

#evaluation#azure-foundry#llm-judge#ai-agents#dotnet