> For the complete documentation index, see [llms.txt](https://docs.neuron-ai.dev/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://docs.neuron-ai.dev/neuron-v3-zh/dai-li/evaluation.md).

# 评估

评估你的 Agent 系统的输出和行为

{% hint style="warning" %}

#### 编码代理技能

使用 **`/neuron-evaluation`** 用于指导你的编码代理如何为你的 agentic 实体实现一整套评估。

[AI 辅助开发](/neuron-v3-zh/gai-lan/agentic-development.md)
{% endhint %}

本指南介绍了评估代理的方法。有效的评估对于衡量代理性能、跟踪改进并确保你的 agentic 系统符合质量标准至关重要。

需要考虑各种定性和定量因素，包括响应语法、任务完成情况、成功率以及不准确或幻觉。在评估中，还应考虑比较不同配置，以针对特定期望结果进行优化。鉴于 LLM 的动态且非确定性特征，还需要进行严格且频繁的评估，以确保有一个稳定的基线来跟踪改进或回退。

### 为什么你需要评估

Evals 的作用超出了技术实践本身，在传统软件开发中也没有对应物。代理的质量是概率性的：不存在一个可以指着说“它能工作”的二元结果，因此“它准备好了吗？”这个问题并没有明显答案。一套强大的评估体系就成为了这个答案，也是一种共享的、客观的“足够好”的定义，它是你与你的客户或利益相关者共同构建的。当他们询问项目进展时，你不会用轶事和演示印象作答：你会展示一个大家都同意代表真实用例的数据集上的成功率，以及它自上一个版本以来的变化。当客户报告一次糟糕的交互时，这次交互就会成为数据集中的一个新条目——一个投诉被转化为可衡量的测试用例，从此再也不会在不知不觉中回退。

这与单元测试有着根本不同。单元测试从未承担过这样的角色：它们是团队内部的工程工具，对团队外部不可见，没有人会仅凭测试套件与客户讨论项目状态。Evals 是面向外部的。它们是你在迭代过程中用来协商预期、管理与依赖系统的人之间关系的工具，并且一版又一版地证明代理在对他们重要的事情上变得更好了。

### 配置你的应用

和单元测试一样，最好将 AI 系统的评估器收集到一个专用目录中。因此，你可以将下面的配置添加到你的应用 `composer.json` 文件中，以便告诉 Composer 如何将你的评估器包含到应用命名空间中：

```json
"autoload-dev": {
    "psr-4": {
        ...,
        "App\\Evaluators\\": "evaluators/"
    }
},
```

接下来在 `evaluators` 目录中创建它，位于项目根目录。将评估代码与生产代码分离，可在部署到生产环境的内容与仅用于开发和质量保证的内容之间建立清晰边界。

### 自定义 bootstrap 文件

默认情况下， `neuron` CLI 只加载你项目的 Composer 自动加载器（`vendor/autoload.php`）。当你的类只是普通的、可由 Composer 解析的 PHP 类时，这已经足够，但很多时候并非如此：某个评估器可能需要通过框架的辅助函数读取配置，需要从 `.env`，依赖常量，或者需要初始化服务容器。在这些情况下，命令会因“找不到类”或缺少配置而失败，因为通常用于准备该环境的代码——你的框架 bootstrap——从未运行。

该 `--autoload-file` 选项通过让 CLI 指向一个要执行的 PHP 文件来解决这个问题 *到达* ，该文件会在命令开始时执行，除了默认的 Composer 自动加载器之外：

```bash
vendor/bin/neuron evaluation /path/to/evaluators --autoload-file=bootstrap.php
```

该文件可以做任何普通 bootstrap 会做的事情——注册额外的自动加载器、加载环境变量、定义常量，或启动你的框架。例如，要运行依赖 Laravel 应用的评估器：

```php
<?php
// bootstrap.php

require __DIR__.'/vendor/autoload.php';

$app = require_once __DIR__.'/bootstrap/app.php';
$app->make(Illuminate\Contracts\Console\Kernel::class)->bootstrap();
```

### 创建评估器

使用下面的命令创建 `AgentEvaluator` 类到 evaluators 文件夹中：

{% tabs %}
{% tab title="Unix" %}

```bash
vendor/bin/neuron make:evaluator App\\Neuron\\Evaluators\\AgentEvaluator
```

{% endtab %}

{% tab title="Windows" %}

```powershell
.\vendor\bin\neuron make:evaluators App\Neuron\Evaluators\AgentEvaluator
```

{% endtab %}
{% endtabs %}

将创建的类将具有以下结构：

```php
namespace App\Neuron\Evaluators;

use NeuronAI\Evaluation\Assertions\StringContains;
use NeuronAI\Evaluation\BaseEvaluator;
use NeuronAI\Evaluation\Contracts\DatasetInterface;
use NeuronAI\Evaluation\Dataset\JsonDataset;

class AgentEvaluator extends BaseEvaluator
{
    /**
     * 1. 获取用于评估的数据集
     */
    public function getDataset(): DatasetInterface
    {
        return new JsonDataset(__DIR__ . '/datasets/dataset.json');
    }

    /**
     * 2. 运行正在测试的代理逻辑
     */
    public function run(array $datasetItem): mixed
    {
        $response = MyAgent::make()->chat(
            new UserMessage($datasetItem['input'])
        )->getMessage();
        
        return $response->getContent();
    }

    /**
     * 3. 使用断言将输出与预期结果进行评估
     */
    public function evaluate(mixed $output, array $datasetItem): void
    {
        $this->assert(
            new StringContains($datasetItem['reference']),
            $output,
        );
    }
} 
```

逻辑相当直接。评估器首先加载数据集，然后对数据集中的每个条目运行评估。

在 `run` 方法，你可以使用示例输入运行你的 agentic 实体并返回输出。随后，该输出会传递给 `求值` 方法，在那里你可以执行断言，将输出与参考值或你想要的任何其他逻辑进行比较。

### 数据集加载器

你可以使用任何你想要的内容作为数据集。没有预定义格式。评估器类只是允许你加载一组测试用例并针对它们运行评估器。你有两种数据集加载器。

#### ArrayDataset

```php
class AgentEvaluator extends BaseEvaluator
{
    public function getDataset(): DatasetInterface
    {
        return new ArrayDataset([
            [
                'input' => 'Hi',
                'reference' => 'help'
            ]
        ]);
    }
    
    ...
}
```

#### JsonDataset

```php
class AgentEvaluator extends BaseEvaluator
{
    public function getDataset(): DatasetInterface
    {
        return new JsonDataset(__DIR__ . '/datasets/dataset.json');
    }
    
    ...
}
```

你最终也可以创建一个自定义数据集加载器，实现 `NeuronAI\Evaluation\Contracts\DatasetInterface`.

### 运行评估

如果你已经正确配置了 composer 文件，就可以使用 Neuron CLI 启动评估器：

{% tabs %}
{% tab title="Unix" %}

```bash
vendor/bin/neuron evaluations --path=evaluators
```

{% endtab %}

{% tab title="Windows" %}

```powershell
.\vendor\bin\neuron evaluations --path=evaluators
```

{% endtab %}
{% endtabs %}

### 断言

我们为最常见的用例提供了一组内置断言。你也可以实现自己的断言来设计自定义评分系统。查看下一节。

**StringContains**

```php
$this->assert(new StringContains('positive'), $output);
```

**StringContainsAll**

检查输出是否包含所有关键词：

```php
$this->assert(new StringContainsAll(['hello', 'world']), $output);
```

**StringContainsAny**

检查输出是否包含任意关键词：

```php
$this->assert(new StringContainsAny(['success', 'completed']), $output);
```

**StringStartsWith**

检查输出是否以某个前缀开头：

```php
$this->assert(new StringStartsWith('Hello'), $output);
```

**StringEndsWith**

检查输出是否以某个后缀结尾：

```php
$this->assert(new StringEndsWith('!'), $output);
```

**StringLengthBetween**

检查字符串长度是否在范围内：

```php
$this->assert(new StringLengthBetween(10, 100), $output);
```

**StringDistance**

使用 Levenshtein 距离检查字符串相似度：

```php
$this->assert(new StringDistance(
    reference: 'expected text',
    threshold: 0.5, // 最小相似度分数
    maxDistance: 50 // 允许的最大编辑次数
), $output);
```

**StringSimilarity**

使用嵌入向量检查字符串相似度：

```php
use NeuronAI\Evaluation\Assertions\StringSimilarity;
use NeuronAI\RAG\Embeddings\OpenAI\OpenAIEmbeddings;

$this->assert(new StringSimilarity(
    reference: 'The quick brown fox',
    embeddingsProvider: new OpenAIEmbeddings(key: 'YOUR_KEY'),
    threshold: 0.6
), $output);
```

**MatchesRegex**

与正则表达式匹配：

```php
$this->assert(new MatchesRegex('/^\d{3}-\d{2}-\d{4}$/'), $output);
```

**IsValidJson**

检查输出是否为有效的 JSON：

```php
$this->assert(new IsValidJson(), $output);
```

### AI 作为裁判

使用 AI 代理根据自定义标准评估输出。Neuron 为你提供了原始类 AgentJudge 来定义自定义标准，或者你也可以使用内置的裁判断言之一。

```php
use NeuronAI\Evaluation\Assertions\AgentJudge;

class AgentJudgeEvaluator extends BaseEvaluator
{
    protected AgentInterface $judge;

    public function setUp(): void
    {
        $this->judge = Agent::make()
            ->setAiProvider(
                new Antrhopic(...)
            )
            ->setInstructions('You are an expert evaluator for customer support responses.');
    }

    public function getDataset(): DatasetInterface
    {
        return new JsonDataset(...);
    }

    public function run(array $datasetItem): mixed
    {
        $response = MyAgent::make()->chat(
            new UserMessage($datasetItem['input'])
        )->getMessage();
        
        return $response->getContent();
    }
    
    public function evaluate(mixed $output, array $datasetItem): void
    {
        $this->assert(new AgentJudge(
            judge: $this->judge,
            criteria: '响应应当有帮助、礼貌，并直接回答客户的问题',
            threshold: $datasetItem['threshold']
        ), $output);
    }
}
```

#### 忠实性裁判

检查输出是否基于上下文（无幻觉）：

```php
$this->assert(new FaithfulnessJudge(
    judge: $this->judge,
    context: $retrievedDocuments,
    threshold: 0.7
), $output);
```

#### 正确性裁判

与预期答案比较：

```php
$this->assert(new CorrectnessJudge(
    judge: $judge,
    expected: $datasetItem['expected_answer'],
    threshold: 0.7
), $output);
```

#### 相关性裁判

检查输出是否回答了问题：

```php
$this->assert(new RelevanceJudge(
    judge: $judge,
    question: $datasetItem['question'],
    threshold: 0.7
), $output);
```

#### 有用性裁判

评估实用性和可操作性：

```php
$this->assert(new HelpfulnessJudge(
    judge: $judge,
    threshold: 0.7
), $output);
```

### 创建自定义断言

```php
use NeuronAI\Evaluation\Assertions\AbstractAssertion;
use NeuronAI\Evaluation\AssertionResult;

class GreaterThanAssertion extends AbstractAssertion
{
    public function __construct(
        private readonly float $threshold
    ) {}

    public function evaluate(mixed $actual): AssertionResult
    {
        if (!is_numeric($actual)) {
            return AssertionResult::fail(
                0.0,
                '期望数值，得到 ' . gettype($actual),
            );
        }

        if ($actual > $this->threshold) {
            return AssertionResult::pass(1.0);
        }

        return AssertionResult::fail(
            0.0,
            "期望 {$actual} 大于 {$this->threshold}",
        );
    }
}
```

### 输出

评估模块使用一个 PHP 配置文件来控制评估结果的显示方式。配置系统支持多个输出驱动程序，可将结果同时发送到控制台、文件、数据库或外部 API。

#### **配置文件**

在你的项目根目录中创建 `evaluation.php` 文件：

```php
<?php

use NeuronAI\Evaluation\OutputDrivers\ConsoleDriver;
use NeuronAI\Evaluation\OutputDrivers\JsonDriver;

return [
    'output' => [
        // 在控制台中输出结果
        ConsoleDriver::class,
        // new ConsoleDriver(verbose: true),

        // 将结果保存为 json 文件
        new JsonDriver(path: 'evaluation-results.json'),
    ],
];
```

你可以为具有无参构造函数的类提供一个 class-string，或者直接提供一个具体实例。

**如果不存在配置文件**，系统默认使用 `ConsoleDriver` 并使用标准输出。

#### 创建自定义输出

实现 `EvaluationOutputInterface` 以创建自定义输出驱动：

```php
namespace App\Neuron\Evaluations;

use NeuronAI\Evaluation\Contracts\EvaluationOutputInterface;
use NeuronAI\Evaluation\Runner\EvaluatorSummary;

class DatabaseOutput implements EvaluationOutputInterface
{
    public function __construct(
        private readonly \PDO $pdo,
        private readonly string $table = 'evaluations'
    ) {}

    public function output(EvaluatorSummary $summary): void
    {
        $stmt = $this->pdo->prepare(
            "INSERT INTO {$this->table} (passed, failed, success_rate, total_time, created_at, updated_at) VALUES (?, ?, ?, ?, NOW(), NOW())"
        );
        $stmt->execute([
            $summary->getPassedCount(),
            $summary->getFailedCount(),
            $summary->getSuccessRate(),
            $summary->getTotalExecutionTime(),
        ]);
    }
}
```

一旦创建了输出类，就可以将其注册到配置文件中，以便下次运行评估时使用。

```php
<?php

use NeuronAI\Evaluation\OutputDrivers\ConsoleDriver;
use NeuronAI\Evaluation\OutputDrivers\JsonDriver;

return [
    'output' => [
        // 在控制台中输出结果
        new ConsoleDriver(verbose: true),

        // 将结果保存为 json 文件
        //new JsonDriver(path: 'evaluation-results.json'),
        
        // 将结果保存到数据库
        new DatabaseOutput(
            pdo: new \PDO(...),
            table: 'evaluations',
        )
    ],
];
```

### 并行评估

默认情况下，评估命令一次处理一个数据集条目。由于大多数评估器的大部分时间都花在等待 AI 提供方响应上，你可以通过使用 `--concurrency` 选项，并行处理多个数据集条目，从而大幅减少总运行时间：

```bash
vendor/bin/neuron evaluation path/to/evaluators --concurrency=3
```

使用 `--concurrency=3`时，最多有 3 个数据集条目同时被评估，每个条目都在各自的 PHP 子进程中运行。对于一个包含 100 个条目的数据集，如果每个条目都需要一次 2 秒的 LLM 调用，评估时间会从大约 200 秒降到大约 66 秒。

#### 要求

并行执行依赖进程 fork，这要求：

* 该 [pcntl](https://www.php.net/manual/en/book.pcntl.php) PHP 扩展（可在 Linux 和 macOS 上使用——Windows 上不可用）
* 该 [spatie/fork](https://github.com/spatie/fork) 包：

```bash
composer require --dev spatie/fork
```

如果缺少其中任一项，命令会打印提示并自动回退到顺序执行，因此同一命令可在任何环境中运行。

#### 选择并发级别

每个正在运行的条目都是对你的 AI 提供方的一次活动请求。先从一个中等值（3–5）开始，只要你没有触发提供方的速率限制，就继续增大。如果你看到速率限制错误作为测试失败出现，就把这个值调低。

#### 其工作方式，以及需要注意什么

每个数据集条目都会在评估器的一个 fork 复制中运行，其结果会返回给父进程。这带来了一些实际影响：

* **结果不受影响。** 条目会独立评估，结果保持其数据集顺序，最终报告与顺序运行完全一致。
* **状态不会在条目之间共享。** 每个条目看到的评估器状态是 `setUp()`之后的状态。处理某个条目时产生的副作用（递增属性、追加文件）对其他条目不可见。如果你的评估器依赖于在条目之间累积状态，请保持顺序运行。
* **输出必须可序列化。** 由 `run()` 返回的值会跨越进程边界，通过 `serialize()`传递。如果它无法被序列化（例如包含闭包或打开的连接），断言结果会被保留，但报告中显示的输出会被替换为占位字符串。

#### 执行时间报告

报告的总时间是本次运行真实的墙钟时间，而每个测试的平均时间反映的是每个独立条目的实际持续时间——因此在并行执行下，每个测试的平均时间可能大于总时间除以测试数量。

## 评估多轮对话

你目前看到的评估器遵循一个简单模式：运行你的代理一次，得到一个输出，然后对其进行断言。这对单次任务很有效，但无法反映 agentic 应用的真实工作方式。AI Agent 的价值体现在整个对话过程中：它在多轮交互中收集信息，决定调用哪些工具以及使用哪些参数，在执行危险操作前向人类请求批准，并在批准被拒绝时恢复。

仅评估最终回复会错过这些行为中的大部分。代理可能给出了非常礼貌的回答，却调用了错误的工具、跳过了必要的查询，或者执行了本应先提交审批的操作。

Neuron 提供两个组件来弥补这一差距，而一句话就能概括它们的关系： **你运行一个 Conversation；你评估它的 Trajectory。**

* `Conversation` 是执行辅助器。它在评估器的 `run()` 方法中驱动你的代理完成多轮交互——传递用户回合、回应审批请求，甚至让另一位 AI 扮演用户。
* `Trajectory` 是被记录的主体。它包装了对话消息，并回答关于实际发生情况的评估问题：调用了哪些工具、使用了什么参数、哪些请求被批准或拒绝，以及代理最终说了什么。

### 在

使用 `Conversation` 中运行一个 Conversation `run()` 来编排多轮交互。每一轮都只会在前一轮完全完成之后才会发送：

```php
use NeuronAI\Evaluation\BaseEvaluator;
use NeuronAI\Evaluation\Contracts\DatasetInterface;
use NeuronAI\Evaluation\Conversation\Conversation;
use NeuronAI\Evaluation\Dataset\JsonDataset;

class RefundConversationEvaluator extends BaseEvaluator
{
    public function getDataset(): DatasetInterface
    {
        return new JsonDataset(__DIR__ . '/datasets/refunds.json');
    }

    public function run(array $datasetItem): mixed
    {
        return Conversation::make(MyAgent::make())
            ->withTurns($datasetItem['turns'])
            ->run();
    }

    public function evaluate(mixed $trajectory, array $datasetItem): void
    {
        // 对返回的 Trajectory 进行断言（下一节）
    }
}
```

这些轮次是一个简单列表，因此可以直接放在你的 JSON 数据集中：

```json
[
    {
        "turns": [
            "Hi, I want a refund for order #123",
            "Yes, please proceed."
        ]
    }
]
```

条目可以是普通字符串，也可以是 `UserMessage` 实例，因此你也可以编排携带图像或文档的轮次。

### Trajectory

聊天历史只是原始消息列表，直接对其进行断言意味着在每个评估器中都要编写相同的样板代码：将工具调用与其结果匹配、读取审批状态、提取最终答案。 `Trajectory` 由 `Conversation::run()` 返回的

```php
$trajectory->toolCalls();                // 按执行顺序列出所有工具调用
$trajectory->toolCalls('refund_order'); // 按工具名称过滤
$trajectory->lastToolCall();            // 最近一次调用（或 null）
$trajectory->finalAnswer();             // 代理的最后回复（如果没有则为 ''）
$trajectory->userMessages();            // 对话中的用户侧消息
$trajectory->usage();                   // 整个对话的聚合 token 使用量
$trajectory->toTranscript();            // 人类可读的转录稿（AI 裁判会读这个）
$trajectory->messages();                // 底层消息，完整保真
```

`toolCalls()` 返回框架自身的 `ToolInterface` 对象，因此你可以用你已经熟悉的 API 来检查它们—— `getInputs()`, `getResult()`, `getApprovalState()`。当历史中同一个调用出现两次时（代理提出时的待处理快照，以及运行后的最终结果），Trajectory 会将它们合并为一条记录，以最终结果为准。

你不需要 `Conversation` 这个辅助器就能得到 Trajectory。如果你自己驱动代理——自定义循环、流式处理、定制工作流——你也可以投影任意聊天历史并使用相同的断言：

```php
use NeuronAI\Evaluation\Trajectory\Trajectory;

$trajectory = Trajectory::fromChatHistory($agent->getChatHistory());
```

由于 `usage()` 会汇总每一轮中提供方报告的 token 数，因此你也可以把成本回退监控纳入评估套件：

```php
$this->assert(
    new GreaterThanAssertion($datasetItem['token_budget']),
    $trajectory->usage()->getTotal()
);
```

### Trajectory 断言

这些断言回答了最终输出检查无法回答的问题：代理是否走了正确的路径得出答案？

#### **ToolWasCalled**

验证代理是否调用了某个工具，也可选择约束其参数。数组形式是子集匹配——每个列出的键都必须存在且相等，允许额外参数：

```php
use NeuronAI\Evaluation\Assertions\Trajectory\ToolWasCalled;

// 工具被调用，参数不限
$this->assert(new ToolWasCalled('search_orders'), $trajectory);

// 工具以这些参数被调用
$this->assert(
    new ToolWasCalled('refund_order', ['order_id' => '123']),
    $trajectory
);

// 使用回调函数完全控制
$this->assert(
    new ToolWasCalled('refund_order', fn (array $inputs): bool => $inputs['amount'] <= 100),
    $trajectory
);
```

#### **ToolWasNotCalled**

护栏断言——验证代理是否没有 *不* 走一条禁止的路径：

```php
use NeuronAI\Evaluation\Assertions\Trajectory\ToolWasNotCalled;

$this->assert(new ToolWasNotCalled('delete_account'), $trajectory);
```

#### **TrajectoryMatches**

根据预期的工具名称列表验证工具调用的顺序。 `模式` 枚举决定比较有多严格：

```php
use NeuronAI\Evaluation\Assertions\Trajectory\Mode;
use NeuronAI\Evaluation\Assertions\Trajectory\TrajectoryMatches;

$this->assert(
    new TrajectoryMatches(['search_orders', 'refund_order'], Mode::Subset),
    $trajectory
);
```

* `Mode::Strict` ——必须严格按照这个顺序的这些调用，不能有其他内容。最适合固定回归基线。
* `Mode::Unordered` ——同样的调用，顺序不限。
* `Mode::Subset` ——预期调用按这个顺序出现，中间允许有额外调用。适合你关注关键路径但能容忍偶发调用的情况。
* `Mode::Superset` ——不允许出现任何超出预期集合的调用。预期列表充当允许列表。

预期列表只是普通字符串，因此可以和对话轮次一起放在你的数据集中。

#### **ToolWasApproved / ToolWasRejected**

验证记录在工具调用上的人工决定（下一节会说明这些决定在评估中是如何产生的）：

```php
use NeuronAI\Evaluation\Assertions\Trajectory\ToolWasApproved;
use NeuronAI\Evaluation\Assertions\Trajectory\ToolWasRejected;

$this->assert(new ToolWasApproved('send_email'), $trajectory);
$this->assert(new ToolWasRejected('refund_order'), $trajectory);
```

请注意，这里没有针对最终答案的专门断言：前面各节学到的内容仍然适用。把 `$trajectory->finalAnswer()` 传给字符串断言，或者传给 AI 评审：

```php
$this->assert(new StringContains('无法处理'), $trajectory->finalAnswer());
```

### 评估人在环

如果你的代理使用 `ToolApproval` 中间件， `chat()` 在请求受限工具时不会返回答案——它会挂起，等待人工决定。在生产环境中，人会看到待处理操作并批准或拒绝它。在评估中没有人，因此如果没有帮助，对话就会直接停住。

与此同时，这些恰恰是你最想测试的流程：代理是否会在危险操作前请求批准？当人类说不时，它能否优雅恢复？

该 `withApprovals()` 方法允许你为审批者编写脚本。你传入的可调用对象会在代理挂起时被调用——无论对话中的哪个时刻——并返回这些决定：

```php
use NeuronAI\Evaluation\Trajectory\Trajectory;
use NeuronAI\Workflow\Interrupt\ApprovalRequest;

Conversation::make($agent)
    ->withTurns($datasetItem['turns'])
    ->withApprovals(function (ApprovalRequest $request, Trajectory $soFar): array {
        $payload = [];

        foreach ($request->getActions() as $action) {
            if ($action->isPending()) {
                $payload[$action->id] = 'approve';   // 或 ['reject', '原因']
            }
        }

        return $payload;
    })
    ->run();
```

它是一个独立的方法——不是轮次脚本中的一项——因为你无法提前知道在 *哪一个* 轮模型会决定调用受限工具。该策略不依赖位置：只要发生挂起，它就会作出响应。

第二个参数会给你截至目前的对话，这就是你编写依赖于工具参数的决策的方式：

```php
->withApprovals(function (ApprovalRequest $request, Trajectory $soFar): array {
    $payload = [];

    foreach ($request->getActions() as $action) {
        $args = $soFar->lastToolCall($action->name)?->getInputs();

        $payload[$action->id] = ($args['amount'] ?? 0) > 100 ? ['reject', '高于自动批准阈值'] : 'approve';
            ? ['reject', '超过自动批准阈值']
            : 'approve';
    }

    return $payload;
})
```

有两条规则确保评估保持诚实，两条都通过以下内容强制执行： `EvaluationException` 它会将数据集项标记为错误：

* 如果代理挂起而没有配置策略，评估就会明确失败。故意没有“全部批准”的默认行为——那会悄悄伪造你正在测量的真实人类行为。
* 返回的载荷必须为每个待处理操作包含一个决定。Neuron 的审批系统绝不会把沉默视为同意，而不完整的决定集会让工作流永远挂起。

拒绝、传给模型的原因，以及代理的恢复过程，都会记录到 Trajectory 中——在那里 `ToolWasRejected` 以及对……的字符串断言 `finalAnswer()` 都可以验证它们。

### 模拟用户

脚本化的轮次列表有一个限制：它无法对代理实际说的话做出反应。如果代理提出了你的脚本未预料到的澄清问题，下一轮脚本内容就会显得前后脱节。真实用户会适应——而测试你的代理如何应对一个会适应的对手，也需要这样一个对手。

`UserSimulator` 是一个扮演用户的 AI 代理。你给它一个人设和一个目标，它会在每一步读取截至目前的对话，并决定：继续发一条新消息，还是因为目标已达成（或者因为它放弃了）而停止：

```php
use NeuronAI\Evaluation\Conversation\UserSimulator;

$simulator = UserSimulator::make()
    ->withPersona('一个没有耐心、只给简短回答的顾客')
    ->withGoal('为订单 #123 退款');

$simulator->setAiProvider(new Anthropic(key: '...', model: '...'));

$trajectory = Conversation::make($agent)
    ->withUser($simulator, maxTurns: 10)
    ->run();
```

`withUser()` 替代 `withTurns()` ——一次对话要么是脚本化的，要么是模拟的，不能两者兼有。有几点需要知道：

* `maxTurns` 是必需的。它是用户轮次数量的硬上限，不存在无限默认值。达到上限会结束对话 *正常*：未完成的对话是否算失败，由你的断言来判断（ `TaskCompletionJudge` 下面的内容最为契合），而不是由运行器决定。
* 模拟器会自行决定何时达成目标——无需另一个裁判模型参与。
* 模拟器绝不会响应审批请求。在生产环境中，与代理聊天的用户和批准其操作的人类并不是同一个人，而评估会保留这种分离：审批仍然由 `withApprovals()`，在模拟对话中也是如此。

由于 `UserSimulator` 是一个普通的 Neuron Agent，因此你可以用任何提供方来支持它——包括比被测试模型更便宜的模型。

### 评判整个对话

前面看到的 AI 评审只会评估一段文本。但有些问题只有查看整个交互才能回答：考虑到它运行的工具、被拒绝的批准，以及一路上说过的所有内容，代理是否真的完成了用户想要的事情？

现在所有评审都接受一个 `Trajectory` 直接传入 Trajectory。传入后，评审会收到完整的对话记录——用户和助手的轮次、带参数和结果的工具调用，以及带理由的审批决定：

```php
use NeuronAI\Evaluation\Assertions\Judges\TaskCompletionJudge;

$this->assert(
    new TaskCompletionJudge(
        judge: $this->judge,
        goal: $datasetItem['goal'],
        threshold: 0.7
    ),
    $trajectory
);
```

`TaskCompletionJudge` 这个评审正是为这个问题而调校。它会根据整个轨迹来评估目标完成度，并且知道如何衡量人在环的结果：如果代理的操作被审批者拒绝，就要根据它如何处理拒绝来评判，而不是因为被拒本身而惩罚它。

通用的 `AgentJudge` 以及其他预配置评审也以同样方式接受 Trajectory，因此你可以针对完整对话来评估语气、帮助性或自定义标准，而不是只评估单条消息。

### 把这些整合起来

一个带人工审批的退款场景完整评估器：

```php
use NeuronAI\Evaluation\Assertions\Judges\TaskCompletionJudge;
use NeuronAI\Evaluation\Assertions\StringContains;
use NeuronAI\Evaluation\Assertions\Trajectory\Mode;
use NeuronAI\Evaluation\Assertions\Trajectory\ToolWasRejected;
use NeuronAI\Evaluation\Assertions\Trajectory\TrajectoryMatches;
use NeuronAI\Evaluation\BaseEvaluator;
use NeuronAI\Evaluation\Contracts\DatasetInterface;
use NeuronAI\Evaluation\Conversation\Conversation;
use NeuronAI\Evaluation\Dataset\JsonDataset;
use NeuronAI\Evaluation\Trajectory\Trajectory;
use NeuronAI\Workflow\Interrupt\ApprovalRequest;

class RefundConversationEvaluator extends BaseEvaluator
{
    private AgentInterface $judge;

    public function setUp(): void
    {
        $this->judge = JudgeAgent::make();
    }

    public function getDataset(): DatasetInterface
    {
        return new JsonDataset(__DIR__ . '/datasets/refunds.json');
    }

    public function run(array $datasetItem): mixed
    {
        return Conversation::make(RefundAgent::make())
            ->withTurns($datasetItem['turns'])
            ->withApprovals(function (ApprovalRequest $request, Trajectory $soFar) use ($datasetItem): array {
                $payload = [];

                foreach ($request->getActions() as $action) {
                    $payload[$action->id] = $datasetItem['decisions'][$action->name] ?? 'approve';
                }

                return $payload;
            })
            ->run();
    }

    public function evaluate(mixed $trajectory, array $datasetItem): void
    {
        // 代理遵循了关键路径
        $this->assert(new TrajectoryMatches($datasetItem['expected_tools'], Mode::Subset), $trajectory);

        // 退款被审批者拒绝了
        $this->assert(new ToolWasRejected('refund_order'), $trajectory);

        // ……而且代理传达了这一点，而不是假装它成功了
        $this->assert(new StringContains('无法'), $trajectory->finalAnswer());

        // 总体而言：在拒绝的情况下，这段对话是否仍然服务了用户？
        $this->assert(new TaskCompletionJudge($this->judge, goal: $datasetItem['goal']), $trajectory);
    }
}
```

使用匹配的数据集：

```json
[
    {
        "goal": "为订单 #123 退款",
        "turns": [
            "Hi, I want a refund for order #123",
            "Yes, please proceed."
        ],
        "decisions": {
            "refund_order": ["reject", "金额超过自动退款上限"]
        },
        "expected_tools": ["search_orders", "refund_order"]
    }
]
```

你对评估框架了解的其他内容都保持不变：Trajectory 只是以下内容的输出： `run()`，因此并行执行、输出驱动和 CLI 运行器都像以前一样正常工作。
