news 2026/10/1 5:17:42

基于Spring AI实现RAG与Tool Calling的岗位分析系统落地实践

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
基于Spring AI实现RAG与Tool Calling的岗位分析系统落地实践

先说一个背景。我之前在团队里经常要做岗位分析,但每次拿到一批新的招聘需求文档,都要人工逐条拆解技能要求、资历门槛、职责重点,几十个岗位下来,半天就没了。后来我试过写死规则匹配,效果很差,因为岗位描述里的表述太灵活了,同一件事有几十种说法。最后我决定用 Spring AI 从零搭一套岗位分析系统,核心思路就两条:用 RAG 把岗位知识库喂给大模型,用 Tool Calling 让大模型在分析过程中真正调用代码去算数据、查资料。整套系统从零开始写,前后跑了两个月,中间踩了不少坑,今天把整个落地过程梳理成一篇完整的记录,希望能帮到正在考虑用类似方案做文本分析系统的朋友。

这套方案适合谁?如果你已经在用 Spring Boot,对 AI 应用感兴趣但不想脱离 Java 生态,或者你正在做招聘、人才分析、文本挖掘相关的项目,那这篇文章可以直接当参考。即使你暂时不做岗位分析,RAG 和 Tool Calling 的组合套路也能迁移到合同审查、政策问答、竞品分析等场景里。

1. 整体架构与设计思路

1.1 为什么最后选了 Spring AI 而不是 LangChain4j 或自己拼 SDK

说实话,一开始我也纠结过。LangChain4j 出现得早,社区案例多,Python 生态里 LangChain 的名气也大,很多教程直接照搬。但我的项目背景是 Spring Boot,整个团队都是 Java 工程师,如果引入 Python 服务,等于要额外维护一套跨语言调用链,开发效率和排障成本都会翻倍。Spring AI 的优势在于它是 Spring 官方生态的一部分,依赖注入、自动装配、配置管理等机制可以直接复用,我们不需要在 Java 和 Python 之间来回折腾。

当然,Spring AI 也不是没有缺点。版本迭代太快,我在开发期间就经历了 0.8.x 到 1.0.0 的跨越,很多 API 在版本升级后直接变了,网上搜到的旧教程基本没法照着用。所以我的建议是,如果你决定用 Spring AI,就把官方文档当主要参考,Google 搜索结果只能当辅助线索。另外,如果项目只做纯聊天机器人,LangChain4j 会更顺手一些,但一旦涉及工具调用、结构化输出、复杂编排,Spring AI 的接口设计反而更贴合 Spring Boot 工程的习惯。

1.2 RAG 和 Tool Calling 在这个系统里分别解决什么问题

RAG 负责的是“知识供给”。岗位分析系统首先得知道每个岗位 JD 里写了什么,但大模型本身并不知道我们手上的岗位数据,也不可能把几十份 JD 全部塞进上下文,那样既浪费 token,又容易让模型抓不住重点。RAG 的思路就是先把岗位文档切成片段,向量化后存进向量数据库,分析时先根据用户提问做相似度检索,把最相关的几个片段连同问题一起交给大模型。这样一来,模型既拿到了证据,又不用被无关信息干扰。

Tool Calling 负责的是“行动能力”。岗位分析不只是读文本,还经常要做计算和查证。比如 JD 里写“熟悉 Redis 缓存”,但资深岗位可能要求“熟悉分布式缓存高可用方案”,那我们就得让模型先判断两个表述是否在一个层次上,遇到拿不准的词汇,调用外部工具去查技能库,或者调用统计函数去算岗位技能出现的频率。没有 Tool Calling,模型只能凭直觉作答,有了 Tool Calling,模型就能自主决定什么时候调用计算、什么时候调用查询,再基于返回结果继续分析。

1.3 数据流向与整体时序

整套系统的调用链路是这样的:用户输入一个职位问题,比如“帮我分析这份 Java 岗位 JD 的核心技能要求”,系统先把问题向量化,到向量库里检索相关岗位片段,拿到检索结果后,组装成系统提示词和用户提示词,交给大模型。大模型分析过程中,如果发现需要额外信息,会返回一个工具调用请求,Spring AI 的执行器接住这个请求,执行对应的 Java 方法,把结果再传回给模型,模型最终生成分析结论。

这个链路最关键的时序点是工具的“第二轮调用”。第一次调用模型可能只做了一个工具请求,拿到结果后还要再调用一次才能给出最终答案。Spring AI 的 ChatClient 可以自动处理多轮工具回调,但我们最开始不太熟悉这个机制,结果出现了好几轮死循环的问题,这个后面会在踩坑部分专门讲。

2. 岗位数据准备与 RAG 管道设计

2.1 数据来源与清洗:岗位 JD 是最难处理的文本之一

岗位 JD 看起来是结构化文本,其实噪声极大。我收集的数据来自多个渠道,有的 JD 是 PDF 导出的,有的简历系统里复制出来的,甚至还有带表格和图片的。文本里充斥着公司介绍、福利待遇、团队文化这些跟技能分析关系不大的内容。所以第一步不是向量化,而是清洗。

我做了三件很基础但收益很大的事:第一,统一编码为 UTF-8,去掉各种不可见字符和换行符的混乱组合;第二,用规则把明显的噪声块剔除,比如“公司福利:五险一金、弹性工作、下午茶”这类关键词命中的段落直接过滤;第三,把每个岗位的数据切分成结构化字段,比如岗位名称、城市、经验要求、薪资范围、职责描述、任职要求,其中任职要求是后续分析的核心。清洗结束后,我先做了一轮人工抽检,确保没有把关键技术信息误删。

清洗这一步千万别偷懒。我有一个同事直接拿着原始 PDF 做切块,结果检索出来的片段全是“我们正在寻找一位充满激情的技术伙伴”这类空洞表述。RAG 的效果强烈依赖数据质量,模型强大的推理能力弥补不了脏数据带来的信息缺失。

2.2 切块策略:固定长度切块的问题比想象中大

切块是 RAG 流程里最容易被低估的一环。固定长度切块,比如每 500 字切一块,听起来简单,实际用起来问题很大:一个完整的技术栈列表可能被切成两半,导致模型只看到“Java、Spring”漏掉后面的“MySQL、Kafka”;两个完全无关的岗位要求也可能因为字数凑巧被切进同一块,造成混淆。

我最终采用的是按语义段落切块的策略。岗位 JD 本身有天然的分段边界,比如每个要求通常以“1.”“2.”开头,或以分号为颗粒度分隔。我先按换行符粗切,再把短句合并成大块,保证每个块至少包含一个完整的能力描述。块的长度我控制在 300 到 600 字之间,既不会因为太短而丢上下文,也不会因为太长而引入噪声。切完块之后,我给每个块加上元数据,比如所属岗位 ID、块序号、来源文件名称,这样检索到的块可以轻松回溯到原始岗位。

这里有个参数需要解释:为什么块之间要有重叠?因为过短的边界可能导致检索召回时漏掉关键句子的后半部分。我给相邻块预留了大约 50 字的重叠区域,确保跨块的句子在检索时不会断。这个经验来自实际测试,对比固定长度无重叠切块,命中率大概提升了十几个百分点。

2.3 Embedding 选型与向量库:本地优先,按成本切换

Embedding 模型的选择直接影响检索质量。我第一阶段用了 BAAI 的 bge-small-zh-v1.5,这个模型对中文支持好,维度是 512,部署成本低,在 8GB 内存的机器上就能跑。后来数据量变大,需要更高的区分度,我切换到了 text-embedding-v3。这里有一个重要教训:不同 embedding 模型产出的向量不能直接混用。我在测试阶段用旧模型存了一批向量,后来切换新模型后没有重新向量化,结果检索效果非常差,因为新旧向量分布差异太大,相似度计算完全失灵。换模型一定要重建索引,没有例外。

向量库我最初用的本地 Chroma,因为部署简单,直接嵌在 Spring Boot 进程里。但数据量到几万个块之后,本地库的检索延迟开始明显上升,而且每次发布新版本重新构建索引也很麻烦。后来我把向量库存到了服务端,通过 API 调用,虽然多了一层网络开销,但胜在索引管理方便,团队其他人也能共用一套知识库。对于个人项目或团队初期,本地向量库完全够用;一旦你要做成多租户或持续更新数据,尽快迁到独立向量库服务。

3. Tool Calling 设计:让模型真正具备“动手能力”

3.1 先分清哪些任务必须用工具,哪些别过度设计

设计 Tool Calling 的第一原则是“能不用就不用”。ChatClient 原生就能完成简单的基于上下文的问答,强行引入工具只会增加延迟和出错概率。在岗位分析系统里,我把任务分成了三层:第一层是纯文本理解,比如“提取这个岗位的主要职责”,直接让模型回答;第二层是需要外部知识补全的,比如模型遇到一个没见过的技术名词,需要查技能库,这种设计成工具;第三层是需要精确计算的,比如统计某类技能在多条 JD 中出现的频率、薪水分位数的计算,这种也设计成工具。

我总共实现了三个工具:技能库查询工具、岗位数量统计工具、薪资处理工具。技能库查询工具用于解决模型知识盲区,岗位数量统计工具用于回答“哪些岗位要求微服务经验”这类计数问题,薪资处理工具用于把文本薪资范围解析成结构化数字并计算分位数。工具数量控制在三个是有意的设计,太多会让模型难以决策,太少又解决不了实际问题。

3.2 工具描述与参数 Schema 的写法:这里写得好不好,直接决定调用成败

Tool Calling 里最容易翻车的地方不是工具的实现代码,而是工具的描述。模型靠描述决定什么时候调用工具,描述写得含糊,模型就会乱来,或者干脆不调用。我用的是 Spring AI 的 @Tool 注解,每个工具写清楚“功能边界”“参数含义”“返回内容格式”。

比如岗位数量统计工具,我之前写的描述是“统计岗位数量”,结果模型在询问薪资分布时也去调用它,因为模型觉得“统计”跟所有数据问题相关。后来我改成“统计满足特定条件的岗位数量,条件参数为技能关键词,返回结果为符合条件的岗位总数”,模型调用准确率立刻上来了。参数 Schema 也要写得明确,比如技能关键词参数我标注为“多个关键词用逗号分隔”,如果模型传入的是一个列表字符串,工具内部再做一次解析,两边对齐。这部分的经验是:写 Tool 描述时,站在模型的角度想问题,它只知道你描述的字面意思,不会自动理解“统计”和“计算分位数”的区别。

3.3 回调与结果注入:Spring AI 的自动工具调用流程

Spring AI 提供了两种工具调用方式:手动接收工具请求再处理,和自动执行工具并返回结果。在 ChatClient 里,通过.tools(技能库查询工具, 岗位数量统计工具, 薪资处理工具)注册工具后,当模型返回工具调用意图时,Spring AI 会自动执行对应的 Java 方法,并把返回结果作为消息回传给模型,继续生成回答。这个机制在单次调用场景下很稳定,多轮工具调用时则需要设置最大调用次数,防止模型陷入循环。

我在实际使用中发现,有些模型在工具调用后,对“工具结果如何融入最终答案”这件事处理得不够好。比如工具返回了 12 个岗位满足条件,但模型生成的回答里写的是“大约 10 个”。解决办法是在工具返回结果里添加一个说明字段,比如“返回数据为精确数值,请直接引用”,引导模型把数值原样使用。这个方法虽然简单,但对输出准确率的提升非常明显。

4. 环境准备、核心代码与全流程落地

4.1 环境与依赖

我的开发环境是 JDK 17、Spring Boot 3.3.x,使用的 Spring AI 版本是 1.0.0。依赖主要引入 spring-ai-starter-model-openai、spring-ai-starter-vector-store-chroma 和 spring-ai-starter-tool-calling。如果你用本地模型,可以换成 spring-ai-starter-model-ollama,但要注意工具调用对模型能力要求较高,小参数模型很可能不支持函数调用或调用格式不正确,建议至少使用支持 fn-call 的模型。

一个很重要的配置细节:Spring AI 的自动配置默认会为 ChatClient 注册工具,但需要开启spring.ai.retry.on-http-codes相关的重试参数。我保持默认,但关闭了超时重试,因为在工具调用场景下,盲目重试会导致工具被重复执行。比如技能库查询工具如果第一次超时重试,第二次可能返回重复数据,影响统计结果。

4.2 核心代码结构解析:数据准备、RAG 检索、ChatClient 组装

整个项目的核心类分为三层:数据准备层、检索增强层、对话执行层。数据准备层负责读取 JD 文档,清洗后切块,调用 Embedding 模型生成向量,存入向量库。检索增强层接收用户问题,做向量检索,返回相关片段。对话执行层组装 Prompt 和工具,调用大模型生成最终答案。

看一段实际的检索代码会更直观。使用 VectorStore 的相似性搜索方法,我设置了 TopK 为 5,即返回 5 个最相关的片段。这个数值不是拍脑袋定的,我实测过 TopK 从 1 到 10 的效果:TopK 太小,关键信息可能遗漏;TopK 太大,噪声增加导致答案冗长。5 到 6 个片段是比较合适的区间。检索公众号时可以再设置一个相似度阈值,低于阈值的片段直接丢弃,能有效过滤不相关内容。

ChatClient 的组装是另一个关键点。我用 Builder 模式创建了 ChatClient,同时指定模型和工具列表。Prompt 模板里先声明“你是岗位分析助手”,再定义回答结构:技能要求、经验匹配、风险提示、薪资参考。模板中我用占位符{context}传入检索到的岗位片段,模型参考片段生成答案。这里有个技巧:在模板末尾强调“如果上下文片段中没有相关信息,请明确说明”,可以大幅降低模型编造的风险。

4.3 关键参数设置:温度、Token 上限与 TopK

大模型参数对输出质量的影响比想象中大。我的配置里,温度设置是 0.2。为什么这么低?岗位分析是偏客观的文档分析任务,我不希望模型自由发挥讲太多无关内容,低温度能让输出更确定。在做“技能提取”这类事实性任务时,我甚至把温度设为 0。但如果任务是生成岗位分析报告,需要模型自己组织语言,温度 0.3 到 0.5 会更自然。

Token 上限我设为 1024,因为岗位分析的回答通常不需要特别长,设定上限能控制成本,也能倒逼模型精简表达。不过要注意,如果分析内容涉及多个岗位的横向对比,1024 可能不够,这时候我会临时把上限提高到 2048。检索时使用的 TopK 和相似度阈值也要注意调整。阈值我常用 0.45,但这个值跟 Embedding 模型相关,换模型之后必须重新标定,否则会出现召回片段虽然相关但相似度均低于阈值的情况。

4.4 一个完整的调用链路示例:问题进来之后到底发生了什么

我用一个具体例子串一遍完整流程。用户提问:“Java 岗位中要求微服务经验的有多少,平均薪资大概多少?”系统先把这个提问向量化,到向量库检索出 6 个相关岗位片段。ChatClient 把这些片段组装进模板,连同问题发给大模型。模型看到问题里有两个任务:计数和薪资统计,于是依次调用两个工具,先拿到岗位数量 7,再拿到薪资分位数数据。工具返回结果后,ChatClient 自动回调模型,模型结合工具结果生成最终回答:“共有 7 个 Java 岗位明确要求微服务经验,薪资中位数为 25K,其中 3 个岗位标注了高并发场景下的微服务要求。”

在这个调用链路里,有一个点特别值得复盘:模型先是根据上下文判断需要调用工具,然后工具结果被融合进第二轮生成。整个过程的耗时大概在 3 到 5 秒,如果工具调用涉及外部 HTTP 请求,耗时可能到 8 秒。必要的优化是给工具执行加缓存,同一个技能关键词在短时间内的查询结果直接复用。我这里用了一个简单的 ConcurrentHashMap 做 30 秒过期缓存,效果很好,明显降低了重复查询的频率。

5. 踩坑记录与排查心得

5.1 类型转换与 JSON 解析:工具调用最常见的崩溃点

工具调用失败的第一大类原因不是模型笨,而是 JSON 解析和类型转换出问题。模型返回的工具参数是 JSON 字符串,Spring AI 反序列化时如果遇到类型不匹配,比如参数定义为 Integer,模型传了字符串 “7”,就会抛异常。解决方案有两层:第一层在参数设计时尽量避免基本类型,多用字符串然后手动解析;第二层给所有工具方法加异常兜底,返回固定格式的错误信息给模型,让它自己调整参数重新发起调用。

我印象最深的一个坑是技能库查询工具的参数是 List 类型,模型有时候传过来的是一个 JSON 对象而不是数组,反序列化直接失败。后来我给参数描述里明确写了“参数为数组格式”,同时在工具实现里兼容了单字符串和数组两种格式,才彻底解决。总体来说,工具参数越简单越好,能用字符串就别用复杂对象,模型对复杂 Schema 的理解能力还远不够成熟。

5.2 Embedding 向量不一致:检索结果差到怀疑人生

这是我踩过最大的坑。开发初期我用 bge-small-zh 生成了向量,后来为了追求检索效果切成 text-embedding-v3,但没有重新向量化历史数据。结果就是,检索时用新模型给问题生成向量,在向量库里却是在跟旧模型生成的向量做相似度计算。由于两个模型的向量维度不同,代码直接报了向量维度不匹配;我调整维度后不报错了,但检索到的内容完全不相关。

后来我才意识到,不同的 Embedding 模型生成的向量分布差异极大,即使维度相同也不能混用。解决方案很简单:每次更换 Embedding 模型后,全量重建向量库,一个块都不能漏。这件事我后来写进了项目文档,还加了一个启动时校验的机制:系统记录当前使用的 Embedding 模型标识,如果与向量库元数据中的标识不一致,直接提示重建索引。如果你也在做 RAG,尽早加这个校验,能省掉很多排查时间。

5.3 工具循环调用与上下文膨胀:模型卡在死循环里出不来

工具调用还有一个隐藏问题:模型可能会因为工具结果不明确而重复调用同一个工具。岗位数量统计工具返回结果没有明确说明“这是最终结果”,模型就以为还需要补充查询,导致多轮工具调用,叠加中间消息后上下文快速膨胀,最终触发 Token 上限,输出被截断或者报错。

解决路径是双管齐下。第一,工具返回值里加“终态标记”,直接说明“该结果为最终查询结果,无需再次调用相同工具”;第二,设置最大工具调用次数为 3,超过次数就终止调用,让模型基于已有信息生成回答。Spring AI 提供了工具调用次数限制的参数,默认值不高,但一定要主动配置,尤其是在任务比较复杂的时候。我实际调优后发现,绝大多数正常分析场景只需要 1 到 2 次工具调用,如果超过 3 次,大概率是工具设计出了问题,需要回头检查描述或参数。

5.4 实测排查清单:如果你也遇到了同样的问题

我把排查过程整理成一张速查表,按问题现象定位原因,效率会高很多。

问题现象可能原因排查方向
检索结果与问题完全不相关Embedding 模型不一致或向量库未重建检查向量库元数据的模型标识,重建索引
模型回答中大量编造技能要求检索片段太少或阈值过低提高 TopK,提高相似度阈值,检查切块质量
工具一直没被调用工具描述不清晰或模型不支持工具调用改写工具描述,换支持函数调用的模型
工具调用报参数解析异常JSON 格式或类型不匹配简化参数类型,增加异常兜底
回答内容过长且抓不住重点Token 上限过高或 Prompt 引导不足降低输出上限,在模板中明确回答结构
多次调用同一工具导致超时工具返回结果未标明终态返回值加终态说明,设置最大工具调用次数
低版本模型工具调用格式错误模型对 fn-call 支持不全升级模型版本,或切换其他能力更强的模型

这张表是我在排障过程中逐步积累出来的。每次遇到问题,先定位到环节,再追溯参数和数据,不盲目调模型。整套系统迭代到后期,新增功能基本不怎么会踩新的坑了,因为大部分风险已经在前面的版本里暴露过一遍。

回到最初的目标,这套岗位分析系统目前已经稳定运行了一段时间,每次拿到新的岗位数据,清洗后入库,就能直接回答问题。实际用下来,效率提升确实明显,人工分析一个岗位可能需要五到十分钟,系统基本在几秒内能给出结构化结论,而且技能提取的准确率经过多轮调参后也能达到八九成的水平,剩下的偏差主要来自语义模糊的岗位描述。

我一直觉得,RAG 和 Tool Calling 不是孤立的技术,它们是一个系统里互补的两半:RAG 决定模型知道什么,Tool Calling 决定模型能做什么。把这两件事从零搭起来,绕不开文档处理、Embedding 选型、向量库管理、工具描述设计这些细碎工作,没有太多捷径,但每一块踩过去之后,都会有实实在在的收获。这套组合方案也完全可以复用到其他文本分析场景——我做岗位分析,你做简历筛选、合同合规检查或者政策解读,底层的架构思路是一样的。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/1 5:17:37

JavaWeb学生信息管理系统毕设骨架:JSP+Servlet+JDBC完整源码与避坑指南

简介:这是一份面向计算机相关专业学生与Java初学者的学生信息管理系统实战项目,适合用作课程设计、毕业设计参考或SSM/JSP入门练手。系统围绕学生、班级、院系、课程、成绩等核心业务展开,覆盖信息录入、查询与维护等基础管理功能&#xff0c…

作者头像 李华
网站建设 2026/10/1 5:17:06

用Flask从零搭建考勤系统:打卡接口、数据库设计与加班计算实践

考勤打卡系统这活儿,看着简单,做起来全是细节。我刚接手的时候,公司用的还是钉钉,但管理层后来提了一堆需求——加班要按工时分段算、不同部门要套不同的考勤规则、节假日倒班得单独配置……在钉钉上绕了一圈发现定制成本太高&…

作者头像 李华
网站建设 2026/10/1 5:17:05

在ARM Linux上运行Windows应用:FEX-Emu、Wine与DXMT兼容层实战指南

1. 项目缘起:为什么要在 Linux 上折腾 Windows 应用第一次看到 "Madeira" 这个代号,很多人会以为是某个旅游项目或者饮料品牌。但在我们这群长期混迹于 Linux 桌面兼容层圈子里的人看来,它指向的是一类非常具体的东西:在…

作者头像 李华
网站建设 2026/10/1 5:16:59

VS Code搭建Spring Boot的环境链路与JDK兼容性实战

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/10/1 5:16:24

Madeira兼容层实战:Wine、FEX-Emu与DXMT在Linux上运行Windows应用

1. 项目缘起:为什么要在 Linux 上折腾 Windows 应用兼容层第一次接触 Madeira 这个项目,是在一台装了统信 UOS 的国产笔记本上。当时的需求很朴素:单位配发的机器只能用国产系统,但日常办公又离不开几个 Windows 下的小工具&#…

作者头像 李华
网站建设 2026/10/1 5:16:14

白板手绘+GPT-4:多模态AI实时生成原型的实操指南

前阵子帮团队做一个小工具的原型,我在会议室的白板上画了一堆歪歪扭扭的框和箭头,同事路过看了一眼说:“你这画得也太抽象了,得配个说明书。”结果我掏出手机把白板拍下来,直接丢给GPT-4,十几秒的功夫&…

作者头像 李华