多彩编程 多彩编程MZPH · CODE BLOG
ARTICLE DETAIL

文章详情

深耕前端与后端开发技术的一线实战笔记与踩坑复盘。

AI简历生成器开发实战:Markdown中间格式与Vibe Coding排版实践

AI简历生成器开发实战:Markdown中间格式与Vibe Coding排版实践 把“AI 简历生成器”做成一个真正常用的工具比我想象中麻烦得多。我断断续续折腾了两周最后做出来一个叫 Resume Hub 的小项目一个以 Markdown 为中间格式、支持 Vibe Coding 排版的简历聚合站。这篇文章就是它的完整复盘从需求拆解、技术选型到落地过程、踩坑记录都会讲到。如果你正准备做类似的 AI 生产力工具或者单纯好奇“Vibe Coding 排版的简历工具”到底是怎么运转的这篇应该能给你一套可以直接抄作业的思路。先说结论AI 简历工具拼的不是模型有多聪明而是工程上把格式约束做得多死。模型负责聪明工程负责稳定。Resume Hub 的核心就是把“AI 生成内容”和“排版渲染”彻底解耦——先用 prompt 约束 AI 只输出结构化 Markdown再设计一套布局描述符让 AI / 用户以自然语言调整版式结构。整个开发过程也是用 Vibe Coding 的方式推进我只写骨架和关键约束剩下大量对话、试错、微调的工作都交给 AI 协作完成。1. 先搞清楚一个“顺手”的简历生成器难点在哪1.1 简历生成器的三个普遍痛点市面上 AI 简历工具不少但多数给人“生成一次就吃灰”的感觉。我复盘了一下自己过去用过的产品发现痛点基本集中在三个地方。第一是内容空心化。输入岗位描述后模型会生成一大堆“具备良好的沟通能力和团队协作精神”“参与了多个项目的开发与交付”这种正确但没有信息量的句子。这种内容拿去投简历HR 扫一眼就知道是模板。核心原因是 prompt 只要求“写简历”没有要求“基于事实提炼量化结果”也没有给模型足够的样例约束。第二是排版不可控。AI 输出 Markdown 或纯文本之后要落到一套好看的版式上非常折腾。很多工具的做法是让用户选择一个固定模板再把内容往字段里塞结果遇到“项目经历特别多”“某段经历描述特别长”这种真实情况时版式直接崩掉换行错乱、字体大小失衡、分页把一条经历拦腰截断。而且用户想微调某个区块的位置往往得进设置里找半天控件改完还不直观。第三是版本管理混乱。求职者一般不会只投一家公司针对不同岗位会调整简历侧重。实际场景里常见的操作是改完 A 公司的简历另存为一份新文件过几天又在原文件上改 B 公司的版本最后电脑里攒出十几份“简历最终版v7”自己也分不清哪份对应哪个岗位。这本质上缺一个“以人为核心、以岗位为维度”的简历管理中枢。Resume Hub 一开始就是奔着这三个痛点去的AI 负责内容优化和润色但所有输出都要落到一份有严格 schema 的数据模型里排版层和内容层分离用户可以像聊天一样描述“我想把技能块放到左边栏”AI 再把这个意图转成布局配置所有简历版本都挂在一个“简历库”里便于随时对比、复制、定向调整。1.2 Vibe Coding 为什么适合解决排版问题“Vibe Coding”这个词大家应该不陌生了核心是开发者描述意图、给出边界AI 大量生成代码并即时反馈人主要做评审和方向把控。它特别适合“结果可快速预览、修改频繁、机械劳动多”的任务——简历排版恰好是这种任务。想象一下传统流程用户想调整简历的视觉重心需要懂一点 CSS、知道模板文件的结构然后在某个样式文件里改grid-template-columns或order。如果用户没有前端经验这个操作基本做不了只能找现成模板凑合用。但用 Vibe Coding 的思路来做用户只需要说“我希望联系方式、技能、教育经历放左侧窄栏工作经历和项目经历放右侧宽栏整体看起来清爽一点主色调用深蓝色。”AI 接收这句话后把它映射成一份布局规范比如columns: [left,right]leftBlocks: [contact,skills,education]之类的 JSON渲染层再根据布局规范把简历画出来。这个交互模式在开发阶段也好用。项目初期前端界面、渲染逻辑、prompt 调优都是我一个人在推但每块“脏活累活”——比如写正则解析 Markdown、写 CSS 分页保护、写布局 JSON 的校验逻辑——我都直接让 AI 生成初版我拿过来改改边界条件就进版本库。这种“人设定意图和约束AI 负责实现细节”的开发方式本身就是 Vibe Coding 的落地样板。2. 整体架构与思路拆解我为什么把 Resume Hub 拆成三个模块2.1 数据层、生成层、排版层分离Resume Hub 的架构没有做得很重但有一件事我坚持下来了就是三层分离数据层、生成层、排版层。每一层只做一件事中间用明确的数据格式衔接。数据层负责“简历内容到底是什么”。我用一棵 JSON 树表达所有简历信息个人基础信息、教育经历、工作经历、项目经历、技能组、自我评价。这棵树和展示方式无关它是最底层的“事实库”。生成层负责“根据事实库和岗位 JD 产出优化版本”它的输出不是 HTML而是结构化 Markdown——每一条经历、每一行 bullet、每一个区块标题都遵循约定俗成的 Markdown 结构。排版层负责“把 Markdown 布局配置渲染成最终简历”它读取数据层的信息、生成层的 Markdown、以及用户/AI 给出的布局参数最终输出 HTMLCSS 并导出 PDF。听起来这可能有点绕但好处在后续迭代中非常明显想换一套视觉模板只需要改排版层的 CSS 和布局 JSON数据层和生成层完全不动想换一家模型的输出风格只需要调整生成层的 prompt数据层和排版层都不受影响。这个解耦带来的维护成本降低远远大于一开始多写几层接口的成本。2.2 多 AI 协作而不是一个 Agent 包打天下很多人做 AI 工具的第一反应是“一个聊天框把用户需求全丢给大模型”这在简历场景下效果很差。原因是一个模型同时承担内容总结、格式转换、岗位匹配分析、排版参数生成这些职责时很容易顾此失彼内容写得好但格式漂移格式稳定了但排版参数给出一堆非标准字段。我实际采用的是“多 AI 协作”的模式或者说按照角色拆成三个轻量 Agent每个 Agent 背后可能是同一个模型 API但 prompt 和上下文完全不同。第一个是内容生成 Agent负责根据原始简历和岗位 JD生成符合 STAR/量化原则的经历描述。它只输出 Markdown且被要求严格遵守“每条 bullet 以动词开头、包含可量化结果、不超过两行”的约束。第二个是审稿 Agent负责用另一个视角检查内容里有没有空话、重复表达、敏感表述并给出修改建议。第三个是排版 Agent负责把用户那句“我想要左侧窄栏、右侧宽栏”之类的自然语言转换成布局 JSON并在渲染前校验 JSON 是否合法。这个拆法的实际收益是问题更容易定位。如果生成出来的 Markdown 里出现了##三级标题结构混乱我知道是内容 Agent 的 prompt 约束不够如果页面渲染出来布局错乱我知道是排版 Agent 输出的 layout 有毛病而不是要去一坨巨型 prompt 里翻找。调试成本低了一个量级。2.3 为什么中间格式选 Markdown中间格式的选择一度让我纠结过直接用 JSON 最方便程序处理但 AI 直接生成 JSON 的稳定性其实没有想象中高容易多一个逗号、少一个引号就整体解析失败直接用富文本块类似 Notion 的 block 结构又太重大模型输出 block 数组的 token 消耗和格式错误率都很高。最后我选了 Markdown 作为生成层和渲染层之间的中间格式理由是它刚好卡在“人类可读写”和“程序可解析”的平衡点上。模型输出 Markdown 是当下最稳、最便宜的文本格式之一极少出现严重语法错误而 Markdown 的标题层级、列表结构又能很好地映射到简历的“区块—条目—要点”三级结构上。我只需要写一个几十行的解析器就能把 Markdown 转回 JSON 树再注入到模板里渲染。中间格式选型还有一个隐藏好处用户可以自己复制、粘贴、修改 Markdown改完回到系统里一键重新渲染。这意味着“让用户直接编辑底层文本”变成了一个可用的兜底方案而不是所有人都必须学 JSON 或 CSS。3. 核心实现从空页面到一份可用简历的关键路径3.1 简历数据模型与字段设计先说数据模型。Resume Hub 的数据结构我直接参考了常见 ATS简历自动筛选系统的字段习惯因为简历最终要被人力系统和 HR 阅读命名和分段越接近主流习惯越不容易被误判。核心的 TypeScript 类型简化后长这样type Resume { meta: { name: string; title: string; email: string; phone: string; location: string; website?: string; avatar?: string; }; summary?: string; education: Array{ school: string; degree: string; major: string; startDate: string; endDate: string; highlights?: string[]; }; work: Array{ company: string; role: string; startDate: string; endDate: string; location?: string; bullets: string[]; }; projects: Array{ name: string; role?: string; startDate: string; endDate: string; bullets: string[]; link?: string; }; skills: Array{ category: string; items: string[]; }; }这里有几个细节值得解释一下。第一每一段工作经历和项目经历里都有一个bullets: string[]这是简历内容的主体我要求所有经历描述都以数组形式存储原因是在排版层可以针对每条 bullet 分别做分页保护、缩进控制和重点标注而不是把整段经历当一个不可拆分的字符串。第二时间字段统一用YYYY-MM字符串方便后续展示时做“至今”“在职”等相对时间的计算也方便做按时间倒序排列。第三教育经历里留了一个highlights可选数组我实际用下来发现名校背景、绩点、主修课程这些信息并不是每个人都有需求做成可选数组比强制字段更灵活。数据模型定好之后下一步就是怎么把“用户口语化的经历描述”变成这种结构化 JSON。这个环节我用了一个小技巧先让用户粘贴自己现有的简历文本纯文本就行内容生成 Agent 先做一次“结构化抽取”把散乱文本里的公司、岗位、时间、经历要点映射到上面的 schema 里然后用户可以对每一条 bullet 单独提出修改要求。这样既保留用户自己的原始表达又能让 AI 在固定框架里做优化。3.2 内容生成 Agent 的 Prompt 设计与格式约束内容生成是整个工具的灵魂prompt 写得好不好直接决定产出的简历是“看起来像人写的”还是“一看就是 AI 生成的”。我经过多轮调优最终形成了一套固定的 prompt 骨架拆开看有四个组成部分角色定义、任务说明、输出格式、硬性约束。以下是一段简化后仍然可以直接拿去用的 prompt 示例你是资深招聘官兼简历优化专家。你只负责根据用户提供的原始经历和岗位 JD重写工作经历和项目经历的 bullet 列表。 任务要求 1. 每条 bullet 必须以“动作动词 工作对象 可量化结果”的结构展开。 2. 优先使用数字、百分比、金额、规模等可验证信息如果原文没有数字合理补全为量级描述但不要虚构精确数值。 3. 删除“负责”“协助”“参与”等弱动词尝试用“搭建”“重构”“推动”“落地”等强动词替代。 4. 每条 bullet 不超过 25 个汉字。 5. 输出格式 ### 工作经历 **公司名 | 职位 | 时间** - bullet 1 - bullet 2 ### 项目经历 **项目名 | 角色 | 时间** - bullet 1 - bullet 2 6. 不要输出任何其他内容不要输出解释、总结、开头问候语。这里最关键的是“输出格式”和“硬性约束”。最初版本我没给输出格式只说了“请优化我的简历”结果模型自由发挥有时输出大段总结有时用##二级标题有时把两条经历合并成一段后续解析逻辑根本没法统一。后来我把输出结构写死在 prompt 里并且用###和**字段**这种明确的 Markdown 标记解析器只要按规则找标题和列表就能稳定还原数据。实测格式漂移率从之前的 30% 以上降到了 5% 以下。另一个容易被忽略的点是“不要虚构精确数值”。简历场景里模型很容易编造一个看起来合理的“提升 30% 效率”这在简历造假审查严格的行业比如金融、医疗里是致命问题。我宁可让模型写“显著缩短交付周期”这种偏定性的描述也不能让它编具体数字去误导。这个边界需要在 prompt 里明确写出来。3.3 排版系统实现模板 布局规范 主题变量排版层是我投入时间最多、也是最能体现“Vibe Coding 排版”特色的部分。传统模板方案是每个模板对应一份固定 HTML/CSS 文件用户换模板等于换整套样式Resume Hub 的做法是把模板拆成“布局规范 主题变量”两个维度换视觉风格不用改布局改布局也不用重构样式。布局规范是一个 JSON 配置核心内容是列结构、区块顺序和每个区块的宽度占比举一个实际用到的例子{ templateName: classic-left-aside, columns: [left, right], leftWidth: 34, rightWidth: 66, blocks: { left: [contact, skills, education], right: [summary, work, projects] }, pagePadding: 14mm 16mm, primaryColor: #1f3a5f }渲染层拿到这份 JSON就知道要画几列、每列放哪些区块、主色用哪个色值。用户说“我想把教育经历挪到右边去”排版 Agent 就把blocks.left里的education移到blocks.right数组的头部并同步调整 JSON 里的顺序字段。整个过程用户看到的是聊天对话和实时预览变化不需要碰任何代码。主题变量则是一组 CSS 变量控制具体的视觉细节:root { --primary: #1f3a5f; --accent: #c0a062; --font-main: Noto Sans SC, PingFang SC, sans-serif; --font-size-base: 10.5pt; --font-size-name: 22pt; --line-height: 1.55; --bullet-gap: 3mm; --page-width: 210mm; --page-height: 297mm; }主题系统做了分层基础主题变量是所有模板共用的默认值模板自己的 CSS 类再基于这些变量做覆盖。这样换主题时只要新增一份 CSS 变量覆盖文件不用动渲染逻辑。我实测下来新增一套视觉主题的开发时间可以从原来的一两天压缩到 20 分钟左右基本就是改几个色值、字体、间距参数的事。PDF 导出是排版层最后一步。我用了无头浏览器方案把 HTML 页面加载到固定尺寸的 A4 页面里再调用打印接口生成 PDF。关键配置有两个一个是把页面宽度固定为 210mm在page规则里设置内容边距另一个是给经历条目加break-inside: avoid保证一条 bullet 列表不会在分页时被劈成两半。这个方案比直接用 HTML2Canvas 方案清晰度高很多也支持 CSS 分页控制中文字体渲染也更加可控。4. 实操中的典型问题与排查记录4.1 AI 输出 Markdown 结构漂移这个是最常见的问题也是我调 prompt 调得最久的一类。具体表现是模型前 80% 的内容都严格遵守了### 标题 bullet 列表的格式但到后面某个公司经历特别长的时候它突然把一条 bullet 展开成一段带句号的正常文字或者用普通文本-和有序列表1.混排。排查下来原因主要有三个。一是输出太长模型在长文本生成的末尾阶段格式注意力会下降二是原文里如果包含分号、冒号、日期格式等特殊符号模型容易顺着原文的措辞改变输出风格三是当某段经历描述本身包含多行时模型倾向于保留原文分段而破坏统一的 bullet 结构。解决方案我推荐双保险。第一道保险是在 prompt 末尾加一行“无论输入多长始终保持上述输出格式不要使用段落形式描述经历”第二道保险是解析端做一个容错正则——把所有以换行为起点的、开头不是-或*的连续文本自动尝试按句号、分号拆分并转成 bullet。这道保险不能保证 100% 语义正确但至少确保渲染不会崩用户可以手动微调个别条目的拆分。4.2 中文 PDF 导出乱码与字体缺失第一次跑通 PDF 导出的时候打开生成的文件差点没崩溃——中文全部变成了方块或乱码。原因是无头浏览器运行在精简版 Linux 容器里系统没有安装中文字体渲染时只能 fallback 到一个不存在的字体。解决方案是给容器安装 Noto Sans CJK 或思源黑体这类开源中文字体并在 CSS 里明确指定字体栈Noto Sans SC, Source Han Sans SC, sans-serif。另外有一个容易被忽视的坑字体文件较大思源黑体一个子集可能就有 10MB 以上需要把字体文件放到项目静态目录里而不是依赖系统级字体目录否则换一个部署环境就会再次出现乱码。之后我又遇到过一个问题PDF 文件在浏览器里看正常但用某些 PDF 阅读器打开后个别标点符号比如中文省略号显示为锯齿状。这个基本是字体子集化导致的解决办法是在字体加载配置里把 unicode-range 范围扩大或者干脆把常用中文标点的字形全部加载进来。我自己图省事直接加载了完整版字体文件大一点但显示效果最稳。4.3 内容“看起来专业但全是空话”怎么检测AI 生成简历还有一个隐蔽问题表面上每条 bullet 都符合结构读起来像是专业表达但把所有信息去掉后会发现什么实质内容都没讲。比如“负责多个项目的有序推进有效提升了团队协作效率”——这种描述没有任何可验证的细节。我加了一个“空话检测 Agent”它在审稿环节检查两条规则一是每条 bullet 是否包含至少一个具体的对象或技术名词比如“重构后端服务”“设计数据库模型”里有“后端服务”“数据模型”这种具体对象二是是否包含可量化的结果比如“缩短交付周期约 30%”“支撑日均 10 万请求”。如果整条 bullet 没有具体对象也没有结果审稿 Agent 就把它标记为“疑似空话”并建议用户补充细节或删除。这个规则机制做起来并不复杂本质上是几个关键词和正则的组合但效果立竿见影经过审稿 Agent 过滤后的简历至少在“信息密度”上肉眼可见地提升了一个档次。4.4 常见问题速查表我把实操中遇到过的高频问题整理成一张表供大家参考。问题现象根因解决办法AI 输出的 Markdown 标题层级混乱没有在 prompt 中限定标题级别指定只能使用###和**字段**并禁止输出其他标题经历条数多时排版错乱模板容器高度固定内容溢出区块容器不设固定高度由内容撑开并依赖分页控制生成结果中出现”1.”“2.”有序列表模型受到原文格式影响解析时把有序列表统一转成无序列表中文 PDF 字体异常环境缺少中文字体安装 Noto Sans CJK并用完整字体文件分页把一条经历切成两页没有设置避免断页属性给经历条目加break-inside: avoid;用户修改 Markdown 后 JSON 不同步双向绑定引发循环更新采用单向数据流Markdown 解析后直接覆盖 JSON用户编辑亦同步回 Markdown简历版本多了之后分不清缺少版本管理增加“岗位 修订时间”双维度命名并在列表页展示 diffPDF 导出太慢无头浏览器启动耗时使用常驻浏览器实例复用而非每次请求都新建5. 一些后续迭代思路和操作心得Resume Hub 做到现在我觉得它是一个“框架很好、细节还能再磨”的项目。后面我想加的扩展能力有几个方向简历质量评分。基于内容生成 Agent 和审稿 Agent 的结果给每份简历打一个“信息密度分”“量化分”“ATS 兼容分”让用户一眼看到自己简历的短板在哪里。多岗位版本对比。同一份基础简历针对三个不同 JD 生成三个版本然后把三个版本在同一个界面里做差异高亮类似 merge 工具那样用户可以快速判断哪段内容改过头了。投递追踪。把简历与投递记录关联起来记录每家公司的 JD 链接、投递时间、跟进状态这样“简历管理”就能变成一个完整的求职工作台。不过我要提醒一下扩展功能不能一上来就铺太开否则容易又一个功能都做不透。每加一个模块之前优先考虑它能不能复用已经建好的数据层和排版层如果复用不了宁可先不做。最后说一点我自己在反复踩坑后最有价值的体会。做 AI 简历生成器最大的风险不是模型不够聪明而是工程上对输出的约束不够。模型生成内容天生具有随机性如果你不把格式、字段、结构这些边界条件死死焊住它就会在长文本的后半段肆意发挥你的解析器、渲染器、审稿规则全都要跟着遭殃。反过来一旦数据结构和输出格式做到稳定可预期模型那点“聪明劲儿”就正好被用在刀刃上——提炼量化结果、重写弱动词、优化关键词密度。这个工具后续所有迭代我都会继续死守这一个原则。
返回列表