1
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

Symfony AI Bundle使ってみた - AIチャット編 -

Posted at

Symfonyから生成AIを使った開発支援コンポーネント群 Symfony AI発表されました。

コンポーネントは、

  • 各種AIエージェントの設定ができるPlatformコンポーネント
  • AIエージェントを利用するAgentコンポーネント
  • RAGを扱うStoreコンポーネント

が用意されています。また、バンドルとして

  • Symfony AI Bundle
  • Symfony MCP Bundle

が用意されています。

まだ実験段階のコンポーネントになりますが、Symfonyと生成AIを掛け合わせた開発がよりしやすくなりそうなので、今回はSymfony AI Bundleの導入からAIエージェントを利用したAIチャットの実装方法をまとめてみました。

Symfonyでの実装になります。Symfonyのインストールなどは割愛します。

インストール

まだ実験段階となるため、composer.jsonの minimum-stabilitydev に変更した後で、以下のコマンドを実行します。

composer require symfony/ai-bundle

設定

通常のSymfonyコンポーネントの場合、Symfony Recipeによって必要な設定ファイルが自動的に生成されますが、まだ実験段階なので、自分で作る必要があります。 config/packages/ai.yaml を作成しましょう。
必要最低限な設定は以下になります。

config/packages/ai.yaml
ai:
    platform:
        openai:
            api_key: '%env(OPENAI_API_KEY)%'
    agent:
        cook: <- エージェント名
            model:
                class: 'Symfony\AI\Platform\Bridge\OpenAi\Gpt'
                name: !php/const Symfony\AI\Platform\Bridge\OpenAi\Gpt::GPT_5_NANO

この例では2つのセクションがあります。

platform セクションでは、AIプラットフォームの設定を行います。
ここでは、OpenAI(ChatGPT)を利用するためにAPIキーを設定しています。

AIプラットフォームは、

  • OpenAI
  • Claude
  • Gemini
  • Amazon's Nova

などが用意されています。

agent セクションでは、AIエージェントの設定を行います。
どのモデルを使うのか、あらかじめ渡しておくメッセージなどが設定できます。
こちらもそれぞれのプラットフォームごとに複数のモデルが用意されています。
モデルは、 Symfony\AI\Platform\Bridge を覗くとたくさん定義されています。

実装

シンプルなAIエージェント

早速シンプルなAIエージェントを使ったサービスを作ってみましょう。今回は今晩の夕食のレシピを教えてくれるコマンドを作ってみます。
上記の例で、 agentcook というエージェントを定義しています。Symfony AI Bundleを利用する場合、いつもの様にオートワイヤリングでこのエージェントオブジェクトをDIしますが、 {エージェント名}Agent という変数名にすることで、設定したエージェントオブジェクトをDIできます。

RecipeCommand.php
<?php

namespace App\Command;

use Symfony\AI\Agent\AgentInterface;
use Symfony\AI\Platform\Message\Message;
use Symfony\AI\Platform\Message\MessageBag;
use Symfony\Component\Console\Attribute\AsCommand;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Input\InputArgument;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Input\InputOption;
use Symfony\Component\Console\Output\OutputInterface;
use Symfony\Component\Console\Style\SymfonyStyle;

#[AsCommand(
    name: 'app:recipe',
    description: 'レシピを教えてくれるコマンド',
)]
class RecipeCommand extends Command
{
    public function __construct(
        private readonly AgentInterface $cookAgent, <- `cook`のエージェントを使う
    )
    {
        parent::__construct();
    }

このエージェントにメッセージを送るには MessageBag を使います。 ここの Message オブジェクトを話しますが、 Message::forSystem はシステムに対してのメッセージで、どのような立ち振る舞いをするかなどをAIエージェントに教えます。 Message::ofUser は、自身のメッセージ( = 質問など) を伝えます。
この MessageBag を、エージェントオブジェクトの call() メソッドに渡して結果を得ます。

RecipeCommand.php
    protected function execute(InputInterface $input, OutputInterface $output): int
    {
        $io = new SymfonyStyle($input, $output);
        $userId = $input->getArgument('userId');

        $messageBag = new MessageBag(
            Message::forSystem('あなたはフレンチのコックさんです。レシピを考えてください。'),
            Message::ofUser('今日の夕食のレシピを考えてください。')
        ); // AIに渡すメッセージの作成

        $io->info('しばらくお待ちください。。');
        $result = $this->cookAgent->call($messageBag); // AIエージェントでメッセージを渡す
        $io->text($result->getContent()); // <- 結果のgetContent()にAIからのメッセージが格納されている

これだけで完成です。コマンドを叩いてみましょう。
.envOPENAI_API_KEY の設定を忘れずに。。

php bin/console app:recipe
 [INFO] しばらくお待ちください。。                                                          
 
 かしこまりました。今夜のフレンチ3品コースをご用意します。2〜3人分の分量です。作業時間の目安は約90分前後。季節の食材があれば差し替えも楽しめます。...

はい。動きました。

Tools

Symfony AI-BundleのAgentには、Tool が設定できます。
Toolを使うことで、AIにメッセージとして補足情報を追加することができ、さらに綿密にAIエージェントとやり取りができます。
今回は、ユーザのランクによってレシピの予算を変更できる様にしてみます。

設定方法

Toolは、 #[AsTool]アトリビュートを付与したクラスを用意し、エージェントのツールとして登録することで利用できます。

#[AsTool] トリビュートには引数が定義されており、ツール名、ツールの説明、ツールで呼び出すメソッド名となっています。メソッド名は指定がなければデフォルトで __invoke が呼ばれる様になっています。
メソッドは必ず string を返す必要があります。

UserRank.php
<?php

namespace App\AI\Tool;

use App\Entity\User;
use App\Repository\UserRepository;
use Symfony\AI\Agent\Toolbox\Attribute\AsTool;

#[AsTool('user_rank', 'ユーザのランクを返します。', 'getRank')]
class UserRank
{
    public function __construct(private readonly UserRepository $userRepository)
    {
    }

    public function getRank(int $userId): string
    {
        $user = $this->userRepository->find($userId);
        return $user?->getRank() ?? 'normal';
    }
}

このツールは、与えられたユーザIDからユーザの会員ランクを返します。 

RecipeBudget.php
<?php

namespace App\AI\Tool;

use Symfony\AI\Agent\Toolbox\Attribute\AsTool;

#[AsTool('recipe_budget', 'ユーザランクごとのレシピの予算を返します。', 'getBudget')]
class RecipeBudget
{
    public function getBudget(string $rank): string
    {
        return match ($rank) {
            'gold' => '5000円以内',
            'silver' => '3000円以内',
            default => '1500円以内'
        };
    }
}

こちらは、ユーザのランクによって、レシピの予算を返すツールです。

こんな感じでデータを用意しています。
ランクには、 gold, silver, normal が設定される様になってます。

sqlite> insert into user values(1, 'test1', 'gold');
sqlite> insert into user values(2, 'test2', 'silver');
sqlite> insert into user values(3, 'test3', 'normal');

作ったツールは ai.yaml で登録します。

ai.yaml
ai:
    platform:
        openai:
            api_key: '%env(OPENAI_API_KEY)%'
    agent:
        cook:
            model:
                class: 'Symfony\AI\Platform\Bridge\OpenAi\Gpt'
                name: !php/const Symfony\AI\Platform\Bridge\OpenAi\Gpt::GPT_5_NANO
+           tools:
+               - 'App\AI\Tool\RecipeBudget'
+               - 'App\AI\Tool\UserRank'

すこし、AIに送るメッセージを調整します。

RecipeCommand.php
    protected function execute(InputInterface $input, OutputInterface $output): int
    {
        $io = new SymfonyStyle($input, $output);
        $userId = $input->getArgument('userId');

        $messageBag = new MessageBag(
            Message::forSystem('あなたはフレンチのコックさんです。レシピを考えてください。ユーザのランクによって予算上限を決めています。結果にはランクやユーザIDは含まないでください'),
            Message::ofUser('今日の夕食のレシピを考えてください。作ったレシピの予算も教えてください。',
                sprintf('ユーザID: %d', $userId),)
        );

        $io->info('しばらくお待ちください。。');
        $result = $this->cookAgent->call($messageBag);
        $io->text($result->getContent());
        return Command::SUCCESS;
    }

コマンドの引数から受け取ったユーザのIDと予算についての補足をメッセージに加えました。
『どのツールを使って欲しい』といった要望は伝えていません。
これでコマンドを実行してみましょう。

# gold会員のレシピ
php bin/console app:recipe 1
 [INFO] しばらくお待ちください。。                                                          
 
 今日の夕食のフレンチ風コースをご用意しました。2名分で予算は5,000円以内を想定しています(目安4,500円前後)。実店舗の価格で多少前後しますので、安い食材を選ぶとより余裕をもって作れます。

コース構成
- Entrée(前菜):Soupe à l'oignon gratinée(オニオンスープのグラタン仕立て)
- Plat principal(メイン):Poulet rôti au citron et herbes(レモンとハーブの鶏のロースト)と一緒に野菜のロティ
- Dessert(デザート):Poire pochée au vin rouge(赤ワイン煮の洋梨)
...

ツールの利用については伝えていませんが、gold会員だから、予算が5,000円以内のレシピとなっています。
どういった処理になっているのでしょうか?

Toolsを使った場合のAIとのやり取り

このツールを設定したことで、AIにはツール情報をメッセージとして追加で送っています。AIはこのツールの情報を考慮し、ツールの実行が必要と判断した場合は、どのツールをどの引数で実行するかをレスポンスとして返します。

たとえば、今回であれば以下の様なレスポンスになっています。

image.png

AIは、user_rankツールを引数 1で実行する様にレスポンスを返しています。
このレスポンスを受け取ったSymfonyは UserRank::getRank(1) を実行し、その結果を含めて再度AIにメッセージを送ります。

image.png

Inputセクションの4つ目のメッセージに注目してください。ここに UserRank::getRank(1) の実行結果である gold が渡されています。このメッセージを受けてAIは次に recipe_budgetを引数goldで実行する様にレスポンスを返しています。同じ様に RecipeBudget::getBudget('gold') が実行され、 再度AIに5000円以内を含めたメッセージを送ります。

image.png

ここで、AIはツールの実行がもう必要ないと判断し、5,000円以内のレシピを生成し、レスポンスを返してきます。
エージェントオブジェクトはこのやりとりをカプセル化し、最終的な結果のみを返します。

Symfony Profilerコンポーネントを使うと、上記のキャプチャの様にAIとのやり取りを確認することができます!

まとめ

このように、かなり簡単に生成AIを使ったサービスを作ることができます。今回はコマンドにしていますが、AIからのレスポンスに時間がかかることを考慮して実装する必要はありますが、当然Webでも利用することができますので、ぜひ一度触ってみてください!

1
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?