offer-catcher-agent-v2 / scripts /test_evidence.py
hungryb's picture
v2: agent report + filtered corpus + evidence contract
54b5b64 verified
Raw
History Blame Contribute Delete
12.4 kB
"""
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
# 加入项目根目录到 sys.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}")
# confidence 必须是 high/medium/low 之一
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] # 取第一个 builtin job
# 对 job 做完整打分(会附加 evidence)
scored = score_job(job, profile, SAMPLE_RESUME, "大模型应用算法", "深圳", "实习")
# 检查 evidence 字段存在
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']}")
# 检查每条 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)
# 解析用户粘贴的 JD
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", []))
# 对 job 做完整打分
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] # 只取前 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, "大模型应用算法", "深圳", "实习")
# 检查 evidence 中的 claim/evidence 是否非空且有实质内容
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()