---
title: "エージェントプラグイン"
description: "ReActスタイルのツールを使用するエージェント。Toolを拡張して、書ける限りのPHP機能を与えてください。"
---

エージェントプラグインを使用すると、LLMに**行動する能力を与えることができます** — SQLクエリを実行したり、HTTPエンドポイントにアクセスしたり、ファイルを読み取ったり、PHPメソッドとして表現できるあらゆる他のことをすることができます。これはReActスタイルのループで動作します：考え → ツール呼び出し → 観察 → 繰り返し → 回答。

## 5行、1エージェント
```php
use Nibiru\Module\Ai\Ai;
use Nibiru\Module\Ai\Plugin\Tools\PdoQuery;

$ai = new Ai();
echo $ai->agent()
        ->withTools([new PdoQuery()])
        ->run('How many active users do we have?');
// → "We have 1,247 active users." (after the agent ran SELECT count(*)…)
```
## 仕組み
```
user task
    ↓
LLM gets system prompt with tool definitions
    ↓
LLM emits ```tool {"tool":"pdo_query","args":{"sql":"SELECT…"}}```
    ↓
Agent runs the tool, captures result
    ↓
LLM gets observation, decides: more tools or final answer?
    ↓
"FINAL: 1,247 active users."
```
このプロトコルは、**フェンスされたJSONセンチネル** — `\`\`\`tool {...}\`\`\`` — を使用します。これにより、任意のモデルが生成できます。ネイティブなツール呼び出しAPIは不要なので、すべてのOllamaモデルでそのまま機能します。（ネイティブなツール呼び出しがサポートされているモデルは、`parseToolCall()`をオーバーライドするサブクラスを通じてプラグインできます。）

## 組み込みツール

Nibiruは3つを搭載しています：

| ツール | 何をしますか |
|---|---|
| `Tools\PdoQuery` | アプリのデータベースに対する単一の読み取り専用 `SELECT`。INSERT/UPDATE/DELETE/DROP/TRUNCATE/ALTER をブロックします。最大50行を JSON として返します。 |
| `Tools\HttpGet` | オプションのヘッダー付きで HTTP/HTTPS URL を GET します。ボディを8 KBに切り捨てて返します。 |
| `Tools\FileRead` | 相対パスでプロジェクトファイルを読み取ります。`..` のトラバーサルをブロックします。最大8 KBを返します。 |
```php
use Nibiru\Module\Ai\Plugin\Tools;

$agent = $ai->agent()->withTools([
    new Tools\PdoQuery(),
    new Tools\HttpGet(),
    new Tools\FileRead(),
]);

// Multi-step task
echo $agent->run(
    'Read application/controller/loginController.php and tell me '
  . 'whether it implements rate limiting.'
);
```
エージェントは `file_read` にパスを指定して呼び出し、ソースコードを観察し、実際に見た内容に基づいて回答します。想像ではなく、実際の内容に基づきます。

## カスタムツールの作成

`Tool` を拡張します：
```php
namespace App\AiTools;

use Nibiru\Module\Ai\Plugin\Tool;

class StripeRefund extends Tool
{
    public function name(): string { return 'stripe_refund'; }

    public function description(): string {
        return 'Issue a Stripe refund for a charge ID.';
    }

    public function schema(): array {
        return [
            'charge_id' => [
                'type' => 'string',
                'description' => 'A Stripe charge ID, e.g. ch_3K…',
                'required' => true,
            ],
            'amount_cents' => [
                'type' => 'integer',
                'description' => 'Amount to refund in cents. Omit for full refund.',
                'required' => false,
            ],
        ];
    }

    public function execute(array $args): mixed {
        $stripe = new \Stripe\StripeClient(getenv('STRIPE_SECRET_KEY'));
        $refund = $stripe->refunds->create(array_filter([
            'charge' => $args['charge_id'],
            'amount' => $args['amount_cents'] ?? null,
        ]));
        return json_encode([
            'refund_id' => $refund->id,
            'status'    => $refund->status,
            'amount'    => $refund->amount,
        ]);
    }
}
```
次に接続します：
```php
$ai->agent()
   ->withTools([new \App\AiTools\StripeRefund(), new Tools\PdoQuery()])
   ->run('Refund order #4421 — they were charged twice.');
```
エージェントは `pdo_query` を使用して支払いを検索し、その後その支払いIDで `stripe_refund` を呼び出します。

## トレースを確認する
```php
$agent = $ai->agent()->withTools([new Tools\PdoQuery()]);
$answer = $agent->run('How many products in the gold-plating category?');

foreach ($agent->trace() as $step) {
    echo "Step {$step['step']}: action={$step['action']}\n  obs={$step['observation']}\n";
}
```
デバッグ、監査トレース、または「自分の作業を示す」UIの構築に役立ちます。

## 安全性

- **`PdoQuery` ブロックは書き込みを許可します。** 書き込みアクセスが必要な場合は、監査トレール付きのより権限のあるサブクラスを作成してください。組み込みツールで SELECT のみの制限を解除しないでください。
- **`HttpGet` はデフォルトで任意の URL を許可します。** `[AI] http_allowed_hosts[]`（計画中）で許可リストを使用してロックダウンするか、URL フィルタリングを行う `RestrictedHttpGet` サブクラスを作成してください。
- **`FileRead` は `..` をブロックします。** アプリケーションのルートに限定されています。
- **最大反復回数。** INI の `agent.max_iterations = 6` が runaway ループを防ぎます。慎重に増やしてください。
- **ツールタイムアウト。** `agent.tool_timeout = 30`（秒）。ハングしたツールはリクエストを永遠に保持しません。

## 一般的な落とし穴

- **`withTools()`を忘れている。** ツールがないと、エージェントはただの`Chat`だけになる。
- **エージェントにシークレットを見せる。** APIキー、生パスワード、PIIなどの機密情報をツールのレスポンスに入れることは決してしない — モデルは完全な文字列を受け取る。
- **長いツール出力。** 各観察は会話に追加される。50 KBをダンプするツールはコンテキストをすぐに使い果たす。組み込みのツールは8 KBまで制限されているので、カスタムツールでも同じようにする。
- **返答にツール呼び出しがない = 最終回答。** モデルが最終回答を生み出すと、それがツール呼び出しのように見えるが検証できない場合、エージェントはそれを最終回答として扱う。プロンプトで明確にする: "ツール呼び出しまたは最終回答のいずれかを出力し、両方同時にはしない。"
