cognesy/instructor-php

基于 LLM 的 PHP 结构化数据提取


README

基于 LLM 的 PHP 结构化数据提取。设计简洁、透明且易于控制。

什么是 Instructor?

Instructor 是一个库,允许您从多种类型的输入中提取结构化、验证过的数据:文本、图像或 OpenAI 风格的聊天序列数组。它由大型语言模型(LLMs)提供支持。

Instructor 简化了 PHP 项目中 LLM 的集成。它处理从 LLM 输出中提取结构化数据的复杂性,使您能够专注于构建应用程序逻辑并快速迭代。

PHP 的 Instructor 是由 Instructor 库的 Python 版本所启发,该库由 Jason Liu 创建。

image

以下是一个简单的 CLI 演示应用程序,使用 Instructor 从文本中提取结构化数据

image

功能亮点

核心功能

  • 无需编写样板代码即可从 LLM 获取结构化响应
  • 验证返回的数据
  • 当 LLM 响应无效数据时,自动重试
  • 以最小的摩擦将 LLM 支持集成到现有的 PHP 代码中 - 无需框架,无需进行大量的代码更改

灵活的输入

  • 使用相同的简单 API 处理各种类型的输入数据:文本、一系列聊天消息或图像
  • '结构化到结构化' 处理 - 提供对象或数组作为输入,并返回包含推理结果的对象
  • 通过示例展示如何提高推理质量

自定义

  • 以您想要的方式定义响应数据模型:类型提示的类、JSON Schema 数组或使用 Structure 类的动态数据形状
  • 自定义提示和重试提示
  • 使用属性或 PHP DocBlocks 为 LLM 提供额外的指令
  • 通过提供您自己的模式、反序列化、验证和转换接口实现来自定义响应模型处理

同步和流支持

  • 支持同步或流响应
  • 获取部分更新并流式传输已完成的序列项目

可观察性

  • 通过事件获取内部处理的详细信息
  • 调试模式以查看 LLM API 请求和响应的详细信息

支持多个 LLM/API 提供商

  • 轻松切换 LLM 提供商
  • 支持大多数流行的 LLM API(包括 OpenAI、Gemini、Anthropic、Cohere、Azure、Groq、Mistral、Fireworks AI、Together AI)
  • OpenRouter 支持 - 访问 100 多种语言模型
  • 使用 Ollama 使用本地模型

其他功能

  • LLM 上下文缓存以减少成本并加快推理(适用于 Anthropic 模型)
  • 从图像中提取数据(适用于 OpenAI、Anthropic 和 Gemini 模型)

文档和示例

  • 从不断增长的文档和 50 多个食谱中了解更多信息

Instructor 其他语言版本

以下列出其他语言的实现

如果您想将 Instructor 转换为其他语言,请通过 Twitter 与我们联系,我们很乐意帮助您开始。

Instructor 如何提升您的效率

讲师介绍了与直接API使用相比的三个关键改进。

响应模型

您只需指定一个PHP类,通过LLM聊天完成的“魔法”将数据提取到该类中。就这样。

讲师通过利用结构化LLM响应来减少从文本数据中提取信息的代码的脆弱性。

讲师帮助您编写更简单、更容易理解的代码 - 您不再需要定义冗长的函数调用定义或将返回的JSON分配到目标数据对象中。

验证

LLM生成的响应模型可以自动验证,遵循一组规则。目前,讲师支持仅Symfony验证。

您还可以提供一个上下文对象以使用增强的验证器功能。

最大重试次数

您可以设置请求的重试次数。

在出现验证或反序列化错误的情况下,讲师将重复请求,直到达到指定的次数,并尝试从LLM获取有效响应。

入门

安装讲师很简单。在您的终端中运行以下命令,您就可以开始体验更流畅的数据处理过程了!

composer install cognesy/instructor-php

用法

基本示例

这是一个简单的示例,展示了讲师如何从提供的文本(或聊天消息序列)中检索结构化信息。

响应模型类是一个普通的PHP类,使用类型提示指定对象的字段类型。

use Cognesy\Instructor\Instructor;

// Step 0: Create .env file in your project root:
// OPENAI_API_KEY=your_api_key

// Step 1: Define target data structure(s)
class Person {
    public string $name;
    public int $age;
}

// Step 2: Provide content to process
$text = "His name is Jason and he is 28 years old.";

// Step 3: Use Instructor to run LLM inference
$person = (new Instructor)->respond(
    messages: $text,
    responseModel: Person::class,
);

// Step 4: Work with structured response data
assert($person instanceof Person); // true
assert($person->name === 'Jason'); // true
assert($person->age === 28); // true

echo $person->name; // Jason
echo $person->age; // 28

var_dump($person);
// Person {
//     name: "Jason",
//     age: 28
// }    

注意:讲师支持将类/对象用作响应模型。如果您想提取简单类型或枚举,您需要将它们包装在标量适配器中 - 请参阅下文:提取标量值。

连接到各种LLM API提供者

讲师允许您在llm.php文件中定义多个API连接。当您想在应用程序中使用不同的LLM或API提供者时,这很有用。

默认配置位于讲师代码库根目录中的/config/llm.php。它包含了一组预定义的连接,这些连接支持讲师开箱即用的所有LLM API。

配置文件定义了LLM API的连接及其参数。它还指定了在调用讲师而没有指定客户端连接时使用的默认连接。

/* This is fragment of /config/llm.php file */
    'defaultConnection' => 'openai',
    //...
    'connections' => [
        'anthropic' => [ ... ],
        'cohere' => [ ... ],
        'gemini' => [ ... ],
        'ollama' => [
            'clientType' => ClientType::Ollama->value,
            'apiUrl' => Env::get('OLLAMA_API_URL', 'https://:11434/v1'),
            'apiKey' => Env::get('OLLAMA_API_KEY', ''),
            'defaultModel' => Env::get('OLLAMA_DEFAULT_MODEL', 'gemma2:2b'),
            'defaultMaxTokens' => Env::get('OLLAMA_DEFAULT_MAX_TOKENS', 1024),
            'connectTimeout' => Env::get('OLLAMA_CONNECT_TIMEOUT', 3),
            'requestTimeout' => Env::get('OLLAMA_REQUEST_TIMEOUT', 30),
        ],
    // ...

要自定义可用的连接,您可以修改现有条目或添加自己的条目。

通过预定义连接连接到LLM API与调用带有连接名称的withClient方法一样简单。

<?php
// ...
$user = (new Instructor)
    ->withConnection('ollama')
    ->respond(
        messages: "His name is Jason and he is 28 years old.",
        responseModel: Person::class,
    );
// ...

您可以通过INSTRUCTOR_CONFIG_PATH环境变量更改讲师使用的配置文件的位置。您可以使用默认配置文件的副本作为起点。

结构化到结构化处理

讲师提供了一种使用结构化数据作为输入的方法。当您想使用对象数据作为输入并获取具有LLM推理结果的另一个对象时,这很有用。

Instructor的respond()request()方法的input字段可以是对象、数组或只是字符串。

<?php
use Cognesy\Instructor\Instructor;

class Email {
    public function __construct(
        public string $address = '',
        public string $subject = '',
        public string $body = '',
    ) {}
}

$email = new Email(
    address: 'joe@gmail',
    subject: 'Status update',
    body: 'Your account has been updated.'
);

$translation = (new Instructor)->respond(
    input: $email,
    responseModel: Email::class,
    prompt: 'Translate the text fields of email to Spanish. Keep other fields unchanged.',
);

assert($translation instanceof Email); // true
dump($translation);
// Email {
//     address: "joe@gmail",
//     subject: "Actualización de estado",
//     body: "Su cuenta ha sido actualizada."
// }
?>

验证

Instructor将LLM响应的结果与您在数据模型中指定的验证规则进行验证。

有关可用验证规则的详细信息,请参阅Symfony验证约束

use Symfony\Component\Validator\Constraints as Assert;

class Person {
    public string $name;
    #[Assert\PositiveOrZero]
    public int $age;
}

$text = "His name is Jason, he is -28 years old.";
$person = (new Instructor)->respond(
    messages: [['role' => 'user', 'content' => $text]],
    responseModel: Person::class,
);

// if the resulting object does not validate, Instructor throws an exception

最大重试次数

如果提供了maxRetries参数并且LLM响应不符合验证标准,讲师将继续进行后续推理尝试,直到结果满足要求或达到maxRetries。

讲师使用验证错误通知LLM在响应中识别的问题,以便LLM可以在下一次尝试中尝试自我纠正。

use Symfony\Component\Validator\Constraints as Assert;

class Person {
    #[Assert\Length(min: 3)]
    public string $name;
    #[Assert\PositiveOrZero]
    public int $age;
}

$text = "His name is JX, aka Jason, he is -28 years old.";
$person = (new Instructor)->respond(
    messages: [['role' => 'user', 'content' => $text]],
    responseModel: Person::class,
    maxRetries: 3,
);

// if all LLM's attempts to self-correct the results fail, Instructor throws an exception

调用讲师的替代方法

您可以通过调用request()方法来设置请求参数,然后调用get()来获取响应。

use Cognesy\Instructor\Instructor;

$instructor = (new Instructor)->request(
    messages: "His name is Jason, he is 28 years old.",
    responseModel: Person::class,
);
$person = $instructor->get();

流式支持

Instructor支持部分结果的流式处理,允许您在数据可用时开始处理数据。

<?php
use Cognesy\Instructor\Instructor;

$stream = (new Instructor)->request(
    messages: "His name is Jason, he is 28 years old.",
    responseModel: Person::class,
    options: ['stream' => true]
)->stream();

foreach ($stream as $partialPerson) {
    // process partial person data
    echo $partialPerson->name;
    echo $partialPerson->age;
}

// after streaming is done you can get the final, fully processed person object...
$person = $stream->getLastUpdate()
// ...to, for example, save it to the database
$db->save($person);
?>

部分结果

您可以定义 onPartialUpdate() 回调以接收部分结果,在LLM完成推理之前即可开始更新UI。

注意:部分更新不会进行验证。只有在完全接收到响应后才会进行验证。

use Cognesy\Instructor\Instructor;

function updateUI($person) {
    // Here you get partially completed Person object update UI with the partial result
}

$person = (new Instructor)->request(
    messages: "His name is Jason, he is 28 years old.",
    responseModel: Person::class,
    options: ['stream' => true]
)->onPartialUpdate(
    fn($partial) => updateUI($partial)
)->get();

// Here you get completed and validated Person object
$this->db->save($person); // ...for example: save to DB

快捷方式

字符串作为输入

您可以使用字符串而不是消息数组。当您想从单个文本块中提取数据并保持代码简单时,这很有用。

// Usually, you work with sequences of messages:

$value = (new Instructor)->respond(
    messages: [['role' => 'user', 'content' => "His name is Jason, he is 28 years old."]],
    responseModel: Person::class,
);

// ...but if you want to keep it simple, you can just pass a string:

$value = (new Instructor)->respond(
    messages: "His name is Jason, he is 28 years old.",
    responseModel: Person::class,
);

提取标量值

有时我们只想快速得到结果,而不需要定义响应模型类,尤其是在我们试图以字符串、整数、布尔值或浮点数的形式获得直接、简单的答案时。讲师提供了一种简化的API用于此类情况。

use Cognesy\Instructor\Extras\Scalar\Scalar;
use Cognesy\Instructor\Instructor;

$value = (new Instructor)->respond(
    messages: "His name is Jason, he is 28 years old.",
    responseModel: Scalar::integer('age'),
);

var_dump($value);
// int(28)

在这个例子中,我们正在从文本中提取单个整数值。您还可以使用 Scalar::string()Scalar::boolean()Scalar::float() 来提取其他类型的值。

提取枚举值

此外,您还可以使用标量适配器通过使用 Scalar::enum() 来提取提供的选项之一。

use Cognesy\Instructor\Extras\Scalar\Scalar;
use Cognesy\Instructor\Instructor;

enum ActivityType : string {
    case Work = 'work';
    case Entertainment = 'entertainment';
    case Sport = 'sport';
    case Other = 'other';
}

$value = (new Instructor)->respond(
    messages: "His name is Jason, he currently plays Doom Eternal.",
    responseModel: Scalar::enum(ActivityType::class, 'activityType'),
);

var_dump($value);
// enum(ActivityType:Entertainment)

提取对象序列

序列是一个包装类,可以用来表示由讲师从提供上下文中提取的对象列表。

通常,创建一个仅具有单个数组属性的专用类来处理特定类的一组对象会更方便。

序列的另一个独特功能是,它们可以按序列中每个完成的项进行流式传输,而不是在任何属性更新时。

class Person
{
    public string $name;
    public int $age;
}

$text = <<<TEXT
    Jason is 25 years old. Jane is 18 yo. John is 30 years old
    and Anna is 2 years younger than him.
TEXT;

$list = (new Instructor)->respond(
    messages: [['role' => 'user', 'content' => $text]],
    responseModel: Sequence::of(Person::class),
    options: ['stream' => true]
);

有关序列的更多信息,请参阅序列部分。

指定数据模型

类型提示

使用PHP类型提示来指定提取数据的类型。

使用可空类型来指示给定字段是可选的。

    class Person {
        public string $name;
        public ?int $age;
        public Address $address;
    }

DocBlock类型提示

您还可以使用PHP DocBlock样式注释来指定提取数据的类型。当您想为LLM指定属性类型,但又不能或不想在代码级别强制类型时,这很有用。

class Person {
    /** @var string */
    public $name;
    /** @var int */
    public $age;
    /** @var Address $address person's address */
    public $address;
}

有关PHPDoc文档的更多信息,请参阅DocBlock网站

类型化集合/数组

PHP当前不支持泛型或类型提示来指定数组元素类型。

使用PHP DocBlock样式注释来指定数组元素的类型。

class Person {
    // ...
}

class Event {
    // ...
    /** @var Person[] list of extracted event participants */
    public array $participants;
    // ...
}

复杂数据提取

讲师可以从文本中检索复杂的数据结构。您的响应模型可以包含嵌套对象、数组和枚举。

use Cognesy\Instructor\Instructor;

// define a data structures to extract data into
class Person {
    public string $name;
    public int $age;
    public string $profession;
    /** @var Skill[] */
    public array $skills;
}

class Skill {
    public string $name;
    public SkillType $type;
}

enum SkillType {
    case Technical = 'technical';
    case Other = 'other';
}

$text = "Alex is 25 years old software engineer, who knows PHP, Python and can play the guitar.";

$person = (new Instructor)->respond(
    messages: [['role' => 'user', 'content' => $text]],
    responseModel: Person::class,
); // client is passed explicitly, can specify e.g. different base URL

// data is extracted into an object of given class
assert($person instanceof Person); // true

// you can access object's extracted property values
echo $person->name; // Alex
echo $person->age; // 25
echo $person->profession; // software engineer
echo $person->skills[0]->name; // PHP
echo $person->skills[0]->type; // SkillType::Technical
// ...

var_dump($person);
// Person {
//     name: "Alex",
//     age: 25,
//     profession: "software engineer",
//     skills: [
//         Skill {
//              name: "PHP",
//              type: SkillType::Technical,
//         },
//         Skill {
//              name: "Python",
//              type: SkillType::Technical,
//         },
//         Skill {
//              name: "guitar",
//              type: SkillType::Other
//         },
//     ]
// }

动态数据模式

如果您想在运行时定义数据的形状,可以使用 Structure 类。

结构允许您定义和修改由LLM提取的数据的任意形状。类可能不适合此目的,因为在执行期间无法声明或更改它们。

使用结构,您可以动态定义自定义数据形状,例如基于用户输入或处理上下文,以指定LLM需要从提供的文本或聊天消息中推断的信息。

下面的示例演示了如何定义结构并将其用作响应模型。

<?php
use Cognesy\Instructor\Extras\Structure\Field;
use Cognesy\Instructor\Extras\Structure\Structure;

enum Role : string {
    case Manager = 'manager';
    case Line = 'line';
}

$structure = Structure::define('person', [
    Field::string('name'),
    Field::int('age'),
    Field::enum('role', Role::class),
]);

$person = (new Instructor)->respond(
    messages: 'Jason is 25 years old and is a manager.',
    responseModel: $structure,
);

// you can access structure data via field API...
assert($person->field('name') === 'Jason');
// ...or as structure object properties
assert($person->age === 25);
?>

有关更多信息,请参阅结构部分。

更改LLM模型和选项

您可以指定将传递给OpenAI/LLM端点的模型和其他选项。

use Cognesy\Instructor\Instructor;
use Cognesy\Instructor\Clients\OpenAI\OpenAIClient;

// OpenAI auth params
$yourApiKey = Env::get('OPENAI_API_KEY'); // use your own API key

// Create instance of OpenAI client initialized with custom parameters
$client = new OpenAIClient(
    $yourApiKey,
    baseUri: 'https://api.openai.com/v1', // you can change base URI
    connectTimeout: 3,
    requestTimeout: 30,
    metadata: ['organization' => ''],
);

/// Get Instructor with the default client component overridden with your own
$instructor = (new Instructor)->withClient($client);

$user = $instructor->respond(
    messages: "Jason (@jxnlco) is 25 years old and is the admin of this project. He likes playing football and reading books.",
    responseModel: User::class,
    model: 'gpt-3.5-turbo',
    options: ['stream' => true ]
);

对语言模型和API提供者的支持

讲师为以下API提供程序提供开箱即用的支持

  • Anthropic
  • Azure OpenAI
  • Cohere
  • Fireworks AI
  • Groq
  • Mistral
  • Ollama (在本地主机上)
  • OpenAI
  • OpenRouter
  • Together AI

有关使用示例,请检查Hub部分或代码存储库中的examples目录。

将DocBlocks用作LLM的附加指令

您可以使用PHP文档注释(/** */)在类或字段级别为LLM提供额外的指令,例如澄清您期望的内容或LLM应如何处理您的数据。

讲师从定义的类和属性中提取PHP文档注释,并将其包含在发送给LLM的响应模型规范中。

使用PHP文档注释指令不是必需的,但有时您可能想要澄清您的意图,以提高LLM的推理结果。

/**
 * Represents a skill of a person and context in which it was mentioned. 
 */
class Skill {
    public string $name;
    /** @var SkillType $type type of the skill, derived from the description and context */
    public SkillType $type;
    /** Directly quoted, full sentence mentioning person's skill */
    public string $context;
}

自定义验证

ValidationMixin

您可以使用ValidationMixin特性来添加易于定制的对象验证能力。

use Cognesy\Instructor\Validation\Traits\ValidationMixin;

class User {
    use ValidationMixin;

    public int $age;
    public int $name;

    public function validate() : array {
        if ($this->age < 18) {
            return ["User has to be adult to sign the contract."];
        }
        return [];
    }
}

验证回调

讲师使用Symfony验证组件验证提取的数据。您可以使用#[Assert/Callback]注释来构建完全定制的验证逻辑。

use Cognesy\Instructor\Instructor;
use Symfony\Component\Validator\Constraints as Assert;
use Symfony\Component\Validator\Context\ExecutionContextInterface;

class UserDetails
{
    public string $name;
    public int $age;
    
    #[Assert\Callback]
    public function validateName(ExecutionContextInterface $context, mixed $payload) {
        if ($this->name !== strtoupper($this->name)) {
            $context->buildViolation("Name must be in uppercase.")
                ->atPath('name')
                ->setInvalidValue($this->name)
                ->addViolation();
        }
    }
}

$user = (new Instructor)->respond(
    messages: [['role' => 'user', 'content' => 'jason is 25 years old']],
    responseModel: UserDetails::class,
    maxRetries: 2
);

assert($user->name === "JASON");

有关如何使用回调约束的更多详细信息,请参阅Symfony文档

内部机制

生命周期

作为PHP讲师处理您的请求时,它会经过几个阶段

  1. 初始化和自我配置(可能由开发者定义的覆盖)。
  2. 分析开发者指定的响应数据模型的类和属性。
  3. 将数据模型编码为LLM可以提供的模式。
  4. 使用指定的消息(内容)和响应模型元数据向LLM发出请求。
  5. 接收LLM的响应或多个部分响应(如果启用了流式传输)。
  6. 将接收到的LLM响应反序列化为最初请求的类及其属性。
  7. 如果响应包含不完整或损坏的数据 - 如果遇到错误,则为LLM创建反馈消息并请求重新生成响应。
  8. 执行开发者定义的数据模型验证 - 如果其中任何一个失败,则为LLM创建反馈消息并请求重新生成响应。
  9. 重复步骤4-8,除非达到指定的重试限制或响应通过验证

接收内部事件通知

讲师允许您通过事件在每个请求和响应处理阶段接收详细的信息。

  • (new Instructor)->onEvent(string $class, callable $callback) 方法 - 当发出指定类型的事件时接收回调
  • (new Instructor)->wiretap(callable $callback) 方法 - 接收讲师发出的任何事件,可能对调试或性能分析很有用
  • (new Instructor)->onError(callable $callback) 方法 - 接收任何未捕获的错误上的回调,因此您可以自定义处理它,例如记录错误或尝试使用某些回退机制来恢复

接收事件可以帮助您监控执行过程,并使开发者更容易理解和解决任何处理问题。

$instructor = (new Instructor)
    // see requests to LLM
    ->onEvent(RequestSentToLLM::class, fn($e) => dump($e))
    // see responses from LLM
    ->onEvent(ResponseReceivedFromLLM::class, fn($event) => dump($event))
    // see all events in console-friendly format
    ->wiretap(fn($event) => dump($event->toConsole()))
    // log errors via your custom logger
    ->onError(fn($request, $error) => $logger->log($error));

$instructor->respond(
    messages: "What is the population of Paris?",
    responseModel: Scalar::integer(),
);
// check your console for the details on the Instructor execution

响应模型

讲师能够处理作为响应模型提供的几种类型的输入,使您在与库交互方面具有更大的灵活性。

Instructor的respond()方法的签名表明responseModel可以是字符串、对象或数组。

处理string $responseModel值

如果提供了string值,则用作响应模型类的名称。

Instructor检查该类是否存在,并分析该类及其属性类型信息与文档注释以生成指定LLM响应约束所需的模式。

提供响应模型类名称的最佳方式是使用NameOfTheClass::class而不是字符串,这样IDE就可以执行类型检查、处理重构等。

处理object $responseModel值

如果提供了object值,则认为它是响应模型的一个实例。讲师检查实例的类,然后分析它及其属性类型数据以指定LLM响应约束。

处理array $responseModel值

如果提供了array值,则被视为原始JSON模式,因此允许讲师可以直接在LLM请求中使用它(在适当的上下文中包装 - 例如函数调用)。

讲师需要您JSON模式中每个嵌套对象的类信息,以便正确地将数据反序列化为适当的类型。

当您以类名或实例传递$responseModel时,讲师可以访问此信息,但在原始JSON模式中缺少此信息。

当前设计使用JSON Schema的$comment字段来解决这个问题。讲师期望开发者使用$comment字段来提供用于反序列化对象或枚举类型属性数据的目标类的完全限定名。

响应模型合约

讲师还允许您通过查看类或实例实现的接口来自定义$responseModel值的处理。

  • CanProvideJsonSchema - 实现以提供JSON Schema或响应模型,覆盖讲师的默认方法,讲师的默认方法是分析$responseModel值类信息,
  • CanDeserializeSelf - 实现以自定义从LLM响应JSON反序列化为PHP对象的方式,
  • CanValidateSelf - 实现以自定义反序列化对象验证的方式,
  • CanTransformSelf - 实现以将验证后的对象转换为调用者接收到的目标值(例如,从类解包简单类型到标量值)。

附加说明

PHP生态系统(目前)没有Pydantic的强大等效产品,它是Instructor for Python的核心。

为了提供这里所需的基本功能,Instructor for PHP利用了以下功能

依赖关系

Instructor for PHP与PHP 8.2或更高版本兼容,并且由于依赖性最小,应与您选择的任何框架一起工作。

  • SaloonPHP - 用于处理与LLM API提供商的通信
  • Symfony组件 - 用于验证、序列化和其他实用工具

待办事项

  • 异步支持
  • 文档

贡献

如果您想帮忙,请查看一些问题。所有贡献都受欢迎 - 代码改进、文档、错误报告、博客文章/文章、或新的食谱和应用程序示例。

许可证

本项目根据MIT许可证的条款进行许可。

支持

如果您有任何问题或需要帮助,请通过TwitterGitHub联系我。

贡献者

Contributors