在聊天中實現圖片上傳與 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,單文件 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 和 imageIdconst parsed = parseHagiImageUrl("hagiimag://session-abc123/20260301-143022-uuid");// 返回: { sessionId: "session-abc123", imageId: "20260301-143022-uuid" }
// 構建預覽 URLconst previewUrl = buildPreviewUrl(parsed.sessionId, parsed.imageId);// 返回: "/api/Images/session-abc123/20260301-143022-uuid/content"通過這兩個工具函數,前端可以輕鬆地在 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 的請求大小限制,防止惡意的大文件上傳。畢竟在網絡世界裡,小心一點總是沒錯的。
注意事項
在實現過程中,有一些細節需要特別注意:
權限校驗:圖片訪問必須驗證用戶身份,確保只能訪問自己會話的圖片。這是一個基本的安全要求,不能省略。安全這東西,不怕一萬就怕萬一。
路徑安全:嚴格驗證 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 免费提供。打开商店即可安装并保持更新;也可以先对比各版本与定价,再决定从哪个渠道开始。