新闻详情

新闻详情

首页 / 资讯中心 / 详情

Spring AI实战:RAG+Tool Calling构建岗位分析系统

发布时间:2026/10/2 5:02:34来源:尧图网络
Spring AI实战:RAG+Tool Calling构建岗位分析系统
上个月 HR 同事给我丢来 40 份简历和 12 条岗位 JD让我帮忙做技术画像分析。第一个小时我还在逐条读第二个小时就开始走神——大部分 JD 的套话高度重复但真正影响用人决策的技能组合、薪资合理性、市场热度这些信息全藏在细节里。我当时就一个想法这种重复劳动必须自动化。于是我用 Spring AI 从零搭了一套RAG Tool Calling 岗位分析系统。核心能力是输入一条 JD文本或 PDF自动抽取岗位职责、技能要求、经验年限、薪资区间再让模型从预先整理好的知识库里检索相关内部资料职级体系、面试评价标准、历史招聘数据同时通过 Tool Calling 实时查询薪资统计和技能热度最终生成一份结构化的岗位分析报告。整个过程完全跑在 Java 技术栈里没有引入 Python 那套生态项目也顺利接到了已有的 Spring Boot 服务上。这篇文章不是官方文档翻译而是我完整复盘的实战记录包含选型对比、架构设计、RAG 落地细节、Tool Calling 接入方式以及从 0 跑通到稳定运行之间踩过的坑。如果你是 Java 后端工程师正在纠结要不要在 Spring Boot 里做 AI 功能想知道 RAG 到底怎么落地、Tool Calling 会不会把系统搞乱这篇也许能帮你省掉几周弯路。1. 技术选型Spring AI、LangChain4j、LangGraph4j 到底选谁1.1 我实际做的方案对比动手之前我先把市面上的 Java 系 AI 框架列了个清单候选方案有三个LangChain4j、LangGraph4j、Spring AI。先说结论我最终选了 Spring AI。理由不是说它一定比另外两个强多少而是它和现有技术栈的融合成本最低。团队里所有人都在写 Spring BootSpring AI 由 Spring 官方团队维护依赖注入、配置体系、starter 机制都是现成的接入一个老项目基本不改变原有代码结构。这对长期维护和团队协作来说价值比单纯的功能多少更重要。LangChain4j 我当时也测过。它的组件设计更像是 Python 版 LangChain 的翻译如果你有 LangChain 经验上手很快工具链也确实丰富。但它的 API 版本迭代比较快我在做简单对话和多轮工具调用时遇到过几次依赖行为随版本变化的情况排查起来比较费劲。LangGraph4j 是 LangGraph 的 Java 移植定位偏 Agent 工作流编排擅长画节点、定边、控制状态流转这种图结构。但岗位分析系统对我来说本质是一条流水线 两个增强手段不需要那么重的工作流引擎引入它反而增加认知负担。1.2 Spring AI 的核心抽象一张图看懂Spring AI 的编程模型很简单核心就是几个接口ChatModel / ChatClient负责对话、结构化输出、工具调用是主要交互入口EmbeddingModel负责把文本变成向量VectorStore负责向量存储和相似度检索Tool 注解把普通 Bean 方法注册成模型可以调用的工具这里我最喜欢的一点是Spring AI 没有把 AI 抽象成什么高深的东西它就是一套普通 Spring Bean。EmbeddingModel 是 BeanVectorStore 是 Bean工具类也是 Bean。这意味着你完全可以用 Spring 已有的配置管理、切面、事件机制去扩展它而不是被迫接受框架的黑盒。1.3 版本怎么对先说一个结论Spring AI 的版本号体系和 Spring Boot 不是一回事。1.0 之前全是 M 系列比如 1.0.0-M6API 说变就变。一直到 1.0.0 GA 发布之后才相对稳定。我自己用的是 Spring Boot 3.4.x 搭配 Spring AI 1.0.0 GA这个组合跑通了整套系统。如果你看到 2.0 系列正在推进建议等它出首个稳定版再动生产项目不要追 M 版这个坑我在第 5 节详细讲。2. 岗位分析系统的整体设计与数据流转2.1 系统要解决哪些具体问题设计之前我把岗位分析拆解成几个子问题第一JD 是非结构化文本岗位职责和任职要求混在一起需要抽取成结构化字段。第二光看 JD 本身信息不够需要结合内部知识职级标准、面试评估维度、历史招聘数据来解读这条 JD 的质量。第三JD 里的薪资范围和市场真实水平需要外部数据校准技能需求热度也需要实时数据支撑。第四最终结果是要给 HR 和业务负责人看的不能是一堆聊天记录必须是一份结构化报告。这四个问题对应的解决方案分别是结构化输出、RAG、Tool Calling、模板化报告生成。这四件事组合起来就是整个系统的骨架。2.2 三条流水线怎么协作整个系统其实由三条数据流水线组成它们不是串行关系而是互相补充第一条是抽取流水线。用户上传 JD 后系统先让模型做结构化抽取得到岗位名称、职责列表、技能列表、经验要求、学历要求、薪资范围等字段。这一步不依赖知识库纯粹是模型能力。第二条是 RAG 流水线。抽取完成后系统拿岗位名称 技能列表去向量库里检索召回内部知识库中与这条 JD 最相关的文档片段比如职级对应的能力要求、历史类似岗位的评价记录、相关项目的技术背景。召回结果拼进上下文让模型在生成报告时有据可依。第三条是 Tool Calling 流水线。模型在生成报告的过程中如果发现需要外部实时数据——比如这个薪资在市场上处于什么水平Spring Cloud 现在的需求量如何——它会调用注册好的工具方法去获取数据。工具返回结果后模型再继续生成。三条流水线的最终汇合点是报告生成模块把抽取结果、知识库召回片段、工具返回数据三部分拼进系统提示词让模型输出一份包含岗位画像、技能匹配度、薪资竞争力、招聘建议的完整报告。2.3 模块与技术栈清单我最终的技术栈是这样Spring Boot 3.4.x Spring AI 1.0.0 GAEmbedding 模型用的 OpenAI text-embedding-3-small原因后面细说聊天模型用的 gpt-4o-mini成本可控中文理解够用向量库先本地跑 SimpleVectorStore联调稳定后换成了 Redis Vector Store文档解析用 Spring AI 内置的 TikaDocumentReader支持 PDF、DOCX、TXT文件上传和报告下载用 Spring MVC 自带能力没有额外引入组件这个组合比较轻。如果公司已经有 Milvus 或者 PostgreSQL 插件也可以把 VectorStore 换成对应的依赖Spring AI 的接口是统一的业务代码不用大改。3. RAG 落地知识库分块、向量化与召回调参3.1 知识库从哪来先解决没有数据的问题做 RAG 最容易犯的错就是先搞模型、再搞向量库最后发现没有知识可检索。我一开始就确定了知识源公司的内部岗位说明书、职级晋升标准、历年的面试评价模板、过去两年的招聘 JD 汇总以及内部技术项目简介文档。合计大概 30 多份文档虽然量不大但覆盖了整套系统需要增强的核心场景。把这些文档规整成统一格式后我做了个批量入库脚本。入库流程很简单读文件 → 解析出文本 → 分块 → 向量化 → 写入向量库。这个流程跑一遍大约 10 分钟之后每次新增文档只需要重新跑一次脚本就行。3.2 文档解析与分块策略实测解析这块Spring AI 的 TikaDocumentReader 开箱即用不需要自己处理 PDF 和 DOCX 的格式差异。我直接用它读 FileSystemResource拿到的 Document 对象里就是正文文本。分块是关键。我一开始用的 TokenTextSplitter默认 500 token 一块overlap 50。跑完测试发现一个问题中文文本被硬切的情况很常见一句话被切成两半检索时召回的内容看起来相关读起来语义却不完整。比如要求熟悉分布式系统设计可能被切成要求熟悉分布式和系统设计两块导致召回结果张冠李戴。后面我把分块策略改成了组合式先按换行符和句号粗切保住语义完整的句子块再对超过 500 token 的超长块用 TokenTextSplitter 二次切分并加大 overlap 到 100。改完之后召回结果的完整性明显提升。3.3 Embedding 模型选型为什么没用本地模型中间我也试过本地部署的嵌入模型理由是避免数据出域。但测试下来效果不理想本地模型对中文长文本的语义理解明显弱一些同一个查询在不同文档上的相似度区分度不高容易召回到一堆相关性很弱的内容。最终我选了 text-embedding-3-small维度适中中文效果在可用范围内成本也很低。这里有个经验嵌入模型的选型要拿真实查询去做回归验证不能只看跑分。我刷了几十条岗位相关查询对比期望召回的文档是否出现在 Top5本地模型只有 60% 左右的命中率而 text-embedding-3-small 能到 85% 以上差距非常明显。如果你对数据安全有硬性要求必须本地跑建议至少用 BGE 系列等对中文优化过的模型并且要在自己的语料上做充分测试不要默认能跑通就代表效果好。3.4 向量检索调参TopK、阈值和召回率之间的平衡Spring AI 的向量检索用法很直接SearchRequest request SearchRequest.builder() .query(Java工程师 任职要求 Spring Cloud) .topK(5) .similarityThreshold(0.6) .build(); ListDocument hits vectorStore.similaritySearch(request);这里最需要调的是 topK 和 similarityThreshold 这两个参数。我实测后发现topK 设 3 太保守很多相关片段被挤掉设到 8 又会让上下文里塞进太多无关内容生成报告时模型容易被噪音带偏。最后定了 5也就是每次最多取 5 个片段拼接进提示词。这个数量在信息充足和上下文可控之间比较平衡。similarityThreshold 的坑更多。一开始我设 0.7大量查询返回空结果因为部分知识库文档本来就和 JD 的表述差异很大余弦相似度天然偏低。后来降到 0.5又发现召回的片段里混入了不少语义弱相关的内容。最后我统计了一批真实查询的相似度分布把阈值定在 0.55~0.6 之间并针对重要字段做了加权查询——比如用岗位名称和技能列表组合成一个更长的查询串比单用岗位名称召回的准确率高不少。3.5 向量库选型从内存库到 Redis 的迁移开发阶段我用 SimpleVectorStore它是内存实现不需要额外部署非常适合本地验证逻辑。但它有几个问题数据不持久化重启就丢没有并发控制多个请求同时写入会出问题。联调稳定后我切到了 Redis Vector Store公司本来就有 Redis 集群加一个 vector 索引不需要新引入基础设施。Spring AI 的接口设计在这时候体现出了价值我把 VectorStore 的 Bean 定义从 SimpleVectorStore 换成 RedisVectorStore业务代码一行没改。唯一要注意的是 Redis 里索引的维度必须和 Embedding 模型输出维度一致如果换模型旧索引要删掉重建这个细节很容易被忽略。4. Tool Calling 接入让模型自己决定查什么数据4.1 哪些场景必须上 Tool Calling而不是让模型瞎编岗位分析里有两类数据模型是不可能天生就知道的一类是实时外部数据比如某岗位在某城市的当前薪资分位数另一类是本地业务数据比如我们内部某个技术方向的岗位需求趋势。如果你不提供工具模型面对这种问题就会一本正经地编数字而且编得很流畅你根本发现不了。所以我给系统定义了三个工具。第一个是薪资统计工具输入岗位名称和城市返回该组合的平均薪资、中位数、P25/P75 分位数。第二个是技能需求工具输入技能关键词返回近三个月的岗位需求量和变化趋势。第三个是内部职级匹配工具输入技能和经验年限返回公司内部的职级建议和参考能力模型。前两个对接外部数据源第三个直接查询本地数据库表。4.2 Tool 注解定义工具比想象中简单Spring AI 的工具调用方式很低侵入定义一个普通 Spring Bean在方法上加 Tool 注解就行Component public class JobAnalysisTools { private final SalaryService salaryService; private final SkillDemandService skillDemandService; private final LevelService levelService; public JobAnalysisTools(SalaryService salaryService, SkillDemandService skillDemandService, LevelService levelService) { this.salaryService salaryService; this.skillDemandService skillDemandService; this.levelService levelService; } Tool(name getSalaryStats, description 查询指定岗位在指定城市的薪资统计数据返回平均薪资、中位数、P25/P75分位数单位为K) public String getSalaryStats(String jobTitle, String city) { return salaryService.queryStats(jobTitle, city).toJson(); } Tool(name getSkillDemand, description 查询指定技能关键词近三个月的岗位需求量及环比变化趋势) public String getSkillDemand(String skill) { return skillDemandService.queryTrend(skill).toJson(); } Tool(name matchInternalLevel, description 根据技能列表和工作年限返回公司内部职级建议与能力模型参考) public String matchInternalLevel(String skills, Integer years) { return levelService.recommend(skills, years).toJson(); } }注册工具只需要在构建 ChatClient 时带上工具对象ChatClient chatClient ChatClient.builder(chatModel) .defaultTools(new JobAnalysisTools()) .build();之后调用模型时遇到需要外部数据的问题模型会在生成回答前自动调用对应方法方法返回值会被拼进上下文模型再基于真实数据继续生成。整个过程从外部看就是一次 prompt 调用内部则是多轮模型思考 → 调工具 → 拿结果 → 再生成的循环。4.3 工具方法的设计约束返回文本越短越好这里我想强调一个很多人忽略的点工具方法的返回值最终是塞进模型上下文的 Token所以返回内容必须精简结构化。我第一次写薪资工具时返回了一个完整 JSON包含几十个字段模型调用一次就吃掉大量 Token对话稍微一长就触顶。后来我把返回值改成平均薪资:K, 中位数:K, P25:K, P75:K这种紧凑格式效果一样Token 少了一半多。工具描述也不能含糊description 写得越清楚模型按你的意图调用的概率越高。另外工具调用是有循环的。模型发现某个数据缺失可能会连续调用多个工具甚至同一个工具调用多次。如果不管控一个长报告生成过程可能会触发十几次工具调用Token 消耗和耗时都翻倍。我在实际运行时给工具调用循环加了上限并要求模型优先基于已有数据生成只有报告确实缺少关键数据时才允许发起新调用。经过这几条约束单次报告生成的工具调用次数从平均 6 次降到了 3 次以内。5. 实战踩坑版本地狱、中文失真与工具调用失控5.1 第一个坑Spring AI 版本号与 Spring Boot 不匹配这个坑几乎是所有入门者必踩的。Spring AI 1.0.0 之前的 M 系列版本依赖坐标是 spring-ai-openai-spring-boot-starter 这种形式而 1.0.0 GA 之后统一改成了 spring-ai-starter-model-openai。如果你照着网上老教程抄依赖很容易启动时报 ClassNotFoundException 或者找不到 Bean 定义。我的排查过程比较笨但有效先确认 Spring Boot 版本再去 Spring AI 官方文档查兼容矩阵最后照着 GA 版本的快速开始重新生成依赖。另外spring-ai-bom 一定要加进 dependencyManagement否则不同 starter 之间的传递依赖版本可能不一致运行时会出现NoSuchMethodError这种最恶心的错误。这里也提醒一句网上搜 Spring AI 教程一定要看发布时间M 系列和 GA 的写法和配置差很多照着旧文章抄会浪费大量时间。5.2 第二个坑中文分块后检索看似相关实则错位这是 RAG 系统里最隐蔽的问题。问题表现是检索结果里每条文档都和查询词有重叠但拼进上下文后模型给出的回答跟 JD 对不上。比如 JD 写熟悉高并发场景下的系统设计系统检索到的是高并发压测方案相关的内部文档两者都含高并发但一个是设计一个是测试语义方向完全不同。根因就是我在 3.2 节说的分块问题。中文没有天然空格按 Token 硬切很容易破坏语义边界。解决分两步先在句子边界切分再对超长块二次切分同时保留足够的 overlap。另外一个有效手段是让查询串更完整——不要只拿岗位名称去检索要把岗位名称核心技能工作年限要求组合成一个查询。组合查询的语义更接近知识库文档的表述方式召回质量明显提升。调整之后我人工抽查 20 组查询相关性达标率从 70% 提高到了 90% 左右。5.3 第三个坑相似度阈值设错导致空召回或噪音这个坑我在 3.4 节提过这里细说排查过程。一开始我把 similarityThreshold 设为 0.7上线试运行当天就收到反馈某些 JD 分析报告里完全没有内部知识参考。日志里一看向量检索返回空列表。我当时的怀疑方向有两个一是 Embedding 模型有问题二是知识库数据入库不对。排查后都排除了因为换一条 JD 又能正常召回。后来我把所有真实 JD 的查询相似度打印出来发现相当一部分合法查询的最高相似度只有 0.5~0.6说明不是模型问题是知识库文档和 JD 的表述方式本来就差异大。把阈值降到 0.55 之后空召回消失同时我在检索后加了一步简单的重排序在召回的 10 条候选中按相似度降序取前 5 条保证进入上下文的都是相对最相关的。这件事的教训是阈值不能拍脑袋要用真实数据分布来定。5.4 第四个坑多轮工具调用把上下文撑爆工具调用循环失控是我遇到的另一个实际问题。由于我注册了三个工具模型在一个长报告生成任务里会不停地想再查一个数据、再查一个数据。有一次我查日志发现一次报告生成过程触发了 8 次工具调用上下文里堆了大量工具返回结果最后因为超出模型上下文窗口直接报错。解决思路分三层第一给工具调用增加数量上限超过限制就终止工具循环并基于现有信息生成第二优化工具返回值格式压缩 Token第三系统提示词里明确仅在数据缺失且影响结论时调用工具从模型行为的源头减少无谓调用。这里尤其要注意第一点因为不是所有模型都对工具调用有天然的自控能力应用层必须兜底。5.5 第五个坑结构化 JSON 输出偶发失败岗位分析报告要求输出固定结构我最开始直接让模型输出 JSON 文本再自己去解析结果问题不断模型偶尔会多输出解释性文字偶尔把 JSON 包在 Markdown 代码块里偶尔键名大小写不一致。后来改用 Spring AI 的 BeanOutputConverter定义好 POJO用 .entity() 方法让框架去处理转换成功率大幅提升。但即使这样也不能完全依赖框架。实测下来约 2%~3% 的请求仍会解析失败原因是模型输出内容结构与 POJO 不完全匹配。我的兜底方案是解析失败时自动重试一次第二次如果还失败就降级为用户返回抽取出来的原始字段而不是直接报错。这个降级策略对生产环境很重要至少保证核心信息能出来。5.6 第六个坑并发环境下 SimpleVectorStore 撑不住联调阶段我接到一个需求支持多个 HR 同时上传 JD 分析。本地测试时用 SimpleVectorStore 没问题但压测发现并发检索和写入时SimpleVectorStore 的内存操作存在明显的性能瓶颈和偶发数据不一致。这个问题的本质是 SimpleVectorStore 定位是开发和测试场景不适合生产并发。解决方案也没什么悬念换成 Redis Vector Store 后并发从几十 QPS 提上到了几百 QPS瓶颈从向量库转移到了模型 API 本身。这个坑提醒我选型时不能只想着本地能跑通还要考虑部署环境。6. 效果评估与上线前的收尾工作6.1 测试集与评估维度上线前我整理了 30 条真实 JD 作为测试集覆盖 Java 后端、前端、产品、测试、运维五个方向每条 JD 都手动标注了期望抽取的字段和期望召回的内部知识片段。评估维度主要有三个第一个是检索命中率即 RAG 召回的 5 个片段里是否包含至少一个与 JD 真正相关的知识片段。第二个是字段抽取准确率对比模型抽取的岗位职责、技能列表、薪资范围与人工标注的差异。第三个是报告可用性我让 HR 同事按能否直接作为招聘参考打分1 到 5 分。三个维度的权重不一样检索命中率是基础字段准确率决定可信度报告可用性是最终业务价值。6.2 实测结果与迭代方向30 条 JD 的实测结果是这样的检索命中率在阈值调到 0.55 后达到了 93%剩下 7% 主要是超短 JD本身信息量太少模型也没有办法。字段抽取准确率在 90% 左右最容易出错的是薪资范围因为部分 JD 写的是面议需要靠外部薪资工具补数据才能给出参考值。报告可用性均分 4.2HR 反馈最多的问题是报告太长了能不能直接给个结论段落后来我在模板里加了结论置顶的调整反馈明显好转。这里有个经验值得分享每次调参或换模型都拿测试集整体重跑一遍不要只盯着单独一条 JD 看效果。我自己就经历过这条调好了另一条变差了的情况只有回归测试能帮你守住整体水平。6.3 上线前容易忽略的三个细节第一个是知识库版本管理。内部知识文档是会更新的文档变更后要重新分块、重新向量化旧数据要清理。现在每次更新文档我都走固定的入库脚本向量库里保留文档来源 ID方便做增量替换。第二个是模型参数固定。报告生成任务里 temperature 设为 0.1 甚至 0保证同样输入尽量产生稳定输出。这个参数对分析型任务极其重要如果习惯性用默认值报告每次生成都不一样业务方会认为系统不可靠。第三个是留一条人工复核路径。我在系统里增加了历史报告列表支持查看原始 JD、召回片段、工具调用日志和最终报告。这一步不是为了展示而是出了问题能快速定位是检索问题、数据问题还是模型问题。一个月下来这招帮忙定位了 80% 的线上反馈问题。最后再分享一个小技巧日志里一定要记录每次请求的工具调用明细和召回片段 ID连同一个请求ID串起来。排查问题的时候你会感谢当时的自己。这套系统从开始搭建到稳定运行用了大约两周时间其中一半时间花在踩坑和调参上但跑通之后原来一个半小时的人工分析变成了一分钟内的自动化输出这个投入产出比我觉得是值得的。
网站建设高端定制企业官网
RELATED

相关资讯

更多精彩内容,欢迎继续阅读

较早相关资讯

最新相关资讯

六款常用工具断网离线能力实测与分层解析 2026/10/2 5:51:43

六款常用工具断网离线能力实测与分层解析

1. 项目概述:当网络突然消失,你手里的工具还剩多少真实战斗力?“断网之后,六款工具还剩什么”——这个标题乍看像一句调侃,实则直击IT从业者、运维工程师、开发人员甚至普通办公族最真实的日常痛点。我做过十年一线技术…

阅读更多 →
WeKnora实战:基于RAG的企业知识库搭建与调优指南 2026/10/2 5:51:43

WeKnora实战:基于RAG的企业知识库搭建与调优指南

上个月帮团队做内部知识库选型的时候,我把 WeKnora 装到一台 Windows 11 笔记本上试了两周。说实话,刚开始我对这种“大厂开源、号称 RAG 知识库”的项目已经有点麻了,大多数都是把 LangChain 包一层壳,传个 PDF 进去问两句话就露…

阅读更多 →
开源物联网平台选型实战:设备接入、数据引擎与业务扩展 2026/10/2 5:51:43

开源物联网平台选型实战:设备接入、数据引擎与业务扩展

1. 开源物联网平台:不是“搭个Web界面连几台ESP32”就叫平台你搜“开源物联网平台”,首页跳出来的可能是GitHub上星标500的某个Python脚本,也可能是某位大学生毕设里用VueNode.js写的带登录页的控制面板——但这些离真正能用在产线、能扛住百…

阅读更多 →
MySQL面试题深度解析:从慢查询到索引、事务与锁的实战指南 2026/10/2 5:51:43

MySQL面试题深度解析:从慢查询到索引、事务与锁的实战指南

简介:这是一份面向后端开发求职者与在校学生的 MySQL 面试知识点总结文档,围绕数据库原理与索引机制展开,帮助读者系统梳理高频考点、补齐知识盲区。内容涵盖关系型与非关系型数据库的区别、一条 SQL 语句从连接器到执行器的完整执行流程、索…

阅读更多 →
PL0编译器扩充实战:从for循环到数组的完整改造 2026/10/2 5:51:43

PL0编译器扩充实战:从for循环到数组的完整改造

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

阅读更多 →
开源物联网平台选型与高可用部署实战指南 2026/10/2 5:51:37

开源物联网平台选型与高可用部署实战指南

1. 什么是真正能落地的开源物联网平台“开源物联网平台”这六个字,最近两年在技术社区、高校实验室和中小硬件创业团队里出现频率极高,但很多人第一次听到时,下意识反应是:这不就是个带Web界面的MQTT服务器?或者——是…

阅读更多 →

今日资讯

本周资讯

本月资讯

看完文章仍有疑问?

联系尧图顾问,获取一对一建站咨询

立即免费咨询 📞 400-888-8888
📞 ✉