EvalMetric
EvalMetric defines evaluation metrics. It identifies the metric with metricName, describes criteria with criterion, and defines thresholds with threshold. A single evaluation can configure multiple metrics. The evaluation run applies them in order and produces scores and statuses for each.
Structure Definition
The EvalMetric structure is defined as follows.
For common usage, metricName identifies the metric in results and selects the evaluator implementation from Registry. The following evaluators are built in by default:
tool_trajectory_avg_score: tool trajectory consistency evaluator, requires expected output.final_response_avg_score: final response evaluator, does not require LLM, requires expected output.llm_final_response: LLM final response evaluator, requires expected output.llm_hallucinations: LLM hallucination evaluator, checks whether the final answer is supported by evidence captured during execution, and typically does not require expected output.llm_judge_template: LLM template evaluator, uses custom prompt, variable bindings, and response scoring strategy fromcriterion.llmJudge.template.llm_verifier_pairwise: LLM pairwise comparison evaluator, compares the quality of the actual-side and expected-side final responses. It requires LLMJudge and rubrics, and the judge model must return logprobs.llm_rubric_critic: LLM rubric critic evaluator, requires expected output plus LLMJudge rubrics.llm_rubric_reference_critic: LLM rubric reference critic evaluator, requires expected output plus LLMJudge rubrics, and uses the reference answer as a quality anchor instead of an exact-match golden target.llm_rubric_response: LLM rubric response evaluator, requires EvalSet to provide session input and LLMJudge plus rubrics.llm_rubric_knowledge_recall: LLM rubric knowledge recall evaluator, requires EvalSet to provide session input and LLMJudge plus rubrics.
threshold defines the threshold. Evaluators output a score and determine pass or fail based on it. The definition of score varies slightly across evaluators, but a common approach is to compute scores per Invocation and aggregate them into an overall score. Under the same EvalSet, metricName must be unique. The order of metrics in the file also affects the evaluation execution order and result display order.
extension carries caller-defined metadata for an evaluation metric, such as platform-side weights, grouping, or display configuration. The framework only reads, stores, and passes this field with EvalMetric; it does not interpret its business meaning or guarantee deep-copy semantics for its contents. Custom evaluators, platform logic, or custom aggregation logic can read it when needed.
Below is an example metric file for tool trajectory.
Criterion
Criterion describes evaluation criteria. Each evaluator reads only the sub-criteria it cares about, and you can combine them as needed.
The framework includes the following criterion types:
| Criterion Type | Applies To |
|---|---|
| LengthCriterion | Content length ranges |
| TextCriterion | Text strings |
| JSONCriterion | JSON objects |
| XMLCriterion | XML documents |
| RougeCriterion | ROUGE text scoring |
| ToolTrajectoryCriterion | Tool call trajectories |
| FinalResponseCriterion | Final response content |
| LLMCriterion | LLM-based evaluation models |
| Criterion | Aggregation of multiple criteria |
LengthCriterion
LengthCriterion validates whether string length falls within an inclusive range. Length is counted by Unicode code points, so Chinese characters, English characters, and symbols each count as one character. min and max are both optional, but at least one of them must be configured.
Example configuration requires actual content to be between 20 and 500 characters.
TextCriterion
TextCriterion describes text-content evaluation rules. It is commonly used for tool name comparison and final response text comparison. It can constrain actual text length and can compare actual text with expected text using a configured strategy. The structure is defined as follows.
When Compare is provided from code, TextCriterion uses that custom logic directly and does not run built-in length validation or text matching. Otherwise, it first applies length to the actual string source, then compares source with the expected string target according to matchStrategy. TextMatchStrategy supports exact, contains, regex, and skip, with a default of exact.
| TextMatchStrategy Value | Description |
|---|---|
| exact | Actual equals expected exactly (default). |
| contains | Actual contains expected. |
| regex | Actual matches expected as a regular expression. |
| skip | Skips built-in text matching, commonly used for length-only validation. |
Example configuration snippet uses regex matching and case-insensitive mode.
If you only want to validate actual text length without comparing it with expected text, configure length and set matchStrategy to skip.
The following snippet uses Compare to trim spaces before comparison.
JSONCriterion
JSONCriterion compares two JSON values, commonly used for tool arguments and tool results. The structure is defined as follows.
During comparison, actual is the actual value and expected is the expected value. JSONCriterion runs in this order:
- If
Compareis provided from code, JSONCriterion uses that custom logic directly and does not run the built-invalid,schema, ormatchStrategylogic. - If
Compareis not provided, JSONCriterion runsvalidvalidation first, thenschemavalidation, and finally usesmatchStrategyto decide whether to run built-in JSON value matching. - If you only want JSON validity validation or Schema validation without comparing against
expected, configurevalid: trueorschema, and setmatchStrategy: "skip".
The schema field itself is a raw JSON Schema JSON value, usually an object, and boolean schemas are also supported. In metric JSON, write the schema directly as JSON instead of an escaped string. Code can use WithSchema with serialized JSON Schema text.
The actual value is validated as its runtime value: json.RawMessage and []byte are parsed as raw JSON first, while a Go string is validated as an already decoded string value by default. When both valid: true and schema are configured, schema validation reuses the JSON value parsed by valid. Empty schema disables Schema validation; schemas without $schema are compiled as Draft 2020-12; invalid schema text or actual validation failure returns (false, error).
Currently, matchStrategy supports exact and skip, with a default of exact. exact compares JSON values structurally, and skip skips built-in JSON value matching. Object comparison requires identical key sets. Array comparison requires identical length and order. Numeric comparison supports a tolerance, default 1e-6.
ignoreTree ignores unstable fields; a leaf node set to true ignores that field and its subtree. onlyTree compares only selected fields; keys not present in the tree are ignored. A leaf node set to true compares that field and its subtree. onlyTree and ignoreTree cannot be set at the same time when both are non-empty.
Example configuration ignores id and metadata.timestamp, and relaxes numeric tolerance.
Example configuration compares only name and metadata.id, and ignores all other fields.
Example configuration validates only whether actual matches the JSON Schema, without comparing against expected.
JSONCriterion provides a Compare extension to override default comparison logic.
The following snippet defines custom matching logic: if both actual and expected contain key common, it matches.
XMLCriterion
XMLCriterion validates whether a string is a legal XML document and also supports custom comparison logic injected from code. Validity checks require non-empty content, exactly one root element, properly closed tags, and no non-whitespace text outside the root element.
XMLCriterion requires matchStrategy to be explicitly configured. Currently only skip is supported. Built-in XML behavior only validates well-formedness and does not perform XML structural value matching; use code-injected Compare when custom XML matching is needed.
Example configuration validates that actual content is a legal XML document:
RougeCriterion
RougeCriterion scores two strings using ROUGE and treats the pair as a match when the scores meet the configured thresholds.
See examples/evaluation/rouge for a complete example.
RougeType supports rougeN, rougeL, and rougeLsum, where N is a positive integer. For example: rouge1, rouge2, rouge3, rougeL, rougeLsum.
Measure supports f1, precision, and recall, with a default of f1 when unset.
Threshold defines minimum requirements. Precision, recall, and f1 all participate in the pass check. Unset fields default to 0. ROUGE scores are in range [0, 1].
UseStemmer enables Porter stemming for the built-in tokenizer. When Tokenizer is set, UseStemmer is ignored.
SplitSummaries controls sentence splitting for rougeLsum only.
Tokenizer injects a custom tokenizer.
The following snippet configures FinalResponseCriterion to match by rougeLsum with thresholds.
Example metric JSON config:
MetricRegistry Extensions
When evaluation metrics come from local files or a database, runtime objects such as compare and tokenizer cannot be written directly into JSON. In this case, you can write the implementation name in the config file, and then register and resolve the actual implementation in code through evaluation.WithMetricRegistry(...).
This mechanism applies to the following cases:
text.compareNamejson.compareNametoolTrajectory.compareNamefinalResponse.compareNamerouge.tokenizerName
If you use a local file manager, you can declare tokenizerName in the metric file like this:
Then register a tokenizer named jieba in code and inject it through evaluation.WithMetricRegistry(...):
During evaluation, the framework first reads metric configs from metricManager, and then resolves the actual implementation from MetricRegistry according to tokenizerName or compareName.
For a complete example, see examples/evaluation/jieba.
ToolTrajectoryCriterion
ToolTrajectoryCriterion compares tool trajectories per turn by comparing tool call lists. The structure is defined as follows.
Tool trajectory comparison only looks at tool name, arguments, and result by default, and does not compare tool id.
orderSensitive defaults to false, which uses unordered matching. Internally, the framework treats expected tool calls as left nodes and actual tool calls as right nodes. If an expected tool and actual tool satisfy the matching strategy, an edge is created between them. The framework then uses the Kuhn algorithm to solve maximum bipartite matching and obtains a set of one-to-one pairs. If all expected tools can be matched without conflict, it passes. Otherwise, it returns the expected tools that cannot be matched.
subsetMatching defaults to false and requires the number of actual tools to match the number of expected tools. When enabled, actual traces may contain extra tool calls, which suits scenarios with unstable tool counts but still need to constrain key calls.
defaultStrategy defines the default matching strategy at the tool level. toolStrategy allows overrides by tool name. If no override matches, it falls back to the default. Each strategy can configure name, arguments, and result, and you can skip comparison by setting ignore to true for a sub-criterion.
The following configuration example uses the tool trajectory evaluator and configures ToolTrajectoryCriterion. Tool name and arguments use strict matching. For calculator, it ignores trace_id in arguments and relaxes numeric tolerance for results. For current_time, it ignores result to avoid matching instability from dynamic timestamps.
ToolTrajectoryCriterion provides a Compare extension to override default comparison logic.
The following snippet uses Compare to treat expected tool list as a blacklist. It matches when none of the expected tool names appear in the actual tools.
Assuming A, B, C, and D are tool calls, matching examples are as follows:
| SubsetMatching | OrderSensitive | Expected Sequence | Actual Sequence | Result | Description |
|---|---|---|---|---|---|
| Off | Off | [A] |
[A, B] |
Mismatch | Different counts. |
| On | Off | [A] |
[A, B] |
Match | Expected is a subset. |
| On | Off | [C, A] |
[A, B, C] |
Match | Subset and unordered match. |
| On | On | [A, C] |
[A, B, C] |
Match | Subset and ordered match. |
| On | On | [C, A] |
[A, B, C] |
Mismatch | Order mismatch. |
| On | Off | [C, D] |
[A, B, C] |
Mismatch | Actual is missing D. |
| Any | Any | [A, A] |
[A] |
Mismatch | Insufficient actual calls; one call cannot match twice. |
FinalResponseCriterion
FinalResponseCriterion compares final responses per turn. It supports text comparison, JSON structural comparison after parsing content, XML validation, and ROUGE scoring. The structure is defined as follows.
When using this criterion, you usually need to fill finalResponse on the expected side for the corresponding turn in EvalSet. If only criteria that do not depend on expected output are configured, the evaluator can validate the actual final response only.
text, json, rouge, and xml can be configured together, and all enabled sub-criteria must match. See each Criterion section for its fields and semantics.
To match by ROUGE, configure rouge and see RougeCriterion for details.
The following example selects final_response_avg_score and configures FinalResponseCriterion to compare final responses by text containment.
The following example validates only that the actual final response length is between 20 and 500 characters and that the content is legal JSON.
The following example validates that the actual final response is legal XML.
FinalResponseCriterion provides a Compare extension to override default comparison logic.
The following snippet uses Compare to treat the expected final response as a blacklist. If the actual final response equals it, it is considered a mismatch. This is suitable for forbidding fixed templates.
LLMCriterion
LLMCriterion configures LLM Judge evaluators. It is suitable for evaluating semantic quality and compliance that are hard to cover with deterministic rules. It selects the judge model and sampling strategy via judgeModel, uses rubrics to provide evaluation criteria, and can also use template to provide a custom prompt, variable bindings, and response scoring strategy. The structure is defined as follows.
judgeModel supports environment variable references in providerName, modelName, variant, baseURL, and apiKey, which are expanded at runtime. For security, avoid writing judgeModel.apiKey or judgeModel.baseURL in plain text in metric configuration files or code.
variant is optional and selects the OpenAI-compatible variant, for example openai, hunyuan, deepseek, qwen. It is only effective when providerName is openai. When omitted, the default variant is openai.
Generation defaults to MaxTokens=2000, Temperature=0.8, Stream=false.
numSamples controls the number of samples per turn. The default is 1. More samples reduce judge variance but increase cost.
sampleParallelismEnabled controls whether judge samples can be requested concurrently for one turn. The default is false, which keeps the original serial behavior. sampleParallelism only caps the concurrency after sample parallelism is enabled. When sampleParallelismEnabled=true and sampleParallelism=0, the evaluator uses runtime.GOMAXPROCS(0) and then caps it at numSamples. When sampleParallelism>0, the evaluator uses min(sampleParallelism, numSamples). If the model provider has QPS or concurrency limits, set sampleParallelism explicitly to a conservative value.
Example configurations:
When sampleParallelismEnabled is not configured, the evaluator keeps the default serial behavior:
When sampleParallelismEnabled=true and sampleParallelism is not configured, sample parallelism is enabled, and the parallelism defaults to runtime.GOMAXPROCS(0) before being capped by numSamples:
When sampleParallelismEnabled=true and sampleParallelism=2, the parallelism is 2:
providerName indicates the judge model provider, which maps to the framework Model Provider. The framework creates a judge model instance based on providerName and modelName. Common values include openai, anthropic, and gemini. See Provider for details.
rubrics split a metric into multiple clear-granularity criteria. Each rubric should be independent and directly verifiable from user input and the final answer, which improves judge stability and makes issues easier to locate. id is a stable identifier, and content.text is the rubric text used by the judge.
EvalCase.rubrics adds extra evaluation criteria for a single case. Each rubric targets a configured metric through metricName; when that case is evaluated, the framework appends those criteria after the metric's shared rubrics. This affects only the current case and leaves the metric file's global configuration unchanged. Rubric id values must be unique after merging.
The target metric uses criterion.llmJudge to carry the rubric list. Built-in rubric evaluators read the merged criteria and use structured output by default to make the judge return per-rubric scores through rubricScores. During Evaluate, after metric-level rubrics and EvalCase.rubrics are merged and before the judge model is called, each merged rubric used by structured output must have a non-empty and unique id. If validation fails, evaluation returns an error such as llm judge rubric id is required for structured output or duplicate llm judge rubric id "accuracy". To debug ID conflicts, inspect the merged criterion.llmJudge.rubrics from the metric configuration and case-level rubrics. Custom rubric evaluators can read the same field.
template is used only by llm_judge_template. It keeps template-based evaluation focused on cases where the prompt changes while the evaluation orchestration stays the same. Template evaluators do not evaluate structured rubrics like the llm_rubric_* family by default; write the evaluation criteria directly into template.prompt, or explicitly bind metric.rubrics when the prompt needs the current metric rubrics.
template.prompt uses double-brace template syntax such as {{question}} and {{answer}}. Every placeholder must be explicitly bound in variableBindings. Unbound variables, unknown variables, or binding resolution failures all result in errors.
template.variableBindings supports values from actual, expected, and the current metric configuration:
actual.userContentactual.finalResponseactual.traceStepInputactual.traceStepOutputactual.traceStepToolsactual.traceStepSkillsexpected.finalResponsemetric.rubrics
actual.userContent, actual.finalResponse, and expected.finalResponse render the current scoring turn's user input, actual final response, and expected final response respectively. actual.traceStepInput, actual.traceStepOutput, actual.traceStepTools, and actual.traceStepSkills require source.selector.nodeID to specify the trace step NodeID; the resolver selects the last matching step from the current invocation's executionTrace.steps. Input and output sources read Input.Text or Output.Text; tool and skill sources render JSON arrays of structured records for that step. When using a trace source, the evaluation call must pass agent.WithExecutionTraceEnabled(true). If the current actual invocation has no ExecutionTrace, evaluation fails. expected.finalResponse requires the current expected turn to contain finalResponse. If the template binds that field but the expected turn has only placeholder userContent and no finalResponse, evaluation fails directly. metric.rubrics renders the effective criterion.llmJudge.rubrics for the current metric as a JSON string, including case-level rubrics after merging.
source.path is optional. It extracts a JSON subfield after the source value is resolved. It supports a restricted JSONPath subset: root selector $, object fields such as .field, and array indexes such as [index], for example $[0].content.text or $[0].name for the first trace tool or skill name. Quoted bracket keys, wildcards, filters, field names containing dots, and missing delimiters after array indexes are not supported. If the resolved source is not valid JSON, or if the path is invalid, missing, out of range, or reaches the wrong type, evaluation fails. Extracted strings are rendered as-is; extracted objects or arrays are encoded back to JSON strings.
For example, a template can bind the first rubric text from the current metric:
If the agent final response is itself a valid JSON string, path can extract fields from it. For example, when actual.finalResponse.content is {"answer":"Paris","confidence":0.98}:
Plain natural-language text, Markdown fenced JSON, or content with extra prefixes or suffixes is not trimmed or repaired automatically.
template.responseScorerName specifies how judge output is parsed. The current supported values are:
single_score: the judge returns{"score": number, "reason": string}.rubric_scores: the judge returns{"rubricScores": [{"id": string, "score": number, "reason": string}]}.boolean: the judge returns{"passed": boolean, "reason": string}.passed=truemaps to score1, andpassed=falsemaps to score0.categorical: the judge returns{"category": string, "reason": string}. Configuretemplate.responseScorerOptions.categoriesto map each allowed label to a numeric score between0and1.
template.structuredOutputName is optional. When omitted, the template evaluator uses the structured output provider with the same name as responseScorerName if one is registered. Set it when the judge JSON schema and response scorer should be named independently, for example when a platform scorer parses a platform-owned schema.
template.sampleAggregatorName and template.invocationAggregatorName are optional. They default to majority_vote and average. Template evaluation reuses the standard LLM Judge sampling and multi-turn aggregation flow.
Below is an example metric configuration that selects llm_rubric_response and configures a judge model with two rubrics.
Case-level rubrics are configured directly in EvalCase.rubrics, for example:
Here, metricName selects the metric that receives the extra criterion. This example appends case:compound-profit to the rubrics for llm_rubric_response.
Below is an example template metric configuration. This is the advanced case where several metric instances reuse the same evaluator implementation: evaluatorName selects llm_judge_template, while metricName remains the metric instance name in results.
Metric Manager
MetricManager is the storage abstraction for Metric, separating metric configuration from code. By switching implementations, you can use local file or in-memory storage, or implement the interface to connect to a database or configuration platform.
Interface Definition
The MetricManager interface is defined as follows.
If you want to read Metric from a database, object storage, or configuration platform, you can implement this interface and inject it when creating AgentEvaluator.
InMemory Implementation
The framework provides an in-memory implementation of MetricManager, suitable for dynamically building or temporarily maintaining metric configuration in code. It is concurrency-safe with read/write locking. To prevent accidental mutation, the read interface returns deep copies, and the write interface copies input objects before writing.
Local Implementation
The framework provides a local file implementation of MetricManager, suitable for keeping Metric as versioned evaluation assets.
It is concurrency-safe with read/write locking. It writes to a temporary file and renames it on success to reduce file corruption risk. In local mode, the default metric file naming rule is <BaseDir>/<AppName>/<EvalSetId>.metrics.json, and you can customize the path rule via Locator.
MySQL Implementation
The MySQL implementation of MetricManager persists metric configuration to MySQL.
Configuration Options
Connection:
WithMySQLClientDSN(dsn string): Connect using DSN directly (recommended). Consider enablingparseTime=true.WithMySQLInstance(instanceName string): Use a registered MySQL instance. You must register it viastorage/mysql.RegisterMySQLInstancebefore use. Note:WithMySQLClientDSNhas higher priority; if both are set, DSN wins.WithExtraOptions(extraOptions ...any): Extra options passed to the MySQL client builder. Note: When usingWithMySQLInstance, the registered instance configuration takes precedence and this option will not take effect.
Tables:
WithTablePrefix(prefix string): Table name prefix. An empty prefix means no prefix. A non-empty prefix must start with a letter or underscore and contain only letters/numbers/underscores.trpcandtrpc_are equivalent; an underscore separator is added automatically.
Initialization:
WithSkipDBInit(skip bool): Skip automatic table creation. Default isfalse.WithInitTimeout(timeout time.Duration): Automatic table creation timeout. Default is30s, consistent with components such as memory/mysql.
Code Example
Configuration Reuse
Storage Layout
When skipDBInit=false, the manager creates required tables during initialization. The default value is false. If skipDBInit=true, you need to create tables yourself. You can use the SQL below, which is identical to evaluation/metric/mysql/schema.sql. Replace {{PREFIX}} with the actual table prefix, e.g. trpc_. If you don't use a prefix, replace it with an empty string.