Skip to content
Projects
Groups
Snippets
Help
Loading...
Help
Contribute to GitLab
Sign in
Toggle navigation
R
recruit-sys
Project
Project
Details
Activity
Cycle Analytics
Repository
Repository
Files
Commits
Branches
Tags
Contributors
Graph
Compare
Charts
Issues
0
Issues
0
List
Board
Labels
Milestones
Merge Requests
0
Merge Requests
0
CI / CD
CI / CD
Pipelines
Jobs
Schedules
Charts
Wiki
Wiki
Snippets
Snippets
Members
Members
Collapse sidebar
Close sidebar
Activity
Graph
Charts
Create a new issue
Jobs
Commits
Issue Boards
Open sidebar
李文光
recruit-sys
Commits
fc0aa5ce
Commit
fc0aa5ce
authored
Sep 02, 2026
by
李文光
Browse files
Options
Browse Files
Download
Email Patches
Plain Diff
feat: 简历解析去掉脱敏,原文直接交大模型提取全部字段
parent
6248f275
Hide whitespace changes
Inline
Side-by-side
Showing
4 changed files
with
64 additions
and
88 deletions
+64
-88
llm.py
backend/app/services/llm.py
+14
-15
resume_parser.py
backend/app/services/resume_parser.py
+15
-19
resume_parser_core.py
backend/app/services/resume_parser_core.py
+6
-8
test_resume_parser_llm.py
backend/tests/test_resume_parser_llm.py
+29
-46
No files found.
backend/app/services/llm.py
View file @
fc0aa5ce
...
@@ -218,8 +218,8 @@ def sanitize_candidate_for_llm(candidate: dict[str, Any]) -> dict[str, Any]:
...
@@ -218,8 +218,8 @@ def sanitize_candidate_for_llm(candidate: dict[str, Any]) -> dict[str, Any]:
# 简历结构化提取:LLM 返回的字段集合(与 llm_resume_fields 的 prompt 保持一致)。
# 简历结构化提取:LLM 返回的字段集合(与 llm_resume_fields 的 prompt 保持一致)。
#
其中姓名/手机/邮箱/出生年月/年龄/城市等 PII 会被本地脱敏,LLM 无法看到真实值
,
#
该路径不做本地脱敏:简历原文(含姓名/手机/邮箱等 PII)直接交给大模型提取全部字段
,
#
这些字段始终以正则预扫描结果为准(见 resume_parser_core.extract_pii_fields)
。
#
大模型不可用或提取失败时由 resume_parser 回退纯正则解析
。
LLM_RESUME_FIELDS
=
(
LLM_RESUME_FIELDS
=
(
"name"
,
"name"
,
"jobTitle"
,
"jobTitle"
,
...
@@ -236,28 +236,27 @@ LLM_RESUME_FIELDS = (
...
@@ -236,28 +236,27 @@ LLM_RESUME_FIELDS = (
)
)
async
def
llm_resume_fields
(
text
:
str
,
hints
:
dict
[
str
,
Any
]
|
None
=
None
)
->
dict
[
str
,
Any
]
|
None
:
async
def
llm_resume_fields
(
text
:
str
)
->
dict
[
str
,
Any
]
|
None
:
"""用大模型从简历文本中提取结构化字段;未配置 Key 或调用失败时返回 None(调用方回退正则)。
"""用大模型
直接
从简历文本中提取结构化字段;未配置 Key 或调用失败时返回 None(调用方回退正则)。
发送前用正则预扫描结果对姓名/手机/邮箱/城市/出生年月/年龄做本地脱敏
,
注意:本路径不做本地脱敏,简历原文(含候选人 PII)会原样发送给大模型
,
被脱敏的字段 LLM 一律返回空值,最终以正则预扫描结果为准
。
调用方需确保大模型环境可信;如需脱敏,请改用 analyze_resume 的脱敏链路
。
"""
"""
settings
=
get_settings
()
settings
=
get_settings
()
if
not
settings
.
effective_llm_api_key
:
if
not
settings
.
effective_llm_api_key
:
return
None
return
None
sanitized
=
sanitize_resume_for_llm
(
text
,
hints
or
{})
if
not
(
text
or
""
)
.
strip
():
if
not
sanitized
.
strip
():
return
None
return
None
user
=
(
user
=
(
"请从下面的简历文本中提取结构化字段,只返回一个 JSON 对象(不要 Markdown 围栏,不要多余解释)。
\n\n
"
"请从下面的简历文本中提取结构化字段,只返回一个 JSON 对象(不要 Markdown 围栏,不要多余解释)。
\n\n
"
"字段与规则:
\n
"
"字段与规则:
\n
"
'- name: 姓名(2-4 个中文字符
)。姓名已被 [本地脱敏] 占位符替换时返回空字符串
""。
\n
'
'- name: 姓名(2-4 个中文字符
,如"薛庆霞");没有则返回
""。
\n
'
'- jobTitle: 求职意向/应聘岗位,只能是简历中明确写出的岗位名(如"销售支持专员");没有明确写出时返回 "",'
'- jobTitle: 求职意向/应聘岗位,只能是简历中明确写出的岗位名(如"销售支持专员");没有明确写出时返回 "",'
"不要根据教育/技能内容推断岗位。
\n
"
"不要根据教育/技能内容推断岗位。
\n
"
'- phone: 手机号
。已被 [手机号已本地脱敏] 替换或没有时
返回 ""。
\n
'
'- phone: 手机号
(11 位,可含 +86/空格/连字符,如 13812345678);没有则
返回 ""。
\n
'
'- email: 邮箱
。已被 [邮箱已本地脱敏] 替换或没有时
返回 ""。
\n
'
'- email: 邮箱
;没有则
返回 ""。
\n
'
'- birthDate: 出生日期,格式 YYYY-MM-DD
。已被 [出生年月已本地脱敏] 替换或没有时
返回 ""。
\n
'
'- birthDate: 出生日期,格式 YYYY-MM-DD
;没有则
返回 ""。
\n
'
'- age: 年龄数字(如 28)
。已被 [年龄已本地脱敏] 替换或没有时
返回 ""。
\n
'
'- age: 年龄数字(如 28)
;没有则
返回 ""。
\n
'
'- years: 工作年限,如 "5年" / "3.5年" / "应届生";简历没有明确写工作年限时返回 "",不要从无关时间推算。
\n
'
'- years: 工作年限,如 "5年" / "3.5年" / "应届生";简历没有明确写工作年限时返回 "",不要从无关时间推算。
\n
'
"- education: 学历,只能是 博士/硕士/本科/大专/中专/高中/其他 之一;没有明确信息返回
\"\"
。
\n
"
"- education: 学历,只能是 博士/硕士/本科/大专/中专/高中/其他 之一;没有明确信息返回
\"\"
。
\n
"
'- school: 毕业院校全称(如"甘肃农业大学");没有则返回 ""。
\n
'
'- school: 毕业院校全称(如"甘肃农业大学");没有则返回 ""。
\n
'
...
@@ -265,8 +264,8 @@ async def llm_resume_fields(text: str, hints: dict[str, Any] | None = None) -> d
...
@@ -265,8 +264,8 @@ async def llm_resume_fields(text: str, hints: dict[str, Any] | None = None) -> d
'独立专业名;没有明确专业返回 ""。
\n
'
'独立专业名;没有明确专业返回 ""。
\n
'
'- city: 现居城市或简历中的城市名(如"太原",不含"市");没有则返回 ""。
\n
'
'- city: 现居城市或简历中的城市名(如"太原",不含"市");没有则返回 ""。
\n
'
"- skills: 技能数组,最多 12 项,只取简历中明确提到的技能。
\n\n
"
"- skills: 技能数组,最多 12 项,只取简历中明确提到的技能。
\n\n
"
"要求:只提取简历中明确出现的信息,绝不编造或猜测
;已被脱敏占位符替换的字段一律返回空字符串/空数组
。
\n\n
"
"要求:只提取简历中明确出现的信息,绝不编造或猜测。
\n\n
"
f
"简历文本:
\n
{
sanitized
[:16000]}"
f
"简历文本:
\n
{
(text or '')
[:16000]}"
)
)
try
:
try
:
parsed
=
await
_chat_json
(
parsed
=
await
_chat_json
(
...
...
backend/app/services/resume_parser.py
View file @
fc0aa5ce
...
@@ -4,7 +4,6 @@ from tempfile import TemporaryDirectory
...
@@ -4,7 +4,6 @@ from tempfile import TemporaryDirectory
from
backend.app.services.file_storage
import
sanitize_filename
from
backend.app.services.file_storage
import
sanitize_filename
from
backend.app.services.llm
import
LLM_RESUME_FIELDS
,
llm_resume_fields
from
backend.app.services.llm
import
LLM_RESUME_FIELDS
,
llm_resume_fields
from
backend.app.services.resume_parser_core
import
(
from
backend.app.services.resume_parser_core
import
(
extract_pii_fields
,
infer_source
,
infer_source
,
parse_resume
,
parse_resume
,
parse_resume_text
,
parse_resume_text
,
...
@@ -12,11 +11,6 @@ from backend.app.services.resume_parser_core import (
...
@@ -12,11 +11,6 @@ from backend.app.services.resume_parser_core import (
school_tags
,
school_tags
,
)
)
# LLM 看不到真实值的 PII 字段:发送前已本地脱敏,始终以正则预扫描结果为准。
_PII_KEYS
=
{
"name"
,
"phone"
,
"email"
,
"birthDate"
,
"age"
,
"city"
,
"resumeName"
}
# 交给大模型提取的内容字段(大模型非空值优先采用,正则仅作整体兜底)。
_LLM_CONTENT_KEYS
=
tuple
(
key
for
key
in
LLM_RESUME_FIELDS
if
key
not
in
_PII_KEYS
)
def
parse_resume_path
(
path
:
Path
)
->
dict
:
def
parse_resume_path
(
path
:
Path
)
->
dict
:
"""纯正则解析入口(无 LLM 时的兜底/调试路径)。"""
"""纯正则解析入口(无 LLM 时的兜底/调试路径)。"""
...
@@ -33,19 +27,15 @@ def parse_uploaded_bytes(content: bytes, original_name: str = "resume.bin") -> d
...
@@ -33,19 +27,15 @@ def parse_uploaded_bytes(content: bytes, original_name: str = "resume.bin") -> d
async
def
parse_resume_path_async
(
path
:
Path
)
->
dict
:
async
def
parse_resume_path_async
(
path
:
Path
)
->
dict
:
"""LLM 优先、正则兜底的简历解析入口(API/CLI 均走这里)。
"""LLM 优先、正则兜底的简历解析入口(API/CLI 均走这里)。
读取简历文本后,先把内容交给大模型理解并提取结构化字段;只有大模型不可用
读取简历文本后直接把原文交给大模型理解并提取全部结构化字段(不做本地脱敏);
或提取失败时才整体回退到纯正则解析(parse_resume_text)。姓名/手机/邮箱/
只有大模型不可用或提取失败时才整体回退到纯正则解析(parse_resume_text)。
出生年月/年龄/城市等 PII 发往大模型前必须先本地脱敏(见
llm.sanitize_resume_for_llm),因此这类字段始终以 extract_pii_fields 的
正则预扫描结果为准,不采用大模型返回值。
"""
"""
text
=
read_resume_text
(
path
)
text
=
read_resume_text
(
path
)
filename
=
path
.
name
filename
=
path
.
name
pii
=
extract_pii_fields
(
text
,
filename
)
llm_result
=
await
llm_resume_fields
(
text
)
llm_result
=
await
llm_resume_fields
(
text
,
pii
)
if
not
_has_llm_content
(
llm_result
):
if
not
_has_llm_content
(
llm_result
):
return
parse_resume_text
(
text
,
filename
)
return
parse_resume_text
(
text
,
filename
)
return
_compose_llm_result
(
text
,
filename
,
pii
,
llm_result
)
return
_compose_llm_result
(
text
,
filename
,
llm_result
)
async
def
parse_uploaded_bytes_async
(
content
:
bytes
,
original_name
:
str
=
"resume.bin"
)
->
dict
:
async
def
parse_uploaded_bytes_async
(
content
:
bytes
,
original_name
:
str
=
"resume.bin"
)
->
dict
:
...
@@ -56,10 +46,10 @@ async def parse_uploaded_bytes_async(content: bytes, original_name: str = "resum
...
@@ -56,10 +46,10 @@ async def parse_uploaded_bytes_async(content: bytes, original_name: str = "resum
def
_has_llm_content
(
result
:
dict
|
None
)
->
bool
:
def
_has_llm_content
(
result
:
dict
|
None
)
->
bool
:
"""大模型是否
提取到可用内容
字段;返回空字典/全空时视为提取失败,回退正则。"""
"""大模型是否
返回了可用
字段;返回空字典/全空时视为提取失败,回退正则。"""
if
not
isinstance
(
result
,
dict
):
if
not
isinstance
(
result
,
dict
):
return
False
return
False
for
key
in
_LLM_CONTENT_KEY
S
:
for
key
in
LLM_RESUME_FIELD
S
:
value
=
result
.
get
(
key
)
value
=
result
.
get
(
key
)
if
isinstance
(
value
,
list
):
if
isinstance
(
value
,
list
):
if
any
(
str
(
item
)
.
strip
()
for
item
in
value
):
if
any
(
str
(
item
)
.
strip
()
for
item
in
value
):
...
@@ -77,8 +67,8 @@ def _clean_text_value(value: object) -> str:
...
@@ -77,8 +67,8 @@ def _clean_text_value(value: object) -> str:
return
str
(
value
)
.
strip
()
return
str
(
value
)
.
strip
()
def
_compose_llm_result
(
text
:
str
,
filename
:
str
,
pii
:
dict
,
llm_result
:
dict
)
->
dict
:
def
_compose_llm_result
(
text
:
str
,
filename
:
str
,
llm_result
:
dict
)
->
dict
:
"""大模型提取成功时的结果组装:
PII 取正则预扫描值,其余内容字段取大模型
值。"""
"""大模型提取成功时的结果组装:
结构化字段全部采用大模型返回
值。"""
school
=
_clean_text_value
(
llm_result
.
get
(
"school"
))
school
=
_clean_text_value
(
llm_result
.
get
(
"school"
))
skills
=
[
skills
=
[
str
(
item
)
.
strip
()
str
(
item
)
.
strip
()
...
@@ -86,15 +76,21 @@ def _compose_llm_result(text: str, filename: str, pii: dict, llm_result: dict) -
...
@@ -86,15 +76,21 @@ def _compose_llm_result(text: str, filename: str, pii: dict, llm_result: dict) -
if
str
(
item
)
.
strip
()
if
str
(
item
)
.
strip
()
]
]
return
{
return
{
**
pii
,
"name"
:
_clean_text_value
(
llm_result
.
get
(
"name"
))
,
"jobTitle"
:
_clean_text_value
(
llm_result
.
get
(
"jobTitle"
)),
"jobTitle"
:
_clean_text_value
(
llm_result
.
get
(
"jobTitle"
)),
"phone"
:
_clean_text_value
(
llm_result
.
get
(
"phone"
)),
"email"
:
_clean_text_value
(
llm_result
.
get
(
"email"
)),
"birthDate"
:
_clean_text_value
(
llm_result
.
get
(
"birthDate"
)),
"age"
:
_clean_text_value
(
llm_result
.
get
(
"age"
)),
"years"
:
_clean_text_value
(
llm_result
.
get
(
"years"
)),
"years"
:
_clean_text_value
(
llm_result
.
get
(
"years"
)),
"education"
:
_clean_text_value
(
llm_result
.
get
(
"education"
)),
"education"
:
_clean_text_value
(
llm_result
.
get
(
"education"
)),
"school"
:
school
,
"school"
:
school
,
"major"
:
_clean_text_value
(
llm_result
.
get
(
"major"
)),
"major"
:
_clean_text_value
(
llm_result
.
get
(
"major"
)),
"schoolTags"
:
school_tags
(
school
),
"schoolTags"
:
school_tags
(
school
),
"city"
:
_clean_text_value
(
llm_result
.
get
(
"city"
)),
"skills"
:
list
(
dict
.
fromkeys
(
skills
)),
"skills"
:
list
(
dict
.
fromkeys
(
skills
)),
"source"
:
infer_source
(
text
,
filename
),
"source"
:
infer_source
(
text
,
filename
),
"resumeText"
:
text
,
"resumeText"
:
text
,
"resumeName"
:
filename
,
"textLength"
:
len
(
text
),
"textLength"
:
len
(
text
),
}
}
backend/app/services/resume_parser_core.py
View file @
fc0aa5ce
...
@@ -432,7 +432,7 @@ def school_tags(school: str):
...
@@ -432,7 +432,7 @@ def school_tags(school: str):
def
read_resume_text
(
path
:
Path
)
->
str
:
def
read_resume_text
(
path
:
Path
)
->
str
:
"""
读取 PDF/DOCX/TXT 简历文本并做统一规范化
,供 LLM 优先解析与正则解析共用。"""
"""
按扩展名读取 PDF/DOCX/TXT 简历原文(不做 normalize)
,供 LLM 优先解析与正则解析共用。"""
suffix
=
path
.
suffix
.
lower
()
suffix
=
path
.
suffix
.
lower
()
if
suffix
==
".pdf"
:
if
suffix
==
".pdf"
:
text
=
read_pdf
(
path
)
text
=
read_pdf
(
path
)
...
@@ -440,16 +440,14 @@ def read_resume_text(path: Path) -> str:
...
@@ -440,16 +440,14 @@ def read_resume_text(path: Path) -> str:
text
=
read_docx
(
path
)
text
=
read_docx
(
path
)
else
:
else
:
text
=
read_text
(
path
)
text
=
read_text
(
path
)
return
normalize
(
text
)
return
text
def
extract_pii_fields
(
text
:
str
,
filename
:
str
)
->
dict
:
def
extract_pii_fields
(
text
:
str
,
filename
:
str
)
->
dict
:
"""正则
预扫描候选人 PII 字段(脱敏依据,不随正文发往大模型)
。
"""正则
提取姓名/手机/邮箱/出生年月/年龄等基础字段,供纯正则解析输出
。
姓名/手机/邮箱/出生年月/年龄/城市 等敏感字段在解析链路中始终以本地正则
说明:简历解析的大模型路径已改为直接把原文交给大模型、不做本地脱敏,
结果为准:发送给大模型前必须先把它们替换为占位符(见
本函数仅服务于 parse_resume_text 的正则兜底结果。
llm.sanitize_resume_for_llm),大模型看不到真实值,最终取值也以这里的
正则结果为准。
"""
"""
phone
=
first_match
([
phone
=
first_match
([
r"((?:\+?86[-\s]?)?1[3-9]\d[-\s]?\d{4}[-\s]?\d{4})"
,
r"((?:\+?86[-\s]?)?1[3-9]\d[-\s]?\d{4}[-\s]?\d{4})"
,
...
@@ -463,7 +461,6 @@ def extract_pii_fields(text: str, filename: str) -> dict:
...
@@ -463,7 +461,6 @@ def extract_pii_fields(text: str, filename: str) -> dict:
"email"
:
email
,
"email"
:
email
,
"birthDate"
:
birth_date
,
"birthDate"
:
birth_date
,
"age"
:
infer_explicit_age
(
text
)
or
age_from_birth_date
(
birth_date
),
"age"
:
infer_explicit_age
(
text
)
or
age_from_birth_date
(
birth_date
),
"city"
:
first_match
([
r"(?:现居|所在地|城市|地点)[::\s]+([一-龥]{2,12})"
],
text
),
"resumeName"
:
filename
,
"resumeName"
:
filename
,
}
}
...
@@ -480,6 +477,7 @@ def parse_resume_text(text: str, filename: str) -> dict:
...
@@ -480,6 +477,7 @@ def parse_resume_text(text: str, filename: str) -> dict:
"school"
:
school
,
"school"
:
school
,
"major"
:
infer_major
(
text
),
"major"
:
infer_major
(
text
),
"schoolTags"
:
school_tags
(
school
),
"schoolTags"
:
school_tags
(
school
),
"city"
:
first_match
([
r"(?:现居|所在地|城市|地点)[::\s]+([一-龥]{2,12})"
],
text
),
"source"
:
infer_source
(
text
,
filename
),
"source"
:
infer_source
(
text
,
filename
),
"skills"
:
extract_skills
(
text
),
"skills"
:
extract_skills
(
text
),
"resumeText"
:
text
,
"resumeText"
:
text
,
...
...
backend/tests/test_resume_parser_llm.py
View file @
fc0aa5ce
...
@@ -23,7 +23,7 @@ def stub_llm(monkeypatch):
...
@@ -23,7 +23,7 @@ def stub_llm(monkeypatch):
"""把 parse_resume_path_async 引用的 llm_resume_fields 替换为固定返回的异步桩。"""
"""把 parse_resume_path_async 引用的 llm_resume_fields 替换为固定返回的异步桩。"""
def
install
(
result
):
def
install
(
result
):
async
def
fake_llm
(
text
,
hints
=
None
):
async
def
fake_llm
(
text
):
return
result
return
result
monkeypatch
.
setattr
(
"backend.app.services.resume_parser.llm_resume_fields"
,
fake_llm
)
monkeypatch
.
setattr
(
"backend.app.services.resume_parser.llm_resume_fields"
,
fake_llm
)
...
@@ -31,7 +31,7 @@ def stub_llm(monkeypatch):
...
@@ -31,7 +31,7 @@ def stub_llm(monkeypatch):
return
install
return
install
def
_write_resume
(
tmp_path
,
name
:
str
=
"张晓燕.txt"
)
->
str
:
def
_write_resume
(
tmp_path
,
name
:
str
=
"张晓燕.txt"
)
->
None
:
resume
=
tmp_path
/
name
resume
=
tmp_path
/
name
resume
.
write_text
(
resume
.
write_text
(
"张晓燕
\n
性别:女年龄:28
\n
电话:18734915261 邮箱:1822742034@qq.com
\n
销售支持专员
\n
教育经历
\n
"
"张晓燕
\n
性别:女年龄:28
\n
电话:18734915261 邮箱:1822742034@qq.com
\n
销售支持专员
\n
教育经历
\n
"
...
@@ -39,82 +39,65 @@ def _write_resume(tmp_path, name: str = "张晓燕.txt") -> str:
...
@@ -39,82 +39,65 @@ def _write_resume(tmp_path, name: str = "张晓燕.txt") -> str:
"主修课程:市场营销、管理学原理、人力资源管理、企业经营战略、财务管理、会计学原理、区域经济学"
,
"主修课程:市场营销、管理学原理、人力资源管理、企业经营战略、财务管理、会计学原理、区域经济学"
,
encoding
=
"utf-8"
,
encoding
=
"utf-8"
,
)
)
return
str
(
resume
)
def
test_compose_llm_result_
keeps_regex_pii_and_uses_llm_content
():
def
test_compose_llm_result_
uses_llm_values_for_all_fields
():
pii
=
{
llm
=
{
"name"
:
"张晓燕"
,
"name"
:
"张晓燕"
,
"jobTitle"
:
"销售支持专员"
,
"phone"
:
"18734915261"
,
"phone"
:
"18734915261"
,
"email"
:
"1822742034@qq.com"
,
"email"
:
"1822742034@qq.com"
,
"birthDate"
:
""
,
"birthDate"
:
"
1995-04-30
"
,
"age"
:
"28"
,
"age"
:
"28"
,
"city"
:
""
,
"years"
:
"3年"
,
"resumeName"
:
"张晓燕.txt"
,
}
llm
=
{
"name"
:
"大模型幻觉姓名"
,
"phone"
:
"13900000000"
,
"email"
:
"fake@llm.com"
,
"birthDate"
:
"1990-01-01"
,
"age"
:
"35"
,
"city"
:
"太原"
,
"jobTitle"
:
"销售支持专员"
,
"years"
:
"2年"
,
"education"
:
"本科"
,
"education"
:
"本科"
,
"school"
:
"甘肃农业大学"
,
"school"
:
"甘肃农业大学"
,
"major"
:
"农林经济管理"
,
"major"
:
"农林经济管理"
,
"city"
:
"太原"
,
"skills"
:
[
"客户管理"
,
"客户管理"
],
"skills"
:
[
"客户管理"
,
"客户管理"
],
}
}
text
=
"简历正文"
text
=
"简历原文"
merged
=
_compose_llm_result
(
text
,
"张晓燕.txt"
,
pii
,
llm
)
merged
=
_compose_llm_result
(
text
,
"张晓燕.txt"
,
llm
)
# PII 字段不采用大模型幻觉值,始终以正则预扫描结果为准
for
key
in
(
"name"
,
"jobTitle"
,
"phone"
,
"email"
,
"birthDate"
,
"age"
,
"years"
,
"education"
,
"school"
,
"major"
,
"city"
):
assert
merged
[
"name"
]
==
"张晓燕"
assert
merged
[
key
]
==
llm
[
key
]
assert
merged
[
"phone"
]
==
"18734915261"
assert
merged
[
"email"
]
==
"1822742034@qq.com"
assert
merged
[
"birthDate"
]
==
""
assert
merged
[
"age"
]
==
"28"
assert
merged
[
"city"
]
==
""
# 内容字段采用大模型值,schoolTags 依据最终 school 重新推导
assert
merged
[
"jobTitle"
]
==
"销售支持专员"
assert
merged
[
"years"
]
==
"2年"
assert
merged
[
"education"
]
==
"本科"
assert
merged
[
"school"
]
==
"甘肃农业大学"
assert
merged
[
"major"
]
==
"农林经济管理"
assert
merged
[
"skills"
]
==
[
"客户管理"
]
assert
merged
[
"skills"
]
==
[
"客户管理"
]
assert
merged
[
"schoolTags"
]
==
[
"非985/211"
]
assert
merged
[
"schoolTags"
]
==
[
"非985/211"
]
assert
merged
[
"resumeText"
]
==
text
assert
merged
[
"resumeText"
]
==
text
assert
merged
[
"resumeName"
]
==
"张晓燕.txt"
assert
merged
[
"textLength"
]
==
len
(
text
)
def
test_async_parse_prefers_llm_content_and_keeps_regex_pii
(
tmp_path
,
stub_llm
):
def
test_async_parse_uses_llm_extraction_without_sanitize
(
tmp_path
,
stub_llm
):
"""大模型可用时直接采用其提取结果(含姓名/手机等字段),不本地脱敏、不回退正则。"""
_write_resume
(
tmp_path
)
_write_resume
(
tmp_path
)
stub_llm
(
stub_llm
(
{
{
"name"
:
"
大模型幻觉姓名
"
,
"name"
:
"
张晓燕
"
,
"jobTitle"
:
"销售支持专员"
,
"jobTitle"
:
"销售支持专员"
,
"phone"
:
"1
3900000000
"
,
"phone"
:
"1
8734915261
"
,
"email"
:
""
,
"email"
:
"
1822742034@qq.com
"
,
"birthDate"
:
""
,
"birthDate"
:
""
,
"age"
:
""
,
"age"
:
"
28
"
,
"years"
:
"
2
年"
,
"years"
:
"
3
年"
,
"education"
:
"本科"
,
"education"
:
"本科"
,
"school"
:
"甘肃农业大学"
,
"school"
:
"甘肃农业大学"
,
"major"
:
"农林经济管理"
,
"major"
:
"农林经济管理"
,
"city"
:
"太原"
,
"city"
:
"太原"
,
"skills"
:
[
"客户管理"
],
"skills"
:
[
"客户管理"
,
"市场营销"
],
}
}
)
)
parsed
=
asyncio
.
run
(
parse_resume_path_async
(
tmp_path
/
"张晓燕.txt"
))
parsed
=
asyncio
.
run
(
parse_resume_path_async
(
tmp_path
/
"张晓燕.txt"
))
assert
parsed
[
"name"
]
==
"张晓燕"
assert
parsed
[
"name"
]
==
"张晓燕"
assert
parsed
[
"phone"
]
==
"18734915261"
assert
parsed
[
"phone"
]
==
"18734915261"
assert
parsed
[
"email"
]
==
"1822742034@qq.com"
assert
parsed
[
"jobTitle"
]
==
"销售支持专员"
assert
parsed
[
"jobTitle"
]
==
"销售支持专员"
assert
parsed
[
"major"
]
==
"农林经济管理"
assert
parsed
[
"major"
]
==
"农林经济管理"
assert
parsed
[
"
years"
]
==
"2年
"
assert
parsed
[
"
school"
]
==
"甘肃农业大学
"
assert
parsed
[
"education"
]
==
"本科"
assert
parsed
[
"education"
]
==
"本科"
assert
parsed
[
"city"
]
==
""
assert
parsed
[
"years"
]
==
"3年"
assert
parsed
[
"skills"
]
==
[
"客户管理"
]
assert
parsed
[
"city"
]
==
"太原"
assert
parsed
[
"skills"
]
==
[
"客户管理"
,
"市场营销"
]
assert
parsed
[
"resumeName"
]
==
"张晓燕.txt"
assert
parsed
[
"resumeName"
]
==
"张晓燕.txt"
assert
parsed
[
"textLength"
]
>
0
def
test_async_parse_skips_regex_when_llm_usable
(
tmp_path
,
stub_llm
,
monkeypatch
):
def
test_async_parse_skips_regex_when_llm_usable
(
tmp_path
,
stub_llm
,
monkeypatch
):
...
@@ -125,7 +108,7 @@ def test_async_parse_skips_regex_when_llm_usable(tmp_path, stub_llm, monkeypatch
...
@@ -125,7 +108,7 @@ def test_async_parse_skips_regex_when_llm_usable(tmp_path, stub_llm, monkeypatch
monkeypatch
.
setattr
(
"backend.app.services.resume_parser.parse_resume_text"
,
boom
)
monkeypatch
.
setattr
(
"backend.app.services.resume_parser.parse_resume_text"
,
boom
)
_write_resume
(
tmp_path
)
_write_resume
(
tmp_path
)
stub_llm
({
"jobTitle"
:
"销售支持专员"
,
"school"
:
"甘肃农业大学"
,
"major"
:
"农林经济管理"
})
stub_llm
({
"
name"
:
"张晓燕"
,
"
jobTitle"
:
"销售支持专员"
,
"school"
:
"甘肃农业大学"
,
"major"
:
"农林经济管理"
})
parsed
=
asyncio
.
run
(
parse_resume_path_async
(
tmp_path
/
"张晓燕.txt"
))
parsed
=
asyncio
.
run
(
parse_resume_path_async
(
tmp_path
/
"张晓燕.txt"
))
assert
parsed
[
"name"
]
==
"张晓燕"
assert
parsed
[
"name"
]
==
"张晓燕"
assert
parsed
[
"major"
]
==
"农林经济管理"
assert
parsed
[
"major"
]
==
"农林经济管理"
...
@@ -145,7 +128,7 @@ def test_async_parse_falls_back_to_regex_when_llm_returns_empty(tmp_path, stub_l
...
@@ -145,7 +128,7 @@ def test_async_parse_falls_back_to_regex_when_llm_returns_empty(tmp_path, stub_l
def
test_llm_resume_fields_returns_none_without_key
(
llm_disabled
):
def
test_llm_resume_fields_returns_none_without_key
(
llm_disabled
):
assert
asyncio
.
run
(
llm_resume_fields
(
"姓名:张三"
,
{}
))
is
None
assert
asyncio
.
run
(
llm_resume_fields
(
"姓名:张三"
))
is
None
def
test_async_parse_falls_back_without_llm
(
tmp_path
,
llm_disabled
):
def
test_async_parse_falls_back_without_llm
(
tmp_path
,
llm_disabled
):
...
...
Write
Preview
Markdown
is supported
0%
Try again
or
attach a new file
Attach a file
Cancel
You are about to add
0
people
to the discussion. Proceed with caution.
Finish editing this message first!
Cancel
Please
register
or
sign in
to comment