AIは極度の「忘れん坊」。文脈を理解させるためのサーバーサイドの役割
こんにちは!「LINUX工房」管理人の「リナックス先生」です。
前回の第2回では、プロンプトエンジニアリングを駆使して、AIの出力をシステム連携しやすいJSON形式に固定する技術を学びました。
いよいよ今回から、ブラウザから利用できる本格的なAI Webアプリケーションの構築に入っていきます。
生成AIを使ってチャットアプリを作ろうとしたとき、初心者が必ずぶつかる巨大な壁があります。それは「AIが直前の会話すら覚えていない」という問題です。
先生!前回学んだ方法で、AIに「僕の名前はコウです」って教えた直後に、「僕の名前は?」って質問してみたんです。
そしたら「あなたの名前は存じ上げません」って冷たく返されました……。ChatGPTはちゃんと前の会話を覚えているのに、APIだとどうしてダメなんですか!?
ふふふ、それはねコウ君。生成AIのAPIが「ステートレス(状態を持たない)」な仕組みで動いているからよ。
APIサーバー側には、誰がどんな質問をしたかという履歴は一切保存されないの。だから、過去の文脈を踏まえた回答をさせたいなら、私たちサーバーサイドエンジニアがデータベースに履歴を保存して、毎回「これまでの会話の全て」をAIに送信し直してあげる必要があるのよ。
今日はPHPとMySQLを使って、AIに「記憶」を実装していくわよ!
本記事では、AlmaLinux 9上に構築したLAMP環境(Linux, Apache, MySQL/MariaDB, PHP)を活用し、生成AIとユーザーの対話履歴をリレーショナルデータベースで管理し、動的にプロンプトを生成する実践的なバックエンド処理を解説します。
📚 本連載のカリキュラム(全8回)
目次
1. なぜAIは過去を忘れるのか?(ステートレスの壁)
REST APIの基本原則として「ステートレス(無状態)」があります。これは、1回1回のリクエストが完全に独立しており、サーバー側(この場合はAIのAPI提供元)は過去のリクエスト状態を保持しないという設計思想です。
この設計により、APIサーバーは数百万のユーザーからのリクエストを同時に、高速かつ負荷分散して処理することができます。
しかし、これはチャットアプリとしては致命的です。
AIに「文脈」を理解させるための唯一の解決策は、リクエストを送信するたびに、過去の会話履歴を含んだ巨大な配列(メッセージリスト)を構築して送ることです。
メッセージ構築のイメージ
APIに送信する messages 配列は、会話が進むごとに以下のように成長していきます。
# 1回目の質問
[
{"role": "user", "content": "私の名前はコウです。"}
]
# ↓ AIの返答が返ってくる
# 2回目の質問(過去の履歴を含めて送信する)
[
{"role": "user", "content": "私の名前はコウです。"},
{"role": "assistant", "content": "こんにちは、コウさん!よろしくお願いします。"},
{"role": "user", "content": "私の名前を覚えてる?"}
]
このように、role: assistant(AIの過去の発言)と role: user(ユーザーの過去の発言)を交互に配列に詰め込んでAPIに渡すことで、AIは「あたかも記憶があるかのように」振る舞うことができます。
この配列を構築するために、自前のサーバー(AlmaLinux 9)のデータベース(MySQL)が必要になるのです。
2. AlmaLinux 9:Apache・PHP・MariaDBの連携とSELinux設定
それでは、基盤となるLAMP環境をAlmaLinux 9上に構築します。今回はWebアプリケーションとして公開するため、PythonではなくWeb開発のスタンダードであるPHPを用いてAPIを直接呼び出します。
2-1. パッケージのインストールと起動
ターミナルから以下のコマンドを実行し、Apache(httpd)、MariaDB、そしてPHP環境(およびAPI通信に必要なcURLやデータベース接続用のモジュール)をインストールします。
# 必要なパッケージの一括インストール sudo dnf install -y httpd mariadb-server php php-mysqlnd php-curl php-json # サービスの起動と自動起動設定 sudo systemctl enable --now httpd sudo systemctl enable --now mariadb
2-2. 【重要】SELinuxによるネットワーク制限の解除
AlmaLinux 9の強固なセキュリティ機能である「SELinux」は、デフォルト設定でApache(httpd)が外部ネットワーク(AIのAPI)へ接続すること、およびローカルのデータベースに接続することをブロックします。これを許可しなければ、PHPスクリプトはエラーで停止します。
プロの現場では必須となる以下の設定コマンドを実行してください。
# Apacheが外部ネットワーク(API)へ通信することを許可 sudo setsebool -P httpd_can_network_connect 1 # Apacheがデータベース(MySQL/MariaDB)へ通信することを許可 sudo setsebool -P httpd_can_network_connect_db 1
これで、セキュアでありながら外部のAI APIと通信できるWebサーバー環境が整いました。
3. MySQL (MariaDB) によるチャット履歴データベースの設計
次に、MariaDBにログインして、会話履歴を保存するためのデータベースとテーブルを作成します。
sudo mysql -u root
テーブル設計の考え方
チャット履歴の管理には、通常2つのテーブルが必要です。
- sessions(セッションテーブル): 「1つのチャットスレッド」を管理します。ユーザーIDや、会話の開始日時を記録します。
- messages(メッセージテーブル): セッションに紐づく、1つ1つの発言(ユーザーの質問とAIの回答)を記録します。
-- データベースの作成
CREATE DATABASE ai_chat_db CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
USE ai_chat_db;
-- 1. セッションテーブル
CREATE TABLE chat_sessions (
session_id VARCHAR(64) PRIMARY KEY, -- PHPのsession_id等を格納
user_id INT DEFAULT 0, -- ログイン機能がある場合に使用
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
-- 2. メッセージテーブル
CREATE TABLE chat_messages (
id INT AUTO_INCREMENT PRIMARY KEY,
session_id VARCHAR(64) NOT NULL,
role ENUM('user', 'assistant', 'system') NOT NULL, -- 誰の発言か
content TEXT NOT NULL, -- 発言内容(長文に対応するためTEXT型)
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (session_id) REFERENCES chat_sessions(session_id) ON DELETE CASCADE
);
-- 検索を高速化するためのインデックス作成
CREATE INDEX idx_session_id ON chat_messages(session_id);
-- アプリケーション用ユーザーの作成と権限付与
CREATE USER 'ai_app_user'@'localhost' IDENTIFIED BY 'StrongPassword123!';
GRANT ALL PRIVILEGES ON ai_chat_db.* TO 'ai_app_user'@'localhost';
FLUSH PRIVILEGES;
EXIT;
日本語のマルチバイト文字(絵文字など)を正しく保存するため、文字コードは必ず utf8mb4 に設定することが重要です。
4. PHPによる実装①:履歴の保存と取得(PDO活用)
ここからは、Web公開ディレクトリ(/var/www/html/)にPHPスクリプトを配置していきます。
データベース接続には、SQLインジェクション対策として極めて堅牢なPDO(PHP Data Objects)のプリペアドステートメントを使用します。
Rocky Linux & AlmaLinux実践ガイド (impress top gear) [ 古賀 政純 ] 価格:3520円 |
db_connect.php(データベース接続用ファイル)
まずは共通のデータベース接続ファイルを切り出します。
<?php
// /var/www/html/db_connect.php
$host = '127.0.0.1';
$db = 'ai_chat_db';
$user = 'ai_app_user';
$pass = 'StrongPassword123!';
$charset = 'utf8mb4';
$dsn = "mysql:host=$host;dbname=$db;charset=$charset";
$options = [
PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC,
PDO::ATTR_EMULATE_PREPARES => false,
];
try {
$pdo = new PDO($dsn, $user, $pass, $options);
} catch (\PDOException $e) {
throw new \PDOException($e->getMessage(), (int)$e->getCode());
}
?>
chat_manager.php(履歴操作の関数群)
データベースへの書き込みと読み込みを行う関数を定義します。セッション管理には、ブラウザを開いている間有効なPHPのネイティブセッション(session_start())のIDを活用します。
<?php
// /var/www/html/chat_manager.php
require_once 'db_connect.php';
// メッセージをDBに保存する関数
function saveMessage($pdo, $session_id, $role, $content) {
// セッションが存在しない場合は作成
$stmt = $pdo->prepare("INSERT IGNORE INTO chat_sessions (session_id) VALUES (?)");
$stmt->execute([$session_id]);
// メッセージの保存
$stmt = $pdo->prepare("INSERT INTO chat_messages (session_id, role, content) VALUES (?, ?, ?)");
$stmt->execute([$session_id, $role, $content]);
}
// 過去のメッセージ履歴を取得する関数
function getHistory($pdo, $session_id, $limit = 10) {
// 最新のやり取りをN件取得し、古い順に並び替える(サブクエリ活用)
$sql = "
SELECT role, content FROM (
SELECT role, content, created_at
FROM chat_messages
WHERE session_id = ?
ORDER BY created_at DESC
LIMIT ?
) AS sub
ORDER BY created_at ASC
";
$stmt = $pdo->prepare($sql);
// LIMIT句は整数としてバインドする必要があるためbindValueを使用
$stmt->bindValue(1, $session_id, PDO::PARAM_STR);
$stmt->bindValue(2, $limit, PDO::PARAM_INT);
$stmt->execute();
return $stmt->fetchAll();
}
?>
getHistory 関数内のSQLがポイントです。「直近の数件のメッセージ」だけを取得し、それをAPIに送信する「古い順(時系列)」に並び直す処理をSQL側で完結させています。
5. PHPによる実装②:文脈を含めたAPIの呼び出し(cURL活用)
準備が整いました。ユーザーからPOSTリクエストを受け取り、過去の履歴と結合してAI API(ここでは一般的なREST APIエンドポイントを想定)にリクエストを送信するメインロジックを作成します。
※本来、APIキーはWeb公開ディレクトリ外の環境変数(.env)などに置くべきですが、今回はコードの可読性を優先し、簡略化しています。
<?php
// /var/www/html/api_chat.php
session_start();
require_once 'db_connect.php';
require_once 'chat_manager.php';
header('Content-Type: application/json; charset=UTF-8');
$api_key = "あなたの_API_KEY"; // 厳重に管理すること
$api_url = "https://api.example.com/v1/generate";
// セッションIDの取得
$session_id = session_id();
// POSTデータの受け取り(JSON形式を想定)
$inputJSON = file_get_contents('php://input');
$input = json_decode($inputJSON, TRUE);
$user_message = $input['message'] ?? '';
if (empty($user_message)) {
echo json_encode(["error" => "メッセージが空です"]);
exit;
}
// 1. ユーザーの新しいメッセージをDBに保存
saveMessage($pdo, $session_id, 'user', $user_message);
// 2. 過去の履歴(直近10件)をDBから取得
$history = getHistory($pdo, $session_id, 10);
// 3. APIに送信する messages 配列の構築
$messages = [];
// システムプロンプト(人格設定)を先頭に追加
$messages[] = [
"role" => "system",
"content" => "あなたは優秀なアシスタントです。過去の会話の文脈を踏まえて回答してください。"
];
// 取得した履歴を追加
foreach ($history as $msg) {
$messages[] = [
"role" => $msg['role'],
"content" => $msg['content']
];
}
// 4. cURLを使用してAPIへリクエスト送信
$post_data = json_encode([
"model" => "high-performance-model",
"messages" => $messages,
"temperature" => 0.7
]);
$ch = curl_init($api_url);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_POST, true);
curl_setopt($ch, CURLOPT_POSTFIELDS, $post_data);
curl_setopt($ch, CURLOPT_HTTPHEADER, [
'Content-Type: application/json',
'Authorization: Bearer ' . $api_key
]);
$response = curl_exec($ch);
$http_code = curl_getinfo($ch, CURLINFO_HTTP_CODE);
curl_close($ch);
if ($http_code === 200) {
$result = json_decode($response, true);
$ai_reply = $result['choices'][0]['message']['content'];
// 5. AIの回答をDBに保存(次回以降の文脈にするため)
saveMessage($pdo, $session_id, 'assistant', $ai_reply);
// クライアントへ回答を返す
echo json_encode(["reply" => $ai_reply]);
} else {
echo json_encode(["error" => "API通信エラー: HTTPステータス $http_code"]);
}
?>
この api_chat.php にJavaScript(Fetch API)などからメッセージをPOSTすることで、AIは以前のやり取りを「思い出しながら」返答してくれるようになります。
6. 運用上の注意点とコスト管理(スライディングウィンドウ手法)
チャット履歴をデータベースで管理するようになると、新たな問題が発生します。それは「会話が長引くほど、APIに送信するデータ量(トークン数)が爆発的に増大する」ということです。
先生、もしかして…100回会話したら、101回目の質問のときには、過去100回分の文章を全部AIに送らなきゃいけないってことですか?
それって、APIの利用料金がとんでもないことになりませんか!?
その通りよ。生成AIのAPI課金は「送信した文字数(入力トークン)+ 生成された文字数(出力トークン)」で決まるから、履歴を全部送るとコストが雪だるま式に増えるわ。
さらに、モデルごとの「最大入力トークン数」の上限を超えるとエラーで落ちてしまうの。
プロの現場では、このコストと上限の壁を突破するために「スライディングウィンドウ(Sliding Window)」という手法を用います。
スライディングウィンドウとは?
「直近のN往復分だけの履歴」を切り取って送信し、古い履歴は切り捨てる手法です。
先ほどの chat_manager.php で実装した LIMIT 10 がまさにこれに該当します。直近10件(5往復分)の会話を保持すれば、人間の一般的な対話においては十分な文脈を維持できます。
より高度な手法として、古い会話履歴を定期的にAI自身に要約(Summarize)させ、短いテキストに圧縮してDBに保存し直すテクニックも存在します。
総まとめ:記憶の管理こそがAIアプリのコア技術
第3回の解説、お疲れ様でした。
今回は、AlmaLinux 9上で稼働するPHPとMariaDBを駆使し、ステートレスなAI APIに「記憶」を実装しました。セッションIDによる会話の分離、データベースへの時系列保存、そしてプロンプトへの動的結合という一連の流れは、ChatGPTのようなWebサービスを自作する際の根幹となる技術です。
次回、【第4回】外部データの活用:RAG(検索拡張生成)入門 では、AIが学習していない社内文書や最新ニュースのPDFを読み込ませ、AIに「専門知識」を付与する画期的な技術に挑戦します。AlmaLinuxのファイルシステムとデータベースを活用した検索システムが登場しますよ!
お楽しみに!LINUX工房の「リナックス先生」でした。
▼ AI×DB連携に強いサーバー環境を ▼
MariaDBの高速処理に最適!
「SSD搭載の高性能VPS」
LAMP環境×AIのスキルを評価
「IT専門の転職エージェント」

コメント