この記事はHubble Advent Calendar 2025の18日目の記事です。
はじめに
株式会社Hubbleでバックエンドを担当している @ktrk-wks です。
弊社では現在、契約AIエージェント「Contract Flow Agent」という、業務の進行や意思決定を支援する機能を提供しています。これらは「エージェント」として設計されており、最初から非同期処理や複雑なステート管理を前提にアーキテクトされています。
しかし一方で、世の中の多くのプロジェクトや、ふとした会話で出てくるのはもっと「素朴な」要望です。
「この業務システムの管理画面、流行りの生成AIで便利にしといてよ。API繋ぐだけでしょ?」
上司やクライアントからこう言われて、「まあRails(やLaravel/Spring)の既存コントローラーでOpenAIのAPI叩けば一瞬か」と安請け合いした経験、ありませんか?
それは数日後に、
- 終わらないローディング: スピナーが回り続け、Gateway Time-out (504) で落ちる
- 整合性の崩壊: 「保存」したはずなのに、AI生成部分だけ空っぽ
- 現場の混乱: 「さっきと同じ入力をしたのに、結果が違うんですけど!」という問い合わせ
が待っています。
これは単なる実装の不備ではありません。2000年代から我々が信じて疑わなかった「Webシステムの常識(同期処理)」が、LLMという異物によって物理的に破壊されているのです。
今回は、長年エンタープライズシステムを見てきた視点から、「なぜいつものUIパターンだと失敗するのか」、そして 「どう設計すれば幸せになれるのか」 をまとめました。
なお、先日このテーマで ゲームエイト・Hubble・mov 3社共催LT大会 〜AIとRailsプロダクト〜 に登壇しました。
LT当日は時間の都合でステッパー1の問題に絞ってお話ししましたが、本記事ではそこで語りきれなかった他のパターンも含めて体系化しています。
そもそも、なぜ「いつもの実装」で壊れるのか?
私たちが慣れ親しんだMVCフレームワークやRDB中心のシステムには、「決定論的システム(Deterministic System)の神話」 という暗黙の前提があります。
- 速い: DBの読み書きはミリ秒、重くても数秒で終わる
- 確実: 同じ入力なら、1bitも狂わず同じ結果が返る
- 白黒はっきり: 成功か失敗(例外)の二択。中途半端な「8割成功」はない
しかし、LLM(確率的システム) は真逆です。
「遅い(秒〜分単位)」「毎回言うことが変わる」「平気で嘘をつく」「たまに機嫌が悪くてエラーになる」という特性を持っています。
この「確率的で揺らぎのあるLLM」を、従来の「1か0かで動く堅牢なシステム」に無理やり押し込むと、以下のアンチパターンが生まれます。
💀 絶対にやってはいけない「5つのアンチパターン」
1. The Blocking Validator(思考を遮る入力チェック)
入力欄からフォーカスが外れた瞬間 (onBlur) に、AIで「不適切な表現チェック」や「校正」を走らせるパターン。
- 何が起きるか: ユーザーは次の項目を入力したいのに、APIのレスポンス待ちでUIが数秒間フリーズします。思考のフローがぶった切られます
- 壊れた前提: 「バリデーションは一瞬で終わる」という前提
2. The Broken Stepper(壊れたステッパー)
ステッパー形式(Step 1 → Step 2...)で、前の入力を元にAIが次ページの下書きを作るパターン。
- 何が起きるか: ユーザーが「戻る」ボタンで前のページを確認し、何も変えずに「次へ」を押しただけで、AIが再生成(ガチャ)を回してしまいます。さっき手動で直した箇所が消えたり、内容が微妙に変わったりして、ユーザーは発狂します
- 壊れた前提: 「同じ操作なら同じ画面が出る(冪等性)」という前提
3. The Atomic Bulk Action(一か八かの一括処理)
一覧画面で50件選択して「AIで一括要約」ボタンを押し、同期的に待つパターン。
- 何が起きるか: 30件目くらいでHTTPタイムアウトします。あるいは「48件成功、2件だけAIエラー」という部分失敗が起きた時、ロールバックもできず、かといって失敗した2件だけをユーザーに伝えるUIもなく、データ不整合の温床になります
- 壊れた前提: 「トランザクションは全成功か全失敗(原子性)」という前提
4. The Deterministic Preview(プレビュー詐欺)
プレビュー結果を保存せず、送信ボタンを押した瞬間にもう一度生成処理を走らせてしまうパターン。
- 何が起きるか: プレビュー生成時と、本番送信時で別々にAPIを叩くと(シード値を固定しない限り)文面が変わります。「確認した内容と違うメールが客に飛んだ」という事故が発生します
- 壊れた前提: 「プレビュー(確認)と実処理(実行)は一致する」という前提
5. The Synchronous Search(待たされる検索)
検索窓に入力するたび、AIが「意図解釈」をして検索結果を返すパターン。
- 何が起きるか: ユーザーは検索条件を少し変えて試行錯誤(連打)したいのに、毎回3秒待たされます。サクサク感が消え、誰も検索しなくなります
- 壊れた前提: 「検索は思考の速度に追従する」という前提
💊 正しくLLMをシステムに組み込む「5つのパターン」
解決の鍵は、「同期(Synchronous)」から「非同期(Asynchronous)と協働(Collaboration)」への頭の切り替えです。
ユーザーを待たせず、AIの不確実性をUIで包み込むためのパターンを紹介します。
1. Draft & Refine(下書きと推敲)
「AIがいきなり正解データを保存する」のをやめさせます。
-
How:
- ユーザーが「AIで下書き作成」を押す
- AIはエディタにテキストを流し込む(まだ保存しない)
- ユーザーがそれを修正・確認し、納得してから「保存」を押す
-
Why:
責任の所在(オーナーシップ)をユーザーに戻します。「AIが勝手にやった」という言い訳をシステム構造レベルで防ぎます
2. Post-Save Enrichment(保存後のあと施工)
「保存ボタン」をブロックしてはいけません。保存は一瞬で終わらせ、AI処理は裏でやりましょう。
-
How:
- 「保存」を押すと即座に「保存しました」と一覧に戻る
- 裏側(Worker)でAIがタグ付けや要約を開始
- 一覧画面には「AI処理中...」のバッジが出て、完了次第 WebSocket 等で更新される
-
Why:
ユーザーの業務フローを止めないためです。結果整合性(Eventual Consistency) を受け入れる設計します
3. Job Queue & Notify(投げっぱなしと部分リカバリ)
多くの開発者が「同期処理でいけるっしょ」と甘く見て、リリース後に痛い目を見るのがこのパターンです。
「ダッシュボードで見守らせる」のもNGです。それはただの豪華な待ち時間です。
-
How:
- Fire & Forget: 一括処理ボタンを押したら、「受け付けました」とだけ伝えて即座にダイアログを閉じる。ユーザーは別の仕事に行ける
- Notification: 終わったら通知センターやメールで知らせる
- Partial Recovery: ここが肝。「成功48件 / 失敗2件」の結果画面を用意し、失敗した2件だけを特定して再実行・修正できるUIを提供する
-
Why:
LLMにおける一括処理は「部分的に失敗する」のが当たり前だからです。システムエラーにせず、「失敗分のケア」を正規の業務フローに組み込む必要があります
4. Streaming Response(ストリーミング応答)
ChatGPTでおなじみのアレです。
-
How:
ローディングスピナーを見せ続けるのではなく、最初の1トークンが生成された瞬間から文字をパラパラ表示する(Server-Sent Events等) -
Why:
処理時間は変わりませんが、体感待ち時間(Perceived Latency) が劇的に減ります。「動いている」という安心感は、UXにおいて機能以上に重要です
5. Optimistic Suggestion(控えめな提案)
AIを「強制的な検閲官」から「横から囁くアドバイザー」に降格させます。イメージとしてはvscodeのGitHub Copilotです。
-
How:
メインの入力欄には干渉せず、サイドパネルやゴーストテキスト(薄い文字)で「こういう書き方はどうですか?」と提案するだけ。採用するかはユーザーの自由(Tabキーやクリック) -
Why:
AIの推論が遅くても、ユーザーの入力作業(メインスレッド)を邪魔しないからです
まとめ
LLMを業務システムに組み込むということは、「DB中心(CRUD)」の設計から「イベント中心(Task-based)」の設計へシフトするということです。
- ユーザーを待たせない(Non-blocking)
- 状態の不整合を許容し、あとで合わせる(Eventual Consistency)
- 失敗を運用でカバーできるUIを用意する(Recoverability)
「API繋げば終わり」という幻想を捨て、これらのパターンを適用することで、初めて「業務で使えるAIシステム」になります。
ぜひユーザーに新しい体験を届けてください!
明日はフロントエンジニアの @moneyan9 です。お楽しみに
-
ウィザードって書いたら最近はステッパーって言うんですよって言われます ↩