
1. 为什么要做这个岗位分析系统而不是直接问一遍大模型1.1 触发点一次被“通用回答”气到的经历事情得从去年下半年说起。那时我在给团队做招聘侧的效率工具日常要处理大量岗位JD把 HR 发来的几十条岗位描述拆成职责、要求、福利再逐条匹配候选人简历还要顺手评估“这个岗位定级定薪合不合理”。这些工作本身不复杂但极其重复而且依赖经验。团队里每个人对同一份 JD 的分析口径都不一样有的只看技能清单有的重视行业背景有的对薪酬敏感。我一开始的设想很简单接一个大模型把 JD 丢进去让它直接给分析结果。结果很不理想。第一次试用时模型把一份 Java 架构师 JD 的薪酬估成了“一线城市 30k-80k”看着没错但要被人力同事追问“30k 的 25 分位和 80k 的 75 分位到底是多少”时它自己就圆不回来了。更难受的是我问“这个岗位在杭州和苏州的薪资差异大概多大”模型反手给我编了一组数字听上去很合理实际完全对不上我们公司历史招聘数据。那一刻我就意识到通用大模型能回答问题但它不知道“我知道什么”。1.2 我真正需要的三种能力绕着需求转了一圈我认为岗位分析系统必须同时具备三个能力缺一个都不行。第一要有场景知识。分析岗位绝不能只看当下这一份 JD至少要参考我们公司过去一年发过的相似岗位、候选人谈薪结果、岗位定级标准。这些数据在业务系统里但大模型没见过所以系统得能“查资料”。第二要有实时事实。薪酬水平、城市生活成本、技能热度这类信息变化很快最稳妥的办法是从专门的数据库或第三方接口去取而不是让模型猜。第三要有结构化的输出。岗位分析结果不是聊天记录它要落到表格、报告、评估模型里所以必须指定固定字段和格式。于是技术方案就很明确了RAG 解决“查历史资料”的问题Tool Calling 解决“调实时数据”的问题Spring AI 负责把这两件事和模型调用串成一条流水线。虽然也可以拆开来分别调大模型 API、自己写向量检索逻辑、再手动维护工具调用协议但既然团队技术栈是 Java Spring Boot用 Spring AI 天然更顺这也是整篇文章的起点。1.3 和直接调 API 相比Spring AI 帮我省了什么我早期写过不少直接调用大模型 HTTP API 的代码最烦的不是发请求而是几个细节对话上下文的维护、工具调用协议function calling 的消息往返、向量库和 embedding 模型的接入、输出 JSON 的反序列化。这些功能每个都是“理论上不难、写起来一团乱麻”的主自行实现不仅代码量大而且换一个模型服务商就崩一遍。Spring AI 把这堆东西抽象成了统一 API。聊天层面有 ChatClient 这种流式 fluent 接口RAG 层面有 Advisor 机制QuestionAnswerAdvisor 可以直接把向量检索结果注入提示词工具调用层面有 Tool 注解一个普通 Spring Bean 方法声明好描述就能被模型发现和调用。这些都是我后来踩了很多坑才体会到值在哪里的设计。当然Spring AI 2.0 的 API 也不是没有坑后面我会专门用一章来讲联调过程中踩到的问题。2. 技术选型与整体架构Spring AI 2.0、百炼 Qwen3、pgvector 如何分工2.1 技术栈一览与选型理由我先列一下最终采用的组件每一项都说明为什么是它层次选型理由应用框架Spring Boot 3.x Spring AI 2.x团队 Java 技术栈与现有业务系统天然集成模型服务阿里云百炼 DashScopeQwen3-Max text-embedding-v3中文理解能力强国内访问稳定支持工具调用Embedding 支持长文本向量数据库PostgreSQL pgvector已有 PostgreSQL 基础设施不用额外引入向量数据库服务支持元数据过滤和 SQL 运维RAG 检索增强QuestionAnswerAdvisor Rerank 模型Spring AI 提供标准 AdvisorRerank 用于提升检索命中率业务数据MySQL存放岗位、薪酬分位、城市系数、技能趋势实时事实数据Tool Calling 直接查表返回模型接入方式Spring AI Alibaba 适配器对百炼 DashScope 的 Spring AI 适配Bean 和配置统一选型时我特别留意了一点不要把“模型服务”和“向量数据库”捆绑太死。Spring AI 的抽象层允许后面把 Qwen3 换成百炼上的其他模型或把 pgvector 换成 Milvus代价都不大。这也是我在对比 LangChain4j 后依然选择 Spring AI 的原因——至少在 Java 生态里Spring AI 的抽象更贴近 Spring Boot 的使用习惯。2.2 一条查询要经过的三条信息通道整个系统的核心逻辑可以通俗地理解为模型在回答一个岗位分析问题前要先“开卷”和“打电话”。“开卷”就是 RAG系统先把历史 JD、定级文档、面评摘要做向量化存进 pgvector。用户提交一份 JD系统用问题向量去库里捞相似岗位内容拼进提示词。这相当于考试时允许你翻以前的试卷。“打电话”就是 Tool Calling当问题涉及实时薪酬、城市生活成本、技能趋势时模型不自己编而是调用我预先写好的 Java 方法去 MySQL 里查真实数字。这比翻旧试卷更进一步相当于直接给业务系统打了个电话。日常开发中很多人容易把这两件事混为一谈。我的经验是归档的静态知识适合 RAG频繁变化或强约束的数据适合 Tool Calling。如果薪酬数据也放进向量库麻烦很快就来——数据更新要重新切分重新 embedding而且检索出来的数值未必是最新的模型还可能在多份矛盾数据里“挑一个看起来对”的。但如果把“如何分析岗位”这种方法论也交给工具去查又会把系统搞得很笨重。区分标准就是会不会变要不要强口径。2.3 应用层怎么拆模块我按职责把工程拆成了几个独立模块job-ingest负责 JD 数据清洗、切分、向量化入库。job-retrieve封装向量检索、元数据过滤、重排逻辑对外只暴露一个“按问题取上下文”的方法。job-tools所有 Tool Calling 方法都放这里每个方法对应一个独立业务查询。job-analysis核心流程编排调用 ChatClient 完成分析并输出结构化结果。common-model统一的请求响应对象、工具返回对象、评估脚本。这个拆法最大的好处是RAG 和 Tool Calling 可以分别调试。我实际开发时就是先把 job-retrieve 单独跑通用单元测试验证检索结果再把 job-tools 当成普通 Service 测数据正确性最后才在 job-analysis 里拼装。否则混在一起出问题很难定位是检索质量差还是模型没选对工具。3. RAG 知识库落地从 JD 数据清洗到向量检索的完整链路3.1 数据收集与清洗脏数据比算法更难处理做 RAG 最容易踩的坑是以为“只要文档进去、向量出来效果就自然好”。实际上知识库的效果 60% 取决于数据质量。我第一批导入了大约 1200 份历史 JD来源有两类一类是业务系统里结构化的岗位数据另一类是市场部门拿到的脱敏公开 JD。后者格式非常乱有的带 HTML 标签有的是 PDF 转出来的乱序文本还有大量全角半角混合、重复段落。清洗流程我按下面几步走去 HTML 标签统一换行符把全角英文和数字转半角。按招聘平台的模板差异尽量把“岗位职责”“任职要求”“薪酬福利”等段落抽成独立字段。去重以岗位名称 公司 发布时间做指纹重复 JD 只保留最近一份。脱敏把联系人电话、邮箱、内部编号全部替换成占位符。补全结构对于缺失“城市”“岗位等级”的 JD用规则模型补上补不上的丢弃。清洗完成后文本质量有明显变化。我专门对比过不清洗时检索命中的内容经常包含乱码片段清洗后同样的查询召回结果干净利落。这里有个容易被忽略的细节模型回答时会直接引用检索片段如果片段里有乱码、脱敏残留模型会一本正经地把这些噪声写进分析报告里。3.2 文档切分策略先试固定长度再改成语义块切分是 RAG 里最影响命中率的一环也是我优化耗时最长的地方。最初我图省事按固定长度 500 字切一个 chunk、重叠 50 字。跑测试集时发现一个典型问题一段 JD 的“任职要求”可能正好被切到两个 chunk 里检索时只召回一半模型看到的要求不完整分析自然有偏差。后来我改成按语义结构切分。JD 本身有很强的段落属性我按“岗位职责”“任职要求”“薪酬福利”“公司描述”四个语义段切每段如果超过 800 字再按句号进一步切小。实测下来语义切分在 hit rate 上比固定窗口高了不少。下表是我在 20 条测试查询上的对比切分策略topK5 命中率备注固定 500 字重叠 5068%职责和任职要求被拆散固定 300 字重叠 5072%碎片化严重噪声多按语义段切800 字再切86%信息完整性与粒度平衡最好经验是切分不是越小越好关键是保证“一个语义单元尽量落在一个 chunk 里”。JD 的段落语义边界清晰按语义段切是性价比最高的方案。如果你处理的文档没有明显段落可以先按 Markdown 标题或编号切再考虑固定长度。3.3 Embedding 模型选型与向量化入库Embedding 模型我选了 text-embedding-v3。选择理由有三个中文效果比 OpenAI 的通用 embedding 好不少支持自定义向量维度我在 768 维下做了归一化单次最大 token 数能覆盖我切出来的大多数 chunk。向量化入库这一步比较机械但有个细节很关键入库的时候必须把元数据一并写进去。我用 PgVectorStore 存储时每条记录都会带上 jobId、city、positionLevel、postDate 这几个字段。为什么要带元数据因为检索时如果不加过滤会出现一个北京 Java 岗的分析问题把上海的 Java 岗也捞出来薪酬参考直接失真。元数据过滤就是给检索加“地域、层级”闸门。pgvector 的建表不用手工写 SQLSpring AI 的 PgVectorStore 会自动建表。索引我选择了 HNSW建好后查询延迟基本在毫秒级。如果你数据量超过百万级可以再考虑 IVFFlat 或换专门的向量数据库但在我们这个规模下 pgvector 已经完全够用。3.4 检索链路与命中率优化topK、阈值、重排一个都不能少检索不是“查出来就行”而是要“把最相关的放在最前面”。我调优过程中主要动了四个旋钮topK最开始设 10发现模型很容易被不相关的内容带偏。降到 4 之后回答准确率反而提升了。检索不是给越多越好模型面对 10 段历史 JD 时注意力会被稀释开始“综合”出一些并不存在的规律。相似度阈值设得太低会放一堆无关内容进来。我根据验证集的分布最终定在 0.35 左右低于这个值的检索结果直接丢弃。元数据过滤按城市、岗位大类过滤。比如分析“杭州前端工程师”就先过滤 city杭州 category前端再做向量相似度排序。重排 Rerank向量相似度是“语义像不像”但它不等于“回答需要”。我接了一个 reranker把向量检索出来的 topK 候选再做一次精排只保留最相关的 2-3 段给模型。这一步对命中率提升非常明显。调优过程中我发现一个现象很多人只盯“返回了什么”不看“返回的顺序”。其实 RAG 回答质量很大程度上取决于 top1 准不准因为模型对第一段内容的信任度天然更高。重排的意义就是把最准的那条顶到第一位。另外中文环境里还有一个容易栽的坑一个问题里有多个语义点比如“Java 工程师需要什么微服务技能”如果只切 JD 的一个 chunk很可能只命中“微服务”或只命中“Java 工程师”相关段落。我后来对查询做了拆分式检索一次查询拆成“岗位职责”“技能要求”“福利薪酬”三个子查询分别检索再把结果合并去重效果又提了一层。4. Tool Calling 设计让模型该回答时回答该查数据时查数据4.1 为什么薪酬等实时数据绝不能靠模型编RAG 能解决“历史知识”的问题但岗位分析里有一类数据是不能靠知识库兜底的实时薪酬分位、城市房价系数、技能热度趋势。这些数据按月甚至按周变化如果放进向量库你得不断重新切分、重新 embedding而且检索出旧数据的概率很高。更关键的是薪酬数字是强口径数据模型一旦“自由发挥”轻则分析失真重则影响招聘定薪决策。Tool Calling 在这里的作用很直接模型发现要回答薪酬问题时不是自己去“想”而是触发我定义好的 Java 方法去 MySQL 或外部接口查询然后把真实返回结果拼进上下文再生成最终回答。这相当于给模型装了一部电话让它只说查到的数字不说猜的数字。4.2 用 Tool 注解实现三个业务工具在 Spring AI 里定义工具非常直接我写了一个 PositionTools 组件注册了三个方法。这里贴一个精简版代码Component public class PositionTools { private final SalaryRepository salaryRepository; private final CityRepository cityRepository; private final SkillTrendRepository skillTrendRepository; public PositionTools(SalaryRepository salaryRepository, CityRepository cityRepository, SkillTrendRepository skillTrendRepository) { this.salaryRepository salaryRepository; this.cityRepository cityRepository; this.skillTrendRepository skillTrendRepository; } Tool(description 按岗位名称和城市查询市场薪酬分位值返回包含25分位、50分位、75分位的文本) public String queryPositionSalary(String positionName, String city) { SalaryStat stat salaryRepository.findByPositionAndCity(positionName, city); if (stat null) { return 暂无[ positionName ]在[ city ]的薪酬数据请提示用户改用一线城市均值; } return String.format( 岗位[%s]在[%s]的月薪分位25分位%d元50分位%d元75分位%d元数据月份%s, positionName, city, stat.getP25(), stat.getP50(), stat.getP75(), stat.getMonth()); } Tool(description 查询城市生活成本指数包括房价系数和月均生活支出用于评估岗位薪酬购买力) public String queryCityCost(String city) { CityCost cost cityRepository.findByCity(city); if (cost null) { return 暂无[ city ]的生活成本数据; } return String.format( [%s]生活成本房价系数%s月均支出%d元数据年份%d, city, cost.getHousingIndex(), cost.getMonthlyExpense(), cost.getYear()); } Tool(description 查询技能在近12个月招聘JD中的热度趋势返回月度招聘需求指数) public String querySkillTrend(String skillName) { ListSkillTrend trends skillTrendRepository.findLast12Months(skillName); if (trends.isEmpty()) { return 暂无[ skillName ]的热度趋势数据; } StringBuilder sb new StringBuilder(技能[ skillName ]近12月招聘需求指数); trends.forEach(t - sb.append(t.getMonth()).append().append(t.getIndex()).append()); return sb.toString(); } }这里有几个我反复强调的要点。工具方法必须无状态且幂等因为模型在调用时可能重试不能因为一次调用就修改了内部状态。方法的 description 要写得非常具体包括单位、口径、缺失数据的兜底说明。返回文本比返回结构化 JSON 在多数场景下更稳因为模型后续要把这段文本继续拼进上下文里“阅读理解”一段通顺的文本比一层嵌套 JSON 更不容易出错。4.3 控制模型“选对工具”的实战经验Tool Calling 不是说定义了工具就完事实际跑起来最大的问题是“模型选错工具”和“模型不选工具”。模型不选工具常见于问题描述和工具描述匹配度不够。我一开始把工具描述写成“查询薪酬”结果模型遇到“杭州这个岗位大概给多少钱”时没有触发调用而是直接回答。后来我把描述改成了“按岗位名称和城市查询市场薪酬分位值返回包含25分位、50分位、75分位的文本”并在系统提示词里明确写了“分析薪酬时必须调用 queryPositionSalary 工具”问题就消失了。模型选错工具典型场景是“Spring Cloud 工程师”被识别成“Java 工程师”去查了 Java 的薪酬。这类问题没有一招鲜的解法我采取的补救措施是工具参数里加上岗位类别的同义词映射同时在返回结果里带上一句“若岗位名称不精确请基于该城市同职级通用数据修正”。还有一个深层问题工具返回空数据时模型会怎样我最初返回的是空串结果模型为了“完成任务”竟然自己编了个数字。改成明确的兜底话术后模型会诚实地说“暂无该城市薪酬数据”。所以工具返回的每一句话都是给模型“看”的不能只返回 true/false要返回一句人话。这个思路在调试时帮我解决了不少“模型胡编”的顽疾。5. 主流程组装RAG、工具与生成式回答的衔接细节5.1 分析一条 JD 的完整时间线系统真正跑起来的完整流程比想象中要细。我拆成下面几个步骤每一步都有明确的输入输出。用户传入 JD 文本和城市信息。系统先用查询词拆出来的“职责”“技能”“薪酬”子查询分别去 pgvector 检索过滤城市和岗位大类topK4再做重排最终选出 2-3 个最相关历史 JD 片段。这些片段作为“背景知识”拼进系统提示词。ChatClient 把用户问题和背景知识交给 Qwen3-Max。模型在生成过程中发现需要薪酬、城市成本、技能趋势数据于是发起 Tool Calling。系统执行对应 Java 方法把返回结果注入模型上下文模型继续生成。最终模型输出结构化 JSON系统反序列化为 AnalysisResponse 对象落库或返回前端。整个过程从用户角度来看就是提交一条 JD十几秒后拿到一份分析报告。但从系统角度看模型实际经历了多轮“思考-调工具-再思考”的过程。这一步如果不用 Spring AI 的 ChatClient 封装手写协议非常痛苦因为要自己去维护 tools call 的消息往返格式。5.2 提示词分层把“背景、任务、实时数据”分开提示词不要一锅烩。我最终实践下来把内容分成三层效果最好系统层定义角色和任务边界例如“你是资深岗位分析师分析必须基于检索到的历史知识和工具返回的实时数据禁止编造”。背景层RAG 检索片段、工具返回结果统一放在一个半透明“参考资料”区域明确告诉模型“这些才是可信依据”。用户层待分析的 JD 原文和指令要求。贴一个组装后的大致结构【系统指令】 你是一名资深岗位分析师。请基于【背景资料】和【实时工具数据】完成分析禁止编造无出处的薪酬数字。 【背景资料-历史JD】 这里放入 RAG 检索出的 2-3 段历史 JD 片段 【用户JD】 这里放入用户提交的 JD 原文 所在城市杭州 【输出要求】 必须输出 JSONjobSummary, coreSkills, salaryEstimate, interviewQuestions 薪酬部分必须调用工具获取实际分位值。在 Spring AI 中用 Query 和 Advisor 做拼接时要注意QuestionAnswerAdvisor 默认会把检索结果直接追加在用户消息后面所以你如果已经在用户文本里写了“请分析以下 JD”需要确认追加位置不会破坏语义。后来我采用了自定义 Advisor把“背景资料”固定插在用户消息之前输出稳定了很多。5.3 结构化输出让模型返回能直接入库的 JSON岗位分析结果最终要落到业务系统里所以我用 ChatClient 的 entity 方法直接反序列化成对象AnalysisResponse response chatClient.prompt() .user(prompt) .tools(new PositionTools(...)) .call() .entity(AnalysisResponse.class);这里有个实战坑如果模型返回的内容里带 Markdown 代码块包裹比如json ... 反序列化会失败。解决办法是在系统提示词里强制加上“只输出 JSON不要 Markdown 代码块”并给 entity 对应的类字段都加上默认值兜底。Qwen3 对 JSON 格式遵循得不错但偶尔还会在前后加解释文字最好在回调里做一层清洗。public class AnalysisResponse { private String jobSummary; private ListString coreSkills; private SalaryEstimate salaryEstimate; private ListString interviewQuestions; // getters/setters 省略 }6. 踩坑记录Spring AI 2.0 与百炼 Qwen3 联调中的真实问题6.1 Spring AI Alibaba 与通用 DashScope Starter 的依赖冲突我一开始的 pom 里既加了 Spring AI 官方的 spring-ai-starter-model-dashscope又加了 Spring AI Alibaba 的 starter。结果启动时直接报 NoUniqueBeanDefinitionException因为两个包都会创建 ChatModel 相关的 Bean。后来我删掉了官方 DashScope Starter统一使用 Spring AI Alibaba 的适配问题才消失。依赖这块我的建议是如果确定用百炼就别混用两套 DashScope 接入包。查看你引入的 Spring AI Alibaba 版本对应的 BOM统一依赖版本避免 Spring AI 核心包和 Alibaba 适配包版本不一致导致的方法签名不兼容。Spring AI 2.0 的 API 变化比较大网上很多 1.0 的写法在 2.0 下会直接编译不过。6.2 RAG 检索不到预期内容元数据过滤表达式的坑有一次测试“分析杭州 Java 架构师职位”结果 RAG 一个历史 JD 都没召回到我一度以为是 embedding 出了问题。后来打印检索日志发现过滤条件写成了city杭州而 pgvector 里存的是city杭州没错但 Spring AI 的 FilterExpression 语法要求的是city 杭州这种写法不是 SQL 的等号。改成符合规范的表达式后召回恢复正常。这个坑提醒我不要凭直觉写过滤条件先看 Spring AI 文档里 FilterExpression 的语法。类似的坑还有字段名大小写、数字类型过滤要加引号等。排查 RAG 不命中问题时第一步永远是打印 VectorStore 的实际查询条件而不是怀疑 embedding 模型。6.3 Tool Calling 参数解析失败返回类型和描述设计不当联调后期我遇到一个奇怪现象薪酬工具偶尔返回正常偶尔模型回答“无法获取数据”。打开日志发现模型有一次调用 queryPositionSalary 时把 positionName 传成了一大段完整 JD 文本而不是岗位名。原因是工具描述里没有明确“岗位名称为简短职位名而非完整JD”。这是 Tool Calling 的经典问题模型的参数抽取依赖你对参数的语义描述描述写得不清楚它就乱塞。修复方法有两个维度。一是把参数描述写严格例如“positionName仅传岗位简称例如Java开发、前端工程师如果是完整JD文本请先提取岗位名称再调用”。二是在工具方法里做入参校验如果参数过长或包含“要求、职责、薪资”等 JD 常见词直接返回“参数格式错误请提取简短岗位名称后重试”。这个兜底方法能有效止损。6.4 上下文窗口溢出与 token 成本失控使用过程中发现当 RAG 检索片段多、工具返回内容长、历史 JD 又长时Qwen3-Max 的上下文窗口也会被顶到边缘响应变慢且费用上升。我统计了一下单次分析最高消耗过 12k token其中大部分是检索片段和工具返回文本。优化动作有三条先把 topK 从 4 降到 3重排后只保留 2 段工具返回文本控制长度比如薪酬工具只返回三个分位数和月份不返回明细对超过 1500 字的 JD先让模型做一轮摘要再用摘要做后续分析。这是我强烈建议做的成本优化尤其是你要对成千上万条 JD 批量分析的时候。6.5 中文长文本切分边界导致的“答非所问”切分做语义化之后大部分问题解决了但还有一个隐蔽问题长句被切到下一段时上一段末尾只剩半个分句模型读到“任职要求精通分布式系统架构熟悉……”就戛然而止分析结果里出现莫名其妙的断尾。后来我做了两个处理语义段切分时以句号、分号作为边界避免中间切断小于 30 字的残留片段直接合并到前一段不单独成 chunk。这样处理后“半句上下文”的情况基本消失了。6.6 并发场景下的 QPS 限流与重试上线后有个批处理任务要对 2000 条 JD 跑分析结果跑到一半开始报限流错误。百炼的 QPS 配额是有限的批量任务必须做并发控制。我加了一个简单的信号量限制最大并发数为 5并实现了指数退避重试。这里要特别提醒Spring AI 的 ChatModel 调用不内置重试你一定要在业务层自己控制重试策略避免模型服务端限流时任务直接失败。问题根因解决办法Bean 冲突启动失败混用两套 DashScope 接入包只保留 Spring AI Alibaba 适配RAG 召不回过滤条件语法错误按 FilterExpression 规范写过滤条件工具参数被乱塞参数描述不严格细化参数说明 方法内入参校验token 成本高检索片段和工具结果太长降 topK、压缩工具返回、JD 先摘要中文切分断尾语义段边界覆盖不足句号边界 残留片段合并批量任务被限流QPS 超配额信号量限并发 指数退避重试7. 效果评估与后续演进从“能跑”到“好用”7.1 我用的评估方法命中率不是唯一指标很多 RAG 项目只盯着 hit rate但岗位分析系统里检索命中率只是基础。我更关心三个指标的组合检索命中率hit5测试查询里正确历史 JD 是否出现在前 5 个检索结果中。这是 RAG 的根基。结构完整性生成的 JSON 是否能无错解析字段是否齐全。事实一致性工具返回的薪酬数字是否被模型原样引用有没有被“润色”导致失真。我建了一个约 30 个 JD、60 条查询的小型测试集手工标注了每个查询对应的相似历史 JD。刚开始 hit5 只有 72%经过语义切分、元数据过滤、查询拆分、重排这一系列优化后提到了 91%。结构完整性和事实一致性也因为有明确的提示词约束和工具兜底基本稳定在 95% 以上。7.2 一次真实输出对比拿一份“杭州资深Java工程师”JD 跑系统优化前后的差异很明显。优化前模型写“建议月薪 25k-40k”没有依据也没说哪个分位优化后输出是“依据 queryPositionSalary 返回数据杭州资深Java 50分位月薪 38k参考历史相似 JD 的定级建议区间 35k-42k”。后者即使数字本身需要业务再确认至少每个数字都有出处HR 同事可以直接去核。我并不是说这套系统能替代人的决策。它的价值是把“查资料—找依据—算薪酬—出题目”这半小时以上的重复劳动压缩到十几秒并且分析可追溯、口径统一。真正难判断的岗位最后还是需要人来定夺。7.3 后续演进Agentic RAG、GraphRAG 与本体约束跑通整套链路后我其实已经看到了下一步的演进方向。当前系统的 RAG 是单轮检索一个问题最多做一次向量查询。但如果 JD 分析要跨多个维度做多跳追问比如“这个岗位要求 DDD而我们团队里 DDD 经验集中在哪些项目里”单轮检索就不够了这就是 Agentic RAG 的场景——让模型自主决定查什么、再根据结果决定要不要继续查。GraphRAG 也是我重点关注的路线。JD 之间有很多实体关系技能依赖岗位、岗位依赖行业、城市系数影响薪酬。把 JD 图谱化之后检索不再只靠向量相似度还能沿着“技能-岗位-行业”的边去推理。配合 Ontology RAG 的思路给检索加一层“岗位本体”约束可以显著减少“表面相似、实质无关”的检索结果。我对后来者的建议很简单先不要急着上复杂架构把 RAG 的数据清洗和切分做扎实把 Tool Calling 的工具描述写严谨再谈 Agent、Graph 这些上层概念。做一个能稳定输出、每个数字都有出处的岗位分析系统比做一个看似聪明但经常编造的系统有价值得多。