> 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/structured-output.md).

# 结构化输出

{% hint style="info" %}

### 前置条件

本指南假设你已经熟悉以下概念：

* [Agent](/neuron-v3-zh/agent/agent.md)
* [工具与函数调用](/neuron-v3-zh/agent/tools.md)
  {% endhint %}

在许多用例中，我们需要 Agent 理解自然语言，但输出为 *结构化格式*。一个常见用例是从文本中提取数据，以插入数据库或供其他下游系统使用。本指南介绍 Neuron 如何让你强制 Agent 输出结构化结果。

<figure><img src="/files/f11a033673629fd43c742dd9a14d24991351a32c" alt=""><figcaption></figcaption></figure>

{% embed url="<https://www.youtube.com/watch?v=T8PM-t_AQ-c>" %}

### 如何使用结构化输出

核心概念是，LLM 响应的输出结构需要以某种方式表示出来。Neuron 用于校验的 schema 由 PHP 类型提示定义。基本上，你必须定义一个具有严格类型属性的类：

```php
<?php

namespace App\Neuron\Output;

use NeuronAI\StructuredOutput\SchemaProperty;

class Person 
{
    #[SchemaProperty(
        description: '用户名称。', 
        required: true
    )]
    public string $name;
    
    #[SchemaProperty(
        description: '用户喜欢吃什么。', 
        required: false
    )]
    public string $preference;
}
```

Neuron 会从 PHP 对象生成相应的 JSON schema，以向底层模型说明你需要的数据格式。然后 Agent 会解析 LLM 输出以提取数据，并返回一个填充了相应值的对象实例：

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

// 与需要结构化输出的 Agent 交互
$person = MyAgent::make()->structured(
    new UserMessage("我是约翰，我喜欢披萨！"),
    Person::class
);

echo $person->name.' 喜欢 '.$person->preference;
// 约翰喜欢披萨
```

### 默认输出类

你也可以将输出格式封装到 Agent 实现中，使其成为 Agent 的标准输出格式。你始终需要调用 `structured()` 方法来要求严格输出。

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

// 封装默认输出格式 
class MyAgent extends Agent
{
    ...

    protected function getOutputClass(): string
    {
        return Person::class;
    }
}

// 如果你想获得结构化输出，请始终使用 structured 方法
$person = MyAgent::make()
    ->structured(new UserMessage("我是约翰，我喜欢披萨"));

echo $person->name.' 喜欢 '.$person->preference;
// 约翰喜欢披萨
```

### 控制输出生成

Neuron 要求你定义两层规则来创建结构化输出类。

第一层是 `SchemaProperty` 属性，它允许你控制发送给 LLM 的 JSON schema，以便其理解所需的数据格式。

第二层是验证。验证属性可确保从 LLM 响应中收集到的数据与你的要求一致。

<figure><img src="/files/633e59fc53054494374df8541a5a7c64d8929b1a" alt=""><figcaption></figcaption></figure>

### SchemaProperty

该 `SchemaProperty` 属性允许你定义每个属性的 JSON schema 参数：

```php
<?php

namespace App\Neuron\Output;

use NeuronAI\StructuredOutput\SchemaProperty;

class Person 
{
    #[SchemaProperty(
        description: '用户名称。',
        required: true,
        minLength: 3,
        maxLength: 255,
    )]
    public string $name;
    
    #[SchemaProperty(
        description: '用户喜欢吃什么。', 
        required: false,
        min: 18,
        max: 64,
    )]
    public ?int $age = null;
}
```

### 嵌套类

你可以使用其他 PHP 对象作为属性类型来构建复杂的输出结构。以 `Person` 类为例，我们可以添加 `address` 这个属性，其类型为另一个结构化类。

```php
<?php

namespace App\Neuron\Output;

use NeuronAI\StructuredOutput\Property;
use Symfony\Component\Validator\Constraints\NotBlank;
use Symfony\Component\Validator\Constraints\Valid;

class Person 
{
    #[SchemaProperty(
        description: '用户名称。', 
        required: true
    )]
    #[NotBlank]
    public string $name;
    
    #[SchemaProperty(
        description: '用户喜欢吃什么。', 
        required: true
    )]
    public string $preference;
    
    #[SchemaProperty(
        description: '用于完成配送的地址。', 
        required: true
    )]
    public Address $address;
}
```

在 `Address` 在定义中，我们只要求 street 和 zip code 属性，并允许 city 为空。

```php
<?php

namespace App\Neuron\Output;

use NeuronAI\StructuredOutput\SchemaProperty;
use NeuronAI\StructuredOutput\Validation\Rules\NotBlank;

class Address
{
    #[SchemaProperty(
        description: '街道名称。', 
        required: true
    )]
    #[NotBlank]
    public string $street;

    #[SchemaProperty(
        description: '城市名称。', 
        required: false
    )]
    public string $city;

    #[SchemaProperty(
        description: '地址的邮政编码。', 
        required: true
    )]
    #[NotBlank]
    public string $zip;
}
```

现在，当你向 Agent 请求结构化输出时，你将得到填充好的实例：

```php
use NeuronAI\Chat\Messages\UserMessage;
use NeuronAI\Observability\AgentMonitoring;

// 与需要结构化输出的 Agent 交互
$person = MyAgent::make()->structured(
    new UserMessage("我是约翰，我想要一个披萨，送到圣詹姆斯街 00560！"),
    Person::class
);

echo $person->name.' 喜欢 '.$person->preference.'. 地址：'.$person->address->street;
// 约翰喜欢披萨。地址：圣詹姆斯街
```

## 数组

如果你将某个属性声明为数组，Neuron 会假定其中的项目列表是字符串列表。如果你希望数组包含其他结构化对象的列表，可以使用 `anyOf` 参数：

```php
<?php

namespace App\Neuron\Output;

use NeuronAI\StructuredOutput\SchemaProperty;
use NeuronAI\StructuredOutput\Validation\Rules\NotBlank;

class Person 
{
    #[SchemaProperty(
        description: '用户名称。', 
        required: true
    )]
    #[NotBlank]
    public string $name;
    
    #[SchemaProperty(
        description: '用户资料标签列表。', 
        required: true,
        anyOf: [Tag::class]
    )]
    public array $tags;
}
```

下面是假设性的 `Tag` 类实现，它有自己的验证规则和属性信息：

```php
<?php

namespace App\Neuron\Output;

use NeuronAI\StructuredOutput\SchemaProperty;
use NeuronAI\StructuredOutput\Validation\Rules\NotBlank;

class Tag
{
    #[SchemaProperty(
        description: '标签名称。', 
        required: true,
    )]
    #[NotBlank]
    public string $name;
}
```

#### 多类型数组

从上面的示例可以看出，anyOf 参数是一个数组。Neuron 也支持由多种对象类型组成的数组。只需列出数组可以包含的结构化对象，Neuron 就会将它们的所有规格包含到供 LLM 使用的 JSON schema 中。

```php
class Report
{
    #[SchemaProperty(
        description: '报告内容。', 
        required: true,
        anyOf: [TextBlock::class, TableBlock::class, ImageBlock::class]
    )]
    public array $content;
}
```

## 最大重试次数

由于 LLM 并非完全确定性，因此如果 LLM 响应中缺少某些内容，必须设置重试机制。

默认情况下，Neuron 会从 LLM 响应中提取并验证数据；如果存在一个或多个验证错误，它会自动再重试一次请求，并向 LLM 说明哪里出错了以及涉及哪些属性。

你可以自行自定义 Agent 为从 LLM 获取正确答案而必须重试的次数：

```php
$person = MyAgent::make()->structured(
    messages: new UserMessage("我是约翰，我喜欢披萨！"),
    class: Person::class,
    maxRetries: 3
);
```

如果你使用能力较弱的 LLM，可以考虑设置一个折中的重试次数，在获得有效答案的概率和潜在的 token 消耗之间取得平衡。

你只需传入 0 即可禁用重试。这将成为一次性尝试：

```php
$person = MyAgent::make()->structured(
    messages: new UserMessage("我是约翰，我喜欢披萨！"),
    class: Person::class,
    maxRetries: 0
);
```

## 监控与调试

使用 Neuron 构建的许多应用程序会包含多个步骤以及对 LLM 的多次调用。随着这些应用程序变得越来越复杂，能够检查你的代理系统内部到底发生了什么就变得至关重要。实现这一点的最佳方式是使用 [Inspector](https://inspector.dev/).

{% embed url="<https://docs.inspector.dev/guides/neuron-ai>" %}

<figure><img src="/files/2b19770d81b78e57f68727209de2d7b1ebd40754" alt=""><figcaption></figcaption></figure>

每个环节都会提供自己的调试信息，以便实时跟踪 Agent 的执行：

<figure><img src="/files/c269327c01ef3c45efe69a311046ad566de7a362" alt=""><figcaption></figcaption></figure>

{% hint style="info" %}
了解如何启用 [**可观测性**](/neuron-v3-zh/agent/observability.md) ，请看下一节。
{% endhint %}

## 验证规则

由于 LLM 是非确定性系统，其输出的一致性会受到输入上下文质量的显著影响，而且它们仍可能产生幻觉，因此我们提供了一组验证规则，你可以将其添加到结构化类属性中，以指示 Neuron 验证 LLM 生成的最终数据集。

如果一个或多个属性无效，验证规则允许 Neuron 连同详细的错误报告一起多次重新向 LLM 发送生成请求，直到达到 [maxRetries](#max-retries) 值。

如果你没有定义任何验证规则，从 LLM 响应中提取的数据将直接填充到结构化输出类中。

### #\[NotBlank]

正在验证的属性不能为空。它接受 `allowNull` 标志，用于决定是否将显式的 null 值视为空值等价。

```php
namespace App\Neuron\Output;

use NeuronAI\StructuredOutput\Validation\Rules\NotBlank;

class Person 
{
    #[NotBlank(allowNull: false)]
    public string $name;
}
```

### #\[Length]

判断一个 `字符串` 是否符合给定条件：

```php
namespace App\Neuron\Output;

use NeuronAI\StructuredOutput\Validation\Rules\Length;

class Person 
{
    #[Length(min: 1, max: 10)]
    public string $name;
    
    #[Length(exactly: 5)]
    public string $zip_code;
}
```

### #\[WordsCount]

判断一个中的单词数量 `字符串` 是否符合给定条件：

```php
namespace App\Neuron\Output;

use NeuronAI\StructuredOutput\Validation\Rules\WordsCount;

class Person 
{
    #[WordsCount(exactly: 10)]
    public string $title;
    
    #[WordsCount(min: 1, max: 10)]
    public string $content;
}
```

### #\[Count]

判断一个 `数组` 是否符合给定条件：

```php
namespace App\Neuron\Output;

use NeuronAI\StructuredOutput\Validation\Rules\Count;

class Person 
{
    #[Count(min: 1, max: 3)]
    public array $dogs;
    
    #[Count(exactly: 1)]
    public array $children;
}
```

### #\[EqualTo] - #\[NotEqualTo]

这些规则具有相同的结构和含义，并接受一个参数来定义要比较的值。正在验证的属性必须严格等于（*#\[EqualTo]*）或不同于（*#\[NotEqualTo]*）参考值：

```php
namespace App\Neuron\Output;

use NeuronAI\StructuredOutput\Validation\Rules\EqualTo;
use NeuronAI\StructuredOutput\Validation\Rules\NotEqualTo;

class Person 
{
    #[EqualTo(reference: 'Rome')]
    public string $city;
    
    #[NotEqualTo(reference: '00502')]
    public string $zip_code;
}
```

### #\[GreaterThan] - #\[GreaterThanEqual]

这些规则具有相同的结构和含义，并接受一个参数来定义要比较的值。正在验证的属性必须严格大于（*#\[GreaterThan]*）或等于（*#\[GreaterThanEqual]*）参考值：

```php
namespace App\Neuron\Output;

use NeuronAI\StructuredOutput\Validation\Rules\GreaterThan;
use NeuronAI\StructuredOutput\Validation\Rules\GreaterThanEqual;

class Person 
{
    #[GreaterThan(reference: 17)]
    public int $age;
    
    #[GreaterThanEqual(reference: 1)]
    public int $cars;
}
```

### #\[LowerThan] - #\[LowerThanEqual]

这些规则具有相同的结构和含义，并接受一个参数来定义要比较的值。正在验证的属性必须严格小于（*#\[LowerThan]*）或等于（*#\[LowerThanEqual]*）参考值：

```php
namespace App\Neuron\Output;

use NeuronAI\StructuredOutput\Validation\Rules\LowerThan;
use NeuronAI\StructuredOutput\Validation\Rules\LowerThanEqual;

class Person 
{
    #[LowerThan(reference: 50)]
    public int $age;
    
    #[LowerThanEqual(reference: 1)]
    public int $cars;
}
```

### #\[OutOfRange]

判断一个 `数字` 是否超出给定范围：

```php
namespace App\Neuron\Output;

use NeuronAI\StructuredOutput\Validation\Rules\InRange;

class Person 
{
    #[OutOfRange(min: 18, max: 35)]
    public int $age;
    
    // strict 参数强制严格保持在范围边界之外
    #[OutOfRange(min: 48, max: 54, strict: true)]
    public int $size;
}
```

### #\[IsFalse] - #\[IsTrue]

正在验证的属性必须具有规则定义的精确布尔值：

```php
namespace App\Neuron\Output;

use NeuronAI\StructuredOutput\Validation\Rules\IsFalse;
use NeuronAI\StructuredOutput\Validation\Rules\IsTrue;

class Phone
{
    #[IsFalse]
    public bool $iphone;
    
    #[IsTrue]
    public bool $refurbed;
}
```

### #\[IsNull] - #\[IsNotNull]

正在验证的属性必须遵守规则定义的可空条件：

```php
namespace App\Neuron\Output;

use NeuronAI\StructuredOutput\Validation\Rules\IsNotNull;
use NeuronAI\StructuredOutput\Validation\Rules\IsNull;

class Phone
{
    #[IsNotNull]
    public string $brand;
    
    #[IsNull]
    public ?string $test;
}
```

### #\[Json]

正在验证的属性必须包含有效的 JSON 字符串：

```php
namespace App\Neuron\Output;

use NeuronAI\StructuredOutput\Validation\Rules\Json;

class Person
{
    #[Json]
    public string $address;
}
```

### #\[Url]

正在验证的属性必须包含有效的 URL：

```php
namespace App\Neuron\Output;

use NeuronAI\StructuredOutput\Validation\Rules\Url;

class Person
{
    #[Url]
    public string $website;
}
```

### #\[Email]

正在验证的属性必须包含有效的电子邮件地址：

```php
namespace App\Neuron\Output;

use NeuronAI\StructuredOutput\Validation\Rules\Email;

class Person
{
    #[Email]
    public string $email;
}
```

### #\[IpAddress]

正在验证的属性必须包含有效的 IP 地址：

```php
namespace App\Neuron\Output;

use NeuronAI\StructuredOutput\Validation\Rules\IpAddress;

class Spec
{
    #[IpAddress]
    public string $ip;
}
```

### #\[ArrayOf]

正在验证的属性必须是一个包含所给对象类型全部元素的数组。

```php
namespace App\Neuron\Output;

use NeuronAI\StructuredOutput\Validation\Rules\ArrayOf;

class Post
{
    #[ArrayOf(Tag::class)]
    public array $tags;
}
```

### #\[Regex]

正在验证的属性必须符合给定的正则表达式。

```php
namespace App\Neuron\Output;

use NeuronAI\StructuredOutput\Validation\Rules\Regex;

class Coupon
{
    #[Regex('/^[A-Z]{2}\\d{4}$/')]
    public string $code;
}
```

## 自定义验证规则

验证规则是 PHP 属性，因此要创建新的规则，你应该继承框架的 AbstractValidationRule，并将该类标记为 PHP 属性：

```php
namespace App\Neuron\Output;

use Attribute;
use NeuronAI\StructuredOutput\Validation\Rules\AbstractValidationRule;

#[Attribute(Attribute::TARGET_PROPERTY)]
class MyFormatRule extends AbstractValidationRule
{
    public function __construct(protected string $format)
    {
    }

    public function validate(string $name, mixed $value, array &$violations): void
    {
        if (!is_string($value)) {
            $violations[] = $this->buildMessage($name, '{name} 必须是字符串。');
        } else if (!$this->respectFormat($this->format, $value)) {
            $violations[] = $this->buildMessage(
                $name,
                '{name} 必须匹配格式 {format}',
                ['format' => $this->pattern]
            );
        }
    }
    
    protected function respectFormat(string $value)
    {
        ...
    }
}
```

现在你可以在结构化输出类中使用该规则：

```php
namespace App\Neuron\Output;

class Route
{
    #[MyFormatRule('apps/{id}/show')]
    public string $path;
}
```
