offer-catcher-agent-final / src /graph.py.backup
hungryb's picture
Deploy Offer Catcher final public demo
c974f5d verified
Raw
History Blame Contribute Delete
16.6 kB
"""
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))