チャットでの画像アップロードとAI認識の実装:設計から導入までの完全ソリューション
チャットでの画像アップロードとAI認識の実装:設計から導入までの完全ソリューション
AIインタラクションシステムにおいて、ユーザーが画像をアップロードし、AIが直接認識できるようにするにはどうすればよいでしょうか?実はこの問題にも長く悩まされてきましたが、HagiCodeの実践を通じていくつかのコツをつかむことができました。本日は、カスタムプロトコルの設計からファイルシステムへの保存、さらにフロントエンドとバックエンドを分離したプレビューまで、この画像アップロードと認識ソリューションについてお話しします。完全な技術メモと言えるでしょう。
背景
AIチャットが盛んになっているこの時代、視覚情報はユーザーの意図を伝える重要な媒体です。しかし、従来のチャットシステムの多くはテキスト入力のみをサポートしており、ユーザーが視覚的なコンテキストをAIに直接渡して分析させることができませんでした。これは少々残念なことです。
HagiCodeの開発過程でも同様の課題に直面しました。ユーザーはチャットやメイン意見の作成時に画像をアップロードできず、AIはユーザーのローカル視覚情報にアクセスできず、画像の入力、保存、レンダリングからAIコンテキストへの伝達に至る完全なクローズドループが欠如していました。
実はこれらの問題は大したことではありません。少し時間と忍耐があれば解決できるだけのことです。私たちは完全な画像アップロードと認識フローを設計・実装し、ClaudeなどのAIがユーザーがアップロードしたスクリーンショットを直接認識・分析できるようにしました。次に、このソリューションの実装詳細をゆっくりと説明していきます。
HagiCodeについて
本記事で紹介するソリューションは、HagiCode プロジェクトでの実践経験に基づいています。HagiCodeはオープンソースのAIコードアシスタントプロジェクトで、OpenSpecベースのワークフロー設計を採用し、よりスマートなコーディング体験の提供に取り組んでいます。
分析
技術的課題
実装を開始する前に、直面する主要な課題を整理する必要があります。毕竟「準備をしてから木を切る」のですから。
クロスモジュール協調:画像アップロードは、フロントエンドUI、アップロードサービス、バックエンドAPI、ファイル保存、メッセージ永続化、AI実行マッピングなど、複数のモジュールに関わります。各モジュールには独自の責任とインターフェースがあり、調和のとれた全体ソリューションを設計する必要があります。
保存戦略の選択:画像はデータベースに保存すべきか、それともファイルシステムに保存すべきか?ファイルシステムを選択する場合、ディレクトリ構造をどのように設計するか?既存のOpenSpecワークフローとどのように統合するか?これらは慎重に検討する必要があります。
参照プロトコルの設計:フロントエンドでレンダリング表示でき、AI実行チェーンで正しく解析できる標準的な画像参照方法が必要です。ファイルパスを直接使用する?HTTP URL?それとも専用のプロトコルを設計する?
AI能力の互換性:異なるAIエグゼキュータのマルチモーダルサポートは様々です。一部のエグゼキュータは画像入力をネイティブにサポートしていますが、テキストのみを処理できるものもあります。すべてのエグゼキュータが画像情報を正しく処理できるように、統一されたアダプタ層をどのように設計するか?
設計上の意思決定
十分な議論と検討を経て、私たちは以下の重要な設計決定を行いました。
決定1:ファイルシステム保存
画像をデータベースではなくファイルシステムに保存することを選択しました。ディレクトリ構造は以下の通りです:
<システムルートディレクトリ>/images/<sessionId>/├── <timestamp>-<uuid>.jpg└── <timestamp>-<uuid>.png理由は明確です。実装を簡素化し、データベースの肥大化を回避し、ファイルをAIが直接読み取れるようにするためです。また、画像ファイルは本質的にデータベースに適しておらず、ファイルシステムこそがより自然な選択です。これは本を本棚に置き、ノートに詰め込まないのと同じ道理です。
決定2:カスタムプロトコル hagiimag://
HTTP URLとの競合を回避し、参照のセマンティクスを明確にするために、カスタム画像参照プロトコルを設計しました:
hagiimag://session-abc123/20260301-143022-a1b2c3d4このプロトコルの形式は hagiimag://<sessionId>/<imageId> で、セマンティクスが明確で、解析とルーティングが容易です。この形式を見れば、開発者はすぐにこれが画像参照であり、通常のURLではないことを理解できます。このような設計上の細部への配慮は、時に非常に役立ちます。
決定3:フロントエンドプレビューとAIアクセスの分離
実装過程で、フロントエンドとAIの画像アクセス要件が異なることがわかりました。フロントエンドはHTTP API経由でプレビューする必要がありますが、AIはローカルファイルパスを直接読み取る必要があります。そのため、分離されたアクセス方法を設計しました:
- フロントエンドは
/api/Images/{sessionId}/{imageId}/contentを使用してプレビュー - AIはサーバーサイドで解析されたローカルファイルパスを使用
これにより、セキュリティ(サーバーパスを露出しない)と可用性(ブラウザから直接アクセス可能)の両方が保証されます。セキュリティと可用性は常にバランスが必要なものです。
決定4:即時アップロード戦略
もう一つの重要な決定は、アップロードのタイミングです。ユーザーが画像を選択または貼り付けたときにすぐにアップロードをトリガーし、メッセージ送信時にはアップロードに成功した画像のみを参照することを選択しました。
この方法の利点は、エラーハンドリングを前処理できること、メッセージ送信APIが複雑にならないこと、JSON契約の簡潔性を保てることです。ユーザーは送信前に画像が正常にアップロードされたかどうかを知ることができ、体験も向上します。このような「未雨綢繆(先手必勝)」の設計思想は、多くの場面で適用できるかもしれません。
解決策
アーキテクチャ設計
上記の決定に基づき、以下の全体アーキテクチャを設計しました:
フロントエンド層├── ConversationInputArea ◄─────── useImageAttachmentManager│ │ ││ ├── ファイル選択 ├── 添付ファイル状態管理│ ├── クリップボード貼り付け ├── アップロード/リトライ/削除│ └── 添付ファイルプレビュー └── 画像参照生成│サービス層├── ImageUploadService│ ├── uploadImage() ◄─────── ImagesController│ ├── deleteImage() ││ ├── parseHagiImageUrl() ◄─────── プロトコルリンクの解析│ └── buildPreviewUrl() ││バックエンド層├── ImagesController ◄─────── ImagesDomainService│ │ ││ ├── POST /upload ├── ファイル検証│ ├── GET /{sessionId}/{imageId} ├── 画像保存│ ├── DELETE ├── 画像圧縮│ └── GET /content └── 参照解析│AI実行層├── ImageContentBlock ◄─────── StructuredMessageDomainService│ │ ││ ├── マルチモーダルエグゼキュータ ├── 画像ブロック解析│ └── テキストエグゼキュータフォールバック └── パスヒント生成このアーキテクチャは、フロントエンドからAIへの完全なデータフローを明確に示しています。各層には明確な責任があり、標準インターフェースを通じて相互作用します。良いアーキテクチャとは、各々が自分の役割を果たし、互いに干渉せず、円滑にコミュニケーションをとるものです。
重要なプロセス
画像アップロードプロセス:
- ユーザーがファイル選択またはクリップボード貼り付けで画像を選択
- フロントエンドでファイルタイプとサイズを検証(JPEG/PNG/WEBP/GIF対応、1ファイル10MB)
- アップロードAPIを呼び出し、画像を
/images/{sessionId}/ディレクトリに保存 - APIが
hagiimag://参照とプレビューURLを返す - フロントエンドが添付ファイルバーにプレビューサムネイルを表示し、ユーザーは送信前にプレビュー可能
AI認識プロセス:
- ユーザーが画像参照を含むメッセージを送信
- バックエンドが
hagiimag://プロトコルリンクを解析し、sessionIdとimageIdを抽出 - 画像参照を
ImageContentBlockにマッピング - エグゼキュータの能力に応じて処理方法を選択:
- マルチモーダルエグゼキュータ:構造化された画像入力を渡す
- テキストエグゼキュータ:画像パスヒントにフォールバック
これで完全なクローズドループが完成します。ユーザーが画像をアップロード → AIが画像を認識 → AIが分析結果を返す。このようなスムーズなプロセスは、ユーザーにより良い体験をもたらします。
実践
フロントエンド実装
フロントエンドでは、画像添付ファイルの状態を管理するための専用Hookを提供しています:
import { useImageAttachmentManager } from '@/hooks/useImageAttachmentManager';
function ChatInput() { const { attachments, uploadedImages, hasBlockingAttachments, isUploading, selectFiles, removeAttachment, clearAttachments, } = useImageAttachmentManager({ ownerId: sessionId, mapUploadedImage: (response) => response, uploadOptions: { compress: false }, });
const handleFileSelect = (files: File[]) => { selectFiles(files); };
const handlePaste = (e: ClipboardEvent) => { const files = Array.from(e.clipboardData?.files || []) .filter(f => f.type.startsWith('image/')); if (files.length > 0) { handleFileSelect(files); } };
return ( <div> {/* 添付ファイルバー */} {attachments.map(att => ( <AttachmentItem key={att.localId} file={att.file} status={att.status} onRemove={() => removeAttachment(att.localId)} /> ))}
{/* 入力ボックス */} <textarea onPaste={handlePaste} />
{/* アップロードボタン */} <button onClick={() => fileInputRef.current?.click()}> 画像をアップロード </button> </div> );}このHookは、アップロード状態の追跡、失敗時のリトライ、添付ファイルの削除など、添付ファイル管理のすべてのロジックをカプル化しています。使用するのは非常に簡単で、いくつかのメソッドを呼び出すだけでプロセス全体を完了できます。良いAPI設計とは、シンプルで使いやすく、柔軟性を失わないものです。
カスタムプロトコルの解析:
// カスタムプロトコルからsessionIdとimageIdを抽出const parsed = parseHagiImageUrl("hagiimag://session-abc123/20260301-143022-uuid");// 返す: { sessionId: "session-abc123", imageId: "20260301-143022-uuid" }
// プレビューURLを構築const previewUrl = buildPreviewUrl(parsed.sessionId, parsed.imageId);// 返す: "/api/Images/session-abc123/20260301-143022-uuid/content"この2つのユーティリティ関数により、フロントエンドは hagiimag:// プロトコルとHTTP URLの間で簡単に変換できます。このような変換ロジックをカプセル化しておけば、使用するのははるかに便利になります。
バックエンド実装
バックエンドはASP.NET Coreで実装されており、核となるのは ImagesController と ImagesDomainService です:
[HttpPost("upload")][RequestSizeLimit(50 * 1024 * 1024)]public async Task<ActionResult<ImageUploadResponseDto>> Upload( [FromForm] UploadImageFormRequest input){ // 1. リクエストを検証 if (file == null || file.Length == 0) throw new UserFriendlyException("No file provided");
// 2. ファイルタイプとサイズを検証 var (isValid, errorMessage) = _imagesDomainService.ValidateImage( file.FileName, file.ContentType, file.Length); if (!isValid) throw new UserFriendlyException(errorMessage);
// 3. ファイルシステムに保存 await using var stream = file.OpenReadStream(); var result = await _imagesDomainService.UploadImageAsync( stream, sessionId, file.FileName, file.ContentType, CurrentUserId, compress: input.Compress);
// 4. 結果を返す return Ok(result);}この実装は、典型的なWeb API開発パターンに従っています。検証、処理、返却です。ここで注意すべき点は、悪意のある大ファイルアップロードを防ぐために、50MBのリクエストサイズ制限を設定していることです。インターネットの世界では、少し注意するに越したことはありません。
注意事項
実装過程では、いくつかの詳細に特に注意する必要があります:
権限検証:画像アクセスではユーザーIDを検証する必要があります。自分のセッションの画像にしかアクセスできないようにするのです。これは基本的なセキュリティ要件であり、省略できません。セキュリティは「万が一」のために備えるものです。
パスセキュリティ:sessionId と imageId を厳密に検証し、パストラバーサル攻撃を防ぐ必要があります。例えば、../ を含むパスを拒否し、ユーザーがシステム内の任意のファイルにアクセスするのを防ぎます。このような境界条件を適切に処理してこそ、システムはより堅牢になります。
ファイルクリーニング:セッション削除時に、関連画像を同期的にクリーンアップし、孤児ファイルの蓄積を回避する必要があります。長期間運用後、これらのファイルが大量のディスク容量を占有する可能性があります。タイムリーなクリーニングも良い習慣です。
圧縮戦略:スクリーンショットのようなファイル名(例:screenshot.png)の場合、スペースを節約するために自動的に圧縮を有効にします。この戦略は実際のニーズに応じて調整できます。ストレージスペースは、節約できるものは節約するのが良いでしょう。
フォールバック処理:マルチモーダルをサポートしないエグゼキュータは、画像パスヒントを受け取る必要があり、画像情報をサイレントに破棄してはいけません。これは重要です。そうでなければ、ユーザーはAIが自分の画像を無視したと思ってしまいます。ユーザー体験は細部が決め手です。
状態管理:アップロード中の添付ファイルはメッセージ送信をブロックし、失敗した添付ファイルはリトライまたは削除を許可します。この設計により、ユーザー体験の一貫性が保証されます。状態管理が明確であれば、ユーザーは混乱しません。
まとめ
この完全な画像アップロードと認識ソリューションにより、HagiCodeはユーザー入力からAI認識までの完全なクローズドループを実現しました。ソリューション全体のコアハイライトは以下の通りです:
- カスタム
hagiimag://プロトコルにより、画像参照の標準化を実現 - ファイルシステム保存により、実装を簡素化しパフォーマンスを向上
- フロントエンドプレビューとAIアクセスの分離により、セキュリティと可用性の両立
- 即時アップロード戦略により、ユーザー体験を最適化
- マルチモーダルとテキストフォールバックの互換設計により、柔軟性を確保
このソリューションはHagiCodeで安定して動作しており、ユーザーからのフィードバックも良好です。同様の機能を実装している場合、これらの経験が役立つことを願っています。
技術ソリューションには絶対的な正誤はなく、自分のプロジェクトに適しているかどうかだけです。自分のプロジェクトに適した道を見つけることが最も重要なのです。
参考資料
- HagiCode GitHub: github.com/HagiCode-org/site
- HagiCode 公式サイト: hagicode.com
- OpenSpec ワークフロー ドキュメント: docs.hagicode.com
开始使用 HagiCode
一次安装,几分钟上手
HagiCode for Windows 在 Microsoft Store 免费提供。打开商店即可安装并保持更新;也可以先对比各版本与定价,再决定从哪个渠道开始。