:從架構(gòu)設(shè)計到模型調(diào)度與踩坑記錄)
“又一個新項目完結(jié)”——這句話說出口的時候我終于能把“全棧 AI 修圖 Agent”從待辦列表里劃掉了。這個項目從立項到交付前后差不多一個多月期間推翻過一版架構(gòu)也踩了不少模型和前后端的坑。如果你最近也在折騰 AI 全棧項目或者想看看 Agent 工程化到底怎么落地這篇博文應(yīng)該能給你省下不少時間。一句話介紹這個項目它是一套基于自然語言的修圖系統(tǒng)。用戶上傳一張圖輸入一句“幫我把背景換掉順便調(diào)亮一點(diǎn)”系統(tǒng)里的 AI Agent 會自動理解意圖、拆分任務(wù)、調(diào)度模型、執(zhí)行修圖最后返回成圖。整個系統(tǒng)覆蓋 Web 端和移動端前端用了 Vue3 和 Uniapp后端用的 Golang中間夾了一層 Agent 任務(wù)編排與模型網(wǎng)關(guān)。本文會把項目的設(shè)計思路、技術(shù)選型、核心實現(xiàn)和踩坑記錄全部拆開來講適合想獨(dú)立做出一個 AI 全棧作品的開發(fā)者參考。1. 項目整體設(shè)計與需求拆解1.1 這個項目到底想解決什么問題傳統(tǒng)修圖軟件的學(xué)習(xí)成本很高。想給一個非專業(yè)人士解釋清楚“圖層、蒙版、選區(qū)、曲線”實在太難了。哪怕是我自己遇到批量老照片修復(fù)或者一鍵換背景也不可能每張圖都去 Photoshop 里手動操作。所以這個項目的核心定位非常明確用自然語言把修圖意圖說清楚剩下的交給 Agent 去拆解和執(zhí)行。舉一個最能說明問題的例子。用戶輸入“把照片的背景換成秋天的銀杏葉”傳統(tǒng)的做法是什么樣的需要把人像摳出來、生成新的背景、再做人像與背景的融合、最后統(tǒng)一色調(diào)。這一套流程在 Photoshop 里至少涉及四五個環(huán)節(jié)。而在我們的系統(tǒng)里Agent 會把這句話拆成“語義分割人像、生成銀杏背景、圖像融合、色彩匹配”四個子任務(wù)再依次調(diào)用對應(yīng)的模型工具最終合成一張成品圖。用戶不需要理解任何技術(shù)細(xì)節(jié)只需要表達(dá)“要什么效果”。這就是 Agent 和普通接口的本質(zhì)區(qū)別。普通接口是“請求-處理-返回”的單次服務(wù)。Agent 則是一個帶循環(huán)判斷的系統(tǒng)接收用戶需求、調(diào)度工具、檢查結(jié)果、決定是否繼續(xù)調(diào)整直到任務(wù)完成或者達(dá)到輪次上限。1.2 核心功能與能力邊界在實際開發(fā)之前我先把功能邊界畫了一個表確定了第一版必須支持和可以延后的部分。這里直接把我們項目最初的功能清單分享出來功能模塊具體能力優(yōu)先級圖片上傳與存儲支持 JPG/PNG/WebP統(tǒng)一壓縮到 2048pxOSS 存儲必須人像美化磨皮、美白、瘦臉等參數(shù)化操作必須背景處理摳圖、背景替換、背景虛化必須老照片修復(fù)去噪、上色、超分必須色彩調(diào)整提亮、加濾鏡、色溫調(diào)整必須畫面擴(kuò)展以圖生圖方式擴(kuò)展畫面外區(qū)域延后批量處理一次提交多張圖延后風(fēng)格模板一鍵套用某種預(yù)設(shè)風(fēng)格延后第一版不做批量處理是我反復(fù)權(quán)衡以后的決定。原因很簡單Agent 帶多輪工具調(diào)用的場景里單張圖的執(zhí)行鏈路已經(jīng)很復(fù)雜如果再加上批量任務(wù)的并發(fā)編排問題排查難度會成倍增加。先跑通單張閉環(huán)再往橫向擴(kuò)展是這一類項目的穩(wěn)妥節(jié)奏。1.3 Agent 修圖與傳統(tǒng)修圖的架構(gòu)差異很多剛接觸 Agent 開發(fā)的人會陷入一個誤區(qū)以為 Agent 就是在普通后端接口前面加一層大模型接口用戶說什么直接轉(zhuǎn)給大模型大模型返回什么就展示什么。但真正做過以后你會發(fā)現(xiàn)修圖場景完全不是這么回事。一張圖的修圖結(jié)果沒辦法在文本對話里直接返回大模型能做的只是“決定該做什么”具體的圖像處理必須由后端去調(diào)模型完成。所以我們的架構(gòu)里Agent 扮演的不是“回答者”而是“調(diào)度中樞”。它接收用戶需求輸出結(jié)構(gòu)化的工具調(diào)用指令后端拿到指令去執(zhí)行真實的圖像處理執(zhí)行完再把結(jié)果摘要回傳給 Agent讓它判斷任務(wù)是否結(jié)束或者下一步該做什么。這個設(shè)計直接決定了后續(xù)數(shù)據(jù)庫要怎么設(shè)計、接口要怎么通信、前端狀態(tài)要怎么管理??梢哉f所有在開發(fā)過程中讓人頭疼的問題最終都指向同一個根因這是一個異步、多步驟、帶狀態(tài)的 AI 工作流而不是一個同步的查詢接口。2. 技術(shù)選型與整體架構(gòu)設(shè)計2.1 前端為什么選 Vue3 和 Uniapp前端選擇 Vue3首先是因為團(tuán)隊技術(shù)棧本來就是 Vue后面接手項目的人不至于看不懂。另外一個更實際的原因修圖工作臺的界面狀態(tài)非常復(fù)雜畫布區(qū)、參數(shù)面板、歷史記錄、指令輸入框之間需要頻繁跨組件通信。Vue3 的組合式 API 在處理這類復(fù)雜交互時代碼組織形式比 Vue2 的 Options API 清晰得多。移動端選擇 Uniapp考慮的是投入產(chǎn)出比。我們的目標(biāo)不只是做 Web 端還希望用戶能在手機(jī)上隨手拍一張圖直接處理。Uniapp 一套代碼能同時輸出 H5 和原生 App雖然性能上不如真正的原生開發(fā)但對于圖片展示和表單交互這種場景已經(jīng)完全夠用。更關(guān)鍵的是uniapp 對 Vue 語法的兼容度很高Web 端的一些邏輯代碼甚至可以直接復(fù)制到移動端使用省了非常多的重復(fù)工作量。這里要補(bǔ)充一個經(jīng)驗如果你只有一個前端同學(xué)想做多端Uniapp 是合理選擇但如果是重交互的圖片編輯器建議 Web 端還是單獨(dú)用 Canvas 和 WebGL 做好基礎(chǔ)再考慮遷移。我們第一版把大部分復(fù)雜操作放在 Web 端移動端只做上傳、預(yù)覽和結(jié)果展示就是為了避開移動端 Canvas 性能的坑。2.2 后端為什么選擇 Golang后端選型我其實考慮過 Node.js、Python 和 Golang 三個方向。Node.js 的好處是前端同學(xué)能直接上手但我們的任務(wù)編排層涉及大量并發(fā)調(diào)度我希望有一個更強(qiáng)的并發(fā)模型。Python 在 AI 生態(tài)里確實無敵但部署要帶運(yùn)行環(huán)境和一堆依賴維護(hù)起來并不輕松。最后敲定 Golang理由有三個。第一Goroutine 的并發(fā)模型對 AI 任務(wù)調(diào)度非常友好。一個修圖 Agent 任務(wù)會拆成多個子任務(wù)其中一些子任務(wù)之間是可以并行的比如同時做人像摳圖和色彩分析。用 goroutine 做并行調(diào)度比起線程池和協(xié)程都更直觀。第二Golang 編譯出來就是單個二進(jìn)制文件部署到服務(wù)器沒有任何環(huán)境依賴問題。我們的 AI 模型部分雖然跑在獨(dú)立的 GPU 機(jī)器上但服務(wù)端只需要通過 HTTP 調(diào)用模型接口所以 Golang 并不影響 AI 能力的接入。第三Gin 框架提供了非常簡潔的路由和中間件機(jī)制SSE 流式輸出的實現(xiàn)也足夠穩(wěn)定。我們整個后端只用了 Gin、GORM 和 SSE 相關(guān)的幾個標(biāo)準(zhǔn)庫沒有引入額外重框架。2.3 AI 模型層本地模型與云端 API 混合路線模型層的設(shè)計是這個項目里最考驗取舍的部分。最開始我考慮過全部接入開源模型自部署比如 Stable Diffusion 和 GFPGAN。但實際跑下來發(fā)現(xiàn)不同子任務(wù)的模型效果差異極大。有些開源小模型在老照片修復(fù)上效果根本不夠看而商業(yè) API 的表現(xiàn)則是開箱即用。最終我采用了混合路線主控 Agent意圖識別、工具參數(shù)抽取使用商用大模型 API因為它的指令理解和 JSON 結(jié)構(gòu)化輸出能力穩(wěn)定這是整個鏈條里最不能出錯的部分。圖像生成和編輯類任務(wù)使用開源 Stable Diffusion 系列模型部署在自己的 GPU 服務(wù)器上因為這類操作請求量大走本地模型可以控制成本。一些專項能力比如人像深度摳圖、老照片修復(fù)用的是商業(yè) API。這類任務(wù)對結(jié)果質(zhì)量要求高開源小模型很難達(dá)到同一水平。這套混合架構(gòu)的核心是一個模型網(wǎng)關(guān)層。后端服務(wù)不關(guān)心具體調(diào)的是本地服務(wù)還是外部 API只面向一個統(tǒng)一的接口封裝。模型網(wǎng)關(guān)根據(jù)任務(wù)類型和當(dāng)前服務(wù)器負(fù)載把請求路由到對應(yīng)的模型實例。這給項目帶來了兩個直接好處一是某個模型掛了不會影響整個鏈路二是后續(xù)替換某個子任務(wù)的模型時不需要改動上層邏輯。2.4 整體架構(gòu)分層整個系統(tǒng)從邏輯上可以分成五層展示層Vue3 Web 管理端、Uniapp 移動端接入層Golang 后端提供 REST 接口和 SSE 流式推送Agent 編排層負(fù)責(zé)意圖理解、任務(wù)拆分、工具調(diào)用、結(jié)果驗證工具執(zhí)行層每個修圖能力都封裝成一個獨(dú)立工具模型網(wǎng)關(guān)層統(tǒng)一調(diào)度本地模型和商業(yè) API這里最關(guān)鍵的一點(diǎn)是 Agent 編排層和工作流運(yùn)行時的邊界要清楚。Agent 只輸出“決策結(jié)果”而工作流運(yùn)行時負(fù)責(zé)執(zhí)行、狀態(tài)管理和錯誤處理。如果沒有這個邊界所有邏輯都塞在 Agent 的對話循環(huán)里后期幾乎沒法維護(hù)。3. Agent 核心工作流的落地實現(xiàn)3.1 給 Agent 建立“能力說明書”一個修圖 Agent 首先要讓大模型知道自己有什么工具、能做什么事、不該做什么事。這部分我通過系統(tǒng)提示詞來實現(xiàn)。Project 的 System Prompt 大致是如下結(jié)構(gòu)你是一個修圖任務(wù)調(diào)度管家負(fù)責(zé)將用戶的修圖需求拆解為可執(zhí)行的工具調(diào)用序列。 你擁有以下工具 1. remove_background(image_id, output_format)移除圖片背景 2. replace_background(image_id, background_prompt)替換圖片背景 3. face_retouch(image_id, smooth_level, whitening_level)人像美化 4. restore_photo(image_id)老照片修復(fù) 5. adjust_color(image_id, brightness, contrast, saturation)色彩調(diào)整 ... 要求 1. 用戶需求無法用現(xiàn)有工具實現(xiàn)時直接返回 error 說明原因 2. 一次請求可以調(diào)用多個工具工具之間按執(zhí)行順序排列 3. 結(jié)果滿意后必須返回 done不要重復(fù)執(zhí)行已完成的步驟這套提示詞的核心是“給模型畫清邊界”。在測試過程中我明顯感覺到提示詞里如果沒有明確說明“不要重復(fù)執(zhí)行已完成的步驟”模型就很容易在第一步執(zhí)行完后繼續(xù)重復(fù)相同的工具調(diào)用白白浪費(fèi)時間和算力。3.2 讓大模型穩(wěn)定輸出工具調(diào)用參數(shù)Agent 和工具層之間的通信必須用結(jié)構(gòu)化的方式不能直接讓模型輸出自然語言否則下游解析會非常痛苦。我們要求大模型始終返回一個 JSON 對象結(jié)構(gòu)固定如下{ thought: 用戶需要替換背景并調(diào)亮我先執(zhí)行背景替換, tool_calls: [ { tool_name: replace_background, arguments: { image_id: img_10001, background_prompt: autumn gingko leaves } }, { tool_name: adjust_color, arguments: { image_id: img_10001, brightness: 1.2 } } ], is_finished: false }這里的is_finished是一個重要字段。模型完成所有工具調(diào)用后要把這個字段置為 true服務(wù)端看到 true 才會結(jié)束循環(huán)。為了提升 JSON 解析的成功率我把大模型接口的 temperature 參數(shù)刻意調(diào)低到 0.2 以內(nèi)同時對返回內(nèi)容做了一層清洗處理去掉 Markdown 代碼塊標(biāo)記、修正未閉合的花括號。還有一個容易踩的坑是圖像處理的中間結(jié)果管理。多個工具需要串行處理同一張圖片時每次執(zhí)行的輸入可能是上一個工具的輸出。因此我們不能讓每個工具都從原始圖片開始處理而是在運(yùn)行時維護(hù)一個“當(dāng)前圖片”狀態(tài)。比如上面對話的 JSON 里第二次調(diào)用的參數(shù)還是 image_id但實際在工具執(zhí)行層該 id 對應(yīng)的圖片地址已經(jīng)被第一次的工具調(diào)用結(jié)果覆蓋了。3.3 工具注冊與動態(tài)調(diào)度機(jī)制為了讓 Agent 的“能力說明書”和實際的工具代碼保持一致我實現(xiàn)了一套簡單的工具注冊機(jī)制。每個工具都是一個結(jié)構(gòu)體type Tool interface { Name() string Description() string Parameters() map[string]interface{} Execute(ctx *ToolContext, args map[string]interface{}) (*ToolResult, error) }系統(tǒng)啟動時所有工具會注冊到一個工具表里。注冊的同時會把工具的名字、描述、參數(shù) Schema 自動拼接到 System Prompt 中。這樣做的好處是以后新增一個修圖能力只需要實現(xiàn) Tool 接口然后在啟動入口注冊一下Agent 下次對話時就會自動感知到新工具的存在不需要手動修改提示詞。動態(tài)調(diào)度的時候通過工具名從注冊表找到對應(yīng)的 Tool 實例參數(shù)則從 Agent 返回的 JSON 中抽取。有一個經(jīng)驗值得記錄參數(shù)校驗一定要做兩層。第一層是 Agent 層的 Schema 校驗第二層是工具內(nèi)部的類型斷言。因為大模型偶爾會返回brightness: 1.2這種字符串類型的數(shù)字如果工具內(nèi)部不轉(zhuǎn)類型程序就會直接 panic。3.4 多輪工具循環(huán)的狀態(tài)管理一個修圖需求往往需要好幾輪工具調(diào)用。比如“把背景換掉順便調(diào)亮”這個需求如果讓 Agent 一次完成其實只需要一輪。但如果是“把背景換掉、調(diào)亮、再把人的皮膚磨一下”這種需求Agent 可能需要兩到三輪才能把參數(shù)逐步確認(rèn)好。整個循環(huán)的代碼結(jié)構(gòu)是經(jīng)典的 while state update 模式for i : 0; i maxIterations; i { currentState : buildState(conversation, currentImageURL) llmResponse : callAgent(systemPrompt, currentState) if llmResponse.IsFinished { break } for _, call : range llmResponse.ToolCalls { tool : registry.Get(call.ToolName) result : tool.Execute(ctx, call.Arguments) appendToolResult(conversation, result) } }循環(huán)跳出條件有兩個一是模型返回is_finished: true二是達(dá)到最大輪次上限。我給默認(rèn)值設(shè)成了 5 輪超過 5 輪直接返回當(dāng)前結(jié)果。這個上限非常重要沒有它Agent 在某些奇怪輸入下會一直循環(huán)調(diào)工具最終超時或燒光 token。3.5 結(jié)果是不是真的符合預(yù)期Agent 修圖還有一個很容易被忽視的問題怎么判斷結(jié)果“成功了”。之前我以為這一步很順理成成實際開發(fā)時才發(fā)現(xiàn)圖像處理工具到底有沒有成功不能只看模型返回的 JSON 里success: true。有些模型返回成功但生成的結(jié)果圖可能是黑圖、糊圖或者與原圖毫無關(guān)系。所以我在工具執(zhí)行層加了一個基礎(chǔ)質(zhì)量校驗?zāi)K對所有模型輸出的圖片做三項檢查寬度和高度是否大于預(yù)設(shè)閾值、圖片文件字節(jié)數(shù)是否低于異常下限、清晰度評分是否超過基礎(chǔ)線。任何一項不通過就會觸發(fā)一次自動重試或者直接讓 Agent 判定當(dāng)前工具執(zhí)行失敗。這個設(shè)計確實救過我們一次。有一版我們在測試背景替換時某個場景的模型輸出偶爾會出現(xiàn)純色填充但接口本身是 200 狀態(tài)。如果沒有質(zhì)量校驗用戶看到的就會是一張全黑圖而且系統(tǒng)還認(rèn)為任務(wù)完成了。4. 全棧實現(xiàn)與多端聯(lián)調(diào)過程4.1 后端接口設(shè)計與 SSE 任務(wù)推送后端的核心接口大致分三組。第一組是會話管理接口負(fù)責(zé)創(chuàng)建會話和查詢歷史記錄第二組是文件接口負(fù)責(zé)圖片上傳和臨時 URL 生成第三組是任務(wù)接口負(fù)責(zé)提交 Agent 任務(wù)和接收進(jìn)度推送。任務(wù)提交通道我最終選擇了 SSEServer-Sent Events方案。原因很直接Agent 任務(wù)的處理時間通常需要 10 到 30 秒甚至更久用戶需要實時看到“正在理解需求”“正在執(zhí)行背景替換”“正在調(diào)整色調(diào)”這樣的中間狀態(tài)。如果只用輪詢不僅浪費(fèi)請求資源體驗也差了很多。SSE 的推送格式設(shè)計成了事件流。前端通過 EventSource 連接后后端按階段推事件event: stage data: {stage: understanding, message: 正在理解您的修圖需求} event: stage data: {stage: tool_executing, tool: replace_background, message: 正在執(zhí)行背景替換} event: stage data: {stage: image_ready, image_url: https://cdn.xxx.com/result.jpg}前端只需要監(jiān)聽不同的事件類型驅(qū)動頁面上狀態(tài)卡片和圖片容器的更新。這里有個后端細(xì)節(jié)要注意SSE 的響應(yīng)頭必須設(shè)成Content-Type: text/event-stream并且關(guān)閉代理層的緩沖。我們第一次上線時Nginx 默認(rèn)開了緩沖SSE 消息全部攢著不發(fā)前端一直停留在“理解中”排查了很久才找到原因。4.2 Web 端交互工作臺的實現(xiàn)Web 工作臺的界面不算復(fù)雜但狀態(tài)管理很容易亂。我把頁面拆成了四個核心模塊左側(cè)圖片列表展示當(dāng)前會話里所有上傳圖片和生成結(jié)果中間畫布區(qū)展示當(dāng)前編輯的圖片支持縮放和比例標(biāo)注右側(cè)工具信息面板展示 Agent 執(zhí)行的工具序列和每一步的執(zhí)行結(jié)果底部指令輸入框接收用戶自然語言修圖指令支持語音轉(zhuǎn)文字狀態(tài)管理使用 Pinia。整個會話上下文全部放在 store 里包括 session_id、image_list、execution_trace、is_processing 等字段。每次 SSE 事件到達(dá)時通過 store 的 action 統(tǒng)一更新狀態(tài)。組件層只負(fù)責(zé)渲染不做數(shù)據(jù)邏輯。組件通信層級要注意一個細(xì)節(jié)Agent 的執(zhí)行軌跡execution_trace是一個數(shù)組包含每一步的 tool_name、status、耗時、輸出圖片 URL。這個數(shù)組要始終在頁面上可見這樣用戶才能理解 AI 為什么做了某些操作。如果只展示最終結(jié)果用戶會對“AI 是不是偷偷改了別的地方”產(chǎn)生疑慮這類工具需要的是過程和結(jié)果都透明。4.3 移動端 Uniapp 的適配難點(diǎn)移動端用 Uniapp 實現(xiàn)功能上做了精簡只保留了上傳圖片、發(fā)送指令、查看執(zhí)行進(jìn)度、查看結(jié)果、保存到相冊這幾個操作。核心業(yè)務(wù)邏輯從 Web 端抽取到公共的 JS 模塊里兩個端共享。實際開發(fā)中主要遇到兩類差異。第一是文件上傳方式。Web 端可以直接用 FormData 上傳 Blob移動端則需要用 uni.uploadFile涉及臨時文件路徑的轉(zhuǎn)換。第二是圖片預(yù)覽方式。Web 端可以渲染任意 CDN 地址的圖片但移動端對某些 URL 有跨域限制需要統(tǒng)一走代理或加時間戳簽名。我選用的做法是所有端前端在調(diào)用 Agent 之前都會對圖片做一次前端預(yù)壓縮統(tǒng)一將最長邊壓縮到 2048 像素內(nèi)。這樣既減小了上傳體積也避免了同一個圖片在不同端處理時尺寸不一致導(dǎo)致的 Agent 判斷偏差。4.4 數(shù)據(jù)庫表設(shè)計和狀態(tài)流轉(zhuǎn)數(shù)據(jù)庫用的是 MySQL一共有五張核心表用戶、會話、消息、圖片、任務(wù)記錄。這里重點(diǎn)說下任務(wù)表和消息表的設(shè)計。任務(wù)表記錄了每次 Agent 執(zhí)行的整體信息核心字段包含task_id、session_id、status、current_stage、error_message、completed_at。剛開始我并沒有單獨(dú)設(shè)計任務(wù)表而是把執(zhí)行狀態(tài)直接存在會話表里。但很快就發(fā)現(xiàn)一個問題一個會話可能包含多輪修圖操作比如用戶先讓人像美顏再讓改背景這兩個是不同的任務(wù)如果狀態(tài)都寫在會話上就會出現(xiàn)互相覆蓋。消息表的作用是保留 Agent 的完整對話記錄也就是每條用戶指令和 Agent 的中間決策過程。它有一個 tool_logs 字段類型是 JSON會記錄每一輪工具調(diào)用時的輸入?yún)?shù)和返回結(jié)果。開發(fā) Agent 類項目時沒有這些日志幾乎沒法調(diào)試你很難知道 Agent 當(dāng)時為什么要做某個操作。狀態(tài)流轉(zhuǎn)上我定義了一套完整的狀態(tài)機(jī)PENDING等待受理→ RUNNINGAgent 執(zhí)行中→ SUCCEEDED完成 PENDING → RUNNING → FAILED失敗任務(wù)超過 60 秒沒有任何狀態(tài)更新時服務(wù)端會強(qiáng)制把狀態(tài)置成 FAILED 并向客戶端推送超時事件。這是為了防止極端情況下的假死連接。4.5 聯(lián)調(diào)過程中如何做端到端驗證端到端驗證這個環(huán)節(jié)我建議做成自動化腳本不要每次靠人工點(diǎn)頁面測。我們寫了一個小的 Go 腳本直接模擬客戶端流程上傳圖片、創(chuàng)建會話、提交任務(wù)、接受 SSE 事件、校驗最終圖片 URL 是否返回 200。跑通之后再打開頁面驗證視覺交互。實際聯(lián)調(diào)時最容易忽略的是圖片在跨環(huán)境傳輸后的狀態(tài)。同一張圖在本地測試是正常的但上傳到 OSS 之后可能因為訪問權(quán)限、防盜鏈、CORS 配置等原因?qū)е?Agent 后端無法正常拉取圖片。所以我在服務(wù)端專門寫了一個“圖片預(yù)檢”環(huán)節(jié)每次 Agent 收到圖片 URL 后會先做一次 HEAD 請求確認(rèn)可訪問如果不可訪問就直接失敗并提示重新上傳。5. 常見問題與排查技巧實錄5.1 Agent 無限循環(huán)調(diào)用工具怎么辦這個我前面提到過總輪次上限但實際項目中還有一種更隱蔽的情況雖然單輪不會卡死但模型會在相鄰的兩輪之間反復(fù)橫跳。比如先調(diào)用了一次背景替換發(fā)現(xiàn)結(jié)果不滿意又調(diào)用了一次恢復(fù)原圖的工具然后再次調(diào)背景替換形成一個死循環(huán)。我在提示詞和代碼里做了雙重防護(hù)。提示詞里增加了“如果你發(fā)現(xiàn)自己即將重復(fù)執(zhí)行已經(jīng)做過的操作說明當(dāng)前工具結(jié)果不滿足預(yù)期應(yīng)該直接返回 error 或 done”。代碼里則增加了“工具調(diào)用指紋”去重如果 Agent 在最近 3 輪里對同一個圖片、同一個工具、相似參數(shù)執(zhí)行了 2 次系統(tǒng)會自動攔截并詢問是否需要中止。5.2 SSE 連接總是掛斷進(jìn)度推送不穩(wěn)定SSE 掛斷的問題后來定位出來原因有三層。第一層是需要心跳機(jī)制。代理層如果沒有數(shù)據(jù)流動空閑連接會被斷開。所以我每 15 秒發(fā)一條event: heartbeat的注釋消息保持連接活躍。第二層是代理層的緩沖。必須在中間件或 Nginx 明確關(guān)閉 response buffering否則數(shù)據(jù)到達(dá)不了瀏覽器。第三層是內(nèi)存泄漏。有些版本的 Golang SSE 庫在廣播事件時如果不處理客戶端斷開的 channel會導(dǎo)致 goroutine 堆積。穩(wěn)定以后我把整條 SSE 推送鏈路做了本地壓力測試。模擬 50 個并發(fā)連接同時在線觀察 10 分鐘確認(rèn)沒有 goroutine 泄漏后才放心上線。5.3 大模型返回 JSON 偶爾解析失敗解析失敗是 Agent 開發(fā)中最常見的坑。大模型返回的內(nèi)容里偶爾會混入額外的解釋文字或者 JSON 外層包了 Markdown 代碼塊。最開始我直接對原字符串做 json.Unmarshal失敗率很高后來做了三層解析容錯。第一層是做字符串清洗去掉所有 Markdown 標(biāo)記和第一個花括號之前的所有字符。第二層是嘗試自動修復(fù)比如未閉合的數(shù)組和對象標(biāo)記。第三層是解析失敗時把錯誤信息回傳給大模型讓它自己修正輸出。這一步效果非常明顯連續(xù)重試兩次后解析失敗率幾乎降到了零。有一個值得注意的細(xì)節(jié)給大模型回傳錯誤信息時要明確告訴它“上一次的返回中包含了多余的文本請只返回 JSON不要其他內(nèi)容”否則它很容易繼續(xù)沿襲上一次的錯誤格式。5.4 圖片方向、格式和 EXIF 問題的坑手機(jī)上傳的圖片普遍存在 EXIF 反轉(zhuǎn)問題。就是說用戶拍照時手機(jī)是橫著的但圖片本身經(jīng)過旋轉(zhuǎn)處理后才正常顯示。如果后端拿到圖片后直接做處理出來的結(jié)果可能是橫的或者顛倒的。這個問題在 Web 端偶爾出現(xiàn)在移動端幾乎百分百觸發(fā)。后來我在所有圖像工具的第一道處理里加了一個“圖像標(biāo)準(zhǔn)化”流程讀取 EXIF 信息、根據(jù) Orientation 字段做旋轉(zhuǎn)矯正、統(tǒng)一轉(zhuǎn)成 RGB 三通道、刪除透明通道、將最長邊縮放到 2048px。做完標(biāo)準(zhǔn)化之后所有下游模型拿到的都是規(guī)格一致的圖片模型因為輸入尺寸不一致引發(fā)的隨機(jī)問題少了很多。5.5 不同模型對輸入和輸出的要求不一致本地 Stable Diffusion 模型和商業(yè) API 的輸入限制差異很大。有的模型要求圖片最小邊長不低于 512px有的模型又限制最大解析度。圖像標(biāo)準(zhǔn)化流程解決了大部分問題但還是有極少數(shù)圖片處理完以后生成的圖片分辨率過低。我們在模型網(wǎng)關(guān)層為每個模型配置了“輸入約束”和“輸出約束”。網(wǎng)關(guān)在調(diào)用模型前會根據(jù)約束做一次檢查比如判斷當(dāng)前圖片的寬度是否低于模型最低要求低了就用智能超分先放大確保模型輸入符合預(yù)期。5.6 移動端真機(jī)測試時才發(fā)現(xiàn)的內(nèi)存暴漲問題這個坑是在移動端出現(xiàn)的。Uniapp 在真機(jī)上連續(xù)處理幾張 2048px 的大圖時內(nèi)存占用一路飆升最后直接白屏。排查發(fā)現(xiàn)是在圖片預(yù)覽組件里沒有釋放歷史圖片資源。后來我們把移動端的圖片預(yù)覽全部改為懶加載模式并且每次切換圖片時手動回收不再可見的圖片對象。真機(jī)調(diào)試這個事情經(jīng)驗就是一定要準(zhǔn)備一臺中低端 Android 手機(jī)做測試很多性能問題在高端機(jī)上根本復(fù)現(xiàn)不出來。最后的一點(diǎn)體會這個項目最大的收獲不是模型調(diào)得多好而是深刻理解了 Agent 工程化到底是做什么模型負(fù)責(zé)理解和生成但工程負(fù)責(zé)邊界、容錯和確定性。用戶能不能穩(wěn)定地用上這個功能取決于你給 Agent 畫了多清晰的邊界、做了多完善的兜底而不是模型本身有多聰明。最后分享一個個人偏好開發(fā)這種帶狀態(tài)和循環(huán)的系統(tǒng)一定要把所有 Agent 決策日志落庫。我們最初只把最終結(jié)果存下來了結(jié)果模型一跑偏很難判斷是哪個環(huán)節(jié)出了問題只能靠猜。后來把每一輪的原始輸入輸出、工具調(diào)用參數(shù)、異常信息全部記錄下來調(diào)試效率直接翻倍。以后再做 Agent 類項目我會把日志體系當(dāng)成基礎(chǔ)設(shè)施一樣在項目第一天就搭好。這個項目做完以后我還在想能不能把任務(wù)編排的邏輯抽出來做一個通用的 Agent 工作流腳手架到時候再跟大家分享。