File size: 11,580 Bytes
3161850
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
"""
jd_parser.py — JD 文本解析器(规则版)
将自由文本 JD 转为标准化 dict,与 jobs.json 结构兼容。
"""
from __future__ import annotations

import re
from typing import Optional

# 方向映射(JD 关键词 → direction 标签)
DIRECTION_KEYWORDS = {
    "大模型应用": "大模型应用算法", "LLM": "大模型应用算法", "Agent": "大模型应用算法",
    "RAG": "大模型应用算法", "大模型算法": "大模型应用算法",
    "推荐算法": "推荐算法", "推荐系统": "推荐算法", "推荐排序": "推荐算法",
    "搜索算法": "大模型应用算法", "检索": "大模型应用算法",
    "自然语言处理": "大模型应用算法", "NLP": "大模型应用算法",
    "计算机视觉": "大模型应用算法", "CV": "大模型应用算法", "YOLO": "大模型应用算法",
    "后端研发": "后端研发", "后端开发": "后端研发", "Go": "后端研发",
    "数据分析": "数据分析", "SQL": "数据分析",
    "产品经理": "产品经理",
    "匹配算法": "大模型应用算法", "人岗匹配": "大模型应用算法",
}

SKILL_PATTERNS = [
    "Python", "PyTorch", "TensorFlow", "Transformer", "BERT", "GPT", "LLM",
    "RAG", "Agent", "Embedding", "LangChain", "FAISS", "Faiss",
    "推荐系统", "召回", "排序", "NDCG", "A/B Test", "ABTest", "A/B 测试",
    "Prompt", "Prompt Engineering", "Prompt 工程",
    "语义检索", "向量检索", "搜索", "重排", "rerank",
    "Java", "Go", "C\\+\\+", "Rust", "Scala",
    "Docker", "Kubernetes", "K8s", "gRPC", "微服务",
    "MySQL", "Redis", "Kafka", "MongoDB", "Elasticsearch",
    "SQL", "Spark", "Hadoop", "数据仓库",
    "模型微调", "LoRA", "QLoRA", "SFT", "RLHF",
    "OpenCV", "YOLO", "目标检测", "图像分类",
    "自然语言处理", "NLP", "命名实体识别", "NER", "文本分类", "Seq2Seq", "摘要生成",
    "Beam Search", "Attention",
    "数据清洗", "特征工程", "模型部署",
]


def parse_jd(jd_text: str) -> dict:
    """解析单个 JD 文本,返回标准化 dict。"""
    text = jd_text.strip()
    lines = text.split("\n")

    title = _extract_title(lines, text)
    company = _extract_company(lines, text)
    city = _extract_city(lines, text)
    stage = _extract_stage(lines, text)
    direction = _infer_direction(title, text)
    skills = _extract_skills(text)
    project_signals = _extract_project_signals(text, skills)
    hard_requirements = _extract_hard_requirements(text)
    bonus_requirements = _extract_bonus_requirements(text)
    risk_flags = _extract_risk_flags(text)
    interview_themes = _infer_interview_themes(skills, text)

    return {
        "id": f"user_{_hash_title(title)}",
        "title": title,
        "company": company,
        "city": city,
        "direction": direction,
        "stage": stage,
        "skills": skills,
        "project_signals": project_signals,
        "jd": text[:500],
        "hard_requirements": hard_requirements,
        "bonus_requirements": bonus_requirements,
        "risk_flags": risk_flags,
        "interview_themes": interview_themes,
        "source": "user_pasted",
        "raw_text": text,
    }


def _hash_title(title: str) -> str:
    h = 0
    for c in title:
        h = (h * 31 + ord(c)) & 0xFFFFFFFF
    return hex(h)[2:]


def _extract_title(lines: list[str], text: str) -> str:
    # 优先从第一行提取
    first = lines[0].strip().lstrip("#").strip()
    # 去掉常见前缀
    for prefix in ["岗位:", "职位:", "岗位名称:", "标题:", "Title:"]:
        if first.startswith(prefix):
            first = first[len(prefix):].strip()
    if first and len(first) < 50:
        if any(kw in first for kw in ["算法", "实习", "工程师", "开发", "产品", "数据"]):
            return first
    for line in lines:
        s = line.strip().lstrip("#").strip()
        for prefix in ["岗位:", "职位:"]:
            if s.startswith(prefix):
                s = s[len(prefix):].strip()
        if any(kw in s for kw in ["实习生", "工程师", "岗位"]) and len(s) < 60:
            return s
    return "算法岗位"


def _extract_company(lines: list[str], text: str) -> str:
    pat = re.compile(r"(公司|部门|事业群|企业)[::]\s*(.+)")
    m = pat.search(text)
    if m:
        return m.group(2).strip()[:30]
    for kw in ["腾讯", "字节", "阿里", "百度", "美团", "快手", "小红书", "B站", "微软", "Google"]:
        if kw in text:
            return kw
    return "未知公司"


def _extract_city(lines: list[str], text: str) -> str:
    cities = ["北京", "上海", "深圳", "广州", "杭州", "成都", "武汉", "南京", "苏州", "西安"]
    for c in cities:
        if c in text:
            return c
    pat = re.compile(r"(城市|地点|工作地点)[::]\s*(.+)")
    m = pat.search(text)
    if m:
        return m.group(2).strip()[:20]
    return "不限"


def _extract_stage(lines: list[str], text: str) -> str:
    if "实习" in text:
        return "实习"
    if "校招" in text or "应届" in text or "毕业" in text:
        return "校招"
    if "社招" in text or "经验" in text or "年以" in text:
        return "社招"
    return "不限"


def _infer_direction(title: str, text: str) -> str:
    combined = title + " " + text
    # 按优先级匹配
    for kw, direction in DIRECTION_KEYWORDS.items():
        if kw.lower() in combined.lower():
            return direction
    # 从 title 推断
    if "算法" in title:
        return "大模型应用算法"
    if "后端" in title or "研发" in title:
        return "后端研发"
    return "通用"


def _extract_skills(text: str) -> list[str]:
    found = []
    text_lower = text.lower()
    for skill in SKILL_PATTERNS:
        if skill.lower() in text_lower:
            found.append(skill)
    # 去重 + 保持顺序
    seen = set()
    result = []
    for s in found:
        if s.lower() not in seen:
            result.append(s)
            seen.add(s.lower())
    return result[:20]


def _extract_project_signals(text: str, skills: list[str]) -> list[str]:
    signals = []
    # 从 skills 中选最具项目信号价值的
    signal_keywords = {
        "RAG", "Agent", "Embedding", "Transformer", "微服务", "推荐系统",
        "召回", "排序", "重排", "检测", "识别", "分类", "搜索",
        "LLM", "Prompt", "模型部署", "模型微调", "LoRA",
    }
    for s in skills:
        if s in signal_keywords:
            signals.append(s)
    return signals[:10]


def _extract_hard_requirements(text: str) -> list[str]:
    reqs = []
    if "Python" in text: reqs.append("熟练使用 Python")
    if "PyTorch" in text or "TensorFlow" in text: reqs.append("熟悉 PyTorch / TensorFlow")
    if "硕士" in text or "研究生" in text: reqs.append("硕士及以上学历")
    if "本科" in text: reqs.append("本科及以上学历")
    if any(kw in text for kw in ["3 年", "3年", "三年", "5 年", "5年"]): reqs.append("相关经验")
    return reqs or ["具备基本编程能力"]


def _extract_bonus_requirements(text: str) -> list[str]:
    bonus = []
    for kw in ["LLM", "大模型", "Agent", "RAG", "论文", "开源", "Kaggle"]:
        if kw in text:
            bonus.append(f"有 {kw} 经验者优先")
    return bonus[:5]


def _extract_risk_flags(text: str) -> list[str]:
    flags = []
    if "5 年" in text or "5年" in text or "资深" in text:
        flags.append("要求较高经验")
    if "985" in text or "211" in text or "一本" in text:
        flags.append("学历门槛")
    return flags


def _infer_interview_themes(skills: list[str], text: str) -> list[str]:
    themes = []
    skill_set = {s.lower() for s in skills}
    if "rag" in skill_set or "检索" in skill_set: themes.append("RAG 检索增强")
    if "agent" in skill_set: themes.append("Agent 工具调用")
    if "transformer" in skill_set: themes.append("Transformer 原理")
    if "推荐系统" in skill_set or "召回" in skill_set: themes.append("推荐系统与召回排序")
    if "embedding" in skill_set: themes.append("Embedding 与向量检索")
    if "模型微调" in skill_set or "lora" in skill_set: themes.append("模型微调技术")
    if not themes: themes = ["算法基础", "项目深挖", "系统设计"]
    return themes[:4]


# ---------------------------------------------------------------------------
# LLM 增强解析(可选,失败自动 fallback 到规则版)
# ---------------------------------------------------------------------------

JD_LLM_SCHEMA = """{
  "title": "岗位名称",
  "company": "公司/部门",
  "city": "城市",
  "direction": "技术方向(大模型应用算法/推荐算法/后端研发/数据分析/产品经理)",
  "stage": "阶段(实习/校招/社招)",
  "skills": ["要求的技能列表"],
  "project_signals": ["项目信号词"],
  "hard_requirements": ["硬性要求"],
  "bonus_requirements": ["加分项"],
  "risk_flags": ["潜在风险提示"],
  "interview_themes": ["面试主题"]
}"""

JD_LLM_PROMPT = """你是一个岗位 JD 解析器。请从以下 JD 文本中提取结构化信息,严格按 JSON Schema 输出。

Schema:
{schema}

要求:
1. 只输出 JSON,不要输出任何其他文字
2. skills 只提取 JD 中明确提到或强烈暗示的技术名称
3. direction 从 大模型应用算法/推荐算法/后端研发/数据分析/产品经理 中选择最匹配的
4. stage 从 实习/校招/社招 中选择
5. 如果某字段无法确定,填写空列表或"不限"

JD 文本:
{jd_text}"""


def parse_jd_with_llm(jd_text: str, llm_client=None) -> dict:
    """LLM 增强 JD 解析,失败自动 fallback 到 parse_jd。"""
    if llm_client is None:
        return parse_jd(jd_text)
    try:
        from src.llm_client import LLMClient
        if not isinstance(llm_client, LLMClient) or not llm_client.available:
            return parse_jd(jd_text)
    except ImportError:
        return parse_jd(jd_text)

    prompt = JD_LLM_PROMPT.format(schema=JD_LLM_SCHEMA, jd_text=jd_text[:3000])
    result = llm_client.chat_json("你是精确的岗位 JD 解析器。", prompt)
    if result is None:
        return parse_jd(jd_text)  # LLM 失败 → fallback

    # 校验 + 补全。LLM 输出只作为结构化候选,类型不可信时退回规则值。
    fallback = parse_jd(jd_text)
    scalar_keys = ["title", "company", "city", "direction", "stage"]
    list_keys = [
        "skills",
        "project_signals",
        "hard_requirements",
        "bonus_requirements",
        "risk_flags",
        "interview_themes",
    ]
    for key in scalar_keys:
        if not isinstance(result.get(key), str) or not result.get(key, "").strip():
            result[key] = fallback.get(key, "不限")
    for key in list_keys:
        if not isinstance(result.get(key), list):
            result[key] = fallback.get(key, [])
        else:
            result[key] = [str(item).strip() for item in result[key] if str(item).strip()]
    if not result["skills"]:
        result["skills"] = fallback.get("skills", [])
    if not result["project_signals"]:
        result["project_signals"] = fallback.get("project_signals", [])
    if not result["interview_themes"]:
        result["interview_themes"] = fallback.get("interview_themes", ["项目深挖"])
    result["id"] = f"llm_{_hash_title(result.get('title', ''))}"
    result["jd"] = jd_text[:500]
    result["source"] = "user_pasted"
    result["raw_text"] = jd_text
    return result