:RAG 與 Tool Calling 構(gòu)建崗位分析系統(tǒng))
1. 為什么我要用 Spring AI 做一套崗位分析系統(tǒng)先把結(jié)論擺在前面這套系統(tǒng)的核心目標(biāo)是把一堆非結(jié)構(gòu)化的招聘 JD職位描述文本通過RAG檢索增強生成加Tool Calling工具調(diào)用兩條腿走路自動產(chǎn)出結(jié)構(gòu)化的崗位畫像——包括技能棧分布、薪資區(qū)間、經(jīng)驗要求、崗位間的技能關(guān)聯(lián)等等。聽起來像是又一個 RAG Demo但真正動手之后你會發(fā)現(xiàn)純 RAG 在崗位分析這個場景里會撞墻必須靠 Tool Calling 補位。我為什么會選 Spring AI 而不是 LangChain4j 或者直接上 Python 那套原因很實際我的主業(yè)務(wù)系統(tǒng)是 Spring Boot 寫的JD 數(shù)據(jù)來自內(nèi)部招聘管理系統(tǒng)數(shù)據(jù)庫、緩存、定時任務(wù)全在 Java 生態(tài)里。如果為了做 RAG 單獨起一個 Python 服務(wù)光是數(shù)據(jù)同步、鑒權(quán)打通、部署運維就夠我喝一壺。Spring AI 的價值就在于它把向量庫、ChatClient、Embedding、Function Calling 這些能力做成了 Spring 風(fēng)格的 Bean直接注入就能用和現(xiàn)有 Spring Boot 項目是無縫的。這套系統(tǒng)適合誰參考如果你是有 Spring Boot 基礎(chǔ)、想在自己的業(yè)務(wù)系統(tǒng)里嵌入 AI 能力的后端開發(fā)或者你正在做招聘、HR SaaS、人才盤點這類產(chǎn)品想加一個智能崗位分析模塊那這篇內(nèi)容基本可以照著抄。如果你完全沒接觸過 Spring Boot建議先把 Bean 注入、自動配置這些基礎(chǔ)過一遍再回來不然中間很多為什么這么寫你會看得云里霧里。我踩過的最大一個坑是一開始天真地以為把 JD 塞進(jìn)向量庫用戶問什么就檢索什么就完事了。結(jié)果用戶問幫我對比一下 Java 后端和 Go 后端這兩個崗位方向近半年的技能要求變化純 RAG 檢索出來的是一堆零散片段LLM 拼出來的答案驢唇不對馬嘴。這就是典型的RAG 瓶頸——它擅長找相似不擅長做聚合、做計算、做對比。而 Tool Calling 恰好能補上這塊讓模型自己決定我需要調(diào)用一個統(tǒng)計工具去算技能詞頻而不是硬靠檢索。下面我按真實搭建順序把整體設(shè)計、核心細(xì)節(jié)、實操過程、踩坑排查四塊拆開講中間會穿插大量參數(shù)選擇和取舍邏輯。2. 系統(tǒng)整體設(shè)計與技術(shù)選型拆解2.1 為什么是 RAG Tool Calling 而不是純 RAG先講清楚這兩者的分工不然后面代碼你會看不懂為什么這么設(shè)計。RAG 負(fù)責(zé)知識召回把歷史 JD、崗位說明書、技能詞典這些文本切片、向量化、存進(jìn)向量庫。用戶提問時先做相似度檢索把最相關(guān)的片段喂給大模型作為上下文。它解決的是模型不知道我們公司內(nèi)部崗位數(shù)據(jù)這個問題。Tool Calling 負(fù)責(zé)確定性計算與外部動作比如統(tǒng)計某個技能在近 100 條 JD 里出現(xiàn)的頻次、計算薪資中位數(shù)、按經(jīng)驗?zāi)晗薹纸M、查詢數(shù)據(jù)庫里某個崗位的在招數(shù)量。這些活兒如果交給 LLM 去心算它要么算錯要么編造。Tool Calling 的本質(zhì)是讓模型輸出一個結(jié)構(gòu)化的調(diào)用意圖由我們的 Java 代碼去執(zhí)行真正的邏輯再把結(jié)果回傳給模型組織語言。我實測下來純 RAG 在崗位分析場景的hit rate命中率大概只有六成左右尤其是涉及對比趨勢排名這類問題時幾乎必掛。加上 Tool Calling 之后這類問題的可用率能拉到九成以上。所以這不是要不要加的問題而是必須加。2.2 技術(shù)棧清單與選型理由組件選型選型理由應(yīng)用框架Spring Boot 3.x主業(yè)務(wù)生態(tài)一致自動配置省心AI 框架Spring AIBean 化集成ChatClient/Embedding 開箱即用大模型通義千問qwen-plus / qwen-max中文崗位文本理解好Function Calling 支持穩(wěn)定向量庫開發(fā)期用 SimpleVectorStore生產(chǎn)用 PGVector開發(fā)零依賴生產(chǎn)復(fù)用現(xiàn)有 PostgreSQLEmbedding通義千問 text-embedding-v3中文語義向量質(zhì)量夠用維度 1024文本切片TokenTextSplitter按 token 切避免超長截斷緩存Caffeine高頻查詢結(jié)果緩存降低模型調(diào)用成本這里重點說兩個選型決策。第一個是向量庫。很多人一上來就裝 Milvus 或者 Chroma我建議開發(fā)階段先用 Spring AI 自帶的SimpleVectorStore它就是個內(nèi)存 Map重啟數(shù)據(jù)就沒了但勝在零配置、啟動快調(diào) prompt 和切片策略的時候特別爽。等邏輯跑通了再切到 PGVector——因為你大概率已經(jīng)有 PostgreSQL 了加個擴展就行不用額外維護(hù)一套向量數(shù)據(jù)庫集群。這個先內(nèi)存后持久化的路徑能幫你省掉至少兩天的環(huán)境折騰。第二個是模型。我選通義千問不是因為它最強而是因為它在中文 JD 這種半結(jié)構(gòu)化、術(shù)語密集的文本上表現(xiàn)穩(wěn)定而且 Function Calling 的 JSON 輸出格式比較規(guī)矩不容易出現(xiàn)模型返回一堆廢話導(dǎo)致解析失敗的情況。qwen-plus 用于日常問答qwen-max 留給復(fù)雜的對比分析任務(wù)按需切換能省不少 token 成本。2.3 整體數(shù)據(jù)流設(shè)計整個系統(tǒng)的數(shù)據(jù)流我畫成文字版方便你對照理解離線階段JD 原始文本 → 清洗去 HTML、去重復(fù)→ 切片 → Embedding → 存入向量庫。在線問答階段用戶提問 → 檢索向量庫拿 Top-K 片段 → 組裝 prompt系統(tǒng)提示 檢索上下文 用戶問題 可用工具列表→ 發(fā)給模型。工具調(diào)用階段模型判斷需要工具 → 返回工具名和參數(shù) → Java 側(cè)執(zhí)行工具 → 結(jié)果回傳模型 → 模型生成最終答案。緩存階段對高頻、結(jié)果穩(wěn)定的查詢做 Caffeine 緩存命中直接返回。這個流程里最容易出問題的是第 2 步的 prompt 組裝和第 3 步的工具描述。工具描述寫得好不好直接決定模型會不會該調(diào)的時候不調(diào)不該調(diào)的時候亂調(diào)。這個后面會專門講。3. 核心細(xì)節(jié)解析與實操要點3.1 JD 文本切片切多長、怎么切切片是 RAG 的地基切不好后面全白搭。我一開始用固定 500 字符切結(jié)果一條 JD 里的崗位職責(zé)和任職要求被從中間劈開檢索出來的片段語義不完整模型經(jīng)常答非所問。后來我改成按語義段落優(yōu)先、token 兜底的策略先用正則按崗位職責(zé)任職要求加分項這類小標(biāo)題切大塊每個大塊再用TokenTextSplitter按 token 切chunkSize 設(shè) 400overlap 設(shè) 80。為什么是 400 和 80因為通義千問的 embedding 模型對單段文本有長度限制400 token 大約對應(yīng) 600-800 個中文字符正好覆蓋一段完整的職責(zé)描述。overlap 設(shè) 80 是為了防止關(guān)鍵信息剛好卡在切片邊界被切斷——這個 20% 左右的重疊比例是我試了好幾組參數(shù)后比較穩(wěn)的。TokenTextSplitter splitter new TokenTextSplitter(400, 80, 5, 10000, true); ListDocument chunks splitter.apply(documents);注意TokenTextSplitter的構(gòu)造參數(shù)順序是 chunkSize、minChunkSizeChars、minChunkLengthToEmbed、maxNumChunks、keepSeparator不同版本可能有差異升級 Spring AI 后一定要重新核對簽名我就因為版本升級參數(shù)錯位導(dǎo)致切片全亂過一次。3.2 向量檢索的 Top-K 與相似度閾值檢索階段有兩個關(guān)鍵參數(shù)topK和similarityThreshold。topK我設(shè)的是 5。設(shè)太小比如 2召回不足模型沒素材設(shè)太大比如 10會引入大量噪聲片段反而干擾模型判斷還費 token。similarityThreshold設(shè) 0.7。低于這個相似度的片段直接丟棄寧可少給也不能給錯。SearchRequest request SearchRequest.query(question) .withTopK(5) .withSimilarityThreshold(0.7);實測下來崗位分析這種問題Top-5 基本能覆蓋到相關(guān) JD 的核心信息。如果你發(fā)現(xiàn)模型總是漏掉某些信息先別急著調(diào)大 topK先檢查切片是不是把關(guān)鍵信息切碎了——十有八九是切片的問題不是檢索的問題。3.3 Tool Calling 的工具設(shè)計原則這是整套系統(tǒng)里我最想強調(diào)的部分。工具設(shè)計有三個原則違反任何一個都會讓模型犯傻。原則一工具職責(zé)單一名字要自解釋。不要搞一個叫analyzeJob的萬能工具模型根本不知道它內(nèi)部干了啥。要拆成countSkillFrequency、calculateSalaryMedian、groupByExperience這種一看名字就知道干嘛的。原則二參數(shù)描述要寫清楚單位和格式。比如薪資工具的參數(shù)要明確寫單位元/月整數(shù)。我一開始沒寫單位模型傳了個15k進(jìn)來Java 側(cè)解析直接拋異常。原則三工具返回結(jié)果要結(jié)構(gòu)化且簡短。返回一大坨 JSON 給模型它會抓不住重點。我一般返回精簡后的字符串或小對象比如Java: 87次, Spring Boot: 65次, MySQL: 52次。Bean Description(統(tǒng)計指定技能關(guān)鍵詞在崗位庫中出現(xiàn)的頻次參數(shù) skill 為技能名稱如 Java) public FunctionSkillRequest, String countSkillFrequency() { return request - jobAnalysisService.countSkill(request.skill()); }Spring AI 里用Description注解描述工具這個描述就是模型判斷要不要調(diào)這個工具的唯一依據(jù)所以一定要寫得像給同事交代任務(wù)一樣清楚。3.4 系統(tǒng)提示詞System Prompt的寫法系統(tǒng)提示詞決定了模型的人設(shè)和行為邊界。我的寫法是這樣的你是一個崗位分析助手。你的職責(zé)是基于檢索到的崗位數(shù)據(jù)回答用戶問題。 規(guī)則 1. 涉及統(tǒng)計、計算、排名的問題必須調(diào)用工具不要自己估算。 2. 回答必須基于檢索到的數(shù)據(jù)數(shù)據(jù)中沒有的信息要明確說數(shù)據(jù)中未提及。 3. 輸出用簡潔的中文涉及數(shù)據(jù)時用表格呈現(xiàn)。這三條規(guī)則分別解決了三個高頻問題模型亂算、模型編造、模型輸出啰嗦。尤其是第二條不加的話模型特別愛腦補崗位要求這在招聘場景里是致命的。4. 實操過程與核心環(huán)節(jié)實現(xiàn)4.1 項目初始化與依賴配置先建一個標(biāo)準(zhǔn)的 Spring Boot 3.x 項目然后在pom.xml里加 Spring AI 的依賴。這里要注意Spring AI 的版本迭代很快建議用 milestone 或正式發(fā)布版別用 snapshot不然今天能跑的代碼明天可能就編譯不過。dependency groupIdorg.springframework.ai/groupId artifactIdspring-ai-starter-model-qwen/artifactId /dependency dependency groupIdorg.springframework.ai/groupId artifactIdspring-ai-starter-vector-store-pgvector/artifactId /dependency然后在application.yml里配置模型和向量庫spring: ai: dashscope: api-key: ${DASHSCOPE_API_KEY} chat: options: model: qwen-plus temperature: 0.3 vectorstore: pgvector: index-type: HNSW distance-type: COSINE_DISTANCE dimensions: 1024temperature設(shè) 0.3 是因為崗位分析要的是穩(wěn)定、可復(fù)現(xiàn)的結(jié)果不需要模型發(fā)揮創(chuàng)造力。設(shè)太高你會發(fā)現(xiàn)同一個問題問兩次答案不一樣這在業(yè)務(wù)系統(tǒng)里是大忌。4.2 數(shù)據(jù)入庫從 JD 文本到向量入庫流程分四步讀取原始 JD、清洗、切片、寫入向量庫。public void ingest(ListJobDescription jobs) { ListDocument documents jobs.stream() .map(job - new Document( job.getContent(), Map.of(jobId, job.getId(), title, job.getTitle()) )) .toList(); ListDocument chunks tokenTextSplitter.apply(documents); vectorStore.add(chunks); }這里有個細(xì)節(jié)我在Document的 metadata 里存了jobId和title。為什么因為檢索出來的片段如果不知道來自哪個崗位后續(xù)做聚合分析時就沒法溯源。metadata 是 RAG 里最容易被忽視但極其重要的東西它讓你的檢索結(jié)果可追溯。實操心得入庫時一定要做去重。招聘系統(tǒng)的 JD 經(jīng)常有大量重復(fù)同一個崗位多渠道發(fā)布不去重的話向量庫里全是冗余檢索時 Top-5 可能全是同一條 JD 的切片白白浪費召回名額。我一般用 JD 內(nèi)容的 MD5 做去重鍵。4.3 檢索增強問答的完整實現(xiàn)問答的核心是ChatClient加QuestionAnswerAdvisor。Spring AI 提供了開箱即用的 Advisor能自動完成檢索 拼上下文這一步。Bean public ChatClient chatClient(ChatClient.Builder builder, VectorStore vectorStore) { return builder .defaultSystem(SYSTEM_PROMPT) .defaultAdvisors(new QuestionAnswerAdvisor(vectorStore, SearchRequest.query().withTopK(5).withSimilarityThreshold(0.7))) .defaultFunctions(countSkillFrequency, calculateSalaryMedian, groupByExperience) .build(); }defaultFunctions這一步就是把工具注冊給模型。注意這里傳的是 Bean 的名字所以你的 Function Bean 命名要和這里一致否則模型根本看不到工具。調(diào)用的時候String answer chatClient.prompt() .user(幫我統(tǒng)計一下近半年 Java 崗位最需要的 5 個技能) .call() .content();模型收到問題后會先判斷這需要統(tǒng)計然后調(diào)用countSkillFrequency拿到結(jié)果后再組織語言輸出。整個過程對調(diào)用方是透明的你只管問工具調(diào)用是模型自己決定的。4.4 工具的具體實現(xiàn)以技能頻次統(tǒng)計為例工具的實現(xiàn)要薄——只做數(shù)據(jù)查詢和計算不做任何自然語言處理因為語言處理是模型的活兒。Service public class JobAnalysisService { private final JdbcTemplate jdbcTemplate; public String countSkill(String skill) { String sql SELECT COUNT(*) FROM job_skill WHERE skill_name ? AND create_time NOW() - INTERVAL 6 months ; Integer count jdbcTemplate.queryForObject(sql, Integer.class, skill); return skill 在近半年出現(xiàn) count 次; } public String calculateSalaryMedian(String jobTitle) { String sql SELECT PERCENTILE_CONT(0.5) WITHIN GROUP (ORDER BY salary_min) FROM job_description WHERE title ? ; Double median jdbcTemplate.queryForObject(sql, Double.class, jobTitle); return jobTitle 的薪資中位數(shù)為 median 元/月; } }用PERCENTILE_CONT(0.5)算中位數(shù)而不是平均值是因為薪資數(shù)據(jù)分布偏斜嚴(yán)重平均值容易被極端值帶偏。這個細(xì)節(jié)很多教程不會講但在真實業(yè)務(wù)里很重要。4.5 緩存與性能優(yōu)化模型調(diào)用是這套系統(tǒng)里最貴、最慢的環(huán)節(jié)。我加了 Caffeine 緩存對技能頻次薪資中位數(shù)這類結(jié)果相對穩(wěn)定的查詢做緩存TTL 設(shè) 30 分鐘。Bean public CacheString, String analysisCache() { return Caffeine.newBuilder() .expireAfterWrite(30, TimeUnit.MINUTES) .maximumSize(1000) .build(); }為什么 TTL 是 30 分鐘而不是更長因為崗位數(shù)據(jù)是動態(tài)的新 JD 每天都在進(jìn)來緩存太久會導(dǎo)致分析結(jié)果滯后。30 分鐘是我權(quán)衡數(shù)據(jù)新鮮度和調(diào)用成本后的折中值。如果你的 JD 更新頻率低可以適當(dāng)延長。5. 常見問題與排查技巧實錄5.1 模型不調(diào)用工具怎么辦這是最高頻的問題。模型明明該調(diào)工具卻自己編了個答案。排查順序如下檢查工具是否真的注冊成功。在啟動日志里搜工具名沒注冊上模型當(dāng)然看不到。檢查工具描述是否清晰。Description寫得太模糊模型判斷不了該不該調(diào)。檢查系統(tǒng)提示詞有沒有強制要求。加一句涉及統(tǒng)計必須調(diào)用工具能顯著提升調(diào)用率。檢查模型本身是否支持 Function Calling。不是所有模型都支持選型時要確認(rèn)。我遇到過一次工具描述寫的是處理技能相關(guān)模型完全不知道這工具能干嘛改成統(tǒng)計指定技能在崗位庫中的出現(xiàn)頻次之后立刻就正常調(diào)用了。5.2 檢索結(jié)果不相關(guān)如果檢索出來的片段和問題八竿子打不著按這個順序查現(xiàn)象可能原因解決方向檢索結(jié)果全是無關(guān) JD切片太碎或太大調(diào)整 chunkSize 和 overlap相關(guān) JD 檢索不到相似度閾值太高降低 threshold 到 0.6 試試結(jié)果重復(fù)度高數(shù)據(jù)沒去重入庫前做 MD5 去重中文語義匹配差embedding 模型不適配換中文優(yōu)化過的 embedding 模型5.3 工具調(diào)用參數(shù)解析失敗模型傳參格式和你的 Java 對象對不上會拋反序列化異常。常見的是模型傳了帶單位的字符串15k而你的字段是 Integer。解決辦法有兩個一是參數(shù)描述里寫死格式要求二是 Java 側(cè)做容錯解析把15k轉(zhuǎn)成 15000。我建議兩個都做雙保險。5.4 響應(yīng)太慢一次完整的 RAG Tool Calling 鏈路涉及檢索 模型判斷 工具執(zhí)行 模型生成四步慢是正常的。優(yōu)化手段檢索和工具執(zhí)行并行化如果互不依賴高頻查詢走緩存簡單問題用 qwen-plus復(fù)雜問題才用 qwen-max控制檢索片段數(shù)量別一股腦塞給模型。5.5 獨家避坑清單別在循環(huán)里調(diào)模型。我見過有人對 100 條 JD 逐條調(diào)模型做分類又慢又貴。批量處理要合并請求。metadata 一定要存業(yè)務(wù)主鍵。不然檢索結(jié)果沒法溯源做聚合分析時你會哭。工具返回結(jié)果別太長。模型上下文有限返回一大坨數(shù)據(jù)會擠掉檢索上下文。prompt 里的規(guī)則要少而精。寫十幾條規(guī)則模型反而記不住三條核心規(guī)則足夠。上線前一定要做回歸測試。模型行為會隨版本更新變化今天好用的 prompt 明天可能就失效。6. 這套系統(tǒng)還能怎么擴展跑通基礎(chǔ)版本之后我陸續(xù)加了幾個擴展效果都不錯這里分享給你。第一個是 GraphRAG 思路的引入。純向量檢索只能找到相似文本但崗位之間其實有技能關(guān)聯(lián)這種圖結(jié)構(gòu)關(guān)系。比如會 Spring Boot 的人大概率也會 MySQL。我把技能共現(xiàn)關(guān)系抽出來存成圖檢索時結(jié)合圖遍歷能回答從 Java 轉(zhuǎn) Go 需要補哪些技能這類關(guān)聯(lián)性問題。這就是熱詞里說的 ontology RAG 的落地場景。第二個是 NL2SQL 的補充。有些問題本質(zhì)是數(shù)據(jù)庫查詢比如薪資大于 30k 的崗位有幾個。與其讓模型在向量庫里瞎找不如讓它生成 SQL 直接查庫。Spring AI Alibaba 的 NL2SQL 能力可以接進(jìn)來和 RAG 形成互補——結(jié)構(gòu)化問題走 SQL非結(jié)構(gòu)化問題走向量檢索。第三個是分析結(jié)果的可視化。把工具返回的統(tǒng)計數(shù)據(jù)直接喂給前端圖表庫用戶看到的不只是文字答案還有技能分布餅圖、薪資區(qū)間柱狀圖。這一步讓系統(tǒng)從問答工具升級成分析平臺。我個人在實際操作中的體會是RAG 和 Tool Calling 的組合不是簡單的11而是讓系統(tǒng)具備了既能查資料又能算數(shù)據(jù)的雙重能力。崗位分析只是其中一個應(yīng)用場景同樣的架構(gòu)套到客服知識庫、合同審查、競品分析上邏輯是通的。真正花時間的從來不是寫代碼而是調(diào) prompt、調(diào)切片、調(diào)工具描述這些臟活——但這些臟活恰恰決定了系統(tǒng)到底能不能用。