建AI智能體與工作流平臺(tái))
簡(jiǎn)介面向全棧開(kāi)發(fā)者這份資源是一套基于Spring Boot 3與LangChain4j的AI應(yīng)用平臺(tái)源碼能幫助快速搭建具備智能代碼生成、智能體編排、工作流管理與工具調(diào)用能力的完整系統(tǒng)。前端使用Vue 3構(gòu)建交互界面后端提供可視化編輯、一鍵部署、應(yīng)用管理和智能路由等核心能力并整合多級(jí)存儲(chǔ)與Nginx通過(guò)ARMS、Prometheus與Grafana實(shí)現(xiàn)應(yīng)用監(jiān)控也兼容Cursor Vibe Coding開(kāi)發(fā)模式。壓縮包內(nèi)共216個(gè)文件以143個(gè)Java文件為主要組成部分承載后端業(yè)務(wù)邏輯與集成配置21個(gè)TypeScript文件和19個(gè)Vue文件對(duì)應(yīng)前端頁(yè)面與交互邏輯其余包括JSON配置、SQL初始化腳本、YAML部署文件以及說(shuō)明文檔等輔助材料。整體壓縮包大小約1.14MB結(jié)構(gòu)清晰便于按需閱讀。截至目前已有250人瀏覽學(xué)習(xí)適合想要深入實(shí)踐LangChain4j、LangGraph4j工作流以及AI應(yīng)用工程化的開(kāi)發(fā)者參考學(xué)習(xí)。1. 這個(gè)“AI應(yīng)用平臺(tái)”到底在解決什么問(wèn)題如果你所在團(tuán)隊(duì)已經(jīng)試過(guò)把大模型接進(jìn)業(yè)務(wù)系統(tǒng)大概率會(huì)遇到這三件事第一模型只會(huì)“聊天”讓它去查數(shù)據(jù)庫(kù)、改工單、調(diào)接口就得寫(xiě)一堆膠水代碼第二一段固定的提示詞應(yīng)付不了多步任務(wù)業(yè)務(wù)要求“先查庫(kù)存再算報(bào)價(jià)最后生成合同”每一步都有嚴(yán)格順序第三做出來(lái)的東西只活在開(kāi)發(fā)者的 IDEA 里業(yè)務(wù)方想要在頁(yè)面上自己拖一拖、改一改、點(diǎn)一下就能上線。這個(gè)標(biāo)題把這件事說(shuō)全了——基于 SpringBoot3 LangChain4j Vue3 搭一個(gè) AI 應(yīng)用平臺(tái)用 AI 智能體和 ToolCalling 讓模型有手有腳用 LangGraph4j 工作流把不可控的對(duì)話變成可控的流程再用 Vue3 做一套可視化編輯、應(yīng)用管理和一鍵部署的殼子。它適合兩類(lèi)人一類(lèi)是 Java 后端為主、想在公司內(nèi)部落地 AI 功能但不想引一堆 Python 微服務(wù)的團(tuán)隊(duì)另一類(lèi)是正在做 AI 應(yīng)用低代碼平臺(tái)、需要參考一條端到端技術(shù)路徑的開(kāi)發(fā)者。這篇筆記按后端底座、智能體、工作流、前端落地和常見(jiàn)坑的順序展開(kāi)照著能做出一版可演示、可評(píng)審、可繼續(xù)投入的骨架。2. 后端底座SpringBoot3 LangChain4j 的模型接入與多路召回2.1 為什么選 SpringBoot3 LangChain4j而不是自己封裝 HTTP 客戶端很多 Java 團(tuán)隊(duì)接到 AI 需求后的第一反應(yīng)是直接調(diào)模型廠商的 HTTP 接口寫(xiě)一個(gè) RestTemplate 封裝再自己管理上下文和歷史消息。短期看沒(méi)毛病一旦要支持多模型切換、流式輸出、多輪記憶、工具調(diào)用這套手寫(xiě)代碼會(huì)迅速膨脹成沒(méi)人敢動(dòng)的“黑匣子”。LangChain4j 在 Java 生態(tài)里的定位相當(dāng)于把 LangChain 那套抽象用 Java 重寫(xiě)了一遍但它更收斂核心就幾個(gè)概念——ChatLanguageModel 管模型對(duì)話EmbeddingModel 管向量化AiService 管聲明式接口Tool 管工具調(diào)用Memory 管對(duì)話歷史。對(duì) SpringBoot3 團(tuán)隊(duì)來(lái)說(shuō)集成成本比引入 Python 服務(wù)低得多而且可以跟現(xiàn)有的 Spring 容器、配置體系、事務(wù)、監(jiān)控直接融合。SpringBoot3 本身的價(jià)值在 AI 場(chǎng)景里會(huì)被放大一是原生支持虛擬線程處理 SSE 流式響應(yīng)和工具并行調(diào)用時(shí)線程開(kāi)銷(xiāo)明顯下降二是 SpringBoot3 的配置綁定和自動(dòng)裝配讓 LangChain4j 的模型參數(shù)可以全部放進(jìn) application.yml換模型時(shí)不需要改 Java 代碼三是 SpringBoot3 對(duì) GraalVM 的支持雖然還不是萬(wàn)能靈藥但做平臺(tái)類(lèi)項(xiàng)目時(shí)預(yù)留了后續(xù)優(yōu)化啟動(dòng)內(nèi)存的余地。這一層選型的核心訴求不是“誰(shuí)的生態(tài)更熱鬧”而是“這個(gè)團(tuán)隊(duì)能不能低成本把 AI 能力接進(jìn)現(xiàn)有的 Java 服務(wù)里”。2.2 模型接入層用 OpenAI 兼容協(xié)議適配多模型平臺(tái)類(lèi)應(yīng)用最忌諱把模型廠商寫(xiě)死在代碼里。常見(jiàn)的做法是底層統(tǒng)一走 OpenAI 兼容的 Chat 接口協(xié)議上層通過(guò)配置決定連哪家服務(wù)。這樣無(wú)論是公有云模型、私有化部署的模型還是開(kāi)源模型網(wǎng)關(guān)只要對(duì)方暴露了兼容接口就能一根配置切過(guò)去。LangChain4j 內(nèi)置的 OpenAiChatModel 支持自定義 baseUrl官方模型和兼容協(xié)議模型都可以掛到同一個(gè)入口下。spring: application: name: ai-platform langchain4j: open-ai: base-url: ${LLM_BASE_URL:https://your-llm-gateway.example.com/v1} api-key: ${LLM_API_KEY:} chat-model: model-name: ${LLM_MODEL_NAME:qwen2.5-72b-instruct} temperature: 0.7 timeout: 60s max-tokens: 4096 log-requests: false這段配置的關(guān)鍵在于base-url和model-name都做成了環(huán)境變量占位。平臺(tái)內(nèi)部測(cè)試用一套模型生產(chǎn)切另一套前端的工作流定義、智能體配置完全不用改。log-requests在聯(lián)調(diào)階段建議開(kāi)成 true能看到實(shí)際發(fā)給模型的 payload排查“模型為什么沒(méi)按預(yù)期調(diào)用工具”時(shí)這是第一手證據(jù)上線前一定關(guān)掉否則每次對(duì)話的完整內(nèi)容都會(huì)落日志時(shí)間長(zhǎng)了下游日志系統(tǒng)先扛不住。然后是聲明式接口。LangChain4j 的 AiService 用注解定義服務(wù)方法框架在運(yùn)行時(shí)生成實(shí)現(xiàn)類(lèi)AiService public interface ChatAssistant { String chat(MemoryId String memoryId, UserMessage String userMessage); Streaming FluxString streamChat(MemoryId String memoryId, UserMessage String userMessage); }這個(gè)接口直接被 Spring 代理方法上的UserMessage表示哪個(gè)人傳參數(shù)拼進(jìn)用戶消息MemoryId表示按業(yè)務(wù)維度隔離對(duì)話歷史——比如一個(gè)表單一個(gè)記憶一個(gè)工單一個(gè)記憶而不是全局共享上下文。Streaming返回 Reactor 的 Flux配合 SpringBoot3 的 WebFlux 或 MVC 異步支持把流式 token 通過(guò) SSE 推給前端。需要注意AiService 的實(shí)現(xiàn)機(jī)制對(duì)“自定義上下文拼接”不夠靈活。常見(jiàn)做法是平臺(tái)的應(yīng)用配置里允許用戶填系統(tǒng)提示詞這部分不適合寫(xiě)死在注解上而是用 ChatMemory 和 MessageWindow 在會(huì)話維度動(dòng)態(tài)構(gòu)造。2.3 多路召回向量檢索 關(guān)鍵詞檢索 融合排序熱詞里那個(gè)“l(fā)angchain4j 多路召回”本質(zhì)是 RAG 里提升召回質(zhì)量的關(guān)鍵手段。單靠向量檢索遇到專(zhuān)有名詞、型號(hào)、工單編號(hào)這類(lèi)沒(méi)有語(yǔ)義但字符高度精確的查詢效果會(huì)很差單靠關(guān)鍵詞檢索又接不住“幫我把最近一周未結(jié)算的訂單按金額排個(gè)序”這種口語(yǔ)化表達(dá)。平臺(tái)里我給知識(shí)庫(kù)場(chǎng)景設(shè)計(jì)的召回鏈路是一路走向量相似度一路走關(guān)鍵詞 BM25兩邊分別取 topK再做歸一化融合。Component public class HybridRetriever { private final EmbeddingStoreTextSegment embeddingStore; private final EmbeddingModel embeddingModel; private final KeywordSearchService keywordSearchService; public ListScoredDocument retrieve(String query, int topK) { // 第一路向量召回 var queryEmbedding embeddingModel.embed(query).content(); var vectorResults embeddingStore.search(queryEmbedding, topK 5) .stream() .map(hit - new ScoredDocument(hit.scoredText().text(), hit.score(), vector)) .toList(); // 第二路關(guān)鍵詞召回走 BM25 或者數(shù)據(jù)庫(kù)全文索引 ListScoredDocument keywordResults keywordSearchService.search(query, topK 5); // 第三路融合用 RRF 公式而非直接加權(quán) return fuse(vectorResults, keywordResults, topK); } }融合邏輯先刷一出分?jǐn)?shù)歸一化再用倒數(shù)排名融合RRFprivate List fuse(List vectorDocs, List keywordDocs, int topK) { MapString, Double scoreMap new HashMap(); addWithRrf(scoreMap, vectorDocs, 60); addWithRrf(scoreMap, keywordDocs, 60); return scoreMap.entrySet().stream() .sorted(Map.Entry.String, DoublecomparingByValue().reversed()) .limit(topK) .map(e - new ScoredDocument(e.getKey(), e.getValue(), fused)) .toList(); }private void addWithRrf(MapString, Double map, List docs, int k) { for (int rank 0; rank docs.size(); rank) { ScoredDocument doc docs.get(rank); map.merge(doc.content(), 1.0 / (k rank 1), Double::sum); } }參數(shù)說(shuō)明里最值得調(diào)的是兩個(gè)值向量召回和關(guān)鍵詞召回的數(shù)量、RRF 的常數(shù) k。常見(jiàn)做法是多召回一部分比如 topK5讓融合階段有得選k 一般取 60 左右調(diào)小會(huì)讓高排名文檔權(quán)重更突出調(diào)大則更平均。實(shí)際跑下來(lái)經(jīng)驗(yàn)是向量召回 top 50 里如果沒(méi)有答案融合也救不回來(lái)問(wèn)題通常出在文檔切分粒度或 Embedding 模型本身。 ### 2.4 代碼生成器場(chǎng)景模型輸出到工程落地之間還有一道閘 標(biāo)題里專(zhuān)門(mén)點(diǎn)出智能代碼生成這是 AI 應(yīng)用平臺(tái)最接地氣的一個(gè)能力。實(shí)現(xiàn)上不是扔一個(gè)“給我生成 UserController”的提示詞就完事而是把代碼生成拆成三步需求理解、工程結(jié)構(gòu)約束、代碼后處理。工程結(jié)構(gòu)約束是最容易被忽略的——直接讓模型自由輸出生成的代碼大概率跟項(xiàng)目現(xiàn)有框架不一致。 常見(jiàn)做法是把項(xiàng)目的技術(shù)棧約束、目錄規(guī)范、接口風(fēng)格寫(xiě)成 system prompt 的一部分再把代碼生成的產(chǎn)物限定為填充式代碼塊。例如生成一個(gè) SpringBoot 的 Service 實(shí)現(xiàn)時(shí)平臺(tái)先注入項(xiàng)目模板模型只需要補(bǔ)全方法體。生成之后的工作站不住腳代碼格式化、編譯校驗(yàn)、導(dǎo)入補(bǔ)全這三步必須自動(dòng)化否則用戶拿到一坨縮進(jìn)混亂、缺 import 文件根本沒(méi)法看。這部分后續(xù)會(huì)展開(kāi)談但在后端底座這里要明確一條邊界——LangChain4j 負(fù)責(zé)和模型打交道代碼生成的工程部分必須由平臺(tái)自己的服務(wù)接管。 ## 3. 讓模型有手有腳ToolCalling 與 AI 智能體的實(shí)現(xiàn)細(xì)節(jié) ### 3.1 ToolCalling 到底是什么為什么智能體離不開(kāi)它 讓模型直接生成“查數(shù)據(jù)庫(kù)、發(fā)郵件、創(chuàng)建工單”這些操作是不現(xiàn)實(shí)的模型本質(zhì)上是概率生成文本它不知道自己能不能執(zhí)行、有沒(méi)有權(quán)限、操作結(jié)果是什么。ToolCalling 解決的是這個(gè)“能力邊界”問(wèn)題模型在對(duì)話中輸出一個(gè)結(jié)構(gòu)化請(qǐng)求比如“調(diào)用 searchContract 工具參數(shù) keyword采購(gòu)合同”應(yīng)用層負(fù)責(zé)真正執(zhí)行再把執(zhí)行結(jié)果作為新的上下文繼續(xù)讓模型推理。智能體之所以叫“智能體”就是因?yàn)樗邆涓兄盏接脩糨斎搿Q策決定調(diào)哪個(gè)工具、行動(dòng)執(zhí)行工具、觀察讀取執(zhí)行結(jié)果的循環(huán)。 ### 3.2 用 LangChain4j 注冊(cè)一個(gè)工具參數(shù)描述決定成敗 LangChain4j 的 Tool 注解會(huì)把方法暴露給模型方法名、參數(shù)名、參數(shù)描述會(huì)拼到模型的工具定義里。這部分是對(duì)模型效果影響最大、也是最容易敷衍的地方。 java Component public class ContractTool { private final ContractRepository contractRepository; Tool(根據(jù)合同編號(hào)或關(guān)鍵字搜索合同返回合同名稱(chēng)、金額、簽訂日期和當(dāng)前狀態(tài)) public ListContractSearchResult searchContract( ToolParameter(搜索關(guān)鍵字可以是合同名稱(chēng)片段或完整合同編號(hào)) String keyword, ToolParameter(value 是否只查有效合同默認(rèn) true) boolean activeOnly) { return contractRepository.search(keyword, activeOnly); } Tool(查詢指定合同金額是否已超過(guò)預(yù)算上限) public BudgetCheckResult checkBudget( ToolParameter(合同編號(hào)) String contractNo, ToolParameter(預(yù)算金額單位元) double budgetAmount) { // 參數(shù)進(jìn) LLM 時(shí)是字符串必須做類(lèi)型校驗(yàn)和范圍校驗(yàn) if (budgetAmount 0 || budgetAmount 1_000_000_000) { throw new IllegalArgumentException(預(yù)算金額超出合理范圍); } return contractRepository.checkBudget(contractNo, budgetAmount); } }兩個(gè)細(xì)節(jié)值得說(shuō)。第一Tool 的工具描述要寫(xiě)清楚“這個(gè)工具能做什么、返回什么”模型依賴(lài)這段描述決定何時(shí)調(diào)用描述寫(xiě)得太短模型容易在其他工具上誤選寫(xiě)得太長(zhǎng)又占用上下文。第二參數(shù)描述必須包含“取值范圍、單位、主鍵格式”等信息——你寫(xiě)“預(yù)算金額單位元”模型就知道把用戶嘴里的“五百萬(wàn)”轉(zhuǎn)成 5000000 而不是 500 萬(wàn)次調(diào)用。這個(gè)環(huán)節(jié)做不好后面所有容錯(cuò)邏輯都是在給提示詞背鍋。3.3 工具執(zhí)行的循環(huán)控制超時(shí)、并發(fā)、死循環(huán)模型輸出工具調(diào)用請(qǐng)求后應(yīng)用層要執(zhí)行工具并回填結(jié)果。LangChain4j 在 AiServices 內(nèi)建了工具執(zhí)行循環(huán)但我一般會(huì)自己控制這個(gè)循環(huán)因?yàn)槠脚_(tái)需要統(tǒng)一記錄每一次工具調(diào)用的入?yún)?、出參、耗時(shí)和錯(cuò)誤。public ChatResponse runAgentLoop(String userMessage, ListObject tools) { ChatRequest request ChatRequest.builder() .messages(List.of(UserMessage.from(userMessage))) .toolSpecs(Specs.from(tools)) .build(); ToolContext toolContext new ToolContext(); for (int step 0; step 5; step) { ChatResponse response model.chat(request); AiMessage aiMessage response.aiMessage(); if (!aiMessage.hasToolExecutionRequests()) { return response; // 模型不再請(qǐng)求工具正常結(jié)束 } ListToolExecutionRequest requests aiMessage.toolExecutionRequests(); ListToolExecutionResultMessage results new ArrayList(); for (ToolExecutionRequest req : requests) { try (var ignored TimeLimiter.timeout(30, SECONDS)) { String result executeTool(req, tools, toolContext); results.add(new ToolExecutionResultMessage(req, result)); } catch (Exception e) { // 工具失敗必須回填給模型而不是中斷整個(gè)循環(huán) results.add(new ToolExecutionResultMessage(req, 工具執(zhí)行失敗: e.getMessage())); } } request appendResults(request, aiMessage, results); } throw new AgentLoopExceededException(工具調(diào)用超過(guò)5輪已終止); }這個(gè)循環(huán)里三個(gè)參數(shù)按場(chǎng)景調(diào)循環(huán)上限建議 3~5超過(guò)就是業(yè)務(wù)設(shè)計(jì)有問(wèn)題而不是模型能力問(wèn)題單工具超時(shí) 30 秒已經(jīng)偏寬松一般查詢類(lèi)接口 5 秒就該返回工具失敗信息回填給模型是非常反直覺(jué)但極重要的點(diǎn)——模型看到“工具執(zhí)行失敗合同編號(hào)不存在”會(huì)自己修正參數(shù)再試一次而不是生硬地報(bào)錯(cuò)給用戶。這里有個(gè)口語(yǔ)經(jīng)驗(yàn)寧可讓模型多問(wèn)一輪也別讓它亂猜答案。3.4 生產(chǎn)環(huán)境給工具加三道鎖工具一旦面向業(yè)務(wù)方開(kāi)放就不能只考慮“能不能跑通”。第一道鎖是冪等控制尤其是寫(xiě)操作工具——?jiǎng)?chuàng)建訂單、發(fā)送通知這類(lèi)工具要支持冪等鍵否則模型在一次循環(huán)里重復(fù)調(diào)用兩次業(yè)務(wù)數(shù)據(jù)就臟了。第二道鎖是范圍校驗(yàn)工具里的參數(shù)不能用默認(rèn)值糊弄數(shù)字范圍、枚舉值、超長(zhǎng)文本都要在 Java 側(cè)做校驗(yàn)不能把校驗(yàn)壓力留給模型。第三道鎖是審計(jì)日志每次工具調(diào)用的入?yún)ⅰ⒊鰠?、耗時(shí)、由哪次會(huì)話觸發(fā)都要落庫(kù)或者打到獨(dú)立的日志通道里——這不是為了排查問(wèn)題是為了出問(wèn)題時(shí)能向業(yè)務(wù)方交代。4. LangGraph4j 工作流從“自由對(duì)話”到“可控流程”4.1 為什么有了智能體還需要工作流智能體自由發(fā)揮適合“幫我寫(xiě)一段代碼”這類(lèi)開(kāi)放任務(wù)但業(yè)務(wù)場(chǎng)景里更多是“先查余額再走審批最后發(fā)通知”這種固定流程。自由決策意味著同樣的輸入每次可能走不同的路徑這在 ToB 場(chǎng)景里是災(zāi)難。LangGraph4j 把工作流建模成一張狀態(tài)圖StateGraph節(jié)點(diǎn)是處理單元邊是流轉(zhuǎn)規(guī)則條件邊根據(jù)當(dāng)前狀態(tài)決定下一步走哪個(gè)分支。這樣既保留了 AI 節(jié)點(diǎn)的靈活性又把整體流程定義成了業(yè)務(wù)方可預(yù)期、可審計(jì)的確定性結(jié)構(gòu)。平臺(tái)里 LangGraph4j 的角色很明確作為后端執(zhí)行引擎承接前端可視化畫(huà)布生成的 JSON 工作流定義編譯后執(zhí)行并實(shí)時(shí)上報(bào)節(jié)點(diǎn)狀態(tài)。它和 Reactor 的契合度也不錯(cuò)——每個(gè)節(jié)點(diǎn)返回的狀態(tài)變更天然適合用不可變對(duì)象傳遞。4.2 定義狀態(tài)和節(jié)點(diǎn)最小可跑的 LangGraph4j 流程StateGraphAgentState workflow new StateGraph(AgentState::new); workflow.addNode(planner, new PlannerNode()); workflow.addNode(coder, new CodeGenNode()); workflow.addNode(reviewer, new ReviewNode()); workflow.addNode(finish, new FinishNode()); workflow.setEntryPoint(planner); workflow.addEdge(planner, coder); workflow.addEdge(coder, reviewer); workflow.addConditionalEdge(reviewer, state - state.isApproved() ? finish : coder, Set.of(finish, coder)); CompiledGraphAgentState app workflow.compile();這段代碼里的核心概念就四個(gè)狀態(tài)AgentState、節(jié)點(diǎn)Node、普通邊addEdge和條件邊addConditionalEdge。AgentState 是這個(gè)流程的“黑板上寫(xiě)的東西”——用戶需求、生成的代碼、評(píng)審意見(jiàn)、循環(huán)次數(shù)都在這個(gè)狀態(tài)對(duì)象里傳遞。常見(jiàn)坑是新手把狀態(tài)設(shè)計(jì)成可變對(duì)象一邊跑一邊改并發(fā)場(chǎng)景下一改就串號(hào)。public class AgentState { private final String userRequirement; private final String generatedCode; private final String reviewComment; private final boolean approved; private final int iterationCount; public AgentState copyWith(String newCode, String newComment, boolean newApproved) { return new AgentState(userRequirement, newCode, newComment, newApproved, iterationCount 1); } }節(jié)點(diǎn)實(shí)現(xiàn)只需要接收當(dāng)前狀態(tài)、返回新?tīng)顟B(tài)。例如 CodeGenNode 拿到 userRequirement 后調(diào)用模型生成代碼生成結(jié)果放進(jìn) copyWith 返回的新?tīng)顟B(tài)里。iterationCount是防止“代碼永遠(yuǎn)評(píng)審不通過(guò)”的保險(xiǎn)絲——ReviewNode 里如果發(fā)現(xiàn) iterationCount 超過(guò) 3直接強(qiáng)制 approved 為 true留一個(gè)明確的人工介入標(biāo)記。4.3 條件邊背后的“意圖識(shí)別”怎么做條件邊是工作流真正復(fù)雜的部分。它會(huì)根據(jù)當(dāng)前狀態(tài)判斷下一步走向這里最常見(jiàn)的設(shè)計(jì)是兩類(lèi)一類(lèi)是規(guī)則判定比如“評(píng)審結(jié)果是否通過(guò)”“金額是否超過(guò)閾值”代碼寫(xiě)死可解釋性強(qiáng)另一類(lèi)是需要模型裁決的比如讓模型判斷“用戶這個(gè)問(wèn)題是否需要查知識(shí)庫(kù)”這時(shí)候條件邊內(nèi)部就內(nèi)嵌了一次模型調(diào)用。這里的實(shí)現(xiàn)細(xì)節(jié)是模型打分結(jié)果要映射成離散的邊名不要直接用自由文本。常見(jiàn)做法是給模型一個(gè)枚舉讓它輸出一個(gè) JSON 字段String rawVerdict model.generate( 根據(jù)用戶問(wèn)題判斷是否需要檢索知識(shí)庫(kù)只返回 JSON: {\needSearch\: true/false}); boolean needSearch JsonPath.read(rawVerdict, $.needSearch); return needSearch ? search_kb : direct_answer;4.4 工作流如何被前端可視化編輯JSON 就是橋梁LangGraph4j 的圖定義本質(zhì)上是一張有向圖而前端可視化畫(huà)布編輯的也正是這張圖。平臺(tái)里把工作流定義統(tǒng)一成一個(gè) JSON 結(jié)構(gòu)節(jié)點(diǎn)列表、邊列表、每個(gè)節(jié)點(diǎn)的類(lèi)型和參數(shù)。前端拖拽生成這個(gè) JSON后端解析后動(dòng)態(tài)構(gòu)建 LangGraph4j 實(shí)例。節(jié)點(diǎn)類(lèi)型做三層收斂普通 LLM 節(jié)點(diǎn)填提示詞和模型參數(shù)、工具節(jié)點(diǎn)綁定已注冊(cè)的 Tool、邏輯節(jié)點(diǎn)條件判斷/循環(huán)/聚合。這就帶來(lái)一個(gè)版本問(wèn)題工作流 JSON 結(jié)構(gòu)會(huì)演化必須加 schemaVersion 字段后端做版本遷移。否則線上跑著 20 個(gè)應(yīng)用改了字段格式老的直接編譯失敗。這塊在避坑章節(jié)再展開(kāi)。5. 避坑從本地 Demo 到可用平臺(tái)最容易翻車(chē)的 5 個(gè)問(wèn)題5.1 模型根本不支持 ToolCalling但代碼沒(méi)做降級(jí)現(xiàn)象功能調(diào)試時(shí)工具調(diào)用一直不觸發(fā)或者模型回答里出現(xiàn)一大段 JSON 工具請(qǐng)求文本而不是結(jié)構(gòu)化請(qǐng)求。 原因接入的模型網(wǎng)關(guān)不支持該協(xié)議或者模型名配置到了不帶工具能力的小參數(shù)版本上。 解決在模型接入層做一個(gè)能力探測(cè)請(qǐng)求啟動(dòng)時(shí)用一條固定消息發(fā)起一次 tool call如果返回里沒(méi)有結(jié)構(gòu)化 request就把該模型標(biāo)記為“不支持工具”平臺(tái)側(cè)在智能體配置頁(yè)直接禁用或提示換模型。這比運(yùn)行時(shí)反復(fù)重試靠譜得多。5.2 工作流狀態(tài)對(duì)象設(shè)計(jì)成可變 Map并發(fā)時(shí)狀態(tài)互相覆蓋現(xiàn)象兩個(gè)用戶同時(shí)觸發(fā)同一個(gè)工作流A 用戶的評(píng)審意見(jiàn)出現(xiàn)在 B 用戶的生成代碼里。 原因狀態(tài)類(lèi)里用了共享的 HashMap 存放中間數(shù)據(jù)沒(méi)有做不可變拷貝。 解決強(qiáng)制使用不可變對(duì)象每次節(jié)點(diǎn)返回新?tīng)顟B(tài)實(shí)例如果有大對(duì)象需要傳遞在節(jié)點(diǎn)側(cè)只傳引用 ID具體內(nèi)容存外部存儲(chǔ)避免整個(gè)對(duì)象在每一步都被復(fù)制一次導(dǎo)致內(nèi)存膨脹。5.3 SSE 流式響應(yīng)老是斷流或者首字遲遲不出現(xiàn)象前端 EventSource 收到幾個(gè)字就斷開(kāi)重連后更亂。 原因中間代理層默認(rèn)緩沖了響應(yīng)模型輸出攢到一定量才刷給瀏覽器另外代理超時(shí)時(shí)間太短模型思考時(shí)間長(zhǎng)一點(diǎn)就掐斷了。 解決服務(wù)端設(shè)置Cache-Control: no-cache和X-Accel-Buffering: no響應(yīng)頭從架構(gòu)上繞開(kāi)緩沖同時(shí)定期發(fā)一個(gè)注釋行或空格作為心跳防止空閑超時(shí)斷開(kāi)。5.4 多路召回的結(jié)果反而比單路更差現(xiàn)象做了向量關(guān)鍵詞融合之后檢索質(zhì)量明顯下降最相關(guān)的文檔排在了第五第六位。 原因兩路分?jǐn)?shù)沒(méi)做歸一化就線性加權(quán)向量相似度分?jǐn)?shù)總體偏高把關(guān)鍵詞那路的優(yōu)勢(shì)項(xiàng)全壓下去了。 解決放棄線性加權(quán)改成 RRF 倒數(shù)排名融合關(guān)鍵詞召回和向量召回各自保證 top 范圍里有真相關(guān)文檔再靠 RRF 將其抬上來(lái)。5.5 LangGraph4j 版本 API 變動(dòng)導(dǎo)致升級(jí)翻車(chē)現(xiàn)象升級(jí)小版本后addEdge 方法簽名變了編譯直接報(bào)錯(cuò)。 原因LangGraph4j 還在快速演進(jìn)API 穩(wěn)定性遠(yuǎn)不如 SpringBoot。如果你搜資料跟著老版本示例寫(xiě)半年后很可能跑不起來(lái)。 解決鎖定版本并記錄遷移步驟升級(jí)后先用固定的測(cè)試工作流批量回歸把工作流定義 JSON 和引擎解耦無(wú)論引擎怎么改業(yè)務(wù)方畫(huà)布里的 JSON 不變只需要適配層改解析代碼。6. Vue3 可視化編輯與一鍵部署最小實(shí)現(xiàn)與驗(yàn)收技巧可視化編輯的本質(zhì)是把工作流 JSON 變成看得見(jiàn)的節(jié)點(diǎn)和連線。Vue3 做這件事的核心優(yōu)勢(shì)是 Composition API 配合 reactive/ref 管理畫(huà)布狀態(tài)比 Vue2 時(shí)期用 data 和大對(duì)象操作清晰得多。最小實(shí)現(xiàn)只需要三塊一個(gè)節(jié)點(diǎn)面板可拖出不同類(lèi)型的節(jié)點(diǎn)、一個(gè)畫(huà)布放置和連線、一個(gè)屬性面板編輯選中節(jié)點(diǎn)的參數(shù)const nodes: RefFlowNode[] ref([]) const edges: RefFlowEdge[] ref([]) function addNode(type: string, position: { x: number; y: number }) { nodes.value.push({ id: crypto.randomUUID(), type, // llm | tool | condition position, config: initConfigByType(type), }) } function toWorkflowJson(): WorkflowDefinition { return { schemaVersion: 1, nodes: nodes.value, edges: edges.value, } } function loadWorkflow(json: WorkflowDefinition) { nodes.value reactive(json.nodes) edges.value reactive(json.edges) }這里要提醒一句不要讓畫(huà)布組件直接持有業(yè)務(wù)數(shù)據(jù)。nodes 里存的是“圖的結(jié)構(gòu)”具體的提示詞、模型參數(shù)、工具名都放在每個(gè)節(jié)點(diǎn)的 config 里。一鍵部署的做法是把 toWorkflowJson() 的產(chǎn)物交給后端接口后端將其持久化并構(gòu)建 LangGraph4j 實(shí)例運(yùn)行時(shí)的模型 API Key、知識(shí)庫(kù)連接串全部通過(guò)環(huán)境變量注入工作流 JSON 里不出現(xiàn)任何敏感信息。驗(yàn)收時(shí)我會(huì)固定用 20 個(gè)典型場(chǎng)景跑一遍離線回放對(duì)比每個(gè)節(jié)點(diǎn)的入?yún)⒊鰠⑹欠穹项A(yù)期再放開(kāi)給業(yè)務(wù)方試用。這是我踩出來(lái)的習(xí)慣——以前總覺(jué)得線上能跑就是好后來(lái)一次升級(jí)把評(píng)審節(jié)點(diǎn)跑丟了只能連夜回滾。從那以后每次改工作流引擎我都先過(guò)一遍回放再上線。希望這套路徑和這些坑能幫你少走一段彎路。本文還有配套的精品資源點(diǎn)擊獲取