この記事で作るもの
管理画面に「AIアシスタント」タブを1つ追加し、そこにチャット欄を置くだけです。
[あなた] UserモデルにSNS連携用のprovider, provider_idカラムを追加するマイグレーションを書いて
[Claude] 以下のマイグレーションを作成しました。中間テーブルではなくusersテーブルへの
直接追加案です。ユニーク制約は (provider, provider_id) の複合キーにしています…
```php
class m260714_000000_add_sns_columns_to_users extends Migration { ... }
```
この設計だと1ユーザーが複数SNS連携する場合に対応できません。もし複数連携を
想定しているなら中間テーブル案も出せますが、どちらにしますか?
[あなた] 複数連携できるようにして
[Claude] では user_social_accounts テーブルを新設する案に切り替えます…単発の「コード生成」ではなく、要件を詰めながら会話でコードを育てていくのがポイントです。Claudeはmax_tokensの許す限り会話履歴を保持するので、前の提案を踏まえた修正依頼にも自然に対応できます。
全体構成
| 要素 | 役割 |
|---|---|
components/ClaudeAgent.php | Claude APIを叩くYii2コンポーネント |
controllers/AiAssistantController.php | チャットのAPIエンドポイント |
models/AiConversation.php | 会話履歴(セッション or DB保存) |
views/ai-assistant/index.php | チャットUI(Bootstrap + Fetch API) |
config/params.php | APIキーなどの設定 |
Yii2のDI/Componentの仕組みは、LaravelでいうServiceクラスをコンテナに登録するのとほぼ同じ感覚で扱えます。
1. APIキーの設定
config/params.php に追記します(.env を使っている場合はそちら経由で読み込んでください)。
<?php
return [
'claudeApiKey' => getenv('ANTHROPIC_API_KEY'),
'claudeModel' => 'claude-sonnet-5', // コーディング用途はSonnetでバランス良好
];web.php の components にも登録しておくとコントローラーから Yii::$app->claudeAgent で呼べて便利です。
'components' => [
// ...既存のコンポーネント
'claudeAgent' => [
'class' => 'app\components\ClaudeAgent',
'apiKey' => $params['claudeApiKey'],
'model' => $params['claudeModel'],
],
],2. Claude APIを叩くコンポーネント
Yii2は標準で yii\httpclient\Client(yiisoft/yii2-httpclient)が使えるので、Guzzleを別途入れなくてもOKです。未導入なら composer require yiisoft/yii2-httpclient を実行してください。
<?php
// components/ClaudeAgent.php
namespace app\components;
use yii\base\Component;
use yii\httpclient\Client;
use yii\base\Exception;
class ClaudeAgent extends Component
{
public string $apiKey;
public string $model = 'claude-sonnet-5';
public int $maxTokens = 2048;
private const API_URL = 'https://api.anthropic.com/v1/messages';
private const API_VERSION = '2023-06-01';
/**
* @param array $history [['role' => 'user'|'assistant', 'content' => string], ...]
* @param string $systemPrompt Yii2固有の文脈を教えるシステムプロンプト
* @return string アシスタントの返答テキスト
*/
public function chat(array $history, string $systemPrompt): string
{
$client = new Client(['baseUrl' => '']);
$response = $client->createRequest()
->setMethod('POST')
->setUrl(self::API_URL)
->setHeaders([
'x-api-key' => $this->apiKey,
'anthropic-version' => self::API_VERSION,
'content-type' => 'application/json',
])
->setContent(json_encode([
'model' => $this->model,
'max_tokens' => $this->maxTokens,
'system' => $systemPrompt,
'messages' => $history,
]))
->send();
if (!$response->isOk) {
throw new Exception('Claude API error: ' . $response->content);
}
$data = $response->data;
// content配列からtext種別を連結(tool_use等が混じる場合を考慮)
$text = '';
foreach ($data['content'] as $block) {
if ($block['type'] === 'text') {
$text .= $block['text'];
}
}
return $text;
}
}ポイント
systemパラメータにYii2固有のルール(後述)を書くことで、的外れなLaravel流の回答を防ぎます。contentを配列でループしているのは、将来ツール実行(tool_use)を組み込む拡張余地を残すためです。
3. 会話履歴を持つコントローラー
会話はDBに残すのが理想ですが、まずは最小構成としてセッション保存にします。
<?php
// controllers/AiAssistantController.php
namespace app\controllers;
use Yii;
use yii\web\Controller;
use yii\web\Response;
use yii\filters\VerbFilter;
class AiAssistantController extends Controller
{
public function behaviors()
{
return [
'verbs' => [
'class' => VerbFilter::class,
'actions' => [
'send' => ['post'],
'reset' => ['post'],
],
],
];
}
public function actionIndex()
{
$history = Yii::$app->session->get('ai_chat_history', []);
return $this->render('index', ['history' => $history]);
}
public function actionSend()
{
Yii::$app->response->format = Response::FORMAT_JSON;
$message = Yii::$app->request->post('message', '');
if (trim($message) === '') {
return ['error' => 'メッセージが空です'];
}
$history = Yii::$app->session->get('ai_chat_history', []);
$history[] = ['role' => 'user', 'content' => $message];
$systemPrompt = <<<PROMPT
あなたはYii2フレームワークの上級エンジニアです。
- 回答はYii2の作法(ActiveRecord, Widget, Behavior, RBAC等)に従うこと
- Laravel的な書き方(Eloquentのスコープ記法など)を混ぜないこと
- コードは動くものを示し、設計判断が分かれる場合は選択肢を提示して確認を取ること
- 日本語で簡潔に回答すること
PROMPT;
try {
$reply = Yii::$app->claudeAgent->chat($history, $systemPrompt);
} catch (\Throwable $e) {
Yii::error($e->getMessage(), 'claude-agent');
return ['error' => 'AIエージェントとの通信に失敗しました'];
}
$history[] = ['role' => 'assistant', 'content' => $reply];
Yii::$app->session->set('ai_chat_history', $history);
return ['reply' => $reply];
}
public function actionReset()
{
Yii::$app->session->remove('ai_chat_history');
Yii::$app->response->format = Response::FORMAT_JSON;
return ['status' => 'ok'];
}
}system プロンプトで「Yii2の作法に従う」「設計判断が割れる場合は確認を取る」と明示しているのが、単なるコード生成器と対話型エージェントを分ける最大のポイントです。
4. チャットUI(View)
Bootstrap 5(Yii2 basic templateに標準搭載)+ 素のFetch APIだけで組めます。Reactなどは不要です。
<?php
// views/ai-assistant/index.php
/** @var array $history */
use yii\helpers\Html;
use yii\helpers\Url;
$this->title = 'AIコーディングアシスタント';
?>
<div class="ai-assistant-index">
<h1><?= Html::encode($this->title) ?></h1>
<div id="chat-log" class="border rounded p-3 mb-3" style="height: 480px; overflow-y: auto; background:#fafafa;">
<?php foreach ($history as $msg): ?>
<div class="mb-3 <?= $msg['role'] === 'user' ? 'text-end' : '' ?>">
<span class="badge <?= $msg['role'] === 'user' ? 'bg-primary' : 'bg-secondary' ?> mb-1">
<?= $msg['role'] === 'user' ? 'あなた' : 'Claude' ?>
</span>
<pre class="bg-white border rounded p-2" style="white-space: pre-wrap;"><?= Html::encode($msg['content']) ?></pre>
</div>
<?php endforeach; ?>
</div>
<form id="chat-form" class="d-flex gap-2">
<input type="text" id="chat-input" class="form-control"
placeholder="例: UserモデルにSNS連携カラムを追加したい" autocomplete="off">
<button type="submit" class="btn btn-primary">送信</button>
<button type="button" id="chat-reset" class="btn btn-outline-secondary">リセット</button>
</form>
</div>
<?php
$sendUrl = Url::to(['ai-assistant/send']);
$resetUrl = Url::to(['ai-assistant/reset']);
$csrfParam = Yii::$app->request->csrfParam;
$csrfToken = Yii::$app->request->csrfToken;
$js = <<<JS
const chatLog = document.getElementById('chat-log');
const form = document.getElementById('chat-form');
const input = document.getElementById('chat-input');
function appendBubble(role, text) {
const wrap = document.createElement('div');
wrap.className = 'mb-3' + (role === 'user' ? ' text-end' : '');
wrap.innerHTML = `
<span class="badge ${role === 'user' ? 'bg-primary' : 'bg-secondary'} mb-1">
${role === 'user' ? 'あなた' : 'Claude'}
</span>
<pre class="bg-white border rounded p-2" style="white-space: pre-wrap;"></pre>
`;
wrap.querySelector('pre').textContent = text;
chatLog.appendChild(wrap);
chatLog.scrollTop = chatLog.scrollHeight;
}
form.addEventListener('submit', async (e) => {
e.preventDefault();
const message = input.value.trim();
if (!message) return;
appendBubble('user', message);
input.value = '';
const res = await fetch('$sendUrl', {
method: 'POST',
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
body: new URLSearchParams({ message: message, '$csrfParam': '$csrfToken' }),
});
const data = await res.json();
appendBubble('assistant', data.reply || ('エラー: ' + data.error));
});
document.getElementById('chat-reset').addEventListener('click', async () => {
await fetch('$resetUrl', {
method: 'POST',
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
body: new URLSearchParams({ '$csrfParam': '$csrfToken' }),
});
chatLog.innerHTML = '';
});
JS;
$this->registerJs($js);
?>5. 動作イメージ
SiteController のメニューか、AppAsset のナビバーに以下を追加すればアクセスできます。
['label' => 'AIアシスタント', 'url' => ['/ai-assistant/index']],実際にブラウザで「Userモデルに論理削除を追加して」と送ると、Claudeは
deleted_atカラム追加のマイグレーションSoftDeleteBehavior(Yii2公式のyii2tech/ar-softdeleteor 自前Behavior)の実装案- 既存クエリへの影響(デフォルトスコープの扱い)についての確認質問
という順で、コードと一緒に「次にどうしたいか」を聞き返してくる会話になります。これがChatGPT的な単発生成との一番の違いです。


コメント