SpringAI+RAG+知识图谱:构建智能招聘助手全解析
我从去年开始就一直在折腾一件事把招聘这个流程里那些重复、耗时、靠人肉堆的工作尽量用AI自动化掉。中间尝试过不少方案最后稳定下来的一套组合是SpringAI RAG向量库 知识图谱 多模型路由 Docker做成了一个叫SmartHR的智能招聘助手。这套系统的定位很清楚不是让AI替HR做决定而是帮HR把简历筛选、岗位匹配、面试问题生成、候选人问答这些环节从“小时级”压缩到“分钟级”。这篇文章把这套方案从设计到落地完整拆开讲包括为什么要同时用向量库和知识图谱、多模型路由怎么避免“让一个模型干所有事”的成本浪费、Docker编排时哪些坑容易踩还有几个我实测下来很有用的排查思路。如果你是做Java后端的想接大模型能力但不知道怎么下手或者已经在做AI应用但检索效果始终不理想这篇文章应该能帮上忙。1. 为什么一个“招聘助手”需要五样东西叠在一起1.1 招聘业务的真实痛点没那么好“接住”先讲业务侧的问题。招聘这件事看起来简单JD发出去、简历收上来、筛一筛、约面试但真跑起来你会发现全是“非结构化数据”JD是长短不一的自然语言简历有PDF有Word有图片扫描件候选人对岗位的理解可能跟HR完全不在一个频道上。传统系统能把简历存进数据库但“这个候选人跟这个岗位到底搭不搭”这个问题数据库回答不了需要人去读、去理解、去做判断。我之前试过直接用一个大模型做简历筛选把JD和简历拼一起丢给模型问“匹配吗”效果很一般。原因在于第一大模型上下文有限一份完整简历动辄几千字JD加简历经常超长第二模型没看过你们公司的历史岗位标准、没有你们内部的岗位能力模型纯靠通用知识回答给出的匹配理由非常泛第三成本是真的高每天几百份简历全走大模型换算成token费用不是小数目。这就是SmartHR一开始要解决的问题在一个可控成本范围内让AI能“读懂”简历和JD能给出有依据的判断而不是凭感觉生成一段模棱两可的话。1.2 每个技术选型都在解决一个具体的痛点我最后选定的技术组合每一层都有明确要解决的问题SpringAI解决的是“Java项目怎么优雅地接大模型”。它是Spring生态的AI集成框架把ChatClient、EmbeddingModel、VectorStore这些抽象好了Java后端接大模型不再是自己封装HTTP调用、自己管理prompt模板代码可以写得非常规范且容易替换模型厂商。RAG向量库解决的是“模型不知道公司内部知识和具体岗位标准”的问题。把历史JD、岗位能力模型、面试评价标准、常见面试题这些资料切片向量化检索后拼进prompt模型回答就有了“依据”。知识图谱解决的是“关系推理”的问题。向量库擅长找相似的文本但不擅长回答“候选人A的项目经验覆盖了岗位B要求的哪些技能”这类需要实体关系推理的问题。用图谱把技能、岗位、候选人、项目经历建模成节点和关系查询就是一次遍历准确率比纯文本检索高很多。多模型路由解决的是“成本与效果的平衡”。不是所有问题都需要最强模型意图分类、简单问答用便宜快的小模型就够了简历深度的匹配分析和面试追问生成才需要调用强模型。Docker解决的是“这套东西怎么在一个标准环境里跑起来”。ES或pgvector、Neo4j、模型服务、应用服务依赖太多手工配置环境会疯掉容器化之后一键拉起方便交付也给后续扩展留了余地。1.3 整体架构与模块划分SmartHR项目的模块设计大致可以分成这么几块模块职责关键技术接入层提供REST接口给HR前端调用Spring Boot Web意图识别与路由层判断请求类型路由到不同模型和策略自定义路由策略 小模型分类RAG检索层对JD/面试题/公司知识库做向量召回向量化 相似度检索 上下文组装知识图谱层简历实体抽取、岗位技能关系、匹配度推理Neo4j Cypher查询模型调用层统一封装不同模型供应商的调用SpringAI ChatClient数据存储层向量数据、图谱数据、业务数据存储PostgreSQL/pgvector、Neo4j部署层一键编排所有依赖与应用Docker Compose这个架构看着复杂实际运行时每个请求走的链路并不长。比如HR问“这个候选人适合前端岗吗”请求进来后先做意图识别命中“简历匹配度评估”然后并行做两件事一是把候选人简历向量化去ES里召回相似岗位的历史评估案例二是通过知识图谱查候选人掌握的技能和前端岗位要求的技能做交集对比。两路结果合并后拼进prompt路由到强模型生成带评估结论的回答。2. RAG向量库与知识图谱的组合检索设计2.1 向量库选型不是越重型越好RAG这块第一步是选向量库。我在SmartHR里用的是PostgreSQL pgvector理由很实际数据量没到千万级团队本来就熟PostgreSQL加一个扩展就能同时管业务数据和向量数据不用额外维护一套Milvus集群。如果你公司已经有一套Elasticsearch在跑日志或搜索直接把ES当向量库也是不错的选择。选型时有一个容易被忽略的点向量相似度的计算方式。pgvector支持L2距离、内积和余弦相似度。文本检索场景我建议直接用余弦相似度它只关心方向不关心向量长度对Embedding结果中不同文本长度带来的模长差异不那么敏感。建索引时用HNSW查询性能比暴力扫描高一个量级千万级向量也能在几十毫秒内返回。切片策略是另一个容易踩坑的地方。最开始我用的是600字符一个chunk、无重叠结果很多语义被拦腰切断。比如简历里一句“负责xx系统的架构设计主导了微服务拆分”被切成两段后“主导了微服务拆分”单独一个chunk检索时因为缺少主语语义就不完整。后面我调成了按段落切分段落太长再按400字符切并且保留50字符重叠效果明显好了。2.2 知识图谱怎么建模“岗位-技能-候选人”知识图谱这部分核心是定义实体和关系。SmartHR的图谱里最常用的几类实体候选人节点属性是姓名、工作年限、学历、当前所在地技能节点属性是技能名称、分类前端/后端/算法、热度岗位节点属性是岗位名称、职级、薪资带宽项目经历节点属性是项目名、时间、职责描述关系方面比较核心的有这么几条(候选人)-[掌握]-(技能)属性里可以带熟练度比如“熟练”“了解”“用过”(岗位)-[要求]-(技能)属性里带优先级比如“必须”“加分”“可选”(候选人)-[参与]-(项目经历)(项目经历)-[使用了]-(技能)为什么简历也能进图谱因为简历不是光靠向量化就能完全吃透的结构化数据。向量检索能找到“相似简历”但说不清楚“这个候选人为了做支付系统学了哪些东西、最后沉淀了什么技能”。图谱把简历里的项目描述拆成了节点和关系让“深度推理”成为可能。这部分的抽取Pipeline我放在应用层做简历文本先走大模型做信息抽取输出JSON格式的实体和关系再写入Neo4j。抽模型不需要很强的底座模型因为抽取目标是固定的三元组一个小参数模型跑抽取配合JSON Schema约束成本很低。2.3 向量召回和图谱查询如何协同组合检索这步是整个系统的关键。我当时的思路是向量库负责“找相似的素材”知识图谱负责“找确定的关系”两者并行再把结果合并。举个例子候选人问“我三年Java经验没做过中间件开发能试试贵司的后端岗位吗”。向量检索会从知识库召回类似“三年Java经验转岗后端”“无中间件经验求职后端”等历史问答或文档片段图谱查询则做两件事查出后端岗位要求了哪些技能以及候选人当前掌握了哪些技能然后算一个技能覆盖率。两路的输出拼在一起后模型看到的不只是“求职者疑问”的类比例子还有一套可量化的技能匹配情况回答就能落到点上哪几项技能已经具备、哪几项有明显缺口、缺口是否可以通过短期学习补齐。实际组合时要注意图谱查询结果别一股脑全塞给模型。之前我试过把整个技能子图序列化成JSON塞进prompt模型很快就“迷失”在大量细节里回答反而变差。后来我改成只传聚合后的摘要比如“岗位要求的5项技能中候选人覆盖2项Java、Spring Boot缺失3项分布式事务、消息队列、容器化”模型拿到的信息足够且干净生成的答案质量立刻上来。这里也分享一个原则知识图谱阶段能算掉的推理尽量在查询阶段就算完不要指望模型去图数据里做算术。检索质量上还有一点要特别留意Embedding模型的选择直接决定了cheng本。一开始图省事用老版本的通用向量模型中文简历领域效果稀碎同义技能词完全拉不开距离。后来换了一个针对中文企业场景微调过的向量模型技能词召回准确率提升非常明显。换模型后不需要重新设计架构代价只是全量重算一次向量这笔账很划算。3. 多模型路由让每个模型干自己最擅长的活3.1 先分清任务类型再决定谁来回答SmartHR里有6类核心任务简历信息抽取、简历与JD匹配度评估、面试问题生成、候选人问答对话、技能图谱实体抽取、普通闲聊或辅助写作。每一类任务对模型能力的要求差别很大任务类型模型能力要求延迟要求成本敏感度简历信息抽取中只需要按schema输出高高匹配度评估强需要综合推理中中面试问题生成强需要结合岗位细节低中候选人对答中需要延续上下文高高技能实体抽取低固定三元组高高普通闲聊低高高设计路由规则时我走了两步。第一步用一个小模型做意图分类模型只输出一个任务类型标签然后用路由表决定用哪个大模型。第二步把“模型选择”做成了一个可配置的策略而不是写死在代码里。这样后面新增模型供应商、调整模型映射改配置不需要重新编译部署。这里有个容易被忽视的点意图分类模型不要跟生成模型走同一通道。如果意图分类也走最贵的大模型那节省成本的目标就失效了。我在实践里让意图分类走的是一个小参数的本地模型几百毫秒出结果单次调用成本几乎可以忽略。只有在意图分类置信度较低时才降级到强模型重新分类。3.2 路由策略与成本控制路由的核心是“不过在能力上超配”。我定义了一个模型能力矩阵每个候选模型绑定能力标签快、强、便宜、支持长上下文、适合结构化输出。路由时按任务需求匹配模型标签并叠加两个策略第一条是超时降级策略。每个模型都有超时阈值比如小模型3秒没返回就自动切到强模型并在日志里标记“耗时异常”。很多AI应用在底层模型慢的时候直接报错给用户这不合理降级链路是必须的。SmartHR里我专门做了多级降级强模型超时就切备用供应商的强模型备用也超时就走本地缓存规则引擎兜底返回。兜底返回的内容质量会降一些但至少不会让HR那边直接看到失败。第二条是token预算策略。每个任务按历史统计数据有一个token预算相应用高消耗的模型组合。匹配度评估涉及简历全文加上JD输入量很大prompt压缩空间有限这类任务直接给长上下文强模型普通问答用快模型上下文中只保留最近几轮对话摘要把成本压住。运营上也要看实际数据。SmartHR上线两周后我导了一次模型调用统计发现“面试问题生成”居然占了总token消耗的37%但这块角色使用频率并不高。查了一下日志发现生成面试问题时把全部岗位技能知识点都塞进了上下文一次生成就是上万token。后面我改成先在图谱里筛出岗位核心技能Top5再按Top5生成问题token消耗直接降了62%回答质量反而更聚焦。3.3 路由中心的Java实现细节路由中心我基于SpringAI的抽象实现了一个自定义Router组件。核心思路是注入多个ChatClient不同模型各一个Router内部通过策略接口决定每个请求绑定到哪个ChatClient。核心伪代码结构大概是这样Component public class SmartRouter { private final IntentClassifier intentClassifier; private final ModelRouteRegistry routeRegistry; private final MapString, ChatClient chatClientMap; public Completion handle(UserRequest request) { // 第一步意图识别 Intent intent intentClassifier.classify(request.getMessage()); // 第二步从路由注册表拿模型策略 RoutePolicy policy routeRegistry.resolve(intent.getType()); // 第三步执行策略获取目标模型ID String modelId policy.selectModel(request); ChatClient client chatClientMap.get(modelId); // 第四步调用并处理降级 return invokeWithFallback(client, policy, request); } }路由策略本身做成独立的策略类每个策略负责“在这个意图下选哪个模型、超时多久、token预算多少”。这样做的好处是后续调整成本不需要动调用方的代码只要换一个Policy实现或者改配置。SpringAI在这一层还有一点很实用通过ChatClient统一接口之后切换模型供应商只需要换依赖和配置因为模型配置差异都被抽象层抹平了。我在项目里同时接了一个商用强模型和一个开源模型切换时ChatClient的实现类跟着换。如果你打算做多模型通路强烈建议一开始就在SpringAI的ChatClient抽象上写代码不要自己包一层乱七八糟的SDK。4. Docker编排整个服务集群4.1 容器编排的架构与版本选择SmartHR的Docker编排我用了Docker Compose而不是Kubernetes。原因很简单这套系统要跑的节点数量12个以内单机部署够用而且目标使用场景是公司内部或小团队私有化部署没有跨主机的弹性伸缩需求。K8s在这里属于过度设计。Compose文件里编排的核心服务包括PostgreSQLpgvector扩展业务数据和向量数据Neo4j知识图谱存储向量模型服务提供Embedding能力意图分类模型服务小参数模型SmartHR应用服务Spring Boot主服务Redis缓存和简单限流镜像版本这里要提醒一下不要无脑latest。Spring Boot应用要先确认JDK版本和基础镜像兼容Neo4j不同大版本存储格式不通用升级前一定要备份并确认版本路径。我踩过一次Neo4j社区版版本不对导致Cypher语法不兼容的坑后来固定了版本反而稳定了。4.2 资源限制与JVM参数调整容器化部署最容易出问题的就是资源限制。我见过很多Java应用在容器里内存直接被打爆原因是JVM默认堆大小是按宿主机内存算的容器明明只限制了512MBJVM却认为有32GB可用GC永远不干活最后OOM。SmartHR的Spring Boot容器我在Dockerfile里显式设置了JVM参数ENV JAVA_OPTS-Xms512m -Xmx512m -XX:MaxMetaspaceSize256m -XX:UseG1GC ENTRYPOINT [sh, -c, java $JAVA_OPTS -jar app.jar]堆内存固定成512MB后配合容器内存限制1GB系统非常稳。需要注意的是如果你用了向量模型本地推理或需要并行处理大量简历JVM堆设置过小也会导致大量GC暂停建议按“容器内存的50%-70%”来设置堆上限其余留给堆外内存和向量计算开销。Neo4j容器也有自己的内存配置。Neo4j的默认堆大小是1GB页面缓存会动态调整。在Compose里我给它单独配了2GB内存限制并设置了NEO4J_dbms_memory_heap_max__size和NEO4J_dbms_memory_pagecache_size两个环境变量。如果你的图谱数据量不超过百万级节点heap给1Gpagecache给512M够用。4.3 启动顺序与依赖检查多容器编排最大的坑是“服务起来了不等于依赖准备好了”。很多人写完depends_on就以为万事大吉其实depends_on只管容器启动顺序不管服务是否真正可用。Spring Boot起来的时候如果连不上PostgreSQL或Neo4j启动会直接失败。我在编排里加了两个处理一是Spring Boot的启动重试配置让数据源连接失败时自动重试而不是立即退出二是在应用启动脚本里增加了一个健康检查等待逻辑。Compose中有depends_on和condition: service_healthy的写法配合每个服务的healthcheck才能真正做到“等数据库ready了再启动应用”。以Neo4j为例healthcheck可以这么配healthcheck: test: [CMD-SHELL, wget -qO- http://localhost:7474 || exit 1] interval: 10s timeout: 5s retries: 12启动顺序我调过好几轮。最佳顺序是PostgreSQL和Redis先起Neo4j其次向量模型服务再次最后才是Spring Boot应用。向量模型服务如果没起好应用会启动成功但调用Embedding时报连接错误所以我在应用里对模型服务的调用也做了懒加载与重试而不是启动时强校验。5. 实操过程中的常见问题与排查技巧5.1 检索质量差答案答非所问SmartHR刚跑通时最典型的问题是问“有没有做过分布式锁的候选人”向量检索出来的候选简历里混着大段无关内容模型最后给出的回答非常散。排查我按这个顺序走先看召回的内容到底相不相关再看拼进prompt之后模型理解有没有跑偏。做了三件事把切片方式从固定长度改成按段落切分语义完整度上去一截。把Embedding模型换成更适配中文技能栈的领域模型技能词的相似度计算效果好了很多。在检索结果进入prompt前加了TopK截断和相似度过滤。之前TopK拉10条里面可能只有3条相关模型被噪声干扰。现在固定TopK5并丢弃相似度低于0.65的chunk输出质量有明显提升。这轮调整给我一个很深的心得RAG效果不好的时候先怀疑检索质量别急着换大模型。很多问题不是模型不够聪明而是喂给它的上下文不对。检索进得去、噪声降下来模型的表现立刻不一样。5.2 Neo4j查询慢知识图谱数据量到十万级节点后我遇到过一次匹配度查询明显变慢的情况有的查询跑到几秒。一开始以为是数据太多后来用PROFILE一查才发现是缺少索引通过候选人姓名查询、通过技能名称查询都走了全库扫描。Neo4j的索引分两类一类是节点属性上的索引对应查询中的等值匹配另一类是全文索引对应文本模糊查询。SmartHR里核心查询基本是等值匹配比如按候选人ID查他掌握的技能、按岗位ID查它要求的技能这类查询只要在候选人和岗位的属性上建好单属性索引就能从秒级降到毫秒级。5.3 模型路由错乱与超时有一阵子发现很多“匹配度评估”任务被路由到了小模型回答明显深度不够。查日志发现是意图分类模型把带简历内容的长输入误判成了“普通问答”。原因是意图分类模型上下文只有几千token输入的简历内容截断后丢了关键特征。解决思路不是强行让意图模型更准而是调动信息源。我加了规则补偿如果请求里包含明显的结构化简历特征如“工作经历”“项目经历”“技能”等重要字段直接强制路由到匹配度评估。意图模型仍然保留用于处理没有结构化特征的文本。规则先行、模型兜底意图判断的准确率从80%提到了95%以上。另外超时参数也要留有余量。强模型在高峰期响应可能变慢我一开始设的20秒超时高峰期频繁触发降级。后面观察了几天的延迟分布把超时调到35秒降级触发率降下来用户体验也稳定了。超时不是越小越好得根据自己的模型供应商的实际延迟分布来设。5.4 容器内存溢出SmartHR早期遇到过好几次cgroup之外的内存问题。其中一个典型场景是向量模型服务并发处理简历Embedding时批次过大导致显存或内存突然冲高。排查后发现不是模型问题而是调用方并发太高模型服务的批次处理压力过大。解决方式很简单应用层加信号量控制并发Embedding调用数超出并发限制的请求排队等待。加上之后模型服务的内存曲线变得非常平滑。另一个情况是pgvector建索引时内存消耗飙升。HNSW索引构建时需要把大量向量加载进内存我在2GB内存的小机器上建索引直接把PostgreSQL进程挤崩了。解决办法是把新建索引的方式改成分批创建或者调到配置更充裕的机器上做一次性索引构建再回切到生产环境。这个教训是向量索引构建不同于普通表索引它很吃内存和CPU规划部署时要把这部分资源考虑进去。5.5 一个List形式的速查表现象排查重点常见解法召回内容不相关Embedding模型是否匹配领域换领域向量模型调整切片粒度回答发散或跑偏上下文噪声太多降低TopK加相似度阈值过滤图谱查询慢是否缺索引、是否全库扫描对核心查询字段建索引路由到错误模型意图分类判断错误规则先行补偿分类模型兜底容器内存溢出JVM堆设置、并发大小显式设置JVM参数、加信号量限流经常降级超时时间设置不合理根据延迟分布重新设超时阈值6. 一些补充的经验与可继续扩展的方向SmartHR从第一个能跑的版本到现在期间最大的变化不是代码量增长了多少而是对“AI应用靠谱”这件事的理解变了。最开始觉得只要接个大模型什么都能解。真正落地才发现体系化的工程手段才是稳定性的来源检索质量需要向量库与图谱配合控制成本需要路由策略来兜底部署需要容器化和资源治理来保证不崩。现在项目里已经沉淀了一套完整的“简历进来、评估出去”的流水线HR拿到的不只是一个匹配总分还有缺什么技能、哪里经验对口、建议面试重点问什么这些结构化的信息才是真正能辅助决策的东西。如果你也想做类似的系统有两点我个人经验认为值得提前想清楚一是数据准备比模型选型更重要切分、清洗、向量化这几步做得越扎实后面所有环节都受益二是不要试图一个阶段就做出所有能力简历解析跑通了再做匹配评分匹配评分稳定了再接面试问题生成逐步迭代系统会更稳健。另外项目扩展上还有一些方向值得探索引入Rerank模型对召回结果做精排、把多位HR的反馈沉淀成强化数据、在知识图谱里加入更多维度的关系比如团队氛围、项目产出质量这些都能让“匹配度评估”更接近人的判断。SmartHR目前已经验证了这套技术组合的可行性接下来就是要让它在更多真实业务场景里持续打磨。