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

# 结构化输出

根据提供的 schema 强制约束 Agent 输出。

{% hint style="warning" %}

#### 编码代理技能

使用 **`/neuron-structured-output`** 用于教你的编码代理如何利用 Neuron 的能力，可靠地从 LLM 响应中提取结构化输出。

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

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

<figure><img src="https://content.gitbook.com/content/GHx4l2LknIex7vFIUg1R/blobs/xv9XXEsc2zlqYCQSFsKa/Neuron%20AI.png" 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，用来向底层模型说明你所需的数据格式。然后代理会解析 LLM 输出来提取数据，并返回一个填充了相应值的对象实例：

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

// 与要求结构化输出的代理交互
$person = MyAgent::make()
    ->setThreadId('chat_id')
    ->structured(
        new UserMessage("I'm John and I like pizza!"),
        Person::class
    );

echo $person->name.' 喜欢 '.$person->preference;
// John 喜欢 pizza
```

### 默认输出类

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

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

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

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

// 如果你想获得结构化输出，请始终使用 structured 方法
$person = MyAgent::make()
    ->setThreadId('chat_id')
    ->structured(new UserMessage("I'm John and I like pizza"));

echo $person->name.' 喜欢 '.$person->preference;
// John 喜欢 pizza
```

### 控制输出生成

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

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

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

<figure><img src="https://content.gitbook.com/content/GHx4l2LknIex7vFIUg1R/blobs/QnOl7e0NLtP9VRBiwaPo/Structured%20Output%20Control.png" 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;
}
```

使用 `SchemaProperty` 作为纯 PHP 属性并不能让你注入动态信息，例如来自数据库的信息，因为属性位于常规代码块之外。想象一下，你需要 schema 描述遵循用户的区域设置，而 PHP 属性无法做到这一点。

你的 DTO 类可以实现 `SchemaPropertiesInterface` 并提供静态方法 `schemaProperties()`。以属性名为键，值是 `SchemaProperty` 在运行时构建的对象：

```php
use NeuronAI\StructuredOutput\SchemaPropertiesInterface;
use NeuronAI\StructuredOutput\SchemaProperty;

class Invoice implements SchemaPropertiesInterface
{
    public string $customerName;

    public float $total;

    // 属性仍然有效——这个没有运行时条目，因此会使用该属性
    #[SchemaProperty(description: '发票编号，格式为 INV-XXXX')]
    public string $number;

    #[SchemaProperty(anyOf: [InvoiceLine::class])]
    public array $lines;

    public static function schemaProperties(): array
    {
        return [
            // 这里可以使用任何运行时 PHP：翻译器、配置、环境变量……
            'customerName' => new SchemaProperty(
                description: translate('invoice.customer_name'), // 获取翻译
                required: true,
            ),
            'total' => new SchemaProperty(
                description: '总金额，货币单位为 '.config('billing.currency'), // 来自应用配置
                min: 0,
            ),
        ];
    }
}
```

解析规则：

* 在 `schemaProperties()` 中列出的属性会使用该 `SchemaProperty` 对象（它会 **替换** 该属性上的整个属性配置——两者不会逐字段合并）。
* 未列出的属性会回退到其 `#[SchemaProperty]` 属性（如果有的话）。
* 下游行为与属性完全一致，包括 `anyOf` ——因此你甚至可以动态构建多态数组的类列表，而 `反序列化器` 会正确地将其填充

### 嵌套类

你可以使用其他 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;
}
```

现在，当你向代理请求结构化输出时，你会得到已填充的实例：

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

// 与要求结构化输出的代理交互
$person = MyAgent::make()->structured(
    new UserMessage("I'm John and I want a pizza at st. James Street 00560!"),
    Person::class
);

echo $person->name.' 喜欢 '.$person->preference.'. 地址：'.$person->address->street;
// John 喜欢 pizza。地址：st.James 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 说明哪里出错了，以及哪些属性有问题。

你最终可以自定义代理必须重试多少次才能从 LLM 获取正确答案：

```php
$person = MyAgent::make()->structured(
    messages: new UserMessage("I'm John and I like pizza!"),
    class: Person::class,
    maxRetries: 3
);
```

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

如果传入 0，就可以禁用重试。这将是一次性尝试：

```php
$person = MyAgent::make()->structured(
    messages: new UserMessage("I'm John and I like pizza!"),
    class: Person::class,
    maxRetries: 0
);
```

## 监控与调试

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

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

## 验证规则

由于 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(schemes: ['http', 'https'])]
    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 Person
{
    #[IpAddress]
    public string $ip;
}
```

### #\[ArrayOf]

正在验证的属性必须是一个包含所给对象类型全部元素的数组。注意，你还需要添加 doc-block，才能让代理实例化正确的类。在 doc-block 中使用完整的类命名空间。

```php
namespace App\Neuron\Output;

use NeuronAI\StructuredOutput\Validation\Rules\ArrayOf;

class Person
{
    /**
     * @var \App\Neuron\Output\Tag[]
     */
    #[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 Attribute：

```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;
}
```
