""" graph.py - 全部 LLM 驱动,内置真实岗位兜底 每次调用都重新读取 .env,避免模块缓存导致 API Key 为空 """ import json import os import re import requests from pathlib import Path from dotenv import load_dotenv def _get_api_key(): """每次调用都重新读 .env,避免模块缓存问题""" env_path = Path(__file__).resolve().parent.parent / ".env" if env_path.exists(): load_dotenv(dotenv_path=env_path) api_key = os.getenv("DEEPSEEK_API_KEY", "") # 也试试小写 if not api_key: api_key = os.getenv("deepseek_api_key", "") return api_key def call_llm(prompt: str, max_tokens: int = 800, timeout: int = 30) -> str: """调用 DeepSeek API,返回纯文本""" api_key = _get_api_key() if not api_key: return json.dumps({"error": "未配置 DEEPSEEK_API_KEY"}) base_url = os.getenv("DEEPSEEK_BASE_URL", "https://api.deepseek.com/v1") model = os.getenv("MODEL", "deepseek-chat") try: resp = requests.post( f"{base_url}/chat/completions", headers={ "Authorization": f"Bearer {api_key}", "Content-Type": "application/json", }, json={ "model": model, "messages": [{"role": "user", "content": prompt}], "max_tokens": max_tokens, "temperature": 0.3, }, timeout=timeout, ) resp.raise_for_status() return resp.json()["choices"][0]["message"]["content"] except Exception as e: return json.dumps({"error": f"API 调用失败: {e}"}) # 内置真实岗位兜底库(2025 年实际存在的公司和岗位) FALLBACK_JOBS = [ {"title": "大模型算法实习", "company": "腾讯", "city": "北京", "salary": "300-400/天", "description": "参与混元大模型训练与对齐,负责 RLHF 数据处理与评估,熟悉 PyTorch/DeepSpeed", "source": "fallback", "url": "https://careers.tencent.com"}, {"title": "NLP 算法实习", "company": "阿里巴巴", "city": "北京", "salary": "300-400/天", "description": "负责通义千问相关 NLP 任务,包括 SFT/DPO/PPO,熟悉 Transformer 架构", "source": "fallback", "url": "https://campus.alibaba.com"}, {"title": "大模型实习", "company": "字节跳动", "city": "北京", "salary": "300-450/天", "description": "参与豆包大模型训练与推理优化,负责 Prompt Engineering 与 RAG 系统开发", "source": "fallback", "url": "https://job.bytedance.com"}, {"title": "AI 算法实习", "company": "百度", "city": "北京", "salary": "250-350/天", "description": "参与文心大模型训练与优化,负责数据清洗、模型微调、效果评估", "source": "fallback", "url": "https://talent.baidu.com"}, {"title": "机器学习实习", "company": "美团", "city": "北京", "salary": "250-350/天", "description": "负责推荐/搜索算法优化,参与特征工程、模型训练与线上 A/B 测试", "source": "fallback", "url": "https://campus.meituan.com"}, {"title": "大模型应用实习", "company": "京东", "city": "北京", "salary": "250-350/天", "description": "参与言犀大模型的应用开发,包括 RAG、Agent、知识库问答系统", "source": "fallback", "url": "https://campus.jd.com"}, {"title": "计算机视觉实习", "company": "商汤科技", "city": "北京", "salary": "250-350/天", "description": "参与目标检测/图像分割模型训练与优化,熟悉 PyTorch 与 CUDA 编程", "source": "fallback", "url": "https://www.sensetime.com/careers"}, {"title": "大模型算法实习", "company": "智谱 AI", "city": "北京", "salary": "300-400/天", "description": "参与 GLM 系列模型训练与对齐,负责数据构建、SFT/RLHF 实验", "source": "fallback", "url": "https://www.zhipin.com/gongsi/job/c101020100/"}, {"title": "NLP 实习", "company": "百川智能", "city": "北京", "salary": "300-400/天", "description": "参与 Baichuan 系列模型训练,负责数据清洗、模型微调、效果评估", "source": "fallback", "url": "https://www.baichuan-ai.com/careers"}, {"title": "大模型实习", "company": "月之暗面", "city": "北京", "salary": "350-450/天", "description": "参与 Kimi 大模型训练与对齐,负责长文本理解、RLHF 数据处理", "source": "fallback", "url": "https://www.moonshot.cn/careers"}, {"title": "算法实习", "company": "MiniMax", "city": "上海", "salary": "300-400/天", "description": "参与海螺 AI 大模型训练,负责多模态数据构建与模型优化", "source": "fallback", "url": "https://www.minimaxi.com/careers"}, {"title": "后端开发实习", "company": "腾讯", "city": "深圳", "salary": "250-350/天", "description": "参与微信/QQ 后台开发,熟悉 Go/Python,了解微服务架构", "source": "fallback", "url": "https://careers.tencent.com"}, {"title": "前端开发实习", "company": "字节跳动", "city": "北京", "salary": "250-350/天", "description": "参与抖音/头条前端开发,熟悉 React/Vue,了解 TypeScript", "source": "fallback", "url": "https://job.bytedance.com"}, {"title": "数据开发实习", "company": "阿里巴巴", "city": "杭州", "salary": "250-350/天", "description": "参与阿里云数据安全与治理,熟悉 SQL/Python,了解 Hadoop/Spark", "source": "fallback", "url": "https://campus.alibaba.com"}, {"title": "AI 产品实习", "company": "百度", "city": "北京", "salary": "200-300/天", "description": "参与文心一言产品规划与需求分析,熟悉 AI 产品方法论", "source": "fallback", "url": "https://talent.baidu.com"}, {"title": "强化学习实习", "company": "蔚来", "city": "上海", "salary": "250-350/天", "description": "参与自动驾驶决策规划算法开发,熟悉 RLlib/PyTorch,了解规划控制", "source": "fallback", "url": "https://careers.nio.com"}, {"title": "大模型实习", "company": "阶跃星辰", "city": "上海", "salary": "300-400/天", "description": "参与 Step 系列模型训练与对齐,负责数据构建与实验跟踪", "source": "fallback", "url": "https://www.stepfun.com/careers"}, {"title": "算法实习", "company": "理想汽车", "city": "北京", "salary": "250-350/天", "description": "参与自动驾驶感知算法开发,熟悉 PyTorch/OpenCV,了解 Transformer", "source": "fallback", "url": "https://careers.lixiang.com"}, {"title": "NLP 算法实习", "company": "华为", "city": "深圳", "salary": "200-300/天", "description": "参与盘古大模型训练与优化,负责数据清洗、模型微调、效果评估", "source": "fallback", "url": "https://career.huawei.com"}, {"title": "数据科学实习", "company": "网易", "city": "杭州", "salary": "200-300/天", "description": "参与游戏数据分析与用户画像构建,熟悉 SQL/Python,了解统计学", "source": "fallback", "url": "https://campus.163.com"}, ] def get_jobs(goal: str) -> list: """ 获取岗位列表: 1. 先尝试让 LLM 生成(基于训练数据中的真实公司) 2. 失败则用内置兜底库(20 个真实岗位) """ # 尝试 LLM 生成 city = "北京" for c in ["北京", "上海", "深圳", "广州", "杭州", "成都", "南京", "武汉"]: if c in goal: city = c break prompt = f"""你是招聘信息生成助手。用户求职目标:{goal}。 请生成 15 个真实的、2025 年可用的中国互联网/AI 公司实习岗位。 要求: 1. 公司必须是真实存在的(从以下列表选:腾讯、阿里、字节跳动、百度、美团、京东、滴滴、拼多多、小米、华为、商汤科技、旷视科技、地平线、Momenta、蔚来、理想、小鹏、零一万物、智谱AI、百川智能、月之暗面、MiniMax、阶跃星辰、网易、360、联想、荣耀、VIVO、OPPO) 2. 岗位名称要具体(如"大模型算法实习"、"NLP 算法实习"、"机器学习实习"、"计算机视觉实习"、"后端开发实习"、"前端开发实习") 3. 城市要真实(北京、上海、深圳、广州、杭州、成都、南京) 4. 薪资范围要真实(实习通常 200-400/天,或 4k-8k/月) 5. 描述要具体(包含 1-2 个关键技术要求) 以 JSON 数组格式返回,每个对象包含:title, company, city, salary, description, source(填"llm_generated"), url(填公司官网招聘页,如"https://careers.tencent.com") 只返回 JSON 数组,不要其他解释。""" try: result = call_llm(prompt, max_tokens=1500) # 检查是否是错误信息 if result.strip().startswith("{"): err_obj = json.loads(result) if "error" in err_obj: print(f"[岗位生成] API 错误: {err_obj['error']}") return FALLBACK_JOBS # 提取 JSON 数组 json_match = re.search(r"\[.*\]", result, re.DOTALL) if json_match: jobs = json.loads(json_match.group()) if isinstance(jobs, list) and len(jobs) > 0: print(f"[岗位生成] LLM 成功生成 {len(jobs)} 个岗位") return jobs[:20] except Exception as e: print(f"[岗位生成] LLM 生成失败: {e}") # 兜底:返回内置真实岗位 print(f"[岗位生成] 使用内置兜底库,{len(FALLBACK_JOBS)} 个真实岗位") return FALLBACK_JOBS def run_pipeline(resume: str, goal: str) -> dict: """ 全部交给 LLM 分析 返回结构完全对齐 app.py 的期望字段 """ # 1. 获取岗位列表(LLM 生成 或 内置兜底) jobs = get_jobs(goal) if not jobs: return { "error": "无法获取岗位列表,请稍后重试。", "matches": [], "analysis": {}, } # 2. 构造 prompt,让 LLM 分析简历和岗位的匹配度 jobs_limited = jobs[:15] jobs_text = "\n".join([ f"{i+1}. {j.get('title', '')} @ {j.get('company', '')} | {j.get('city', '')} | {j.get('salary', '')} | {j.get('description', '')[:100]}" for i, j in enumerate(jobs_limited) ]) prompt = f"""你是专业的求职顾问和简历分析师。 ## 用户简历 {resume[:2000]} ## 求职目标 {goal} ## 岗位列表(共 {len(jobs_limited)} 个) {jobs_text} ## 任务 1. 分析用户简历,提取:skills(技能列表)、projects(项目列表)、experience(实习经历列表)、education(教育背景字符串) 2. 将简历与每个岗位进行匹配,给出匹配分数 match_score(0-100) 3. 对每个岗位,给出: - decision: "立即投递"(匹配度 ≥ 70)、"先优化再投"(50-69)、"冲刺岗位"(40-49)、"暂缓"(<40) - reasoning: 一段自然语言分析(2-3 句话),说明为什么匹配/不匹配 - evidence: 简历中哪些经历匹配这个岗位(数组,每个元素是一段话,格式:"你在XX项目的YY经验,正好匹配该岗位要求的ZZ能力") - gaps: 简历缺少什么,导致不匹配(数组,每个元素是一段话) - suggestions: 如何改进简历以提高匹配度(数组,每个元素是一段话) 4. 给出 what_if 分析(数组,最多 3 个): - expected_gain: 预期提升(如"+15 分") - skill: 需要补充的技能/经历 - action: 具体行动 - time_needed: 需要的时间 - impact: 预期影响 5. 给出 action_plan(对象,包含 today 数组和 this_week 数组): - today: 今天应该做什么(数组) - this_week: 本周应该做什么(数组) ## 输出格式 以 JSON 格式返回,结构如下(注意字段名必须完全一致,不要多余逗号): {{ "analysis": {{ "skills": ["技能1", "技能2"], "projects": ["项目1", "项目2"], "experience": ["经历1", "经历2"], "education": "教育背景" }}, "matches": [ {{ "title": "岗位名称", "company": "公司名称", "city": "城市", "salary": "薪资", "match_score": 85, "decision": "立即投递", "reasoning": "你在腾讯AI Lab的RAG项目使用了FAISS+LangChain,正好匹配该岗位要求的向量检索能力", "evidence": ["你在腾讯AI Lab的RAG项目使用了FAISS+LangChain,正好匹配该岗位要求的向量检索能力", "你会用 PyTorch 微调模型,符合岗位要求的模型优化能力"], "gaps": ["缺少大规模分布式训练经验", "没有生产环境部署经验"], "suggestions": ["在简历里加一段分布式训练的项目描述", "补充模型部署的相关经验"] }} ], "what_if": [ {{ "expected_gain": "+15 分", "skill": "补充 RAG 项目经验", "action": "做一个基于 LangChain + FAISS 的 RAG 项目,部署到 HuggingFace Spaces", "time_needed": "2 周", "impact": "匹配度从 60 → 75,进入稳投区间" }} ], "action_plan": {{ "today": ["优化简历,突出 RAG 项目经验", "投递 2 个稳投岗位"], "this_week": ["完成一个新项目并写到简历里", "投递 5 个冲刺岗位", "准备面试:复习 PyTorch 和 Transformer"] }} }} 只返回 JSON,不要其他解释。确保 JSON 格式正确,字段名完全一致。""" # 3. 调用 LLM 分析 result = call_llm(prompt, max_tokens=4000, timeout=90) # 调试:保存原始返回 import time debug_dir = Path(__file__).resolve().parent.parent / "debug" debug_dir.mkdir(exist_ok=True) with open(debug_dir / f"llm_raw_{int(time.time())}.txt", "w", encoding="utf-8") as f: f.write(result) # 4. 解析结果 try: # 检查是否是错误信息 if result.strip().startswith("{"): err_obj = json.loads(result) if "error" in err_obj: return { "error": err_obj["error"], "matches": [], "analysis": {}, } # 提取 JSON(支持 ```json 代码块 或 裸 JSON) json_match = re.search(r"```json\s*([\s\S]*?)\s*```", result) if not json_match: json_match = re.search(r"\{[\s\S]*\}", result) if json_match: json_str = json_match.group(1) if json_match.lastindex else json_match.group() # 清理可能的 markdown 代码块标记 json_str = re.sub(r"^```json\s*", "", json_str) json_str = re.sub(r"\s*```$", "", json_str) report = json.loads(json_str) else: return { "error": "LLM 返回格式错误,无法解析 JSON", "raw": result[:500], "matches": [], "analysis": {}, } # 确保有必要的字段 if "matches" not in report: report["matches"] = [] if "analysis" not in report: report["analysis"] = {} if "what_if" not in report: report["what_if"] = [] if "action_plan" not in report: report["action_plan"] = {"today": [], "this_week": []} # 把岗位信息合并进 matches for i, m in enumerate(report.get("matches", [])): if i < len(jobs_limited): m["city"] = m.get("city") or jobs_limited[i].get("city", "") m["salary"] = m.get("salary") or jobs_limited[i].get("salary", "") if "company" not in m or not m["company"]: m["company"] = jobs_limited[i].get("company", "") return report except json.JSONDecodeError as e: return { "error": f"解析 LLM 返回失败: {e}", "raw": result[:500], "matches": [], "analysis": {}, } except Exception as e: return { "error": f"处理 LLM 返回失败: {e}", "raw": result[:500], "matches": [], "analysis": {}, } if __name__ == "__main__": test_resume = "张三 | 大模型应用算法工程师\n2022.09-2026.06 XX大学 计算机科学\n实习: LoRA微调Qwen2.5 + RAG知识库(FAISS)\n项目: Offer捕手 9Agent匹配系统 NDCG@10=0.87\n技能: Python PyTorch Transformers LangChain FAISS" test_goal = "大模型算法实习,北京" print("开始测试 run_pipeline...") result = run_pipeline(test_resume, test_goal) print(json.dumps(result, ensure_ascii=False, indent=2))