Spaces:
Paused
Paused
| """ | |
| 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 | |
| # ========== State 定义 ========== | |
| class AgentState(TypedDict): | |
| """LangGraph Agent 状态""" | |
| # 输入 | |
| resume_text: str | |
| jd_text: Optional[str] # 可选:单个 JD 匹配模式 | |
| jobs: list[dict] # 岗位库 | |
| # 解析结果 | |
| profile: Optional[dict] # ResumeParser 输出 | |
| jd_parsed: Optional[dict] # JDParser 输出(如果输入了 jd_text) | |
| # 匹配结果 | |
| matched_jobs: Optional[list[dict]] # Matcher 输出 | |
| ranked_jobs: Optional[list[dict]] # 排序后的岗位 | |
| # 分析和策略 | |
| gaps: Optional[list[dict]] # GapAnalyzer 输出 | |
| strategy: Optional[list[dict]] # StrategyPlanner 输出 | |
| # 状态控制 | |
| current_step: str | |
| error: Optional[str] | |
| retry_count: int | |
| max_retries: int | |
| # 元数据 | |
| trace: list[str] # 执行轨迹(用于调试和可解释性) | |
| # ========== Node 定义 ========== | |
| 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") | |
| # 如果没有 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 | |
| # 如果是单个 JD 匹配模式 | |
| if jd_parsed: | |
| # 把 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: | |
| # 使用 rank_jobs 进行批量匹配 | |
| resume_text = state.get("resume_text", "") | |
| ranked = rank_jobs(jobs, profile, resume_text) | |
| state["matched_jobs"] = ranked | |
| state["ranked_jobs"] = ranked[:10] # Top 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 | |
| # Gap 分析:对比简历和 Top3 岗位的要求 | |
| 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 | |
| # 调用现有模块生成 Top3 策略 | |
| 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: | |
| # 默认重试从 matcher 开始 | |
| 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": | |
| # 如果有 jd_text,先解析 JD;否则直接匹配 | |
| 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" | |
| # ========== 构建 Graph ========== | |
| def build_graph() -> StateGraph: | |
| """构建 LangGraph 状态机""" | |
| print("[LangGraph] 🏗️ 构建 Graph...", file=sys.stderr) | |
| # 创建 StateGraph | |
| 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 | |
| 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": [], | |
| } | |
| # 运行 Graph | |
| 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 = [] | |
| # 运行 Agent | |
| 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) | |