资讯动态

基于Spring AI的JD智能分析:RAG与Tool Calling实战解析

发布时间:2026/10/1 9:49:26 来源:尧图企业网站定制
最近在给公司的 HR 团队做一套岗位分析系统。背景很简单招聘旺季一到每天要处理几十份甚至上百份 JDHR 得逐条拆解职责、提炼技能、比对内部薪资带宽、核对职级要求整个人被机械化劳动淹没。我接这个需求的时候就在想这事儿的本质其实是两个问题一是如何让 AI 读懂 JD 这种半结构化文本二是如何让 AI 在分析时能随时查到公司的私域规则和实时数据。前者靠 RAG后者靠 Tool Calling而整个系统的骨架我选了 Spring AI 来做。这套系统跑通之后JD 处理效率翻了不止一倍而且分析结果的规范性比人工还稳定。这篇文章把全流程的架构思路、关键实现、模型选型和踩坑记录都写出来打算做同类项目的朋友可以直接照着走一遍。1. 需求拆解先想清楚系统到底要干什么1.1 岗位分析的真实痛点HR 拿到一份 JD实际上要做的事远远不止读一遍。她把职责拆成几条把技能要求按必需/加分分类判断候选人画像还要把薪资范围套到公司现有的职级体系里。这些操作背后依赖大量内部信息比如 P6 对应什么职级范围、这个城市的技术岗薪资带宽是多少、哪些岗位有学历硬性要求。这些信息散落在不同的地方——职级制度文档、历年薪资统计表、招聘 SOP、面试评估标准。平时靠资深 HR 的记忆人一变动就断层。而且不同 HR 分析同一份 JD口径经常不一样有的把熟悉分布式系统归为加分项有的归为必需项最后给用人部门的结论完全不同。所以系统的核心价值不是翻译JD而是建立一套统一的、可复用的分析口径把 JD 里的信息结构化绑定到公司已有的规则上。这正是 RAG 加 Tool Calling 最擅长的事——RAG 负责把内部规则捞出来作为参考依据Tool Calling 负责在分析过程中动态查数据。1.2 RAG 在这个系统里解决什么问题RAG检索增强生成本质上是给大模型外挂一个知识库。我做这套系统的第一个版本时走的是纯 Prompt 路线把公司职级说明、薪资带宽直接写死在提示词里。结果有两个问题一是提示词越长模型越容易忽略后面的信息二是规则更新了要改代码极其难受。RAG 解决的就是这两个问题。知识库独立于模型存在规则变化只需要更新文档重新走一遍向量化流程模型每次分析时会主动检索相关片段作为上下文参考。一句话RAG 让模型从凭记忆胡猜变成看着资料说话。在这个岗位分析系统里RAG 检索的对象包括职级描述文档、历史薪资数据、招聘制度、岗位任职要求。这些统称为私域知识大模型原本并不知道也不可能通过提示词全部塞进去。RAG 是唯一既能控制成本又可靠的方案。1.3 Tool Calling 负责补充什么能力RAG 擅长处理静态知识但岗位分析里有很多数据是动态的。比如这个岗位在杭州的 P6 薪资带宽到底是多少RAG 检索出来的可能是去年的一份文档而真实的带宽数据已经在月初更新到了薪资系统里。再比如需要实时查一下某个技能在当前市场上的需求量、需要调用外部 API 获取公司基本信息这些 RAG 都做不了。Tool Calling 就是让模型具备动手能力。模型在分析 JD 的过程中如果发现需要精确的薪资数据会发起一次工具调用由系统执行真实的数据库查询再把结果返回给模型继续生成分析结论。RAG 负责喂资料Tool Calling 负责接系统和数据两者配合下来这个岗位分析系统才真正像一个会干活的助手而不是一个只会读文档的聊天机器人。2. 技术选型为什么最终选定 Spring AI2.1 Spring AI 的现状和定位先回答一个很多人纠结的问题Spring AI 是什么简单说它是 Spring 官方推出的 AI 应用开发框架定位是把大模型接入能力标准化让 Java 开发者用写 Spring Boot 的方式写 AI 应用。支持 OpenAI、Ollama、通义千问等模型提供方提供统一的 ChatClient、EmbeddingModel、VectorStore 抽象。Spring AI 的版本演进非常快。0.8 时代还像个毛坯API 动不动就变1.0 发布之后基本稳定下来到 2025 年的 2.0 版本Spring AI 已经支持了经典的 RAG 流程、Tool Calling、Agent 编排等能力而且跟 Spring Boot 3.x 的自动配置无感集成。我用到的核心模块大致有spring-ai-starter-model-openai对接模型、spring-ai-vector-store-pgvector向量库、spring-ai-starter-document-reader-pdf读 PDF、spring-ai-advisorsRAG Advisor 和 Tool Advisor。2.2 为什么不选 LangChain4j 或 LangGraph4j这个选择我必须展开说因为不少朋友问过。市场上 Java 生态里能做大模型应用的框架除了 Spring AI主要就是 LangChain4j 和 LangGraph4j。对比来看LangChain4j 起步早很多概念向 Python 版 LangChain 靠拢如果你的团队有 Python 背景上手会容易一些。但它的短板也很明显跟 Spring Boot 生态的融合深度不如 Spring AI很多配置要手动组装与 Spring Security、Actuator 这些常用组件的配合需要额外开发。LangGraph4j 更适合做复杂的 Agent 工作流编排它对节点、边、状态机的抽象非常灵活。但代价是学习曲线陡峭对于岗位分析这种场景——大部分流程是固定的读 JD、查知识、调工具、输出结构化结果——用 LangGraph4j 有点杀鸡用牛刀。我最终选了 Spring AI理由非常实际团队本来就是 Spring Boot 技术栈Spring AI 的配置风格、开发模型跟 Spring Boot 完全一致自动配置开箱即用有问题社区活跃度高。更重要的是Spring AI 2.0 对 RAG 和 Tool Calling 的支持已经非常成熟标准的开发模式一套就能走通全流程。对比维度Spring AILangChain4jLangGraph4jSpring Boot 集成度原生融合自动配置需要手动整合需要手动整合RAG 流程支持ChatClient AdvisorPipeline 式 API需要自己搭建节点Tool CallingTool 注解声明式Tool 注解也支持通过节点调用学习曲线平缓中等较陡适合场景业务系统集成 AI通用 AI 应用复杂 Agent 编排如果你的团队是全 Java 背景、系统本身是 Spring Boot 构建主流选择里 Spring AI 的性价比最高。2.3 模型与基础设施选型模型选型这块我前后试了几个方案。最初用云端 OpenAI GPT-4o-mini 做测试效果确实好但有两个顾虑一是 JD 数据涉及公司在招岗位的薪资和编制信息出域有合规风险二是调用成本在批量处理场景下不低。后来切换到了本地模型用 Ollama 跑 Qwen2.5 7B 做核心推理embedding 用 BGE-M3。这套组合的优点是数据完全内网闭环、零 API 成本缺点也明显7B 模型的推理能力和指令遵循性相比 GPT-4o-mini 还是弱一些尤其是复杂 JSON 结构化输出偶尔会出格式问题。最后的方案是内外结合核心分析链路走云端高性能模型敏感数据的检索和初筛放本地后面加了一层规则校验来兜底。具体模型配置可以在 application.yml 里切换开发环境用本地模型生产环境切云端代码逻辑完全不用动。这也是 Spring AI 的一个明显好处——模型变更是配置级切换而不是代码重写。3. RAG 知识库从零到能跑通的核心链路3.1 知识库里到底放了什么数据RAG 的效果七分靠数据三分靠技术。我建知识库的第一步不是写代码而是把 HR 团队手里的资料全部梳理了一遍。最终沉淀下来几类主要文档职级体系说明每个职级的能力要求、经验年限、晋升条件薪资带宽表分城市、分职级的月薪和年薪范围招聘制度 SOP学历要求、背景调查规则、Offer 审批红线岗位任职标准常见技术岗和非技术岗的能力模型历史 JD 优秀案例之前成功交付的 JD 拆解范例这些文档有 Word、PDF、Excel还有几张扫描件。Excel 我做了单独处理转成 CSV 后按行写入因为表格数据天然是结构化的直接解析成文本再切分反而丢信息。3.2 文档解析与切分策略Spring AI 提供了文档读取的抽象我引入 spring-ai-starter-document-reader-pdf 和 tika 依赖来处理 PDF 和 Word。这块第一个坑来自扫描件 PDF——直接读出来全是乱码必须接 OCR。我用的是 Tesseract 中英文模型识别率勉强够用但遇到排版复杂的表格还是会串行。建议团队里有条件的话尽量让 HR 输出原始电子版文档扫描件作为 RAG 知识源效果很差。文档切分是我反复调优最多的环节。最开始用 TokenTextSplitter 按固定 300 token 切chunk overlap 设为 50。跑了一轮测试发现检索命中率只有六成左右排查后定位到问题中文文档按 token 硬切经常把分布式事务高并发这种关键短语拦腰截断切出来的片段语义不完整embedding 质量跟着下降。后来我改成了混合切分策略先按 Markdown 标题和段落做结构切分再对过长的段落用 TokenTextSplitter 二次切分。这样既能保留文档的逻辑边界又避免单个 chunk 过长超出 embedding 模型的输入上限。var structureSplitter new DocumentStructureSplitter(); var tokenSplitter new TokenTextSplitter.Builder() .withChunkSize(400) .withChunkOverlap(100) .build(); ListDocument processedDocuments tokenSplitter.apply(structureSplitter.apply(rawDocuments));这里 withChunkOverlap 我特意调到了 100比默认值大不少。原因很简单岗位文档里经常出现一个完整概念跨段落的情况比如某一段结尾说需要熟悉以下技术栈下一段才罗列具体技术清单overlap 太小会切断这种指代关系检索时信息不完整。3.3 向量化与向量库选型embedding 模型我用过 nomic-embed-text 和 bge-m3。实测下来nomic 的英文表现不错但中文语义理解一般bge-m3 在中英文混合场景下更稳特别是处理Java和JAVA这类大小写不敏感的词时语义距离更合理。最终全线切到了 bge-m3512 维向量索引性能和检索效果都比较均衡。向量数据库这块我对比了 Chroma、Milvus 和 PGVector。Chroma 轻量但不太适合生产环境的持久化和权限控制Milvus 功能强大但部署运维成本高一个系统不那么值最后选了 PGVector——公司的系统本来就有 PostgreSQL直接复用零额外运维负担。Spring AI 对接 PGVector 有现成的 starter配置很方便Bean public VectorStore vectorStore(EmbeddingModel embeddingModel, JdbcTemplate jdbcTemplate) { return new PgVectorStore(jdbcTemplate, embeddingModel, PgVectorStore.VectorStoreType.DOCUMENT, true, PgIndexType.HNSW); }HNSW 索引类型对相似度检索的加速效果显著数据量到十万级也能保持毫秒级响应。初始化后会自动建表不用手写 DDL。3.4 检索从 Top-K 到可信结果RAG 的检索环节我刚开始做得很天真把用户问题直接 embedding然后取 Top-5 相似片段拼到 prompt 里。结果经常把不相关的内容也捡进来模型容易被噪音带偏分析结果里出现无关的内容参考了公司 XX 制度这种奇怪表述。后来我做了三个改进第一加相似度阈值过滤。相似度低于 0.5 的结果直接丢弃宁可少给资料也不能给错资料。第二做元数据过滤。知识库里的文档都打了来源分类标签职级体系、薪资带宽、招聘 SOP检索时优先匹配当前分析请求对应的分类避免问职级却检索出薪资这种事。第三接了一个重排序层。Top-20 候选先用 BM25 和向量相似度各刷一遍再按融合分数取 Top-5效果提升非常明显。SearchRequest request SearchRequest.builder() .query(query) .withTopK(20) .withSimilarityThreshold(0.5) .withFilterExpression(docType salary city city ) .build(); ListDocument docs vectorStore.similaritySearch(request);这里多强调一句RAG 的效果瓶颈往往不在模型而在检索质量。知识库里的文档如果本身混乱再强的模型也救不回来。4. Tool Calling让 AI 能够动手查数据4.1 系统里定义了哪些工具工具清单不是想当然定的而是从 HR 的实际工作流里反推出来的。最终定义了四个工具工具名入参出参用途querySalaryBand职级、城市该职级在该城市的薪资带宽精确获取薪酬范围queryInternalPolicy关键词匹配的内部招聘政策条目核查招聘红线querySkillTrend技能名称该技能的近期市场需求热度判断技能含金量webSearch查询词外部公开的岗位/公司信息补充外部情报前三个工具对接的是公司内部数据源最后一个 webSearch 是接的外部搜索 API。工具定义得越具体、描述写越清楚模型调用就越准确。4.2 在 Spring AI 中实现一个 ToolSpring AI 的 Tool Calling 用 Tool 注解声明框架会自动把方法注册成模型可调用的工具并在模型发起调用时执行把结果注入回对话上下文。public class JobTools { private final SalaryService salaryService; public JobTools(SalaryService salaryService) { this.salaryService salaryService; } Tool(name querySalaryBand, description 查询指定职级在指定城市的薪资带宽职级格式如 P6、P7城市为中文名) public String querySalaryBand(ToolParam(description 职级如 P6) String level, ToolParam(description 城市如 杭州) String city) { SalaryBand band salaryService.getBand(level, city); if (band null) { return 未查询到该职级在该城市的薪资数据; } return 职级 level 在 city 的月薪带宽为 band.getMinSalary() - band.getMaxSalary() 元; } }接入方式也简单把工具实例传给 ChatClient 即可ChatClient chatClient ChatClient.builder(chatModel) .defaultTools(new JobTools(salaryService)) .build();这段代码背后发生的事值得细说。模型在生成回复前会先判断是否需要调用工具如果需要模型输出的并不是最终答案而是一段结构化的工具调用请求包含工具名和参数。Spring AI 收到请求后反射调用对应方法拿到结果再回传给模型模型基于这个结果继续生成后续内容。整个过程对开发者是透明的像写了个普通 Java 方法。4.3 如何让模型稳定地使用工具Tool Calling 最磨人的不是注册工具而是让模型该调的时候调不该调的时候别乱调。我调试的前期模型经常干两件蠢事一是在完全不需要工具时强行调用一次返回一堆无意义数据二是需要调用时却忘了调用直接凭幻觉生成薪资数字。解决思路分两步。第一步是把工具描述写精确尤其是参数约束。比如 querySalaryBand 的职级参数我会在描述里写明只能是 P4-P9 区间其他值视为无效第二步是在系统 Prompt 里加了几条硬性规则——薪资数据必须调用 querySalaryBand 获取禁止自行推测薪资范围、只有用户明确要求查询外部信息时才调用 webSearch。实测下来还有一个小技巧给每个工具加一个使用条件的说明字段告诉模型什么情况下用这个工具。比如 queryInternalPolicy 的使用条件是当 JD 中出现学历要求需要核查是否与公司制度冲突时。这种说明模型确实能读懂调用准确率能提升一大截。4.4 RAG 与 Tool Calling 如何协同很多人问这两者的优先级和调用顺序。我在系统里设定了一套固定的决策流程首先系统对 JD 做第一轮分析提取关键要素岗位名称、职责、技能、经验要求等。然后基于这些要素发起 RAG 检索获取公司内部的职级规则和薪酬体系。接着模型对照检索结果判断是否需要工具补充精确数据——比如 RAG 检索到的带宽表可能是上一季度的而数据库里有实时数据这时就触发 querySalaryBand 的调用。RAG 保证模型有背景知识工具保证模型拿到的是准确、实时的数据两者答案是互相印证的。有一次测试中模型先通过 RAG 检索到 P6 薪资带宽在 25K-35K又调用工具拿到了当月数据是 28K-38K最终生成的岗位画像自动采纳了工具返回的较新范围并标注了数据时效性。这个协同逻辑跑通之后系统的可信度高了很多。5. 岗位分析主流程把系统串起来5.1 输入处理与指令设计系统入口接收两类输入批量上传的 JD 文档或者是单条粘贴的 JD 文本。我先对原始文本做一轮清洗包括去空白、统一全角半角、剥离多余 HTML 标签等。这些看似琐碎的预处理直接影响后续提取和切分的质量值得单独写一个清洗函数。Prompt 设计方面我把系统指令拆成了三层基础角色层你是一名资深招聘分析师、流程指令层先分析职责再匹配技能最后给出职级建议、输出约束层严格 JSON 格式、不要编造数据。三层职责分离后后续迭代只需要改对应层的文本逻辑清晰很多。5.2 结构化输出从自由文本到岗位画像岗位画像的结构化输出是整个系统能否落到业务的关键。如果用自由文本让模型分析结果没法直接进系统、没法排序、没法统计。我定义了一个岗位画像的 Java Record把分析结果强约束成结构化数据。public record JobProfile( String jobTitle, ListString coreResponsibilities, ListSkillDemand skills, ExperienceRequirement experience, String educationRequirement, SalaryRecommendation salary_range, String levelSuggestion, ListString riskFlags ) { public record SkillDemand(String skillName, String level, boolean required) {} public record ExperienceRequirement(int minYears, int maxYears, String description) {} public record SalaryRecommendation(String level, int minK, int maxK, String source) {} }使用 Spring AI 的 BeanOutputConverter可以让模型严格按照这个结构输出 JSON然后框架自动反序列化成对象。这里有一个关键参数temperature 要调到 0 附近。结构化输出场景下不需要创造性输出稳定性远比多样性重要。BeanOutputConverterJobProfile converter new BeanOutputConverter(JobProfile.class); String output chatClient.prompt() .system(systemPrompt \n converter.getFormat()) .user(userPrompt) .call() .content(); JobProfile profile converter.convert(output);5.3 一次完整分析过程的现场回放拿一份真实 JD 做例子某互联网公司招聘高级 Java 开发工程师要求五年以上经验、熟悉微服务架构、有高并发项目经验薪资面议。系统依次经历了四个阶段第一阶段分析职责提取了服务拆分、系统优化、团队协作等关键词第二阶段 RAG 检索命中了公司职级体系文档中关于 P6 的任职标准以及历史薪资统计中杭州地区 P6 的带宽第三阶段模型判定需要精确薪资数据调用 querySalaryBand 拿到当月实时带宽第四阶段生成结构化岗位画像输出建议职级 P6、建议薪资区间、核心技能清单及风险提示薪资面议标注为需要 HR 与候选人确认。整个流程耗时约 12 秒包含一次工具调用和一次向量检索相比 HR 人工拆解 JD 平均 20 分钟效率提升是非常明显的。6. 踩坑记录与排查思路6.1 检索命中率低不是模型问题是切分和数据问题这是调试期间最让我头疼的问题。现象是同样的知识库用英文测试效果尚可换成中文 JD 检索经常召回不相关文档。排查后定位了三个原因第一中文里同义表达丰富。JD 写五年工作经验知识库里写的是5 年以上经验embedding 对数字和单位变化的鲁棒性并没有想象中好。第二固定 token 切分切断语义。第三PDF 解析出来的文本带了不少不可见字符和排版残留导致向量化效果被污染。解决思路是清洗阶段加强文本规范化把所有数字统一成阿拉伯数字同义词通过一个自定义词典做扩写后再去检索切分换成了结构感知策略embedding 模型换成 bge-m3。三轮修正后Top-5 检索命中率从六成提升到八成半。6.2 模型喜欢编造薪资数据这是最危险的坑。有一版测试中模型在没调用 querySalaryBand 的情况下直接根据 RAG 检索到的旧带宽合理推测出了一个范围和现行数据差了 20% 以上。虽然流程上说了必须调用工具但模型偶尔还是会忽略指令。我的解法是在输出校验层加了一道硬检查生成的结果里只要有薪资字段就强制去 salaryService 验证该字段是否落在工具返回的带宽内不在则直接拒绝输出并重新发起一次工具调用。同时把薪资面议的 JD 标注为需要 HR 人工确认的字段而不是让模型猜。这道校验层我强烈建议做。大模型的幻觉在生成场景下可以忍但在带业务决策性质的系统里一次错误数据可能直接导致招聘策略跑偏。6.3 Tool Calling 的参数传值问题Tool Calling 的坑主要体现在参数上。模型返回参数时偶尔会把数字类型的字段用字符串传回来比如把职级P7传成P7 级别还有一次模型把城市参数传成了杭州市而接口里只认杭州。这些不显眼的小问题会导致工具调用失败或查不到数据系统又不报大错排查起来很耗时间。解决方法是双管齐下接口层做参数归一化字符串包含市就自动去掉等Prompt 里加 few-shot 示例。模型看到城市参数是中文无后缀的示例后调用成功率明显上升。对于基础模型能力不够的场景趁早加一个参数校验和纠偏层别指望模型自纠。6.4 中文场景下的隐藏坑同义词、分词、全角半角中文 RAG 的坑比英文多得多整理几个典型分词差异导致 embedding 效果漂移不同 tokenizer 对中文词的切分不同检索时岗位职责和責任描述匹配不到全角半角符号混用干扰解析docType 元数据过滤字段如果用了中文值在向量库拼接过滤表达式时容易出问题。我的建议是面向中文的 RAG 系统预处理统一规则要前置——全角转半角、数字统一、英文小写、过滤空字符。这块枯燥但极其治本别在这上面省时间。6.5 性能与成本批量场景下的三个优化批量处理 JD 时最容易碰到性能瓶颈。我做了三个优化第一RAG 检索结果加了短暂缓存同一类岗位比如Java 开发和Java 工程师共用知识检索结果减少 embedding 计算量第二工具调用改成异步并行多个工具可以同时发出去再合并结果第三对重复出现的 JD 做 hash 去重完全相同的文档直接命中缓存不再跑一次模型。成本方面模型调用是按 token 计费RAG 检索结果被全量塞进 prompt 会浪费很多 token。我加了一个检索结果裁剪和后处理把相似度低于阈值的内容直接丢弃再把每个 chunk 截短到合理长度。这套优化让单次分析的成本降了差不多三分之一。7. 沉淀与后续扩展这套系统跑了一个多月最大的感受是RAG 和 Tool Calling 不是两个独立功能而是同一个 Agent 能力的两个支撑腿。RAG 提供知的基础Tool Calling 提供行的手段缺少任何一边岗位分析都做不完整。如果只做 RAG 不做工具模型分析出的薪资永远是过期数据只做工具不做 RAG模型面对不同岗位的差异化要求时又缺少上下文支撑。后续有几个明确的扩展方向一是让岗位画像直接对接招聘系统的 API分析完自动写入岗位发布流程二是引入更细粒度的 Agent 决策让模型自己判断是走 RAG、调工具还是两者都做三是把知识库从岗位分析扩展到薪酬调查、行业报告做成一个统一的 HR 智能分析平台底层架构不用变加数据源和工具就行。最后分享一个个人觉得很有用的习惯每次模型输出结果后我会保留完整的调用链日志包括检索命中的文档、tool calling 的参数和返回、最后生成的内容。出了问题拉出日志就能定位是哪一环掉了链子。这套排障思路比任何调试技巧都管用。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑