把简历 PDF 解析成结构化数据,并自动生成一份人可读的 HTML。电子版直取文字层、扫描件自动 OCR;用多模态 LLM「看图」保真,规则校验防幻觉。
Parse résumé PDFs into structured data + a readable HTML report. Born-digital via the text layer, scanned via an OCR fallback; multimodal-LLM reading with rule-based grounding to fight hallucination.
一个端到端的简历解析小工具:输入 PDF → 输出结构化字段(JSON)+ 可读 HTML。强调三件事:尽量 1:1 不丢内容、防幻觉/张冠李戴、每步可拆可重放。
- 📄 电子版 / 扫描件都能处理:有文字层直接读;没有就自动走 OCR 兜底。
- 🖼️ 图像为真源:版面再怪(双栏 / 侧边栏 / 伪粗体重影 / CJK 乱码)也按「人眼看到的」来,靠模型理解而非写死正则——不挑格式。
- 🧷 grounded 抽取:每条经历带
evidence,逐字命中原文才算数,低置信标红走人工。 - 🧱 结构化到细节:公司/部门分离、项目带公司、每段经历的「职责 / 成果 / 技术栈 / 亮点」分组保留、schema 外章节(自我评价 / 荣誉…)兜底。
- 🔌 可插拔 OCR:默认用多模态 LLM,可切到自建 PaddleOCR-VL(零业务改动)。
- 🧾 全程留痕 + 单步重放:每步产物落盘,换模型/prompt 只改一处。
git clone https://github.com/ethanyandev/resume-parser-poc
cd resume-parser-poc
python -m venv .venv && .venv/bin/pip install -r requirements.txt
cp .env.example .env # 填 OPENAI_BASE_URL / OPENAI_API_KEY / OPENAI_MODEL
.venv/bin/python samples/make_sample.py # 生成一个示例简历(虚构数据)
.venv/bin/python run.py samples/sample_resume.pdf --open # 跑全流程,浏览器打开结果需要一个 OpenAI 兼容、且支持图片输入(多模态) 的模型;默认
gpt-5.4-mini,改OPENAI_MODEL即可换。 跑完后artifacts/<doc_id>/resume.html就是给人看的可读结果。
# 全流程(扫描件会自动插入 OCR 步)
.venv/bin/python run.py 你的简历.pdf [--open]
# 单步 / 重放(不重复跑 PDF 解析、不重复调 LLM)
.venv/bin/python run.py 你的简历.pdf --step 1 # 取文本 + 渲染页图(本地,零成本)
.venv/bin/python run.py --doc-id <id> --step ocr # 扫描件:转写页面图
.venv/bin/python run.py --doc-id <id> --step 2 # 重放结构化抽取
.venv/bin/python run.py --doc-id <id> --step 3 # 重放 source-span 校验(无 LLM)
.venv/bin/python run.py --doc-id <id> --step html # 重渲 HTML(无 LLM)
# 批量:多份简历 -> 各自 HTML + 一张汇总对比页
.venv/bin/python batch.py a.pdf b.pdf c.pdf启动服务:
.venv/bin/uvicorn resume_poc.api:app --host 0.0.0.0 --port 8000健康检查:
curl http://127.0.0.1:8000/healthz解析简历:
curl -X POST http://127.0.0.1:8000/internal/resumes/parse \
-H "Authorization: Bearer $RESUME_API_TOKEN" \
-F "file=@你的简历.pdf" \
-F "use_vision=true" \
-F "include_source_text=true" \
-F "compat=talentos_agent" \
-F "include_html=false" \
-F "trace_id=caller-trace-id"如果未设置 RESUME_API_TOKEN,接口不会校验 Authorization,仅适合本地调试。响应里返回
request_id / doc_id / resume / verify / source / mode / usage /
cost_usd;include_source_text=true 时返回用于抽取/校验的原文文本;compat=talentos_agent
时额外返回 compat.talentos_agent.markdown 与 compat.talentos_agent.parsed_json,
用于 talentos-agent 保持现有 parse_resume tool 输出契约。include_html=true 时额外返回
渲染后的 HTML 字符串。
服务默认限制上传 PDF 不超过 RESUME_MAX_UPLOAD_MB=10、页数不超过
RESUME_MAX_PAGES=20;超限会返回稳定错误码 file_too_large 或 too_many_pages。
Docker:
docker build -t resume-parser-poc .
docker run --rm -p 8000:8000 --env-file .env resume-parser-poc除 HTTP API 外,也可以用 MCP stdio 暴露给 agent 工具平台:
.venv/bin/python -m resume_poc.mcp_serverMCP 运行依赖 mcp Python 包,需要 Python 3.10+;默认 Dockerfile 使用 Python 3.12
运行环境。
MCP tool 名称为 parse_pdf_resume,支持传本地 pdf_path,也支持传
pdf_base64 + filename。返回结构与 HTTP /internal/resumes/parse 一致,默认包含
compat.talentos_agent.markdown 与 compat.talentos_agent.parsed_json。
| 步 | 模块 | 做什么 | 用 LLM? |
|---|---|---|---|
| 1 | extract_text.py |
PDF→阅读顺序 markdown(pymupdf4llm)+ 渲染每页 PNG;无文字层→标记 needs_ocr |
否 |
| ocr | ocr.py |
仅扫描件:页面图→阅读顺序文本(可插拔 provider) | 是 |
| 2 | extract_fields.py |
一次结构化抽取 + 严格 schema;默认把页面图 + 文字层一起喂多模态模型 | 是 |
| 3 | verify.py |
每条 evidence 必须逐字命中来源文本(电子版=文字层 / 扫描件=OCR 文本),否则标红 |
否 |
| html | render_html.py |
渲染成可读 HTML(resume.html),每条带「已核 / 待核」徽章 |
否 |
产物落在 artifacts/<doc_id>/(页面图在 pages/、OCR 文本 step_ocr.json、可读输出 resume.html),LLM 调用全量留痕在 artifacts/<doc_id>/ai_calls.jsonl。
对电子版简历,渲染出来的页面图才是「内容真相」:无论简历用什么工具生成,人眼看到的永远是干净的、每样东西只出现一次。文字层是 PDF「怎么写」的副产物,可能被排版方式搞坏(有的 PDF 用重复描字做粗体,导致文字层整块重复;CJK 还常出现 Kangxi 部首等兼容码点)。所以:
页面图 = 内容真相(按图中出现次数,每项只输出一次);文字层只当拼写参考;凡文字层里有、图里没有的重复/乱码一律忽略。
这是个交给模型判断的通用原则,不针对任何具体格式——遇到新写法的 PDF 通常无需改代码。
无文字层时 step1 标 needs_ocr,自动插入独立 ocr 步把页面图转成文本,再走正常的 grounded 抽取。
vlm(默认):用多模态模型逐页转写,走统一 capability 层 + 全量留痕。paddleocr_vl(槽位):ocr.py里的 adapter,POST 页面图到自建 PaddleOCR-VL 服务,设OCR_HTTP_URL后启用(服务端映射待接)。OCR_PROVIDER=paddleocr_vl切换。
.venv/bin/python samples/make_sample_scanned.py # 造一个图片版(无文字层)PDF
.venv/bin/python run.py samples/sample_resume_scanned.pdf # 自动走 ocr→抽取→校验| 变量 | 说明 | 默认 |
|---|---|---|
OPENAI_BASE_URL |
OpenAI 兼容端点 | https://api.openai.com/v1 |
OPENAI_API_KEY |
API key | — |
OPENAI_MODEL |
多模态模型名 | gpt-5.4-mini |
RESUME_USE_VISION |
0=纯文本臂(更省,但不抗文字层污染) |
1 |
OCR_PROVIDER |
vlm / paddleocr_vl |
vlm |
OCR_HTTP_URL |
paddleocr_vl 服务端点 |
— |
AI_TIMEOUT / AI_MAX_RETRIES |
单次调用超时(秒)/ 重试次数 | 300 / 3 |
AI_PRICE_IN / AI_PRICE_OUT |
成本估算(每 1M token,留痕用) | 0.15 / 0.60 |
RESUME_ARTIFACT_CLEANUP_ENABLED |
是否启用本地产物自动清理 | 1 |
RESUME_ARTIFACT_RETENTION_DAYS |
请求产物保留天数 | 7 |
RESUME_CLEANUP_INTERVAL_SECONDS |
清理扫描间隔(秒) | 3600 |
- 统一接入:
gateway.call(capability=...),业务不碰模型名;能力(文本/视觉/OCR)都在config.py注册,换模型/网关只改这一处。 - 全量留痕:
artifacts/<doc_id>/ai_calls.jsonl(request_id / trace_id / model / prompt_version / 输入快照 / 原始输出 / 延迟 / token / 成本 / n_images)。 - 可拆可重放:每步 fixed in/out,产物落盘,单步可独立重跑(
--step 1/ocr/2/3/html)。 - 结果有兜底:source-span 规则校验 + confidence,低置信走人工;HTML 每条带「已核/待核」徽章。
- 改动可回归:每能力独立 prompt 版本;
tests/是回归集(pytest -q,含 NFKC / 多模态 payload / OCR provider / HTML 渲染转义)。 - 降级:无文字层→可插拔 OCR 兜底;LLM 失败→重试后由上层降级。
服务默认在启动时及之后每小时清理一次超过 7 天的请求产物。原始 PDF、页面图、 解析 JSON、HTML 和包含 PII 的原始 AI 调用记录作为同一个请求组一起删除:
RESUME_ARTIFACT_CLEANUP_ENABLED=1
RESUME_ARTIFACT_RETENTION_DAYS=7
RESUME_CLEANUP_INTERVAL_SECONDS=3600生产链路日志以单行 JSON 输出到标准输出,由日志平台采集。使用响应中的
request_id 可以串起请求接收、上传、PDF 提取、OCR、模型调用、校验、HTML 渲染、
响应和清理事件。标准输出只记录耗时、状态、稳定错误码、页数、模型、Token 用量和
成本等元数据,不记录文件名、简历正文、Prompt、模型原始输出或密钥。
需要核查原始输入输出时,可在 7 天内查看
artifacts/<request_id>/ai_calls.jsonl;该文件包含 PII,访问权限应与简历原件一致。
.venv/bin/python -m pytest -q.env(密钥)与artifacts/(简历 PII、留痕)已在.gitignore,不会被提交。- 务必用
git推送、不要打包整个目录,否则.env会被带走。 - 简历内容会发送给你配置的 LLM 服务;生产环境请改用自有网关并做 PII 脱敏。
- 扫描件 OCR 兜底(可插拔 provider,
vlm默认已跑通) -
paddleocr_vlHTTP adapter 的服务端映射 - 完整性校验:用图当真相、让模型当 critic 列出「图里有但没抽到」的内容,量化 1:1
- OCR 文本 vs 原图的二次保真校验
- 字段 schema 按需细化、work/education 加
location - PII 脱敏、批量并发
这是一个 POC / 学习项目,欢迎 issue 与 PR。注意:LLM 抽取并非 100% 可靠,关键场景请配合
evidence/confidence 做人工复核。