個々のプログラム名を覚えるのではなく、利用者の操作がどの層を通り、どこにデータが保存され、どの境界で安全性や失敗が管理されるのかを追えるように整理しています。
- 画面、API、サービス層、PostgreSQL・Redis、外部LLMまでの処理の流れ
- 認証、所有者確認、生成UI、AIエージェント、MCPの安全境界
- 容量見積もり、SLO、障害対応、10倍・100倍スケール、面接の深掘り対策
第1〜25章は現在の実装と設計、第26〜42章はシステムデザイン面接と大規模化の考え方です。前半で仕組みを理解してから、後半で容量・信頼性・セキュリティ・トレードオフへ進む構成です。
- Q1. なぜセッションをCookieだけに保存しないのか
- Q2. Redisが落ちたら全機能を止めるのか
- Q3. LLMの回答が二重に保存されない保証はどこにあるか
- Q4. SSEが切断したら回答を失わないか
- Q5. 6,900件の同時生成をどう処理するか
- Q6. なぜチャット履歴を全部LLMへ渡さないのか
- Q7. 他の利用者のメモをどう防ぐか
- Q8. Prompt Injectionを完全に防げるか
- Q9. URL取得のSSRFをどう防ぐか
- Q10. 画像をデータベースへ保存しない理由は何か
- Q11. いいねと閲覧数は同じ一貫性でよいか
- Q12. MCPは通常のWeb APIと何が違うか
- Q13. データベースのマイグレーション中に旧環境が動いていたらどうするか
- Q14. 最も大きい現在の弱点は何か
- Q15. 何を最初に測るか
- Q16. AIが生成したUIをそのまま実行して安全なのか
1. この文書の目的
この文書は、Chat-Coreを初めてシステム設計の観点から読む人に向けた全体説明です。個々のプログラムの名前を覚えることではなく、利用者の操作がどの層を通り、どこにデータが保存され、どの境界で安全性や失敗が管理されるのかを理解することを目的にしています。
図は外部画像に頼らず、ページ内で完結する形で示しています。本文と図を合わせて読むと、「画面」「API」「業務処理」「データ保存」「外部サービス」がどのように協力しているかを追えます。
2. Chat-Coreは何をするシステムか
Chat-Coreは、複数の大規模言語モデルを使って会話するサービスを中心に、会話を整理し、再利用し、他の利用者や外部のAIサービスと安全に連携するための機能をまとめたWebアプリケーションです。利用者から見える主な価値は、次の四つの方向に整理できます。
- AIとリアルタイムに会話できる。
- 会話を話題ごとの部屋に分け、通常保存または一時利用できる。
- よく使う指示をタスクとして保存し、繰り返し利用できる。
- 部屋をプロジェクトにまとめ、プロジェクト固有の指示を会話へ反映できる。
- AIの回答をメモとして保存し、検索、整理、共有できる。
- 自分の好み、経歴、決定事項などをパーソナル・コンテキストとして管理できる。
- 公開プロンプトやSkillを探し、自分でも投稿、評価、コメント、再利用できる。
- AIの回答の中に、隔離された環境で動く簡単な図解やシミュレーションなどのインタラクティブな表示(生成UI)を差し込める。
- 画面の使い方を尋ねたり、確認付きで画面操作を補助したりするAIエージェントを使える。
- MCPに対応した外部AIサービスから、許可した範囲で公開コンテンツ、メモ、パーソナル・コンテキストを利用できる。
つまり、単なるチャット画面ではありません。会話を中心に、知識の保存、再利用、共有、外部AIとの接続までを一つの利用者単位で扱う「個人向けAIワークスペース」です。
3. まず覚えるべきシステムの部品
システム設計では、処理を役割ごとに分けて考えます。Chat-Coreを大きく分けると、次の部品があります。
| 部品 | 初心者向けの意味 | Chat-Coreでの役割 |
|---|---|---|
| ブラウザ画面 | 利用者が見る場所 | 会話入力、一覧表示、モーダル、設定、ストリーミング表示を担当する |
| フロントエンド | ブラウザで動く画面の集合 | Next.jsとReactでページ、部品、状態、翻訳、API通信を構成する |
| バックエンド | サーバー側の本体 | FastAPIで認証、会話、メモ、共有、MCPなどのAPIを提供する |
| API | 画面とサーバーの約束 | JSONによる通常通信と、SSEによるAI回答の逐次配信を扱う |
| サービス層 | 業務ルールをまとめる場所 | 認証確認、クォータ、生成処理、検索、共有、候補抽出などを担当する |
| リポジトリ層 | データベースへの窓口 | 読み書き、所有者確認、検索、トランザクションを担当する |
| PostgreSQL | 永続データを保存する本体 | ユーザー、会話、メモ、プロンプト、OAuth情報などを保存する |
| Redis | 高速な共有状態の置き場 | セッション、キャッシュ、クォータ、ロック、生成イベントを扱う |
| 外部サービス | Chat-Coreの外にある専門機能 | LLM、メール、Google認証、Web検索、MCPクライアントなど |
| リバースプロキシ | 外部からの入口 | HTTPS、転送先の切り替え、フロントエンドとバックエンドの振り分けを担う |
ブラウザからデータベースへ直接アクセスしないことです。ブラウザはAPIだけを呼び、バックエンドが認証、入力検証、所有者確認、業務ルールを適用してからデータへアクセスします。
4. 全体構成
利用者のブラウザ
│
│ HTTPS、Cookie、JSON、SSE
▼
公開入口(リバースプロキシ)
├──────────────────────┐
│ │
▼ ▼
フロントエンド バックエンド
(Next.js / React) (FastAPI)
│ │
│ 画面用のAPI呼び出し ├──────────────┐
│ │ │
│ ▼ ▼
│ PostgreSQL Redis
│ 永続データ セッション・高速状態
│
└─────────────── 利用者へ画面を返す
バックエンド ──→ LLM提供者、メール送信、Google認証、Web検索
バックエンド ──→ 添付画像の保存領域
外部AIクライアント ──→ MCP認証 ──→ MCPの公開コンテンツ・メモ・コンテキスト機能図1 ── ブラウザから外部サービスまでの全体構成。入口は一つ、保存先は用途別に分かれる。
通常の画面表示はフロントエンドが担当します。一方、ログイン状態、ユーザー所有データ、AI生成、公開範囲などの判断はバックエンドが担当します。
この境界を守ることで、ブラウザの表示を改変されても、サーバー側の認可を通らないデータ操作は成立しません。
5. 1回のリクエストが通る道
利用者が「メッセージを送信する」場合を例にすると、処理は次の順番で進みます。
- 入力利用者がメッセージを入力する。
- 送信ブラウザが入力を整え、APIへ送る。
- セッション復元セッションを復元し、言語やリクエスト情報を準備する。
- CSRF検査状態を変える操作ならCSRFを検査する。
- 入力検証入力形式、長さ、添付内容を検証する。
- 認可ログイン状態、部屋の所有者、利用上限を確認する。
- 文脈の収集必要な履歴、要約、タスク、プロジェクト指示、記憶を集める。
- 生成開始LLM生成ジョブを開始し、進行イベントを共有する。
- 逐次表示ブラウザがSSEで回答を少しずつ表示する。
- 保存と確定完了した回答と関連情報を保存し、最終状態を返す。
入力を検証する前にLLMを呼ぶと、巨大な入力や不正な添付ファイルで外部費用やメモリを消費する可能性があります。所有者を確認する前に部屋の履歴を読むと、他人の会話が漏れる可能性があります。したがって、安価で安全な検査を先に行い、外部サービスやデータベースへの処理を後ろに置きます。
5.1 画面の読み込み
フロントエンドは、ページの枠、共通ナビゲーション、翻訳、テーマ、ネットワーク状態、エラー境界を初期化します。ページごとの部品は、会話、メモ、プロンプト共有、設定などの領域に分かれています。
一覧データの取得にはキャッシュと再検証の仕組みが使われます。画面が再表示されたときに毎回すべてを取り直すのではなく、直近のデータを使いながら必要な部分だけ更新します。
5.2 API通信
通常の読み書きはJSONで行います。状態を変更する通信にはCSRFトークンを付けます。ネットワーク切り替えや一時的な失敗には、タイムアウト、再試行、エラーの標準化を行う共通通信層が対応します。
バックエンドから返るデータの形は、バックエンド側のPydanticモデルを中心に管理します。そのモデルからフロントエンドのZodスキーマを生成するため、両方で同じデータ構造を手書きする必要がありません。これにより、項目の追加や必須・任意の変更を、型検査と実行時検証の両方で見つけやすくなります。
5.3 非同期処理とブロッキング処理
FastAPIは非同期処理を中心に動きます。ただし、LLM SDK、メール送信、画像変換、文書の文字抽出など、待ち時間のある同期処理も存在します。それらをイベントループ上で直接実行すると、他の利用者の通信まで止まります。そのため、ブロッキング処理は専用の実行枠へ移し、同時実行数を制御します。
現在の「専用の実行枠」は、外部の分散ジョブキューではなく、バックエンドのプロセス内に用意した固定サイズのスレッドプールです。既定のワーカー数は運用設定で調整でき、生成ジョブの状態や進行イベント自体はRedisで複数ワーカー間に共有しますが、実際に処理を実行する仕組みはプロセスの外へ出ていません。そのため、単一ホストのCPUとスレッド数が実行の上限になります。第30章で述べる大規模化では、この実行枠を独立した生成ワーカー群へ切り出すことが最初の分離ポイントになります。
6. バックエンドの責務分割
バックエンドは、入口と業務処理と保存処理を分けています。
- ルート層入力を受ける、認証を要求する、応答を整える。
- サービス層業務ルール、クォータ、外部サービス、複数処理の調整。
- リポジトリ層SQL、ORM、トランザクション、所有者条件、検索。
- データストアPostgreSQL / Redis / ファイル保存領域。
ルート層にすべての処理を書くと、一つの関数が長くなり、同じ認証確認やエラー処理が機能ごとに複製されます。サービス層へ業務ルールを寄せ、リポジトリ層へデータアクセスを寄せることで、同じルールをWeb画面とMCPの両方から再利用できます。
エラーは、次の八種類を区別します。
- 入力不正
- 未認証
- 権限不足
- 対象なし
- 利用上限
- 競合
- 外部サービス失敗
- 予期しない内部障害
利用者に見せるメッセージと、ログに残す詳細を分けることで、原因調査に必要な情報を保持しつつ、内部情報を外へ漏らしません。
7. フロントエンドの設計
フロントエンドは、ページ、表示部品、画面状態、API・ブラウザ共通処理の四つに分けて考えると理解しやすくなります。
| 層 | 役割 | 例 |
|---|---|---|
| ページ | URLに対応する画面の入口 | チャット、ログイン、メモ、設定、共有ページ、管理画面 |
| コンポーネント | 見た目と局所的な操作 | メッセージ一覧、入力欄、モーダル、カード、サイドバー |
| フック・コンテキスト | 画面をまたぐ状態や複雑な状態遷移 | チャット生成、部屋、タスク、プロジェクト、テーマ、言語 |
| ライブラリ・共通スクリプト | 通信、SSE解析、再試行、サニタイズ、ブラウザ操作 | API取得、CSRF、Markdown表示、認証状態、ストレージ |
状態の分割と描画量の制御
チャット画面は特に状態が多いため、入力中、生成中、停止、再接続、再生成、分岐切り替え、モーダル表示などを複数の状態管理へ分割しています。これにより、一つの巨大な画面部品がすべてを管理する構造を避けています。
メッセージが多い部屋では仮想スクロールを使い、画面に必要な範囲だけを描画します。古い履歴は追加読み込みにし、ブラウザへ戻った直後はローカルの一時表示を使ってからサーバーの正式な履歴へ合わせます。これにより、長い会話でも初期表示の遅さとブラウザのメモリ使用量を抑えます。
受け取ったものを信頼しない描画
表示するMarkdownや外部由来の文章は、そのままHTMLとして挿入しません。安全なHTMLへ変換し、危険な要素を除去します。SSEの内容も受信したものを無条件に信頼せず、期待するイベント種別とデータ形を確認してから画面へ反映します。
言語とテーマ
日本語と英語の翻訳カタログを持ち、利用者の言語設定とリクエストの言語情報を使って表示を切り替えます。テーマはライト、ダーク、システム連動を選べ、端末のブラウザ側に保存されるため、端末ごとに設定が異なる場合があります。
サーバー側で先に組み立てる画面
サーバー側で先に内容を組み立てる画面もあります。共有チャット、共有プロンプトの個別ページ、共有メモ、公開プロンプトの一覧・検索ページの4系統は、検索エンジンやSNSのプレビューでも内容を見せやすくするため、バックエンドから取得したデータを安全なHTMLへ変換して初期表示に含めます。あわせて、サイトマップとrobots情報を返す入口も、リクエストのたびにサーバー側で内容を組み立てて返します。ログイン後の操作画面は、ブラウザ側でAPIを呼びながら状態を更新します。
キャッシュと再検証
サーバー由来の一覧は、SWRによるキャッシュと再検証で管理します。前回のデータを表示したまま裏側で更新し、ウィンドウへ戻ったときやネットワーク復帰時に再取得します。テーマ、言語、最後に見ていた部屋、選択中のモデルなどは、表示を速くする目的でブラウザ側に保存されるものがあります。
同じ読み取りの重複は抑える一方、権限判定はブラウザのキャッシュを信頼せず、必ずバックエンドへ確認します。
画面の一覧
画面群には、チャット、認証、設定、メモ、公開プロンプト、共有コンテンツ、管理画面、OAuth同意画面、ヘルプ、規約、プライバシー案内、各機能の紹介ページがあります。運用確認や検索エンジン向けの情報を返す入口もあります。通知設定の画面枠はありますが、現状は将来提供予定の案内が中心です。
8. 認証、セッション、セキュリティ
8.1 ログイン方法
パスワードを常用しない設計で、主に次の三つの方法を使います。
メールアドレスへ6桁の確認コードを送る方式。
Google OAuthによる外部認証。
WebAuthnを使ったPasskey認証。指紋、顔認証、端末のロック解除などを利用する。
メール確認コードは短時間だけ有効で、試行回数にも上限があります。メールアドレス単位や接続元単位の送信・試行制限も設け、コードの総当たりや大量送信を抑えます。認証に成功すると、未ログイン時に作成したゲスト用プロンプトを本人のアカウントへ引き継ぐ処理も行われます。
8.2 セッションの保存
署名された「セッションを探すための参照値」だけ。
ユーザーID、認証状態、確認コード、OAuth状態、Passkeyの一時情報など。
Cookieへ認証コードや管理者状態などの本体を入れず、セッション本体をRedisへ置きます。Cookieが読まれても、重要情報そのものがそのまま見えないようにするためです。認証成功時にはセッション参照値を再発行し、ログイン前のセッションを使い続ける攻撃を防ぎます。
通常のログイン状態は標準で30日間維持されます。Redisが利用できない場合に、機密情報をCookieへ退避することはしません。安全側に倒してセッションを保存せず、必要なら再認証を求めます。このため、Redisはログイン状態にとって重要な運用依存先です。
8.3 CSRFとCookie
ブラウザから状態を変更する操作には、セッションに紐づいたCSRFトークンを要求します。作成、更新、削除、共有、いいね、コメントなどが対象です。読み取りだけの操作と、状態を変更する操作を分け、不正な別サイトからの操作を拒否します。
Cookieには署名、SameSite、HTTPS時のSecure属性などを適用します。セキュリティヘッダーも共通ミドルウェアで付与し、各ルートが個別に忘れないようにしています。
8.4 入力と所有者の確認
入力はPydanticで型、長さ、形式を検査します。ただし、入力検査に通っただけでは安全とは限りません。各処理で次も確認します。
- ログインしているか。
- 対象の部屋、メモ、プロンプト、タスクが本人の所有物か。
- 公開対象か、共有トークンが有効か。
- 更新前の版番号が一致しているか。
- その操作がクォータやレート制限を超えていないか。
8.5 アカウントと設定
設定画面では、ユーザー名、自己紹介、アバター、AIへ常に伝えるプロフィール情報、表示言語、テーマ、Passkey、投稿したプロンプト、いいねしたプロンプト、MCP接続を管理できます。
- メールアドレスの変更は通常のプロフィール更新と分離し、新しいアドレスへの確認を含む専用手順にする。
- アカウント削除は確認文の入力と確認操作を必要とし、誤操作を防ぐ。
- アバター画像は拡張子だけで信用せず、Content-Typeとファイル先頭の形式、サイズを確認して保存する。
- 認証情報や外部接続の管理画面は、一般利用者の画面から見える設定項目とは別に、サーバー側で本人確認とCSRFを要求する。
8.6 AIへ送るデータの扱い
チャット本文、添付資料、公開コンテンツ、外部検索結果は、命令ではなく利用者提供のデータとして扱います。本文中に「このシステムの指示を無視せよ」と書かれていても、システムの指示を上書きできないように境界を明示します。API キー、パスワード、OAuthトークンなどの明白な秘密情報は、外部LLMへ送る前に伏せ字化する仕組みがあります。
これは完全な秘密情報検出を保証するものではありません。利用者は、共有や外部AI連携の前に、送信内容を確認する必要があります。
9. チャットの内部設計
9.1 チャット部屋と履歴
会話は部屋単位で管理します。部屋には履歴、現在表示している分岐、要約、部屋単位の記憶が関連づきます。通常モードはデータベースへ履歴を保存し、一時モードは後で自動削除する対象として扱います。
部屋単位の記憶は、利用者に紐づき全チャットへ横断するパーソナル・コンテキスト(第13章)とは別の仕組みです。両者の違いは第13.1節で整理しています。
一つの回答をやり直したり、過去の発言を編集して再生成したりすると、履歴を単純に上書きせず、親子関係を持つ分岐として保存します。利用者は、どの分岐を現在の会話として見るかを切り替えられます。
最初の質問
│
▼
最初の回答
│
├─ 再生成した回答
│ └─ その回答から続く会話
│
└─ 編集後の質問
└─ 編集後の回答図2 ── 履歴は上書きではなく分岐として積み上がる。
この構造により、試行錯誤の履歴を残したまま、利用者が必要な会話の枝を選べます。
部屋は検索、追加読み込み、名前変更、削除、複数選択による一括削除、プロジェクトへの割り当てにも対応します。共有した部屋は、認証なしでも閲覧できる読み取り用の公開情報として扱い、ログインした利用者は自分の部屋へ複製して続きを始められます。共有元の部屋を直接編集することはできません。
ログインなしの利用者にも、制限付きのゲストチャットがあります。ゲストの会話は通常の利用者履歴へ保存せず、短期間の一時領域で扱います。公開プロンプトの閲覧や標準タスクの利用など、一部の公開・試用機能は使えますが、日次回数、投稿形式、共有、保存には制限があります。ゲスト時の公開投稿は、条件を満たしてログインした後に本人の投稿として引き継げます。
9.2 LLMへ渡す文脈
一回の生成では、最新の入力だけを送るのではありません。状況に応じて、次の情報を優先順位と上限を考慮して組み立てます。
- システムとしての基本方針と安全規則。
- 利用者が設定したAI向けプロフィール情報。
- プロジェクトの指示。
- 選択したタスクのテンプレート、応答ルール、例。
- 部屋の要約と、直近の会話履歴。
- 部屋に保存された記憶。
- 必要に応じたパーソナル・コンテキスト。
- 添付資料、Web検索結果、画像検索結果などの参照情報。
- 最新の利用者メッセージ。
会話が長くなると、すべての履歴を無制限に送ることはできません。古い部分を要約し、直近の重要な発言を残し、モデルの入力上限内へ収めます。最新の質問の条件を失わないよう、単純に先頭から切り捨てるのではなく、残す部分を選びます。
9.3 生成ジョブとストリーミング
ブラウザ ── 生成開始 ──→ バックエンド
│
├─ 入力、所有者、クォータを確認
├─ 生成ジョブを登録
└─ LLMへ要求
│
▼
生成された断片
│
Redisでイベントと状態を共有
│
ブラウザ ←── SSEで断片を受信 ←── バックエンド
│
├─ 画面へ逐次表示
├─ 再接続時に状態と未受信イベントを確認
├─ 停止操作を送る
└─ 完了後に履歴を再取得して整合させる図3 ── 生成の開始・配信・再接続を分離した流れ。
生成処理をHTTPリクエストの中だけで待ち続けると、接続切断や複数ワーカーでの再接続に弱くなります。そこで、生成ジョブの状態とイベントを共有状態へ置き、開始、配信、状態確認、停止、再接続を分離しています。
フロントエンドはイベント番号で重複を除き、切断した場合は最後に受信した位置から再接続します。生成開始の応答が届かなかった場合でも、サーバー側にジョブが残っていないか確認して復帰を試みます。SSEが利用できない状況では、完了したJSON応答を処理する経路へ切り替えます。部屋を切り替えた後に古い生成結果が混ざらないよう、生成世代も確認します。
LLM提供者の一時的な失敗には限定的な再試行を行います。利用者が停止した場合は、外部生成とバックグラウンド処理を止め、途中の状態を不整合にしないようにします。サーバー終了時も実行中のジョブを安全に停止し、一定時間で待機を打ち切ります。
9.4 チャットの補助機能
- 部屋名を会話内容から補助的に生成する。
- 長い履歴を要約する。
- 会話から部屋の記憶を抽出する。
- Web検索、URL取得、検索結果の画像候補などを必要に応じて使う。
- 生成中の思考状態や構造化された表示部品(第9.6節の生成UIを含む)を、利用者に安全な形で表示する。
- 回答をコピーし、メモへ保存し、共有する。
検索結果に含まれる文章をシステム命令として実行せず、結果の長さや表示内容も制限します。
9.5 メッセージ添付
テキスト、Markdown、CSV、JSON、HTML、コード、設定、ログなどのテキスト資料と、PDF、Word、Excel、PowerPointなどの文書を扱えます。標準では一度に最大5件1件あたり1MBまでで、抽出後の本文にも上限があります。
文書はそのままLLMへ渡すのではなく、形式を検証し、安全に文字を抽出し、制御文字や過剰な空白を整えます。Office形式はZIP爆弾やパス走査を防ぎ、XMLを安全に解析し、PDFのページ数や表計算のセル数にも上限を設けます。抽出された本文は添付資料として履歴とプロンプトへ組み込みます。
9.6 生成UI(サンドボックスアーティファクト)
通常のMarkdown回答に加えて、AIはHTML・CSS・JavaScriptをひとまとまりにした「アーティファクト」を回答へ埋め込み、チャット画面の中に簡単な図形やシミュレーション、3D表示のようなインタラクティブな表示を差し込めます。ただし、これは通常のテキスト生成より多くの関門を経る、安全側に倒した専用の処理経路です。
- 必要性を判定するアーティファクトが必要かを別枠で判定する(無し/平面表示/立体表示)。
- 抽出する応答中の該当ブロックを抽出する。
- 正規化する表記ゆれ(項目名の違いなど)を吸収して標準形へ整える。
- 検証する決まったスキーマで検証する(通過しないものは破棄する)。
- 安全化するHTML・CSS・JavaScriptをそれぞれ個別に安全化する。
- 受け渡す検証済みの内容だけがメッセージの表示部品として渡る。
- 隔離実行するブラウザが隔離されたiframeの中だけで実行する。
利用者の入力ではなくAIが提案した表示なので、通常のPrompt Injection対策と同じ考え方をここでも適用します。「LLMが作った」という理由だけでは実行を許可せず、サーバー側の決まったスキーマを通過し、危険とみなせる記述を取り除いたものだけを、隔離された環境でのみ動かします。
生成の可否判定
通常の応答生成とは別に、軽量なLLM呼び出しで「アーティファクトが必要か、必要なら平面表示か立体表示か」を判定します。利用者の文章を直接それと解釈するのではなく、専用の判定用プロンプトと出力形式を使うことで、意図しない場面での誤生成を減らしています。
唯一の通過点となる検証
抽出した候補は、まず項目名の表記ゆれを吸収する下処理を経てから、決まったスキーマでの検証に通します。この検証を経ていないデータは、どのような経路であっても画面へは渡りません。検証では、種類・タイトル・高さ・本文の長さに上限を設けます。表示できるアーティファクトは1メッセージあたり最大3件、高さは一定の範囲(160〜900px相当)に丸められ、HTML・CSS・JavaScriptそれぞれと合計の文字数にも上限があり、超えた分は切り詰めます。
個別の安全化
HTML・CSS・JavaScriptは、それぞれ別の検査を通します。JavaScriptは危険とみなせる記述(外部通信の試行など)をコメントや文字列リテラルも踏まえて検出し、該当すれば拒否します。CSSやHTMLも、埋め込み表示として不適切な要素を取り除きます。
サンドボックス実行
ブラウザ側は、検証済みのHTML・CSS・JavaScriptを、隔離されたiframeの中だけで動かします。iframeには、外部通信・フォーム送信・入れ子のフレーム表示などを許可しないセキュリティポリシーを付け、スクリプトの実行だけを許可し、埋め込み元のページの情報へアクセスできない状態で動かします。これにより、アーティファクトの中のコードがブラウザ本体や他のタブの情報へアクセスすることを防ぎます。
ライブラリの扱い
アーティファクトが利用できる外部ライブラリは、あらかじめ許可した一種類(3D表示用のライブラリ)に限られます。外部のCDNから読み込むのではなく、Chat-Core自身が配信する静的ファイルから読み込みます。セキュリティポリシーが外部ホストへの接続そのものを遮断しているため、そもそもCDN読み込みは機能しません。これにより、外部ホストの改ざんや配信停止がアーティファクト表示に影響しない状態と、想定外のライブラリを勝手に読み込まれない状態を両立しています。
表示の統合
生成UIは、回答本文とは別の「メッセージの表示部品」として扱われ、Web検索の画像候補などほかの構造化された表示部品と同じ枠組みで管理されます。同じ返信の中で複数の種類の構造化表示が競合しないよう、表示できる組み合わせにも制約があります。
10. タスク、プロジェクト、再利用
同じ指示を毎回書き直さないための仕組みが二つあります。タスクは「よく使う指示のテンプレート」、プロジェクトは「複数の部屋をまとめ、共通の前提を与える箱」です。
10.1 タスク
タスクは、よく使うプロンプトを再利用するためのテンプレートです。次の要素を持ちます。
名前本文応答ルール出力形式入力例出力例
タスクを選ぶと入力欄へ内容を展開し、必要なら編集して送信できます。標準で用意されたタスクと、利用者が作成したタスクを同じ仕組みで扱います。利用者ごとに独立し、並び替え、編集、削除ができます。
編集や削除の履歴を版として残すため、後から変更の経緯を追いやすく、プロンプト共有から追加したタスクとの関係も保てます。
10.2 プロジェクト
プロジェクトは、複数のチャット部屋を一つの目的や案件にまとめる単位です。プロジェクトには名前と指示を設定でき、その指示が属する部屋のAI文脈へ追加されます。部屋をプロジェクトへ割り当てたり、外したり、プロジェクトを削除したりできます。
プロジェクトの指示は、利用者の全会話へ無条件に入るプロフィール情報とは違い、対象プロジェクトの部屋だけへ入ります。情報の範囲を狭めることで、別の話題へ不要な指示が混ざるのを防ぎます。
11. 公開プロンプトとSkillの共有
11.1 公開コンテンツの流れ
- 入力利用者が投稿内容を入力する。
- 検証形式、長さ、カテゴリ、メディア種別を検証する。
- 資料の処理画像やSkillの補助資料を安全に処理する。
- 保存公開範囲を確認して保存する。
- 反映一覧、検索、詳細、共有リンクへ反映する。
- 利用他の利用者が閲覧、いいね、コメント、タスク追加を行う。
AIへの指示文そのもの。
Markdownを中心とした、AI開発ツールなどで再利用する手順や定義を保存する形式。
どちらも公開投稿として扱える一方、表示形式、必須本文、補助資料の扱いが異なります。投稿にはタイトル、カテゴリ、説明、本文、入力例、出力例、確認したAIモデル、メディア種別などを持たせられます。SkillにはMarkdown本体や補助スクリプトなどの資源を追加できます。
AI補助機能でタイトルや説明を作ることもできますが、投稿前に利用者が内容を確認する設計です。
11.2 検索と利用
公開コンテンツは、カテゴリ、形式、メディア種別、キーワードなどで絞り込めます。タイトル、本文、説明、作者情報、SkillのMarkdownを検索対象にし、一覧では短い抜粋を返します。詳細画面では本文、例、資源、作者、評価、コメントを確認できます。
気に入ったプロンプトにはいいねを付け、設定画面から後で確認できます。自分のチャットで使うためのタスクとして追加することもできます。
共有リンクは、チャットやメモと同じく、リンクを知っている人が見られる公開情報として扱う必要があります。
11.3 コメントとモデレーション
コメントはログイン利用者に限定します。次のような投稿を制限します。
- 短時間の連続投稿
- 重複投稿
- URLを大量に含む投稿
コメントの削除は投稿者本人、対象プロンプトの投稿者、管理者に限り、通報が一定数に達したコメントは自動的に非表示にできます。
コメントを物理的にすぐ消すのではなく、削除・非表示・通報を分けて運用上の判断を残します。
11.4 ゲスト投稿
ログイン前の投稿を一時的な識別情報で管理し、後からログインした利用者へ引き継げる仕組みがあります。ゲストの識別情報はそのまま保存せず、ハッシュ化などで濫用防止に使います。引き継ぎ時には、本人が所有する投稿だけを対象にします。
11.5 画像添付の保存境界
プロンプト共有の画像は、利用者が送った原本をそのまま公開しません。
- アップロード利用者が画像を送信する。
- 検査完全デコード、形式確認、画素数確認、アニメーション拒否。
- 正規化EXIF向きの反映、メタデータ除去、サイズ縮小。
- 派生生成表示用WebPとカード用WebPを生成する。
- 保存原子書き込みで保存する。
- 公開判定データベースの添付情報から公開対象を決定する。
現在の保存先は単一ホストの永続ボリュームです。表示用とカード用の二つの派生画像を保存し、投稿のデータベース行が参照元になります。保存途中で失敗しても不完全な画像を残さないよう、原子書き込みと後片付けを行います。データベースから参照されなくなった孤立画像は、猶予時間を置いた定期処理で削除します。
この境界を独立させているため、将来オブジェクトストレージやCDNへ移行する場合も、画像の安全な変換処理や投稿APIを大きく変更せずに済みます。ただし、現在のローカル保存は複数ホストへそのまま拡張できないという制約があります。
12. メモ機能
メモは、AIの回答や利用者が残した文章を、会話履歴とは別に長く使うための保存場所です。次の操作ができます。
- タイトルと本文を保存する。
- コレクションで分類する。
- ピン留め、アーカイブ、並び替えを行う。
- 複数選択による一括操作を行う。
- Markdownとして表示し、コピーする。
- キーワード検索と意味検索を使う。
- AIにタイトル案を作らせ、失敗時は簡易な代替案へ切り替える。
- 共有リンクを作成し、期限や撤回を管理する。
- Markdown、JSON、CSV形式で書き出す。
二種類の検索を併用する
メモの検索では、文字列検索用の索引と、文章の意味を数値化した埋め込みを併用します。短い完全一致には通常検索が向き、表現が違っても意味が近い文章にはベクトル検索が向きます。埋め込みを作れない場合でも、メモの保存そのものが失敗しないよう、補助情報と本体を分けています。
版番号による衝突防止
メモには版番号があります。編集や追記では、利用者が直前に読んだ版番号を送ります。別の画面やMCP接続が先に編集していれば版番号が一致せず、上書きせずに再読込を求めます。これは「最後に保存した人が無条件に勝つ」問題を避けるための仕組みです。
共有中のメモを更新すると公開内容も変わるため、通常の編集より慎重に扱います。共有トークンは撤回可能で、期限切れも判定します。
書き出しの扱い
メモの書き出しは、保存された内容を別の場所で扱うための機能です。書き出した内容は共有リンクとは別物で、ファイルを作っただけで他の利用者へ公開されることはありません。
本文には個人的な情報が含まれる可能性があるため、書き出し先の保管場所は利用者が管理する必要があります。
13. パーソナル・コンテキスト
パーソナル・コンテキストは、メモとは別の機能です。メモが文章を保管する場所なのに対し、パーソナル・コンテキストは「長期間使う小さな事実」を管理します。
例として、次の種類があります。
- 利用者の好み。
- 経歴やプロフィール。
- プロジェクトの背景。
- 過去の決定事項。
- 参考資料や再利用したい前提。
一つの事実には、種類タイトル本文重要度状態出典改訂番号があります。有効な事実と無効化した事実を分け、不要になったときも完全削除ではなく無効化して履歴を残します。重要度が高い事実ほど、外部AIへ渡す限られたダイジェストの中で優先されます。
13.1 「部屋の記憶」との違い
第9.1節で触れた「部屋の記憶」と、この章のパーソナル・コンテキストは、どちらも「会話から抽出した長期情報」という点で似ていますが、仕組みも扱いも別物です。混同しやすいため、違いを整理します。
| 観点 | 部屋の記憶 | パーソナル・コンテキスト |
|---|---|---|
| 保存範囲 | その部屋だけに紐づく | 利用者に紐づき、全チャットへ横断的に使われる |
| 抽出のタイミング | 通常のメッセージのやり取りの中で、都度自動的に抽出・保存される | 利用者が明示的に有効化した場合のみ、チャット完了後に非同期で候補を作る |
| 承認の要否 | 承認なしでそのまま保存される | 承認するまでは正式な事実にならない(次節) |
| データの形 | 短い一文の事実 | 種類・タイトル・本文・重要度・状態・出典・改訂番号を持つ構造化データ |
| 主な用途 | その部屋の会話を自然に続けるための短期的な補助 | 利用者の好みや経歴など、複数の部屋・複数の外部AI連携で再利用する長期的な情報 |
部屋の記憶は「この部屋の会話を続けやすくするための軽い記録」であり、承認フローを持ちません。一方でパーソナル・コンテキストは「利用者本人が内容を確認し、必要なら編集してから正式採用する、より重い記録」です。同じ「AIが会話から自動的に覚える仕組み」という説明で両者を一括りにすると、承認の要否や共有範囲を取り違えるおそれがあるため、機能を選ぶ・説明する際はこの区別を意識してください。
13.2 チャットからの候補抽出
利用者が明示的に有効化した場合、通常チャットの後で、長期的に役立ちそうな利用者自身の文脈を非同期に抽出します。
- 通常チャット完了やり取りが終わる。
- バックグラウンド抽出候補抽出を裏側で実行する。
- 秘密情報の除外明らかな秘密情報を除外する。
- 重複確認既存の事実や候補と文字列類似検索で重複を確認する。
- 提示確認待ち候補として表示する。
- 判断利用者が承認、編集して承認、または却下する。
- 確定承認されたものだけ正式な事実になる。
候補は自動的に正式保存されません。未承認の候補は検索や外部AIへのダイジェストに返しません。自動抽出が初期状態で無効なのは、利用者が意図しない記憶を作らないためです。
13.3 エクスポートとインポート
有効・無効化済みを含む事実をJSONまたはMarkdownで書き出せます。読み込みは、まず件数、重複、状態をプレビューし、利用者が確定した後に追加します。既存データは変更せず、同じ内容は重複として省略します。途中で上限を超えたり不正なデータがあったりした場合は、一部だけを保存せず全体を取り消します。
上限は次のとおりです。
事実:一人あたり最大200件本文:1件あたり最大2,000文字
読み書きしたファイルには個人的な情報が平文で含まれるため、安全な場所で保管する必要があります。パーソナル・コンテキストには通常の共有リンクを作らず、外部AIへ渡す場合も明示的な権限が必要です。
14. 画面操作を補助するAIエージェント
14.1 何をする機能か
AIエージェントは、通常のチャットとは別に、現在のページの説明と操作補助を行う機能です。画面左下の起動ボタンから呼び出すドラッグ可能なミニチャットとして提供され、チャット本編、プロンプト共有、メモ、設定などのページで使えます。
ログイン・登録・OAuth同意などの認証系ページでは、誤操作や認証情報の混乱を避けるため起動ボタン自体を表示しません。
エージェントは「現在のページ」と「そのページで許可された操作の一覧(カタログ)」だけを参照し、カタログに無い操作は提案できません。利用者の依頼に対して、説明文だけを返す場合と、段階的な操作計画(アクションプラン)を返す場合があり、後者は利用者が中身を確認して実行ボタンを押すまで、実際の画面操作を一切行いません。
利用者の依頼
↓
現在ページと利用可能な操作のカタログを確認
↓
依頼の種類を判定(説明/検索/画面操作)
↓
AIが回答または操作計画を作る
↓
利用者が計画の各ステップを確認
├─ 読み取り・移動中心の操作 → そのまま実行
└─ 保存・送信・削除など取り消しにくい操作 → 追加確認モーダル後に実行
↓
実行後、画面側の状態を検証
└─ 想定外の結果なら再計画を要求図4 ── 依頼から実行までの流れ。リスクの高い操作だけ関門が一つ増える。
14.2 依頼の種類を最初に振り分ける
エージェントは一つの汎用チャットではなく、依頼をまず次の4種類に振り分けてから応答を作ります。
| 種別 | 内容 | 主な参照先 |
|---|---|---|
| action | 画面上の操作を代行してほしい依頼 | ページの操作カタログ、現在ページのUI構造 |
| page_info | このページの使い方・機能を知りたい依頼 | 利用マニュアルの検索(RAG) |
| search | 実装や仕様について具体的に知りたい依頼 | ソースコード検索(RAG) |
| direct | 上記に当てはまらない一般的な質問 | 通常の回答生成のみ |
振り分け自体も軽量なLLM呼び出しで行い、通常会話で利用者が選んだモデルには依存させません。補助タスクの挙動を、利用者のモデル選択によって不安定にしないためです。
page_info と search で参照するマニュアルやコードの検索結果は、チャット本編の外部検索結果と同じく未信頼データとして扱い、区切りマーカーを付けてシステム指示と分離した状態でLLMへ渡します。これは第8.6節・第9.4節で述べたPrompt Injection対策と同じ考え方です。
14.3 ページと操作のカタログ
エージェントが「何ができるか」を判断する材料は、次の二層のカタログです。
- ページ定義: URLのパターンごとに、そのページの機能概要と、要素をクリックする・フォーカスするといった旧来のCSSセレクタ操作を列挙したもの。
- 型付き操作API(ツール): ページ遷移、入力欄への下書き入力、メッセージ送信、メモ保存のように、コマンド名・引数・検証方法・リスクレベル(low/medium/high)を持つ、より安全な操作の一覧。
このカタログはAPI呼び出しのたびに文章化されてシステムプロンプトへ埋め込まれ、現在のページには目印が付きます。加えて、現在ページのフロントエンド実装の冒頭部分もあわせてLLMへ渡し、実際にどのような入力欄やボタンが存在するかを踏まえて計画を作れるようにしています。
カタログに存在しないコマンドやページ外への遷移は、後述のとおりバックエンドとフロントエンドの双方で拒否されるため、LLMがカタログの外を「創作」しても実行段階で弾かれます。
14.4 操作計画(アクションプラン)のデータ構造
画面操作の依頼に対して、LLMは自由文ではなく決まった形のJSONのみを返すよう指示されます。
{
"description": "設定画面を開いて表示言語を日本語に変更します",
"steps": [
{ "action": "navigate", "path": "/settings", "risk": "low",
"description": "設定ページへ移動" },
{ "action": "app_action", "command": "settings.setLanguage",
"args": { "language": "ja" }, "risk": "medium",
"description": "表示言語を日本語に設定" }
]
}ステップの action には次の種類があります。
app_action— 型付きコマンド実行。click/input/focus/scroll/select/check— CSSセレクタに対する低レベルDOM操作。navigate— 許可されたページへの遷移。wait— 0〜5000msにクランプされる待機。
応答はまずJSONとしてパースを試み、旧テキスト形式で返ってきた場合のフォールバック解析も備えています。各ステップは受け取った直後に検証・正規化され、許可リストにないコマンドや、// で始まる・制御文字を含む・スキーム付きURLのような不正な遷移先はこの時点で除去されます。
メモの編集だけは別の簡易スキーマを使います。memo_edit という単一ステップに、編集後の本文全体と任意のタイトルだけを持たせ、リスクレベルは常に低リスクを機械的に付与しつつ、UI側では常に確認を挟みます。これは、メモ編集がCSSセレクタの積み重ねではなく「本文を書き換える」という一つの意味のある操作だからです。
14.5 フロントエンドでの受信・確認・実行
バックエンドの応答はSSEで届き、イベント種別は次の4種類です。
処理中の状態操作計画が完成した合図最終回答の確定エラー
ミニチャットは操作計画を受け取ると、メッセージの下にステップ一覧と実行ボタンを表示します。各ステップは行をクリックすると、実行されるコマンドやパラメータ、対象のUI要素などの詳細を開閉できるため、利用者は「何が行われるか」をブラックボックスのまま実行することを避けられます。
実行ボタンが押されると、実行エンジンがステップを順番に処理します。1ステップごとに、次の一連のライフサイクルを経ます。
- ページ遷移と準備待ち必要ならページを移動し、準備が整うまで待つ。
- 確認モーダル必要ならリスクに応じた確認を挟む。
- 実際の操作型付きコマンドの呼び出し、またはDOM操作。
- 状態検証実行後の状態を検証する。
ページ遷移をまたぐ操作列は、ブラウザのセッションストレージへ一時保存し、新しいページの準備が整ったことを確認してから残りのステップを再開します。
14.6 危険な操作への追加確認
すべてのステップが同列に扱われるわけではありません。次のいずれかに該当するステップは、実行前に必ず確認モーダルを挟みます。
- リスクレベルが中または高のステップ(例: メッセージ送信、メモ保存など状態を変更する操作)。
- CSSセレクタに対する生のクリック操作(型を持たない操作は一律に確認対象とする)。
- メモの本文書き換え(常に確認する)。
- 型付きコマンドのうち、あらかじめ「非破壊」と分かっているコマンド(ページ内移動、入力欄への下書き入力、検索など)以外すべて。
つまり、確認を省略できるのは「読むだけ・下書きするだけ・移動するだけ」の操作に限られ、実際に送信・保存・削除といった取り消しにくい操作は、たとえLLMが低リスク相当の説明を添えていても機械的に確認を強制されます。リスク判定はバックエンドのカタログ由来の値とLLM応答内の値のうち、より安全側(強い方)を採用します。
14.7 実行結果の検証と誤動作の防止
一つのチェックに頼らず、バックエンドとフロントエンドの双方で多重に防御しています。
| 層 | 防いでいること | 方法 |
|---|---|---|
| バックエンド | カタログにないページ・操作の提案 | 遷移先とコマンドをホワイトリストと照合し、該当しなければステップごと除去 |
| バックエンド | 不正な遷移先(//開始、制御文字、スキーム付きURLなど) | 遷移先文字列の形式検証 |
| フロントエンド | バックエンドの検証をすり抜けた遷移 | フロント側にも独立した許可リストを持ち、実行直前に再検証 |
| フロントエンド | エージェント自身のモーダル内を操作対象にすること | 自分のUI要素を操作対象の探索から除外 |
| フロントエンド | 想定外のログイン・登録画面への遷移 | リダイレクトを検知した時点で実行を止め、利用者へ理由を提示 |
| フロントエンド | パスワードや隠しフィールドなど機微な値の送出 | 画面のDOM情報をLLMへ渡す際、機微とみなす入力の値は収集しない |
| フロントエンド | 実行しても反映されていない操作 | 各ステップ実行後にDOMの状態(入力値、チェック状態など)を確認し、一致しなければ最新のページ状態を取り直して再計画を要求 |
この設計により、LLMの出力そのものを最終的な権限とはせず、「LLMが提案し、カタログとホワイトリストが許可した範囲だけを、利用者が確認して実行する」という三重の関門を経て初めて画面が変化します。これは第33.3節で述べたPrompt Injection対策の考え方(重要操作はLLMの文章ではなくサーバー側の認可で決める)を、画面操作という別の文脈へ適用したものです。
14.8 使用するLLMと通常チャットとの違い
エージェントの応答生成も、通常のチャットと同じLLM抽象化層(モデル名からプロバイダを自動振り分けする共通の仕組み)を土台にしていますが、上位の設計は通常チャットと明確に異なります。
| 観点 | 通常チャット | AIエージェント |
|---|---|---|
| 使用モデル | 利用者が選択したモデル | 補助タスク用に固定されたモデル(利用者の選択に依存しない) |
| 応答の粒度 | トークン単位で逐次配信 | フェーズ単位(進行状況→操作計画→最終回答)でまとめて配信 |
| 実行方式 | 生成ジョブを登録し、バックグラウンドで実行 | リクエストの中でブロッキング処理を専用の実行枠へ逃がして実行 |
| 会話の保存 | 部屋の履歴としてデータベースへ保存 | サーバー側では保存しない(次節) |
補助タスク(依頼の種類判定、メモ編集意図の判定など)に軽量なモデルを固定で使うのは、利用者がどのモデルを選んでいても、画面操作の判定という裏方の処理品質を安定させるためです。第37章で述べた「軽量モデルを分類・要約へ、高品質モデルを複雑な回答へ」という費用対効果の考え方がここにも表れています。
14.9 会話履歴とセッションの性質
バックエンドはエージェントの会話をデータベースへ永続化しません。リクエストのたびに、ブラウザが保持している直近のやり取り(件数・文字数に上限あり)を送り、サーバーはそれを一時的な入力として扱うだけのステートレスな設計です。継続性は主にブラウザ側で演出しています。
会話はブラウザのセッションストレージに一定時間だけ保持され、ページを移動してもエージェントのウィンドウを開き直せば会話が続いているように見える。
対象メモごとに会話を保存せず、開くたびに新規会話として扱う。個々のメモ編集の文脈が別のメモへ混ざらないようにするための区別。
14.10 レート制限と月間クォータ
画面操作を伴うため、通常チャット以上に濫用時の影響(誤った大量操作、外部費用の消費)を抑える必要があります。接続元IPアドレス単位、ログイン利用者またはゲストセッション単位の短時間レート制限に加え、全利用者合算の月間クォータを設け、上限に達すると再試行までの待ち時間とともに拒否します。具体的な代表値は第24.1節の上限一覧にまとめています。
15. MCP連携
15.1 MCPとは
AIクライアントが外部サービスの機能を共通形式で呼び出すための仕組みです。Chat-CoreはMCPサーバーとして、他のAIサービスへ許可されたデータと操作を提供します。
MCPはブラウザのログインCookieをそのまま使う仕組みではありません。外部AIクライアントはOAuthで認証し、利用者の同意に基づくアクセストークンを使います。
15.2 認証と同意
- 発見・登録外部AIクライアントが接続先を発見し、クライアント登録を行う。
- ログイン利用者のブラウザでChat-Coreへログインする。
- 同意要求された権限を同意画面で確認する。
- 認可コード発行一度だけ使える認可コードを発行する。
- トークン発行アクセストークンと更新トークンを発行する。
- 毎回の検証MCPツール呼び出しごとに権限と利用者を確認する。
権限は、公開コンテンツの読み取り、公開投稿、非公開メモの読み取り、メモの編集、パーソナル・コンテキストの読み取り、保存・編集・無効化などに分かれています。
既存の接続へ新しい権限を自動追加せず、再同意を必要とします。設定画面から接続を確認し、いつでも解除できます。
認可コードは短命で一度だけ使い、アクセストークンや更新トークンは生の値をそのままデータベースへ保存しない設計です。OAuthの秘密情報は暗号化やダイジェスト化を使い、鍵の世代交代にも対応できる構造になっています。外部クライアントの登録、リダイレクト先、接続先ホスト、ブラウザOriginも検証します。
15.3 提供する機能
MCPから使える機能は、次の三つの領域に分かれます。
| 領域 | できること | 重要な制約 |
|---|---|---|
| 公開コンテンツ | 公開プロンプトやSkillの一覧、検索、詳細取得、カテゴリ取得、投稿 | 投稿は直ちに公開される。画像添付や下書きは対象外 |
| メモ | 一覧、検索、詳細取得、コレクション取得、作成、編集、追記 | 更新には直前の版番号が必要。共有中の変更は追加確認が必要 |
| パーソナル・コンテキスト | ダイジェスト取得、検索、保存、編集、無効化 | 明示したスコープが必要。候補は対象外。最大件数と本文長の制限がある |
メモ本文は一度に扱える文字数を抑え、長い本文は分割して取得できます。コンテキストのダイジェストは重要度と文字数予算を考慮して返し、全件数、返却件数、省略件数、切り詰めの有無を示します。
更新操作では、Web画面と別のMCP接続が先に変更していないかを版番号で確認します。保存操作には再試行時の重複を防ぐための冪等キーを使えるものがあります。
15.4 MCPの安全性
MCPから返す公開コンテンツ、Skill、メモ本文は未信頼データです。外部AIクライアントは、その本文の指示をシステム命令として実行してはいけません。Chat-Core側でも、ツールごとのスコープ、利用者、所有権、レート制限、書き込み対象を毎回確認します。
登録、認可、投稿、読み取り、書き込み、意味検索にはそれぞれ制限があります。これにより、一つの接続や利用者が大量の登録、公開投稿、検索、編集を行ってサービスを圧迫することを防ぎます。
15.5 管理者機能
管理画面は一般利用者の認証とは別の管理者境界を持ちます。管理者ログインには専用のパスワードハッシュ照合と接続元単位の試行制限を適用し、成功時には管理者用セッションを作ります。管理者として認証されていない利用者は、管理画面や管理用APIを利用できません。
管理者はダッシュボードでシステムのデータ状態を確認し、許可された範囲でテーブル一覧、列情報、データのプレビューを確認できます。管理用のテーブル作成・削除、列の追加・削除といった操作もありますが、入力された識別子、型、制約、既定値を厳格に検証し、複数SQL文や未対応の定義を拒否します。
これは便利な運用機能である一方、データベースを直接変更できる強い権限なので、通常の機能追加や日常操作に使うものではありません。
管理画面が持つ機能は、現状ではデータベースのテーブル・列を確認・操作するダッシュボードに限られます。利用者管理専用の画面、投稿のモデレーション専用画面、利用状況の統計画面はまだありません。ただし、管理者かどうかを示すフラグ自体は管理画面の外でも参照されており、たとえば公開プロンプトのコメントを削除できる権限は、投稿者本人だけでなくこのフラグを持つ管理者にも与えられます。また、コメントの通報は件数として記録されますが、通報内容だけを一覧・確認するための専用画面はなく、確認するには前述の汎用テーブル閲覧機能を使う運用になっています。
16. データモデルの考え方
PostgreSQLは、関係のあるデータを一貫して保存する中心です。概念上の主なグループは次のようになります。
| データ群 | 含まれる情報 |
|---|---|
| 利用者と認証 | 利用者、認証方式、Passkey、プロフィール、言語設定 |
| チャット | 部屋、履歴、分岐、要約、共有情報、部屋の記憶 |
| プロジェクト | プロジェクト、説明、部屋との所属関係、添付資料 |
| タスク | 標準タスク、利用者タスク、版、削除状態、元プロンプトとの関係 |
| 公開コンテンツ | プロンプト、Skill、版、資源、作者、表示数、いいね、コメント、通報 |
| メモ | 本文、コレクション、アーカイブ、ピン、並び順、埋め込み、共有状態 |
| コンテキスト | 正式な事実、抽出候補、状態、重要度、出典、版 |
| MCP OAuth | クライアント、利用者向け表示、同意、認可コード、アクセストークン、更新トークン |
多くのデータは利用者へ外部キーで紐づきます。利用者を削除したときに一緒に消すもの、履歴として残すもの、公開情報の整合性を保つために別扱いにするものを、データごとに決めています。
16.1 論理削除と履歴
プロンプト、コメント、タスク、コンテキストなどは、削除日時や状態を持つものがあります。
すぐに物理削除する代わりに「通常の一覧には出さない」状態へ移す方法です。履歴、監査、関連データの整合性、復元可能性を保ちやすくなります。
16.2 JSONとベクトル
形式が変わりやすい属性、添付情報、検索結果の一部。
所有者、親子関係、版番号、公開状態など、厳密に守るべき関係。
メモやコンテキストの意味検索には埋め込みベクトルを使い、PostgreSQLのベクトル拡張で近い文章を探します。
埋め込みは検索を助ける派生データであり、タイトルや本文の本体を置き換えません。
16.3 データベース変更
データベースの構造変更は、適用順序を持つマイグレーション履歴で管理します。既に適用された履歴を書き換えず、新しい変更を後から追加します。開発環境では起動時に適用する流れがありますが、本番のBlue/Greenデプロイでは、トラフィック切り替え前に対象環境へ明示的に適用します。
17. PostgreSQL、Redis、ファイル保存の使い分け
- PostgreSQL — 失ってはいけない正本利用者、履歴、メモ、公開情報、権限、版番号。
- Redis — 複数プロセスで共有したい高速状態セッション、キャッシュ、クォータ、ロック、生成イベント。
- 永続ボリューム — データベースの行から参照される大きな派生ファイルプロンプト共有の表示画像とカード画像。
キャッシュやロックは失われても再計算できるようにし、セッションのように失うと安全上の意味が変わるものは別の扱いにします。キャッシュには期限を持たせ、メモリ逼迫時にセッションまで無差別に消えない設定にします。
画像本体はデータベースへ大きなバイナリとして詰め込まず、データベースには参照情報と寸法、形式、サイズなどを保存します。データベースが参照元なので、孤立ファイルの掃除や保存先の移行を管理できます。
17.1 クォータとレート制限の共通実装パターン
LLMの日次利用上限、メール送信の日次上限、AIエージェント呼び出しの月間上限、Web検索の月間上限、接続元IPや利用者単位のレート制限など、「一定期間あたりの回数を数え、上限を超えたら拒否する」という機能はサービス内の複数箇所に登場します。これらは機能ごとに個別実装するのではなく、共通の仕組みを使い回しています。
利用回数を1消費したい
↓
Redisが使えるか
├─ 使える → キーのINCRとEXPIREを1回のLuaスクリプトでアトミックに実行
│ (読み取りと書き込みの間に他の処理が割り込んで
│ 上限を超過するのを防ぐ)
└─ 使えない → ロックで保護したプロセス内カウンタへフォールバック
(単一プロセスの範囲でのみ正しく数えられる)
↓
消費後の残り回数と、拒否する場合は再試行までの待ち時間を返す図5 ── 回数消費の共通経路。Redisが落ちてもサービスは止めない。
複数の処理がほぼ同時に同じ利用者の回数を消費しようとしたとき、「読み取り→判定→書き込み」を別々の操作で行うと上限を超えて通過してしまう競合が起きるためです。
Redisに接続できない場合は、ロックで保護したプロセス内の辞書へフォールバックし、サービスの利用そのものは止めません。ただし、この場合の上限は複数のバックエンドプロセスをまたいで共有されないため、複数プロセス構成では実際の上限がやや緩くなり得ることを理解しておく必要があります。
この一つの仕組みを、日次・月間・短時間(数分単位)など期間の異なるさまざまなクォータへ使い回すことで、機能ごとに個別のカウント処理を実装する手間と、実装ごとの競合バグの発生源を減らしています。
18. 外部サービスとの境界
18.1 LLM提供者
利用者が選んだモデルや機能に応じて、複数のLLM提供者へ振り分けます。通常の会話、タイトルや要約、メモのタイトル案、コンテキスト候補抽出、AI補助などは、用途ごとに適した処理として呼び出します。
外部LLMの障害や応答形式の揺れを画面へそのまま露出しないため、バックエンドで応答を正規化します。外部へ送る前の秘密情報除去、添付内容の境界、利用上限を共通処理で管理します。
18.2 メールとGoogle
メール送信は確認コード、メールアドレス変更などに使います。送信回数、確認コードの期限、試行回数を管理します。Google OAuthでは、認証状態とコールバックの正当性を検証し、成功した利用者をChat-Coreの利用者へ結び付けます。
18.3 WebAuthn
Passkeyの登録と認証では、ブラウザと認証器のチャレンジ、期待するサイト識別子、Origin、ユーザー検証、署名カウンターを検証します。登録や認証の一時情報はセッションに置き、短時間で失効させます。
18.4 Web検索とURL取得
検索やURL取得は、LLMの回答を補助する外部データです。
検索結果は信頼できるシステム命令ではなく、根拠候補として扱います。取得結果にはサイズ、内容、表示の上限を設け、悪意のある文章や巨大な応答が会話文脈を占有しないようにします。
19. コンテナと運用構成
開発・本番とも、主にコンテナで動かす構成です。
| サービス | 役割 | 永続性 |
|---|---|---|
| バックエンドコンテナ | FastAPI、生成ジョブ、定期処理 | プロンプト共有画像の永続領域を参照 |
| フロントエンドコンテナ | Next.jsの画面配信 | アプリ自体は基本的に使い捨て |
| PostgreSQLコンテナ | 永続データ、ベクトル検索 | データボリュームへ保存 |
| Redisコンテナ | セッション、キャッシュ、協調状態 | Redisボリュームへ保存 |
| リバースプロキシ | 外部公開、HTTPS、経路切替 | 設定を保持 |
外部へ直接公開するのはリバースプロキシで、フロントエンド、バックエンド、Redisのポートはホスト内の限定された入口へ束縛します。Redisをインターネットへ公開しないことは必須です。
19.1 起動順序
- PostgreSQL準備完了になる。
- バックエンド起動待ちののち、必要な環境でマイグレーションを適用する。
- 準備確認バックエンドの準備状態を確認する。
- 初期データ投入初期タスクやサンプル公開コンテンツを不足分だけ投入する。
- フロントエンド起動する。
- 公開リバースプロキシが公開先を選択する。
初期データ投入は複数ワーカーが同時に行っても重複しないよう、共有ロックと一意制約を使います。起動時には一時チャットの削除や孤立画像の掃除を定期的に行うバックグラウンド処理も開始します。
19.2 ヘルスチェック
「プロセスが応答できるか」を見る。
データベースなどの依存先まで含めて、サービスとして利用可能かを見る。
フロントエンドにも別のヘルスチェックがあります。
生存と準備を分けることで、プロセスは動いていてもデータベースへ接続できない状態を、ロードバランサーが区別できます。
19.3 Blue/Greenデプロイ
本番では、稼働中の色と次に起動する色を分けるBlue/Green方式を取れます。
現在の環境(利用者の通信)
│
│ 共通のPostgreSQL / Redis
▼
新しい環境を起動
├─ マイグレーション
├─ バックエンド準備確認
└─ フロントエンド準備確認
↓
リバースプロキシの向き先を切り替え
↓
旧環境を停止図6 ── データストアは共通のまま、実行環境だけを入れ替える。
データベース接続プールは、ワーカー数と同時接続上限の積で増えます。Blue/Greenで二色が同時に動く時間は、両方の接続数を合計してPostgreSQLの接続上限を超えないように設計する必要があります。
画像保存が同一ホストのローカルボリュームであるため、複数ホストへ水平拡張する前にはオブジェクトストレージなどへ移行する必要があります。
このBlue/Greenの切り替え自体は、通常はmainブランチへの変更の取り込みをきっかけに、テスト・依存関係の監査・型検査などの継続的インテグレーション(CI)の一連の確認を通過した後で自動的に開始されます。CIを通過しない変更が本番へ流れ込まないようにする点は、この文書が扱うランタイムのシステム設計と表裏一体の運用上の前提です。
20. 起動時・停止時・定期処理
- デフォルトタスクを不足分だけ投入する。
- 初期の公開プロンプトを不足分だけ投入する。
- MCPが有効ならMCPのライフサイクルを開始する。
- 定期クリーンアップを開始する。
- データベース接続や外部依存を準備する。
- 一時チャットを期限に応じて削除する。
- 参照されなくなった添付画像を掃除する。
- 非同期の要約、タイトル、記憶、埋め込み、候補抽出を実行する。
- Redisの共有ロックで、複数ワーカーが同じ一回限りの処理を重複実行しないようにする。
- 実行中のチャット生成を停止要求する。
- 生成ジョブが終了するまで短時間待つ。
- 定期クリーンアップを停止する。
- 保留中のバックグラウンド処理を待つか、時間切れなら取り消す。
- データベース接続を閉じる。
21. テストと品質管理
テストはすべてを一度に実行するより、変更した契約と境界に合わせて絞ります。
バックエンド
- 認証、セッション、Cookie、CSRF、Redis障害。
- チャット入力、所有者、部屋、分岐、SSE、再接続、停止、クォータ。
- タスクの初期投入、言語、並び順、重複、編集履歴。
- 公開プロンプトの投稿、検索、添付変換、いいね、コメント、通報。
- メモの作成、検索、埋め込み、共有、版番号競合。
- コンテキストの候補、承認、却下、ページング、入出力。
- MCPのOAuth、権限、トークン、ツール、レート制限。
- データベース接続、マイグレーション、制約、インデックス。
外部LLM、メール、データベース、Redisは原則モックし、ルートのHTTP動作やエラーを決定的に検証します。実際のDBやRedisを使う統合テストでは、所有者境界、障害時の挙動、トランザクションを確認します。
フロントエンド
- APIレスポンスの正規化、空値、HTTPエラー。
- SSEのイベント解釈、ストリーミング表示、再接続、停止。
- チャットの履歴、部屋、タスク、プロジェクトの状態遷移。
- プロンプト共有、Skill資源、メモ、コンテキストの画面操作。
- 認証、設定、言語、テーマ、MCP同意画面。
- Markdownの安全な表示、モーダル、フォーカス、ネットワーク状態。
APIモデルを変更した場合は、バックエンドの型からフロントエンドのスキーマを再生成し、型検査と直接影響するテストを行います。SSEのデータ形を変える場合は、通常のJSONだけでなくストリーム解析も同時に確認します。
22. 設計上の重要な判断
22.1 API契約はバックエンドを正本にする
フロントエンドとバックエンドで同じ型を別々に手書きすると、片方だけ項目が変わる危険があります。バックエンドのPydanticモデルを正本にし、フロントエンドのスキーマを生成することで、契約の二重管理を減らしています。
22.2 セッション本体をRedisへ置く
Cookieへ機密セッションを置くと、読み取りや漏えい時の影響が大きくなります。Redisへ分離することで、Cookieを参照値に限定し、複数ワーカー間で同じログイン状態を共有できます。Redis障害時に安全側へ倒すことも明確になります。
22.3 画像処理と保存先を分離する
画像の安全な変換と、ローカルディスクやオブジェクトストレージへの保存は別の関心事です。変換処理が保存先を知らないことで、保存先を移行しても、検査と投稿のロジックを再利用できます。
22.4 データベース構造を履歴で管理する
起動時に場当たり的なSQLを実行すると、どの環境へ何が適用されたか分かりにくくなります。マイグレーション履歴で順序と適用状態を管理し、既存履歴を改変しないことで、開発、テスト、本番の構造を追跡できます。
22.5 更新競合を版番号で検出する
メモ、コンテキスト、MCPのように複数画面や複数クライアントから編集されるデータは、読んだ時点の版を要求します。古い画面が新しい内容を無意識に上書きしないためです。
22.6 AIが生成した表示は、サーバー検証とサンドボックスの二重で守る
AIが提案する画面操作の計画(第14章)や、AIが生成するインタラクティブな表示(第9.6節の生成UI)は、どちらも「LLMの出力を信頼して即座に実行・表示する」設計を避けています。LLMの応答は決まったスキーマでの検証を必ず経てから使い、実行・表示の段階でも、許可された範囲外の操作やライブラリを機械的に拒否します。
画面操作は許可リストとの照合、生成UIは隔離されたiframeとセキュリティポリシーが最後の防波堤です。LLMの応答そのものを信頼の起点にせず、「検証を通過したものだけを、隔離された・限定された権限の中で動かす」という同じ設計方針を、異なる機能へ繰り返し適用しています。
23. 典型的なシナリオ
23.1 メールで登録してチャットする
- ブラウザでメールアドレスを入力する。
- バックエンドが送信制限を確認し、6桁コードをメールで送る。
- コードを一時セッションへ保存する。
- 利用者がコードを入力すると、期限、試行回数、コードを検証する。
- 利用者を有効化し、セッションをローテーションする。
- 標準タスクを利用者用に準備する。
- 利用者がメッセージを送ると、部屋所有者、クォータ、文脈を確認する。
- 生成ジョブを開始し、SSEで回答を配信する。
- 完了後、履歴、タイトル、要約、必要な記憶を更新する。
23.2 プロンプトを投稿してタスクとして使う
- 利用者がタイトル、カテゴリ、形式、本文、例を入力する。
- サーバーが投稿制限と入力形を検証する。
- 参照画像があれば安全な派生画像へ変換する。
- 公開コンテンツと添付情報をデータベースへ保存する。
- 他の利用者が検索して詳細を開く。
- いいね、コメント、共有を行える。
- 「チャットで使う」を選ぶと、所有者のタスクとして追加される。
23.3 メモをMCP経由で更新する
- 外部AIクライアントがOAuth同意を得る。
- MCP呼び出しで本人のメモ一覧を取得する。
- 対象メモの本文と版番号を読む。
- 利用者が編集を依頼する。
- 外部AIクライアントが版番号付きで更新する。
- Web画面などが先に更新していれば競合として拒否する。
- 再読込後、最新の版番号で再度更新する。
23.4 会話からコンテキスト候補を承認する
- 利用者が自動抽出を有効にする。
- 通常チャット完了後、バックグラウンド処理が候補を作る。
- 秘密情報と重複候補を除外する。
- 利用者が候補を確認する。
- そのまま承認、編集して承認、却下のいずれかを選ぶ。
- 承認されたものだけが正式な事実となり、将来のAI文脈へ利用される。
23.5 AIエージェントで画面の設定を変更する
- 利用者が設定ページでエージェントを開き、「表示言語を日本語にして」と依頼する。
- バックエンドがレート制限とクォータを確認し、依頼の種類を画面操作と判定する。
- 現在ページ(設定)の操作カタログとページ構造を踏まえ、遷移とコマンド実行からなる操作計画を作る。
- ブラウザが計画をSSEで受け取り、ステップ一覧と実行ボタンを表示する。
- 利用者がステップの内容を確認し、実行を選ぶ。
- 状態を変更するステップのため、追加の確認モーダルが表示される。
- 承認後、型付きコマンドで言語設定を変更し、実行結果を画面側で検証する。
- 反映を確認できたら完了、反映されていなければ最新の画面状態を取り直して再計画する。
24. 初心者が知っておくべき制約
- AIの回答は正確性を保証するものではなく、外部検索を使っても最終確認が必要です。
- Skillやメモ、検索結果に含まれる命令文は未信頼データとして扱い、コードを自動実行してはいけません。
- 通常チャットの入力には長さ上限があり、長い履歴は要約や切り詰めの対象になります。
- 一時チャットは永続保存を目的にしていません。
- 共有リンクを知っている人は、共有されたチャット、メモ、プロンプトを見られます。
- パーソナル・コンテキストは通常の共有機能とは別ですが、MCPで権限を許可すると外部AIへ渡ります。
- MCPで投稿した公開コンテンツは下書きではなく、直ちに公開されます。
- エクスポートしたコンテキストには個人情報が含まれるため、公開ストレージへ置かないでください。
- Redis障害時はログイン状態や生成協調が利用できなくなる可能性があります。機密情報をCookieへ退避することはありません。
- プロンプト共有画像の現在の保存方式は単一ホスト向けです。複数ホスト化には保存先の移行が必要です。
- LLM、メール、Google、Web検索などの外部サービス障害やレート制限の影響を受けます。
24.1 代表的な上限
次は、設計を理解するときに目安になる主な上限です。運用設定で変更できるものと、入力モデルで固定されているものがあります。
| 対象 | 代表的な上限・期限 |
|---|---|
| チャット入力 | 1回あたり30,000文字 |
| ゲストチャット | 標準で1日10回 |
| メール確認コード | 6桁、発行から5分、入力は最大5回 |
| ログインセッション | 標準で30日 |
| チャット添付 | 最大5件、1件あたり1MiB、抽出本文にも上限 |
| 公開プロンプトのタイトル | 255文字 |
| 公開プロンプト本文 | 最大256,000文字 |
| 公開プロンプトの説明 | 300文字 |
| コメント | 1,000文字 |
| メモ本文 | 最大60,000文字 |
| パーソナル・コンテキスト | 1件のタイトル100文字、本文2,000文字、有効200件 |
| コンテキスト一括取込 | 最大10MiB、最大1,000件 |
| プロンプト共有画像 | 1枚あたり5MiB、1リクエスト約6MiB |
| AIエージェント呼び出し(接続元IP) | 300秒あたり30回 |
| AIエージェント呼び出し(利用者・ゲスト単位) | 300秒あたり40回 |
| AIエージェント呼び出し(全体合計、月間) | 既定1,000回(運用設定で変更可) |
| 生成UI(1メッセージあたり) | 最大3件 |
| 生成UIの表示高さ | 160〜900pxの範囲に丸め |
| 生成UIのHTML/CSS | それぞれ最大12,000文字 |
| 生成UIのJavaScript | 最大18,000文字(HTML・CSS・JS合計で最大36,000文字) |
上限は、料金・メモリ・データベース容量・悪用防止・外部サービスの制限を同時に考慮して設定されています。上限を増やすだけでは解決せず、入力検証、キュー、接続数、保存容量、検索速度も一緒に再設計する必要があります。
25. 用語集
| 用語 | 意味 |
|---|---|
| API | 画面や外部クライアントがサーバー機能を呼び出すための約束 |
| 非同期処理 | 長い処理を待つ間も、他の通信を止めずに進める方式 |
| セッション | 利用者が誰か、認証途中か、短期状態は何かを管理する情報 |
| CSRF | 利用者の意図しない状態変更を別サイトから送られる攻撃 |
| クォータ | 利用者や機能ごとの一定期間の利用量上限 |
| レート制限 | 短時間に集中するリクエストの回数制限 |
| SSE | サーバーからブラウザへ、接続を保ったままイベントを逐次送る方式 |
| LLM | 文章を理解・生成する大規模言語モデル |
| 文脈 | LLMが回答を作るために参照する指示、履歴、資料、記憶 |
| 埋め込み | 文章の意味を数値ベクトルへ変換した検索用データ |
| 楽観的同時実行制御 | 読み取り時の版を使い、古い内容の上書きを検知する方法 |
| 論理削除 | 実体をすぐ消さず、通常の表示から除外する方法 |
| OAuth | 外部クライアントへ、利用者の同意範囲だけ権限を委譲する仕組み |
| Passkey | WebAuthnを利用した、端末の生体認証などによるログイン方式 |
| MCP | AIクライアントと外部ツール・データを共通形式で接続する仕組み |
| Blue/Green | 新旧の実行環境を並べ、検証後に通信先を切り替えるデプロイ方式 |
| リバースプロキシ | 外部から受けた通信を適切な内部サービスへ転送する入口 |
| CSP(コンテンツセキュリティポリシー) | 通信先やスクリプト実行の許可範囲をブラウザへ指示し、想定外の読み込みや通信を遮断する仕組み |
| アーティファクト | AIの回答に埋め込まれる、生成UIとして表示するHTML・CSS・JavaScriptのまとまり |
| サンドボックス | 外部への影響やアクセスを遮断した、隔離された実行環境 |
26. 面接でこのシステムを説明するための前提
この章以降は、実装を読むための説明に加えて、ビッグテックや大手テック企業のシステムデザイン面接で深掘りされたときに考えを整理するための補足です。特定企業の採点表や合否を保証するものではありませんが、要件の確認、容量見積もり、設計、トレードオフ、障害対応、セキュリティ、運用までを一貫して説明できるように構成しています。
最初からすべての機能を細部まで語ると時間を失います。まず「何を作るか」と「どの規模か」を確認し、最も重要な一つの経路を深く設計します。Chat-Coreなら、代表的な深掘り対象は「LLMチャットのストリーミング生成」です。その後、認証、データ境界、共有、MCP、検索、運用へ広げると、中心設計と周辺設計がつながります。
26.1 最初の60秒で伝える内容
次のように要約すると、面接官がシステムの中心を理解しやすくなります。
Chat-Coreは、利用者ごとの会話、メモ、公開プロンプト、パーソナル・コンテキストを扱うAIワークスペースです。ブラウザにはNext.js、業務処理にはFastAPI、永続データにはPostgreSQL、セッションと生成中の共有状態にはRedisを使います。通常のAPIはJSON、長時間のLLM生成はバックグラウンドジョブとSSEで配信します。最も重要な不変条件は、利用者の所有データを越境させないこと、生成結果を二重保存しないこと、外部コンテンツを命令として実行しないことです。現在の単一ホスト構成から、負荷が増えれば生成ワーカー、キュー、オブジェクトストレージ、読み取り分離、必要に応じたシャーディングへ段階的に拡張します。
次の話題へ進むための地図です。面接官が「ストリーミングを詳しく」と言えば生成経路へ、「認証を詳しく」と言えばセッションとOAuthへ、「大規模化を」と言えば容量とスケール戦略へ移ります。
26.2 面接の時間配分
60分程度の面接を想定するなら、次の配分が扱いやすいです。実際の時間は面接官の質問を優先します。
| 時間 | 目的 | Chat-Coreで話す内容 |
|---|---|---|
| 最初の5分 | 要件確認 | 利用者、会話、保存、共有、外部AI、対象外 |
| 次の5分 | 規模とSLO | DAU、ピーク、同時生成、応答時間、保持期間 |
| 次の10分 | 全体構成 | ブラウザ、API、DB、Redis、LLM、保存領域 |
| 次の15分 | 中心経路 | 生成ジョブ、SSE、再接続、停止、二重保存防止 |
| 次の10分 | データと整合性 | 履歴分岐、版番号、所有者、検索、共有 |
| 次の10分 | 障害とセキュリティ | Redis、DB、LLM、SSRF、CSRF、Prompt Injection |
| 最後の5分 | トレードオフと将来 | 10倍・100倍の拡張、未解決リスク、まとめ |
細かい箱を増やすより、利用者のリクエストがどこを通り、どこが正本で、どこが一時状態かを明確にします。オンライン面接で図を使う場合も、図形の美しさではなく、設計上の判断とデータの流れを説明できることを優先します。
27. 要件定義:最初に確認する質問
27.1 機能要件
面接の冒頭では、次の質問でスコープを固定します。
- 利用者はログイン必須か、ゲスト利用が必要か。
- 会話は保存するか、一時利用を許すか。
- 回答は全文を待ってから表示するか、生成途中から表示するか。
- 生成停止、再生成、編集後の再生成、会話分岐は必要か。
- 添付資料、Web検索、画像検索、メモ参照は必要か。
- プロンプトやメモを他人へ共有するか。共有は期限付きか撤回可能か。
- 外部AIクライアントからメモやコンテキストを操作するか。
- 複数のLLM提供者を使うか。切り替えやフォールバックが必要か。
- 管理者によるモデレーションやデータ確認が必要か。
すべてを同時に作るのではありません。面接での最小構成は「ログイン済み利用者が一つの部屋で質問を送り、LLMの回答をストリーミング表示し、履歴へ保存する」までです。メモ、共有、MCP、検索はその後の拡張として扱えます。
27.2 非機能要件
機能要件だけでは大規模システムの設計を決められません。次を確認します。
- 1日あたりの登録利用者、DAU、ピーク時の同時利用者。
- 1人あたりの平均会話数、1会話のターン数、平均入力・出力サイズ。
- 初回文字が見えるまでの時間と、回答全体が終わるまでの時間。
- 読み取りと書き込みの可用性目標。
- 履歴、メモ、画像、ログの保持期間。
- データ損失を何分まで許せるかというRPO。
- 障害から何分で復旧したいかというRTO。
- 利用者間の完全なデータ分離が必要か。
- 地域分散、データ所在地、災害対策が必要か。
- LLM、検索、メールなど外部費用の上限。
数値が与えられない場合は、勝手に隠して進めず、仮定を宣言します。「以下は面接用の仮定で、実装の現在値ではありません」と言えば、見積もりと実装事実を混同せずに済みます。
27.3 不変条件
設計の中心に置くルールを、機能ではなく不変条件として説明します。
- 利用者は、自分が所有するデータまたは明示的に公開されたデータだけを読める。
- 状態変更は認証、所有者、CSRF、レート制限を通過しなければならない。
- 一つの生成要求は、同じ回答を履歴へ二重保存しない。
- 停止、完了、失敗のどれか一つだけが最終状態になる。
- 外部コンテンツ、添付資料、公開Skill、検索結果は命令ではなくデータである。
- パーソナル・コンテキストは明示的な同意なしに共有・MCP公開しない。
- 古い画面が新しいメモやコンテキストを無意識に上書きしない。
- 画像原本、アクセストークン、秘密情報を不用意に公開しない。
- LLMや検索の障害で、保存済みの利用者データを壊さない。
- 監視とログから、障害の原因をリクエスト単位で追跡できる。
「どのテーブルを使うか」より先に不変条件を語ると、設計の優先順位が伝わります。
28. 面接用の容量見積もり
28.1 見積もりの手順
容量見積もりは、正確な未来予測ではなく、ボトルネックを発見するための道具です。
- 利用者数を置く
- 日次の操作数を置く
- 平均レートを出す1日を秒へ変換する。
- ピーク係数を掛ける
- 1操作のコストを掛けるサイズ、処理時間、外部呼び出し数。
- 余裕を加える可用性のための余裕、再試行、レプリカ。
- 支配的な資源を見つける
基本式は次のとおりです。
平均RPS = 1日あたりの操作数 ÷ 86,400
ピークRPS = 平均RPS × ピーク係数
同時生成数 = ピーク生成開始レート × 平均生成時間
日次保存量 = 1日あたりの保存件数 × 1件あたりの平均サイズ
帯域 = 同時配信数 × 1秒あたりの平均配信バイト数28.2 会話の例
Chat-Coreの現在の実利用規模を示す数字ではありません。
登録500万人DAU 50万人うち20%が会話開始1人平均20ターン生成時間30秒ピークは日平均の10倍保存平均12KiB
計算すると、1日のターン数は次のようになります。
500,000人 × 20% × 20ターン = 2,000,000ターン/日
2,000,000 ÷ 86,400 ≒ 23ターン/秒(平均)
23 × 10 ≒ 230ターン/秒(ピーク)
230 × 30秒 ≒ 6,900件(ピーク同時生成)図7 ── ターン数から同時生成数を導く。
単純なHTTPサーバーをリクエストごとに30秒保持する設計は危険だと分かります。生成をジョブへ分け、APIワーカーと生成ワーカーを分離し、イベント配信を共有する必要があります。
保存量は次のようになります。
2,000,000ターン × 12KiB ≒ 24GiB/日
90日保持なら約2.2TiB(インデックス、レプリカ、検索情報を除く)履歴をすべて短期のホットテーブルに置くと、インデックスとバックアップが重くなります。時間または利用者単位のパーティション、古い履歴のアーカイブ、検索用の別インデックス、保持期間の明確化が必要です。
28.3 ストリーミング帯域の例
AI回答が平均6KiBで、1ターンを20秒かけて配信すると仮定します。ピーク同時生成が6,900件なら、平均的な配信帯域は次のように概算できます。
6,900件 × 6KiB ÷ 20秒 ≒ 2,070KiB/秒実際にはSSEイベントのメタデータ、再接続、検索状態、TLS、ピークの偏りが加わります。リバースプロキシの接続数、アイドルタイムアウト、ワーカーのファイルディスクリプタ、Redisのイベント保持量を確認します。
生成本文を毎回データベースへ一文字ずつ書くのではなく、イベント配信と最終保存を分けるのが重要です。
28.4 添付画像の例
1%のターンが平均0.5MiBの画像を1枚添付すると仮定します。
2,000,000ターン × 1% × 0.5MiB = 10GiB/日(原入力の概算)表示用とカード用の派生画像、バックアップ、削除猶予を考えると、原入力の10GiBだけを見積もるのは不十分です。現在のローカルボリューム方式ではホスト容量が上限になるため、規模が増えたらオブジェクトストレージ、CDN、ライフサイクル削除へ移行します。
28.5 見積もりから導く設計判断
この仮定から、次の判断を説明できます。
- LLM生成はAPIワーカーから分離する。
- ストリームイベントは一時共有状態またはイベント基盤へ置く。
- PostgreSQLの接続を長時間占有しない。
- 履歴をパーティションまたはアーカイブで管理する。
- 画像はデータベース本体から分離する。
- LLM提供者ごとの同時実行数とクォータを制御する。
- 利用者ごと、接続ごと、提供者ごとのバックプレッシャーを設ける。
29. SLO、SLI、エラーバジェット
29.1 SLIを分ける
「速い」「安定している」と言うだけでは曖昧です。観測する指標を定義します。
| 指標 | 測定方法 | 面接での意味 |
|---|---|---|
| API可用性 | 成功レスポンスの割合 | 認証、一覧、保存が使えるか |
| API遅延 | p50、p95、p99 | 画面操作の体感と混雑の検知 |
| 初回トークン時間 | 生成開始から最初の表示まで | LLMサービスとキューの待ち時間 |
| 生成完了時間 | 生成開始から完了まで | 回答全体の体験 |
| ストリーム再接続率 | 切断後に復帰した割合 | ネットワークとプロキシの品質 |
| 部分回答の保存成功率 | 停止・切断時に復元できた割合 | データ損失への耐性 |
| テナント越境件数 | 認可違反の検知数 | 0件を絶対条件にする安全指標 |
| DBプール枯渇時間 | 空き接続がない時間 | 接続数設計の妥当性 |
| 生成キュー待ち時間 | ジョブ登録から開始まで | LLMワーカーの混雑 |
| 外部プロバイダー失敗率 | 提供者別のタイムアウト・429・5xx | フォールバック判断 |
29.2 面接用の目標例
次は設計議論のための仮の目標です。実際のサービス契約として断定してはいけません。
- 読み取りAPIの月間可用性99.9%以上。
- 認証済みの通常APIはp95 300ms以内。ただし外部LLM待ちは除く。
- チャットの初回トークンはp95 2秒以内。ただし提供者障害時を別扱いにする。
- 生成ジョブのp95完了時間は30秒以内を目標にする。
- ストリーム切断の大半を数秒以内に再接続する。
- 正常に保存されたデータのRPOは数分以内、復旧目標は30分以内を目指す。
- 利用者間のデータ越境は0件。
現行リポジトリには自動バックアップや復元訓練を運用として完成させた仕組みが確認できないため、RPOとRTOは「達成済み」と言わず、「本番導入前に設計・検証する目標」として説明します。
29.3 エラーバジェット
月間可用性99.9%なら、理論上許される停止時間は約43分です。その時間をすべて新機能のデプロイに使うのではなく、障害、移行、依存サービス停止、計画メンテナンスの扱いを決めます。
エラーバジェットを使い切ったら、機能追加より信頼性改善を優先します。
30. 現状から10倍・100倍へ拡張する
30.1 現状の性質
現在の構成は、単一ホストのDocker Composeを中心に、バックエンド、フロントエンド、PostgreSQL、Redis、画像用永続領域を分けています。Blue/Greenで同一ホスト内の切り替えはできますが、画像保存は単一ホスト制約を持ちます。LLM生成、SSE、Redisセッション、PostgreSQL接続プールが主要なスケール境界です。
生成やクォータ判定などの重い処理を逃がす「専用の実行枠」(第5.3節)も、現状は分散ジョブキューではなくバックエンドプロセス内のスレッドプールです。生成ジョブの状態と進行イベントはRedisで複数ワーカー間に共有できますが、実際に処理を実行する部分は単一ホストのCPUとスレッド数に縛られます。したがって、10倍規模を検討する際に最初に切り離すべき境界の一つは、このスレッドプールを独立した生成ワーカー群へ分離することです。
30.2 10倍になったときの構成
面接で10倍を聞かれたら、最初から全分散化するのではなく、ボトルネックの順に分離します。
利用者
↓
CDN / リバースプロキシ
├─ フロントエンドの静的・SSR配信
└─ APIの負荷分散
↓
複数のステートレスAPI
├─ Redisまたはマネージド共有状態
├─ PostgreSQL主系・読み取りレプリカ
├─ 生成ジョブキュー
├─ 生成ワーカー群 ── LLM提供者ゲートウェイ
├─ オブジェクトストレージ ── CDN
└─ 検索・ベクトル検索基盤図8 ── 10倍規模の構成。ステートを外へ出し、生成を専用群へ寄せる。
変更の順番は次のようにします。
- APIをステートレスにするセッションと生成イベントを共有状態へ置く。
- 生成を分離するキューと専用ワーカーへ分ける。
- Redisを冗長化する接続数、メモリ、イベント保持期間を監視する。
- PostgreSQLを強化する接続プーラー、読み取りレプリカ、適切なインデックスを導入する。
- 画像を外へ出すオブジェクトストレージへ移し、CDNと署名付き配信を使う。
- 検索を検討する埋め込み検索や公開フィードを独立した検索基盤へ移すか検討する。
- LLMをゲートウェイへ集約する提供者ごとのレート制御とフォールバックをまとめる。
30.3 100倍になったときの構成
100倍では、単にAPIの台数を増やすだけでは足りません。
- チャット履歴を利用者または部屋のハッシュで分割する。
- 履歴を時間でパーティションし、古いデータを低コストストレージへ移す。
- 公開フィードを読み取り専用の検索インデックスへ複製する。
- メモやコンテキストの意味検索を専用ベクトル基盤へ分離する。
- LLM生成を地域、モデル、優先度、クォータでスケジューリングする。
- 接続が長いストリームを専用ゲートウェイで扱う。
- 利用者のホームリージョンを決め、データ所在地を固定する。
- リージョン障害では読み取り専用または別リージョンへ切り替える。
この段階では、強い一貫性を全機能へ適用するのではなく、機能ごとに一貫性を選びます。本人のメモ更新や権限は強い一貫性、閲覧数や検索結果は結果整合性でよい、というように分けます。
30.4 スケールのトレードオフ
分散化すると、次の代償が増えます。
- サービス間通信の失敗が増える。
- 分散トレーシングとデバッグが必要になる。
- データの二重化と結果整合性を管理する必要がある。
- デプロイ、マイグレーション、ロールバックが難しくなる。
- 運用費と監視費が増える。
まず現在のボトルネック、必要なSLO、データ境界、組織の運用能力を確認します。
31. 一貫性、競合、冪等性
31.1 機能ごとの一貫性
| 機能 | 望ましい一貫性 | 理由 |
|---|---|---|
| 所有者・権限 | 強い一貫性 | 越境や権限昇格を許してはいけない |
| メモ更新 | 強い一貫性と版番号 | 古い画面の上書きを防ぐ |
| コンテキスト更新 | 強い一貫性と版番号 | AIへ渡す個人情報を正確に保つ |
| いいね | 一意制約を伴う強い一貫性 | 二重評価を防ぐ |
| コメント | 保存時は強い一貫性 | 本文、作者、通報関係を壊さない |
| 閲覧数 | 結果整合性でもよい | 数件の遅れより書き込み性能を優先できる |
| 公開検索 | 結果整合性でもよい | 投稿直後の反映遅れを許容できる |
| 埋め込み | 結果整合性 | 本文保存と非同期生成を分離できる |
| SSEイベント | 順序と重複排除 | 再接続で同じ断片を二度表示しない |
| 生成の最終保存 | 強い一回性 | 同じ回答を二重に履歴へ入れない |
31.2 生成ジョブの状態機械
作成済み
↓
実行中 ─────────────→ 失敗
│ ▲
│ 停止要求 │
▼ │
停止処理中 ─────────────┘
│
├─ 停止完了
└─ 先にLLMが終わった場合 → 完了図9 ── 停止要求とLLM完了が競合しても、終端状態は一つに定まる。
停止要求とLLM完了が同時に起きる競合では、データベースまたは共有状態で「まだ終端状態でない」ことを条件に一度だけ状態を遷移させます。先に終端状態へ確定した側が勝ち、後から来た停止や完了は保存処理を再実行しません。
31.3 冪等性
ネットワークが切れると、ブラウザは「送信できたか分からない」状態になります。新規生成、メモ保存、コンテキスト保存、MCP投稿には、必要に応じて再試行を識別する一意な要求IDを使います。
サーバーは処理を二度実行せず、最初の結果を返します。要求IDの保存期間、利用者との紐付け、異なる内容で同じIDを使った場合の拒否を決めます。
32. 障害マトリクス
| 障害 | 現在の影響 | 大規模化したときの改善 | 利用者への説明 |
|---|---|---|---|
| PostgreSQL停止 | 永続データの読み書き、準備状態に影響 | 主系・レプリカ、フェイルオーバー、バックアップ復元 | 保存や履歴が一時的に利用不可 |
| Redis停止 | セッション、生成協調に影響。クォータ判定はプロセス内カウンタへ自動的にフォールバックするが、複数プロセスをまたいだ上限共有はできなくなる | 冗長化、監視、用途ごとの分離 | 再ログインや生成再接続が必要になる場合がある |
| LLMタイムアウト | 回答が遅延・失敗 | 提供者別サーキットブレーカー、フォールバック、キュー | 再試行または別モデルを案内 |
| LLMの429 | クォータや同時実行上限 | 利用者・提供者別スケジューラー | 待機時間を表示 |
| SSE切断 | 表示が止まる可能性 | イベント番号、再接続、専用ゲートウェイ | 生成状態を確認して復帰 |
| APIワーカー停止 | その接続が切れる | 複数ワーカー、共有ジョブ状態 | 再接続後に続きから表示 |
| 画像保存途中の失敗 | 孤立または不完全な派生画像の可能性 | 原子保存、参照確定、孤立掃除 | 投稿を失敗として再試行 |
| Web検索停止 | 根拠や最新情報が取得できない | 検索キャッシュ、機能縮退 | 検索なしで回答する旨を表示 |
| 埋め込み停止 | 意味検索が使えない | 非同期再生成、キーワード検索への縮退 | 通常検索を案内 |
| メール停止 | 登録・認証コードが届かない | 別配信経路、送信状態の監視 | 時間を置いた再試行を案内 |
| Google停止 | Googleログインができない | メール・Passkeyを代替にする | 他のログイン方法を案内 |
| MCP OAuth停止 | 新規接続やトークン更新に失敗 | 既存トークンの短期継続、可観測性 | 連携を再試行 |
| マイグレーション失敗 | 新旧環境の構造が合わない | Expand/Contract、事前検証、切り替え停止 | デプロイを止める |
障害対応では、すべてを無理に動かすのではなく、データを壊さない縮退を選びます。たとえば埋め込み検索はキーワード検索へ縮退できますが、セッション本体を安全でないCookieへ移すことはしません。
33. セキュリティ深掘り
33.1 脅威モデル
| 資産 | 攻撃例 | 防御の中心 |
|---|---|---|
| 会話・メモ | 他人のデータの読み取り | 利用者ID、所有者条件、テスト、監査 |
| セッション | Cookie盗難、固定攻撃 | Redis本体分離、署名、Secure、ID更新 |
| 認証コード | 総当たり、メール大量送信 | 短い期限、試行制限、レート制限、定時間比較 |
| OAuth | コールバック偽装、権限過剰 | state、PKCE、リダイレクト検証、スコープ、ローテーション |
| MCP | 混乱した代理人、権限越境 | 接続ごとの同意、スコープ、所有者、監査、失効 |
| 添付画像 | 巨大画像、アニメーション、形式偽装 | 完全デコード、画素上限、派生変換、原本非公開 |
| 文書添付 | ZIP爆弾、XML外部参照、命令注入 | 展開上限、安全な解析、データと指示の分離 |
| URL取得 | SSRF、内部ネットワーク探索 | スキーム、DNS、IP、リダイレクト、サイズ検証 |
| Markdown | 保存型XSS | サニタイズ、安全なHTML変換、CSP |
| LLM | Prompt Injection、情報引き出し | 未信頼データ境界、ツール許可表、最小文脈、人の確認 |
| 管理画面 | 強権限の悪用、SQL注入 | 専用認証、試行制限、入力制限、監査、最小権限 |
| 画面操作AIエージェント | カタログ外操作の実行、危険操作の無確認実行、他ページやエージェント自身のUIへの誤操作 | 型付きコマンドの許可リスト、危険操作の強制確認、実行結果の検証と再計画、機微フィールドの送出除外 |
| 生成UI(サンドボックスアーティファクト) | 任意コード実行、外部通信によるデータ持ち出し、想定外ライブラリの読み込み | スキーマ検証を唯一の通過点にする、CSPで外部通信・フォーム送信・入れ子フレームを遮断、許可ライブラリのみ自オリジンから配信 |
| ログ | 秘密や個人情報の漏えい | 伏せ字、アクセス制御、保持期間、監視 |
33.2 テナント分離
一つのシステムを複数の利用者が共有することです。最も危険なバグは、検索条件に利用者の所有条件が抜けることです。
安全策は一つだけに依存させません。
- ルート層ログイン利用者を得る。
- サービス層対象操作と利用者の関係を確認する。
- リポジトリ層取得・更新条件にも利用者を含める。
- 返却前所有者や公開状態を再確認する。
- テスト異なる利用者のテストデータで横断アクセスを検証する。
- 経路の分離管理者、ゲスト、MCP利用者で権限経路を分ける。
IDと利用者の組み合わせで問い合わせます。共有コンテンツだけは、所有者ではなく有効な共有状態を別の認可条件にします。
33.3 Prompt Injectionへの答え
添付文書、Webページ、公開Skill、メモなどの中に、LLMへ別の命令を出す文章が含まれる攻撃です。文字列をエスケープするだけでは解決しません。
- システム指示、利用者指示、参照データを明確に区切る。
- 参照データの命令はデータとして読むようLLMへ指示する。
- 参照データから直接ツールを呼ばせない。
- ツールは機能ごとの許可表、利用者権限、対象所有者、確認を通す。
- 外部ページから取得したURLへ自動送信や自動投稿をしない。
- 重要操作は利用者の確認を必要にする。
- LLMが守れる範囲と、サーバーで強制する範囲を分ける。
「LLMへ注意書きを付ければ安全」と答えるのは不十分です。最終的な権限、送信先、保存先は、LLMの文章ではなくサーバーのコードとデータベース制約で決めます。
33.4 SSRFへの答え
利用者やLLMが指定したURLをサーバーから取得すると、内部ネットワークを探られる危険があります。防御はURL文字列の確認だけでは不十分です。
- HTTPとHTTPSだけを許可する。
- localhost、ループバック、プライベートネットワーク、クラウドメタデータ用アドレスを拒否する。
- DNS解決後のIPを検証し、接続時に再び別IPへ解決されないようにする。
- リダイレクトごとに同じ検査をする。
- 応答サイズ、時間、コンテンツ種別、リンク追跡深さを制限する。
- HTML本文をサニタイズし、スクリプトを実行しない。
34. API、イベント、互換性
34.1 APIの分け方
APIは「画面のボタンごとの命令」ではなく、資源と状態遷移を中心に設計します。
| API群 | 主な責務 |
|---|---|
| 認証 | セッション、確認コード、Google、Passkey、プロフィール |
| 会話 | 部屋、履歴、生成開始、状態、停止、分岐 |
| 再利用 | タスク、プロジェクト、公開プロンプト |
| 保存 | メモ、コレクション、共有、コンテキスト |
| 検索 | 公開検索、メモ検索、意味検索、Web検索 |
| 連携 | OAuth同意、接続、MCP機能 |
| 運用 | 生存、準備、管理、監視 |
成功、入力不正、未認証、権限不足、競合、上限、外部障害を、HTTPステータスと安定したエラー種別で区別します。
画面に表示する日本語や英語は変えられても、フロントエンドの分岐は安定したエラー種別を使うべきです。
34.2 SSEイベントの契約
SSEでは、各イベントに少なくとも「順序を確認する識別情報」「イベントの種類」「データ本体」を持たせます。種類には、進行状態、回答断片、検索情報、表示部品、完了、停止、エラーなどがあります。
生成ジョブ
↓
イベント番号付きの進行イベント
↓
ブラウザが番号を確認して重複排除
↓
接続断なら最後の番号から再接続
↓
完了または停止を一度だけ確定図10 ── イベント番号が重複排除と再接続の両方を支える。
古いブラウザが未知のイベントを無視できるようにし、必須項目を急に変えないようにします。イベントを一定期間だけ再生できるようにする場合は、保持量と利用者ごとのアクセス制御も設計します。
34.3 ページングと検索
履歴、公開フィード、メモ、コンテキストは、すべてを一度に返しません。作成時刻と一意なIDなどを組み合わせたカーソルで、次のページを安定して取得します。ページング中に新しいデータが追加されても、同じ行が重複したり欠落したりしにくい順序を選びます。
検索結果は、本人の所有条件、公開状態、削除状態、検索語、カテゴリ、順位、カーソルを一つの契約として扱います。
検索の正本と表示用キャッシュを分ける場合は、投稿直後に検索へ反映されない時間を明示します。
34.4 APIの進化
- 任意で追加新しい項目を任意で追加する。
- 旧クライアント確認旧クライアントが無視できることを確認する。
- スキーマ再生成バックエンドの契約からフロントエンドのスキーマを再生成する。
- テスト更新ロジック、SSE解析、画面テストを更新する。
- 旧項目の廃止十分な移行期間を置いてから廃止する。
データベース変更は、まず新旧両方を読める状態にし、次に新形式を書き、バックフィルを完了してから旧形式を削除するExpand/Contract方式が安全です。コードとデータベースを同じ瞬間に切り替えられないBlue/Green環境では特に重要です。
35. データベースと検索の深掘り
35.1 なぜPostgreSQLか
Chat-Coreでは、利用者、所有権、部屋、履歴、分岐、共有、コメント、版番号、OAuthの関係が多く、書き込みの一貫性が重要です。PostgreSQLを使う理由は、次の関係と制約を一つのトランザクションで扱いやすいからです。
- 利用者と所有データの外部キー。
- 部屋と履歴の親子関係。
- プロンプトとコメント、いいねの整合性。
- メモとコレクションの関係。
- 版番号を条件にした更新。
- 一意制約による二重いいねや重複タスクの防止。
ドキュメントDBは柔軟ですが、所有権と複数資源の整合性をアプリケーションだけで維持する負担が増えます。規模やアクセスパターンが変われば一部を検索基盤やイベント基盤へ複製する余地はありますが、最初の正本としては関係データベースが適しています。
35.2 ホットスポット
大量利用時に集中しやすい場所は、最新メッセージ、人気プロンプト、閲覧数カウンター、生成イベント、メモ検索、OAuthのレート制限です。
- 人気一覧は短期キャッシュし、安定したカーソルで読む。
- 最新履歴は部屋単位の索引を使い、全履歴走査を避ける。
- 閲覧数は書き込みをまとめ、正確なカウントが不要なら非同期集計する。
- 生成イベントはデータベースへ一文字ずつ書かず、一時イベント基盤で配る。
- 埋め込み生成は保存と非同期化し、検索不能時は文字列検索へ縮退する。
- 一つの利用者やMCP接続が全体を占有しないよう、公平なクォータを設ける。
35.3 パーティションとアーカイブ
履歴が増え続ける場合、部屋や作成時刻でパーティションを分けます。古いデータを別ストレージへ移す場合は、次を決めます。
- 画面から過去履歴を読み戻せるか。
- 検索対象に残す期間。
- 要約だけをホット領域へ残すか。
- 共有リンクが古い履歴を参照できるか。
- 削除要求をアーカイブにも反映できるか。
- バックアップと復元で正本とアーカイブを同時に戻せるか。
個人情報を含むため、暗号化、アクセス制御、削除、監査を一緒に設計します。
36. 運用、可観測性、復旧
36.1 監視するメトリクス
利用者操作
↓
リクエストID
├─ APIレイテンシ、ステータス、エラー種別
├─ DBプール使用率、クエリ時間、ロック待ち
├─ Redisメモリ、ヒット率、接続、イベント遅延
├─ 生成キュー長、待ち時間、同時実行数
├─ LLM提供者別の成功、429、5xx、費用
├─ SSE切断、再接続、完了、停止
├─ 添付変換失敗、保存容量、孤立画像
└─ OAuth同意、更新、失効、権限エラー図11 ── 一本のリクエストIDから全レイヤーの指標をたどる。
ログはリクエストIDでつなぎ、ブラウザ、リバースプロキシ、API、生成ワーカー、外部呼び出しを追跡します。外部サービスの秘密、メッセージ本文、個人情報を無制限にログへ書かないようにします。
36.2 アラート
「高い」だけでなく、利用者影響と行動を結び付けます。
- API可用性低下 → 直近デプロイ、DB、依存先を確認し、必要なら切り戻す。
- DBプール枯渇 → 長いトランザクション、接続数、重い検索を調べる。
- Redisメモリ逼迫 → TTLのないキャッシュ、イベント保持、セッション量を確認する。
- 生成キュー増大 → LLM提供者の429、ワーカー数、利用者クォータを確認する。
- SSE再接続急増 → リバースプロキシのタイムアウト、ネットワーク、ワーカー停止を確認する。
- テナント越境の疑い → 即時に該当機能を止め、監査と影響範囲調査を行う。
- 添付容量急増 → 大量投稿、孤立掃除、ユーザー別クォータを確認する。
36.3 バックアップと復元
PostgreSQL、画像、Redisは復元の性質が異なります。
- PostgreSQL最優先の正本。定期バックアップ、世代管理、暗号化、復元検証が必要。
- 画像データベースの添付情報と対応する時点でバックアップする。
- Redis再構築でログイン再認証や生成イベント消失が起こることを、利用者影響として受け入れるか決める。
- 復元訓練バックアップだけでなく、実際に別環境へ復元できるか定期的に確認する。
現状の単一ホスト構成では、データベースと画像ボリュームを同じ障害で失う可能性があります。面接では「バックアップがある」と言うだけでなく、保存先、暗号化、RPO、復元手順、整合性、削除要求の扱いまで説明します。
36.4 マイグレーションの安全な流れ
- 変更の設計
- 追加新しい列・新しい構造を追加する(旧コードでも動く)。
- 両読み対応アプリを新旧両方読めるように更新する。
- バックフィル低速・再開可能に実行する。
- 書き込み確認新しい形式への書き込みを確認する。
- 監視期間
- 削除旧列・旧構造を削除する。
大きなテーブルへ長いロックをかけない、途中で停止しても再開できる、旧バージョンが読み取れる、切り戻してもデータが壊れない、を条件にします。破壊的な変更を一回のデプロイへ詰め込まないことが重要です。
37. コストと性能のトレードオフ
AIシステムでは、CPUやDBだけでなくLLMトークン費用が主要なコストになります。
37.1 コストを下げる方法
- 古い会話を要約し、毎回送るトークンを減らす。
- 同じ公開検索やタイトル案を短時間キャッシュする。
- 軽量モデルを分類、要約、タイトル案へ使う。
- 高品質モデルを複雑な回答だけへ使う。
- 埋め込みを同期処理せず、変更時だけ再生成する。
- Web検索を必要な質問に限定する。
- 利用者・機能・期間ごとにクォータを設ける。
- 失敗時の自動再試行が外部費用を二重化しないようにする。
37.2 性能との交換条件
| 選択 | 得られるもの | 失うもの |
|---|---|---|
| 文脈を短くする | コストと速度が改善する | 過去の条件を失う |
| 強いキャッシュ | 速度が改善する | 最新情報とのずれを生む |
| 小さいモデル | 安価 | 構造化出力や安全判定の品質が下がる可能性 |
| 非同期保存 | 初回応答が速くなる | 完了前にプロセスが落ちると再処理が必要 |
| 検索結果を多く渡す | 根拠が増える | 入力長、費用、Prompt Injectionの面積も増える |
| 複数提供者のフォールバック | 可用性が上がる | 応答品質、費用、モデルごとの契約差の管理が必要 |
「最も速い構成」ではなく、「SLO、品質、費用、データ保護のどれを優先し、何を犠牲にしたか」を述べます。
38. 主要な選択肢とトレードオフ
| 選択 | 採用・推奨する理由 | 代替案と代償 |
|---|---|---|
| SSE | サーバーからブラウザへの一方向配信に合い、再接続が比較的単純 | WebSocketは双方向性に強いが、接続管理と運用が複雑 |
| PostgreSQL | 所有権、履歴、版、共有、制約を強い一貫性で扱える | NoSQLは柔軟だが、複数資源の整合性をアプリで担う |
| Redis | セッション、TTL、一時イベント、ロックを低遅延で共有できる | Kafkaは耐久性・大規模再生に強いが、運用が重い |
| バックグラウンド生成 | 長いLLM処理とHTTP接続を分離できる | 同期処理は実装が簡単だが、切断・再試行・スケールに弱い |
| ローカル画像保存 | 小規模・単一ホストで簡単、低遅延 | オブジェクトストレージは冗長・多ホストに強いが、コストと権限設計が増える |
| PostgreSQL内ベクトル検索 | 正本と近く、システム数を増やさず始められる | 専用ベクトル基盤は大規模検索に強いが、同期と運用が増える |
| 版番号による更新 | 画面、MCP、複数接続の競合を明示的に拒否できる | 最終書き込み勝者は簡単だが、利用者の変更を失う |
| スコープ別MCP権限 | 最小権限と再同意を説明しやすい | 一つの広い権限は簡単だが、漏えい時の影響が大きい |
| 要約と直近履歴 | 入力費用と遅延を抑えられる | 全履歴送信は文脈を保てるが、上限と費用に耐えない |
| 生成UIのライブラリを許可制+自オリジン配信 | CDNの改ざんや停止の影響を受けず、CSPで外部読み込みを一律遮断できる | 都度使えるライブラリは限られ、追加のたびにレビューと配信作業が必要になる |
現在の主な通信はサーバーからブラウザへの生成配信で、開始・停止・操作は通常APIで十分です。双方向イベントが増え、接続数やインタラクションの要件が変われば、WebSocketを再評価します。
39. 想定される深掘り質問と回答の骨子
各質問は結論を一文で述べてから根拠を足す形にしています。面接では、この骨子をそのまま読むのではなく、自分の言葉で再構成してください。
Q1. なぜセッションをCookieだけに保存しないのか
結論: Cookieには参照値だけを置き、機密情報をRedisへ分離します。
複数ワーカーで共有でき、認証コード、OAuth状態、Passkeyチャレンジ、管理者状態をブラウザへ露出しません。Redis障害時は安全側に倒して再認証を求めます。大規模化では、Redisを冗長化し、セッション専用の可用性を確保します。
Q2. Redisが落ちたら全機能を止めるのか
結論: 用途別に縮退します。
キャッシュや意味検索はなくても処理できますが、セッション本体と生成協調は影響が大きいので、偽のログイン状態を作らず、再認証や生成再接続を案内します。
現在のプロセス内フォールバックは単一プロセス向けで、複数ホスト運用の解決にはなりません。
Q3. LLMの回答が二重に保存されない保証はどこにあるか
結論: 開始時の重複拒否と、終端状態への条件付き遷移の二段構えです。
生成ジョブに利用者・部屋・要求IDの組み合わせを持たせ、開始時の重複を拒否します。イベント配信は重複しても番号で捨て、最終保存は終端状態への条件付き遷移と一意条件で一度だけ行います。停止と完了の競合は、最初に確定した終端状態を採用します。
Q4. SSEが切断したら回答を失わないか
結論: 生成はHTTP接続の寿命と分離されているため失いません。
イベントに番号を付け、ブラウザは最後に受信した番号から再接続します。再接続できない場合でも、ジョブ状態と保存済み部分回答を確認します。長期的には、イベント基盤の保持期間、再生位置、接続ゲートウェイのタイムアウトを設計します。
Q5. 6,900件の同時生成をどう処理するか
結論: APIワーカーは生成を直接実行せず、キューへ入れます。
生成ワーカーは提供者ごとの同時実行上限、利用者クォータ、優先度、公平性でスケジュールします。キュー長と待ち時間がSLOを超えたら、軽量モデルへの切り替え、待機表示、利用者ごとの制限を行います。ストリーム接続は生成ワーカーと分け、状態を共有します。
Q6. なぜチャット履歴を全部LLMへ渡さないのか
結論: 入力上限、費用、遅延、秘密情報の露出が増えるためです。
古い履歴を要約し、直近の会話と重要な記憶を残します。要約は派生データなので原文を置き換えず、必要なら再構築できます。要約が失敗しても直近履歴だけで回答できる縮退を用意します。
Q7. 他の利用者のメモをどう防ぐか
結論: すべての読み書きで利用者IDと対象IDを同時に条件へ入れます。
ルート、サービス、リポジトリで防御を重ねます。共有メモだけは有効な共有状態を別途検証します。キャッシュキーにも利用者の境界を含め、他人のレスポンスを再利用しないようにします。二人以上の利用者を使った越境テストを必須にします。
Q8. Prompt Injectionを完全に防げるか
結論: LLMだけでは完全には防げません。
参照データと命令を分離し、外部コンテンツのツール利用を許可表で制限し、保存・送信・公開などの重要操作はサーバー認可と人の確認を通します。
LLMが「許可された」と文章で言っても、サーバーの権限判定を通らなければ実行できないようにします。
Q9. URL取得のSSRFをどう防ぐか
結論: 文字列検査ではなく、解決後のIPまで検証します。
スキームを限定し、DNS解決結果のIPがループバック、プライベート、メタデータ用でないことを検証します。リダイレクトごとに再検査し、接続時のDNS再解決による変化も防ぎます。応答サイズ、時間、コンテンツ種別、リンク追跡深さを制限します。
Q10. 画像をデータベースへ保存しない理由は何か
結論: 大きなバイナリと、所有権・公開状態・投稿メタデータは異なる性質だからです。
データベースには参照情報を置き、画像は安全な派生形式として保存します。小規模ではローカルボリュームで単純に始められますが、複数ホストではオブジェクトストレージとCDNへ移行します。
Q11. いいねと閲覧数は同じ一貫性でよいか
結論: 別でよい。機能ごとに一貫性とコストを選びます。
利用者ごとの重複を防ぐ必要があるため、一意制約を伴う強い一貫性にする。
数秒・数分の遅れが利用者体験を壊さないため、イベントをまとめて非同期集計できる。
Q12. MCPは通常のWeb APIと何が違うか
結論: 外部AIクライアントが利用する機械向けのツール境界です。
ブラウザCookieではなくOAuthトークンを使い、ツールごとにスコープ、利用者、所有権、レート制限を検証します。メモとコンテキストの更新には版番号を要求し、公開投稿は直ちに公開されることを同意画面で明示します。
Q13. データベースのマイグレーション中に旧環境が動いていたらどうするか
結論: 旧環境が新しい列を知らなくても動く追加から始めます。
新旧両方を読めるコードをデプロイし、バックフィル後に新形式の書き込みへ切り替え、監視してから旧形式を削除します。Blue/Greenの切り替え前に検証し、破壊的な変更を一回のデプロイへ詰め込みません。
Q14. 最も大きい現在の弱点は何か
結論: 単一ホストのローカル画像保存と、バックアップ・復元訓練の運用です。
画像はオブジェクトストレージへ移し、データベースと画像を整合した状態でバックアップします。RPOとRTOを決め、実際の復元訓練を行うまで「災害対策済み」とは言いません。
Q15. 何を最初に測るか
結論: 利用者体験、費用、データ安全性に直結する指標から測ります。
API可用性p95遅延初回トークン時間生成キュー待ち時間LLM提供者別エラー率SSE再接続率DBプール使用率Redisメモリテナント越境
これらは次のスケール投資の判断材料になります。
Q16. AIが生成したUIをそのまま実行して安全なのか
結論: そのまま実行はしません。検証と隔離という機械的な仕組みで担保します。
LLMの応答からアーティファクト候補を抽出した後、決まったスキーマでの検証を必ず通し、HTML・CSS・JavaScriptをそれぞれ個別に安全化します。この検証を通過したものだけが、外部通信・フォーム送信・入れ子フレームを禁止したセキュリティポリシー付きの隔離されたiframeの中で動きます。利用できる外部ライブラリも一種類に限定し、CDNからではなく自オリジンの静的ファイルから配信します。
「LLMの出力だから安全」という前提を置かない点は、画面操作AIエージェントにおける「カタログと確認モーダルによる担保」と同じ考え方です。
40. 現状実装と面接上の発展案を分けて話す
面接で最も信頼を失うのは、将来案を実装済みのように話すことです。次の三種類に言い分けます。
| 表現 | 使う場面 | 例 |
|---|---|---|
| 現在実装している | リポジトリで確認できる事実 | Redisをセッション本体と生成協調に使っている |
| 現在の制約 | 実装から分かる弱点 | 画像保存が単一ホストに依存している |
| 規模拡大時の提案 | 面接での設計判断 | 生成キュー、専用ワーカー、オブジェクトストレージへ移行する |
現在はRedisでイベントとジョブ状態を共有しています。6,900同時生成まで増えるなら、Redisの一時イベントだけに依存せず、生成キューと長期再生可能なイベント基盤を導入します。ただし、その分、再生位置、保持期間、重複、運用費が増えます。
実装済みの事実、仮の数字、提案の境界を明確にすると、深掘りで「それは今あるのか」「なぜ今は不要なのか」と聞かれても崩れません。
41. 面接前の実践チェックリスト
- 利用者、最小機能、対象外を最初に言える。
- DAU、ピーク、同時生成、保持期間を質問できる。
- レイテンシ、可用性、RPO、RTOを分けて確認できる。
- 共有とパーソナル・コンテキストの境界を説明できる。
- ブラウザ、リバースプロキシ、API、DB、Redis、LLMを図に描ける。
- 正本とキャッシュと一時状態を区別できる。
- チャット生成を同期HTTPではなくジョブとストリームで説明できる。
- APIワーカー、生成ワーカー、イベント配信を分ける理由を言える。
- 版番号、冪等性、終端状態で競合を説明できる。
- 平均RPS、ピークRPS、同時生成数、日次保存量を手計算できる。
- 10倍と100倍で異なる設計変更を提案できる。
- DB接続数をワーカー数とプールサイズから見積もれる。
- 画像、検索、埋め込み、イベントを正本から分離する理由を言える。
- 分散化の代償を、運用、費用、一貫性、デバッグで説明できる。
- DB、Redis、LLM、SSE、画像、OAuth、検索の障害時挙動を言える。
- CSRF、セッション固定、SSRF、Prompt Injection、XSS、アップロード爆弾を区別できる。
- 利用者IDを全データアクセス条件へ入れる理由を言える。
- 重要操作はLLMの判断ではなくサーバー認可で決めると言える。
- ログ、メトリクス、リクエストID、アラート、復元訓練を説明できる。
- 仮定を声に出し、数値が不明なら質問する。
- 現状実装と将来案を混同しない。
- 一つの深掘りを完了してから次へ進む。
- 各選択肢について、採用理由と代償を一つずつ言う。
- 最後に不変条件、SLO、最大のリスク、次の改善をまとめる。
42. まとめ
Chat-Coreの中心は「ブラウザのチャット画面」ではなく、利用者単位のデータ境界を守りながら、会話を生成し、保存し、再利用し、公開し、外部AIへ明示的に連携する一連の仕組みです。
理解の順番としては、次のように追うと分かりやすくなります。
- 1. 境界フロントエンドとバックエンドの境界。
- 2. 認可認証と所有者確認。
- 3. 中心経路チャット生成とストリーミング。
- 4. 拡張メモ、公開コンテンツ、コンテキスト、MCP。
どの機能も、入力検証 → 権限確認 → 業務処理 → データ保存 → 利用者への応答という同じ基本の流れを持っています。
面接では、機能をたくさん知っていることよりも、要件を確認し、規模を数値化し、中心経路を一貫して設計し、障害と安全性を先回りして説明し、規模に応じてトレードオフを選べることが重要です。この文書を使うときは、各章を暗記するのではなく、自分の言葉で図を描き、仮定を置き、計算し、面接官の深掘りに合わせて一つの論点を掘り下げる練習をしてください。
- ブラウザは API だけを呼び、認証・所有者確認・業務ルールはすべてサーバー側の境界で守る。
- 永続データは PostgreSQL、セッションや生成イベントなどの共有状態は Redis に置き、役割を混ぜない。
- LLM 出力・外部検索・添付資料は「命令」ではなく「データ」として扱い、検証を通ったものだけを画面へ渡す。
- スケールの最初の分離点は生成ワーカー。容量見積もり・SLO・障害時の縮退を先に決めてから増やす。

