
简历生成器这个方向看着人畜无害真做起来才知道水有多深。内容生成好说丢给 AI 大模型半天就能跑通一个能用的 demo真正让人反复返工的是排版——同一份工作经历用 A 模板排出来是资深专家用 B 模板排出来像刚毕业的实习生。我花了几周时间做了一个 Resume Hub一个把 Vibe Coding 思路落到简历排版上的 AI 简历生成器核心玩法是你用自然语言描述想要的版式系统直接把描述翻译成样式规则实时预览效果最后导出 PDF。这篇文章把整个设计和踩坑过程都摊开讲你在做类似的 AI 应用时会少走很多弯路。要说为什么从“排版”这个点切进去是因为我自己被传统简历工具折磨过太多回。市面上很多 AI 简历生成器点进去就是让你选模板、填空模板之间只换颜色和栏目位置稍微想个性化一点要么付费要么导出后自己改改着改着版式就崩了。Vibe Coding 这套思路正好治这个病不让你手动调 CSS而是直接描述“我想要什么感觉”让 AI 把感觉翻译成规则。顺着这个思路Resume Hub 从数据模型到提示词、从样式翻译管线到 PDF 渲染每一层都有自己的讲究下面一个一个说。1. 项目定位与需求拆解1.1 简历生成器的真正痛点不是内容是排版做 AI 简历工具绝大多数团队会把精力放在“生成内容”上这其实是方向性偏差。大模型写一段项目经历、改一句自我评价现在的水平已经足够应付了真正的差距在排版。你可以做一个简单的测试同一份简历内容用 Notion 随便排一版再拿专业模板排一版投出去的回馈率完全不一样。HR 看一份简历的平均时间只有十几秒版式就是第一印象而这个印象跟内容质量是叠加关系不是替代关系。更麻烦的是简历排版的约束条件特别多。要控制在一页或者两页以内字号行距要有呼吸感技能项要突出又不能喧宾夺主经历部分的时间线要一目了然还要保证导出 PDF 后字体不乱、分页不劈叉。这些要求放到传统网页里就是 CSS 基本功但放到 AI 生成场景里就变成了一个系统工程——因为 AI 生成的内容是变化的你没法像静态网页那样把所有情况都写死在样式表里。Resume Hub 解决的核心问题就是让 AI 既负责内容生成又负责把“用户对版式的感性描述”翻译成一套可靠的样式规则并且这套规则能适配任意长度的简历数据。说白了就是把排版从“设计师手动调”变成“AI 按意图生成”这是项目的灵魂。1.2 什么是 Vibe Coding它凭什么叫法这么绕Vibe Coding 这个词是这两年才热起来的指的是一个人用自然语言描述需求让 AI 写出大量代码开发者只负责运行、看效果、提反馈通过这种“边感受边调整”的循环把项目往前推。它不是一种新的编程语言而是一种新的工作方式你不需要逐行读懂代码只需要对产出的效果保持敏感像调音师一样不断把声音调到舒服的位置。把 Vibe Coding 用到简历排版上逻辑非常顺。传统的模板编辑器是“所见即所得”鼠标拖拽调整精度高但学习成本高更传统的做法是直接改 CSS那就更劝退了。Vibe 的方式是用户说一句“风格偏欧洲极简标题用深蓝色技能项做成标签卡片”AI 就把它拆成颜色变量、间距比例、组件形态、栏目布局四类指令直接作用到渲染层。用户不需要懂 CSS甚至不需要知道什么叫 CSS 变量效果不对就再补一句“再紧凑一点”系统会基于当前状态做增量调整。这个思路放在简历场景里还有一个额外红利简历是高度格式化的文档版式的语义非常明确。栏目、标题、时间线、技能块、项目条目都是约定俗成的结构AI 很容易在这些结构上做风格化调整而不至于画蛇添足。比起“AI 帮你设计一张海报”简历排版的约束更强成功率高得多。1.3 Resume Hub 的边界不做什么比做什么更重要做项目最容易犯的毛病是想一口吃个胖子。我给 Resume Hub 定的边界有三条。第一不做复杂交互的在线编辑器——不做拖拽式画布、不做无限调参面板交互只保留“填写/导入资料、给出版式描述、预览、导出”这四步操作路径短到三分钟能走完。第二不做内容编造——AI 只整理用户提供的信息不允许自己发挥造经历这是底线。第三不做多语言国际化先用好中文场景把中文排版、中文 ATS 兼容性这些最扎手的问题解决透。这个边界设定直接影响了后面的技术选型。不搞复杂编辑器前端就不用引一堆 DnD 库不做内容编造提示词里就要写死禁用规则并做 schema 校验专注中文场景字体渲染和 PDF 生成的坑就能提前锁定。很多 AI 应用死掉不是因为功能太少而是因为做得太多导致每个环节都粗糙。Resume Hub 的体验是模块少、路径短、每个模块的完成度都足够高。2. 核心模块设计与技术选型2.1 结构化数据模型所有排版的地基简历生成器最忌讳的一件事就是让 AI 直接输出一段排好的 HTML 或 Markdown。那种做法 demo 看起来很快但用户只要改一个字、加一段经历整个版式就乱了。我的做法是先把简历拆成一个严格的结构化 JSON 模型AI 只负责产出数据排版层根据数据长度和类型动态计算布局两者完全解耦。这个数据模型我设计了几个核心区块基本信息姓名、职位、联系方式、个人摘要、专业技能、工作经历、教育背景、项目经历、附加信息证书、语言、兴趣。关键设计在于每个区块都带结构化的字段。比如工作经历不是一坨长文本而是拆成公司名、职位、开始时间、结束时间、每条成果描述数组技能不是字符串列表而是带熟练度等级的标签对象。这样做的好处是渲染层可以根据数组长度自动收缩间距时间线可以按时间排序技能等级可以转换成进度条或标签样式。{ basic: { name: 张铭, title: 前端工程师, contact: { email: zhangexample.com, phone: 138xxxx0000, city: 上海 } }, summary: 7 年前端开发经验专注于中后台复杂应用……, skills: [ { name: TypeScript, level: 4 }, { name: React, level: 5 } ], experience: [ { company: 某云服务公司, position: 高级前端工程师, start: 2021-03, end: 2024-06, achievements: [ 主导搭建组件库覆盖 80% 业务页面开发周期缩短 35%, 推动微前端改造支持 5 条业务线独立发布 ] } ], education: [ { school: 某大学, degree: 本科, major: 计算机科学, start: 2013-09, end: 2017-06 } ] }这个 JSON 结构不是拍脑袋定的每加一个字段我都要问自己渲染层用得上吗AI 生成这个字段的准确率够吗用户手工填写成本高吗三个条件缺一个就砍掉。比如“技能熟练度”这个字段AI 判断起来其实有主观性所以生成后允许用户手动微调但渲染层会把它映射成“无需/了解/熟练/精通”四档标签避免做精确百分比这种既难生成又难展示的设计。结构化数据模型带来的另一个好处是 ATS 友好。很多公司的招聘系统会解析 PDF 里的文本结构混乱的简历会被解析得七零八落。数据模型本身兼具语义和顺序渲染成 PDF 后文本流是规整的机器可读性天然就好。这点在后面会展开讲。2.2 提示词工程怎么让 AI 老实输出干净数据有了数据模型下一步就是让 AI 稳定输出这个模型。我用的是“系统提示词 少量示例 后置 schema 校验”三层策略。系统提示词里明确规定只输出合法 JSON、禁止编造、所有未提供的信息一律用 null 占位、每条经历只保留结果导向的描述并尽量量化。这几点看起来简单实际调试时每一条都对应一个真实翻车场景。禁止编造这条最重要。早期测试时我丢给模型一句话“从事过电商平台开发”它直接给我生成了一段“负责‘某知名电商’订单系统重构日均订单量提升 40%”的经历。数据漂亮得吓人但全是幻觉。我在提示词里加了强约束同时在后置校验里加了敏感字段检查凡是公司名、项目名、时间、数字没有出现在用户原文里的一律判为非法并要求模型重生成。宁可空着不能编造这是做简历工具必须守住的底线。多轮交互的处理也花了不少心思。用户第一次输入往往是很零散的聊天记录比如“我毕业后在 A 公司做了两年后端后来跳去 B 公司搞大数据中间还做过一个开源项目”。我的做法是先让 AI 把这段口语内容转成 JSON之后用户每补充一句系统都会把上一轮 JSON 和用户新描述一起发给模型做增量合并更新。这里的关键是给模型一张“变更说明”的入场券明确告诉它“你只需要把用户最新表述涉及到的字段更新掉其他内容原样保留”。没有这条约束模型经常会自作主张把整个简历重写一遍格式和口径全变。# 增量更新提示词的核心骨架 你是简历数据整理助手。用户会给你一段新描述你需要更新已有的 JSON 简历数据。 已有数据 {resume_json} 用户新描述 {user_input} 要求 1. 只更新用户新描述涉及到的字段 2. 未涉及的内容严格保持原值不允许自行改写润色 3. 只输出合并后的完整 JSON不要输出任何解释 4. 不确定的信息使用 null禁止推测编造后置校验我用的是 JSON Schema 校验器把模型输出直接套进 schema类型不对、缺字段、多字段都会抛错校验不通过就带着错误信息让模型重新生成最多重试两次。这套方案实测下来模型输出合法 JSON 的成功率能从最初的六七成拉到九成五以上。剩下那百分之几让用户手动填一下就解决了不值得为它上特别复杂的修复逻辑。2.3 样式描述到 CSS 的翻译管线Vibe 排版的发动机Vibe 排版的入口是一个很不起眼的输入框上面写着“描述你想要的版式比如简洁、商务、偏冷色调、技能用标签卡片”。这个输入框背后的翻译管线是整个项目最核心的部分。用户的自由文本会先被送入模型输出格式是一个固定的“样式指令集”包含颜色板、间距倍率、字体风格、技能展示形态、栏目布局、标题样式六类字段。{ palette: { primary: #1a3557, accent: #2f6db5, background: #ffffff, text: #222222 }, spacing: { scale: 1.05, sectionGap: 14, itemGap: 10 }, typography: { titleFont: bold-serif, bodyFont: sans, baseSize: 10.5 }, skillsDisplay: chips, layout: single-column, sectionTitleStyle: bottom-border }拿到这个 JSON 后前端会根据里面的字段组装 CSS 变量再通过一套“布局引擎”应用样式。布局引擎本质上是一个带条件分支的模板函数skillsDisplay 是 chips 就渲染标签卡片是 bars 就渲染带进度的条形layout 是 single-column 就切单栏栅格是 two-column 就把左侧窄栏留给联系方式和个人摘要。这套映射是确定的不经过模型所以不会出现“模型说了一句奇怪话导致页面崩掉”的情况。这里有个经验值得多说一句不要让模型直接生成 CSS 字符串。一开始我觉得让模型直接输出 CSS 变量值最灵活结果发现它经常输出带浏览器前缀的怪异属性、兼容性不一的单位甚至夹杂注释把样式表搞坏。约束成结构化的 JSON 指令集再翻译成 CSS等于给模型加了一个安全的“表达通道”效果可预期排查也容易。为了让 Vibe 描述更可控我还维护了一张“意图 → 规则”的映射表把用户常见表达翻译成具体的样式变化。比如用户说“干净、简约、留白多”系统会把间距倍率上调、去掉多余的边框和底纹、减少装饰线用户说“正式、商务、偏冷色”系统会把主色换成深蓝灰、标题改成短横线底边样式、字体用更中性的黑体系用户说“活泼、鲜明、有个人风格”系统会引入一个饱和度更高的强调色、技能改成彩色标签。这张表本身不参与模型推理只是产品逻辑帮助我确认翻译管的输出是不是真的覆盖到了用户的需求。2.4 PDF 渲染链路浏览器是最后一道保险Resume Hub 的预览和导出一律走 HTML/CSS 渲染前端先出实时预览然后调用无头浏览器把同一个页面打印成 PDF。技术栈上我选了 Puppeteer 跑 Chromium原因是简历页面本身是标准 Web 技术渲染的用同一套代码做预览和导出几乎没有偏差不用维护两套渲染逻辑。对比过 WeasyPrint 和 Typst前者对 CSS 的支持太弱很多现代布局属性不认后者渲染质量高但意味着要维护一套和 HTML 并行的模板成本翻倍在“用户描述 → 样式指令 → CSS 变量”这条主线下显得很绕。打印成 PDF 的配置有几个关键参数纸张 A4尺寸 210mm × 297mmmargin 按页面留白算参考值上下左右 12mm 到 15mm 之间。字号基准设在 10.5pt正文行高 1.4 左右这个组合在大多数屏幕上看起来紧凑又不压抑。打印时通过 Puppeteer 的page.emulateMediaType(print)把屏幕样式切换成打印样式再设置printBackground: true保住背景色和标签卡片的视觉效果。这个链路最大的坑在字体。简历 PDF 里中文是最容易出问题的Chromium 在 Linux 服务器上默认不带中文字体导出的 PDF 要么一片方块要么乱码。我最终方案是把字体文件打包进项目通过font-face引用 woff2 格式字体栈第一顺位用打包的思源黑体后面兜底系统字体。字体文件会增加包体积但稳定性提升是实打实的。后面专门有一节讲我在字体上踩的坑这里先不展开。3. 实操过程从零搭一个能跑的 Resume Hub3.1 技术栈选择宁愿无聊不要花哨Resume Hub 的前端选了 React Vite预览区用 iframe 隔离样式避免用户生成的版式污染编辑器本身的界面后端是 Node.js Express主要负责转发 AI 请求、做 schema 校验、管理生成任务数据存储先用 JSON 文件加简单索引单一用户的个人项目根本不到上数据库的量级。整个技术栈没有任何新东西全是稳定选项我的原则是项目价值在 AI 管线和排版引擎上基础设施能无聊就无聊。vibe 排版的核心 API 只有一个接收用户描述和当前简历数据返回样式指令集。接口内部先拼提示词再调用大模型输出 JSON 后做 schema 校验失败自动重试。整个调用链用异步任务跑前端轮询任务状态避免用户在接口前傻等。实测下来一次完整的“描述 → 出新版式 → 预览刷新”在 3 秒到 6 秒之间这个速度在可接受范围内。3.2 让 AI 从零整理经历实测一版提示词用户第一次进来通常会贴一大段语无伦次的经历“我在XX公司做过开发后来又跳去哪儿来着反正就是写代码还自学了 Python搞过一个小程序”。我实测的最好用的处理方式是不要求模型一次生成完整 JSON而是让它先做“信息抽取 问题标注”把不确定的部分标出来再返回结构化数据。给个实测效果相当不错的提示词版本你是资深职业简历顾问。下面是一段用户写的零散经历描述请把它整理成结构化简历数据。 用户描述 {user_text} 输出要求 1. 只输出合法的 JSON 数组每个 JSON 对象对应一段经历、一个技能或一段教育信息。 2. 字段必须符合给定 schema类型严格匹配。 3. 日期统一为 YYYY-MM 或 YYYY 格式无法确定的用 null。 4. 凡是没有在原文中出现过的公司、项目、数据指标一律使用 null 占位绝对禁止推测补全。 5. 每条经历总结 2 到 4 条成果优先保留可量化、有动词、有实际产出的句子。 6. 口语化的描述需要改写为书面语但不能改变原意。这套提示词的关键在于把“抽取”和“润色”合并成一件事同时用 null 兜住幻觉。用户描述里没写时间你就给我 null渲染层自动显示“至今”或者隐藏时间没写公司名就显示“某公司”。这个兜底机制让 AI 永远不会“编一个公司出来骗你”也让用户意识到信息缺失是可以随时补的。3.3 Vibe 排版功能落地用户说一句样式变一次Vibe 排版的交互循环我尽量做短。用户点击“Vibe 排版”按钮弹出一个输入框默认提示是“一句话描述风格比如简约、正式、双栏、技能区做成标签样式”。发送后前端把当前简历数据、当前样式指令集、用户描述一起发给模型模型输出新的样式指令集前端立即用新指令刷新 iframe 预览。整个循环的核心是“增量调整”用户说“再紧凑一点”模型收到的上下文里已经有上一轮的样式指令它会在这个基础上微调间距倍率而不是推倒重来。这里我必须强调一个容易踩的坑不要把包含完整简历数据的文本和样式指令混在同一个上下文里让模型自由发挥。模型一旦看到大量内容文本容易“忍不住”改内容。我的做法是用不同角色分隔简历数据走 system 或 user 角色固定段落用户对版式的描述走独立的、带明确边界的输入字段并且在提示词里写死样式管线的输出只涉及样式指令字段任何情况下不得修改简历数据。加了这条约束之后内容被误改的情况几乎为零。增量调整需要解决 “前后一致性”的问题否则调整三次之后版式会漂移得妈都不认识。我做了两个保险第一每次 UI 调整都直接把指令集里的 spacing.scale、palette.primary 这类 JSON 字段展示给用户用户看了不满意可以直接手动改透明度高第二模型在生成新指令时系统会把上一轮的指令 JSON 原样传过去并明确要求未涉及的字段保持原值。实测下来连续调五六次版式“只改需要改的”这条约束基本能稳住。3.4 一键导出 PDF从预览到打印的一百米预览稳了之后导出 PDF 反而是最需要抠细节的部分。Puppeteer 的打印流程我会先注入一个“导出专用类名”比如给 body 加is-exporting在这个类名下把预览里的交互元素隐藏掉、把间距微调成更适合打印的值。这个做法的好处是预览和导出共享一套代码但各自有微调空间不会出现“预览完美导出崩掉”的经典事故。导出完成后还有一道验证工序把生成的 PDF 重新解析一遍文本。我会用pdf-parse一类库把 PDF 文本抽出来检查几个关键点文本是否完整、顺序是否和页面视觉一致、有没有乱码或有缺失方块的现象。这一步是防 ATS 兼容问题的最后防线因为很多简历工具导出的 PDF 看起来漂亮但机器解析出来是一堆乱序碎片投出去直接进垃圾箱。Resume Hub 在导出后默认检查一次发现问题就提示用户重新生成虽然多了一步操作但换来的可靠性非常值。4. 常见问题与排查技巧实录4.1 AI 幻觉怎么拦重试不是万能的校验才是AI 生成简历数据时幻觉高发区有三个公司名字、项目名字、量化指标。防范的关键在于把校验尽量前置。我用 zod 做了严格的 schema 校验凡是字段值在用户原始输入中找不到对应文本的直接判非法。实现上是在提示词里要求模型对每个字段附上source标记标注该值来自哪段原文校验器拿到 JSON 后逐字段回查原文。这一步把幻觉率压到了很低。一个小技巧原文匹配时不区分全角半角也不做精确包含而是做“模糊包含”判断否则用户原句“XX科技”和模型输出“XX科技股份有限公司”会因为不完全相等被判非法。模糊匹配的度要自己调太松会放进幻觉太紧会让正常改写也被拦下。我的经验是先跑一批测试样本统计拦截率和误杀率再设定阈值这类参数不要拍脑袋。4.2 中文字体渲染Linux 服务器上的一片乱码这应该是我踩过最深的坑。开发环境是 macOS一切正常部署到 Linux 服务器之后导出的 PDF 里所有中文都变成方块。排查了半天问题根源就是服务器系统没装中文字体Chromium 找不到可用字体就直接渲染成空白方块。网上很多教程会让你在服务器上安装fonts-noto-cjk这在能控制服务器的场景下有效但换一台新机器就要重新部署太脆弱。我的最终方案是把字体文件打进前端资源包用font-face加载自带的 woff2 字体。要注意的是font-family的字体栈第一顺位必须用这个本地字体名称而不是系统字体名而且 Puppeteer 打印时font-face字体通常需要等待字体加载完毕才能正确渲染所以要加一段字体加载完成再触发打印的逻辑。这个坑花了我大半天时间查了无数资料才确认是异步加载问题不是字体文件损坏。4.3 分页劈叉与溢出简历打印的卫生问题简历内容稍长导出 PDF 就会出现各种卫生问题一段工作经历被从中间劈到第二页、标题孤零零挂在页面底部、长 URL 撑破布局、末尾多出一页空白。解决这些问题的核心武器就是 CSS 打印属性break-inside: avoid让单个条目尽量不跨页break-after: avoid防止标题成为页面孤儿overflow-wrap: anywhere让长链接可以安全断行。但这些属性应急还行长期还是要靠预检。我在导出前会跑一遍内容量估算把每条经历的字数、每个区块的高度、总行数测算出来用 A4 可容纳的行数做除法提前算出会出问题的区块直接提醒用户“内容预计超出两页建议压缩 XX 段经历的描述”。这个功能实现成本很低但用户体感提升巨大因为没人喜欢对着导出的 PDF 数页码才发现白费了功夫。4.4 速查表常见问题与我用的解法症状根因我采用的解法模型输出不是合法 JSON提示词约束不够硬加入“只输出 JSON不要解释”并后置 schema 校验失败自动重试两次AI 编造公司名/数据幻觉缺乏事实锚点要求字段附 source 标记回查用户原文模糊匹配校验中文 PDF 变方块服务器缺字体、字体异步加载失败本地font-face打包 woff2打印前等待字体加载完成经历被跨页劈开未设置打印分页属性条目加break-inside: avoid标题加break-after: avoid长 URL 撑破版面默认不换行overflow-wrap: anywhereword-break: break-all导出尾部多空白页容器高度残留导出前强制清空多余布局节点检查 PDF 页数与预期一致Vibe 调整后内容被误改样式与数据混在同一个上下文角色分离样式管线提示词严格限定只输出样式字段AI 生成耗时过长单次完整生成链路过重异步任务 前端轮询增量调整只传差异上下文5. 实操心得收尾这套东西还能往哪走整个 Resume Hub 做下来我个人体会最深的一点是AI 应用想做得顺手关键不在“让 AI 多做”而在“让 AI 只做自己擅长的那一小段”。内容生成、样式翻译、数据校验、PDF 渲染每一段都有各自适合的工具硬把全部职责塞给大模型必然翻车。结构化的数据模型是底座提示词工程是缰绳样式指令集是连接用户意图和渲染引擎的桥这条链路打牢了后面加什么功能都比在草稿上堆功能快得多。最后再分享一个小技巧简历生成这个场景用户对“版本对比”的需求比想象中强烈得多。我给 Resume Hub 做了简单的快照功能每次导出 PDF 之前自动存一份 JSON 快照用户可以随时回退到任何一个历史版本。这个功能代码量不大但实际使用率非常高因为用户反复调整后经常怀念“两小时前那版的感觉”有快照在手这种遗憾就少了很多。如果你也在做类似的生成类工具强烈建议一开始就把版本管理设计进去别等用户提需求了再补。后续我打算把“行业差异化”做进去比如针对外企习惯、国企风格、技术岗位、设计岗位分别调校排版和措辞口径再配一套更精细的样式指令集。这些扩展都建立在现有的结构化模型和翻译管线上不需要伤筋动骨这也就是当初坚持把层次拆清楚的回报。真做一遍之后你会认同我说的那句话简历生成器这行排版是脸面数据是骨架AI 是肌肉哪块都得练。