CareerNexus 职联:岗位能力图谱构建与动态演化分析平台(挑战杯 XH-202621)

- 全链路:多源JD接入/清洗交叉验证/结构化抽取(星火+离线双通道)/时序知识图谱/新岗位发现/演化分析/人岗匹配/学习路径
- WebUI 五视图:驾驶舱/图谱工作台/岗位演化/匹配(单人+批量)/学习路径
- 设置中心(config.json)/Token鉴权/SQLite状态库/语义向量通道(BGE-M3本地或联网API)
- 71 个 pytest 用例;数据目录仅保留结构(.gitkeep),数据由 scripts/generate_dataset.py 复现
This commit is contained in:
RoyirinRosmontis
2026-08-03 13:15:35 +08:00
commit a10442d78c
78 changed files with 12860 additions and 0 deletions
+329
View File
@@ -0,0 +1,329 @@
"""真实数据集构建脚本:把 datasets/XH-202621/data 下的公开数据集加工成 data/real/。
四个产物:
1. data/real/autoweaks_jd/jd_sample.jsonl + gold_answers.json
—— AutoWeakS(arXiv:2012.14234)真实 JD,去分词 + 技术岗过滤 + 比例抽样,
gold 为"离线抽取器初标",人工抽检修正在 data/real/README.md 留痕;
2. data/real/aistudio_resumes/*.txt + gold.json
—— AIStudio CC0 中文简历标注集抽 50 条渲染为简历文本,保留 PII 以验证脱敏链路;
3. data/real/batch_demo_resumes/*.txt
—— chinese_resume 5001 条结构化简历抽 12 条渲染后经 mask_text 脱敏(原始 PII 不落盘);
4. data/real/esco_reference/
—— ISCO/ESCO 职业分类锚点文件(只拷关键 labels,不整目录拷贝)。
源数据目录只读。random.seed 固定,可复现重建。
"""
from __future__ import annotations
import csv
import json
import random
import re
import shutil
import sys
from pathlib import Path
ROOT = Path(__file__).resolve().parents[1]
sys.path.insert(0, str(ROOT))
from app.extract.base import SkillMatcher, load_lexicon # noqa: E402
from app.match.resume import mask_text # noqa: E402
SRC = ROOT / "datasets" / "XH-202621" / "data"
DST = ROOT / "data" / "real"
SEED = 2026
JD_TOTAL, JD_TENCENT = 200, 160 # 腾讯:京东 ≈ 4:1(与源文件行数比一致)
RESUME_N = 50
DEMO_N = 12
# 技术岗过滤关键词(2019-2020 年代 JD 常见写法)
TECH_RE = re.compile(
r"Python|Java|JavaScript|PHP|前端|后端|软件|算法|数据|开发|云|AI|机器学习|深度学习"
r"|测试|运维|Android|iOS|Go(?:lang)?|Hadoop|Spark|Hive|Kafka|Redis|MySQL|Linux|数据库"
r"|NLP|CV|推荐系统|大数据", re.IGNORECASE)
_PHONE_RE = re.compile(r"^1[3-9]\d{9}$")
_LATIN_RE = re.compile(r"[A-Za-z0-9+#./\-]+")
def detokenize(line: str) -> str:
"""去分词:CJK 之间、CJK 与拉丁 token 之间去空格,拉丁-拉丁保留单空格。"""
toks = line.strip().split()
if not toks:
return ""
out = toks[0]
prev = toks[0]
for t in toks[1:]:
if _LATIN_RE.fullmatch(prev) and _LATIN_RE.fullmatch(t):
out += " " + t
else:
out += t
prev = t
return out
# ---------- 1. AutoWeakS → 真实 JD 测试集 ----------
_TITLE_RULES: list[tuple[re.Pattern, str]] = [
(re.compile(r"算法|机器学习|深度学习|NLP|自然语言|计算机视觉|CV|推荐算法", re.I), "算法工程师"),
(re.compile(r"前端|web开发", re.I), "前端开发工程师"),
(re.compile(r"后端|服务端|后台开发"), "后端开发工程师"),
(re.compile(r"测试开发"), "测试开发工程师"),
(re.compile(r"测试"), "测试工程师"),
(re.compile(r"运维|SRE|devops", re.I), "运维工程师"),
(re.compile(r"android|安卓", re.I), "Android开发工程师"),
(re.compile(r"ios", re.I), "iOS开发工程师"),
(re.compile(r"数据分析|商业分析|bi工程师", re.I), "数据分析师"),
(re.compile(r"数据开发|大数据|数据仓库|数仓|etl", re.I), "数据开发工程师"),
(re.compile(r"产品经理|产品策划"), "产品经理"),
(re.compile(r"运营"), "运营专员"),
(re.compile(r"开发|软件"), "软件开发工程师"),
]
def guess_title(text: str) -> str:
"""真实 JD 无岗位名称行,按关键词启发式归一个岗位名(在 README 如实标注)。"""
head = text[:200]
for pat, title in _TITLE_RULES:
if pat.search(head):
return title
return "技术专员"
def build_autoweaks(matcher: SkillMatcher) -> None:
rng = random.Random(SEED)
out_dir = DST / "autoweaks_jd"
out_dir.mkdir(parents=True, exist_ok=True)
pools: dict[str, list[str]] = {}
for fn, tag in [("Tecent_job_phrase.txt", "腾讯"), ("JD_job_phrase.txt", "京东")]:
lines = (SRC / "autoweaks" / fn).read_text(encoding="utf-8").splitlines()
seen, pool = set(), []
for ln in lines:
text = detokenize(ln)
# 技术岗过滤 + 至少命中 1 个词表技能(保证可评测)+ 去重
if len(text) < 60 or not TECH_RE.search(text):
continue
if not matcher.find_names(text):
continue
if text in seen:
continue
seen.add(text)
pool.append(text)
pools[tag] = pool
picked: list[tuple[str, str]] = [] # (company, text)
for tag, n in [("腾讯", JD_TENCENT), ("京东", JD_TOTAL - JD_TENCENT)]:
pool = pools[tag]
rng.shuffle(pool)
picked.extend((tag, t) for t in pool[:n])
rng.shuffle(picked)
records, gold = [], {}
for i, (company, text) in enumerate(picked, start=1):
jid = f"AW{i:04d}"
title = guess_title(text)
pub = f"2019-{rng.randint(1, 12):02d}-{rng.randint(1, 28):02d}"
records.append({
"jid": jid, "job_title": title, "company": company,
"publish_date": pub, "source": "autoweaks", "url": "",
"text": text,
})
# silver gold:离线匹配器初标(全文技能 + 场景),人工抽检修正见 README
gold[jid] = {
"job_title": title,
"responsibilities": [],
"required_skills": matcher.find_names(text),
"preferred_skills": [],
"scenarios": matcher.find_scenarios(text),
}
with (out_dir / "jd_sample.jsonl").open("w", encoding="utf-8") as f:
for r in records:
f.write(json.dumps(r, ensure_ascii=False) + "\n")
(out_dir / "gold_answers.json").write_text(
json.dumps(gold, ensure_ascii=False, indent=1), encoding="utf-8")
print(f"[autoweaks] 腾讯池 {len(pools['腾讯'])} 京东池 {len(pools['京东'])}"
f" → 抽样 {len(records)} 条")
# ---------- 2. AIStudio → 真实简历集 ----------
def _degree(教育经历: list[dict]) -> str:
order = ["博士", "硕士", "研究生", "本科", "大专", "专科", "MBA"]
degs = [e.get("学位", "") for e in 教育经历]
for d in order:
if any(d in x for x in degs):
return d
return degs[-1] if degs else ""
def render_aistudio_resume(rec: dict) -> str:
"""把结构化字段渲染为我们的简历文本格式(保留 PII,供脱敏链路验证)。"""
lines = [f"姓名:{rec.get('姓名', '')}"]
if rec.get("电话"):
lines.append(f"电话:{rec['电话']}")
deg = _degree(rec.get("教育经历", []))
if deg:
lines.append(f"学历:{deg}")
if rec.get("籍贯"):
lines.append(f"籍贯:{rec['籍贯']}")
lines.append("\n教育经历")
for e in rec.get("教育经历", []):
lines.append(f"- {e.get('毕业时间', '')} {e.get('毕业院校', '')} {e.get('学位', '')}".strip())
if rec.get("工作经历"):
lines.append("\n工作经历")
for w in rec["工作经历"]:
head = " ".join(x for x in [w.get("工作时间", ""), w.get("工作单位", ""),
w.get("职务", "")] if x)
lines.append(f"- {head}".strip("- "))
duty = (w.get("工作内容") or "").strip()
if duty:
lines.append(f" {duty}")
if rec.get("项目经历"):
lines.append("\n项目经历")
for p in rec["项目经历"]:
head = " ".join(x for x in [p.get("项目名称", ""), p.get("项目时间", "")] if x)
lines.append(f"- {head}".strip("- "))
resp = (p.get("项目责任") or "").strip()
if resp:
lines.append(f" {resp}")
return "\n".join(lines) + "\n"
def build_aistudio(matcher: SkillMatcher) -> None:
rng = random.Random(SEED + 1)
out_dir = DST / "aistudio_resumes"
out_dir.mkdir(parents=True, exist_ok=True)
data = json.loads((SRC / "aistudio" / "train.json").read_text(encoding="utf-8"))
# 优先工作经历/项目经历含技术内容、且电话合法的条目
cands = []
for key, rec in data.items():
blob = json.dumps([rec.get("工作经历"), rec.get("项目经历")], ensure_ascii=False)
if not TECH_RE.search(blob):
continue
if not _PHONE_RE.match(str(rec.get("电话", ""))):
continue
if not rec.get("姓名"):
continue
cands.append((key, rec))
rng.shuffle(cands)
picked = cands[:RESUME_N]
gold = {}
for i, (key, rec) in enumerate(picked, start=1):
fname = f"aistudio_{i:03d}.txt"
text = render_aistudio_resume(rec)
(out_dir / fname).write_text(text, encoding="utf-8")
gold[fname] = {
"name": rec["姓名"],
"phone": rec["电话"],
"skills": matcher.find_names(text), # 初标,人工抽检复核见 README
"source_id": key,
}
(out_dir / "gold.json").write_text(
json.dumps(gold, ensure_ascii=False, indent=1), encoding="utf-8")
print(f"[aistudio] 候选 {len(cands)} → 抽样 {len(picked)} 份")
# ---------- 3. chinese_resume → 批量演示素材(脱敏后落盘) ----------
def render_chinese_resume(row: dict) -> str:
"""按"姓名/性别/手机/邮箱/求职意向/教育/技能清单/自我评价"渲染(未脱敏,仅内存)。"""
lines = [
f"姓名:{row['姓名']}",
f"性别:{row['性别']}",
f"手机:{row['电话']}",
f"邮箱:{row['邮箱']}",
f"求职意向:{row['意向岗位']}",
"",
"教育背景",
f"- 学历:{row['学历层次']}({row['院校类别']},{row['专业类别']})",
f"- 英语水平:{row['英语水平']}",
"",
"技能清单",
]
for col in ["编程语言", "前端技术", "后端技术", "数据库", "云计算/运维",
"数据与算法", "移动开发", "测试工具"]:
val = row.get(col, "")
if val and val != "NULL":
prof = row.get(col + "熟练度", "")
items = "、".join(
f"{s}({p})" if p else s
for s, p in zip(val.split(","), (prof.split(",") + [""] * 8)))
lines.append(f"- {col}:{items}")
lines += [
"",
"自我评价",
f"具有{row['大型企业工作经验']}大型企业、{row['中型企业工作经验']}中型企业工作经验,"
f"参与过{row['大规模项目']}个大规模项目,求职方向为{row['意向岗位']}。",
]
return "\n".join(lines) + "\n"
def build_demo_resumes() -> None:
rng = random.Random(SEED + 2)
out_dir = DST / "batch_demo_resumes"
out_dir.mkdir(parents=True, exist_ok=True)
with (SRC / "chinese_resume" / "Chinese_resume_data.csv").open(encoding="utf-8") as f:
rows = [r for r in csv.DictReader(f) if r.get("筛选结果") == "通过"]
by_intent: dict[str, list[dict]] = {}
for r in rows:
by_intent.setdefault(r["意向岗位"], []).append(r)
# 覆盖不同意向岗位:每类取 1 条,剩余名额补足到 DEMO_N
intents = sorted(by_intent, key=lambda k: -len(by_intent[k]))
picked: list[dict] = []
for intent in intents:
if len(picked) >= DEMO_N:
break
picked.append(rng.choice(by_intent[intent]))
rest = [r for intent in intents for r in by_intent[intent] if r not in picked]
rng.shuffle(rest)
picked.extend(rest[: DEMO_N - len(picked)])
for i, row in enumerate(picked, start=1):
raw = render_chinese_resume(row)
masked = mask_text(raw) # 脱敏后才落盘,原始 PII 不写入 data/
slug = re.sub(r"\W+", "", row["意向岗位"])
(out_dir / f"demo_{i:02d}_{slug}.txt").write_text(masked, encoding="utf-8")
print(f"[chinese_resume] 意向岗位 {len(intents)} 类 → 脱敏渲染 {len(picked)} 份")
# ---------- 4. ISCO/ESCO 锚点 ----------
ESCO_FILES = [
"labels_isco_codes.txt",
"labels_isco_labels.txt",
"labels_esco_code.txt",
"labels_esco_occupation.txt",
]
def build_esco_reference() -> None:
out_dir = DST / "esco_reference"
out_dir.mkdir(parents=True, exist_ok=True)
for name in ESCO_FILES:
shutil.copyfile(SRC / "isco_esco_taxonomy" / name, out_dir / name)
print(f"[esco] 拷贝锚点文件 {len(ESCO_FILES)} 个")
def main() -> None:
if not SRC.exists():
raise SystemExit(f"源数据目录不存在: {SRC}")
matcher = SkillMatcher(load_lexicon())
build_autoweaks(matcher)
build_aistudio(matcher)
build_demo_resumes()
build_esco_reference()
print(f"全部产物写入 {DST}")
if __name__ == "__main__":
main()
+251
View File
@@ -0,0 +1,251 @@
"""指标复现脚本:真实“从文本抽取 → 与 gold 比对”流程,不硬编码任何答案。
三项硬指标(合成测试集):
1. JD 解析准确率 —— 对 data/testset/jd_testset.jsonl 重新抽取,与 gold_answers.json
算字段级 P/R/F1(必备技能/加分技能/职责/场景 micro-F1 + 岗位名称准确率);
2. 简历提取准确率 —— 对 data/resumes/*.txt 解析技能,与 gold.json 算 F1;
3. 人岗匹配准确率 —— 对 data/testset/match_gold.json 标注对跑匹配引擎,
判定“命中预期匹配/不匹配”的比例。
两个真实数据维度(data/real/,与合成集分开报告、不混算):
4. 真实 JD(AutoWeakS 初标+抽检 silver gold)—— 对 jd_sample.jsonl 重新抽取,
与 gold_answers.json 算技能/场景字段级 F1;
5. 真实简历(AIStudio CC0)—— 对 50 份简历解析,姓名/电话命中率 + 技能 F1。
输出 reports/metrics.json 与控制台报告。
"""
from __future__ import annotations
import json
import sys
from pathlib import Path
ROOT = Path(__file__).resolve().parents[1]
sys.path.insert(0, str(ROOT))
from app.extract.offline import OfflineExtractor # noqa: E402
from app.extract.base import SkillMatcher, load_lexicon # noqa: E402
from app.graph.tkg import TemporalKG # noqa: E402
from app.ingest.loader import load_jd_file # noqa: E402
from app.match.engine import MatchEngine # noqa: E402
from app.match.resume import parse_resume # noqa: E402
from app.models import JDRecord # noqa: E402
def prf(tp: int, pred: int, gold: int) -> tuple[float, float, float]:
p = tp / pred if pred else 0.0
r = tp / gold if gold else 0.0
f1 = 2 * p * r / (p + r) if p + r else 0.0
return round(p, 4), round(r, 4), round(f1, 4)
def eval_jd_extraction() -> dict:
"""JD 解析:从文本重新抽取,与标准答案做字段级比对。"""
records, errors = load_jd_file(ROOT / "data" / "testset" / "jd_testset.jsonl")
gold = json.loads((ROOT / "data" / "testset" / "gold_answers.json").read_text(encoding="utf-8"))
extractor = OfflineExtractor(load_lexicon())
fields = ["required_skills", "preferred_skills", "responsibilities", "scenarios"]
tp = {f: 0 for f in fields}
pred = {f: 0 for f in fields}
gold_n = {f: 0 for f in fields}
title_hit = 0
for rec in records:
g = gold[rec.jid]
ext = extractor.extract(rec)
title_hit += int(ext.job_title == g["job_title"])
for f in fields:
got, want = set(getattr(ext, f)), set(g[f])
tp[f] += len(got & want)
pred[f] += len(got)
gold_n[f] += len(want)
detail = {}
f1s = []
for f in fields:
p, r, f1 = prf(tp[f], pred[f], gold_n[f])
detail[f] = {"precision": p, "recall": r, "f1": f1}
f1s.append(f1)
title_acc = round(title_hit / len(records), 4)
# 综合准确率:技能字段权重更高(技能是岗位图谱的核心)
accuracy = round(0.6 * (f1s[0] + f1s[1]) / 2 + 0.2 * f1s[2] + 0.1 * f1s[3] + 0.1 * title_acc, 4)
return {"accuracy": accuracy, "title_accuracy": title_acc, "fields": detail,
"samples": len(records), "bad_lines": len(errors)}
def eval_resume() -> dict:
"""简历技能提取准确率(逐份 F1 取平均)。"""
gold = json.loads((ROOT / "data" / "resumes" / "gold.json").read_text(encoding="utf-8"))
matcher = SkillMatcher(load_lexicon())
f1s, detail = [], {}
for name, g in gold.items():
text = (ROOT / "data" / "resumes" / name).read_text(encoding="utf-8")
profile = parse_resume(text, matcher)
got, want = set(profile.skills), set(g["skills"])
p, r, f1 = prf(len(got & want), len(got), len(want))
f1s.append(f1)
detail[name] = {"precision": p, "recall": r, "f1": f1,
"extra": sorted(got - want), "missing": sorted(want - got)}
return {"accuracy": round(sum(f1s) / len(f1s), 4), "detail": detail}
def eval_match() -> dict:
"""人岗匹配准确率:命中预期匹配/不匹配。"""
graph_path = ROOT / "data" / "graph.json"
if not graph_path.exists():
from app.services.pipeline import run_pipeline
run_pipeline()
kg = TemporalKG.load(graph_path)
by_title = {j.title: j for j in kg.jobs.values()}
pairs = json.loads((ROOT / "data" / "testset" / "match_gold.json").read_text(encoding="utf-8"))
engine = MatchEngine(load_lexicon())
from app.models import ResumeProfile
hit, detail = 0, []
for pair in pairs:
job = by_title.get(pair["job_title"])
if job is None:
continue
profile = ResumeProfile(skills=pair["skills"], years=pair["years"],
scenarios=pair["scenarios"])
result = engine.match(profile, job)
ok = result.is_match == pair["expected_match"]
hit += int(ok)
detail.append({"job": pair["job_title"], "expected": pair["expected_match"],
"score": result.total_score, "ok": ok})
return {"accuracy": round(hit / len(pairs), 4), "pairs": len(pairs), "detail": detail}
def eval_real_jd() -> dict:
"""真实 JD(AutoWeakS):从文本重新抽取,与 silver gold(初标+抽检)字段级比对。
真实 JD 是自由文本、无编号版面,gold 的 responsibilities/preferred_skills 为空,
口径 = 全文词表技能(required_skills)+ 应用场景 + 启发式岗位名,见 data/real/README.md。
"""
records, errors = load_jd_file(ROOT / "data" / "real" / "autoweaks_jd" / "jd_sample.jsonl")
gold = json.loads(
(ROOT / "data" / "real" / "autoweaks_jd" / "gold_answers.json").read_text(encoding="utf-8"))
extractor = OfflineExtractor(load_lexicon())
fields = ["required_skills", "preferred_skills", "scenarios"]
tp = {f: 0 for f in fields}
pred = {f: 0 for f in fields}
gold_n = {f: 0 for f in fields}
title_hit = 0
for rec in records:
g = gold[rec.jid]
ext = extractor.extract(rec)
title_hit += int(ext.job_title == g["job_title"])
for f in fields:
got, want = set(getattr(ext, f)), set(g[f])
tp[f] += len(got & want)
pred[f] += len(got)
gold_n[f] += len(want)
detail, active_f1 = {}, []
for f in fields:
if not gold_n[f] and not pred[f]:
detail[f] = None # 该字段在真实语料中无标注口径,不参与
continue
p, r, f1 = prf(tp[f], pred[f], gold_n[f])
detail[f] = {"precision": p, "recall": r, "f1": f1}
active_f1.append((f, f1))
weights = {"required_skills": 0.7, "preferred_skills": 0.2, "scenarios": 0.1}
wsum = sum(weights[f] for f, _ in active_f1) or 1.0
accuracy = round(sum(weights[f] * f1 for f, f1 in active_f1) / wsum, 4)
return {"accuracy": accuracy,
"title_accuracy": round(title_hit / len(records), 4),
"fields": detail, "samples": len(records), "bad_lines": len(errors)}
def eval_real_resume() -> dict:
"""真实简历(AIStudio CC0):姓名/电话命中率(脱敏形态比对)+ 技能 F1。"""
gold = json.loads(
(ROOT / "data" / "real" / "aistudio_resumes" / "gold.json").read_text(encoding="utf-8"))
matcher = SkillMatcher(load_lexicon())
f1s, name_hit, phone_hit, phone_total = [], 0, 0, 0
empty_skills = 0
for name, g in gold.items():
text = (ROOT / "data" / "real" / "aistudio_resumes" / name).read_text(encoding="utf-8")
profile = parse_resume(text, matcher)
# gold 存明文,解析结果是脱敏形态(姓氏+* / 138****5678),按脱敏形态比对
want_name = g["name"][0] + "*" * (len(g["name"]) - 1)
name_hit += int(profile.name == want_name)
if g.get("phone"):
phone_total += 1
want_phone = g["phone"][:3] + "****" + g["phone"][-2:]
phone_hit += int(profile.phone == want_phone)
got, want = set(profile.skills), set(g["skills"])
if not got and not want:
empty_skills += 1
f1s.append(1.0) # 双方均无词表技能 = 完全一致(真实简历大量非技术内容)
continue
_, _, f1 = prf(len(got & want), len(got), len(want))
f1s.append(f1)
n = len(gold)
return {"accuracy": round(sum(f1s) / n, 4),
"name_accuracy": round(name_hit / n, 4),
"phone_accuracy": round(phone_hit / phone_total, 4) if phone_total else 0.0,
"samples": n, "phone_samples": phone_total,
"empty_skill_resumes": empty_skills}
def main() -> None:
jd = eval_jd_extraction()
resume = eval_resume()
match = eval_match()
real_jd = eval_real_jd()
real_resume = eval_real_resume()
metrics = {
"jd_extraction_accuracy": jd["accuracy"],
"resume_extraction_accuracy": resume["accuracy"],
"match_accuracy": match["accuracy"],
"jd_detail": jd,
"resume_detail": resume["detail"],
"match_detail": match["detail"],
"real_jd": real_jd,
"real_resume": real_resume,
}
out = ROOT / "reports"
out.mkdir(exist_ok=True)
(out / "metrics.json").write_text(
json.dumps(metrics, ensure_ascii=False, indent=1), encoding="utf-8")
print("=" * 56)
print("CareerNexus 指标复现报告")
print("=" * 56)
print(f"JD 解析准确率 : {jd['accuracy']:.2%} (样本 {jd['samples']} 条)")
for f, d in jd["fields"].items():
print(f" - {f:<18} P={d['precision']:.2%} R={d['recall']:.2%} F1={d['f1']:.2%}")
print(f" - 岗位名称准确率 {jd['title_accuracy']:.2%}")
print(f"简历提取准确率 : {resume['accuracy']:.2%}")
for name, d in resume["detail"].items():
print(f" - {name:<22} F1={d['f1']:.2%}")
print(f"人岗匹配准确率 : {match['accuracy']:.2%} (标注对 {match['pairs']} 对)")
print("-" * 56)
print("真实数据维度(data/real/,与合成集分开报告)")
print(f"真实 JD(AutoWeakS 初标+抽检): 综合 F1 {real_jd['accuracy']:.2%}"
f" (样本 {real_jd['samples']} 条)")
for f, d in real_jd["fields"].items():
if d is None:
print(f" - {f:<18} (真实语料无标注口径,不参与)")
else:
print(f" - {f:<18} P={d['precision']:.2%} R={d['recall']:.2%} F1={d['f1']:.2%}")
print(f" - 岗位名称准确率 {real_jd['title_accuracy']:.2%} (启发式标注)")
print(f"真实简历(AIStudio CC0) : 技能 F1 {real_resume['accuracy']:.2%}"
f" 姓名命中 {real_resume['name_accuracy']:.2%}"
f" 电话命中 {real_resume['phone_accuracy']:.2%} ({real_resume['samples']} 份)")
print(f"\n指标已写入 {out / 'metrics.json'}")
target = 0.90
for name, v in [("JD解析", jd["accuracy"]), ("简历提取", resume["accuracy"]),
("人岗匹配", match["accuracy"])]:
status = "达标" if v >= target else "未达标"
print(f"[{status}] {name} {v:.2%} (目标 ≥{target:.0%})")
real_targets = [("真实JD技能F1", real_jd["fields"]["required_skills"]["f1"], 0.80),
("真实姓名命中", real_resume["name_accuracy"], 1.00)]
for name, v, t in real_targets:
status = "达标" if v >= t else "未达标"
print(f"[{status}] {name} {v:.2%} (目标 ≥{t:.0%})")
if __name__ == "__main__":
main()
+932
View File
@@ -0,0 +1,932 @@
"""自备数据集生成器(random.seed(2026) 固定可复现)。
产出:
- data/skills_lexicon.json 技能词表(>=160 技能点:名称/类别/别名/级别/前置技能)
+ 行业词表(7 个行业)与岗位-行业归属
- data/jd_raw/batch_YYYY.jsonl 原始 JD(2019-2026,演化信号烘焙,约 8% 近重复、噪声植入)
每条 JD 含 industry 字段(招聘平台标注的所属行业),文本经多模板族渲染:
职责句式 >=15 套(含时代门槛,2019 年不会出现"大模型"),可选公司介绍/
项目背景/团队福利段落,长度分布 150-500 字。
- data/jd_raw/_meta.json 植入的抄袭簇与噪声清单(供测试核验)
- data/testset/jd_testset.jsonl + gold_answers.json >=120 条带标准答案的 JD
- data/testset/match_gold.json 标注好的人岗匹配对(>=30 对)
- data/resumes/*.txt + gold.json 7 份简历样例(完整/部分/错位)与技能标准答案
- data/reports/*.txt 3 份行业趋势报告样例(与 JD 演化信号相互印证)
- data/review/audit.jsonl 预置审核/编辑留痕种子记录
公平性说明:gold_answers 在生成 JD 文本之前先确定结构(标题/职责/必备/加分/场景),
再据此渲染文本;评测脚本从文本重新抽取再与 gold 比对,不是反向拟合。
"""
from __future__ import annotations
import json
import random
import sys
from datetime import date, timedelta
from pathlib import Path
ROOT = Path(__file__).resolve().parents[1]
sys.path.insert(0, str(ROOT))
from app.extract.offline import DUTY_HEADERS, PREF_HEADERS, REQ_HEADERS, TAIL_HEADERS # noqa: E402
SEED = 2026
rng = random.Random(SEED)
# ---------------------------------------------------------------- 技能词表
# (名称, 类别, [别名], 级别, [前置技能])
SKILLS = [
# 编程语言
("Java", "编程语言", [], "L1", []), ("Python", "编程语言", [], "L1", []),
("Go", "编程语言", ["Golang"], "L1", []), ("C++", "编程语言", [], "L1", ["C"]),
("C", "编程语言", ["C语言"], "L1", []), ("JavaScript", "编程语言", ["JS"], "L1", []),
("TypeScript", "编程语言", ["TS"], "L2", ["JavaScript"]), ("PHP", "编程语言", [], "L1", []),
("Scala", "编程语言", [], "L2", ["Java"]), ("Kotlin", "编程语言", [], "L2", ["Java"]),
("Rust", "编程语言", [], "L3", []), ("SQL", "编程语言", [], "L1", []),
("Shell", "编程语言", ["Bash"], "L1", ["Linux"]), ("R", "编程语言", [], "L2", []),
("MATLAB", "编程语言", [], "L2", []),
# 后端框架
("Spring", "后端框架", [], "L2", ["Java"]), ("Spring Boot", "后端框架", ["SpringBoot"], "L2", ["Spring"]),
("Spring Cloud", "后端框架", ["SpringCloud"], "L3", ["Spring Boot", "微服务"]),
("MyBatis", "后端框架", ["Mybatis"], "L2", ["Java", "SQL"]),
("Django", "后端框架", [], "L2", ["Python"]), ("Flask", "后端框架", [], "L2", ["Python"]),
("FastAPI", "后端框架", [], "L2", ["Python"]), ("Gin", "后端框架", [], "L2", ["Go"]),
("Express", "后端框架", [], "L2", ["Node.js"]), ("NestJS", "后端框架", [], "L3", ["TypeScript"]),
("Netty", "后端框架", [], "L3", ["Java"]), ("Dubbo", "后端框架", [], "L3", ["Java", "微服务"]),
("gRPC", "后端框架", [], "L3", ["微服务"]),
("Struts", "后端框架", [], "L1", ["Java"]), ("Servlet", "后端框架", [], "L1", ["Java"]),
("JSP", "后端框架", [], "L1", ["Java"]), ("SSM", "后端框架", [], "L2", ["Spring", "MyBatis"]),
# 前端
("Vue", "前端", ["Vue.js"], "L2", ["JavaScript"]), ("React", "前端", ["React.js"], "L2", ["JavaScript"]),
("Angular", "前端", ["AngularJS"], "L2", ["TypeScript"]), ("jQuery", "前端", [], "L1", ["JavaScript"]),
("Webpack", "前端", [], "L2", ["JavaScript"]), ("Vite", "前端", [], "L2", ["JavaScript"]),
("Node.js", "前端", ["Node"], "L2", ["JavaScript"]), ("小程序开发", "前端", ["微信小程序"], "L2", ["JavaScript"]),
("Element UI", "前端", [], "L1", ["Vue"]), ("Ant Design", "前端", [], "L1", ["React"]),
("HTML", "前端", ["HTML5"], "L1", []), ("CSS", "前端", ["CSS3"], "L1", []),
("Tailwind CSS", "前端", [], "L2", ["CSS"]), ("低代码平台", "前端", [], "L2", []),
("微前端", "前端", [], "L3", ["JavaScript"]),
# 数据库与中间件
("MySQL", "数据库", [], "L1", ["SQL"]), ("PostgreSQL", "数据库", [], "L2", ["SQL"]),
("Oracle", "数据库", [], "L2", ["SQL"]), ("Redis", "数据库", [], "L2", []),
("MongoDB", "数据库", [], "L2", []), ("Elasticsearch", "数据库", ["ElasticSearch"], "L2", ["Linux"]),
("ClickHouse", "数据库", [], "L3", ["SQL"]), ("HBase", "数据库", [], "L3", ["Hadoop"]),
("SQLite", "数据库", [], "L1", ["SQL"]), ("TiDB", "数据库", [], "L3", ["MySQL"]),
("Kafka", "中间件", [], "L2", ["Linux"]), ("RabbitMQ", "中间件", [], "L2", []),
("RocketMQ", "中间件", [], "L2", ["Java"]), ("Pulsar", "中间件", [], "L3", ["Kafka"]),
("ZooKeeper", "中间件", [], "L2", ["Linux"]), ("Tomcat", "中间件", [], "L1", ["Java"]),
("WebLogic", "中间件", [], "L2", ["Java"]), ("Nginx", "中间件", [], "L1", ["Linux"]),
# 大数据
("Hadoop", "大数据", [], "L2", ["Linux", "Java"]), ("Spark", "大数据", [], "L2", ["Hadoop"]),
("Flink", "大数据", [], "L3", ["Kafka"]), ("Hive", "大数据", [], "L2", ["Hadoop", "SQL"]),
("HDFS", "大数据", [], "L2", ["Hadoop"]), ("Airflow", "大数据", [], "L2", ["Python"]),
("DataX", "大数据", [], "L2", []), ("Sqoop", "大数据", [], "L2", ["Hadoop"]),
("Presto", "大数据", [], "L3", ["SQL"]), ("Doris", "大数据", [], "L3", ["SQL"]),
("数据挖掘", "大数据", [], "L2", ["Python", "SQL"]),
# AI 与算法
("机器学习", "AI算法", ["机器学习算法"], "L2", ["Python"]),
("深度学习", "AI算法", [], "L3", ["机器学习"]),
("PyTorch", "AI算法", [], "L2", ["Python", "深度学习"]),
("TensorFlow", "AI算法", [], "L2", ["Python", "深度学习"]),
("Keras", "AI算法", [], "L2", ["TensorFlow"]),
("Scikit-learn", "AI算法", ["sklearn"], "L2", ["机器学习"]),
("XGBoost", "AI算法", [], "L2", ["机器学习"]), ("LightGBM", "AI算法", [], "L2", ["机器学习"]),
("NLP", "AI算法", ["自然语言处理"], "L3", ["机器学习"]),
("计算机视觉", "AI算法", ["CV"], "L3", ["深度学习"]),
("OpenCV", "AI算法", [], "L2", ["Python"]), ("OCR", "AI算法", ["文字识别"], "L2", ["计算机视觉"]),
("语音识别", "AI算法", [], "L3", ["深度学习"]), ("推荐算法", "AI算法", ["推荐系统"], "L3", ["机器学习"]),
("强化学习", "AI算法", [], "L3", ["深度学习"]), ("知识图谱", "AI算法", [], "L3", ["NLP"]),
("大模型应用开发", "AI算法", ["LLM应用开发", "大模型应用", "大语言模型应用"], "L3", ["Python", "NLP"]),
("RAG", "AI算法", ["检索增强生成"], "L3", ["大模型应用开发", "向量数据库"]),
("提示词工程", "AI算法", ["Prompt工程"], "L2", ["大模型应用开发"]),
("向量数据库", "AI算法", ["向量库"], "L2", ["数据库基础"]),
("LangChain", "AI算法", [], "L2", ["大模型应用开发"]),
("模型微调", "AI算法", ["Fine-tuning", "SFT"], "L3", ["深度学习", "PyTorch"]),
("AI辅助编程", "AI算法", ["Copilot", "AI编程助手"], "L1", []),
# 云原生与运维
("Linux", "运维", [], "L1", []), ("Docker", "云原生", [], "L2", ["Linux"]),
("Kubernetes", "云原生", ["K8s", "k8s"], "L3", ["Docker"]),
("微服务", "云原生", ["微服务架构"], "L2", []),
("分布式系统", "云原生", ["分布式"], "L3", []), ("高并发", "云原生", [], "L3", ["分布式系统"]),
("云计算", "云原生", ["云平台"], "L2", []), ("Service Mesh", "云原生", ["服务网格"], "L3", ["Kubernetes"]),
("CI/CD", "运维", ["持续集成"], "L2", ["Git"]), ("Git", "运维", [], "L1", []),
("Svn", "运维", ["SVN"], "L1", []), ("Jenkins", "运维", [], "L2", ["CI/CD"]),
("Terraform", "运维", [], "L3", ["云计算"]), ("Ansible", "运维", [], "L2", ["Linux"]),
("Prometheus", "运维", [], "L2", ["Kubernetes"]), ("Grafana", "运维", [], "L2", ["Prometheus"]),
("ELK", "运维", [], "L2", ["Elasticsearch"]), ("DevOps", "运维", [], "L2", ["CI/CD"]),
("AIOps", "运维", ["智能运维"], "L3", ["Prometheus", "机器学习"]),
("AWS", "云原生", ["亚马逊云"], "L2", ["云计算"]), ("阿里云", "云原生", [], "L2", ["云计算"]),
("腾讯云", "云原生", [], "L2", ["云计算"]),
# 测试
("自动化测试", "测试", [], "L2", ["Python"]), ("Selenium", "测试", [], "L2", ["自动化测试"]),
("JUnit", "测试", [], "L1", ["Java"]), ("Pytest", "测试", [], "L1", ["Python"]),
("性能测试", "测试", [], "L2", []), ("接口测试", "测试", ["API测试"], "L2", []),
("JMeter", "测试", [], "L2", ["性能测试"]), ("Postman", "测试", [], "L1", ["接口测试"]),
("单元测试", "测试", [], "L1", []), ("测试用例设计", "测试", [], "L2", []),
# 嵌入式与物联网
("嵌入式开发", "嵌入式", ["嵌入式"], "L2", ["C"]), ("单片机", "嵌入式", [], "L1", ["C"]),
("STM32", "嵌入式", [], "L2", ["单片机"]), ("RTOS", "嵌入式", ["实时操作系统"], "L2", ["C"]),
("FreeRTOS", "嵌入式", [], "L2", ["RTOS"]), ("ARM", "嵌入式", [], "L2", []),
("驱动开发", "嵌入式", ["驱动程序开发"], "L3", ["C", "Linux"]),
("Verilog", "嵌入式", [], "L3", []), ("FPGA", "嵌入式", [], "L3", ["Verilog"]),
("PCB设计", "嵌入式", ["PCB"], "L2", []),
("MQTT", "物联网", [], "L2", []), ("传感器", "物联网", ["传感器技术"], "L1", []),
("物联网平台", "物联网", ["IoT平台"], "L2", []), ("边缘计算", "物联网", [], "L3", ["云计算"]),
("LoRa", "物联网", [], "L2", []), ("Zigbee", "物联网", [], "L2", []),
# 产品与分析
("产品设计", "产品", [], "L2", []), ("需求分析", "产品", [], "L2", []),
("项目管理", "产品", [], "L2", []), ("Axure", "产品", [], "L1", []),
("竞品分析", "产品", [], "L1", []), ("用户增长", "产品", [], "L2", []),
("AI产品", "产品", ["AI产品经理"], "L3", ["产品设计"]),
("数据分析", "数据分析", [], "L2", ["SQL"]), ("Excel", "数据分析", [], "L1", []),
("Tableau", "数据分析", [], "L2", ["数据可视化"]), ("Power BI", "数据分析", ["PowerBI"], "L2", ["数据可视化"]),
("SPSS", "数据分析", [], "L2", []), ("数据可视化", "数据分析", [], "L2", ["数据分析"]),
("A/B测试", "数据分析", ["AB测试"], "L2", ["数据分析"]),
]
SCENARIOS = ["电商", "金融", "物流", "社交", "教育", "医疗", "政务", "游戏",
"出行", "智能制造", "短视频", "广告", "供应链", "零售", "安防"]
# 行业词表(岗位归属 1-2 个行业,图谱中构建 Industry 节点)
INDUSTRIES = ["互联网/软件", "人工智能", "金融科技", "智能制造",
"物联网", "智慧城市", "智慧医疗"]
# ---------------------------------------------------------------- 岗位原型
def arc(title, category, years, salary, scenarios, industries,
duty_tmpls, duty_objs, timeline, volumes):
return dict(title=title, category=category, years=years, salary=salary,
scenarios=scenarios, industries=industries,
duty_tmpls=duty_tmpls, duty_objs=duty_objs,
timeline=timeline, volumes=volumes)
BACKEND_DUTIES = ["负责{o}的设计与开发", "参与{o}的性能优化与重构", "配合产品团队完成{o}的需求评审",
"编写{o}的技术文档与接口文档", "承担{o}的线上问题排查与修复", "推动{o}的代码评审与质量改进"]
BACKEND_OBJS = ["后端服务", "核心业务系统", "交易网关", "订单中心", "用户中心", "缓存体系", "消息管道", "权限平台"]
# 通用职责句式库:(模板, 最早出现年份)。时代词(大模型/AI/低代码)只出现在
# 对应年份之后,保证 2019 年的 JD 不会出现"大模型"等未来措辞。
GENERIC_DUTIES = [
("参与{o}从0到1的搭建与持续迭代", 2019),
("负责{o}高并发场景的优化与稳定性保障", 2020),
("与产品、测试团队协作,推动{o}高质量落地", 2019),
("沉淀{o}的通用组件与最佳实践", 2020),
("主导{o}的技术选型与难点攻坚", 2021),
("建设{o}的监控与告警体系", 2021),
("负责{o}的架构演进与技术债治理", 2022),
("推动{o}的自动化与工程效率提升", 2022),
("探索AI工具在{o}中的应用与提效", 2024),
("引入大模型能力改造{o}的现有流程", 2025),
]
ARCHETYPES = [
arc("Java开发工程师", "后端开发", 3, "15K-30K·14薪", ["电商", "金融", "物流"],
["互联网/软件", "金融科技"],
BACKEND_DUTIES, BACKEND_OBJS,
[(2019, 2021, ["Java", "MySQL", "Spring", "MyBatis", "Servlet"], ["Struts", "JSP", "Linux"]),
(2022, 2023, ["Java", "Spring Boot", "Spring Cloud", "MySQL", "Redis", "微服务"], ["Docker", "Kafka", "Kubernetes"]),
(2024, 2026, ["Java", "Spring Boot", "Spring Cloud", "Redis", "MySQL", "分布式系统"], ["AI辅助编程", "大模型应用开发", "Kubernetes"])],
{2019: 8, 2020: 8, 2021: 8, 2022: 10, 2023: 12, 2024: 24, 2025: 24, 2026: 24}),
arc("Python开发工程师", "后端开发", 3, "14K-28K", ["教育", "金融", "电商"],
["互联网/软件"],
BACKEND_DUTIES, BACKEND_OBJS,
[(2019, 2021, ["Python", "MySQL", "Django", "Linux", "Git"], ["Flask", "Redis", "Nginx"]),
(2022, 2023, ["Python", "FastAPI", "MySQL", "Redis", "Linux"], ["Docker", "Kafka", "Git"]),
(2024, 2026, ["Python", "FastAPI", "Redis", "MySQL", "分布式系统"], ["Docker", "大模型应用开发", "AI辅助编程"])],
{2019: 6, 2020: 6, 2021: 6, 2022: 8, 2023: 12, 2024: 24, 2025: 24, 2026: 24}),
# —— 前端:jQuery/AngularJS 时代 → Vue/React + TypeScript → 工程化/Vite → 低代码 + AI 辅助 ——
arc("前端工程师", "前端", 3, "13K-26K", ["电商", "社交", "短视频"],
["互联网/软件"],
["负责{o}的页面开发与交互实现", "参与{o}的前端性能优化", "与后端联调{o}的数据接口",
"维护{o}的组件库与工程化体系", "承担{o}的兼容性适配工作"],
["移动端页面", "管理后台", "可视化大屏", "运营活动页", "组件库", "工程化脚手架"],
[(2019, 2021, ["JavaScript", "HTML", "CSS", "Vue", "jQuery"], ["Webpack", "Angular", "小程序开发"]),
(2022, 2023, ["JavaScript", "TypeScript", "Vue", "React", "Webpack"], ["Vite", "Node.js", "小程序开发"]),
(2024, 2026, ["TypeScript", "Vue", "React", "Vite", "低代码平台"], ["AI辅助编程", "微前端", "Node.js"])],
{2019: 6, 2020: 6, 2021: 8, 2022: 12, 2023: 24, 2024: 24, 2025: 24, 2026: 24}),
arc("数据工程师", "大数据", 3, "16K-32K", ["金融", "物流", "零售"],
["互联网/软件", "金融科技"],
["负责{o}的采集与清洗", "搭建{o}的离线数仓分层", "开发{o}的实时计算任务",
"维护{o}的调度与监控体系", "参与{o}的数据治理工作"],
["业务数据", "日志数据", "实时数仓", "离线数仓", "数据湖", "指标体系"],
[(2019, 2021, ["SQL", "Hadoop", "Hive", "Spark", "Python"], ["HBase", "Sqoop", "Scala"]),
(2022, 2026, ["SQL", "Spark", "Flink", "Kafka", "Python"], ["ClickHouse", "Doris", "Airflow"])],
{2019: 5, 2020: 5, 2021: 5, 2022: 6, 2023: 12, 2024: 24, 2025: 24, 2026: 24}),
arc("算法工程师", "AI算法", 3, "20K-40K", ["广告", "电商", "金融"],
["人工智能"],
["负责{o}的建模与迭代", "参与{o}的特征工程建设", "跟踪{o}的前沿论文并落地",
"优化{o}的线上推理性能", "与工程团队协作上线{o}"],
["推荐模型", "排序模型", "风控模型", "用户画像", "召回策略", "预估服务"],
[(2019, 2021, ["Python", "机器学习", "深度学习", "TensorFlow", "SQL"], ["XGBoost", "Spark", "C++"]),
(2022, 2023, ["Python", "机器学习", "深度学习", "PyTorch", "推荐算法"], ["XGBoost", "模型微调", "强化学习"]),
(2024, 2026, ["Python", "机器学习", "深度学习", "PyTorch", "推荐算法"], ["大模型应用开发", "模型微调", "强化学习"])],
{2019: 5, 2020: 5, 2021: 5, 2022: 6, 2023: 12, 2024: 24, 2025: 24, 2026: 24}),
# —— 数据分析:Excel/SPSS 时代 → SQL/Python → BI 平台 → LLM 增强分析 ——
arc("数据分析师", "数据分析", 2, "12K-22K", ["零售", "电商", "金融"],
["互联网/软件", "金融科技"],
["负责{o}的报表体系建设", "输出{o}的专题分析报告", "搭建{o}的核心指标看板",
"支持业务团队的{o}取数需求", "参与{o}的复盘与归因分析"],
["经营数据", "用户行为", "活动效果", "渠道投放", "留存漏斗", "销售大盘"],
[(2019, 2021, ["Excel", "SQL", "SPSS", "数据分析", "数据可视化"], ["Tableau", "R", "Power BI"]),
(2022, 2023, ["SQL", "Python", "数据分析", "数据可视化", "Excel"], ["Power BI", "Tableau", "A/B测试"]),
(2024, 2026, ["SQL", "Python", "数据分析", "数据可视化", "A/B测试"], ["Power BI", "Tableau", "大模型应用开发"])],
{2019: 5, 2020: 5, 2021: 6, 2022: 8, 2023: 12, 2024: 24, 2025: 24, 2026: 24}),
arc("测试开发工程师", "测试", 3, "14K-26K", ["金融", "电商", "政务"],
["互联网/软件"],
["负责{o}的测试方案设计", "开发{o}的自动化测试脚本", "搭建{o}的测试平台",
"执行{o}的性能压测", "推动{o}的质量效能改进"],
["核心交易系统", "支付链路", "回归测试", "接口自动化", "压测体系", "质量平台"],
[(2019, 2021, ["Python", "自动化测试", "Selenium", "接口测试", "SQL"], ["JMeter", "JUnit", "Jenkins"]),
(2022, 2026, ["Python", "自动化测试", "接口测试", "性能测试", "CI/CD"], ["Pytest", "JMeter", "Docker"])],
{2019: 4, 2020: 4, 2021: 4, 2022: 6, 2023: 12, 2024: 24, 2025: 24, 2026: 24}),
# —— 运维:脚本/手工 → Ansible 自动化 → DevOps/K8s → AIOps ——
arc("运维工程师(SRE)", "运维", 3, "15K-28K", ["金融", "游戏", "电商"],
["互联网/软件"],
["负责{o}的稳定运行与监控", "参与{o}的容量规划", "建设{o}的自动化运维体系",
"处理{o}的线上故障与应急", "完善{o}的告警与巡检机制"],
["生产集群", "发布流水线", "监控体系", "容器平台", "中间件", "灾备体系"],
[(2019, 2021, ["Linux", "Shell", "Nginx", "MySQL", "Tomcat"], ["Ansible", "Jenkins", "Python"]),
(2022, 2023, ["Linux", "Docker", "Kubernetes", "CI/CD", "Ansible"], ["Prometheus", "DevOps", "Grafana"]),
(2024, 2026, ["Linux", "Kubernetes", "Docker", "Prometheus", "DevOps"], ["Grafana", "Terraform", "AIOps"])],
{2019: 4, 2020: 4, 2021: 6, 2022: 8, 2023: 12, 2024: 24, 2025: 24, 2026: 24}),
arc("嵌入式工程师", "嵌入式", 3, "14K-28K", ["智能制造", "安防", "出行"],
["智能制造", "物联网"],
["负责{o}的固件开发", "参与{o}的驱动调试", "完成{o}的板级联调与测试",
"优化{o}的功耗与稳定性", "编写{o}的开发文档"],
["主控固件", "通信模组", "传感器驱动", "boot 启动流程", "低功耗方案", "量产固件"],
[(2019, 2022, ["C", "C++", "嵌入式开发", "单片机", "RTOS"], ["STM32", "FreeRTOS", "Linux"]),
(2023, 2026, ["C", "C++", "嵌入式开发", "RTOS", "驱动开发"], ["STM32", "边缘计算", "MQTT"])],
{2019: 4, 2020: 4, 2021: 4, 2022: 4, 2023: 5, 2024: 6, 2025: 6, 2026: 6}),
arc("物联网工程师", "物联网", 3, "13K-25K", ["智能制造", "物流", "安防"],
["物联网", "智慧城市"],
["负责{o}的接入方案设计", "开发{o}的设备管理功能", "搭建{o}的数据采集链路",
"参与{o}的平台联调", "优化{o}的通信稳定性"],
["设备接入", "网关", "传感网络", "边缘节点", "设备影子", "远程升级"],
[(2019, 2021, ["C", "MQTT", "传感器", "单片机", "物联网平台"], ["Zigbee", "LoRa", "STM32"]),
(2022, 2026, ["MQTT", "物联网平台", "边缘计算", "Python", "传感器"], ["LoRa", "嵌入式开发", "云计算"])],
{2019: 4, 2020: 4, 2021: 4, 2022: 5, 2023: 5, 2024: 6, 2025: 6, 2026: 6}),
arc("NLP工程师", "AI算法", 3, "18K-36K", ["教育", "政务", "金融"],
["人工智能"],
["负责{o}的算法研发", "参与{o}的语料建设", "优化{o}的模型效果",
"跟进{o}的前沿技术", "支持{o}的工程化落地"],
["智能客服", "文本分类", "信息抽取", "问答系统", "搜索相关性", "舆情分析"],
[(2019, 2021, ["Python", "NLP", "机器学习", "TensorFlow", "SQL"], ["知识图谱", "数据挖掘", "深度学习"]),
(2022, 2023, ["Python", "NLP", "深度学习", "PyTorch", "知识图谱"], ["模型微调", "数据挖掘", "机器学习"]),
(2024, 2026, ["Python", "NLP", "深度学习", "PyTorch", "大模型应用开发"], ["模型微调", "RAG", "LangChain"])],
{2019: 4, 2020: 4, 2021: 4, 2022: 5, 2023: 6, 2024: 8, 2025: 8, 2026: 8}),
arc("CV工程师", "AI算法", 3, "18K-36K", ["安防", "智能制造", "出行"],
["人工智能", "智能制造"],
["负责{o}的算法研发", "参与{o}的数据标注体系", "优化{o}的模型精度与速度",
"部署{o}到边缘设备", "跟进{o}的最新论文"],
["缺陷检测", "目标检测", "视频结构化", "人脸识别", "车道线检测", "工业质检"],
[(2019, 2021, ["Python", "计算机视觉", "深度学习", "TensorFlow", "C++"], ["OCR", "OpenCV", "模型微调"]),
(2022, 2023, ["Python", "计算机视觉", "深度学习", "PyTorch", "OCR"], ["模型微调", "边缘计算", "OpenCV"]),
(2024, 2026, ["Python", "计算机视觉", "深度学习", "PyTorch", "OCR"], ["模型微调", "边缘计算", "大模型应用开发"])],
{2019: 4, 2020: 4, 2021: 4, 2022: 5, 2023: 6, 2024: 8, 2025: 8, 2026: 8}),
arc("云原生工程师", "云原生", 3, "18K-35K", ["金融", "政务", "电商"],
["互联网/软件"],
["负责{o}的架构设计与落地", "建设{o}的容器编排体系", "推进{o}的服务治理",
"优化{o}的资源利用率", "保障{o}的高可用"],
["容器平台", "微服务治理", "多云架构", "弹性伸缩", "服务网格", "可观测体系"],
[(2020, 2022, ["Docker", "Kubernetes", "微服务", "Linux", "Go"], ["Service Mesh", "Prometheus", "DevOps"]),
(2023, 2026, ["Kubernetes", "Docker", "微服务", "CI/CD", "Go"], ["Service Mesh", "Terraform", "云计算"])],
{2020: 4, 2021: 4, 2022: 5, 2023: 6, 2024: 8, 2025: 8, 2026: 8}),
arc("AI产品经理", "产品", 3, "18K-35K", ["教育", "金融", "医疗"],
["人工智能", "智慧医疗"],
["负责{o}的规划与落地", "输出{o}的需求文档", "跟进{o}的研发进度",
"设计{o}的评估指标", "调研{o}的行业动态"],
["智能问答产品", "AI 助手", "智能化应用", "知识库产品", "智能审核", "AI 创作工具"],
[(2020, 2022, ["产品设计", "需求分析", "数据分析", "项目管理", "AI产品"], ["Axure", "竞品分析", "SQL"]),
(2023, 2026, ["AI产品", "需求分析", "大模型应用开发", "产品设计", "数据分析"], ["提示词工程", "用户增长", "A/B测试"])],
{2020: 3, 2021: 3, 2022: 3, 2023: 4, 2024: 6, 2025: 6, 2026: 6}),
arc("Go开发工程师", "后端开发", 3, "16K-32K", ["游戏", "社交", "金融"],
["互联网/软件"],
BACKEND_DUTIES, BACKEND_OBJS,
[(2019, 2021, ["Go", "MySQL", "Redis", "Linux", "Gin"], ["Docker", "gRPC", "Kafka"]),
(2022, 2026, ["Go", "微服务", "gRPC", "Redis", "Kubernetes"], ["Kafka", "Prometheus", "高并发"])],
{2019: 4, 2020: 4, 2021: 4, 2022: 5, 2023: 6, 2024: 8, 2025: 8, 2026: 8}),
# —— 2023 年后才出现且快速增长的新岗位(支撑“新岗位发现”演示) ——
arc("大模型应用工程师", "AI算法", 2, "25K-50K", ["金融", "教育", "医疗"],
["人工智能"],
["负责{o}的应用开发与落地", "设计{o}的提示词与流程编排", "建设{o}的评测体系",
"优化{o}的推理成本与延迟", "跟进{o}的生态工具"],
["智能助手", "企业知识库", "Agent 应用", "代码助手", "智能审核", "多轮对话"],
[(2023, 2026, ["Python", "大模型应用开发", "提示词工程", "LangChain", "NLP"], ["RAG", "模型微调", "向量数据库"])],
{2023: 2, 2024: 8, 2025: 14, 2026: 20}),
arc("RAG工程师", "AI算法", 2, "25K-45K", ["金融", "政务", "教育"],
["人工智能"],
["负责{o}的检索链路搭建", "优化{o}的召回与重排", "建设{o}的知识库加工流水线",
"评估{o}的问答质量", "跟进{o}的新范式"],
["企业知识库", "文档问答", "语义检索", "混合检索", "知识中台", "智能客服"],
[(2024, 2026, ["Python", "RAG", "向量数据库", "大模型应用开发", "LangChain"], ["模型微调", "Elasticsearch", "知识图谱"])],
{2024: 4, 2025: 9, 2026: 15}),
arc("提示词工程师", "AI算法", 1, "15K-30K", ["教育", "电商", "广告"],
["人工智能"],
["负责{o}的提示词设计", "沉淀{o}的提示词模板库", "评估{o}的输出质量",
"与业务团队共创{o}的使用范式", "编写{o}的最佳实践文档"],
["营销文案生成", "智能客服话术", "内容创作", "教学辅导", "商品描述", "对话流程"],
[(2024, 2026, ["提示词工程", "大模型应用开发", "Python", "数据分析"], ["LangChain", "RAG", "AI产品"])],
{2024: 2, 2025: 6, 2026: 10}),
]
COMPANY_PRE = ["星澜", "云启", "数联", "智汇", "蓝湾", "极光", "恒宇", "锐思", "博远", "辰安",
"飞鱼", "墨白", "青藤", "远望", "砺石", "潮汐", "峰行", "微光", "若谷", "启航"]
COMPANY_SUF = ["科技", "网络", "信息", "数据", "智能", "软件", "互联", "数字"]
# 15 城:一线 + 新一线(成都/武汉/西安/苏州/长沙等)
CITIES = ["北京", "上海", "深圳", "杭州", "成都", "广州", "南京", "武汉",
"西安", "苏州", "长沙", "重庆", "天津", "青岛", "合肥"]
SOURCES = ["BOSS直聘", "拉勾网", "智联招聘", "前程无忧", "猎聘网"]
# ---------------------------------------------------------------- 文本渲染素材
# 公司介绍:按年代分池,时代措辞只出现在对应年份之后
INTROS_EARLY = [
"{c}是一家专注于新一代信息技术服务的高新技术企业,现诚邀优秀人才加入。",
"{c}深耕行业多年,团队氛围开放,技术栈前沿,期待你的加入。",
"{c}业务高速增长中,提供有竞争力的薪酬与完善的培养体系。",
"{c}核心团队来自一线大厂,正在寻找志同道合的工程师。",
"{c}处于快速扩张期,移动业务用户量持续攀升,欢迎加入我们。",
]
INTROS_MID = INTROS_EARLY + [
"{c}已完成新一轮融资,正全面推进云原生架构升级,期待你的加入。",
"{c}业务覆盖多个城市,研发体系成熟,重视工程质量与技术沉淀。",
]
INTROS_LATE = INTROS_MID + [
"{c}全面拥抱 AI 原生开发,大模型已深度融入研发流程,欢迎同行者。",
"{c}正在推进 AIGC 与核心业务的深度融合,寻找敢想敢做的工程师。",
"{c}智能化转型加速,AI 辅助研发已成团队标配,期待你的加入。",
]
# 项目背景/团队介绍(可选段落,{a} 只会代入本条 JD 已标注的场景词)
BACKGROUNDS = [
"关于我们:{c}的{a}业务正处于高速增长期,你将直接参与支撑千万级用户的核心系统建设,与团队一起解决真实复杂的业务问题。",
"项目背景:本岗位属于{a}方向核心团队,业务数据量大、迭代节奏快,技术挑战与成长空间并存。",
"团队介绍:{a}研发团队规模 50+,核心成员来自一线大厂,重视工程质量与技术沉淀。",
]
# 团队与福利(可选尾部段落,抽取器按标题截断,不进入任何技能区)
WELFARE_LINES = [
"弹性工作不打卡,五险一金全额缴纳,年度体检与带薪年假一应俱全。",
"技术氛围浓厚,每周内部分享,提供外部培训与技术会议预算。",
"扁平化管理,晋升通道清晰,核心成员可参与期权激励计划。",
"免费三餐与下午茶,团队年轻化,协作氛围轻松高效。",
]
REQ_YEAR_LINES = [
"{y}年以上相关工作经验,责任心强,具备良好的沟通能力;",
"{y}年以上相关岗位经验,具备良好的沟通与推动能力;",
"{y}年以上实战经验,结果导向,能独立承担模块 Owner 工作;",
]
REQ_VERBS = ["熟练掌握", "精通", "熟悉并掌握", "深入理解"]
REQ_SOFT_LINES = [
"具备良好的学习能力与团队协作精神;",
"逻辑清晰,自驱力强,对技术有热情;",
"具备良好的文档习惯,重视工程质量与可维护性;",
"能适应快速迭代的研发节奏,抗压能力强;",
]
SCEN_TEMPLATES = ["具有{a}或{b}行业经验者优先;", "熟悉{a}/{b}业务场景者优先;",
"有{a}、{b}行业项目经验者优先;", "具备{a}或{b}相关业务背景者优先;"]
PREF_TEMPLATES = ["了解{s}者优先;", "熟悉{s}者优先考虑;", "有{s}相关实践经验者优先;"]
PREF_EXTRA_LINES = ["有开源项目贡献或高质量技术博客者优先;", "参与过大型技术社区分享者优先;"]
NUM_FORMATS = ["{i}、", "{i}、", "{i}、", "{i}.", "- "]
NOISE_TAILS = [
"【广告】扫码关注公众号,领取面试大礼包和内部资料。",
"点击关注我们的招聘号,第一时间获取内推信息。",
"【广告】商务合作请加群,扫码领取免费课程资料。",
]
NOISE_FULL = [
"【广告】扫码关注公众号领取求职资料\n点击关注领取福利红包\n长期回收二手办公设备,非诚勿扰",
"【广告】推广信息:扫码加入求职群领取资料\n商务合作请加群详谈\n加盟代理热线见官网",
"【广告】点击关注即可领取现金红包\n扫码进福利群,天天有惊喜\n代办各类证件,长期有效",
"【广告】推广信息:刷单兼职日结\n扫码添加客服领取任务\n长期回收库存尾货",
]
def intros_for(year: int) -> list[str]:
if year >= 2024:
return INTROS_LATE
if year >= 2022:
return INTROS_MID
return INTROS_EARLY
def skillset_for(a: dict, year: int) -> tuple[list[str], list[str]]:
for y0, y1, req, pref in a["timeline"]:
if y0 <= year <= y1:
return list(req), list(pref)
req, pref = a["timeline"][-1][2], a["timeline"][-1][3]
return list(req), list(pref)
def pick_duties(a: dict, year: int) -> list[str]:
"""按年份过滤句式池(时代门槛),抽 3-6 条职责。"""
pool = list(a["duty_tmpls"]) + [t for t, min_y in GENERIC_DUTIES if year >= min_y]
tmpls = rng.sample(pool, k=min(len(pool), rng.choice([3, 4, 4, 5, 5, 6])))
objs = rng.sample(a["duty_objs"], k=min(len(a["duty_objs"]), len(tmpls)))
while len(objs) < len(tmpls):
objs.append(rng.choice(a["duty_objs"]))
return [t.format(o=o) + ";" for t, o in zip(tmpls, objs)]
def _skill_req_lines(req: list[str]) -> list[str]:
"""必备技能行的多种自然写法(都在要求区内,抽取结果与措辞无关)。"""
verb = rng.choice(REQ_VERBS)
if len(req) >= 4 and rng.random() < 0.4:
cut = rng.randint(2, len(req) - 2)
return [f"{verb}{'、'.join(req[:cut])},熟悉{'、'.join(req[cut:])};"]
if rng.random() < 0.25:
return [f"具备扎实的{'、'.join(req)}功底;"]
return [f"{verb}{'、'.join(req)};"]
def build_text(a: dict, company: str, year: int, years: int, duties: list[str],
req: list[str], pref: list[str], scens: list[str]) -> str:
duty_h = rng.choice(DUTY_HEADERS)
req_h = rng.choice(REQ_HEADERS)
pref_h = rng.choice(PREF_HEADERS)
num_fmt = rng.choice(NUM_FORMATS)
scen_sentence = rng.choice(SCEN_TEMPLATES).format(a=scens[0], b=scens[-1])
lines = [
f"岗位名称:{a['title']}",
rng.choice(intros_for(year)).format(c=company),
]
# 可选段落:项目背景/团队介绍(约 35%)
if rng.random() < 0.35:
lines.append(rng.choice(BACKGROUNDS).format(c=company, a=scens[0]))
lines.append(f"{duty_h}:")
lines += [f"{num_fmt.format(i=i + 1)}{d}" for i, d in enumerate(duties)]
lines.append(f"{req_h}:")
req_lines = [rng.choice(REQ_YEAR_LINES).format(y=years)]
req_lines += _skill_req_lines(req)
req_lines.append(scen_sentence)
req_lines += rng.sample(REQ_SOFT_LINES, k=rng.choice([0, 1, 1, 2]))
lines += [f"{num_fmt.format(i=i + 1)}{t}" for i, t in enumerate(req_lines)]
lines.append(f"{pref_h}:")
lines.append(f"1、{rng.choice(PREF_TEMPLATES).format(s='、'.join(pref))}")
if rng.random() < 0.4:
lines.append(f"2、{rng.choice(PREF_EXTRA_LINES)}")
# 可选尾部段落:团队与福利(约 40%),抽取器按标题截断
if rng.random() < 0.40:
lines.append(f"{rng.choice(TAIL_HEADERS)}:")
lines.append(rng.choice(WELFARE_LINES))
return "\n".join(lines)
def make_jd(a: dict, year: int, jid: str, month: int | None = None) -> tuple[dict, dict]:
"""生成一条 JD 记录与其标准答案(先定结构,再渲染文本)。
month 可由调用方指定(原始数据按年内均匀铺开,避免季度窗口计数随机抖动
制造虚假演化事件);不传则随机(测试集保持自然随机)。
"""
company = rng.choice(COMPANY_PRE) + rng.choice(COMPANY_SUF)
req, pref = skillset_for(a, year)
rng.shuffle(req)
rng.shuffle(pref)
if len(req) > 4 and rng.random() < 0.15: # 轻微抖动:偶尔少写一项必备
req = req[:-1]
pref = pref[: rng.choice([2, 2, 3])]
duties = pick_duties(a, year)
scens = rng.sample(a["scenarios"], k=min(2, len(a["scenarios"])))
years = rng.randint(max(1, a["years"] - 1), a["years"] + 2)
text = build_text(a, company, year, years, duties, req, pref, scens)
month = month or rng.randint(1, 12)
day = rng.randint(1, 28)
record = {
"jid": jid,
"job_title": a["title"],
"company": company,
"city": rng.choice(CITIES),
"salary": a["salary"],
"industry": rng.choice(a["industries"]),
"publish_date": date(year, month, day).isoformat(),
"source": rng.choice(SOURCES),
"url": f"https://jobs.example.com/{jid}",
"text": text,
}
gold = {
"job_title": a["title"],
"responsibilities": duties,
"required_skills": req,
"preferred_skills": pref,
"scenarios": scens,
}
return record, gold
def make_dup_copy(rec: dict, jid: str) -> dict:
"""生成近重复(抄袭)副本:同文不同公司,仅换公司/城市/渠道/薪资与发布日期。"""
new_company = rng.choice(COMPANY_PRE) + rng.choice(COMPANY_SUF)
text = rec["text"].replace(rec["company"], new_company)
copy = dict(rec)
copy.update({
"jid": jid,
"company": new_company,
"city": rng.choice(CITIES),
"source": rng.choice(SOURCES),
"url": f"https://jobs.example.com/{jid}",
"salary": rec["salary"].replace("14薪", "13薪") if "14薪" in rec["salary"] else rec["salary"],
"text": text,
"publish_date": min(
date.fromisoformat(rec["publish_date"]) + timedelta(days=rng.randint(10, 120)),
date(2026, 12, 28)).isoformat(),
})
return copy
def write_jsonl(path: Path, rows: list[dict]) -> None:
path.parent.mkdir(parents=True, exist_ok=True)
with open(path, "w", encoding="utf-8") as f:
for r in rows:
f.write(json.dumps(r, ensure_ascii=False) + "\n")
# ---------------------------------------------------------------- 简历样例(全合成,无真实个人信息)
RESUME_FRONTEND_FULL = (
"姓名:陈思远\n手机:13655557777\n邮箱:chensy@example.com\n"
"4年前端开发经验,主导过大促会场与管理后台的前端架构设计。\n"
"核心技能:熟练掌握JavaScript、TypeScript、HTML、CSS,精通Vue、React双栈,"
"熟悉Vite、Webpack、Node.js与小程序开发,有低代码平台建设经验,日常使用AI辅助编程提效。\n"
"项目经历:负责可视化大屏与管理后台开发,基于Vue与Vite搭建工程化体系;"
"使用React与TypeScript重构信息流页面,首屏性能提升40%。\n"
)
RESUME_DATA_PARTIAL = (
"姓名:刘洋\n手机:13566668888\n邮箱:liuyang.data@example.com\n"
"2年数据分析经验,服务于零售业务线,支持运营团队的日常取数与复盘。\n"
"核心技能:熟练使用SQL取数,掌握Excel、数据可视化与Tableau看板搭建,"
"能用Python完成数据清洗与报表自动化,了解Power BI。\n"
"项目经历:搭建活动效果核心指标看板,输出渠道投放专题分析,"
"在校期间曾用SPSS完成调研统计。\n"
)
RESUME_LLM_FULL = (
"姓名:赵启明\n手机:13777778888\n邮箱:zhaoqm@example.com\n"
"3年AI应用开发经验,专注大模型应用开发与落地。\n"
"核心技能:精通Python,熟悉大模型应用开发、提示词工程、LangChain与RAG,"
"掌握向量数据库与模型微调,了解NLP基础,熟悉FastAPI服务开发。\n"
"项目经历:主导企业知识库问答系统建设,基于RAG与LangChain实现混合检索与重排,"
"通过提示词工程与模型微调将回答准确率提升25%。\n"
)
RESUME_LEGACY_MISMATCH = (
"姓名:孙建国\n手机:13899990000\n邮箱:sunjg@example.com\n"
"8年传统软件经验,长期维护企业内部办公系统。\n"
"核心技能:精通Struts、JSP、Servlet,熟悉Svn与Tomcat部署,"
"掌握jQuery页面开发,了解WebLogic。\n"
"项目经历:负责政务办公系统的日常维护与功能迭代,使用Struts与JSP完成多个模块改造。\n"
)
def _cand(title: str, req: list[str], pref: list[str], scens: list[str],
src: int, comp: int, status: str = "pending") -> dict:
"""构造一条待审核候选的快照(审核留痕种子用)。"""
return {"job_id": f"cand_{title}", "title": title, "category": "新岗位候选",
"required_skills": req, "preferred_skills": pref, "responsibilities": [],
"scenarios": scens, "expected_years": 3, "status": status,
"source_count": src, "company_count": comp}
AUDIT_SEED = [
{"candidate_id": "cand_大模型应用工程师", "action": "accept",
"operator": "王蕾(评审专家)",
"reason": "岗位簇证据充分:40+ 条 JD、独立企业数达标,2024 年起持续高增长,技能结构清晰,准予入库。",
"before": _cand("大模型应用工程师",
["提示词工程", "Python", "LangChain", "大模型应用开发", "NLP"],
["模型微调", "向量数据库", "RAG"], ["教育", "医疗", "金融"], 42, 38),
"after": _cand("大模型应用工程师",
["提示词工程", "Python", "LangChain", "大模型应用开发", "NLP"],
["模型微调", "向量数据库", "RAG"], ["教育", "医疗", "金融"], 42, 38, "active"),
"time": "2026-07-18 10:24:31"},
{"candidate_id": "cand_RAG工程师", "action": "modify",
"operator": "李强(评审专家)",
"reason": "簇内 JD 普遍要求检索引擎实操经验,补充 Elasticsearch 为必备技能后入库。",
"before": _cand("RAG工程师",
["Python", "RAG", "向量数据库", "大模型应用开发", "LangChain"],
["模型微调", "知识图谱"], ["金融", "政务", "教育"], 28, 22),
"after": _cand("RAG工程师",
["Python", "RAG", "向量数据库", "大模型应用开发", "LangChain", "Elasticsearch"],
["模型微调", "知识图谱"], ["金融", "政务", "教育"], 28, 22, "active"),
"time": "2026-07-19 15:02:11"},
{"candidate_id": "cand_元宇宙社区运营", "action": "reject",
"operator": "王蕾(评审专家)",
"reason": "簇内 JD 仅 5 条且集中于 2 家企业,近一年增长不明朗,证据不足,暂不入库。",
"before": _cand("元宇宙社区运营", ["用户增长", "竞品分析"], ["数据分析"],
["社交", "游戏"], 5, 2),
"after": {},
"time": "2026-07-21 09:41:03"},
{"kind": "node_update", "node_id": "job_Java开发工程师",
"operator": "张铭(领域专家)",
"reason": "2024 年后分布式系统已成为该岗位核心要求,上调该关系权重。",
"before": {"weight": 0.82}, "after": {"weight": 0.95},
"time": "2026-07-23 14:12:55"},
{"kind": "node_update", "node_id": "Kubernetes",
"operator": "张铭(领域专家)",
"reason": "词表归类校正:由未标注类别调整为云原生类别。",
"before": {"category": ""}, "after": {"category": "云原生"},
"time": "2026-07-23 14:15:20"},
{"candidate_id": "cand_提示词工程师", "action": "accept",
"operator": "陈静(评审专家)",
"reason": "2024 年起多家企业独立设岗,职责与传统运营岗差异显著,予以入库观察。",
"before": _cand("提示词工程师",
["提示词工程", "大模型应用开发", "Python", "数据分析"],
["LangChain", "RAG", "AI产品"], ["教育", "电商", "广告"], 18, 14),
"after": _cand("提示词工程师",
["提示词工程", "大模型应用开发", "Python", "数据分析"],
["LangChain", "RAG", "AI产品"], ["教育", "电商", "广告"], 18, 14, "active"),
"time": "2026-07-25 11:30:44"},
{"kind": "node_update", "node_id": "job_运维工程师(SRE)",
"operator": "李强(评审专家)",
"reason": "按 2026 年 JD 样本补全岗位级别标注。",
"before": {"level": None}, "after": {"level": "中级"},
"time": "2026-07-28 16:48:09"},
]
# ---------------------------------------------------------------- 报告样例(与 JD 演化信号相互印证)
REPORT_TREND = (
"新一代信息技术岗位能力趋势报告(样例)\n\n"
"一、总体趋势\n"
"2019-2026 年,后端岗位从 SSM/Servlet 体系加速转向 Spring Boot/Spring Cloud 微服务体系,"
"容器化(Docker/Kubernetes)已成为运维与后端岗位的通用要求。\n\n"
"二、AI 原生岗位崛起\n"
"2023 年起,大模型应用工程师、RAG工程师、提示词工程师等新岗位快速增长,"
"大模型应用开发、提示词工程、向量数据库成为增速最快的技能点。\n\n"
"三、传统技能退出\n"
"Struts、JSP、Svn 等传统技术栈在 2022 年后基本退出主流 JD。\n"
"四、AI 辅助编程普及\n"
"2024 年起,AI辅助编程(Copilot 类工具)开始作为加分项出现在后端岗位 JD 中。\n"
)
REPORT_TALENT_2026 = (
"新一代信息技术人才发展报告(2026)(节选·样例)\n\n"
"一、总体格局:能力图谱加速重构\n"
"2019-2026 年,信息技术岗位的能力要求经历了从“框架驱动”到“云原生驱动”再到“AI 原生驱动”"
"的三次跃迁。招聘 JD 数据显示,微服务、容器化已于 2022 年前后完成普及,成为后端与运维岗位的"
"基础要求;2024 年起,大模型相关能力以前所未有的速度进入各岗位的能力图谱。\n\n"
"二、AI 原生岗位爆发式增长\n"
"大模型应用工程师自 2023 年出现以来需求连年翻倍,2026 年新发岗位量已达 2023 年的十倍以上;"
"RAG工程师、提示词工程师自 2024 年起独立设岗。值得注意的是,提示词工程相关岗位需求从 2023 年"
"起爆发,企业对该能力的要求正从“会用”转向“体系化”:提示词模板库沉淀、输出质量评估、与业务"
"流程的编排结合成为高频要求。\n\n"
"三、AI 辅助编程渗透率快速上升\n"
"2024 年起,AI辅助编程(Copilot 类工具)开始以加分项形式进入后端、前端岗位 JD;到 2026 年,"
"约半数新发后端岗位 JD 提及 AI 辅助研发能力,“使用大模型辅助开发与代码审查”一类职责表述明显"
"增多。AI 工具链正从“个人效率工具”升级为“团队工程标配”。\n\n"
"四、前端:从框架之争到工程化与智能化\n"
"jQuery、AngularJS 相关要求在 2022 年后加速退场,Vue/React + TypeScript 成为绝对主流;构建"
"工具方面,Vite 已基本取代 Webpack 成为新项目标配。2024 年起,低代码平台与 AI 辅助生成进入"
"前端岗位核心要求,前端工程师的能力重心从“写页面”转向“搭体系”。\n\n"
"五、运维与分析岗位的智能化迁移\n"
"运维岗位沿“脚本手工 → Ansible 自动化 → DevOps/Kubernetes → AIOps”路径演进,2024 年起 AIOps"
"首次进入岗位加分项;数据分析岗位则完成了“Excel/SPSS 退场、SQL/Python 成标配”的换代,2024 年"
"起大模型增强分析(自然语言取数、报告自动生成)开始进入要求清单。\n\n"
"六、传统技术栈出清\n"
"Struts、JSP、Svn、WebLogic 等传统技术栈在 2022 年后基本退出主流 JD,仅存的少量需求集中于"
"存量系统维护岗位,且普遍标注“接受老系统维护经验”作为过渡安排。\n"
)
REPORT_DIGITAL_ECONOMY = (
"数字经济就业趋势观察(2026 年上半年)(节选·样例)\n\n"
"一、区域格局:新一线城市承接效应显著\n"
"2026 年上半年,信息技术岗位的区域分布持续多极化。北京、上海、深圳、杭州仍是岗位密度最高的"
"第一梯队,但成都、武汉、西安、苏州、长沙等新一线城市的岗位占比连续第三年上升,合计占比已"
"接近三成。其中成都以后端与游戏研发见长,武汉、西安依托高校资源在嵌入式与智能制造方向增速"
"领先,苏州、长沙则在工业互联网与物联网岗位上放量明显。\n\n"
"二、岗位结构:AI 岗位从“概念热”走向“用工实”\n"
"与 2023 年大模型岗位“重算法研究”不同,2026 年的需求主体已转向应用落地:大模型应用工程师、"
"RAG工程师合计新发岗位量超过传统算法工程师的六成,提示词工程能力则从独立岗位演变为产品、"
"运营、数据分析等多岗位的通用加分项。用人单位普遍要求候选人同时具备工程落地能力与业务理解"
"能力,单纯的“调参型”简历竞争力明显下降。\n\n"
"三、技能更替:老栈出清与新栈下沉同步发生\n"
"上半年数据显示,Struts、JSP、Svn 等传统技术栈在新增 JD 中已基本绝迹;与之相对,原本集中"
"于头部企业的 Kubernetes、CI/CD、可观测体系等云原生能力,正快速下沉至中小企业 JD。运维岗位"
"的 AIOps 要求、数据分析岗位的大模型增强分析要求,虽然仍以加分项形式出现,但提及率环比均"
"翻倍增长,预计 1-2 年内转为必备项。\n\n"
"四、薪酬观察:AI 应用岗溢价收窄但基数抬高\n"
"大模型应用工程师的薪酬中位数仍领先通用后端岗位约 40%,但溢价幅度较 2024 年高点收窄;同时,"
"掌握 AI 工具链的通用岗位(后端、前端、测试)薪酬基数整体抬升,具备“AI+工程”复合能力的候选"
"人在议价中优势明显。\n\n"
"五、趋势研判\n"
"预计下半年岗位能力图谱的演化将沿三条主线展开:一是 AI 辅助研发能力从加分项转为默认要求;"
"二是低代码与 AI 生成进一步压缩初级前端岗位需求,中高级工程化人才缺口扩大;三是新一线城市"
"与一线城市的技能要求趋于同构,区域间人才竞争加剧。\n"
)
def main() -> None:
data = ROOT / "data"
data.mkdir(parents=True, exist_ok=True)
# 1. 技能词表 + 行业词表 + 岗位-行业归属
lexicon = {
"skills": [{"name": n, "category": c, "aliases": al, "level": lv, "prereqs": pre}
for n, c, al, lv, pre in SKILLS],
"scenarios": SCENARIOS,
"industries": INDUSTRIES,
"jobs": [{"title": a["title"], "category": a["category"],
"industries": a["industries"], "expected_years": a["years"]}
for a in ARCHETYPES],
}
(data / "skills_lexicon.json").write_text(
json.dumps(lexicon, ensure_ascii=False, indent=1), encoding="utf-8")
# 2. 原始 JD(按年份分批次)
raw_by_year: dict[int, list[dict]] = {y: [] for y in range(2019, 2027)}
seq = 0
for a in ARCHETYPES:
for year, count in sorted(a["volumes"].items()):
for i in range(count):
seq += 1
# 月份年内均匀铺开:季度窗口计数只随剧本(时间轴/量级)变化
rec, _ = make_jd(a, year, f"J{seq:05d}", month=i * 12 // count + 1)
raw_by_year[year].append(rec)
# 3. 植入噪声(约 3%):一半混排尾注(清洗时剔除行),一半整篇广告(整条标记)
all_records = [r for y in raw_by_year.values() for r in y]
noise_jids: list[str] = []
mixed = rng.sample(all_records, k=10)
for rec in mixed:
rec["text"] = rec["text"] + "\n" + rng.choice(NOISE_TAILS)
noise_jids.append(rec["jid"])
for i in range(8):
full = NOISE_FULL[i % len(NOISE_FULL)]
seq += 1
jid = f"J{seq:05d}"
year = rng.randint(2020, 2026)
raw_by_year[year].append({
"jid": jid, "job_title": rng.choice(ARCHETYPES)["title"],
"company": rng.choice(COMPANY_PRE) + rng.choice(COMPANY_SUF),
"city": rng.choice(CITIES), "salary": "面议",
"industry": rng.choice(INDUSTRIES),
"publish_date": date(year, rng.randint(1, 12), rng.randint(1, 28)).isoformat(),
"source": rng.choice(SOURCES), "url": f"https://jobs.example.com/{jid}",
"text": full,
})
noise_jids.append(jid)
# 4. 植入近重复(约 8%):抄袭源避开噪声记录,副本与原文高度相似
dup_groups: list[list[str]] = []
clean_pool = [r for r in all_records if r["jid"] not in noise_jids]
dup_sources = rng.sample(clean_pool, k=int(len(all_records) * 0.08))
for src in dup_sources:
seq += 1
copy = make_dup_copy(src, f"J{seq:05d}")
year = min(2026, int(copy["publish_date"][:4]))
raw_by_year[year].append(copy)
dup_groups.append([src["jid"], copy["jid"]])
for year, rows in raw_by_year.items():
write_jsonl(data / "jd_raw" / f"batch_{year}.jsonl", rows)
(data / "jd_raw" / "_meta.json").write_text(json.dumps(
{"dup_groups": dup_groups, "noise_jids": noise_jids}, ensure_ascii=False, indent=1),
encoding="utf-8")
# 5. 测试集(与 raw 不同 jid 命名空间)+ 标准答案
testset, gold = [], {}
tseq = 0
per_arc = 8 # 18 岗位 × 8 = 144 条 >= 120
for a in ARCHETYPES:
years = sorted(a["volumes"])
for _ in range(per_arc):
tseq += 1
year = years[-1] if rng.random() < 0.6 else rng.choice(years)
rec, g = make_jd(a, year, f"T{tseq:05d}")
testset.append(rec)
gold[rec["jid"]] = g
write_jsonl(data / "testset" / "jd_testset.jsonl", testset)
(data / "testset" / "gold_answers.json").write_text(
json.dumps(gold, ensure_ascii=False, indent=1), encoding="utf-8")
# 6. 人岗匹配标注对(正例:技能覆盖当前必备;反例:技能体系错位)
unrelated = {
"后端开发": ["C", "嵌入式开发", "单片机", "RTOS", "STM32"],
"前端": ["C", "单片机", "Verilog", "FPGA", "驱动开发"],
"大数据": ["Verilog", "FPGA", "PCB设计", "单片机", "C"],
"AI算法": ["PCB设计", "Verilog", "FPGA", "Zigbee", "LoRa"],
"数据分析": ["Verilog", "FPGA", "驱动开发", "单片机", "C"],
"测试": ["Verilog", "FPGA", "PCB设计", "LoRa", "Zigbee"],
"运维": ["Verilog", "FPGA", "PCB设计", "传感器", "Zigbee"],
"嵌入式": ["Excel", "SPSS", "Axure", "竞品分析", "用户增长"],
"物联网": ["Excel", "SPSS", "Axure", "竞品分析", "用户增长"],
"云原生": ["Verilog", "FPGA", "PCB设计", "单片机", "传感器"],
"产品": ["Verilog", "FPGA", "驱动开发", "单片机", "C"],
}
# 既有岗位(2023 年前已存在)才进入正式图谱,匹配标注只覆盖这些岗位
known_arcs = [a for a in ARCHETYPES if min(a["volumes"]) < 2023]
match_gold = []
for a in known_arcs:
req, pref = skillset_for(a, 2025)
pos_skills = req[: max(3, len(req) - 1)] + pref[:1]
match_gold.append({
"job_title": a["title"], "skills": pos_skills,
"years": a["years"] + 2, "scenarios": a["scenarios"][:2],
"expected_match": True,
})
neg_scen = [s for s in SCENARIOS if s not in a["scenarios"]][:2]
match_gold.append({
"job_title": a["title"], "skills": unrelated.get(a["category"], ["Excel", "SPSS"]),
"years": 1, "scenarios": neg_scen, "expected_match": False,
})
# 部分覆盖正例(剧本三岗位:缺 1-2 项必备但总体达标)
by_title = {a["title"]: a for a in ARCHETYPES}
for title in ["前端工程师", "数据分析师", "运维工程师(SRE)"]:
a = by_title[title]
req, pref = skillset_for(a, 2025)
match_gold.append({
"job_title": a["title"], "skills": req[:-2] + pref[:2],
"years": a["years"] + 1, "scenarios": a["scenarios"][:2],
"expected_match": True,
})
# 新增简历衍生的标注对(前端完整 / 数据分析部分 / 传统栈错位)
match_gold.append({
"job_title": "前端工程师",
"skills": ["JavaScript", "TypeScript", "HTML", "CSS", "Vue", "React",
"Vite", "Webpack", "Node.js", "小程序开发", "低代码平台", "AI辅助编程"],
"years": 4, "scenarios": ["电商", "社交"], "expected_match": True,
})
match_gold.append({
"job_title": "数据分析师",
"skills": ["SQL", "Excel", "数据分析", "数据可视化", "Tableau", "Python",
"Power BI", "SPSS"],
"years": 2, "scenarios": ["零售", "电商"], "expected_match": True,
})
match_gold.append({
"job_title": "数据分析师",
"skills": ["Struts", "JSP", "Servlet", "Svn", "Tomcat", "jQuery", "WebLogic"],
"years": 8, "scenarios": ["政务", "安防"], "expected_match": False,
})
(data / "testset" / "match_gold.json").write_text(
json.dumps(match_gold, ensure_ascii=False, indent=1), encoding="utf-8")
# 7. 简历样例(完整 / 部分 / 错位,含虚构姓名手机邮箱以验证脱敏)+ 技能标准答案
resumes_dir = data / "resumes"
resumes_dir.mkdir(parents=True, exist_ok=True)
r1 = (
"姓名:张伟\n手机:13812345678\n邮箱:zhangwei@example.com\n"
"5年后端开发经验,先后负责交易系统与支付链路的建设。\n"
"核心技能:熟练掌握Java、Spring Boot、Spring Cloud、MySQL、Redis,"
"熟悉微服务与分布式系统设计,了解Docker、Kafka、Kubernetes与高并发优化。\n"
"项目经历:主导订单中心微服务化改造,基于Spring Cloud与Redis支撑高并发下单;"
"使用Kafka搭建异步消息管道,覆盖电商与金融两大业务场景。\n"
)
r2 = (
"姓名:李娜\n手机:13998765432\n邮箱:lina.dev@example.com\n"
"2年后端开发经验,参与过企业内部管理系统开发。\n"
"核心技能:熟悉Java、Spring Boot、MyBatis、MySQL,了解Redis与Linux常用命令,"
"日常使用Git协作。\n"
"项目经历:负责报表模块开发,使用Spring Boot与MySQL实现数据查询接口。\n"
)
r3 = (
"姓名:王强\n电话:13755556666\n邮箱:wangq@example.com\n"
"4年嵌入式开发经验,服务于智能制造行业。\n"
"核心技能:精通C、C++,熟悉嵌入式开发、单片机、RTOS,"
"掌握STM32与FreeRTOS,了解MQTT协议。\n"
"项目经历:负责工业网关固件开发,完成传感器数据采集与边缘上报。\n"
)
resumes = {
"resume_full.txt": (r1, ["Java", "Spring Boot", "Spring Cloud", "MySQL", "Redis",
"微服务", "分布式系统", "Docker", "Kafka", "Kubernetes", "高并发"]),
"resume_partial.txt": (r2, ["Java", "Spring Boot", "MyBatis", "MySQL", "Redis", "Linux", "Git"]),
"resume_mismatch.txt": (r3, ["C", "C++", "嵌入式开发", "单片机", "RTOS",
"STM32", "FreeRTOS", "MQTT", "传感器"]),
"resume_frontend_full.txt": (
RESUME_FRONTEND_FULL,
["JavaScript", "TypeScript", "HTML", "CSS", "Vue", "React",
"Vite", "Webpack", "Node.js", "小程序开发", "低代码平台", "AI辅助编程"]),
"resume_data_partial.txt": (
RESUME_DATA_PARTIAL,
["SQL", "Excel", "数据分析", "数据可视化", "Tableau", "Python", "Power BI", "SPSS"]),
"resume_llm_full.txt": (
RESUME_LLM_FULL,
["Python", "大模型应用开发", "提示词工程", "LangChain", "RAG",
"向量数据库", "模型微调", "NLP", "FastAPI"]),
"resume_legacy_mismatch.txt": (
RESUME_LEGACY_MISMATCH,
["Struts", "JSP", "Servlet", "Svn", "Tomcat", "jQuery", "WebLogic"]),
}
resume_gold = {}
for name, (text, skills) in resumes.items():
(resumes_dir / name).write_text(text, encoding="utf-8")
resume_gold[name] = {"skills": skills}
(resumes_dir / "gold.json").write_text(
json.dumps(resume_gold, ensure_ascii=False, indent=1), encoding="utf-8")
# 8. 行业趋势报告样例(3 份,内容与 JD 数据中的演化信号相互印证)
reports_dir = data / "reports"
reports_dir.mkdir(parents=True, exist_ok=True)
(reports_dir / "industry_trend_report.txt").write_text(REPORT_TREND, encoding="utf-8")
(reports_dir / "talent_report_2026.txt").write_text(REPORT_TALENT_2026, encoding="utf-8")
(reports_dir / "digital_economy_2026H1.txt").write_text(REPORT_DIGITAL_ECONOMY, encoding="utf-8")
# 9. 审核留痕种子记录(accept/modify/reject 混合 + 节点编辑,覆盖多名操作人)
review_dir = data / "review"
review_dir.mkdir(parents=True, exist_ok=True)
with open(review_dir / "audit.jsonl", "w", encoding="utf-8") as f:
for rec in AUDIT_SEED:
f.write(json.dumps(rec, ensure_ascii=False) + "\n")
n_raw = sum(len(v) for v in raw_by_year.values())
print(f"生成完成:技能词表 {len(SKILLS)} 项,行业 {len(INDUSTRIES)} 个,"
f"原始 JD {n_raw} 条({len(CITIES)} 城),测试集 {len(testset)} 条,"
f"匹配标注 {len(match_gold)} 对,简历 {len(resumes)} 份,报告 3 份,"
f"审核留痕种子 {len(AUDIT_SEED)} 条,"
f"抄袭簇 {len(dup_groups)} 组,噪声 {len(noise_jids)} 条")
if __name__ == "__main__":
main()
+68
View File
@@ -0,0 +1,68 @@
"""端到端冒烟:生成数据(若缺失)→ 跑 pipeline → 打印图谱规模、演化事件数、一个匹配示例。
不需要启动服务,用于快速验证全链路可用。
"""
from __future__ import annotations
import json
import sys
from pathlib import Path
ROOT = Path(__file__).resolve().parents[1]
sys.path.insert(0, str(ROOT))
from app.config import load_config, resolve # noqa: E402
from app.graph.tkg import TemporalKG # noqa: E402
from app.match.engine import MatchEngine # noqa: E402
from app.match.resume import parse_resume # noqa: E402
from app.models import ResumeProfile # noqa: E402
from app.services.pipeline import run_pipeline # noqa: E402
from app.extract.base import load_lexicon # noqa: E402
def main() -> None:
if not (ROOT / "data" / "jd_raw").exists():
print("数据不存在,先生成数据集……")
import subprocess
subprocess.run([sys.executable, str(ROOT / "scripts" / "generate_dataset.py")],
check=True)
print("=" * 56)
print("步骤 1/3:端到端 pipeline(导入→清洗→抽取→图谱→演化→发现)")
print("=" * 56)
stats = run_pipeline(load_config())
print(json.dumps(stats["stages"], ensure_ascii=False, indent=2))
print("\n步骤 2/3:图谱规模与演化事件")
kg = TemporalKG.load(resolve(load_config(), "graph_path"))
print("图谱统计:", kg.stats())
java_id = "job_Java开发工程师"
java_events = kg.events_of(java_id)
print(f"Java开发工程师 演化事件 {len(java_events)} 条,示例:")
for ev in java_events[:4]:
print(" -", ev.description)
snap_2020 = kg.snapshot(java_id, "2020-06-01")
snap_2025 = kg.snapshot(java_id, "2025-06-01")
print("2020 时点技能:", sorted(s["skill"] for s in snap_2020["skills"])[:8])
print("2025 时点技能:", sorted(s["skill"] for s in snap_2025["skills"])[:8])
print("\n步骤 3/3:人岗匹配示例(resume_full.txt vs Java开发工程师)")
text = (ROOT / "data" / "resumes" / "resume_full.txt").read_text(encoding="utf-8")
profile = parse_resume(text)
job = kg.jobs[java_id]
result = MatchEngine(load_lexicon()).match(profile, job)
print(f"脱敏后姓名: {profile.name} 手机: {profile.phone} 邮箱: {profile.email}")
print(f"总分 {result.total_score},维度 {result.dimension_scores},是否匹配 {result.is_match}")
print("缺失必备技能:", result.missing_required)
for stage in result.learning_path:
print(" ", stage["goal"])
# 断言冒烟通过的关键指标
assert kg.stats()["jobs"] >= 15, "岗位节点数量异常"
assert kg.stats()["events"] > 0, "未产生演化事件"
assert result.total_score >= 60, "完整简历应匹配 Java 岗位"
print("\n冒烟测试通过 [OK]")
if __name__ == "__main__":
main()
+104
View File
@@ -0,0 +1,104 @@
"""把 data/graph.json 的节点/边同步到 Neo4j(可选高级组件,MERGE 幂等)。
Neo4j 官方驱动为**可选依赖**,不在 requirements.txt 中:
pip install neo4j # 1. 安装驱动
docker compose --profile advanced up -d neo4j # 2. 启动图库
python scripts/sync_neo4j.py # 3. 同步
python scripts/sync_neo4j.py --dry-run # 只统计,不连接
连接参数:--uri/--user/--password,或环境变量 NEO4J_URI/NEO4J_USER/NEO4J_PASSWORD。
退出码:0 成功;1 连接失败;2 缺少 neo4j 驱动。
"""
from __future__ import annotations
import argparse
import json
import os
import sys
from collections import Counter
from pathlib import Path
ROOT = Path(__file__).resolve().parents[1]
def _props(d: dict, skip: tuple[str, ...]) -> dict:
"""抽取 Neo4j 属性:标量原样保留,列表/dict 序列化为 JSON 字符串(None 不落库)。"""
out: dict = {}
for k, v in d.items():
if k in skip or v is None:
continue
if isinstance(v, (str, int, float, bool)):
out[k] = v
else:
out[k] = json.dumps(v, ensure_ascii=False)
return out
def _safe_ident(value: object, default: str) -> str:
"""标签/关系类型只允许字母数字下划线(防 Cypher 注入)。"""
s = str(value or default)
return s if s.replace("_", "").isalnum() else default
def main() -> int:
ap = argparse.ArgumentParser(description="把 data/graph.json 同步到 Neo4j(MERGE 幂等)")
ap.add_argument("--graph", default=str(ROOT / "data" / "graph.json"), help="图谱 JSON 快照路径")
ap.add_argument("--uri", default=os.environ.get("NEO4J_URI", "bolt://localhost:7687"))
ap.add_argument("--user", default=os.environ.get("NEO4J_USER", "neo4j"))
ap.add_argument("--password", default=os.environ.get("NEO4J_PASSWORD", "careernexus"))
ap.add_argument("--dry-run", action="store_true",
help="只打印将写入的节点/边统计,不连接 Neo4j")
args = ap.parse_args()
graph = json.loads(Path(args.graph).read_text(encoding="utf-8"))
nodes, edges = graph["nodes"], graph["edges"]
by_type = Counter(n.get("type", "Node") for n in nodes)
by_rel = Counter(e.get("rel", "REL") for e in edges)
print(f"图谱文件:{args.graph}")
print(f"节点 {len(nodes)} 个:" + ",".join(f"{t}×{c}" for t, c in sorted(by_type.items())))
print(f"边 {len(edges)} 条:" + ",".join(f"{r}×{c}" for r, c in sorted(by_rel.items())))
if args.dry_run:
print("--dry-run:未连接 Neo4j,以上为将写入的节点/边统计。")
return 0
try:
from neo4j import GraphDatabase
except ImportError:
print("缺少 Neo4j 官方驱动(可选依赖,不在 requirements.txt)。\n"
"请先安装:\n pip install neo4j", file=sys.stderr)
return 2
try:
driver = GraphDatabase.driver(args.uri, auth=(args.user, args.password))
driver.verify_connectivity()
except Exception as exc:
print(f"无法连接 Neo4j({args.uri}):{exc}\n"
"请先启动图库:\n"
" docker compose --profile advanced up -d neo4j", file=sys.stderr)
return 1
with driver, driver.session() as session:
tx = session.begin_transaction()
try:
for n in nodes:
label = _safe_ident(n.get("type"), "Node")
tx.run(f"MERGE (n:{label} {{id: $id}}) SET n += $props",
id=n["id"], props=_props(n, ("id",)))
for e in edges:
rel = _safe_ident(e.get("rel"), "REL")
# REQUIRES 边的 intervals/sources/importance 等属性随边写入
tx.run(f"MATCH (a {{id: $s}}), (b {{id: $t}}) "
f"MERGE (a)-[r:{rel}]->(b) SET r += $props",
s=e["source"], t=e["target"],
props=_props(e, ("source", "target", "rel")))
tx.commit()
except Exception:
tx.rollback()
raise
print(f"同步完成:节点 {len(nodes)} 个、边 {len(edges)} 条已 MERGE 写入 {args.uri}。")
return 0
if __name__ == "__main__":
sys.exit(main())