Open11
【個人開発】LLM連携チャットボット開発
はじめに
このスクラップはwebアプリ開発初心者に個人開発のメモです
目的
- チャットボットを開発してみてLLM APIとはなんぞやを知る
- 自分の利用目的に特化したアプリで月額20ドルより安く利用できたらBe Happy
- webアプリ開発のスキルアップ
想定している技術スタック
バックエンド
言語: PHP
フレームワーク: Laravel
フロントエンド
Livewire
Alpine.js (Livewireに付属)
Tailwind CSS (スタイリング)
データベース
MySQL 8.0+ (LaravelのデフォルトDB)
キャッシュ/セッション管理
ファイルキャッシュ (開発初期段階)
将来的にRedisへの移行を検討
検索機能
初期段階:MySQLの全文検索機能
将来:Laravel Scout+検索エンジン
LLM API連携
OpenAI API
ファイルストレージ
ローカルファイルシステム (開発初期段階)
将来的にAmazon S3への移行を検討
開発環境
Laravel Sail (Docker開発環境)
認証
Laravel Breeze (認証スターターキット)
開発環境構築
Laravel sailで構築
参考記事
メモ
-
sail up -d
の後、localhostにアクセスするにはマイグレーションする必要あり
試し接続
手順
- openAI API keyの取得
- openai-php/laravelのインストール
https://github.com/openai-php/client
composer require openai-php/laravel
- 接続の記述
$client = OpenAI::client(config('services.openai.api_key'));
$result = $client->chat()->create([
'model' => 'gpt-3.5-turbo',
'messages' => [
['role' => 'user', 'content' => $request->message],
],
]);
openAI APIのドキュメントを読む
★=必読
☆=後で読む
Docs
プロンプトの例文 ★
これを使ってメッセージの種類分けができそう
関数呼び出し
よくわからん がそのうち使ってみたい
構造化出力
Advanced Usage ☆
パラメータとかトークン管理について書かれてる
Chat Completions ★
OpenAIのコアAPI
まずはシンプルにこれを使ってチャットボット開発ファインチューニング ☆
将来的にこれに挑戦したい
API reference
Chat
基本的なhttpリクエストとレスポンスの説明
連続した会話を実現する
概要
openAI APIはステートレスなAPIなため連続した会話を実現するにはそれまでの会話をメッセージに含める必要がある
方法
過去の会話を含めるためには会話を保存・取り出す必要がある。そのためには以下の2つの方法がある
- セッションベース
- DBベース
どちらも一長一短があるが、今回は「セッションベースでセッションの期限が切れたらDBから取り出す」ハイブリッド方式を選択する。
サンプルコード
<?php
namespace App\Http\Controllers;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Http;
use App\Models\Conversation;
use App\Models\Message;
use Carbon\Carbon;
class ChatbotController extends Controller
{
public function chat(Request $request)
{
$userMessage = $request->input('message');
$userId = $request->user()->id;
// セッションから会話履歴を取得
$conversationHistory = $request->session()->get('conversation_history', []);
if (empty($conversationHistory)) {
// セッションが空の場合、DBから最新のアクティブな会話を取得
$conversation = Conversation::where('user_id', $userId)
->where('is_active', true)
->first();
if (!$conversation) {
// アクティブな会話がない場合、新しい会話を作成
$conversation = Conversation::create([
'user_id' => $userId,
'is_active' => true,
'started_at' => now(),
]);
}
// DBから最新の10件のメッセージを取得
$conversationHistory = $conversation->messages()
->orderBy('created_at', 'desc')
->take(10)
->get()
->reverse()
->map(function ($message) {
return ['role' => $message->role, 'content' => $message->content];
})
->values()
->toArray();
// 取得した履歴をセッションに保存
$request->session()->put('conversation_history', $conversationHistory);
$request->session()->put('conversation_id', $conversation->id);
}
// 新しいメッセージを履歴に追加
$conversationHistory[] = ['role' => 'user', 'content' => $userMessage];
// OpenAI APIリクエストの準備と送信
$response = Http::withHeaders([
'Authorization' => 'Bearer ' . config('services.openai.api_key'),
'Content-Type' => 'application/json',
])->post('https://api.openai.com/v1/chat/completions', [
'model' => 'gpt-3.5-turbo',
'messages' => $conversationHistory,
]);
// APIレスポンスの処理
$botReply = $response->json()['choices'][0]['message']['content'];
// ボットの返答を履歴に追加
$conversationHistory[] = ['role' => 'assistant', 'content' => $botReply];
// 更新された履歴をセッションに保存(最新の10メッセージのみ保持)
$request->session()->put('conversation_history', array_slice($conversationHistory, -10));
// DBにも保存
$conversationId = $request->session()->get('conversation_id');
$conversation = Conversation::findOrFail($conversationId);
$conversation->messages()->createMany([
['role' => 'user', 'content' => $userMessage],
['role' => 'assistant', 'content' => $botReply]
]);
$conversation->touch(); // 最終更新時刻を更新
return response()->json(['reply' => $botReply]);
}
public function clearHistory(Request $request)
{
$userId = $request->user()->id;
// セッションをクリア
$request->session()->forget(['conversation_history', 'conversation_id']);
// DBのアクティブな会話を終了
Conversation::where('user_id', $userId)
->where('is_active', true)
->update(['is_active' => false, 'ended_at' => now()]);
return response()->json(['message' => 'Conversation history cleared']);
}
}
phpmyadminの導入
参考記事
メモ
- DBユーザーに権限付与必要
laravel Breezeの導入
livewireも使いたいので認証機能もlivewire volt functionで導入
volt使ったことないのでここもキャッチアップ必要
ひとまずログイン/サインアップ→チャット画面へのルート定義
開発
まずはメッセージを送信したらその内容とAPIからのレスポンスを保存する機能を作る
■ table作成
作成するテーブル
- conversations
- messages
■ modelの作成とリレーションシップ定義
開発
ChatbotControllerの作成
ここでの作るもの
- ユーザーによるメッセージの送信
- APIからの返信メッセージの表示
- conversationの保存
- conversationのtitle生成+保存
- ユーザーからのメッセージの保存
- APIからの返信メッセージの保存
メモ
- openAI APIはステートレスなAPIのため、それまでの会話内容を含めた会話をするには会話の履歴を
message
として配列で渡してあげる必要がある
public function getMessageHistory()
{
return $this->messages()
->orderBy('created_at', 'asc')
->take(10)
->get()
->map(function ($message) {
return [
'role' => $message->role,
'content' => $message->content,
];
})
->toArray();
}
$result = $client->chat()->create([
'model' => 'gpt-3.5-turbo',
'messages' => $conversation->getMessageHistory(),
]);
改善
URLにconversation_idを使用するのでidをauto_incrementの値ではなくuuidに変更
public function up(): void
{
Schema::create('conversations', function (Blueprint $table) {
$table->uuid('id')->primary();
$table->foreignId('user_id')->constrained()->onDelete('cascade');
$table->text('title');
$table->timestamps();
});
}
conversationの閲覧権限をログインユーザーに限定
// ChatbotController.php
public function index(?string $conversationId = null): View
{
$conversations = Conversation::where('user_id', auth()->id())->get();
if ($conversationId) {
$conversation = Conversation::forUser(auth()->id())->findOrFail($conversationId);
$messages = $conversation->messages()->orderBy('created_at', 'asc')->get();
$title = $conversation->title;
} else {
$conversation = null;
$messages = [];
$title = '';
}
return view('index', compact('conversations', 'conversationId', 'title', 'messages'));
}
// App/Models/Conversation.php
public function scopeForUser(Builder $query, $userId): Builder
{
return $query->where('user_id', $userId);
}
note
クエリスコープを活用することでコードの再利用性とビジネスロジックの意図の明確を行う
参考記事