HireMind:从 0 到 1,用 LangGraph 打造 7 Agent 协作的智能招聘平台
一、引言
招聘场景中,HR 每天要面对大量简历,手工评估不仅耗时,而且标准不一。能不能让 AI 来完成这件事?从简历解析、技能匹配,到面试题生成、综合评分,全流程自动化——听起来简单,但每一步都需要不同的能力:解析需要抽取结构化信息,匹配需要检索知识库,评分需要综合判断。单次 LLM 调用无法覆盖这么长的链路。
HireMind 是我独立设计实现的一个开源项目,基于 DeepSeek + LangGraph 构建了 7 个专业 Agent 协作的智能招聘评估平台。如果要用一句话概括它的核心思路,那就是:把复杂任务拆成多个小步骤,每个步骤交给专门的 Agent 处理,用状态图编排它们之间的流转逻辑。
在线演示 👉 http://36.151.144.50
Git 仓库 👉 https://gitee.com/monan1122/hire-mind
二、架构总览
项目采用 Vue 3 + Java Spring Boot 3 + Python FastAPI 三层架构。前端负责交互,Java 层管理业务数据(用户、岗位、简历、报告),Python 层承载所有 AI 能力。Java 通过 WebClient 异步调用 Python 的/evaluate接口,拿到评估报告后存入 MySQL,前端轮询展示结果。
用户上传 PDF 简历 ↓ Java 业务层 (Spring Boot) ← MySQL / Redis ↓ WebClient POST /evaluate Python AI 层 (FastAPI) ↓ LangGraph StateGraph 编排 ↓ Resume → Desensitize → Skill → Router → Interview → Score → Report三、7 Agent 协作 Pipeline
LangGraph 的StateGraph是整个 AI 层的骨架。每个 Agent 接收一个共享的AgentContext对象,处理完后更新状态,传递给下一个节点。关键设计在于条件边(Conditional Edge)——每个节点执行完后,根据结果走不同的分支。
builder.add_node("resume", resume_node) builder.add_node("resume_fallback", resume_fallback) builder.add_node("desensitization", desensitize_node) builder.add_node("skill", skill_node) builder.add_node("condition_router", condition_router_node) builder.add_node("interview", interview_node) builder.add_node("score", score_node) builder.add_node("score_fallback", score_fallback) builder.add_node("report", report_node) # Resume 节点:成功 → 脱敏,重试 → 再试,降级 → 正则兜底,失败 → 终止 builder.add_conditional_edges( "resume", lambda ctx: route_after_agent(ctx, "resume"), { "success": "desensitization", "retry": "resume", "fallback": "resume_fallback", "fail": "error_handler", } )真正有意思的是Condition Router。如果候选人的技能匹配度不到 50%,说明他很可能不适合这个岗位,这时候再花 token 去生成面试题纯粹是浪费。于是我们在 Skill Agent 之后插入了一个纯规则节点:
async def condition_router_node(ctx: AgentContext) -> AgentContext: match_score = ctx.skill_result.match_score if ctx.skill_result else 0 ctx.skip_interview = match_score < 50 return ctx def route_after_condition(ctx: AgentContext) -> str: return "score" if ctx.skip_interview else "interview"别看这只是个简单的 if-else,在每天 100 份简历的场景下,假设 30% 匹配度不达标,就省下了 30 次 LLM 调用。
四、异常处理:让 Pipeline 不会一碰就碎
Agent Pipeline 是串行的,任何一个节点挂了都可能让整个任务失败。我给每个 Agent 设计了三级异常处理:
- Tier 1 可重试:API 超时、网络抖动 → 指数退避重试,最多 3 次
- Tier 2 可降级:JSON 解析失败、字段缺失 → 规则兜底,不中断流程
- Tier 3 终止:核心服务不可用 → 返回 PartialResult + 错误原因
async def call_deepseek_safe(prompt: str, node: str, ctx: AgentContext) -> str: max_retries = MAX_RETRIES.get(node, 1) for attempt in range(max_retries): try: content = await deepseek.chat(prompt, model=model) return content except Exception as e: ctx.retry_counts[node] = ctx.retry_counts.get(node, 0) + 1 if attempt < max_retries - 1: await asyncio.sleep(2 ** attempt) continue raiseScore Agent 的降级策略是一个典型的纯规则兜底:
async def score_fallback(ctx: AgentContext) -> AgentContext: match = ctx.skill_result.match_score if ctx.skill_result else 0 project_count = len(ctx.resume_desensitized.projects) if ctx.resume_desensitized else 0 raw_score = match * 0.7 + project_count * 5 ctx.score_result = { "final_score": min(raw_score, 100), "recommendation": "进入技术面试" if match >= 60 else "建议淘汰", "strengths": [], "weaknesses": ["评分系统暂时不可用"], } ctx.error = None return ctx实践验证:即使 DeepSeek API 完全不可用,系统仍能基于关键词匹配和规则评分返回可用的评估结果——matchScore × 0.7 + 项目数 × 5,简单但有效。
五、混合检索 RAG
岗位知识库和面试题库的查询有两种截然不同的需求:有时是精确技能标签匹配("Java"、"Redis"),有时是语义描述匹配("有分布式系统经验")。纯 ES BM25 解决不了语义问题,纯 Milvus 向量对精确标签效果差。于是两边都查,然后 RRF 融合:
RRF_K = 60 def _rrf_fusion(es_results, milvus_results, k=RRF_K): scores = {} for rank, doc in enumerate(es_results): scores[hash(str(doc.get("id", "")))] = 1 / (k + rank + 1) for rank, doc in enumerate(milvus_results): key = hash(str(doc.get("id", ""))) scores[key] = scores.get(key, 0) + 1 / (k + rank + 1) merged = {} for doc in es_results + milvus_results: did = hash(str(doc.get("id", ""))) if did in scores: merged[did] = {**doc, "_fusion_score": scores[did]} return sorted(merged.values(), key=lambda x: x["_fusion_score"], reverse=True)同时 Milvus 检索做了 3 秒超时保护,不可用时自动降级为纯 ES:
async def _safe_milvus_search(collection: str, query: str, top_k: int): try: return await asyncio.wait_for( milvus_client.search(collection, query, top_k), timeout=3.0 ) except (asyncio.TimeoutError, Exception): return []六、数据脱敏与合规
所有发往 DeepSeek API 的数据在离开本地网络前必须脱敏。但教育和技能信息又必须保留——删掉学历和专业就没法正确匹配了。这是一个「选择性脱敏」的问题。
实现方式很直接:正则匹配 + 占位符替换,零外部依赖,单次 < 5ms。关键是在 Pipeline 中找准插入点——脱敏节点必须在 Resume Agent 之后、任何数据流向外部 API 之前:
async def desensitize_node(ctx): if ctx.resume_raw: ctx.resume_desensitized = desensitize_resume(ctx.resume_raw) return ctx脱敏只针对 PII(个人身份信息),教育/技能/项目描述完整保留,不影响下游的匹配和评分准确度。
七、写在最后
这个项目从最初的单文件 Python 脚本,到后来加入 LangGraph 编排、Java 业务层、Vue 前端,经历了好几轮迭代。几个关键体会:
- 拆解比堆 prompt 更可靠:把大任务拆成 7 个小 Agent,每个只做一件事,调试和优化都更可控
- 降级比完美更重要:API 会超时、JSON 会解析失败、服务会不可用——每个节点都有 Plan B 比追求 100% 准确率更实际
- 脱敏不是可选项:从第一版就把脱敏做进 Pipeline,比后期补救省心得多
欢迎试用和提 Issue。在线演示 👉 http://36.151.144.50,管理员账号admin/admin123,候选人账号user1/user123。
