llm-agents/agents

LLM Agents PHP SDK - 基于 PHP 的自主语言模型代理

1.7.0 2024-09-15 18:44 UTC

This package is auto-updated.

Last update: 2024-09-20 07:19:52 UTC


README



LLM Agents PHP SDK

LLM Agents 是一个用于构建和管理基于语言模型(LLM)代理的 PHP 库。它提供了一个框架,用于创建能够执行复杂任务、做出决策并与各种工具和 API 交互的自主代理。

该库使开发者能够高效地将 LLM 功能集成到 PHP 应用程序中,从而创建能够理解和响应用户输入、处理信息并根据处理结果执行动作的智能系统。

关于 LLM 代理及其应用的全面解释,您可以阅读文章:PHP 开发者的梦想:PHP 开发者的梦想:一个真正了解您的 AI 家庭

PHP Latest Version on Packagist Total Downloads

有关包含示例代理和与它们交互的 CLI 接口的完整示例,请查看我们的示例应用程序存储库 https://github.com/llm-agents-php/sample-app

此示例应用程序演示了 LLM Agents 库的实际实现和使用模式。

该包不包含任何特定的 LLM 实现。相反,它提供了一个框架,用于创建可以与任何 LLM 服务或 API 交互的代理。

✨ 关键功能

  • 🤖 代理创建:使用可自定义行为在 PHP 中创建和配置基于 LLM 的代理。
  • 🔧 工具集成:无缝集成各种工具和 API 以供代理在 PHP 应用程序中使用。
  • 🧠 内存管理:支持代理内存,允许在交互中保留和回忆信息。
  • 💡 提示管理:高效处理提示和指令以引导代理行为。
  • 🔌 可扩展架构:轻松将新代理类型、工具和能力添加到您的 PHP 项目中。
  • 🤝 多代理支持:在 PHP 中构建具有多个交互代理的系统,以解决复杂问题解决场景。

📀 安装

您可以通过 Composer 安装 LLM Agents 包

composer require llm-agents/agents

💻 使用

→ 创建代理

要创建代理,您需要定义其行为、工具和配置。以下是一个基本示例

use LLM\Agents\Agent\AgentAggregate;
use LLM\Agents\Agent\Agent;
use LLM\Agents\Solution\Model;
use LLM\Agents\Solution\ToolLink;
use LLM\Agents\Solution\MetadataType;
use LLM\Agents\Solution\SolutionMetadata;

class SiteStatusCheckerAgent extends AgentAggregate
{
    public const NAME = 'site_status_checker';

    public static function create(): self
    {
        $agent = new Agent(
            key: self::NAME,
            name: 'Site Status Checker',
            description: 'This agent checks the online status of websites.',
            instruction: 'You are a website status checking assistant. Your goal is to help users determine if a website is online. Use the provided tool to check site availability. Give clear, concise responses about a site\'s status.',
        );

        $aggregate = new self($agent);

        $aggregate->addMetadata(
            new SolutionMetadata(
                type: MetadataType::Memory,
                key: 'check_availability',
                content: 'Always check the site\'s availability using the provided tool.',
            ),
            new SolutionMetadata(
                type: MetadataType::Configuration,
                key: 'max_tokens',
                content: 500,
            )
        );

        $model = new Model(model: 'gpt-4o-mini');
        $aggregate->addAssociation($model);

        $aggregate->addAssociation(new ToolLink(name: CheckSiteAvailabilityTool::NAME));

        return $aggregate;
    }
}

→ 实现工具

现在,让我们实现此代理使用的工具

use LLM\Agents\Tool\PhpTool;
use LLM\Agents\Tool\ToolLanguage;

class CheckSiteAvailabilityTool extends PhpTool
{
    public const NAME = 'check_site_availability';

    public function __construct()
    {
        parent::__construct(
            name: self::NAME,
            inputSchema: CheckSiteAvailabilityInput::class,
            description: 'This tool checks if a given URL is accessible and returns its HTTP status code and response time.',
        );
    }

    public function getLanguage(): ToolLanguage
    {
        return ToolLanguage::PHP;
    }

    public function execute(object $input): string
    {
        $ch = curl_init($input->url);
        curl_setopt_array($ch, [
            CURLOPT_RETURNTRANSFER => true,
            CURLOPT_HEADER => true,
            CURLOPT_NOBODY => true,
            CURLOPT_FOLLOWLOCATION => true,
            CURLOPT_MAXREDIRS => 10,
            CURLOPT_TIMEOUT => 30,
        ]);

        $startTime = microtime(true);
        $response = curl_exec($ch);
        $endTime = microtime(true);

        $statusCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
        $responseTime = round(($endTime - $startTime) * 1000, 2);

        curl_close($ch);

        $isOnline = $statusCode >= 200 && $statusCode < 400;

        return json_encode([
            'status_code' => $statusCode,
            'response_time_ms' => $responseTime,
            'is_online' => $isOnline,
        ]);
    }
}

以及工具的输入模式

use Spiral\JsonSchemaGenerator\Attribute\Field;

class CheckSiteAvailabilityInput
{
    public function __construct(
        #[Field(title: 'URL', description: 'The full URL of the website to check')]
        public readonly string $url,
    ) {}
}

→ 链接代理

LLM Agents 支持通过链接多个代理来创建复杂的系统。这允许您构建层次结构或协作代理网络。以下是如何将一个代理链接到另一个代理的方法

创建代理链接

要将一个代理链接到另一个代理,您可以使用 AgentLink 类。以下是如何修改我们的 SiteStatusCheckerAgent 以包含对另一个代理的链接的示例

use LLM\Agents\Solution\AgentLink;

class SiteStatusCheckerAgent extends AgentAggregate
{
    public const NAME = 'site_status_checker';

    public static function create(): self
    {
        // ... [previous agent setup code] ...

        // Link to another agent
        $aggregate->addAssociation(
            new AgentLink(
                name: 'network_diagnostics_agent',
                outputSchema: NetworkDiagnosticsOutput::class,
            ),
        );

        return $aggregate;
    }
}

在此示例中,我们正在链接一个 network_diagnostics_agentoutputSchema 参数指定了链接代理的预期输出格式。输出模式用于标准化链接代理应返回的数据格式。

使用链接代理

我们在此处不提供链接代理的实现,但您可以在代理的执行中使用链接代理。

以下是如何调用链接代理的示例

use LLM\Agents\Tool\PhpTool;
use LLM\Agents\Agent\AgentExecutor;
use LLM\Agents\LLM\Prompt\Chat\ToolCallResultMessage;
use LLM\Agents\LLM\Response\ToolCalledResponse;
use LLM\Agents\Tool\ToolExecutor;
use LLM\Agents\Tool\ToolLanguage;

/**
 * @extends PhpTool<AskAgentInput>
 */
final class AskAgentTool extends PhpTool
{
    public const NAME = 'ask_agent';

    public function __construct(
        private readonly AgentExecutor $executor,
        private readonly ToolExecutor $toolExecutor,
    ) {
        parent::__construct(
            name: self::NAME,
            inputSchema: AskAgentInput::class,
            description: 'Ask an agent with given name to execute a task.',
        );
    }

    public function getLanguage(): ToolLanguage
    {
        return ToolLanguage::PHP;
    }

    public function execute(object $input): string|\Stringable
    {
        $prompt = \sprintf(
            <<<'PROMPT'
%s
Important rules:
- Think before responding to the user.
- Don not markup the content. Only JSON is allowed.
- Don't write anything except the answer using JSON schema.
- Answer in JSON using this schema:
%s
PROMPT
            ,
            $input->question,
            $input->outputSchema,
        );

        while (true) {
            $execution = $this->executor->execute($input->name, $prompt);
            $result = $execution->result;
            $prompt = $execution->prompt;

            if ($result instanceof ToolCalledResponse) {
                foreach ($result->tools as $tool) {
                    $functionResult = $this->toolExecutor->execute($tool->name, $tool->arguments);

                    $prompt = $prompt->withAddedMessage(
                        new ToolCallResultMessage(
                            id: $tool->id,
                            content: [$functionResult],
                        ),
                    );
                }

                continue;
            }

            break;
        }

        return \json_encode($result->content);
    }
}

以及工具的输入模式

use Spiral\JsonSchemaGenerator\Attribute\Field;

final class AskAgentInput
{
    public function __construct(
        #[Field(title: 'Agent Name', description: 'The name of the agent to ask.')]
        public string $name,
        #[Field(title: 'Question', description: 'The question to ask the agent.')]
        public string $question,
        #[Field(title: 'Output Schema', description: 'The schema of the output.')]
        public string $outputSchema,
    ) {}
}

并将工具添加到具有链接代理的代理。当代理执行时,如果它决定这样做,它将调用链接代理。

→ 执行代理

要执行代理,您将使用 AgentExecutor 类。

use LLM\Agents\AgentExecutor\ExecutorInterface;
use LLM\Agents\LLM\Prompt\Chat\Prompt;
use LLM\Agents\LLM\Prompt\Chat\MessagePrompt;

class AgentRunner
{
    public function __construct(
        private ExecutorInterface $executor,
    ) {}

    public function run(string $input): string
    {
        $prompt = new Prompt([
            MessagePrompt::user($input),
        ]);

        $execution = $this->executor->execute(
            agent: MyAgent::NAME,
            prompt: $prompt,
        );

        return (string)$execution->result->content;
    }
}

// Usage
$agentRunner = new AgentRunner($executor);
$result = $agentRunner->run("Do something cool!");
echo $result;

以下示例演示了如何创建一个简单的代理,该代理可以使用自定义工具执行特定任务。

→ 代理内存和提示

代理可以使用内存和预定义的提示来指导其行为。

use LLM\Agents\Solution\SolutionMetadata;
use LLM\Agents\Solution\MetadataType;

// In your agent creation method:
$aggregate->addMetadata(
    new SolutionMetadata(
        type: MetadataType::Memory,
        key: 'user_preference',
        content: 'The user prefers concise answers.',
    ),
    
    new SolutionMetadata(
        type: MetadataType::Prompt,
        key: 'check_google',
        content: 'Check the status of google.com.',
    ),
    
    new SolutionMetadata(
        type: MetadataType::Prompt,
        key: 'check_yahoo',
        content: 'Check the status of yahoo.com.',
    ),
    
    //...
);

执行器拦截器

此包包含一个强大的拦截器系统,用于执行器。这使得开发者能够在执行过程的各个阶段注入数据到提示中、修改执行选项以及处理LLM响应。以下是每个可用拦截器的详细说明。

use LLM\Agents\AgentExecutor\ExecutorInterface;
use LLM\Agents\AgentExecutor\ExecutorPipeline;
use LLM\Agents\AgentExecutor\Interceptor\GeneratePromptInterceptor;
use LLM\Agents\AgentExecutor\Interceptor\InjectModelInterceptor;
use LLM\Agents\AgentExecutor\Interceptor\InjectOptionsInterceptor;
use LLM\Agents\AgentExecutor\Interceptor\InjectResponseIntoPromptInterceptor;
use LLM\Agents\AgentExecutor\Interceptor\InjectToolsInterceptor;

$executor = new ExecutorPipeline(...);

$executor = $executor->withInterceptor(
    new GeneratePromptInterceptor(...),
    new InjectModelInterceptor(...),
    new InjectToolsInterceptor(...),
    new InjectOptionsInterceptor(...),
    new InjectResponseIntoPromptInterceptor(...),
);

$executor->execute(...);

可用拦截器

  1. GeneratePromptInterceptor

    • 目的:生成代理的初始提示。
    • 功能:
      • 使用 AgentPromptGeneratorInterface 创建一个全面的提示。
      • 将代理指令、内存和用户输入合并到提示中。
    • 何时使用:始终包含此拦截器以确保正确的提示生成。
  2. InjectModelInterceptor

    • 目的:为代理注入适当的语言模型。
    • 功能:
      • 检索与代理关联的模型。
      • 将模型信息添加到执行选项中。
    • 何时使用:当您想要确保每个代理使用正确的模型,特别是在多代理系统中时,包含此拦截器。
  3. InjectToolsInterceptor

    • 目的:将代理的工具添加到执行选项中。
    • 功能:
      • 检索与代理关联的所有工具。
      • 将工具架构转换为LLM可理解的格式。
      • 将工具信息添加到执行选项中。
    • 何时使用:当您的代理使用工具并且您希望在执行期间使用这些工具时,包含此拦截器。
  4. InjectOptionsInterceptor

    • 目的:为代理合并额外的配置选项。
    • 功能:
      • 检索为代理定义的任何自定义配置选项。
      • 将这些选项添加到执行选项中。
    • 何时使用:当您有应在执行期间应用的代理特定配置时,包含此拦截器。
  5. InjectResponseIntoPromptInterceptor

    • 目的:将LLM的响应添加回提示中,以进行连续对话。
    • 功能:
      • 从之前的执行中获取LLM的响应。
      • 将此响应追加到现有提示中。
    • 何时使用:在对话代理中或当重要于之前交互的上下文时,包含此拦截器。

创建自定义拦截器

您可以为您的代理执行流程添加专用行为而创建自定义拦截器。

以下是一个自定义拦截器的示例,该拦截器将时间感知和用户特定的上下文添加到提示中。

use LLM\Agents\AgentExecutor\ExecutorInterceptorInterface;
use LLM\Agents\AgentExecutor\ExecutionInput;
use LLM\Agents\AgentExecutor\InterceptorHandler;
use LLM\Agents\Agent\Execution;
use LLM\Agents\LLM\Prompt\Chat\Prompt;
use LLM\Agents\LLM\Response\ChatResponse;
use Psr\Log\LoggerInterface;

class TokenCounterInterceptor implements ExecutorInterceptorInterface
{
    public function __construct(
        private TokenCounterInterface $tokenCounter,
        private LoggerInterface $logger,
    ) {}

    public function execute(ExecutionInput $input, InterceptorHandler $next): Execution
    {
        // Count tokens in the input prompt
        $promptTokens = $this->tokenCounter->count((string) $input->prompt);

        // Execute the next interceptor in the chain
        $execution = $next($input);

        // Count tokens in the response
        $responseTokens = 0;
        if ($execution->result instanceof ChatResponse) {
            $responseTokens = $this->tokenCounter->count((string) $execution->result->content);
        }

        // Log the token counts
        $this->logger->info('Token usage', [
            'prompt_tokens' => $promptTokens,
            'response_tokens' => $responseTokens,
            'total_tokens' => $promptTokens + $responseTokens,
        ]);

        return $execution;
    }
}

然后,您可以向执行器添加您的自定义拦截器。

use Psr\Log\LoggerInterface;

// Assume you have implementations of TokenCounterInterface and LoggerInterface
$tokenCounter = new MyTokenCounter();
$logger = new MyLogger();

$executor = $executor->withInterceptor(
    new TokenCounterInterceptor($tokenCounter, $logger),
);

此示例演示了如何创建一个更复杂且有用的拦截器。令牌计数拦截器对于监控API使用、优化提示长度或确保您保持在LLM提供商的令牌限制内非常有价值。

您可以根据您的特定需求创建各种类型的其他拦截器,例如

  • 缓存拦截器,用于存储和检索相同提示的响应
  • 速率限制拦截器,用于控制API调用的频率
  • 错误处理拦截器,用于优雅地管理和记录异常
  • 分析拦截器,用于收集有关代理性能和用法模式的有关数据

实现所需接口

要使用LLM代理包,您需要在项目中实现所需接口。

→ LLMInterface

它作为您的应用程序和您正在使用的LLM(如OpenAI、Claude等)之间的桥梁。

use LLM\Agents\LLM\ContextInterface;
use LLM\Agents\LLM\LLMInterface;
use LLM\Agents\LLM\OptionsInterface;
use LLM\Agents\LLM\Prompt\Chat\MessagePrompt;
use LLM\Agents\LLM\Prompt\Chat\PromptInterface as ChatPromptInterface;
use LLM\Agents\LLM\Prompt\PromptInterface;
use LLM\Agents\LLM\Prompt\Tool;
use LLM\Agents\LLM\Response\Response;
use OpenAI\Client;

final readonly class OpenAILLM implements LLMInterface
{
    public function __construct(
        private Client $client,
        private MessageMapper $messageMapper,
        private StreamResponseParser $streamParser,
    ) {}

    public function generate(
        ContextInterface $context,
        PromptInterface $prompt,
        OptionsInterface $options,
    ): Response {
        $request = $this->buildOptions($options);

        $messages = $prompt instanceof ChatPromptInterface
            ? $prompt->format()
            : [MessagePrompt::user($prompt)->toChatMessage()];

        $request['messages'] = array_map(
            fn($message) => $this->messageMapper->map($message),
            $messages
        );

        if ($options->has('tools')) {
            $request['tools'] = array_values(array_map(
                fn(Tool $tool): array => $this->messageMapper->map($tool),
                $options->get('tools')
            ));
        }

        $stream = $this->client->chat()->createStreamed($request);

        return $this->streamParser->parse($stream);
    }

    private function buildOptions(OptionsInterface $options): array
    {
        $defaultOptions = [
            'temperature' => 0.8,
            'max_tokens' => 120,
            'model' => null,
            // Add other default options as needed
        ];

        $result = array_intersect_key($options->getIterator()->getArrayCopy(), $defaultOptions);
        $result += array_diff_key($defaultOptions, $result);

        if (!isset($result['model'])) {
            throw new \InvalidArgumentException('Model is required');
        }

        return array_filter($result, fn($value) => $value !== null);
    }
}

以下是 MessageMapper 的示例,它将消息转换为LLM API所需的格式。

use LLM\Agents\LLM\Prompt\Chat\ChatMessage;
use LLM\Agents\LLM\Prompt\Chat\Role;
use LLM\Agents\LLM\Prompt\Chat\ToolCalledPrompt;
use LLM\Agents\LLM\Prompt\Chat\ToolCallResultMessage;
use LLM\Agents\LLM\Prompt\Tool;
use LLM\Agents\LLM\Response\ToolCall;

final readonly class MessageMapper
{
    public function map(object $message): array
    {
        if ($message instanceof ChatMessage) {
            return [
                'content' => $message->content,
                'role' => $message->role->value,
            ];
        }

        if ($message instanceof ToolCallResultMessage) {
            return [
                'content' => \is_array($message->content) ? \json_encode($message->content) : $message->content,
                'tool_call_id' => $message->id,
                'role' => $message->role->value,
            ];
        }

        if ($message instanceof ToolCalledPrompt) {
            return [
                'content' => null,
                'role' => Role::Assistant->value,
                'tool_calls' => \array_map(
                    static fn(ToolCall $tool): array => [
                        'id' => $tool->id,
                        'type' => 'function',
                        'function' => [
                            'name' => $tool->name,
                            'arguments' => $tool->arguments,
                        ],
                    ],
                    $message->tools,
                ),
            ];
        }

        if ($message instanceof Tool) {
            return [
                'type' => 'function',
                'function' => [
                    'name' => $message->name,
                    'description' => $message->description,
                    'parameters' => [
                            'type' => 'object',
                            'additionalProperties' => $message->additionalProperties,
                        ] + $message->parameters,
                    'strict' => $message->strict,
                ],
            ];
        }

        if ($message instanceof \JsonSerializable) {
            return $message->jsonSerialize();
        }

        throw new \InvalidArgumentException('Invalid message type');
    }
}

提示生成

它在为代理准备上下文和指令以处理用户请求之前起着至关重要的作用。它确保代理拥有所有必要的信息,包括其自身的指令、内存、相关代理以及任何相关的会话上下文。

  • 包含代理指令和重要规则的系统消息。
  • 包含代理记忆(经验)的系统消息。
  • 关于相关代理的系统消息(如果有)。
  • 包含会话上下文的系统消息(如果提供)。
  • 包含实际提示的用户消息。

您可以根据特定需求自定义提示生成逻辑。

您不必自己实现AgentPromptGeneratorInterface,可以使用llm-agents/prompt-generator包作为实现。此包提供了一个灵活且可扩展的系统,用于生成带有所有必需系统消息和用户消息的聊天提示。

注意:请在此处阅读llm-agents/prompt-generator包的完整文档here

要使用它,首先安装包

composer require llm-agents/prompt-generator

然后在您的项目中设置它。以下是一个使用Spiral框架的示例

use LLM\Agents\PromptGenerator\Interceptors\AgentMemoryInjector;
use LLM\Agents\PromptGenerator\Interceptors\InstructionGenerator;
use LLM\Agents\PromptGenerator\Interceptors\LinkedAgentsInjector;
use LLM\Agents\PromptGenerator\Interceptors\UserPromptInjector;
use LLM\Agents\PromptGenerator\PromptGeneratorPipeline;

class PromptGeneratorBootloader extends Bootloader
{
    public function defineSingletons(): array
    {
        return [
            PromptGeneratorPipeline::class => static function (
                LinkedAgentsInjector $linkedAgentsInjector,
            ): PromptGeneratorPipeline {
                $pipeline = new PromptGeneratorPipeline();

                return $pipeline->withInterceptor(
                    new InstructionGenerator(),
                    new AgentMemoryInjector(),
                    $linkedAgentsInjector,
                    new UserPromptInjector(),
                    // Add more interceptors as needed
                );
            },
        ];
    }
}

→ SchemaMapperInterface

此类负责处理JSON模式和PHP对象之间的转换。

我们提供了一个schema mapper包,您可以使用它来在项目中实现SchemaMapperInterface。此包是LLM Agents项目的超实用JSON Schema Mapper。

要安装包

composer require llm-agents/json-schema-mapper

注意:请在此处阅读llm-agents/json-schema-mapper包的完整文档here

→ ContextFactoryInterface

它提供了一种干净的方法,通过系统传递执行特定的数据,而不会紧密耦合组件或过度复杂化方法签名。

use LLM\Agents\LLM\ContextFactoryInterface;
use LLM\Agents\LLM\ContextInterface;

final class ContextFactory implements ContextFactoryInterface
{
    public function create(): ContextInterface
    {
        return new class implements ContextInterface {
            // Implement any necessary methods or properties for your context
        };
    }
}

→ OptionsFactoryInterface

选项是一个简单的键值存储,允许您存储和检索可以传递给LLM客户端和其他组件的配置选项。例如,您可以将模型名称、最大令牌和其他配置选项传递给LLM客户端。

use LLM\Agents\LLM\OptionsFactoryInterface;
use LLM\Agents\LLM\OptionsInterface;

final class OptionsFactory implements OptionsFactoryInterface
{
    public function create(): OptionsInterface
    {
        return new class implements OptionsInterface {
            private array $options = [];

            public function has(string $option): bool
            {
                return isset($this->options[$option]);
            }

            public function get(string $option, mixed $default = null): mixed
            {
                return $this->options[$option] ?? $default;
            }

            public function with(string $option, mixed $value): static
            {
                $clone = clone $this;
                $clone->options[$option] = $value;
                return $clone;
            }

            public function getIterator(): \Traversable
            {
                return new \ArrayIterator($this->options);
            }
        };
    }
}

🏗️ 架构

LLM Agents包围绕几个关键组件构建

  • AgentInterface:定义所有代理的合约。
  • AgentAggregate:实现AgentInterface并将代理实例与其他解决方案对象聚合。
  • Agent:代表一个具有其键、名称、描述和指令的单个代理。
  • Solution:Model和ToolLink等组件的抽象基类。
  • AgentExecutor:负责执行代理并管理它们的交互。
  • Tool:代表代理可以使用以执行任务的能力。

有关架构的视觉表示,请参阅文档中的类图。

🎨 类图

这是一个类图,说明了LLM Agents PHP SDK的关键组件

🙌 想要贡献吗?

感谢您考虑为llm-agents-php社区做出贡献!我们欢迎所有类型的贡献。如果您想要

您非常受欢迎。在贡献之前,请查看我们的贡献指南

Conventional Commits

⚖️ 许可证

LLM Agents是开源软件,许可协议为MIT许可

Licence