| """ |
| test_evidence.py — 证据链模块测试脚本 |
| |
| 测试目标: |
| 1. builtin job 能生成 evidence |
| 2. user pasted JD 能生成 evidence |
| 3. evidence 字段存在且非空 |
| 4. 每条 evidence 都有 type/claim/evidence/source/confidence |
| """ |
|
|
| from __future__ import annotations |
|
|
| import json |
| import sys |
| from pathlib import Path |
|
|
| |
| ROOT = Path(__file__).resolve().parent.parent |
| sys.path.insert(0, str(ROOT)) |
|
|
| from src.resume_parser import parse_resume |
| from src.jd_parser import parse_jd |
| from src.matcher import load_jobs, score_job, rank_job_list |
| from src.evidence import ( |
| build_jd_evidence, |
| build_resume_evidence, |
| build_gap_evidence, |
| build_action_evidence, |
| attach_evidence, |
| attach_evidence_to_jobs, |
| ) |
|
|
|
|
| |
| |
| |
|
|
| SAMPLE_RESUME = """张同学 | 计算机科学与技术 | 2026 届硕士 |
| 求职方向:大模型应用算法 / 推荐算法实习生,期望城市深圳或北京。 |
| 技能:Python、PyTorch、Transformer、RAG、Agent、Embedding、Faiss、LangChain、推荐系统、召回排序、NDCG、A/B Test、SQL。 |
| 项目经历: |
| 1. GenAdRec 生成式广告推荐项目:基于 Transformer 建模用户行为序列,将广告候选集转化为生成式 likelihood rerank 问题;构建 Semantic ID 表示广告 item,结合多兴趣召回提升 NDCG@10。 |
| 2. LLM 求职助手 Demo:使用 DeepSeek API 和 bge embedding 实现 JD 检索、简历关键词诊断、Prompt 模板优化,支持输出岗位匹配解释。 |
| 3. MIND 多兴趣推荐复现:复现 capsule routing 用户多兴趣建模,在公开数据集上对比召回 HitRate 与 NDCG。 |
| 实习经历: |
| 曾参与推荐系统离线评估脚本开发,负责样本构造、特征清洗和模型结果分析。 |
| 补充:希望找能结合 LLM、Agent、RAG 和推荐排序的算法岗位。""" |
|
|
| SAMPLE_JD = """职位名称:大模型应用算法实习生 |
| 公司:腾讯 |
| 城市:深圳 |
| 方向:大模型应用算法 |
| 阶段:实习 |
| 技能要求:LLM、RAG、Agent、Prompt Engineering、LangChain、Embedding、Faiss、Python |
| 项目信号:RAG、Agent、Semantic Retrieval、Multi-turn Dialogue |
| JD 描述: |
| 负责大模型应用算法研发,包括 RAG 系统搭建、Agent 工作流设计、Prompt 模板优化。 |
| 要求熟悉 LLM 应用开发,了解 RAG 技术栈(Embedding + Vector DB),有 Agent 框架使用经验者优先。 |
| """ |
|
|
|
|
| |
| |
| |
|
|
| def _check_evidence_fields(ev: dict, context: str) -> list[str]: |
| """检查单条 evidence 是否包含所有必填字段。返回错误列表。""" |
| errors = [] |
| required_fields = ["type", "claim", "evidence", "source", "confidence"] |
| for field in required_fields: |
| if field not in ev: |
| errors.append(f"{context}: 缺少字段 {field}") |
| |
| if "confidence" in ev and ev["confidence"] not in ("high", "medium", "low"): |
| errors.append(f"{context}: confidence={ev['confidence']} 不在允许范围内") |
| return errors |
|
|
|
|
| def _print_evidence_list(label: str, ev_list: list[dict]) -> None: |
| """打印 evidence 列表,方便人工检查。""" |
| print(f"\n 【{label}】(共 {len(ev_list)} 条)") |
| for i, ev in enumerate(ev_list): |
| print(f" [{i+1}] type={ev.get('type','?')} | confidence={ev.get('confidence','?')}") |
| print(f" claim: {ev.get('claim','')}") |
| print(f" evidence: {ev.get('evidence','')}") |
| print(f" source: {ev.get('source','')}") |
|
|
|
|
| |
| |
| |
|
|
| def test_01_builtin_job_evidence() -> bool: |
| """测试 1:builtin job 能生成 evidence。""" |
| print("\n=== test_01_builtin_job_evidence ===") |
| profile = parse_resume(SAMPLE_RESUME) |
| jobs = load_jobs(ROOT / "data" / "jobs.json") |
| job = jobs[0] |
|
|
| |
| scored = score_job(job, profile, SAMPLE_RESUME, "大模型应用算法", "深圳", "实习") |
|
|
| |
| required_evidence_fields = ["jd_evidence", "resume_evidence", "gap_evidence", "action_evidence"] |
| errors = [] |
| for field in required_evidence_fields: |
| if field not in scored: |
| errors.append(f"scored 缺少字段 {field}") |
| elif not scored[field]: |
| errors.append(f"scored[{field}] 为空") |
|
|
| if errors: |
| print(" [FAIL] " + "; ".join(errors)) |
| return False |
|
|
| _print_evidence_list("jd_evidence", scored["jd_evidence"]) |
| _print_evidence_list("resume_evidence", scored["resume_evidence"]) |
| _print_evidence_list("gap_evidence", scored["gap_evidence"]) |
| print(f" action_evidence: {scored['action_evidence']}") |
|
|
| |
| all_errors = [] |
| for ev in scored["jd_evidence"]: |
| all_errors.extend(_check_evidence_fields(ev, "jd_evidence")) |
| for ev in scored["resume_evidence"]: |
| all_errors.extend(_check_evidence_fields(ev, "resume_evidence")) |
| for ev in scored["gap_evidence"]: |
| all_errors.extend(_check_evidence_fields(ev, "gap_evidence")) |
|
|
| if all_errors: |
| print(" [FAIL] " + "; ".join(all_errors)) |
| return False |
|
|
| print(" [PASS] builtin job 证据链生成正常") |
| return True |
|
|
|
|
| def test_02_user_pasted_jd_evidence() -> bool: |
| """测试 2:user pasted JD 能生成 evidence。""" |
| print("\n=== test_02_user_pasted_jd_evidence ===") |
| profile = parse_resume(SAMPLE_RESUME) |
|
|
| |
| from src.jd_parser import parse_jd |
| jd_result = parse_jd(SAMPLE_JD) |
| if not jd_result: |
| print(" [SKIP] parse_jd 返回空,可能是规则解析失败(非错误)") |
| return True |
|
|
| job = jd_result |
| job.setdefault("source", "user_pasted") |
| job.setdefault("stage", "实习") |
| job.setdefault("city", "深圳") |
| job.setdefault("direction", "大模型应用算法") |
| job.setdefault("company", "测试公司") |
| job.setdefault("title", "大模型应用算法实习生") |
| job.setdefault("skills", job.get("skills", [])) |
| job.setdefault("project_signals", job.get("project_signals", [])) |
|
|
| |
| scored = score_job(job, profile, SAMPLE_RESUME, "大模型应用算法", "深圳", "实习") |
|
|
| required_evidence_fields = ["jd_evidence", "resume_evidence", "gap_evidence", "action_evidence"] |
| errors = [] |
| for field in required_evidence_fields: |
| if field not in scored: |
| errors.append(f"scored 缺少字段 {field}") |
|
|
| if errors: |
| print(" [FAIL] " + "; ".join(errors)) |
| return False |
|
|
| _print_evidence_list("jd_evidence (user JD)", scored["jd_evidence"]) |
| _print_evidence_list("resume_evidence (user JD)", scored["resume_evidence"]) |
| _print_evidence_list("gap_evidence (user JD)", scored["gap_evidence"]) |
|
|
| print(" [PASS] user pasted JD 证据链生成正常") |
| return True |
|
|
|
|
| def test_03_evidence_fields_complete() -> bool: |
| """测试 3:每条 evidence 都有 type/claim/evidence/source/confidence。""" |
| print("\n=== test_03_evidence_fields_complete ===") |
| profile = parse_resume(SAMPLE_RESUME) |
| jobs = load_jobs(ROOT / "data" / "jobs.json") |
| job = jobs[0] |
|
|
| scored = score_job(job, profile, SAMPLE_RESUME, "大模型应用算法", "深圳", "实习") |
|
|
| all_errors = [] |
| for ev in scored.get("jd_evidence", []): |
| all_errors.extend(_check_evidence_fields(ev, "jd_evidence")) |
| for ev in scored.get("resume_evidence", []): |
| all_errors.extend(_check_evidence_fields(ev, "resume_evidence")) |
| for ev in scored.get("gap_evidence", []): |
| all_errors.extend(_check_evidence_fields(ev, "gap_evidence")) |
|
|
| if all_errors: |
| print(" [FAIL] " + "; ".join(all_errors[:5])) |
| return False |
|
|
| print(" [PASS] 所有 evidence 字段完整") |
| return True |
|
|
|
|
| def test_04_action_evidence_format() -> bool: |
| """测试 4:action_evidence 是 list[str],且内容非空。""" |
| print("\n=== test_04_action_evidence_format ===") |
| profile = parse_resume(SAMPLE_RESUME) |
| jobs = load_jobs(ROOT / "data" / "jobs.json") |
| job = jobs[0] |
|
|
| scored = score_job(job, profile, SAMPLE_RESUME, "大模型应用算法", "深圳", "实习") |
|
|
| action_ev = scored.get("action_evidence", []) |
| if not isinstance(action_ev, list): |
| print(f" [FAIL] action_evidence 类型错误:{type(action_ev)}") |
| return False |
| if not action_ev: |
| print(" [FAIL] action_evidence 为空") |
| return False |
| for i, item in enumerate(action_ev): |
| if not isinstance(item, str): |
| print(f" [FAIL] action_evidence[{i}] 不是 str:{type(item)}") |
| return False |
| if not item.strip(): |
| print(f" [FAIL] action_evidence[{i}] 为空字符串") |
| return False |
|
|
| print(f" [PASS] action_evidence 格式正确({len(action_ev)} 条)") |
| return True |
|
|
|
|
| def test_05_attach_evidence_to_jobs() -> bool: |
| """测试 5:attach_evidence_to_jobs 能批量处理 job list。""" |
| print("\n=== test_05_attach_evidence_to_jobs ===") |
| profile = parse_resume(SAMPLE_RESUME) |
| jobs = load_jobs(ROOT / "data" / "jobs.json")[:3] |
|
|
| scored_list = attach_evidence_to_jobs(jobs, SAMPLE_RESUME, profile) |
|
|
| errors = [] |
| for i, job in enumerate(scored_list): |
| for field in ["jd_evidence", "resume_evidence", "gap_evidence", "action_evidence"]: |
| if field not in job: |
| errors.append(f"job[{i}] 缺少 {field}") |
|
|
| if errors: |
| print(" [FAIL] " + "; ".join(errors)) |
| return False |
|
|
| print(f" [PASS] attach_evidence_to_jobs 批量处理 {len(scored_list)} 个岗位正常") |
| return True |
|
|
|
|
| def test_06_evidence_grounded() -> bool: |
| """测试 6:evidence 是 grounded 的(不能写空泛建议)。""" |
| print("\n=== test_06_evidence_grounded ===") |
| profile = parse_resume(SAMPLE_RESUME) |
| jobs = load_jobs(ROOT / "data" / "jobs.json") |
| job = jobs[0] |
|
|
| scored = score_job(job, profile, SAMPLE_RESUME, "大模型应用算法", "深圳", "实习") |
|
|
| |
| all_errors = [] |
| for ev in scored.get("jd_evidence", []): |
| if not ev.get("claim", "").strip(): |
| all_errors.append("jd_evidence 中有空的 claim") |
| if not ev.get("evidence", "").strip(): |
| all_errors.append("jd_evidence 中有空的 evidence") |
|
|
| if all_errors: |
| print(" [FAIL] " + "; ".join(all_errors)) |
| return False |
|
|
| print(" [PASS] evidence 内容 grounded(非空泛建议)") |
| return True |
|
|
|
|
| |
| |
| |
|
|
| def main() -> None: |
| print("=" * 60) |
| print("证据链模块测试开始") |
| print("=" * 60) |
|
|
| results = [] |
| results.append(("builtin job 生成 evidence", test_01_builtin_job_evidence())) |
| results.append(("user pasted JD 生成 evidence", test_02_user_pasted_jd_evidence())) |
| results.append(("evidence 字段完整", test_03_evidence_fields_complete())) |
| results.append(("action_evidence 格式正确", test_04_action_evidence_format())) |
| results.append(("attach_evidence_to_jobs 批量处理", test_05_attach_evidence_to_jobs())) |
| results.append(("evidence grounded", test_06_evidence_grounded())) |
|
|
| print("\n" + "=" * 60) |
| print("测试结果汇总") |
| print("=" * 60) |
| pass_count = 0 |
| for name, result in results: |
| status = "[PASS]" if result else "[FAIL]" |
| print(f" {status} {name}") |
| if result: |
| pass_count += 1 |
| print(f"\n总计:{pass_count}/{len(results)} 通过") |
| if pass_count == len(results): |
| print("[OK] 所有测试通过!") |
| else: |
| print("[WARN] 有测试失败,请检查 above。") |
|
|
|
|
| if __name__ == "__main__": |
| main() |
|
|