Files
RoyirinRosmontis a10442d78c CareerNexus 职联:岗位能力图谱构建与动态演化分析平台(挑战杯 XH-202621)
- 全链路:多源JD接入/清洗交叉验证/结构化抽取(星火+离线双通道)/时序知识图谱/新岗位发现/演化分析/人岗匹配/学习路径
- WebUI 五视图:驾驶舱/图谱工作台/岗位演化/匹配(单人+批量)/学习路径
- 设置中心(config.json)/Token鉴权/SQLite状态库/语义向量通道(BGE-M3本地或联网API)
- 71 个 pytest 用例;数据目录仅保留结构(.gitkeep),数据由 scripts/generate_dataset.py 复现
2026-08-03 13:15:35 +08:00

132 lines
5.2 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""抽取层测试:Schema 校验、坏 JSON 重试与降级、多源投票、离线抽取与别名归一。"""
from __future__ import annotations
import pytest
from pydantic import ValidationError
from app.extract.base import SkillMatcher, load_lexicon, vote_field
from app.extract.llm import LLMExtractor
from app.extract.offline import OfflineExtractor
from app.models import ExtractionResult, JDRecord
SAMPLE_TEXT = """岗位名称:Java开发工程师
某科技公司诚邀优秀人才加入。
岗位职责:
1、负责后端服务的设计与开发;
2、参与核心系统的性能优化;
任职要求:
1、3年以上相关工作经验;
2、熟练掌握Java、Spring Boot、MySQL、Redis;
3、具有电商或金融行业经验者优先;
加分项:
1、了解Docker、Kubernetes者优先;
"""
def _record(text: str = SAMPLE_TEXT) -> JDRecord:
return JDRecord(jid="X1", job_title="Java开发工程师", company="某科技",
publish_date="2024-05-01", text=text)
def test_offline_extract_fields_and_evidence():
ext = OfflineExtractor(load_lexicon())
r = ext.extract(_record())
assert r.job_title == "Java开发工程师"
assert set(r.required_skills) == {"Java", "Spring Boot", "MySQL", "Redis"}
assert set(r.preferred_skills) == {"Docker", "Kubernetes"}
assert r.responsibilities == ["负责后端服务的设计与开发;", "参与核心系统的性能优化;"]
assert set(r.scenarios) == {"电商", "金融"}
assert r.evidence["required_skills"] # 每字段附证据片段
assert r.confidence == 1.0
def test_alias_normalization():
matcher = SkillMatcher(load_lexicon())
text = "要求LLM应用开发经验,熟悉K8s与SpringBoot,了解检索增强生成"
names = matcher.find_names(text)
assert "大模型应用开发" in names # LLM应用开发 归一化
assert "Kubernetes" in names # K8s 归一化
assert "Spring Boot" in names # SpringBoot 归一化
assert "RAG" in names # 检索增强生成 归一化
def test_tail_section_not_mixed_into_preferred():
"""尾部“团队与福利/福利待遇”段落不属于加分区:其中的技能词不得混入加分技能。"""
text = SAMPLE_TEXT + "团队与福利:\n团队标配Kubernetes与Terraform,提供外部培训预算。\n"
ext = OfflineExtractor(load_lexicon())
r = ext.extract(_record(text))
assert set(r.preferred_skills) == {"Docker", "Kubernetes"}
assert "Terraform" not in r.preferred_skills
def test_java_not_confused_with_javascript():
matcher = SkillMatcher(load_lexicon())
names = matcher.find_names("精通JavaScript与Vue,不写Java后端")
assert "JavaScript" in names
assert "Java" in names # “Java后端”中的 Java 是独立词
only_js = matcher.find_names("精通JavaScript与Vue")
assert "Java" not in only_js
def test_schema_validation_rejects_bad_payload():
with pytest.raises(ValidationError):
ExtractionResult(job_title="X", required_skills="不是列表")
with pytest.raises(ValidationError):
ExtractionResult(job_title="X", confidence="不是数字")
def _make_llm() -> LLMExtractor:
return LLMExtractor(api_key="fake", base_url="http://localhost:1", model="m",
timeout=0.1, fallback=OfflineExtractor(load_lexicon()))
def test_llm_bad_json_retries_then_success(monkeypatch):
"""坏 JSON 触发重试,第 3 次返回合法 JSON 则成功。"""
llm = _make_llm()
calls = {"n": 0}
good = ('{"job_title": "Java开发工程师", "responsibilities": [], '
'"required_skills": ["Java"], "preferred_skills": [], "scenarios": []}')
def fake_call(text):
calls["n"] += 1
return "{坏JSON" if calls["n"] < 3 else good
monkeypatch.setattr(llm, "_call_api", fake_call)
r = llm.extract(_record())
assert calls["n"] == 3
assert r.extractor == "llm"
assert r.required_skills == ["Java"]
def test_llm_all_fail_fallback_offline(monkeypatch):
"""重试耗尽(含超时/坏 JSON)降级到离线抽取,链路不断。"""
llm = _make_llm()
monkeypatch.setattr(llm, "_call_api", lambda text: (_ for _ in ()).throw(TimeoutError("超时")))
r = llm.extract(_record())
assert r.extractor == "offline"
assert "Java" in r.required_skills
def test_llm_schema_invalid_retries(monkeypatch):
"""返回 JSON 合法但不符合 Schema(类型错误)同样重试并降级。"""
llm = _make_llm()
monkeypatch.setattr(llm, "_call_api",
lambda text: '{"job_title": 123, "required_skills": "wrong"}')
r = llm.extract(_record())
assert r.extractor == "offline"
def test_vote_field_requires_agreement():
"""关键字段多源一致才入选:A 三源全票,B 两票,C 一票出局。"""
chosen, conf = vote_field([
(["A", "B", "C"], 1.0),
(["A", "B"], 1.0),
(["A"], 1.0),
], min_agreement=0.5)
assert chosen == ["A", "B"]
assert conf == 1.0
chosen2, _ = vote_field([(["A"], 1.0), (["B"], 1.0)], min_agreement=0.5)
assert chosen2 == ["A", "B"] # 平票 0.5 恰好达到门槛
chosen3, conf3 = vote_field([(["A"], 1.0), (["B"], 1.0), (["C"], 1.0)], min_agreement=0.5)
assert chosen3 == []