 AI 編排引擎)
1. 為什么要在 RuoYi-Vue-Plus 里塞進一個 AI 編排引擎RuoYi-Vue-Plus 這套腳手架在國內(nèi)中小型管理系統(tǒng)開發(fā)里出鏡率極高權(quán)限、數(shù)據(jù)權(quán)限、多租戶、代碼生成、工作流這些企業(yè)級能力基本開箱即用很多團隊拿它當(dāng)后臺底座來搭業(yè)務(wù)系統(tǒng)。但這兩年有個明顯變化業(yè)務(wù)方開始不滿足于表單列表審批這套老三樣越來越多的需求變成能不能讓系統(tǒng)自己判斷一下能不能根據(jù)這段描述自動生成工單能不能把這幾份文檔自動歸類。說白了就是要在傳統(tǒng) CRUD 系統(tǒng)里長出一點智能。問題在于絕大多數(shù) RuoYi 系項目是純 Java 技術(shù)棧團隊里沒人愿意為了一個 AI 功能再單獨維護一套 Python 服務(wù)跨語言調(diào)用、部署、監(jiān)控、鑒權(quán)全是額外成本。這時候把 AI 編排能力直接以 Java 庫的形式嵌進現(xiàn)有工程就成了最省事的路子。AgentScope 的 Harness 模塊正好提供了這樣一套 Java 側(cè)的 Agent 編排能力它把模型調(diào)用、工具注冊、多輪對話、執(zhí)行循環(huán)這些原本散落在各處的邏輯收斂成一個可編程的運行時。這篇內(nèi)容面向的是已經(jīng)熟悉 RuoYi-Vue-Plus 基本結(jié)構(gòu)、想在不引入第二套語言棧的前提下接入 AI 能力的后端開發(fā)者。我會把整個集成過程拆開講從依賴怎么引、模塊怎么放到 Agent 怎么注冊、工具怎么暴露、會話怎么和系統(tǒng)用戶綁定再到實際跑起來之后那些文檔里不會寫的坑。核心關(guān)鍵詞就三個AgentScope Harness 集成、純 Java AI 平臺、RuoYi-Vue-Plus 擴展。讀完你應(yīng)該能自己動手把一套可用的 AI 編排能力掛到現(xiàn)有后臺里而不是停留在知道有這么個東西。2. 集成前的架構(gòu)判斷Harness 到底該放在哪一層2.1 RuoYi-Vue-Plus 的模塊劃分與 AI 能力的歸屬RuoYi-Vue-Plus 默認(rèn)是多模塊 Maven 結(jié)構(gòu)常見的是ruoyi-admin啟動入口、ruoyi-system系統(tǒng)業(yè)務(wù)、ruoyi-common通用工具、ruoyi-framework框架配置這幾塊。AI 編排能力放哪里直接決定了后面依賴會不會打架、循環(huán)依賴會不會出現(xiàn)。我的建議是單獨開一個ruoyi-ai模塊而不是塞進ruoyi-system或者ruoyi-common。理由很實際Harness 會引入自己的 HTTP 客戶端、JSON 處理、可能的日志門面這些和ruoyi-common里已有的工具類存在版本沖突風(fēng)險。單獨成模塊依賴隔離干凈ruoyi-admin只依賴ruoyi-ai的接口層業(yè)務(wù)模塊通過接口調(diào)用不會把 AI 的傳遞依賴污染到整個工程。具體依賴方向是這樣的ruoyi-ai依賴ruoyi-common復(fù)用它的統(tǒng)一返回、異常、工具類ruoyi-system按需依賴ruoyi-airuoyi-admin負(fù)責(zé)把ruoyi-ai的 Bean 掃描進來。這樣 AI 模塊既能用上框架的通用能力又不會反向被業(yè)務(wù)模塊依賴保持單向。2.2 為什么不做成獨立微服務(wù)有人會問既然 AI 調(diào)用可能耗時、可能不穩(wěn)定為什么不干脆拆成獨立服務(wù)我的實測結(jié)論是在中小規(guī)模場景下獨立服務(wù)的運維成本遠(yuǎn)大于它帶來的隔離收益。拆出去意味著你要多維護一套注冊發(fā)現(xiàn)、多一套鑒權(quán)透傳、多一套鏈路追蹤而 AI 調(diào)用本身在 RuoYi 這種后臺系統(tǒng)里往往是低頻、非核心路徑的操作比如生成一段摘要、做一次分類完全可以用線程池 異步任務(wù)的方式在單體里消化掉。真正需要拆服務(wù)的臨界點是當(dāng) AI 調(diào)用變成高頻核心鏈路或者需要獨立擴縮容的時候。在那之前單體里嵌一個模塊部署簡單、調(diào)試方便、事務(wù)和權(quán)限都能復(fù)用現(xiàn)成的這才是純 Java AI 平臺這個思路的價值所在。2.3 Harness 在運行時的角色定位把 Harness 理解成一個Agent 運行時容器比較準(zhǔn)確。它本身不負(fù)責(zé)具體業(yè)務(wù)而是提供Agent 的生命周期管理、消息在 Agent 之間的流轉(zhuǎn)、工具的注冊與調(diào)用分發(fā)、以及一輪對話的執(zhí)行循環(huán)。你在 RuoYi 里要做的事情本質(zhì)上是把系統(tǒng)的業(yè)務(wù)能力包裝成 Harness 能識別的工具再把 Harness 的執(zhí)行結(jié)果接回系統(tǒng)的業(yè)務(wù)流。這個定位決定了集成的兩個關(guān)鍵接口一個是工具注冊把 RuoYi 的 Service 方法暴露給 Agent另一個是會話管理把 Harness 的會話和 RuoYi 的登錄用戶、租戶綁定起來。這兩塊做扎實了剩下的就是配置和調(diào)優(yōu)。3. 依賴引入與模塊骨架搭建的實操細(xì)節(jié)3.1 Maven 依賴的引入方式與版本對齊Harness 作為 Java 庫通常以 Maven 坐標(biāo)形式提供。在ruoyi-ai模塊的pom.xml里引入時第一件要確認(rèn)的事是它依賴的 JSON 庫和 HTTP 客戶端版本。RuoYi-Vue-Plus 較新版本用的是 Jackson 或者 Hutool 的 JSON 工具如果 Harness 內(nèi)部用的是別的 JSON 實現(xiàn)就可能在序列化時出現(xiàn)字段丟失、日期格式不一致這類隱蔽問題。我的做法是在ruoyi-ai的pom.xml里顯式聲明 Harness 依賴然后用mvn dependency:tree把整棵依賴樹打出來重點看三類包JSON 處理、HTTP 客戶端、日志門面。如果發(fā)現(xiàn)和父工程已有版本沖突用exclusions把 Harness 傳遞進來的舊版本排掉統(tǒng)一走父工程的版本管理。這一步看著瑣碎但不做的話后面會出現(xiàn)本地跑得好好的打包上線就報 NoSuchMethodError這種經(jīng)典問題。dependency groupIdcom.example.agentscope/groupId artifactIdharness-core/artifactId version${harness.version}/version exclusions exclusion groupIdcom.fasterxml.jackson.core/groupId artifactIdjackson-databind/artifactId /exclusion /exclusions /dependency提示版本號統(tǒng)一放到父工程的properties里管理別在子模塊里寫死否則多模塊升級時會漏改。3.2 模塊目錄結(jié)構(gòu)與包命名約定ruoyi-ai模塊內(nèi)部我習(xí)慣按職責(zé)分四個包config自動配置和 Bean 聲明、agentAgent 定義與注冊、tool業(yè)務(wù)工具封裝、service對外暴露的服務(wù)接口。這樣分的好處是當(dāng)你要排查某個工具為什么沒被 Agent 調(diào)用到時直接去tool包看注冊邏輯不用在幾百個類里翻。包名沿用 RuoYi 的約定比如com.ruoyi.ai.config、com.ruoyi.ai.agent這樣和框架其他模塊風(fēng)格一致團隊新人接手時不會有認(rèn)知負(fù)擔(dān)。配置類上用Configuration加ConditionalOnProperty讓 AI 能力可以通過配置開關(guān)控制是否啟用——這點很重要因為不是每個部署環(huán)境都需要 AI 功能能關(guān)掉就意味著出問題時可以快速降級。3.3 自動配置類的編寫要點自動配置類是整個模塊的入口它要做三件事初始化 Harness 運行時、注冊工具、暴露服務(wù) Bean。初始化運行時的時候模型相關(guān)的配置比如接口地址、密鑰、超時時間應(yīng)該從 RuoYi 的配置體系里讀而不是硬編碼。Configuration ConditionalOnProperty(prefix ruoyi.ai, name enabled, havingValue true) public class AiAutoConfiguration { Bean ConditionalOnMissingBean public HarnessRuntime harnessRuntime(AiProperties properties) { HarnessConfig config HarnessConfig.builder() .endpoint(properties.getEndpoint()) .apiKey(properties.getApiKey()) .connectTimeout(properties.getConnectTimeout()) .readTimeout(properties.getReadTimeout()) .build(); return HarnessRuntime.create(config); } }這里有個細(xì)節(jié)ConditionalOnMissingBean要加上方便測試時用 Mock 替換真實運行時。超時時間一定要顯式配置Harness 默認(rèn)的超時往往偏長在 Web 請求線程里同步調(diào)用會拖垮整個接口響應(yīng)后面第 6 節(jié)會專門講這個。4. 把 RuoYi 的業(yè)務(wù)能力包裝成 Agent 可調(diào)用的工具4.1 工具注冊的核心機制Harness 調(diào)用業(yè)務(wù)能力的方式是工具調(diào)用——Agent 在推理過程中決定要調(diào)用哪個工具、傳什么參數(shù)Harness 負(fù)責(zé)把調(diào)用分發(fā)到對應(yīng)的實現(xiàn)上。所以集成的核心工作就是把 RuoYi 的 Service 方法包裝成工具。工具注冊一般有兩種模式注解掃描和手動注冊。注解掃描寫起來簡潔在方法上加個AgentTool之類的注解啟動時自動收集手動注冊則是在配置類里一個個register。我傾向于混合使用通用的、穩(wěn)定的工具用注解掃描需要動態(tài)參數(shù)或者依賴運行時上下文的用手動注冊。原因是注解掃描在遇到重載方法、泛型參數(shù)時容易出歧義而手動注冊雖然啰嗦但每個工具的入?yún)⒊鰠⒍记迩宄?.2 工具描述的質(zhì)量決定 Agent 的可用性這是最容易被低估的一點。很多人把工具注冊完就完事了結(jié)果 Agent 要么不調(diào)用工具要么亂調(diào)用。問題幾乎都出在工具描述上。Harness 是靠描述來判斷這個工具是干什么的、什么時候該用的描述寫得含糊Agent 的決策就一塌糊涂。舉個例子你有一個查詢訂單的工具描述寫成查詢訂單信息Agent 根本不知道它和查詢物流有什么區(qū)別。正確的寫法是把適用場景、參數(shù)含義、返回內(nèi)容都寫清楚AgentTool( name queryOrderDetail, description 根據(jù)訂單編號查詢訂單的詳細(xì)信息包括下單時間、金額、狀態(tài)。 適用于用戶明確提供了訂單編號、需要了解訂單具體內(nèi)容的場景。 如果用戶只提供了手機號而沒有訂單號請先調(diào)用 queryOrderByPhone。 ) public OrderDetail queryOrderDetail(ToolParam(訂單編號純數(shù)字字符串) String orderNo) { return orderService.getByOrderNo(orderNo); }描述里明確寫了什么時候用什么時候不用前置工具是什么Agent 的調(diào)用準(zhǔn)確率會有肉眼可見的提升。這是我在實際項目里反復(fù)驗證過的經(jīng)驗比調(diào)模型參數(shù)管用得多。4.3 參數(shù)校驗與異常兜底工具被 Agent 調(diào)用時傳進來的參數(shù)是模型生成的不能假設(shè)它一定合法。訂單號可能傳成中文日期可能格式不對枚舉值可能超出范圍。所以每個工具方法的第一件事應(yīng)該是參數(shù)校驗校驗失敗要返回一個結(jié)構(gòu)化的錯誤信息而不是直接拋異常。為什么強調(diào)結(jié)構(gòu)化錯誤因為 Agent 拿到錯誤信息后可能會自我修正、重新調(diào)用。如果你直接拋IllegalArgumentExceptionHarness 可能把它當(dāng)成系統(tǒng)故障終止整個流程如果你返回{error: 訂單號格式不正確應(yīng)為純數(shù)字},Agent 就有機會換個參數(shù)再試一次。這個差別在復(fù)雜任務(wù)里非常關(guān)鍵。public ToolResult queryOrderDetail(String orderNo) { if (orderNo null || !orderNo.matches(\\d)) { return ToolResult.error(訂單號格式不正確應(yīng)為純數(shù)字字符串); } OrderDetail detail orderService.getByOrderNo(orderNo); if (detail null) { return ToolResult.error(未找到該訂單請確認(rèn)訂單號是否正確); } return ToolResult.success(detail); }4.4 工具與數(shù)據(jù)權(quán)限的綁定RuoYi-Vue-Plus 有完善的數(shù)據(jù)權(quán)限體系工具調(diào)用不能繞過它。也就是說Agent 通過工具查數(shù)據(jù)時必須帶上當(dāng)前登錄用戶的身份走和普通接口一樣的數(shù)據(jù)權(quán)限過濾。實現(xiàn)方式是在工具方法里通過SecurityUtils拿到當(dāng)前用戶把用戶上下文傳給 Service 層。這里有個坑如果 AI 調(diào)用是異步執(zhí)行的比如放到線程池里跑SecurityUtils依賴的ThreadLocal在新線程里是空的會直接報獲取用戶失敗。解決辦法是在提交異步任務(wù)前把用戶上下文取出來作為參數(shù)傳進去在異步線程里手動設(shè)置。這個細(xì)節(jié)不處理異步 AI 功能一上線就會大面積報錯。5. 會話管理讓 Agent 記住誰在問、問到哪了5.1 會話與系統(tǒng)用戶的映射關(guān)系Harness 的會話Session是它內(nèi)部維護的對話上下文而 RuoYi 的用戶是系統(tǒng)級的身份。兩者需要建立映射一個系統(tǒng)用戶可能有多個會話比如開了多個對話窗口一個會話必須歸屬于某個用戶和某個租戶。我的做法是在數(shù)據(jù)庫里建一張ai_session表字段包括會話 ID、用戶 ID、租戶 ID、創(chuàng)建時間、最后活躍時間、會話狀態(tài)。Harness 側(cè)生成會話 ID 后往這張表里插一條記錄。這樣做的價值在于會話可以持久化、可以審計、可以按用戶清理。純內(nèi)存的會話在服務(wù)重啟后就丟了用戶體驗很差。5.2 多輪對話上下文的存儲策略多輪對話的上下文如果全放內(nèi)存服務(wù)一重啟就斷片如果每輪都全量存數(shù)據(jù)庫數(shù)據(jù)量會爆炸。折中方案是內(nèi)存 持久化雙層活躍會話的上下文放內(nèi)存用帶過期時間的緩存同時定期把關(guān)鍵節(jié)點快照到數(shù)據(jù)庫。用戶下次進來時先從內(nèi)存找找不到再從數(shù)據(jù)庫恢復(fù)最近 N 輪。N 取多少合適我的經(jīng)驗是保留最近 10 到 20 輪足夠覆蓋絕大多數(shù)場景。再往前的上下文對當(dāng)前對話的貢獻很小反而會占用大量 token、拖慢響應(yīng)、增加成本。超過這個輪數(shù)的歷史可以只保留摘要而不是原文。5.3 會話并發(fā)與線程安全同一個會話如果被并發(fā)訪問比如用戶快速點了兩次發(fā)送Harness 的內(nèi)部狀態(tài)可能被破壞。解決辦法是對會話加鎖同一會話的請求串行執(zhí)行??梢杂肅oncurrentHashMap存會話鎖對象或者用 Redis 的分布式鎖多實例部署時必須用后者。public String chat(String sessionId, String userInput) { Lock lock sessionLocks.computeIfAbsent(sessionId, k - new ReentrantLock()); lock.lock(); try { return harnessRuntime.chat(sessionId, userInput); } finally { lock.unlock(); } }單機用ReentrantLock夠了多實例部署就得換成 Redis 鎖否則兩個實例同時操作一個會話照樣出問題。這個點在做集群部署時特別容易漏。6. 實測踩坑那些文檔里不會寫的集成問題6.1 同步調(diào)用拖垮 Web 線程池最開始我把 AI 調(diào)用直接放在 Controller 里同步執(zhí)行測試環(huán)境沒問題一上生產(chǎn)就發(fā)現(xiàn)接口響應(yīng)時間從幾百毫秒飆到十幾秒并發(fā)一高整個線程池被打滿連不相關(guān)的接口都開始超時。原因很簡單AI 調(diào)用是秒級甚至十秒級的操作而 Tomcat 的請求線程是寶貴資源被這種長耗時操作占住其他請求就排隊了。解決方案是異步化。接口收到請求后立即返回一個任務(wù) IDAI 調(diào)用丟到獨立線程池里執(zhí)行前端通過輪詢或者長連接拿結(jié)果。線程池要單獨配置核心線程數(shù)、隊列長度、拒絕策略都要根據(jù)實際并發(fā)量調(diào)別用默認(rèn)的。Bean(aiExecutor) public ThreadPoolTaskExecutor aiExecutor() { ThreadPoolTaskExecutor executor new ThreadPoolTaskExecutor(); executor.setCorePoolSize(4); executor.setMaxPoolSize(16); executor.setQueueCapacity(100); executor.setThreadNamePrefix(ai-task-); executor.setRejectedExecutionHandler(new ThreadPoolExecutor.CallerRunsPolicy()); executor.initialize(); return executor; }6.2 工具調(diào)用超時導(dǎo)致的半截任務(wù)Agent 執(zhí)行一個任務(wù)可能連續(xù)調(diào)用多個工具如果某個工具卡住比如查了個慢 SQL整個任務(wù)就掛在那里。Harness 一般有整體超時但工具級別的超時往往需要自己控制。我的做法是給每個工具方法加超時保護用Future.get(timeout)或者信號量控制超時后返回一個明確的錯誤讓 Agent 決定是重試還是放棄。這里有個反直覺的點工具超時后不要直接拋異常終止而是返回該操作超時請稍后重試或換一種方式。因為 Agent 可能會根據(jù)這個信息調(diào)整策略比如換個查詢條件、或者告訴用戶稍后再試。直接拋異常會讓整個對話中斷體驗很差。6.3 模型返回格式不穩(wěn)定引發(fā)的解析失敗模型輸出是自然語言即使你要求它返回 JSON它也可能給你包一層 markdown 代碼塊或者在 JSON 前后加一句好的結(jié)果如下。如果代碼里直接JSON.parse就會頻繁解析失敗。處理方式是容錯解析先嘗試直接解析失敗后剝離可能的代碼塊標(biāo)記再解析再失敗就用正則提取第一個完整的 JSON 對象。同時在提示詞里明確要求只返回 JSON不要任何額外文字能減少大部分問題。但容錯邏輯必須留著因為模型的行為不是 100% 可控的。6.4 日志與可觀測性缺失AI 調(diào)用出問題時如果沒有詳細(xì)的日志你根本不知道是模型沒返回、工具沒調(diào)用、還是參數(shù)傳錯了。所以從第一天起就要把日志打全每次模型請求的輸入輸出、每次工具調(diào)用的入?yún)⒊鰠?、每輪對話的耗時。這些日志在排查問題時是救命的。但要注意脫敏。用戶輸入里可能有手機號、身份證號工具返回里可能有敏感業(yè)務(wù)數(shù)據(jù)日志里不能明文記錄。RuoYi 有現(xiàn)成的脫敏工具直接復(fù)用。排查場景關(guān)鍵日志常見原因Agent 不調(diào)用工具模型原始輸出工具描述不清、提示詞未引導(dǎo)工具調(diào)用報錯工具入?yún)? 異常棧參數(shù)格式不符、權(quán)限上下文丟失響應(yīng)特別慢各階段耗時打點模型響應(yīng)慢、工具慢 SQL、線程池排隊會話串話會話 ID 用戶 ID會話映射錯誤、并發(fā)未加鎖7. 上線前的配置調(diào)優(yōu)與降級預(yù)案7.1 超時與重試參數(shù)的取值邏輯超時和重試這兩個參數(shù)取值不能拍腦袋。連接超時一般設(shè) 3 到 5 秒因為網(wǎng)絡(luò)握手正常都在毫秒級超過這個數(shù)基本是網(wǎng)絡(luò)問題重試也沒用。讀取超時要看模型響應(yīng)速度一般設(shè) 30 到 60 秒復(fù)雜推理任務(wù)可能更久。重試次數(shù)建議最多 1 次因為 AI 調(diào)用不是冪等友好的場景重試可能產(chǎn)生重復(fù)的工具調(diào)用副作用。重試還要區(qū)分錯誤類型網(wǎng)絡(luò)超時可以重試參數(shù)錯誤重試沒意義限流錯誤要退避后再試。一刀切的重試策略只會放大問題。7.2 降級開關(guān)的設(shè)計AI 功能必須能一鍵關(guān)掉。設(shè)計上分兩級全局開關(guān)配置中心控制關(guān)掉后所有 AI 接口返回功能維護中和單功能開關(guān)某個具體 Agent 出問題只關(guān)它。全局開關(guān)用于模型服務(wù)整體不可用時的緊急降級單功能開關(guān)用于局部問題隔離。開關(guān)狀態(tài)要能動態(tài)刷新不能改配置還得重啟。RuoYi 集成了配置刷新能力的話直接用沒有的話用定時任務(wù)輪詢配置中心也行。7.3 成本控制的實際手段AI 調(diào)用是花錢的尤其是 token 消耗??刂瞥杀居袔讉€立竿見影的手段限制單次對話的最大輪數(shù)防止用戶無限聊下去、限制單次請求的最大 token防止超長輸入、對高頻用戶做限流防止刷接口、緩存常見問題的回答相同問題直接返回緩存。其中緩存最容易被忽略。很多后臺系統(tǒng)的 AI 問答其實是重復(fù)的比如這個功能怎么用緩存命中率能到 30% 以上省下來的成本很可觀。緩存 key 用問題文本的哈希注意設(shè)置合理的過期時間。8. 我個人在集成過程中的幾點體會整套東西跑通之后回頭看最耗時間的不是寫代碼而是調(diào)工具描述和排查上下文丟失這兩件事。工具描述我前后改了七八版每改一版 Agent 的調(diào)用準(zhǔn)確率就上一個臺階這個過程沒有捷徑只能拿真實場景反復(fù)測。上下文丟失則集中在異步和權(quán)限兩個環(huán)節(jié)凡是跨線程的地方都要檢查一遍用戶上下文有沒有正確傳遞。另外一個體會是別指望 Agent 一次就把復(fù)雜任務(wù)做對。真實場景里一個需要調(diào)用三四個工具的任務(wù)第一次成功率可能只有六七成。所以產(chǎn)品設(shè)計上要允許重試和人工兜底把 AI 定位成輔助而不是全自動這樣即使它偶爾出錯業(yè)務(wù)也不會崩。等準(zhǔn)確率通過持續(xù)調(diào)優(yōu)穩(wěn)定到九成以上再考慮擴大它的自主權(quán)。最后純 Java 集成這條路是走得通的而且對 RuoYi 系團隊來說是最平滑的選擇。它不需要你引入新語言、新部署單元復(fù)用現(xiàn)有的權(quán)限、日志、監(jiān)控體系出問題也在熟悉的排查路徑里。代價是你要接受 Java 生態(tài)在 AI 編排這塊相對年輕一些高級特性可能不如其他語言棧豐富但對絕大多數(shù)后臺系統(tǒng)的智能化需求來說夠用了。