简历拷问机 —— 上传简历,先让面试官问一遍
赛道一 · TextIn xParse 实用价值赛道 作品形态:可运行的单页 Web 应用(本地 + 线上),配文档与演示样例
一、作品介绍
1.1 名称与一句话定位
简历拷问机:上传一份简历(PDF 或照片),系统把简历拆成段落、时间线和技能清单,再按 HR 的追问习惯生成 10 条左右的问题,每条都写清楚面试官在确认什么、你该怎么答。
它不点评简历、不打分排名、不给录用建议。它只做一件很具体的事:把简历上的模糊点,提前翻译成面试现场会被问出来的问题。
分享链接:简历拷问机 · 先替面试官问一遍
1.2 应用场景
场景一:求职者投递前的最后一遍自查。小周要投一个产品经理岗位,简历改到第五版,自己已经看不出问题了。他把 PDF 拖进页面,看到第一条问题:「简历里写『DAU 提升 30%』,这个 30% 的基数是多少、怎么算出来的?」他愣了一下——这确实是他答不上来的地方。面试前一天,他把 4 条标红的问题各写了一段回答。
场景二:转行者的履历断层。做了三年运营想转产品,简历上 2022 年 12 月到 2023 年 8 月是空的。系统把这段在时间轴上画成斜纹块,并生成对应问题。他提前准备了「这段时间在补什么、做了什么项目」的两句话,而不是在被问到时说「休息了一下」。
场景三:帮别人看简历。学长帮学弟看简历,说「感觉写得有点虚」但说不出具体哪虚。跑一遍之后,两个人对着清单逐条讨论,比空泛的「再具体一点」有用得多。
1.3 要解决的问题
| 现象 | 后果 |
|---|---|
| 简历上写「明显提升」「大幅优化」,没有一个数字 | 面试官会当场要求给数,答不上来会影响整份简历的可信度 |
| 写了「精通 Python」,正文里没有一个 Python 项目 | 技能项是最容易被当场验证的,一验就穿 |
| 两段工作之间有空窗,简历上不解释 | 空窗期是必问题,主动解释是加分,被问出来是减分 |
| 「参与」「协助」通篇,说不清自己独立做了什么 | 岗位级别会被判低一档 |
| 「担任项目负责人,带领团队」,但没有人数和分工 | 面试官会怀疑是挂名 |
| 自我评价全是形容词,没有一个例子 | 这一段在多数面试官眼里等于没写 |
这些问题的共同点是:当事人自己看不出来。因为写简历的人知道上下文,而看简历的人只有字面。这个工具补的就是这个视角差。
1.4 核心功能
上传 PDF / JPG / PNG / BMP / TIFF / WEBP,单文件 40MB 以内
上传前做文件头嗅探,HEIC、Office 文档、压缩包、视频当场拦下并给出转格式的具体步骤
自动识别简历段落:基本信息、教育背景、工作经历、项目经历、技能、自我评价
还原时间线,把教育、工作、项目画成横向条形,相邻工作之间超过 5 个月的空窗画成斜纹块
解析技能自评等级(精通 / 熟练 / 熟悉 / 了解),并检查正文里是否有对应经历佐证
按 11 类规则生成追问清单,按危险等级排序(必被追问 / 可能追问 / 加分问题)
每条问题附「他在确认什么」与「你该怎么答」两段建议
拷问指数(0-100)与一句话判词,给整体印象
导出 PDF(真生成文件)、导出 CSV、一键复制全部问题
页面内保留引擎识别的原文,用户可自行核对
主界面效果:
1.5 创新点
① 把「简历优化」换成「追问预测」。市面上的简历工具几乎都在做润色和评分,而评分是主观的。追问是客观的:HR 对「DAU 提升 30%」一定会问基数和口径,对「精通」一定会要例子。把目标从「写得更好看」换成「这些问题你答不答得上来」,输出就变成了可执行的动作。
② 每条问题都带证据链。问题不是模板填空,而是从简历原文里定位出来的。页面同时展示原文引用、判定原因、回答框架三层,用户能判断这条是真问题还是误判。
③ 技能项与正文做交叉验证。技能栏写「精通 SQL」,正文里如果只出现过 Excel,系统会标出「正文无对应经历」。这个交叉检查用到了文档的段落结构——只有拿到分段的文本才能做,纯字符串搜索做不到。
④ 空窗期可视化。把时间轴画出来,空窗会自动变成斜纹段。数字本身(8 个月)比一句「注意时间连贯性」直观得多。
⑤ 零依赖架构,开箱即用。后端只用 Node 内置http,前端单文件原生 JS,无框架无构建,凭据内置在lib/credentials.js,不需要任何环境变量就能跑。已在云端部署成在线服务,评委打开链接即可上传简历,本地什么环境都不用装。
二、技术方案
2.1 工具与选型
| 工具 | 用途 | 为什么选它 / 不选什么 |
|---|---|---|
| TextIn xParse | 文档结构化解析 | 能给出elements[]的元素类型(Title / NarrativeText)和坐标,纯 OCR 只给一堆无序文本 |
Node.js 内置http | 后端服务 | 零依赖、单文件可跑;不选 Express,因为多一层node_modules就多一层部署失败的可能 |
| 原生 JS + 单 HTML | 前端 | 单文件可直接托管;不选 Vue/React,因为构建产物会让「打开就能用」这件事变复杂 |
| html2canvas + jsPDF | 真 PDF 导出 | 不依赖浏览器打印,排版可控;不用window.print(),因为打印出来带页眉页脚和 URL |
| puppeteer-core | 端到端验证 | 用本机 Chrome 跑真实点击,验证渲染、导出、下载全链路 |
2.2 开发方式
对话式开发 + 自动化验证。每一轮改动后都跑同一套检查:
端到端渲染:无头浏览器打开
?demo=1,等待结果区出现,收集pageerror与console.error导出验证:真实点击「导出 PDF / 导出 CSV」,检查下载目录里是否出现文件并核对体积(PDF 1.2MB、CSV 5KB)
布局验证:读取元素
getBoundingClientRect(),确认时间轴条形的位置与宽度符合按月份计算的结果移动端验证:390×844 视口重跑一遍
这套检查能挡住三类最常见的问题:页面白屏、导出无反应、移动端错位。
2.3 xParse 接入
认证走 AppKey,凭据硬编码在lib/credentials.js里,服务启动即生效,不需要配置环境变量或登录授权:
// lib/credentials.js —— 随应用一起发布,服务启动即读到 module.exports = { appId: '1eb9ffc1********************c9479c', secretCode: 'c9ee19bd********************b36f72bf', };(以上为脱敏展示,完整值随代码交付。)
服务端按「有 AppKey 走 AppKey,没有才回退本机 profile」的顺序拼参数,因此同一份代码既能云端跑、也能本机跑:
# 主路径:AppKey(当前部署即此路径) xparse-cli --app-id <ID> --secret-code <SECRET> parse <文件> --api auto --view json --output <目录> # 兜底:AppKey 缺失时回退到本机已授权 profile xparse-cli --profile workbuddy parse <文件> --api auto --view json --output <目录>
api/health会直接回报当前认证链路,线上实测返回"auth": "appkey",说明走的是 AppKey 而不是本机授权——这是它能被独立部署的关键。
参数选择的理由:
--view json:本项目要的是elements[].type与段落顺序,markdown 会把标题层级和段落边界拍平,时间线和段落归类会失真--api auto:自动选择可用通道,不硬编码付费/免费--task-context:向引擎声明「解析简历并定位可追问的模糊点」,需要按阅读顺序拿到每一段文字及坐标--output <目录>:必须指定,否则 JSON 与日志混在 stdout 里,难以分离
实际调用记录(本地命令回放,21 个元素,387 字):
2.4 系统结构
四层职责清晰分离:
apps/resume/index.html 单文件前端:上传、进度、结果渲染、导出、下载投递 server.js HTTP 服务:格式嗅探、错误翻译、调用 CLI、临时文件清理 lib/resume/extract.js 抽取与判定:段落归类、时间解析、11 类追问规则、技能交叉验证 lib/resume/credentials.js 凭据:AppKey 硬编码内置,缺失时回退 profile
关键设计:服务端不返回引擎原始 JSON。用户看到的是「这份文件读不出来,可能是三种原因,建议这样处理」,原始错误只进服务端日志。
2.5 交付形态与访问方式
三个入口,同一套代码:
| 形态 | 地址 | 说明 |
|---|---|---|
| 在线服务(推荐) | https://resume-griller.app.workbuddy.host/resume/ | 完整功能:上传任意文件、真实调用 xParse、导出 PDF/CSV |
| 作品总入口 | https://resume-griller.app.workbuddy.host/ | 两个参赛作品的导航页 |
| 资料库分享页 | 见提交说明 | 单文件版,把html2canvas/jsPDF与全部逻辑内联,双击即可打开,也发得出链接 |
两个作品合并为一个服务、按路径分流(/resume/、/corpspeak/),而不是各起一个进程。原因很实际:一台实例只能绑一个端口,拆成两个服务要么买两台机器,要么在网关层再加一层反代;合成一个服务后,两份作品共用同一套格式嗅探、错误翻译和临时文件回收逻辑,也共享同一份凭据配置。
前端页面里API_BASE是按路径自动推导的:
var API_BASE = (function () { var p = location.pathname; if (/^\/corpspeak(\/|$)/.test(p)) return '/corpspeak'; if (/^\/resume(\/|$)/.test(p)) return '/resume'; return ''; })();同一份 HTML 放在根路径、子路径、或资料库单文件页里都能找到正确的接口前缀,不用改代码。
三、处理流程
3.1 输入类型与预处理
| 格式 | 支持 | 预处理动作 | 不支持时怎么提示 |
|---|---|---|---|
| ✅ | 直接解析 | 加密 PDF 提示先取消密码另存 | |
| JPG / PNG / BMP / TIFF / WEBP | ✅ | 直接解析 | 模糊或裁切时提示重拍 |
| HEIC(iPhone 默认) | ❌ | 文件头识别后拦截 | 给出「相册 → 分享 → 拷贝为 JPG」的具体路径 |
| DOCX / XLSX / ZIP | ❌ | 文件头识别后拦截 | 提示另存为 PDF 或拍照上传 |
| MP4 / MOV | ❌ | 文件头识别后拦截 | 提示上传的是媒体文件不是图片 |
扩展名不可信:把 HEIC 改名成.jpg再上传是很常见的操作,所以必须读文件头判断。只拦确定解析不了的格式,unknown一律放行交给引擎兜底,避免误伤合法变体。
3.2 端到端流程
用户选择或拖入文件,前端做扩展名与体积预检
服务端读文件头嗅探真实格式,命中黑名单直接返回可操作提示(不消耗额度)
文件落到临时目录,调用
xparse-cli parse --view json解析成功后读取
data.elements[],按页与纵坐标恢复阅读顺序清洗文本:去掉 markdown 星号、项目符号、把全角数字与
%归一化段落归类:用 8 组段落关键词把 21 个元素分进 6 个桶
时间解析:从「2023.08-至今」这类字符串里取出起止年月,算出月数
规则判定:11 类追问规则逐条跑,命中即生成问题与证据
去重与限流:同一句话只保留一个问题(等级高者胜),同类追问限 1-2 条
排序输出:按危险等级排序,生成拷问指数与判词
返回报告,临时文件中的非 JSON 文件立即删除
3.3 关键指令与参数
| 指令 | 参数 | 作用 | 为什么这么传 |
|---|---|---|---|
parse | --view json | 输出结构化 JSON | 需要elements[].type区分标题与正文 |
parse | --api auto | 自动选择通道 | 不可用时自动回退,不硬编码 |
parse | --output <dir> | 指定输出目录 | 便于分离 JSON 与日志 |
parse | --task-context | 声明解析意图 | 提升对字段的针对性,用完立即删除(权限 0600) |
quota | --output json | 查询额度 | 控制台只显示单个套餐包,不够准 |
3.4 核心处理逻辑
① 阅读顺序还原。引擎输出的元素顺序基本可用,但不保证。按page_number分组,组内按coordinates[0][1](纵坐标)排序,避免双栏或浮动元素打乱顺序。
out.sort((a, b) => { if (a.page !== b.page) return a.page - b.page; const ay = a.coord && a.coord[0] ? a.coord[0][1] : 0; const by = b.coord && b.coord[0] ? b.coord[0][1] : 0; return ay - by; });② 段落归类。用 8 组正则匹配段标题(「教育背景 / 教育经历 / 学历背景」都归到edu),匹配到就切换当前桶,后续行归入该桶。段标题要求长度 ≤ 8 字且整行匹配,避免「教育背景:某大学」这种行被误判成标题。
③ 时间解析(正则分组设计)。中文简历的时间写法很杂:2023.08-至今、2018.09-2022.06、2021年9月—2022年5月。第一个版本的正则把结束年份写成了非捕获组,导致「2023.08-至今」被解析成「NaN.01 - 2023.08」。修正后把起止年月和「至今」分成两组:
const PERIOD_RE = new RegExp( '((?:19|20)\\d{2})\\s*[年.\\-/]?\\s*(\\d{1,2})?\\s*月?\\s*[-–—~~至到]+\\s*' + '(?:((?:19|20)\\d{2})\\s*[年.\\-/]?\\s*(\\d{1,2})?\\s*月?|(至今|现在|目前|present))', 'i');月份统一换算成「年×12+月」的绝对月数,空窗、任职时长都基于这个数计算。
④ 技能交叉验证(带词边界)。技能词表里既有中文(数据分析)也有 ASCII(SQL、R、C++)。最初直接用includes判断,结果「R」命中了「Axure」里的r,「PR」命中了「Office」里的…。修正为:ASCII 词加前后非字母数字边界。
function hitSkill(text, name) { if (/^[A-Za-z0-9+#.]+$/.test(name)) { const esc = name.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); return new RegExp('(^|[^A-Za-z0-9+#])' + esc + '([^A-Za-z0-9+#]|$)', 'i').test(text); } return text.includes(name); }技能等级按小句判定再向后继承:精通 Python、SQL、Axure切开后只有第一个小句带「精通」,需要把等级往后传,否则 SQL 和 Axure 会变成无等级。
⑤ 追问规则的优先级与限流。同一句话可能同时命中多条规则。比如「担任项目负责人,带领团队完成平台从 0 到 1 上线」,既是「成果缺失」也是「管理幅度」。规则按信息量排优先级(管理幅度 6 > 成果缺失 3),同分位时高优先级覆盖。之后再对同类追问限流(技能验证最多 2 条、成果缺失最多 2 条),避免一份简历刷出 6 个「没有量化」把别的角度全挤掉。
⑥ 拷问指数。28 + 必被追问×8 + 可能追问×3 + 加分问题×2,上限 94。有 5 条必被追问时得 85 分,判词是「这份简历很敢写」。分数只用于给出第一印象,不参与任何筛选。
四、解决效果
4.1 实测数据
| 样张 | 类型 | 元素数 | 识别字数 | 追问点 | 必被追问 | 拷问指数 | 耗时 |
|---|---|---|---|---|---|---|---|
| 产品经理简历(1 页 PDF) | 印刷体 | 21 | 387 | 11 | 5 | 85 | 3.2s(首次冷启动)/ 1.2s(复跑) |
样例简历的 11 条问题分布:
| 类型 | 条数 | 典型问题 |
|---|---|---|
| 数字口径 | 1 | 「DAU 提升 30%」的基数是多少、怎么算出来的? |
| 定性表述 | 1 | 「转化率有明显提升」里的「明显」,具体是多少? |
| 技能验证 | 2 | 技能栏写「精通 Python」,正文里没有对应项目 |
| 时间断层 | 1 | 2022.12 到 2023.08 之间空了 8 个月 |
| 角色边界 | 1 | 「参与中台建设项目」里,哪一部分是你独立完成的? |
| 管理幅度 | 1 | 「担任项目负责人,带领团队」——团队多少人? |
| 成果缺失 | 2 | 「负责日常数据报表整理」——最后做成了什么? |
| 稳定性 | 1 | 「某电子商务有限公司」只待了 5 个月,为什么离开? |
| 空话 | 1 | 「学习能力强」有没有一个具体例子? |
时间轴把空窗直接画了出来(第二行入职前的斜纹段):
4.2 前后对比
| 维度 | 用 xParse 之前 | 用之后 | 变化 |
|---|---|---|---|
| 判断依据 | 靠人眼通读、凭感觉说「有点虚」 | 按 11 类规则逐条定位 | 从主观到可复核 |
| 段落归类 | 手工摘抄 | 自动分出 6 个段落桶 | — |
| 时间线 | 自己在纸上画 | 按月份还原,空窗自动标出 | 8 个月空窗一眼看见 |
| 技能核对 | 要来回翻两遍 | 技能栏与正文自动交叉比对 | 5 项技能 4 项无佐证 |
| 单份简历耗时 | 通读 + 记录 10~15 分钟 | 3.2 秒 | — |
| 结果留存 | 截图 / 备忘录 | PDF + CSV + 一键复制 | 可直接带去面试 |
4.3 细节改进
段标题误判:
教育背景:华东理工大学这类行原本会被当成段标题,加上「长度 ≤ 8 字」的限制后不再误判时间解析:修掉捕获组错位后,
2023.08-至今从NaN变成正确区间,时间线才画得出来技能误命中:加了 ASCII 词边界后,
R不再命中Axure,这条例误报被彻底消除问题重复:加同句去重 + 同类限流后,「成果缺失」这一类从 6 条收紧到 2 条,腾出的位置给了时间断层、稳定性等不同角度,清单的可读性明显提升
页面过长:第一版把每条问题的「他在确认什么 / 你该怎么答 / 原文」全部展开,11 条问题从头铺到尾,首屏只放得下一条。改成
<details>折叠、答案默认收起、时间轴与技能并成两栏、技能从卡片压成 chips 后,整页在 1180px 宽度下实测 1727px,首屏就能看到拷问指数和三条最该准备的问题
4.4 体验改善
用户视角的三点变化:
问题变成可执行的清单:不是「建议补充量化成果」,而是「这句话缺基数,准备『从 X 到 Y』的绝对值」
知道自己被问在哪:时间轴把空窗画成斜纹,视觉上比文字提示更有压迫感
能核对:每条问题下面都挂着原文引用,误判一眼能看出来,不会盲信
4.5 边界与局限
只做结构判断,不做能力评价:不判断这个人是否适合某个岗位,也不给录用建议
手写简历识别率下降:印刷体 PDF 效果最好,手机拍的手写简历段落边界容易断
图表化简历:用图标和进度条表达技能的简历,文本层信息很少,能抽到的内容有限
规则是保守的:宁可漏判,也不产出无依据的问题。因此写得已经很扎实的简历可能只出 3-5 条
追问建议是通用框架:不同行业、不同岗位的追问重点不同,目前是通用版本,没有做行业分支
五、结语
这个作品的出发点很小:简历上每一个模糊的地方,面试里都会变成一个必答题。
TextIn xParse 给的是结构——段落、标题、坐标、阅读顺序。有了结构才谈得上「找模糊点」,因为模糊不是关键词,而是「有动作没结果」「有数字没口径」「有技能没项目」这类关系。纯文本搜索做不了这个判断,这也是整个作品选择--view json而不是 markdown 的根本原因。