| """ |
| LangGraph Agent 版本 - 把 ResumeParser、JDParser、Matcher、GapAnalyzer、StrategyPlanner 变成真正的图节点 |
| 包含状态管理、条件边、失败重试 |
| """ |
| from __future__ import annotations |
|
|
| import json |
| import sys |
| import os |
| from typing import Any, Dict, List, Optional, TypedDict, Annotated, Literal |
| import operator |
|
|
| |
| sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) |
|
|
| from langgraph.graph import StateGraph, END, START |
|
|
| |
| from src.resume_parser import parse_resume |
| from src.jd_parser import parse_jd |
| from src.matcher import rank_jobs, score_job |
| from src.conversion import attach_conversion_scores |
| from src.strategy_planner import gen_priority_top3 |
|
|
| |
| class AgentState(TypedDict): |
| """LangGraph Agent 状态""" |
| |
| |
| resume_text: str |
| jd_text: Optional[str] |
| jobs: list[dict] |
| |
| |
| profile: Optional[dict] |
| jd_parsed: Optional[dict] |
| |
| |
| matched_jobs: Optional[list[dict]] |
| ranked_jobs: Optional[list[dict]] |
| |
| |
| gaps: Optional[list[dict]] |
| strategy: Optional[list[dict]] |
| |
| |
| current_step: str |
| error: Optional[str] |
| retry_count: int |
| max_retries: int |
| |
| |
| trace: list[str] |
|
|
|
|
| |
| def resume_parser_node(state: AgentState) -> AgentState: |
| """简历解析节点""" |
| print("[LangGraph] 📄 ResumeParser Node", file=sys.stderr) |
| |
| try: |
| resume_text = state.get("resume_text", "") |
| if not resume_text: |
| state["error"] = "resume_text 为空" |
| state["current_step"] = "error" |
| return state |
| |
| |
| profile = parse_resume(resume_text) |
| |
| state["profile"] = profile |
| state["current_step"] = "resume_parsed" |
| state["error"] = None |
| state["trace"] = state.get("trace", []) + ["resume_parser: success"] |
| |
| print(f"[LangGraph] ✅ ResumeParser 完成: skills={len(profile.get('skills', []))}, has_llm_project={profile.get('has_llm_project', False)}", file=sys.stderr) |
| |
| except Exception as e: |
| state["error"] = f"ResumeParser 失败: {str(e)}" |
| state["current_step"] = "error" |
| state["retry_count"] = state.get("retry_count", 0) + 1 |
| state["trace"] = state.get("trace", []) + [f"resume_parser: error - {str(e)}"] |
| print(f"[LangGraph] ❌ ResumeParser 失败: {e}", file=sys.stderr) |
| |
| return state |
|
|
|
|
| def jd_parser_node(state: AgentState) -> AgentState: |
| """JD 解析节点(可选,如果输入了 jd_text)""" |
| print("[LangGraph] 📋 JDParser Node", file=sys.stderr) |
| |
| try: |
| jd_text = state.get("jd_text") |
| |
| |
| if not jd_text: |
| state["jd_parsed"] = None |
| state["current_step"] = "jd_skipped" |
| state["trace"] = state.get("trace", []) + ["jd_parser: skipped (no jd_text)"] |
| print("[LangGraph] ⏭️ JDParser 跳过(无 jd_text)", file=sys.stderr) |
| return state |
| |
| |
| jd_parsed = parse_jd(jd_text) |
| |
| state["jd_parsed"] = jd_parsed |
| state["current_step"] = "jd_parsed" |
| state["error"] = None |
| state["trace"] = state.get("trace", []) + ["jd_parser: success"] |
| |
| print(f"[LangGraph] ✅ JDParser 完成: title={jd_parsed.get('title', '')}, direction={jd_parsed.get('direction', '')}", file=sys.stderr) |
| |
| except Exception as e: |
| state["error"] = f"JDParser 失败: {str(e)}" |
| state["current_step"] = "error" |
| state["retry_count"] = state.get("retry_count", 0) + 1 |
| state["trace"] = state.get("trace", []) + [f"jd_parser: error - {str(e)}"] |
| print(f"[LangGraph] ❌ JDParser 失败: {e}", file=sys.stderr) |
| |
| return state |
|
|
|
|
| def matcher_node(state: AgentState) -> AgentState: |
| """岗位匹配节点""" |
| print("[LangGraph] 🎯 Matcher Node", file=sys.stderr) |
| |
| try: |
| profile = state.get("profile") |
| jobs = state.get("jobs", []) |
| jd_parsed = state.get("jd_parsed") |
| |
| if not profile: |
| state["error"] = "profile 为空(ResumeParser 未执行)" |
| state["current_step"] = "error" |
| return state |
| |
| if not jobs: |
| state["error"] = "jobs 岗位库为空" |
| state["current_step"] = "error" |
| return state |
| |
| |
| if jd_parsed: |
| |
| job_with_score = score_job(jd_parsed, profile) |
| state["matched_jobs"] = [job_with_score] |
| state["ranked_jobs"] = [job_with_score] |
| state["current_step"] = "matched_single" |
| state["trace"] = state.get("trace", []) + ["matcher: single_jd_mode"] |
| print(f"[LangGraph] ✅ Matcher 完成(单 JD 模式): pass_score={job_with_score.get('pass_score', 0)}", file=sys.stderr) |
| |
| |
| else: |
| |
| resume_text = state.get("resume_text", "") |
| ranked = rank_jobs(jobs, profile, resume_text) |
| state["matched_jobs"] = ranked |
| state["ranked_jobs"] = ranked[:10] |
| state["current_step"] = "matched_batch" |
| state["error"] = None |
| state["trace"] = state.get("trace", []) + [f"matcher: batch_mode ({len(ranked)} jobs)"] |
| |
| print(f"[LangGraph] ✅ Matcher 完成(批量模式): {len(ranked)} 个岗位匹配", file=sys.stderr) |
| |
| except Exception as e: |
| state["error"] = f"Matcher 失败: {str(e)}" |
| state["current_step"] = "error" |
| state["retry_count"] = state.get("retry_count", 0) + 1 |
| state["trace"] = state.get("trace", []) + [f"matcher: error - {str(e)}"] |
| print(f"[LangGraph] ❌ Matcher 失败: {e}", file=sys.stderr) |
| |
| return state |
|
|
|
|
| def gap_analyzer_node(state: AgentState) -> AgentState: |
| """差距分析节点""" |
| print("[LangGraph] 🔍 GapAnalyzer Node", file=sys.stderr) |
| |
| try: |
| profile = state.get("profile") |
| ranked_jobs = state.get("ranked_jobs", []) |
| |
| if not profile or not ranked_jobs: |
| state["error"] = "profile 或 ranked_jobs 为空" |
| state["current_step"] = "error" |
| return state |
| |
| |
| gaps = [] |
| for i, job in enumerate(ranked_jobs[:3]): |
| missing_skills = job.get("missing_skills", []) |
| gaps.append({ |
| "rank": i + 1, |
| "title": job.get("title", ""), |
| "company": job.get("company", ""), |
| "missing_skills": missing_skills, |
| "pass_score": job.get("pass_score", 0), |
| "risk_score": job.get("risk_score", 0), |
| "suggestions": [ |
| f"补充技能: {skill}" for skill in missing_skills[:3] |
| ], |
| }) |
| |
| state["gaps"] = gaps |
| state["current_step"] = "gaps_analyzed" |
| state["error"] = None |
| state["trace"] = state.get("trace", []) + [f"gap_analyzer: {len(gaps)} gaps found"] |
| |
| print(f"[LangGraph] ✅ GapAnalyzer 完成: {len(gaps)} 个岗位差距分析", file=sys.stderr) |
| |
| except Exception as e: |
| state["error"] = f"GapAnalyzer 失败: {str(e)}" |
| state["current_step"] = "error" |
| state["retry_count"] = state.get("retry_count", 0) + 1 |
| state["trace"] = state.get("trace", []) + [f"gap_analyzer: error - {str(e)}"] |
| print(f"[LangGraph] ❌ GapAnalyzer 失败: {e}", file=sys.stderr) |
| |
| return state |
|
|
|
|
| def strategy_planner_node(state: AgentState) -> AgentState: |
| """策略规划节点""" |
| print("[LangGraph] 🎯 StrategyPlanner Node", file=sys.stderr) |
| |
| try: |
| ranked_jobs = state.get("ranked_jobs", []) |
| profile = state.get("profile") |
| |
| if not ranked_jobs or not profile: |
| state["error"] = "ranked_jobs 或 profile 为空" |
| state["current_step"] = "error" |
| return state |
| |
| |
| strategy = gen_priority_top3(ranked_jobs, profile) |
| |
| state["strategy"] = strategy |
| state["current_step"] = "strategy_generated" |
| state["error"] = None |
| state["trace"] = state.get("trace", []) + [f"strategy_planner: {len(strategy)} strategies"] |
| |
| print(f"[LangGraph] ✅ StrategyPlanner 完成: {len(strategy)} 个岗位策略", file=sys.stderr) |
| for s in strategy: |
| print(f" - {s['rank']}. {s['title']} @ {s['company']}: {s['apply_action']}", file=sys.stderr) |
| |
| except Exception as e: |
| state["error"] = f"StrategyPlanner 失败: {str(e)}" |
| state["current_step"] = "error" |
| state["retry_count"] = state.get("retry_count", 0) + 1 |
| state["trace"] = state.get("trace", []) + [f"strategy_planner: error - {str(e)}"] |
| print(f"[LangGraph] ❌ StrategyPlanner 失败: {e}", file=sys.stderr) |
| |
| return state |
|
|
|
|
| def error_handler_node(state: AgentState) -> AgentState: |
| """错误处理节点:决定是否重试或终止""" |
| print("[LangGraph] 🚨 ErrorHandler Node", file=sys.stderr) |
| |
| retry_count = state.get("retry_count", 0) |
| max_retries = state.get("max_retries", 3) |
| |
| if retry_count < max_retries: |
| print(f"[LangGraph] 🔄 重试 {retry_count + 1}/{max_retries}", file=sys.stderr) |
| state["retry_count"] = retry_count + 1 |
| state["trace"] = state.get("trace", []) + [f"error_handler: retry {retry_count + 1}/{max_retries}"] |
| |
| |
| error = state.get("error", "") |
| if "ResumeParser" in error: |
| state["current_step"] = "resume_parser" |
| elif "JDParser" in error: |
| state["current_step"] = "jd_parser" |
| elif "Matcher" in error: |
| state["current_step"] = "matcher" |
| elif "GapAnalyzer" in error: |
| state["current_step"] = "gap_analyzer" |
| elif "StrategyPlanner" in error: |
| state["current_step"] = "strategy_planner" |
| else: |
| |
| state["current_step"] = "matcher" |
| |
| return state |
| else: |
| print(f"[LangGraph] 🛑 达到最大重试次数 ({max_retries}),终止", file=sys.stderr) |
| state["current_step"] = "failed" |
| state["trace"] = state.get("trace", []) + [f"error_handler: max retries reached"] |
| return state |
|
|
|
|
| |
| def should_continue(state: AgentState) -> Literal["jd_parser", "matcher", "gap_analyzer", "end", "error"]: |
| """决定是否继续执行、报错或结束""" |
| current_step = state.get("current_step", "") |
| error = state.get("error") |
| |
| |
| if error and state.get("retry_count", 0) < state.get("max_retries", 3): |
| return "error" |
| |
| |
| if state.get("retry_count", 0) >= state.get("max_retries", 3): |
| return "end" |
| |
| |
| if current_step == "resume_parsed": |
| |
| return "jd_parser" if state.get("jd_text") else "matcher" |
| elif current_step == "jd_parsed" or current_step == "jd_skipped": |
| return "matcher" |
| elif current_step == "matched_single" or current_step == "matched_batch": |
| return "gap_analyzer" |
| elif current_step == "gaps_analyzed": |
| return "strategy_planner" |
| elif current_step == "strategy_generated": |
| return "end" |
| elif current_step == "failed": |
| return "end" |
| else: |
| return "end" |
|
|
|
|
| |
| def build_graph() -> StateGraph: |
| """构建 LangGraph 状态机""" |
| print("[LangGraph] 🏗️ 构建 Graph...", file=sys.stderr) |
| |
| |
| graph = StateGraph(AgentState) |
| |
| |
| graph.add_node("resume_parser", resume_parser_node) |
| graph.add_node("jd_parser", jd_parser_node) |
| graph.add_node("matcher", matcher_node) |
| graph.add_node("gap_analyzer", gap_analyzer_node) |
| graph.add_node("strategy_planner", strategy_planner_node) |
| graph.add_node("error_handler", error_handler_node) |
| |
| |
| graph.add_edge(START, "resume_parser") |
| |
| |
| graph.add_conditional_edges( |
| "resume_parser", |
| should_continue, |
| { |
| "jd_parser": "jd_parser", |
| "matcher": "matcher", |
| "error": "error_handler", |
| "end": END, |
| } |
| ) |
| |
| graph.add_conditional_edges( |
| "jd_parser", |
| should_continue, |
| { |
| "matcher": "matcher", |
| "error": "error_handler", |
| "end": END, |
| } |
| ) |
| |
| graph.add_conditional_edges( |
| "matcher", |
| should_continue, |
| { |
| "gap_analyzer": "gap_analyzer", |
| "error": "error_handler", |
| "end": END, |
| } |
| ) |
| |
| graph.add_conditional_edges( |
| "gap_analyzer", |
| should_continue, |
| { |
| "strategy_planner": "strategy_planner", |
| "error": "error_handler", |
| "end": END, |
| } |
| ) |
| |
| graph.add_conditional_edges( |
| "strategy_planner", |
| should_continue, |
| { |
| "end": END, |
| "error": "error_handler", |
| } |
| ) |
| |
| |
| graph.add_conditional_edges( |
| "error_handler", |
| should_continue, |
| { |
| "resume_parser": "resume_parser", |
| "jd_parser": "jd_parser", |
| "matcher": "matcher", |
| "gap_analyzer": "gap_analyzer", |
| "strategy_planner": "strategy_planner", |
| "end": END, |
| } |
| ) |
| |
| print("[LangGraph] ✅ Graph 构建完成", file=sys.stderr) |
| |
| return graph |
|
|
|
|
| |
| def run_agent( |
| resume_text: str, |
| jobs: list[dict], |
| jd_text: Optional[str] = None, |
| max_retries: int = 3, |
| ) -> dict: |
| """ |
| 运行 LangGraph Agent |
| |
| Args: |
| resume_text: 简历文本 |
| jobs: 岗位库列表 |
| jd_text: 可选,单个 JD 文本(用于 JD 匹配模式) |
| max_retries: 最大重试次数 |
| |
| Returns: |
| dict: 包含 profile, strategy, gaps, trace 等 |
| """ |
| print("[LangGraph] 🚀 启动 Agent...", file=sys.stderr) |
| |
| |
| graph = build_graph() |
| app = graph.compile() |
| |
| |
| initial_state = { |
| "resume_text": resume_text, |
| "jd_text": jd_text, |
| "jobs": jobs, |
| "profile": None, |
| "jd_parsed": None, |
| "matched_jobs": None, |
| "ranked_jobs": None, |
| "gaps": None, |
| "strategy": None, |
| "current_step": "start", |
| "error": None, |
| "retry_count": 0, |
| "max_retries": max_retries, |
| "trace": [], |
| } |
| |
| |
| final_state = app.invoke(initial_state) |
| |
| print(f"[LangGraph] ✅ Agent 完成,轨迹: {final_state.get('trace', [])}", file=sys.stderr) |
| |
| |
| return { |
| "profile": final_state.get("profile"), |
| "strategy": final_state.get("strategy"), |
| "gaps": final_state.get("gaps"), |
| "matched_jobs": final_state.get("matched_jobs"), |
| "ranked_jobs": final_state.get("ranked_jobs"), |
| "trace": final_state.get("trace", []), |
| "error": final_state.get("error"), |
| "current_step": final_state.get("current_step"), |
| } |
|
|
|
|
| |
| if __name__ == "__main__": |
| |
| import json |
| |
| |
| test_resume = """ |
| 教育背景:XX大学 计算机科学 硕士 2024-2027 |
| 项目经历: |
| 1. 基于 RAG 的简历匹配系统:使用 LLM + Embedding + FAISS 实现人岗匹配 |
| 2. 推荐系统项目:使用 DeepFM 实现 CTR 预估,NDCG@5 提升 12% |
| 技能:Python, PyTorch, LLM, RAG, Agent, 推荐系统, Transformer |
| """ |
| |
| |
| try: |
| with open("data/jobs.json", "r", encoding="utf-8") as f: |
| jobs = json.load(f) |
| except: |
| jobs = [] |
| |
| |
| result = run_agent(test_resume, jobs, max_retries=3) |
| |
| print("\n" + "="*60) |
| print("LangGraph Agent 运行结果") |
| print("="*60) |
| |
| if result.get("profile"): |
| print(f"Profile: {json.dumps(result['profile'], ensure_ascii=False, indent=2)}") |
| |
| if result.get("strategy"): |
| print(f"\nStrategy ({len(result['strategy'])} 个岗位):") |
| for s in result['strategy']: |
| print(f" {s['rank']}. {s['title']} @ {s['company']}: {s['apply_action']}") |
| |
| print(f"\nTrace: {result['trace']}") |
| print(f"Error: {result['error']}") |
| print(f"Current Step: {result['current_step']}") |
| print("="*60) |
|
|