做 AI 简历生成器这事儿,起因特别朴素——我自己的简历改不下去了。市面上的 AI 工具试了一圈,生成的内容像流水线下来的,改一次排版乱一次,模板锁得死死的。后来我想明白了,问题不在模型能力,而在工具把内容、排版、AI 生成这三件事搅成了一大锅粥。所以我动手做了个 Resume Hub:内容交给 AI 整理,排版交给代码控制,模板修改还能用 Vibe Coding 的方式对 AI 下指令。这篇文章把设计思路、核心实现和踩过的坑一次性摊开,对想自己做简历工具、或者正在做 AI 结构化生成产品的朋友,应该能省不少弯路。
1. 为什么我会动手做一个简历生成器
1.1 市面工具的三大痛点
先说说我观察到的三大痛点,这不是拍脑袋想的,是真实用了一轮之后总结出来的。
第一个痛点是"AI 味"太重。你把真实的工作经历用大白话丢进去,它输出的项目描述几乎是一个模子刻出来的:开头"负责某某系统的设计与开发",中间"显著提升了某某效率",结尾"获得团队一致认可"。表面上看没什么毛病,但 HR 一天看几十份简历,这种模板化措辞扫一眼就能识别。简历内容的核心是差异化,你把差异化交给一个倾向输出平均值的模型,最后只能得到平庸文案。
第二个痛点是"改一次全乱"。我理想中的流程是先让 AI 把零散经历整理成结构化内容,我再手工微调措辞,最后套模板出排版。可是现成工具往往把这三步揉在一起——你想改项目描述里一个数字,它把整段重新生成一遍,排版也跟着变。改到后面你根本分不清哪版是对的,只能从头再来。
第三个痛点是排版自由度太低。大多数工具的模板就是萝卜填坑:标题、时间、公司单位置固定,你只能改改颜色换换字体。但简历恰恰是"每个人的结构都不一样"的东西,转行的人想把技能放最前,做学术的想突出论文,自由职业想展示作品集,固定模板根本装不下这些差异。我需要的不是"漂亮模板",而是"能按我的结构长出来的排版系统"。
1.2 Resume Hub 想解决的问题
想清楚痛点之后,我把需求收敛成一句话:内容与样式分离,AI 分别处理。简历本质上只有两部分——讲什么(内容)和怎么呈现(样式)。现有工具的问题是把这两部分绑得太死,动一个就牵连另一个,这是"不顺手"的根源。
Resume Hub 的流程是这样设计的:你先丢一段凌乱的真实经历给 AI,AI 把它整理成结构化 JSON——工作经历、项目经历、技能列表,每个条目都填好时间、公司、岗位、做了什么、结果如何。然后这套 JSON 喂给一个 HTML/CSS 代码模板,由模板负责渲染成排版清晰的简历。你想改内容,只改 JSON 字段,不会重建整个文档;你想换样式,只换模板,内容纹丝不动;同一份内容可以一键套多套模板,不需要复制粘贴重新排。
Vibe Coding 则是这套设计里最亮的一点。它本来指的是"用自然语言指挥 AI 生成和修改代码"的工作方式,我把这种方式直接接在了模板修改上。比如你想让技能栏从两列改成一列,不用自己翻 CSS,直接跟配套的 AI 助手说"技能改成单列、整体紧凑一点",它会帮你改模板代码。对大多数不懂代码的求职者来说,这是"顺手"的关键一环。
2. 设计思路:内容与样式彻底分离
2.1 为什么模板要代码化
最开始我也考虑过拖拽式编辑器,做了个小原型实验,很快放弃了。拖拽式的痛点是:你往下拖一个模块,周围的元素要跟着让位,间距、对齐、是否换页全靠肉眼判断;改配色要在好几个面板里来回切;等你做出满意的样式,想复制一份到另一份简历上,只能整页复制。总之是"操作门槛低,但是折腾成本高"。
代码化模板反而是更优解。用 HTML/CSS 描述简历样式,天生自带精确控制:一个 padding 值就是固定的,一行grid-template-columns: 1fr 2fr就决定了左右栏宽度,不用猜。模板本身就是文本文件,可以放进 Git 做版本管理,每次改动都有记录,改坏了随时回滚。同类型的模板可以抽象成组件,一份内容换主题不过是替换一个 CSS 文件的事。
更现实的一点是:在 AI 时代,代码是唯一能让 AI 直接修改的样式载体。拖拽配置项散布在界面各处,AI 很难精准操作;而 HTML/CSS 就是文本,大模型天然擅长改文本。你想实现"对 AI 说人话改排版",前提就是排版本身必须代码化。这个判断,是我从一开始就押着全栈走的核心决策。
2.2 Vibe Coding 在简历场景里的实际含义
Vibe Coding 是过去一年在开发者圈子里火起来的概念,核心是:你用自然语言描述意图,AI 帮你生成或修改代码,你在旁边做审查和微调。它强调的是"用直觉驱动编码"——你把想法说出来,代码就长出来了。
把 Vibe Coding 用到简历排版上,是我觉得最有价值的地方。传统流程里,你选了模板之后想微调,遇到不懂 CSS 的情况就只能放弃;懂一点 CSS 的人也得先找到对应类名,再改属性值,来回刷新预览几次才能确认。有了 Vibe Coding 能力之后,整个闭环缩短成一句话:"把项目经历的时间提到公司名前面""技能标签加个浅色背景""整体行距缩到 1.2",AI 直接给你改好,你预览确认就行。
为了让这个功能真正可用,我在模板代码的注释里做了点埋点。每个 CSS 区块上方都写了清晰的语义注释,比如/* skills: two-column list */,这样 AI 在修改时能准确定位要动的位置。实测下来,明确的注释比让 AI 自己去猜整个模板的结构要可靠得多,这也是一个很小的工程经验,但效果非常明显。
2.3 数据模型:先定 Schema 再动手
整个项目里我最得意的一个设计,是先把简历数据模型定得死死的,再写任何功能。当时我并不知道这有多重要,是踩了几次"AI 输出格式飘忽不定"的坑之后才悟出来的:如果连目标结构都不明确,AI 每次输出的 JSON 都会在字段命名和嵌套层级上发生细微漂移,下游渲染代码就得不断打补丁。
我用的 JSON Schema 大致是这样:
{ "basicInfo": { "name": "姓名", "title": "一句话定位", "email": "", "phone": "", "location": "", "links": [{ "label": "GitHub", "url": "" }] }, "summary": "两到三句个人简介", "workExperience": [ { "company": "公司名", "position": "职位", "startDate": "YYYY-MM", "endDate": "YYYY-MM 或 至今", "highlights": ["成果1", "成果2"] } ], "projects": [ { "name": "项目名", "role": "承担角色", "startDate": "", "endDate": "", "description": "一句话项目背景", "highlights": ["具体做了什么", "量化结果"], "techStack": ["技术名"] } ], "skills": [{ "category": "前端", "items": ["Vue", "React"] }], "education": [ { "school": "", "major": "", "degree": "", "startDate": "", "endDate": "" } ] }这套 Schema 覆盖了我调研过的百份真实简历里的高频模块。字段全部用复数数组表达"一条简历里会有多条经历"的常识,每条经历内部用highlights数组承载具体成果,而不是用一个长字符串,这样渲染时可以逐条加项目符号。日期统一用YYYY-MM的字符串格式,保证前端排序和展示都稳定。
这个 Schema 有三个隐形好处。第一,它成了全项目的事实标准——后端存储、前端渲染、AI Prompt、模板变量全部围着它转。第二,它让 AI 的输出有了"锚点",因为我在 Prompt 里直接贴了这份 JSON 结构。第三,它让模板组件的接口稳定了,模板作者写代码时永远知道能用哪些字段,不会出现"某个模板能显示,另一个模板报错"的怪事。
3. 核心实现:AI 生成、解析、渲染的关键链路
3.1 让 AI 稳定输出结构化数据的三招
AI 生成简历内容,最大的工程难点不是"生成得好不好",而是"每次输出格式稳不稳定"。我在调 Prompt 的过程中总结了三个很实用的招式。
第一招,角色和任务分开写。先告诉 AI 它是什么角色,比如"你是一名有 10 年经验的简历顾问",再明确任务"把用户提供的原始经历整理成符合给定 JSON 结构的简历数据"。分开写比混在一起写效果稳定得多,角色设定帮模型建立了输出风格,任务描述则限制了输出形式,两者分开不容易互相污染。
第二招,给一个严格的"输出模板"。我不是仅仅说"返回 JSON",而是直接把完整的目标结构贴进 Prompt,并注明"只允许返回 JSON 本身,不要包裹 markdown 代码块,不要添加任何解释"。这里有个细节:一定要在 Prompt 里写一句"不要返回任何前缀或后缀",否则模型经常会在 JSON 前面加一句"好的,这是整理后的简历数据"。
第三招,few-shot 示例胜过长篇规则。与其写五条"日期格式要统一、时间新到旧排列"之类的规则,不如直接在 Prompt 里放一条示例输入和示例输出。模型对示例的跟随能力比对规则强很多,做过类似东西的朋友应该深有体会。
我实际在用的 Prompt 模板大概长这样:
你是资深简历顾问。请把用户提供的原始经历整理成简历数据。 要求: 1. 严格按下方 JSON 结构输出,字段名和嵌套层级不得更改。 2. 只返回 JSON 本身,不包裹代码块,不输出任何解释。 3. 日期一律使用 YYYY-MM 格式,"至今"用 "present"。 4. 把口语化表述改写成专业书面语,但保留具体数字和事实。 5. highlights 数组中每一条控制在 20 字以内的一句话,避免空洞。 目标结构: {...完整 Schema 示例...} 用户原始经历: """${rawText}"""这套 Prompt 用下来,在主流大模型 API 上的成功率能到九成左右。剩下的幺蛾子,就交给解析层去兜底。
提示:Prompt 里的目标结构建议直接从代码里的 Schema 定义动态拼接,不要手写硬编码。手写容易和代码里的定义漂移,动态拼接能保证 Prompt 和渲染永远看到的是同一套结构。
3.2 JSON 容错解析层
不要指望大模型每次输出都规规矩矩,我后来拿真实数据统计过,格式问题千奇百怪。最常见的是把 JSON 包在三个反引号代码块里——模型觉得这样"清晰",但对程序来说就是一个必须剥掉的外壳。其次是 JSON 里混入注释、尾部多了一个逗号、字符串里用了中文引号,还有字段名被模型悄悄改成company_name这种下划线风格。
我的处理方式是写一个多级容错解析层,按顺序尝试:先提取代码块内部内容,再做括号配平——从字符串里找到第一个{和最后一个},裁剪出疑似 JSON 的区间;然后尝试用 JSON.parse 解析,失败就进入修复模式:去掉尾部多余逗号、把中文引号替换成英文引号、删除常见的//注释行。如果这些招都用完还解析失败,就返回一个明确错误提示,让用户点"重新生成"而不是干瞪眼。
这个解析层不复杂,项目里大约一百行代码,但它直接决定了产品的可靠感。一个"十个请求有两次解析失败"的工具,体验上和"偶尔抽风一次"的工具是两种完全不同的口碑。从第一天起就把它当一等公民对待,是工程的常态。
3.3 渲染与分页控制:让 PDF 不翻车的细节
数据解析成功之后,剩下的是渲染问题。我的渲染链路其实很朴素:数据绑定到 HTML 模板,模板加载对应主题 CSS,然后浏览器渲染成页面,再通过调用打印接口导出 PDF。
这里最值得一提的细节是分页控制。简历是要打印或导出 PDF 的,而浏览器默认的流式排版会把一条经历拦腰切断:上半页结尾是项目名,下一页开头是成果列表,非常难看。解决办法是在模板 CSS 里给经历条目设置break-inside: avoid,告诉浏览器"这一块尽量不要拆开";再配合break-before: page控制是否强制换页,比如教育经历总要单独起页。
中文排版还有两个容易忽略的点。一是中文字符的换行逻辑和英文不同,很多浏览器默认不处理超长 URL 和英文串,导致一个链接顶出去破坏排版,需要设置overflow-wrap: break-word。二是中文字体度量不一致,简体中文常见的字体之间行高差异肉眼可见,我会在模板里明确指定font-family和line-height的固定值,避免不同机器渲染出不同效果。这些都是很小的坑,但每一个都能让"顺手"的感觉打折扣。
4. 实操过程:从零搭一个 Resume Hub
4.1 技术选型与项目结构
技术选型上我坚持"怎么省事怎么来",因为这是一个个人工具,维护成本要压到最低。前端我选了 Vue 3 + Vite,理由很简单:单文件组件写模板预览最顺手,启动快,生态也成熟。后端直接用一个 Node.js 的轻量服务,提供几个接口:保存简历、拉取模板列表、调用大模型 API 生成结构化内容。数据库没有上重型的,用了 SQLite,因为它足够支撑个人工具的规模,而且一个文件备份全带走。
项目的目录结构大概是这样:
resume-hub/ ├── server/ # Node.js 后端 │ ├── routes/ # 接口路由 │ ├── llm/ # 大模型调用与 prompt 模板 │ └── db/ # SQLite 数据访问 ├── web/ # Vue 3 前端 │ ├── components/ # 编辑面板、预览面板 │ └── templates/ # 内置简历模板(HTML/CSS) ├── shared/ │ └── schema.ts # 前后端共用的简历数据模型 └── scripts/ # 模板构建、测试用脚本Shared 目录里的 schema 文件,前后端都从它引入,保证数据模型只有一份定义。这个设计帮我避免了好几类"前端改了字段、后端没跟上"的 bug,也让我在做 AI 生成时能直接引用它来构造 Prompt。
4.2 核心代码:解析与渲染的关键实现
数据模型用 TypeScript 写的话,最早的定义其实很简单,后面根据使用场景慢慢加字段。我贴一段相对完整的关键逻辑:解析大模型输出的函数。
function parseLLMJson(raw: string): ResumeData { // 1. 剥离 markdown 代码块外壳 let text = raw.replace(/```json|```/g, '').trim(); // 2. 括号配平:找到第一个 { 和最后一个 } const start = text.indexOf('{'); const end = text.lastIndexOf('}'); if (start === -1 || end === -1) { throw new Error('输出中未找到 JSON 结构'); } text = text.slice(start, end + 1); // 3. 直接解析,不行再修复 try { return JSON.parse(text); } catch { // 常见修复:尾逗号、中文引号、注释行 text = text .replace(/,\s*}/g, '}') .replace(/[\u201c\u201d]/g, '"') .replace(/\/\/[^\n]*/g, ''); return JSON.parse(text); } }这段代码看着简单,其实每一个正则都对应一类真实踩过的坑,第 2 步的括号配平尤其关键,能处理大量"解析报错但主体内容没问题"的情况。
渲染侧的核心逻辑更简单:把数据对象传入模板函数,模板函数返回完整的 HTML 字符串。我在内置模板里给每个区块都写了注释,方便 AI 精确修改。下面是一个简化的模板片段:
<section class="experience"> <h2>工作经历</h2> <!-- experience list, one card per item --> {{#each workExperience}} <div class="item"> <div class="header"> <span class="company">{{company}}</span> <span class="date">{{startDate}} - {{endDate}}</span> </div> <div class="position">{{position}}</div> <ul> {{#each highlights}} <li>{{this}}</li> {{/each}} </ul> </div> {{/each}} </section>注意:改别人写好的模板时,先看清根 class 再动手。AI 一旦误用了全局选择器,会把所有模板都改坏,所以每个模板我都用唯一的根 class 作为作用域边界。
4.3 一次完整的实战演示
我拿自己的一段真实经历演示一遍完整流程,你会发现从凌乱的原始信息到一份可看的简历,中间几乎不需要碰任何格式。
先看用户输入,也就是最原始的素材:
2021-2023 年在某某科技做前端,主要做的是内部后台系统重构,用 Vue3 重写了大概 20 个页面,原来系统打开要 4 秒,重构之后 1 秒内。还带过两个实习生。后来还做了个组件库,被公司其他三个项目引用了。
这段输入很口语化,信息密度不算低,但结构是乱的。丢给 Resume Hub 的 AI 生成模块之后,它整理出来的数据大概是:
{ "workExperience": [{ "company": "某某科技", "position": "前端工程师", "startDate": "2021-01", "endDate": "2023-06", "highlights": [ "主导后台系统重构,用 Vue3 重写 20+ 页面", "优化首屏加载,打开耗时从 4s 降至 1s 内", "搭建内部组件库,被 3 个业务项目复用", "带教 2 名实习生的日常开发工作" ] }] }这里有个细节值得注意:原素材里"带过两个实习生"其实没什么可量化的价值,AI 把它收拾成了"带教 2 名实习生的日常开发工作"这种能进简历的写法,没有乱编数字,这就是好的生成。接下来这份 JSON 直接套上模板,配合对应 CSS,一份排版干净的 PDF 就出来了。如果你想换个风格,一键切换模板,内容还是这份 JSON,渲染出来又是另一种气质。整个过程的核心价值就是:再也不用为排版翻车重头来一遍了。
5. 踩坑记录与问题排查
5.1 高频问题速查表
我把实际使用中遇到的高频问题整理成一个速查表,每个问题都附上现场表现和我的解法,遇到类似情况可以直接对着查。
| 问题现象 | 出现场景 | 根因 | 解决方法 |
|---|---|---|---|
| AI 返回内容被代码块包裹 | 生成结构化内容时频繁出现 | 模型习惯用 markdown 表达 | 解析层优先剥离代码块,同时 Prompt 中强调不要包裹 |
| JSON 解析报错,提示有尾逗号 | 输入较长的经历时 | 输出含,}或,] | 正则统一去掉尾逗号再解析 |
| 两段经历显示在同一页、被截断 | 导出 PDF 后检查发现 | 未设置分页保护 | 给条目加break-inside: avoid |
| 模板改过颜色后其他模板也变色 | 用 AI 改模板时 | AI 修改命中全局选择器 | 每个模板用唯一根 class,并建议 AI 只改该 class 内样式 |
日期显示成2021-01-01 | 套用模板渲染后 | Schema 中日期格式未严格执行 | Schema 约束 + Prompt 明确YYYY-MM |
| 英文 URL 把页面撑爆 | 简历里放 GitHub 链接 | 长串不换行 | overflow-wrap: break-word |
| 改内容后预览还是旧数据 | 前端缓存问题 | 编辑保存后未刷新 store | 保存后强制走一遍数据重载流程 |
| AI 把经历里的公司名写成同类大厂 | 生成时幻觉 | 模型补全未知信息 | 在 Prompt 中写"不得虚构事实,不确定留空" |
| 不同电脑渲染行高不一样 | 导出 PDF 发给别人看 | 字体度量不一致 | 模板固定 font-family 与 line-height |
| 分页时项目标题和内容被拆开 | 项目经历跨页 | 标题和第一个列表项之间无绑定 | 标题与内容用break-after: avoid关联 |
这张排查表对开发过程本身就是个大价值。初期每隔几天就会遇到新幺蛾子,我总是先打开这个表从头扫一遍,命中了就直接按解法处理;没命中就把问题、根因、解法补进去。这样迭代两周之后,新增问题的频率明显下降,文档慢慢就成了项目的活字典。如果你也在做类似的 AI 生成型工具,建议从第一天就维护一份自己的问题清单,别小看它的作用,很多看起来玄学的 AI 输出问题,回头看都是能归纳出规律的。
5.2 五条用真金白银换来的心得
最后分享几条在开发过程中换来的心得,我觉得比任何功能列表都有价值。
第一条:先定 Schema,再写 Prompt,再写 UI。顺序不能反。我最早是反着来的,先做了一个漂亮的 UI,结果 AI 输出格式一直对不上,UI 渲染各种崩,后来才推倒重来。定好 Schema 之后,整个项目一下子顺了,因为所有模块都拿到了一个稳定的契约。
第二条:给 AI 一个"坏例子"。Prompt 里除了好示例,最好还写一句"不要把经历写成:负责某某系统、提升了效率"这种空泛描述。我发现 AI 对负面约束的遵循度比我想象中好,明确点名要避免的坏例子能明显降低"AI 味"。
第三条:模板注释是给人看更是给 AI 看的。我在每个模板模块上方都写了语义化注释,这不仅方便我自己维护,更重要的是让 AI 在修改模板时能准确定位到要动的代码块。实测中,带注释的模板被 AI 改对的概率明显高于没注释的。
第四条:历史版本永远要保留。用户改简历一定会反复,我在每次保存时都记录一份 JSON 快照。出现"上一版比这一版好"的情况,点一下就能找回,这种细节对"顺手"的感知提升非常大。
第五条:调用大模型生成时,去重和缓存很有价值。同一个输入内容用户可能会反复生成几次,我在后端做了一层结果缓存,同样的原始文本命中缓存后直接返回,既省钱又减少等待时间。个人工具的 API 成本本来就不高,但体验的稳定性能带来完全不同的使用感受。
写到这儿,Resume Hub 基本把我想要的"顺手"做到了:内容、排版、AI 各司其职,想改哪里就改哪里,不用陪它重新走一遍流程。我自己的体会是,这类 AI 工具拼的往往不是某一项功能多惊艳,而是整个链路里的小细节有没有被照顾到——输入是不是顺手、解析是不是扛得住、模板是不是改得动、历史版本找不找得回。如果你也在做类似的 AI 生成型工具,我的建议就一句话:先搭数据骨架,再让 AI 往里填内容,最后用模板和容错解析兜住所有不确定性。剩下的,就是把它用起来,在真实需求里慢慢磨。