> 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/agent/testing.md).

# 测试

当你测试一个代理时，你不希望每次测试运行都真正向 OpenAI、Anthropic 或任何其他提供商发起 API 调用。真实调用速度慢、费用高，而且每次返回的结果都不同，这会让你的测试不稳定且昂贵。RAG 代理也是如此：你不想为了验证代理的逻辑就启动一个向量数据库或调用嵌入 API。

Neuron 自带可直接替换的测试替身，解决了这个问题。 `FakeAIProvider` 替换 AI 提供者， `FakeEmbeddingsProvider` 替换嵌入提供者，以及 `FakeVectorStore` 替换向量存储。它们返回预设响应，绝不会访问网络，并记录每一次交互，因此你可以精确断言你的代理做了什么。

### 设置

创建一个 `FakeAIProvider` 用你期望模型返回的响应进行配置，然后将其注入到你的代理中：

```php
use NeuronAI\Chat\Messages\Stream\AssistantMessage;
use NeuronAI\Testing\FakeAIProvider;

$provider = new FakeAIProvider(
    new AssistantMessage('你好！我能帮你什么？')
);

$agent = MyAgent::make()->setAiProvider($provider);
```

响应会按顺序返回。如果你的代理对提供者进行了多次调用（例如工具调用），请排队多个响应：

```php
$provider = new FakeAIProvider(
    new AssistantMessage('第一条响应'),
    new AssistantMessage('第二条响应'),
);
```

### 聊天

```php
public function test_agent_responds(): void
{
    $provider = new FakeAIProvider(
        new AssistantMessage('法国的首都是巴黎。')
    );

    $agent = MyAgent::make()->setAiProvider($provider);

    $message = $agent->chat(new UserMessage('法国的首都是什么？'))->getMessage();

    $this->assertSame('法国的首都是巴黎。', $message->getContent());
    $provider->assertCallCount(1);
}
```

### 流式传输

假的提供者会将响应文本拆分成多个块，模拟真实流：

```php
public function test_agent_streams_response(): void
{
    $provider = new FakeAIProvider(
        new AssistantMessage('Hello world')
    );

    $agent = MyAgent::make()->setAiProvider($provider);

    $handler = $agent->stream(new UserMessage('Hi'));

    $chunks = [];
    foreach ($handler->events() as $event) {
        if ($event instanceof \NeuronAI\Chat\Messages\Stream\Chunks\TextChunk) {
            $chunks[] = $event->content;
        }
    }

    // 默认情况下，响应会被拆分为每 5 个字符一个块
    $this->assertSame(['Hello', ' worl', 'd'], $chunks);

    // 流被消费后，最终消息就可用
    $state = $handler->run();
    $this->assertSame('Hello world', $state->getMessage()->getContent());
}
```

你可以使用 `setStreamChunkSize()`:

```php
$provider->setStreamChunkSize(10);
```

### 结构化输出

提供一个与输出类 schema 匹配的 JSON 字符串。代理将像往常一样对其进行反序列化和验证：

```php
public function test_agent_returns_structured_output(): void
{
    $provider = new FakeAIProvider(
        new AssistantMessage('{"name": "Alice"}')
    );

    $agent = MyAgent::make()->setAiProvider($provider);

    $user = $agent->structured(new UserMessage('生成一个用户'), User::class);

    $this->assertInstanceOf(User::class, $user);
    $this->assertSame('Alice', $user->name);
}
```

### 工具调用

当模型决定调用某个工具时，它会返回一个 `ToolCallMessage`。代理执行该工具，然后回到提供者获取最终答案。请将两次响应都排队：

```php
use NeuronAI\Chat\Messages\ToolCallMessage;

public function test_agent_uses_tools(): void
{
    $searchTool = Tool::make('search', 'Search the web')
        ->addProperty(new ToolProperty('query', PropertyType::STRING, 'Search query', true))
        ->setCallable(fn (string $query): string => "{$query} 的结果：");

    $provider = new FakeAIProvider(
        // 第一次调用：模型请求使用搜索工具
        new ToolCallMessage(null, [
            (clone $searchTool)->setCallId('call_1')->setInputs(['query' => 'PHP frameworks']),
        ]),
        // 第二次调用：模型使用工具结果进行响应
        new AssistantMessage('以下是顶级的 PHP 框架...')
    );

    $agent = MyAgent::make()
        ->setAiProvider($provider)
        ->addTool($searchTool);

    $message = $agent->chat(new UserMessage('最好的 PHP 框架？'))->getMessage();

    $this->assertSame('以下是顶级的 PHP 框架...', $message->getContent());
    $provider->assertCallCount(2);
}
```

### 断言

`FakeAIProvider` 包含可在测试中使用的内置断言：

```php
// 验证提供者调用总数
$provider->assertCallCount(2);

// 按方法验证调用次数
$provider->assertMethodCallCount('chat', 1);
$provider->assertMethodCallCount('stream', 1);

// 验证没有发起任何调用
$provider->assertNothingSent();

// 验证系统提示词
$provider->assertSystemPrompt('You are a helpful assistant.');

// 验证已配置工具
$provider->assertToolsConfigured(['search', 'calculator']);

// 带回调的自定义断言
$provider->assertSent(fn (RequestRecord $record): bool =>
    $record->method === 'chat'
    && $record->messages[0]->getContent() === 'Hello'
);
```

### 检查请求

如需更高级的检查，可以访问原始记录的请求：

```php
$records = $provider->getRecorded();

$records[0]->method;          // 'chat'、'stream' 或 'structured'
$records[0]->messages;        // 发送给提供者的 Message[]
$records[0]->systemPrompt;    // 调用时的系统提示词
$records[0]->tools;           // 调用时配置的工具
$records[0]->structuredClass; // 输出类（仅结构化调用）
$records[0]->structuredSchema; // JSON schema（仅结构化调用）
```

## RAG

RAG 代理除了 AI 提供者外，还依赖嵌入提供者和向量存储。Neuron 提供 `FakeEmbeddingsProvider` 和 `FakeVectorStore` 以在测试中同时替换这两者。

#### FakeEmbeddingsProvider

生成确定性的嵌入，而无需调用任何外部 API。你需要嵌入提供者的任何地方都可以直接使用它：

```php
use NeuronAI\Testing\FakeEmbeddingsProvider;

$embeddings = new FakeEmbeddingsProvider();
```

#### FakeVectorStore

返回预设文档，来自 `similaritySearch()` 与传入的嵌入无关。将你想返回的文档传入构造函数：

```php
use NeuronAI\RAG\Document;
use NeuronAI\Testing\FakeVectorStore;

$vectorStore = new FakeVectorStore([
    new Document('巴黎是法国的首都。'),
    new Document('柏林是德国的首都。'),
]);
```

#### RAG 聊天

```php
public function test_rag_answers_from_documents(): void
{
    $provider = new FakeAIProvider(
        new AssistantMessage('巴黎是法国的首都。')
    );

    $vectorStore = new FakeVectorStore([
        new Document('法国是欧洲的一个国家。它的首都是巴黎。'),
    ]);

    $rag = MyRAG::make()
        ->setAiProvider($provider);
        ->setEmbeddingsProvider(new FakeEmbeddingsProvider());
        ->setVectorStore($vectorStore);

    $message = $rag->chat(new UserMessage('法国的首都是什么？'))->getMessage();

    $this->assertSame('巴黎是法国的首都。', $message->getContent());
    $provider->assertCallCount(1);
    $vectorStore->assertSearchCount(1);
}
```

#### 添加文档

测试你的索引管道是否正确地嵌入并存储文档：

```php
public function test_documents_are_embedded_and_stored(): void
{
    $embeddings = new FakeEmbeddingsProvider();
    $vectorStore = new FakeVectorStore();

    $rag = MyRAG::make()
        ->setAiProvider(new FakeAIProvider());
        ->setEmbeddingsProvider($embeddings);
        ->setVectorStore($vectorStore);

    $rag->addDocuments([
        new Document('第一份文档'),
        new Document('第二份文档'),
    ]);

    $embeddings->assertCallCount(2);
    $vectorStore->assertDocumentCount(2);
    $vectorStore->assertHasDocumentWithContent('第一份文档');
}
```

#### RAG 断言

```php
// FakeEmbeddingsProvider
$embeddings->assertCallCount(2);
$embeddings->assertEmbeddedText('某段特定文本');
$embeddings->assertNothingEmbedded();

// FakeVectorStore
$vectorStore->assertSearchCount(1);
$vectorStore->assertDocumentCount(3);
$vectorStore->assertHasDocumentWithContent('预期内容');
$vectorStore->assertNothingStored();
```
