跳转至

评估集 EvalSet

EvalSet 用于描述评估覆盖的场景集合,提供评估集输入。每个场景对应一个评估用例 EvalCase,EvalCase 再按轮组织 Invocation。默认模式支持两种推理输入:静态 conversation 与动态 conversationScenario。使用 conversation 时,框架会按轮读取 userContent 驱动 Runner 推理;使用 conversationScenario 时,框架会通过 UserSimulator 动态生成下一轮用户输入并采集实际轨迹。预期轨迹默认来自 conversation;使用 conversationScenario 且未开启 expectedRunnerEnabled 时,评估阶段会根据实际轨迹构造仅保留 userContent 的占位 expecteds;当用例开启 expectedRunnerEnabled 时,框架会在推理阶段通过 ExpectedRunner 预生成 expecteds,并在评估阶段直接复用。Trace 模式会跳过推理,并由 actualConversation 提供实际轨迹。评估运行时,Service 会将实际轨迹与预期轨迹交给 Evaluator 对比打分。

结构定义

EvalSet 是评估用例的集合,每个用例用 EvalCase 表达。默认模式下,可以使用 Conversation 描述静态多轮输入,也可以使用 ConversationScenario 描述动态用户模拟;Trace 模式下 ActualConversation 用于描述实际输出轨迹,结构定义如下。

import (
    "trpc.group/trpc-go/trpc-agent-go/evaluation/epochtime"
    "trpc.group/trpc-go/trpc-agent-go/evaluation/toolmock"
    "trpc.group/trpc-go/trpc-agent-go/model"
)

// EvalSet 表示评估集,用于组织一组评估用例
type EvalSet struct {
    EvalSetID         string               // EvalSetID 是评估集标识
    Name              string               // Name 是评估集名称
    Description       string               // Description 是评估集说明,可选
    EvalCases         []*EvalCase          // EvalCases 是评估用例列表,必填
    CreationTimestamp *epochtime.EpochTime // CreationTimestamp 是创建时间戳,可选
}

// EvalCase 表示单个评估用例
type EvalCase struct {
    EvalID                string                // EvalID 是用例标识
    EvalMode              EvalMode              // EvalMode 是用例模式,可选为空或 trace
    ExpectedRunnerEnabled bool                  // ExpectedRunnerEnabled 表示是否通过 ExpectedRunner 预生成预期输出,可选
    ContextMessages       []*model.Message      // ContextMessages 是上下文消息,可选
    Conversation          []*Invocation         // Conversation 是静态多轮交互序列,默认模式下与 ConversationScenario 二选一
    ConversationScenario  *ConversationScenario // ConversationScenario 是动态用户模拟场景,默认模式下与 Conversation 二选一
    ActualConversation    []*Invocation         // ActualConversation 是 Trace 模式下的实际输出轨迹,可选
    SessionInput          *SessionInput         // SessionInput 是会话初始化信息,必填
    Rubrics               []*EvalCaseRubric     // Rubrics 是用例级评估细则,可选
    CreationTimestamp     *epochtime.EpochTime  // CreationTimestamp 是创建时间戳,可选
}

// EvalCaseRubric 表示只作用于单个评估用例的评估细则
type EvalCaseRubric struct {
    MetricName  string                 // MetricName 是该细则补充的指标实例名
    ID          string                 // ID 是用例级细则的唯一标识
    Content     *EvalCaseRubricContent // Content 是裁判可读取的细则内容
    Description string                 // Description 是人类可读说明,默认不参与裁判
    Type        string                 // Type 是细则类型,用于结果排查
}

// EvalCaseRubricContent 表示用例级细则的裁判可读内容
type EvalCaseRubricContent struct {
    Text string // Text 是 rubric 评估器实际使用的细则文本
}

// ConversationScenario 表示动态用户模拟场景
type ConversationScenario struct {
    Driver                ConversationScenarioDriver // Driver 指定由 actual 或 expected runner 驱动对话轨迹,可选,默认 actual
    StartingPrompt        string // StartingPrompt 是固定首轮输入,可选
    ConversationPlan      string // ConversationPlan 是用户目标与结束条件描述,必填
    StopSignal            string // StopSignal 是模拟用户输出该内容时结束对话的标记,可选
    MaxAllowedInvocations *int   // MaxAllowedInvocations 是最大允许轮数,0 表示不限制,可选
}

// Invocation 表示对话中的一轮交互
type Invocation struct {
    InvocationID          string               // InvocationID 是本轮标识,可选
    ContextMessages       []*model.Message     // ContextMessages 是本轮上下文消息,可选
    UserContent           *model.Message       // UserContent 是本轮用户输入,必填
    FinalResponse         *model.Message       // FinalResponse 是最终响应,可选
    Tools                 []*Tool              // Tools 是工具轨迹,可选
    ToolMock              *toolmock.ToolMock   // ToolMock 是本轮工具返回 Mock 配置,可选
    IntermediateResponses []*model.Message     // IntermediateResponses 是中间响应,可选
    CreationTimestamp     *epochtime.EpochTime // CreationTimestamp 是创建时间戳,可选
}

// Tool 表示一次工具调用及其结果
type Tool struct {
    ID        string // ID 是工具调用标识,可选
    Name      string // Name 是工具名,必填
    Arguments any    // Arguments 是工具入参,可选
    Result    any    // Result 是工具输出,可选
}

// SessionInput 表示会话初始化信息
type SessionInput struct {
    AppName string         // AppName 是应用名,可选
    UserID  string         // UserID 是用户标识,必填
    State   map[string]any // State 是会话初始状态,可选
}

EvalSet 由 evalSetId 标识,包含多个 EvalCase,每个用例用 evalId 标识。

默认模式推理阶段有两种组织方式。配置 conversation 时,框架会按轮读取 userContent 作为输入;配置 conversationScenario 时,框架会先创建被测 Agent 的会话,再通过 UserSimulator 根据场景动态生成每一轮用户输入。两种方式都使用 sessionInput.userId 创建会话,必要时通过 sessionInput.state 注入初始状态,contextMessages 会在每次推理前注入额外上下文。Trace 模式下不会推理,而是直接使用 actualConversation 作为实际轨迹。

EvalSet 中的 toolsfinalResponse 用于描述工具轨迹与最终响应,是否需要填写取决于所选评估指标。

toolMock 用于推理阶段替换工具执行返回,不是评估阶段的预期输出。它只作用于所在 invocation;配置后模型仍基于真实工具声明决定是否发起 tool call,框架只在工具执行点替换返回值,并把 mock 结果继续写入实际工具轨迹。

Trace 模式下可以通过 actualConversation 显式配置实际输出轨迹。

当 Trace 模式同时配置了 conversationactualConversation 时,需要按轮次对齐,且 actualConversation 每轮应包含 userContent。当仅配置 actualConversation 且未配置 conversation 时,表示不提供静态预期输出;如果用例开启了 expectedRunnerEnabled 并注入 ExpectedRunner,则标准评测流程会在推理阶段预生成预期输出。

evalMode 为空表示默认模式,此时必须二选一配置 conversationconversationScenarioevalModetrace 时跳过推理,使用 actualConversation 作为实际轨迹参与评估;conversation 可选用于提供预期输出,conversationScenario 不支持在 Trace 模式下使用。

EvalSet Manager

EvalSetManager 是 EvalSet 的存储抽象,用于将评估用例资产从代码中分离。通过切换实现可以选择本地文件或内存存储,也可以自行实现接口接入数据库或配置平台。

接口定义

EvalSetManager 的接口定义如下。

type Manager interface {
    // Get 获取评估集
    Get(ctx context.Context, appName, evalSetID string) (*EvalSet, error)
    // Create 创建评估集
    Create(ctx context.Context, appName, evalSetID string) (*EvalSet, error)
    // List 列出评估集列表
    List(ctx context.Context, appName string) ([]string, error)
    // Delete 删除评估集
    Delete(ctx context.Context, appName, evalSetID string) error
    // GetCase 获取评估用例
    GetCase(ctx context.Context, appName, evalSetID, evalCaseID string) (*EvalCase, error)
    // AddCase 添加评估用例
    AddCase(ctx context.Context, appName, evalSetID string, evalCase *EvalCase) error
    // UpdateCase 更新评估用例
    UpdateCase(ctx context.Context, appName, evalSetID string, evalCase *EvalCase) error
    // DeleteCase 删除评估用例
    DeleteCase(ctx context.Context, appName, evalSetID, evalCaseID string) error
    // Close 释放资源
    Close() error
}

如果希望从数据库、对象存储或配置平台读取 EvalSet,可以实现该接口并在创建 AgentEvaluator 时注入。

1
2
3
4
5
6
7
8
import "trpc.group/trpc-go/trpc-agent-go/evaluation"

evalSetManager := myevalset.New()
agentEvaluator, err := evaluation.New(
    appName,
    runner,
    evaluation.WithEvalSetManager(evalSetManager),
)

InMemory 实现

框架提供了 EvalSetManager 的内存实现,适合在代码中动态构建或临时维护评估集。该实现并发安全,读写通过锁保护。为避免调用方误修改内部数据,读接口会返回深拷贝副本。

Local 实现

框架提供了 EvalSetManager 的本地文件实现,适合将 EvalSet 作为评估资产纳入版本管理。

该实现并发安全,读写通过锁保护。写入时使用临时文件并在成功后重命名,降低异常导致的文件损坏风险。

Local 实现通过 BaseDir 指定根目录,通过 Locator 统一管理文件路径规则。Locator 负责将 evalSetId 映射为文件路径,并列出某个 appName 下已有的评估集列表。评估集文件的默认命名规则为 <BaseDir>/<AppName>/<EvalSetId>.evalset.json

当希望复用既有目录结构时,可以自定义 Locator 并在创建 EvalSetManager 时注入。

import (
    "trpc.group/trpc-go/trpc-agent-go/evaluation/evalset"
    "trpc.group/trpc-go/trpc-agent-go/evaluation/evalset/local"
)

type customLocator struct{}

// Build 返回自定义文件路径格式 <BaseDir>/<AppName>/custom-<EvalSetId>.evalset.json
func (l *customLocator) Build(baseDir, appName, evalSetID string) string {
    return filepath.Join(baseDir, appName, "custom-"+evalSetID+".evalset.json")
}

// List 列出指定 appName 下的评估集 ID 列表
func (l *customLocator) List(baseDir, appName string) ([]string, error) {
    dir := filepath.Join(baseDir, appName)
    entries, err := os.ReadDir(dir)
    if err != nil {
        if errors.Is(err, os.ErrNotExist) {
            return []string{}, nil
        }
        return nil, err
    }
    var results []string
    for _, entry := range entries {
        if entry.IsDir() {
            continue
        }
        if strings.HasPrefix(entry.Name(), "custom-") && strings.HasSuffix(entry.Name(), ".evalset.json") {
            name := strings.TrimPrefix(entry.Name(), "custom-")
            name = strings.TrimSuffix(name, ".evalset.json")
            results = append(results, name)
        }
    }
    return results, nil
}

evalSetManager := local.New(
    evalset.WithBaseDir(dataDir),
    evalset.WithLocator(&customLocator{}),
)

MySQL 实现

EvalSetManager 的 MySQL 实现会将 EvalSet 与 EvalCase 持久化到 MySQL。

该实现会将评估集与评估用例分别写入两张表,并在读取评估集时按用例插入顺序返回用例列表。

配置选项

连接配置:

  • WithMySQLClientDSN(dsn string):直接使用 DSN 连接,推荐优先使用该方式,建议开启 parseTime=true
  • WithMySQLInstance(instanceName string):使用已注册的 MySQL instance。使用前需要通过 storage/mysql.RegisterMySQLInstance 注册。注意:WithMySQLClientDSN 优先级更高,同时设置时以 DSN 为准。
  • WithExtraOptions(extraOptions ...any):传递给 MySQL client builder 的额外参数。注意:当使用 WithMySQLInstance 时,以注册 instance 的配置为准,本参数不会生效。

表配置:

  • WithTablePrefix(prefix string):表名前缀。prefix 为空表示不加前缀;prefix 非空时必须以字母或下划线开头,且只能包含字母/数字/下划线。trpctrpc_ 等价,实际表名会自动补齐下划线分隔。

初始化配置:

  • WithSkipDBInit(skip bool):跳过自动建表。默认值为 false
  • WithInitTimeout(timeout time.Duration):自动建表超时。默认值为 30s

代码示例

import (
    "trpc.group/trpc-go/trpc-agent-go/evaluation"
    evalsetmysql "trpc.group/trpc-go/trpc-agent-go/evaluation/evalset/mysql"
)

evalSetManager, err := evalsetmysql.New(
    evalsetmysql.WithMySQLClientDSN("user:password@tcp(localhost:3306)/dbname?parseTime=true&charset=utf8mb4"),
    evalsetmysql.WithTablePrefix("trpc_"),
)
if err != nil {
    log.Fatalf("create mysql evalset manager: %v", err)
}

agentEvaluator, err := evaluation.New(
    appName,
    runner,
    evaluation.WithEvalSetManager(evalSetManager),
)
if err != nil {
    log.Fatalf("create evaluator: %v", err)
}
defer agentEvaluator.Close()

配置复用

import (
    storagemysql "trpc.group/trpc-go/trpc-agent-go/storage/mysql"
    evalsetmysql "trpc.group/trpc-go/trpc-agent-go/evaluation/evalset/mysql"
)

// 注册 MySQL instance
storagemysql.RegisterMySQLInstance(
    "my-evaluation-mysql",
    storagemysql.WithClientBuilderDSN("user:password@tcp(localhost:3306)/dbname?parseTime=true&charset=utf8mb4"),
)

// 在 EvalSetManager 中复用
evalSetManager, err := evalsetmysql.New(evalsetmysql.WithMySQLInstance("my-evaluation-mysql"))
if err != nil {
    log.Fatalf("create mysql evalset manager: %v", err)
}

存储结构

skipDBInit=false 时,manager 会在初始化阶段按需创建所需表结构。该选项默认值为 false。若设置 skipDBInit=true,需要自行建表;可以直接使用下面的 SQL,与 evaluation/evalset/mysql/schema.sql 一致。并将 {{PREFIX}} 替换为实际表名前缀,例如 trpc_。不使用前缀时将其替换为空字符串。

CREATE TABLE IF NOT EXISTS `{{PREFIX}}evaluation_eval_sets` (
  `id` BIGINT NOT NULL AUTO_INCREMENT,
  `app_name` VARCHAR(255) NOT NULL,
  `eval_set_id` VARCHAR(255) NOT NULL,
  `name` VARCHAR(255) NOT NULL,
  `description` TEXT DEFAULT NULL,
  `created_at` TIMESTAMP(6) NOT NULL DEFAULT CURRENT_TIMESTAMP(6),
  `updated_at` TIMESTAMP(6) NOT NULL DEFAULT CURRENT_TIMESTAMP(6) ON UPDATE CURRENT_TIMESTAMP(6),
  PRIMARY KEY (`id`),
  UNIQUE KEY `uniq_eval_sets_app_eval_set` (`app_name`, `eval_set_id`),
  KEY `idx_eval_sets_app_created` (`app_name`, `created_at`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;

CREATE TABLE IF NOT EXISTS `{{PREFIX}}evaluation_eval_cases` (
  `id` BIGINT NOT NULL AUTO_INCREMENT,
  `app_name` VARCHAR(255) NOT NULL,
  `eval_set_id` VARCHAR(255) NOT NULL,
  `eval_id` VARCHAR(255) NOT NULL,
  `eval_mode` VARCHAR(32) NOT NULL DEFAULT '',
  `eval_case` JSON NOT NULL,
  `created_at` TIMESTAMP(6) NOT NULL DEFAULT CURRENT_TIMESTAMP(6),
  `updated_at` TIMESTAMP(6) NOT NULL DEFAULT CURRENT_TIMESTAMP(6) ON UPDATE CURRENT_TIMESTAMP(6),
  PRIMARY KEY (`id`),
  UNIQUE KEY `uniq_eval_cases_app_set_case` (`app_name`, `eval_set_id`, `eval_id`),
  KEY `idx_eval_cases_app_set_order` (`app_name`, `eval_set_id`, `id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;

Trace 评估模式

Trace 模式用于评估既有轨迹,可以将一次真实运行采集到的 Invocation 轨迹写入评估集 EvalSet,并在运行评估时跳过推理阶段。

启用方式是在 EvalCase 中将 evalMode 设为 trace。Trace 模式下 actualConversation 表示实际输出,conversation 表示预期输出,有两种配置方式:

  • 仅配置 actualConversationactualConversation 作为实际轨迹,不提供预期轨迹。
  • 同时配置 actualConversationconversationactualConversation 作为实际轨迹,conversation 作为预期轨迹,按轮次对齐。
{
  "evalSetId": "trace-basic",
  "name": "trace-basic",
  "evalCases": [
    {
      "evalId": "trace_calc_add",
      "evalMode": "trace",
      "conversation": [
        {
          "invocationId": "trace_calc_add-1",
          "userContent": {
            "role": "user",
            "content": "calc add 123 456"
          },
          "finalResponse": {
            "role": "assistant",
            "content": "calc result: 579"
          },
          "tools": [
            {
              "id": "call_00_example",
              "name": "calculator",
              "arguments": {
                "a": 123,
                "b": 456,
                "operation": "add"
              },
              "result": {
                "a": 123,
                "b": 456,
                "operation": "add",
                "result": 579
              }
            }
          ]
        }
      ],
      "actualConversation": [
        {
          "invocationId": "trace_calc_add-1",
          "userContent": {
            "role": "user",
            "content": "calc add 123 456"
          },
          "finalResponse": {
            "role": "assistant",
            "content": "calc result: 579"
          },
          "tools": [
            {
              "id": "call_00_example",
              "name": "calculator",
              "arguments": {
                "a": 123,
                "b": 456,
                "operation": "add"
              },
              "result": {
                "a": 123,
                "b": 456,
                "operation": "add",
                "result": 579
              }
            }
          ]
        }
      ],
      "sessionInput": {
        "appName": "trace-eval-app",
        "userId": "demo-user"
      }
    }
  ]
}

在 Trace 模式下,推理阶段不会运行 Runner,而是直接将 actualConversation 写入 InferenceResult.Inferences 作为实际轨迹。conversation 可选用于提供预期输出;未配置 conversation 时,应使用不依赖预期输出的指标,评估阶段会生成仅保留每轮 userContent 的占位 expecteds,避免将 trace 轨迹误当作参考答案参与对比。

当只提供实际轨迹时,适合只依赖实际轨迹的指标,例如 llm_rubric_responsellm_rubric_knowledge_recallllm_hallucinations。如果需要对比参考工具轨迹或参考最终回答,例如 llm_final_responsellm_rubric_criticllm_rubric_reference_critic,可以额外配置预期轨迹。

完整示例参见 examples/evaluation/trace

ExpectedRunner 动态预期输出

有些评估任务里,希望使用动态的预期输出,而非静态内容。例如,参考答案需要由一套参考 Runner 基于输入样本实时生成。此时可以为 EvalCase 开启 expectedRunnerEnabled,并在创建 AgentEvaluator 时注入 ExpectedRunner,由推理阶段预生成 expecteds。

expectedRunnerEnabled=true 时,标准评测流程会在推理阶段使用 ExpectedRunner 对同一组 userContent 按轮推理生成 expecteds,并将结果写入 InferenceResult.ExpectedInferences。默认模式下如果使用静态 conversationuserContent 直接来自该对话;如果使用 conversationScenario,则取决于 driver:当 driver=expected 时,由 ExpectedRunner 先驱动整段 transcript,再由 target runner 回放这组生成出的 userContent;否则 userContent 来自 conversationScenario 生成出的实际轨迹。Trace 模式下 userContent 来自 actualConversation。评估阶段会直接复用这组 expecteds 与 actuals 按轮对齐后交给 Evaluator。此时 EvalSet 中的预期输出字段可以省略,只需保留每轮 userContent

配置文件示例如下:

{
  "evalId": "case-1",
  "expectedRunnerEnabled": true,
  "conversation": [
    {
      "invocationId": "case-1-1",
      "userContent": {
        "role": "user",
        "content": "calc add 2 3"
      }
    }
  ],
  "sessionInput": {
    "appName": "math-eval-app",
    "userId": "demo-user"
  }
}

代码示例如下:

import (
    "trpc.group/trpc-go/trpc-agent-go/evaluation"
    "trpc.group/trpc-go/trpc-agent-go/runner"
)

actualRunner := runner.NewRunner(appName, candidateAgent)
expectedRunner := runner.NewRunner(appName, expectedAgent)
agentEvaluator, err := evaluation.New(
    appName,
    actualRunner,
    evaluation.WithExpectedRunner(expectedRunner),
)

ToolMock 模拟工具执行结果

当评估目标依赖外部工具、实时服务或不稳定数据时,可以在 EvalSet 的单轮 Invocation 中配置 toolMock,让推理阶段在工具执行点返回指定结果。ToolMock 不会改变模型可见的工具声明,也不会强制模型调用工具;模型仍按真实工具声明决定是否发起 tool call,框架只在命中工具名和参数规则后替换工具返回。

toolMock 只支持配置在 Invocation 级别。EvalCaseMetric 不配置 ToolMock;conversationScenario 没有预声明 invocation,当前不支持声明式 ToolMock。

结构定义如下:

package toolmock

type ToolMock struct {
    Actual   []*Tool // Actual 作用于被测 Runner 推理
    Expected []*Tool // Expected 作用于 ExpectedRunner 推理
}

type Tool struct {
    Name         string          // Name 是需要 Mock 的工具名
    Arguments    *ArgumentsMatch // Arguments 是工具入参匹配规则;为空时只按工具名匹配
    Result       any             // Result 是静态工具返回
    LLMGenerator *LLMGenerator   // LLMGenerator 表示由 ToolMockRunner 动态生成工具返回
}

type ArgumentsMatch struct {
    Ignore          bool           // Ignore 表示忽略入参,只按工具名匹配
    Expected        any            // Expected 是期望工具入参
    OnlyTree        map[string]any // OnlyTree 只比较指定字段
    IgnoreTree      map[string]any // IgnoreTree 忽略指定字段
    NumberTolerance *float64       // NumberTolerance 是数字比较容差;默认 0
}

type LLMGenerator struct {
    Prompt string // Prompt 是 ToolMockRunner 的 instruction
}

actualexpected 分别作用于被测 Runner 与 ExpectedRunner。两侧可以为同一个工具配置不同结果,用于固定候选实现与参考实现各自看到的外部环境。同一个工具名可以出现多次,按配置顺序匹配,先命中先返回;更具体的参数规则应放在前面,忽略参数的兜底规则放在后面。

参数匹配通常按使用意图选择写法。最常见的是只按工具名固定返回,此时省略 arguments 即可:

1
2
3
4
{
  "name": "get_weather",
  "result": {"condition": "sunny"}
}

如果同一个工具需要按入参返回不同结果,再配置 arguments.expected。默认会完整比较 JSON,数字也必须精确相等;只关心部分稳定字段时用 onlyTree,需要跳过不稳定字段时用 ignoreTree,数字允许误差时再配置非负的 numberTolerance

1
2
3
4
5
6
7
8
{
  "name": "get_weather",
  "arguments": {
    "expected": {"city": "Shenzhen", "date": "2026-07-01"},
    "onlyTree": {"city": true, "date": true}
  },
  "result": {"condition": "sunny"}
}

ignore=true 是“显式忽略入参”的写法,语义等价于省略 arguments,适合希望在配置中明确标注“不比较参数”的场景。使用 ignore=true 时不要再配置 expectedonlyTreeignoreTreenumberToleranceonlyTreeignoreTree 也不要同时配置。

静态返回示例如下:

{
  "evalId": "weather-case",
  "conversation": [
    {
      "invocationId": "turn-1",
      "userContent": {
        "role": "user",
        "content": "深圳明天适合户外活动吗?"
      },
      "toolMock": {
        "actual": [
          {
            "name": "get_weather",
            "arguments": {
              "expected": {"city": "Shenzhen", "date": "2026-07-01"},
              "onlyTree": {"city": true, "date": true}
            },
            "result": {"city": "Shenzhen", "condition": "sunny", "temperature": 28}
          }
        ],
        "expected": [
          {
            "name": "get_weather",
            "result": {"city": "Shenzhen", "condition": "sunny", "temperature": 28}
          }
        ]
      }
    }
  ],
  "sessionInput": {
    "appName": "weather-eval-app",
    "userId": "demo-user"
  }
}

如果配置了某个工具名的 ToolMock,但工具调用参数没有命中任何规则,本轮推理会失败,而不是回退到真实工具执行。这样可以避免评估在 mock 配置失效时静默访问真实外部依赖。

除了静态 result,也可以使用 llmGenerator 由单独的 ToolMockRunner 动态生成工具返回。prompt 会作为 ToolMockRunner 的 instruction,本次工具调用参数 JSON 会作为 user message。未配置工具调用参数时 user message 为 {}。使用 llmGenerator 时,需要在创建 AgentEvaluator 时注入 ToolMockRunner。

{
  "toolMock": {
    "actual": [
      {
        "name": "search_hotels",
        "arguments": {"ignore": true},
        "llmGenerator": {
          "prompt": "Return only the tool result JSON, for example {\"hotels\":[]}."
        }
      }
    ]
  }
}
1
2
3
4
5
6
mockRunner := runner.NewRunner(appName, toolMockAgent)
agentEvaluator, err := evaluation.New(
    appName,
    actualRunner,
    evaluation.WithToolMockRunner(mockRunner),
)

直接使用 evaluation/service 层 API 时,可以通过 service.WithToolMockRunner(mockRunner) 注入同一个 ToolMockRunner。

如果工具声明包含类型为 objectOutputSchema,框架会在调用 ToolMockRunner 时复用该 schema 作为结构化输出约束;schema name 使用真实工具声明名,description 使用工具输出 schema 的描述。ToolMock 不定义额外的输出 schema 字段,也不要求用户为 mock 结果单独配置 schema。未使用结构化输出时,ToolMockRunner 的最终文本如果能解析为 JSON,则使用解析后的结构;否则使用原始文本。空输出和 JSON null 不合法。

Trace 模式下 actual 侧不运行被测 Runner,因此 toolMock.actual 不会生效。如果开启 expectedRunnerEnabled,ExpectedRunner 仍会执行,此时 toolMock.expected 可以生效。当 Trace 用例同时配置 actualConversationconversation 时,ExpectedRunner 使用 actualConversation[i].userContent 作为输入,并使用 conversation[i].toolMock.expected 作为 expected 侧工具 mock 配置。

完整示例参见 examples/evaluation/toolmock

UserSimulation 动态用户模拟

有些评估任务只有一个起始问题和一组对话目标,而没有完整的静态多轮 conversation。例如,希望评估 Agent 在“先澄清需求,再完成任务,最后确认结果”这类长流程中的表现。此时可以在 EvalCase 中配置 conversationScenario,并在创建 AgentEvaluator 时注入 UserSimulator,让框架在推理阶段动态生成下一轮用户输入。

conversationScenario 只支持默认模式,且与 conversation 互斥。driver 可选,默认值为 actual,表示由被测 Runner 的回复驱动后续用户输入;设置为 expected 时,表示由 ExpectedRunner 先驱动整条用户输入轨迹,再让被测 Runner 回放同一组 userContent,因此需要同时注入 ExpectedRunnerstartingPrompt 可选,用于固定首轮输入以提升复现性;conversationPlan 必填,用于描述用户目标、约束和结束条件;stopSignalmaxAllowedInvocations 用于控制对话停止,默认实现要求两者至少保留一个终止条件。

conversationScenario 本身支持以下字段:

  • driver:指定由哪一侧 Runner 驱动整条用户输入轨迹,可选值为 actualexpected,默认 actual
  • startingPrompt:固定首轮用户输入,可选;不配置时由 UserSimulator 基于 conversationPlan 生成第一轮输入。
  • conversationPlan:描述模拟用户目标、约束和结束条件,必填。
  • stopSignal:模拟用户输出该内容时结束对话的标记,可选。
  • maxAllowedInvocations:限制被测 Agent 的最大轮数,可选;0 表示不限制。

默认 UserSimulator 通过 usersimulation.New(simRunner, opt...) 支持以下 option:

  • usersimulation.WithStopSignal(...):覆盖 conversationScenario.stopSignal
  • usersimulation.WithMaxAllowedInvocations(...):覆盖 conversationScenario.maxAllowedInvocations
  • usersimulation.WithUserIDSupplier(...):自定义模拟器内部 user ID 生成逻辑,默认使用 UUID。
  • usersimulation.WithSessionIDSupplier(...):自定义模拟器内部 session ID 生成逻辑,默认使用 UUID。
  • usersimulation.WithSystemPromptBuilder(...):自定义默认模拟器发给 simRunner 的初始 system prompt。

接入 UserSimulation 能力时,通常还会用到以下框架级 option:

  • evaluation.WithUserSimulator(...):必填,用于注入 UserSimulator。
  • evaluation.WithExpectedRunner(...):当 driver=expectedexpectedRunnerEnabled=true 时需要注入。

完整示例参见 examples/evaluation/usersimulation。如果希望同时查看 UserSimulationExpectedRunner 的组合用法,可参考 examples/evaluation/usersimulation_expectedrunner,该示例当前演示的是 conversationScenario.driver=expected,由 ExpectedRunner 先驱动整条用户输入轨迹,再由被测 Runner 回放。

配置文件示例如下:

{
  "evalId": "travel-plan",
  "conversationScenario": {
    "startingPrompt": "帮我规划下周去北京出差的行程。",
    "conversationPlan": "先说明出差时间和预算,再补充酒店与航班偏好。等机票、酒店和提醒事项都确认后,只输出 </finished>。",
    "stopSignal": "</finished>",
    "maxAllowedInvocations": 12
  },
  "sessionInput": {
    "appName": "travel-eval-app",
    "userId": "demo-user",
    "state": {
      "today": "2026-03-24"
    }
  }
}

代码示例如下:

import (
    "trpc.group/trpc-go/trpc-agent-go/evaluation"
    "trpc.group/trpc-go/trpc-agent-go/evaluation/usersimulation"
    "trpc.group/trpc-go/trpc-agent-go/runner"
)

actualRunner := runner.NewRunner(appName, candidateAgent)
simRunner := runner.NewRunner(appName, simulatorAgent)
userSimulator, err := usersimulation.New(simRunner)
if err != nil {
    panic(err)
}

agentEvaluator, err := evaluation.New(
    appName,
    actualRunner,
    evaluation.WithUserSimulator(userSimulator),
)
if err != nil {
    panic(err)
}
defer agentEvaluator.Close()

对上面的 conversationScenario,一次可能的对话展开如下:

User:
帮我规划下周去北京出差的行程。

Assistant:
可以,先确认几个关键信息:你从哪个城市出发?下周具体哪天去、待几天?预算大概多少?更倾向飞机还是高铁?

Simulated user:
我从上海出发,下周二去、周四回,预算适中,倾向上午出发的航班,酒店希望住在国贸附近。

Assistant:
明白了。我可以先给你一版省心方案:周二上午从上海飞北京,酒店优先看国贸附近的商务型酒店。你这边对酒店价格区间还有要求吗?

Simulated user:
每晚大概 800 到 1000 元就可以,再帮我补一点出行提醒。

Assistant:
可以,我会推荐国贸附近 800 到 1000 元区间的商务酒店,并补充机场出发时间、北京早晚高峰和返程预留时间等提醒。

Simulated user:
</finished>

这里的关键点是:

  • 首轮用户输入可以直接来自 startingPrompt
  • 后续每一轮用户输入由 UserSimulator 根据 conversationPlan 和驱动 Runner 的最新回复动态生成,所以实际措辞不一定完全相同。
  • 当模拟用户输出 </finished> 时,对话结束。

默认实现会把驱动 Runner 最新一轮 finalResponse 作为下一次模拟输入传给 simRunner,并将 simRunner 的最终回复视为“下一句用户输入”。当 driver=actual 时,驱动 Runner 是被测 Runner;当 driver=expected 时,驱动 Runner 是 ExpectedRunner。如果未配置 startingPrompt,默认实现会基于 conversationPlan 生成第一轮用户输入。未开启 expectedRunnerEnabled 时,评估阶段仍会根据实际轨迹构造仅保留 userContent 的占位 expecteds,因此更适合依赖实际轨迹或 LLM Judge 的指标。

conversationScenario 可以与 expectedRunnerEnabled 搭配使用。driver=actual 时,ExpectedRunner 会在推理阶段复用 actual 侧已经生成好的 userContent 序列产出 expecteds;driver=expected 时,ExpectedRunner 先驱动生成整条用户输入轨迹,再由被测 Runner 回放,同样在推理阶段完成 expected 轨迹生成。评估阶段只复用预先生成的 ExpectedInferences,不再动态重跑。conversationScenario 仍然不支持 Trace 模式。

上下文注入

contextMessages 用于为 EvalCase 提供一组额外上下文消息,常用于补充背景信息、角色设定或样本示例。它也适用于纯模型评估场景,将 system prompt 作为评估数据按用例配置,便于对比不同模型与提示词组合的能力。

上下文注入示例:

{
  "evalSetId": "contextmessage-basic",
  "name": "contextmessage-basic",
  "evalCases": [
    {
      "evalId": "identity_name",
      "contextMessages": [
        {
          "role": "system",
          "content": "You are trpc-agent-go bot."
        }
      ],
      "conversation": [
        {
          "invocationId": "identity_name-1",
          "userContent": {
            "role": "user",
            "content": "Who are you?"
          }
        }
      ],
      "sessionInput": {
        "appName": "contextmessage-app",
        "userId": "demo-user"
      }
    }
  ]
}

完整示例参见 examples/evaluation/contextmessage

实时流量沉淀为评估集

在业务迭代中,评估集往往需要从真实交互中沉淀。框架提供 evaluation/evalset/recorder Runner 插件,用于在运行时捕获 runner.Run() 的事件流,并将交互过程写入 EvalSet/EvalCase,形成可复用的评估资产。

默认情况下,recorder 以 sessionID 同时作为 EvalSetIDEvalCase.EvalID,从而使同一 sessionID 的多轮对话持续追加到同一个 EvalCase 的 conversation 中。除了对话本身,recorder 还会把回放所需的输入一并沉淀下来:RunOptions.RuntimeState 会写入 EvalCase.SessionInput.State,注入型上下文消息会存入 EvalCase.ContextMessages。写入会在 runner.completion 到达,或者观测到终态错误事件时触发。runner.completion 表示本轮推理成功完成。终态错误既可以是对象类型为 ObjectTypeErrorerror 事件,也可以是携带 Response.Error 的响应事件;这两类情况都会以失败调用的形式写入评估集。

import (
    "trpc.group/trpc-go/trpc-agent-go/evaluation/evalset"
    evalsetlocal "trpc.group/trpc-go/trpc-agent-go/evaluation/evalset/local"
    "trpc.group/trpc-go/trpc-agent-go/evaluation/evalset/recorder"
    "trpc.group/trpc-go/trpc-agent-go/runner"
)

evalSetManager := evalsetlocal.New(evalset.WithBaseDir("./data"))
rec, err := recorder.New(
    evalSetManager,
    recorder.WithWriteTimeout(2*time.Second),
)
if err != nil {
    log.Fatalf("create evalset recorder: %v", err)
}

run := runner.NewRunner(appName, agent, runner.WithPlugins(rec))

如需调整沉淀粒度与写入行为,可以通过 Option 进行配置。WithEvalSetIDResolverWithEvalCaseIDResolver 用于自定义 EvalSetIDEvalCase.EvalID 的生成规则,常用于按业务维度分桶,或将多个 session 汇聚到同一评估集。写入模式默认同步,以保证在本轮推理完成后尽快落盘;当不希望写入阻塞事件处理时,可以通过 WithAsyncWriteEnabled(true) 开启异步写入。为避免慢存储导致单次写入耗时不可控,可以通过 WithWriteTimeout(d) 为落盘设置超时,d==0 表示不额外设置 deadline。

如果希望把实时流量按 trace mode 作为 actual trace 落盘,可以开启 WithTraceModeEnabled(true)。开启后,recorder 会创建 EvalModeTrace 的 case,并将 turn 追加到 ActualConversation,而不是默认模式下的 Conversation。由于 ConversationActualConversation 在评估中的语义不同,向已有 EvalCase 追加时要求 mode 一致。

完整示例参见 examples/evaluation/evalsetrecorder