
1. 为什么我要用 Spring AI 做岗位分析系统招聘网站上的岗位描述JD看多了会发现一个规律同一个岗位名称在不同公司写出来的技术栈要求能差出十万八千里。比如“Java 后端开发”这个岗位有的公司要求精通 Spring Cloud 微服务全家桶有的公司只要求会写 CRUD 加一点 Redis 缓存还有的公司把“熟悉大模型应用开发”也塞了进来。如果靠人工一条条去读、去归类、去提取技能关键词一天下来眼睛都花了效率极低。我这次要做的就是一个能自动抓取岗位描述、理解岗位内容、提取核心技能要求、并给出匹配度分析的岗位分析系统。核心思路是用Spring AI作为大模型应用开发框架把RAG检索增强生成和Tool Calling工具调用结合起来让系统不仅能基于知识库回答问题还能主动调用外部工具去查数据、做计算、生成结构化报告。为什么选 Spring AI 而不是直接调大模型 API因为 Spring AI 提供了一套标准化的抽象层把提示词模板、对话记忆、向量存储、工具调用这些常用能力都封装好了和 Spring Boot 的自动装配风格一脉相承。你不需要自己从头写 HTTP 客户端、管理对话上下文、处理向量检索的胶水代码省下来的时间可以花在业务逻辑上。而且 Spring AI 支持多种模型提供商切换模型时改动很小这对国内开发者来说很实用因为很多时候我们需要在通义千问、DeepSeek、智谱等模型之间做选择。这个系统适合谁参考如果你已经会写 Spring Boot 项目想往 AI 应用开发方向走或者你正在做招聘、HR 数据分析、职业规划相关的产品这篇文章里的思路和代码可以直接拿去改。即使你之前没接触过 RAG 和 Tool Calling我也会把每个环节拆开讲清楚保证你能跟着做出来。2. 系统整体设计与技术选型拆解2.1 核心架构三层结构各司其职整个系统我分成了三层数据层、AI 能力层、应用层。数据层负责岗位数据的采集、清洗和向量化存储AI 能力层封装 RAG 检索和 Tool Calling 逻辑应用层对外提供 REST 接口和简单的分析报告输出。这样分层的好处是每层可以独立替换。比如数据层今天用内存向量库明天数据量大了换成 Milvus 或 Redis StackAI 能力层的代码基本不用动。AI 能力层里RAG 负责“知识问答”Tool Calling 负责“执行动作”两者结合才能让系统从“只会聊天”变成“能干活”。我画一个简单的数据流向用户提交一个岗位分析请求 → 应用层接收 → AI 能力层先通过 RAG 从岗位知识库中检索相似岗位和技能要求 → 再把检索结果和用户问题一起交给大模型 → 大模型判断是否需要调用工具比如计算技能匹配度、查询薪资统计→ 如果需要触发 Tool Calling → 工具执行完把结果返回给大模型 → 大模型生成最终分析报告。2.2 为什么 RAG 和 Tool Calling 必须一起用单独用 RAG 的问题是它只能基于已有知识库回答没法做实时计算或查询外部数据。比如用户问“这个岗位要求的技能和我简历的匹配度是多少”RAG 只能告诉你岗位要求什么技能但没法算匹配度因为它不知道你的简历内容也不会做集合运算。单独用 Tool Calling 的问题是大模型本身不知道你的岗位知识库里有什么内容它只能根据训练时的通用知识来回答容易产生幻觉。比如你问“我们公司内部岗位体系里 P7 对应什么技能要求”大模型根本不知道你们公司的内部标准。两者结合后RAG 负责“知道”Tool Calling 负责“做到”。RAG 把相关知识喂给大模型Tool Calling 让大模型能调用外部能力去完成具体任务。这个组合在岗位分析场景里特别合适因为岗位分析既需要理解文本RAG 擅长又需要做结构化计算和对比Tool Calling 擅长。2.3 模型选型为什么我用通义千问做主力Spring AI 支持很多模型提供商我最终选了通义千问作为主力模型原因有三个。第一国内访问稳定延迟低不需要额外处理网络问题。第二通义千问在中文理解上表现很好岗位描述里经常出现“熟悉”、“精通”、“了解”这类程度词中文模型对语义程度的把握更准。第三Spring AI Alibaba 对通义千问的集成做得比较完善配置简单文档也全。当然你也可以用 DeepSeek 或智谱Spring AI 的抽象层让切换成本很低。我建议在开发阶段用便宜或免费的模型调试上线前再根据效果和成本决定最终用哪个。2.4 向量存储选型从内存到持久化的演进开发初期我直接用 Spring AI 自带的SimpleVectorStore数据存在内存里重启就没了。这样做的好处是零配置适合快速验证 RAG 流程。但一旦岗位数据超过几百条内存占用和检索速度都会成为问题。后来我换成了 Redis Stack 作为向量存储因为 Redis 本身在 Spring Boot 项目里很常见Redis Stack 又原生支持向量相似度搜索。配置也不复杂加一个spring-ai-redis-store依赖配好连接信息就能用。如果你数据量更大比如几十万条岗位可以考虑 Milvus 或 PostgreSQL 的 pgvector 扩展Spring AI 都有对应的 starter。3. 核心细节解析与实操要点3.1 RAG 流程拆解从文档到向量再到答案RAG 的全称是 Retrieval-Augmented Generation翻译过来就是“检索增强生成”。它的核心思想是大模型在回答问题时先从一个外部知识库里检索出相关内容再基于这些内容生成答案。这样做的好处是答案有依据不容易胡编。在岗位分析系统里我的知识库就是一堆岗位描述文档。每条岗位描述先经过文档读取器DocumentReader读成文本再用文本分割器TextSplitter切成小块。为什么要切块因为大模型的上下文窗口有限而且整篇岗位描述直接塞进去检索精度反而会下降。切块后每块大概 500 到 1000 个字符块与块之间留一点重叠避免把一句话切断。切好的文本块通过嵌入模型EmbeddingModel转成向量存到向量数据库里。当用户提问时问题也会被转成向量然后在向量数据库里找最相似的几个文本块把这些块的内容拼成上下文和问题一起发给大模型。这里有个关键参数相似度阈值和返回条数。我实测下来返回条数设 3 到 5 条比较合适。太少可能漏掉关键信息太多会引入噪声反而干扰大模型判断。相似度阈值我一般设 0.7 左右低于这个值的块就不返回了。3.2 Tool Calling 机制让大模型学会“动手”Tool Calling 的本质是你提前定义好一些工具函数告诉大模型这些工具是干什么的、需要什么参数。大模型在对话过程中如果判断需要调用某个工具就会输出一个结构化的调用请求你的代码接收到请求后执行对应函数再把结果返回给大模型大模型继续生成最终回答。在 Spring AI 里定义一个工具非常简单。你只需要写一个普通的 Java 方法加上Tool注解然后在 ChatClient 构建时注册这个工具类就行。Spring AI 会自动把方法的名称、描述、参数类型转换成大模型能理解的工具定义。我定义了三个工具第一个是calculateSkillMatch接收岗位技能列表和用户技能列表返回匹配度百分比第二个是getSalaryRange接收岗位名称和城市返回薪资范围第三个是generateAnalysisReport接收分析结果返回格式化的报告文本。这里有个坑要注意工具方法的参数类型要尽量简单用 String、Integer、List 这些基础类型不要用复杂的自定义对象。因为大模型生成工具调用参数时复杂对象的嵌套结构容易出错。如果确实需要传复杂数据可以拆成多个简单参数或者用 JSON 字符串传递。3.3 提示词模板设计让大模型按套路出牌Spring AI 的PromptTemplate可以让你把提示词写成模板用占位符替换变量。我在岗位分析场景里设计了两个核心模板。第一个是 RAG 问答模板结构是系统角色说明 检索到的岗位知识 用户问题 输出格式要求。系统角色说明里我明确告诉大模型“你是一个岗位分析专家只基于提供的岗位知识回答问题如果知识里没有相关信息就说不知道不要编造”。第二个是工具调用模板结构是系统角色说明 可用工具列表 用户请求 调用规则。调用规则里我强调“如果需要计算匹配度或查询薪资必须调用对应工具不要自己估算”。提示词模板的写法直接影响输出质量。我踩过的坑是一开始没写输出格式要求大模型返回的内容格式五花八门有时候是纯文本有时候是 Markdown有时候还带一堆解释性废话。后来我在模板里明确要求“用 JSON 格式返回包含 skills、matchScore、suggestions 三个字段”输出就稳定多了。3.4 对话记忆管理多轮对话不串味岗位分析往往不是一问一答就结束的。用户可能先问“这个岗位要求什么技能”再问“我只会 Java 和 MySQL匹配度多少”接着问“那我应该补哪些技能”。这就需要对话记忆ChatMemory来保持上下文。Spring AI 提供了InMemoryChatMemory和基于 Redis 的持久化记忆。开发阶段用内存版就够了生产环境建议用 Redis避免服务重启后对话历史丢失。记忆的窗口大小也要控制我一般保留最近 10 轮对话太早的对话对当前问题帮助不大反而占用上下文窗口。这里有个细节RAG 检索和对话记忆要配合使用。每次用户提问时先用当前问题去检索知识库再把检索结果和对话历史一起发给大模型。如果只用对话历史不用 RAG大模型会逐渐偏离岗位知识库的内容如果只用 RAG 不用对话历史多轮对话就失去了连贯性。4. 实操过程与核心环节实现4.1 项目初始化与依赖配置我用 Spring Initializr 创建项目选 Spring Boot 3.2.x 和 Java 17。依赖方面除了常规的spring-boot-starter-web还需要加 Spring AI 的 starter。如果你用通义千问加spring-ai-alibaba-starter如果用 OpenAI 兼容接口加spring-ai-openai-spring-boot-starter。向量存储我选了 Redis Stack所以还要加spring-ai-redis-store-spring-boot-starter和spring-boot-starter-data-redis。完整的pom.xml关键依赖如下dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-web/artifactId /dependency dependency groupIdcom.alibaba.cloud.ai/groupId artifactIdspring-ai-alibaba-starter/artifactId version1.0.0-M2/version /dependency dependency groupIdorg.springframework.ai/groupId artifactIdspring-ai-redis-store-spring-boot-starter/artifactId version1.0.0-M2/version /dependency dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-data-redis/artifactId /dependency配置文件application.yml里要配好模型和 Redis 连接信息spring: ai: dashscope: api-key: ${DASHSCOPE_API_KEY} chat: options: model: qwen-plus temperature: 0.7 vectorstore: redis: index: job-index prefix: job: data: redis: host: localhost port: 6379注意API Key 不要硬编码在配置文件里用环境变量注入。我见过有人直接把 Key 提交到代码仓库结果被扫到后产生大量异常调用这个坑一定要避开。4.2 岗位数据向量化入库我准备了一批岗位描述文本每条包含岗位名称、公司、城市、技能要求、经验要求等字段。先用TokenTextSplitter把长文本切成块切块参数我设的是chunkSize800、chunkOverlap100。为什么是 800因为通义千问的上下文窗口足够大800 字符的块能保留完整的技能描述段落同时检索时粒度也够细。切块后调用vectorStore.add(documents)批量入库。这里有个性能优化点批量入库比逐条入库快很多。我一开始循环调用add1000 条数据花了将近 3 分钟改成批量后20 秒左右就完成了。Spring AI 的add方法支持传 List底层会做批处理。入库完成后我写了一个简单的测试接口用vectorStore.similaritySearch验证检索效果。输入“Java 后端 微服务”返回的前 3 条结果确实都是相关岗位说明向量化质量没问题。4.3 RAG 问答链路实现RAG 问答的核心代码在 Service 层。我注入ChatClient和VectorStore在方法里先检索再生成public String analyzeJob(String question) { ListDocument docs vectorStore.similaritySearch( SearchRequest.query(question).withTopK(5).withSimilarityThreshold(0.7) ); String context docs.stream() .map(Document::getContent) .collect(Collectors.joining(\n---\n)); return chatClient.prompt() .system(你是一个岗位分析专家只基于提供的岗位知识回答问题。) .user(u - u.text(岗位知识\n{context}\n\n问题{question}) .param(context, context) .param(question, question)) .call() .content(); }这段代码里withTopK(5)表示返回最相似的 5 条withSimilarityThreshold(0.7)表示相似度低于 0.7 的不返回。实测下来这两个参数对结果影响很大。TopK 设太小会漏信息设太大引入噪声阈值设太低会返回不相关内容设太高可能一条都返回不了。4.4 Tool Calling 工具定义与注册工具类的定义我放在一个单独的JobTools类里每个方法加Tool注解Component public class JobTools { Tool(description 计算岗位技能与用户技能的匹配度返回百分比) public double calculateSkillMatch( ToolParam(description 岗位要求的技能列表) ListString jobSkills, ToolParam(description 用户掌握的技能列表) ListString userSkills) { if (jobSkills null || jobSkills.isEmpty()) return 0; long matchCount jobSkills.stream() .filter(skill - userSkills.stream() .anyMatch(us - us.equalsIgnoreCase(skill))) .count(); return (double) matchCount / jobSkills.size() * 100; } Tool(description 根据岗位名称和城市查询薪资范围) public String getSalaryRange( ToolParam(description 岗位名称) String jobName, ToolParam(description 城市名称) String city) { // 实际项目中这里查数据库或调外部接口 return 根据市场数据 city 的 jobName 薪资范围约为 15K-30K; } }注册工具时在构建 ChatClient 的地方加上.defaultTools(jobTools)。Spring AI 会自动扫描Tool注解的方法生成工具定义发给大模型。这里有个关键点工具方法的 description 要写清楚。大模型是根据 description 来判断什么时候调用哪个工具的。如果 description 写得模糊大模型可能该调的时候不调或者调错工具。我一开始把calculateSkillMatch的 description 写成“计算匹配度”结果大模型经常不调用它而是自己估算一个数字。后来改成“计算岗位技能与用户技能的匹配度返回百分比当用户询问匹配度时必须调用此工具”调用率就上来了。4.5 完整分析流程串联把 RAG 和 Tool Calling 串起来后完整的分析流程是这样的用户提交岗位分析请求 → 系统先用 RAG 检索相关岗位知识 → 把检索结果和用户问题发给大模型 → 大模型判断是否需要调用工具 → 如果需要执行工具并把结果返回 → 大模型生成最终分析报告。我写了一个 Controller 接口来触发这个流程PostMapping(/analyze) public AnalysisResult analyze(RequestBody AnalyzeRequest request) { String context ragService.retrieveContext(request.getQuestion()); String answer chatClient.prompt() .system(你是岗位分析专家基于岗位知识回答需要计算时调用工具。) .user(u - u.text(岗位知识{context}\n\n用户问题{question}) .param(context, context) .param(question, request.getQuestion())) .tools(jobTools) .call() .content(); return new AnalysisResult(answer); }实测下来这个流程能处理大部分岗位分析场景。比如用户问“我会 Java、Spring Boot、MySQL这个岗位匹配度多少”系统会先检索到相关岗位的技能要求然后大模型调用calculateSkillMatch工具算出匹配度最后生成带建议的分析报告。5. 常见问题与排查技巧实录5.1 RAG 检索不准怎么办这是最常见的问题。我遇到过的表现是用户问“微服务经验要求”检索出来的却是“前端开发”的岗位描述。排查下来主要有三个原因。第一个原因是文本切块不合理。如果切块太大一个块里混了多个岗位的信息检索时就会串味。解决办法是减小 chunkSize或者按岗位 ID 分组切块确保每个块只属于一个岗位。第二个原因是嵌入模型不适合中文。有些默认的嵌入模型对中文语义理解不好导致相似度计算不准。换成专门优化中文的嵌入模型或者用通义千问自带的 embedding 接口效果会明显提升。第三个原因是相似度阈值设得太低。阈值低意味着什么都能返回噪声自然多。我建议从 0.7 开始试根据实际效果上下调整。如果发现返回结果太少降到 0.6如果噪声太多升到 0.8。5.2 工具调用不触发或调错工具大模型不调用工具通常是因为工具描述不够明确。我前面提过description 要写清楚“什么时候调用”和“调用后返回什么”。另外工具方法的参数名也要有意义不要用arg1、arg2这种用jobSkills、userSkills这种语义清晰的名称。调错工具的情况也有。比如用户问薪资大模型却调了匹配度计算工具。这通常是因为两个工具的 description 有重叠大模型分不清。解决办法是在 description 里明确区分场景比如薪资工具的 description 写“当用户询问薪资、工资、待遇时调用”匹配度工具写“当用户询问匹配度、适合程度时调用”。5.3 对话记忆导致上下文混乱多轮对话时如果记忆窗口太大早期不相关的对话会干扰当前回答。我遇到过用户先问 A 岗位再问 B 岗位结果大模型把 A 岗位的信息混进了 B 岗位的回答里。解决办法是控制记忆窗口大小或者在做 RAG 检索时把当前问题和最近几轮对话一起作为检索输入而不是只用当前问题。还有一个技巧在系统提示词里加一句“只基于当前问题和最新检索到的知识回答不要参考过时的对话内容”。这句话能明显减少上下文串扰。5.4 性能瓶颈与优化系统跑起来后我发现两个性能瓶颈。第一个是向量检索速度数据量到几千条后每次检索要几百毫秒。优化方法是给 Redis 向量索引加合适的参数比如调整ef_construction和ef_runtime在精度和速度之间找平衡。第二个是大模型响应速度。通义千问的qwen-plus模型响应时间在 2 到 5 秒左右如果加上工具调用可能到 8 秒以上。优化方法是把不必要的大模型调用去掉比如简单的技能提取可以用规则引擎先处理只把复杂分析交给大模型。下面这张表是我整理的问题速查表方便你快速定位问题现象可能原因排查方向解决办法检索结果不相关切块太大或嵌入模型不适配检查 chunkSize 和嵌入模型减小切块换中文嵌入模型工具不调用description 不明确检查工具描述补充调用场景说明调错工具工具描述重叠对比工具 description明确区分各工具适用场景多轮对话串味记忆窗口太大检查 ChatMemory 配置缩小窗口加系统提示词约束响应太慢模型调用次数多统计大模型调用链路合并调用规则前置5.5 几个我踩过的坑第一个坑是依赖版本冲突。Spring AI 的版本和 Spring Boot 版本有对应关系我用 Spring Boot 3.2.x 配 Spring AI 1.0.0-M2 没问题但换成 3.3.x 后有些 API 变了编译报错。建议锁定版本不要随意升级。第二个坑是Redis 向量索引创建失败。Redis Stack 需要先创建索引才能做向量搜索Spring AI 的 starter 会自动创建但如果 Redis 版本不对或者配置有误索引创建会静默失败。排查方法是连上 Redis 用FT._LIST命令看索引是否存在。第三个坑是工具方法抛异常导致整个对话中断。工具方法里如果抛了未捕获的异常Spring AI 会把异常信息返回给大模型大模型可能会因此生成奇怪的回答。解决办法是在工具方法里做好异常处理返回友好的错误提示而不是让异常直接抛出去。6. 后续可以继续扩展的方向这套系统目前能完成基本的岗位分析和技能匹配但还有很多可以深挖的地方。比如把Agentic RAG的思路引进来让系统能自主决定检索策略而不是固定 TopK 和阈值。再比如接入GraphRAG把岗位、技能、公司、行业之间的关系建成知识图谱检索时能利用图结构找到更精准的关联信息。工具方面也可以继续丰富。现在只有匹配度计算和薪资查询后面可以加简历解析工具、面试题生成工具、学习路径推荐工具。每个工具都是一个独立的 Spring Bean加进去就能用扩展成本很低。如果你也在做类似的项目我的建议是先把 RAG 链路跑通确保检索质量没问题再加 Tool Calling。因为 RAG 是基础工具是锦上添花。基础不牢工具调得再花哨回答也是错的。另外提示词一定要反复调我前后改了十几版才达到比较稳定的效果这个功夫省不得。