news 2026/11/4 9:38:23

基于React+FastAPI+DeepSeek的AI简历生成器实战:从Markdown到PDF全链路解析

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
基于React+FastAPI+DeepSeek的AI简历生成器实战:从Markdown到PDF全链路解析

1. 为什么我要做这个 AI 简历生成器

简历这东西,写过的人都懂。每次换工作,最痛苦的不是面试,而是对着 Word 调格式——行距差 0.5 磅、项目符号对不齐、导出 PDF 后字体莫名其妙变了。更别提投不同岗位还得改措辞、调重点,一份简历改十遍是常态。

我自己的痛点很具体:作为一个写了七八年前端的人,我习惯用 Markdown 写一切文档,但 HR 和招聘系统只认 PDF。市面上简历工具要么模板丑得离谱,要么导出要付费,要么改一行字得在富文本编辑器里跟光标搏斗半天。于是我想,能不能做一个工具——我用 Markdown 写内容,AI 帮我润色措辞,一键导出排版专业的 PDF。

这个项目就是这么来的:前端用React,后端用FastAPI,AI 能力接DeepSeek的 API,内容格式用Markdown,最终产出PDF。整套技术栈选型不是拍脑袋,而是每一环都有明确的理由。React 负责交互和实时预览,FastAPI 负责异步调用 AI 接口和 PDF 渲染,DeepSeek 负责把"负责了 XX 项目"这种干巴巴的描述改写成有数据、有动词、有结果的表达,Markdown 作为中间格式既方便人写也方便程序解析,PDF 则是最终的交付物。

这篇文章我会把整个项目的设计思路、技术选型、核心实现、踩过的坑全部摊开讲。适合谁看?如果你是会一点前端或后端、想做一个完整 AI 应用的开发者,或者你单纯想搞明白"一个 AI 产品从想法到落地要经历什么",这篇应该能给你不少参考。我不会只贴代码,更会讲清楚每个决策背后的权衡——这才是真正能复用的东西。

2. 整体架构设计与技术选型拆解

2.1 三层架构:前端、后端、AI 服务的职责边界

这个项目本质上是"输入文本 → AI 处理 → 渲染输出"的流水线,但拆成三层之后,每层的职责必须划清楚,否则后期维护会非常痛苦。

React 前端只做三件事:收集用户输入(Markdown 文本、目标岗位、风格偏好)、实时预览渲染结果、触发导出。它不碰任何 AI 逻辑,也不做 PDF 生成。为什么?因为 AI 调用需要密钥,密钥绝对不能出现在前端代码里,这是安全底线。而且 PDF 渲染在浏览器端做的话,中文字体处理会非常麻烦,交给后端更稳。

FastAPI 后端是核心枢纽,承担四类工作:接收前端请求、调用 DeepSeek API 做文本润色、把 Markdown 转成结构化数据、调用 PDF 渲染引擎生成文件。选 FastAPI 而不是 Flask 或 Django,核心原因是原生异步支持。调用 DeepSeek API 是典型的 IO 密集型操作,一次请求可能等好几秒,如果用同步框架,并发几个用户就卡死了。FastAPI 基于 Starlette,用async def写接口,配合httpx异步 HTTP 客户端,能轻松扛住并发。

DeepSeek API负责智能部分。我对比过几个模型,选 DeepSeek 的理由很实际:中文简历润色效果明显好于同价位的模型,API 价格便宜,响应速度也够快。它的deepseek-chat模型处理这种"改写+结构化输出"的任务非常合适。

三层之间的数据流是这样的:前端把 Markdown 原文和岗位信息 POST 给后端 → 后端组装 prompt 调用 DeepSeek → 拿到润色后的 Markdown → 解析成结构化 JSON → 渲染成 HTML → 转 PDF → 返回文件流给前端下载。整个链路清晰,任何一层出问题都好定位。

2.2 为什么用 Markdown 作为中间格式

很多人会问:为什么不直接让 AI 输出 JSON,或者直接输出 HTML?

我试过让 AI 直接输出 JSON,结果很不稳定。模型经常在 JSON 里漏逗号、多引号,或者把嵌套结构搞乱,解析失败率很高。而 Markdown 是模型训练数据里海量的格式,它对 Markdown 的"语感"极好,输出质量稳定得多。而且 Markdown 对人友好——用户可以直接看到 AI 改了什么,不满意还能手动微调。

Markdown 作为中间层还有个好处:内容和样式分离。用户只管写内容(标题、经历、技能),样式由后端的模板决定。想换简历风格?改模板就行,不用动内容。这跟前端开发里"结构、样式、行为分离"是一个道理。

这里有个细节要注意:Markdown 的换行规则很容易踩坑。标准 Markdown 里,单个换行符不会产生新段落,必须空一行才行。但简历里经常需要紧凑的列表项,所以我用了markdown-it这类解析器,并开启了breaks: true选项,让单个换行也生效。这个小配置不改,导出的 PDF 里所有内容会挤成一坨。

2.3 技术选型对照表

把关键选型和替代方案放一起对比,方便你判断是否适合自己复刻:

环节我的选择备选方案选择理由
前端框架ReactVue / Svelte生态成熟,Markdown 编辑器组件多
后端框架FastAPIFlask / Django原生异步,自动生成 API 文档
AI 模型DeepSeek其他大模型中文润色强,价格低,响应快
Markdown 解析markdown-itmarked / remark插件丰富,支持自定义渲染规则
PDF 生成WeasyPrintPuppeteer / wkhtmltopdf纯 Python,HTML+CSS 直接转,无需浏览器
异步 HTTPhttpxrequests / aiohttp同时支持同步异步,API 简洁

PDF 生成这块我重点说下为什么选 WeasyPrint。Puppeteer 需要跑一个无头浏览器,部署时依赖一大堆系统库,镜像体积大,冷启动慢。wkhtmltopdf 对现代 CSS 支持差,flex 布局经常渲染错乱。WeasyPrint 是纯 Python 库,直接吃 HTML 和 CSS,对@page规则、分页控制支持得很好,特别适合简历这种需要精确控制分页的场景。缺点是它对某些高级 CSS 特性支持有限,但简历排版用不到那些。

3. 核心模块的实操要点与避坑指南

3.1 FastAPI 项目目录结构怎么组织才不乱

项目一开始我图省事,所有代码塞在一个main.py里,写到第三天就崩溃了——AI 调用、PDF 渲染、路由、数据模型全混在一起,改一个地方怕影响另一个。后来重构成了下面这个结构,强烈建议你一开始就这么分:

backend/ ├── app/ │ ├── main.py # 应用入口,注册路由和中间件 │ ├── api/ │ │ ├── routes_resume.py # 简历相关接口 │ │ └── routes_export.py # 导出相关接口 │ ├── services/ │ │ ├── ai_service.py # DeepSeek 调用封装 │ │ ├── md_parser.py # Markdown 解析 │ │ └── pdf_service.py # PDF 渲染 │ ├── models/ │ │ └── schemas.py # Pydantic 数据模型 │ ├── core/ │ │ └── config.py # 配置和密钥管理 │ └── templates/ │ └── resume.html # PDF 渲染用的 HTML 模板 ├── requirements.txt └── .env

这个结构的核心思想是按职责分层:api层只管收请求、校验参数、返回响应;services层是真正的业务逻辑;models层定义数据结构;core放全局配置。这样当你想换掉 DeepSeek 换成别的模型时,只需要改ai_service.py一个文件,其他代码完全不动。

配置管理用pydantic-settings,把 API 密钥、模型名、超时时间都放.env文件里,代码里通过Settings类读取。千万别把密钥硬编码进代码然后提交到 Git,我见过太多人这么干然后密钥泄露被刷爆的。

# app/core/config.py from pydantic_settings import BaseSettings class Settings(BaseSettings): deepseek_api_key: str deepseek_base_url: str = "https://api.deepseek.com" model_name: str = "deepseek-chat" request_timeout: int = 60 class Config: env_file = ".env" settings = Settings()

3.2 DeepSeek API 调用:prompt 设计决定成败

整个项目里,最影响最终效果的不是代码,而是prompt 怎么写。我前后改了十几版 prompt,总结出几条硬经验。

第一,给模型明确的角色和输出格式约束。不要只说"帮我润色简历",要说清楚你是谁、要做什么、输出成什么样。我的 prompt 大致是这样组织的:

SYSTEM_PROMPT = """你是一位资深简历顾问,擅长把平淡的工作描述改写成有冲击力的表达。 要求: 1. 每条经历用"动词 + 具体工作 + 量化结果"的结构 2. 保留用户原有的 Markdown 结构,不要改变标题层级 3. 不要编造用户没提供的数据,如果原文没有数字,用占位符 [待补充] 提示 4. 只输出润色后的 Markdown,不要有任何解释性文字 """

这里有个关键点:明确禁止模型编造数据。早期版本我没写这条,结果模型自作主张给用户加了"提升效率 300%"这种数字,用户根本没法解释。简历造假是大事,这个坑必须堵死。

第二,用 few-shot 示例引导输出风格。光靠文字描述,模型对"有冲击力"的理解可能跟你不一致。我在 prompt 里塞了一两个改写前后的对比示例,效果立竿见影:

改写前:负责公司官网的开发和维护 改写后:主导公司官网重构,采用组件化方案将页面加载速度提升 40%,支撑日均 5 万次访问

第三,控制 temperature 参数。简历润色需要稳定、专业,不需要天马行空。我把temperature设成 0.3 左右,太高了模型会乱发挥,太低了又显得死板。这个值我实测调了好几轮,0.3 是个平衡点。

调用的时候用httpx.AsyncClient,设置合理的超时和重试:

# app/services/ai_service.py import httpx from app.core.config import settings async def polish_resume(markdown_text: str, target_role: str) -> str: payload = { "model": settings.model_name, "messages": [ {"role": "system", "content": SYSTEM_PROMPT}, {"role": "user", "content": f"目标岗位:{target_role}\n\n{markdown_text}"} ], "temperature": 0.3, "stream": False } headers = {"Authorization": f"Bearer {settings.deepseek_api_key}"} async with httpx.AsyncClient(timeout=settings.request_timeout) as client: resp = await client.post( f"{settings.deepseek_base_url}/chat/completions", json=payload, headers=headers ) resp.raise_for_status() return resp.json()["choices"][0]["message"]["content"]

注意:DeepSeek 的 API 兼容 OpenAI 的接口格式,所以如果你之前用过 OpenAI SDK,迁移成本几乎为零。但 base_url 一定要改对,路径是/chat/completions。

3.3 Markdown 转 PDF 的完整链路

这是整个项目技术含量最高的部分,也是坑最多的部分。链路是:Markdown → HTML → 加样式 → PDF。

第一步,Markdown 转 HTML。我用markdown-it-py,配置里开启表格、删除线等扩展,关键是breaks选项:

from markdown_it import MarkdownIt md = MarkdownIt("commonmark", {"breaks": True, "html": False}) html_body = md.render(markdown_text)

html: False是安全考虑,禁止用户输入的原始 HTML 被渲染,防止 XSS。简历内容虽然是自己写的,但万一以后开放给多用户,这个口子不能开。

第二步,套模板。我用 Jinja2 把 HTML 内容注入到一个完整的 HTML 模板里,模板里包含 CSS 样式和@page规则:

@page { size: A4; margin: 15mm 18mm; } body { font-family: "Noto Sans CJK SC", sans-serif; font-size: 10.5pt; line-height: 1.5; color: #333; } h1 { font-size: 20pt; border-bottom: 2px solid #2c3e50; } h2 { font-size: 14pt; color: #2c3e50; margin-top: 16pt; }

第三步,WeasyPrint 渲染:

from weasyprint import HTML def render_pdf(html_content: str, output_path: str): HTML(string=html_content).write_pdf(output_path)

看起来简单,但中文字体是最大的坑。WeasyPrint 默认字体不含中文,不配置的话导出的 PDF 里中文全是方块。解决办法是在系统里装Noto Sans CJK字体,然后在 CSS 里显式指定。Docker 部署时记得在 Dockerfile 里apt-get install fonts-noto-cjk,否则线上环境又是方块。

3.4 React 前端的实时预览实现

前端体验的核心是"边写边看"。左边是 Markdown 编辑区,右边是实时预览,用户改一个字右边立刻更新。这个用 React 的受控组件 +useMemo就能实现,不需要什么复杂状态管理。

function Editor() { const [markdown, setMarkdown] = useState(""); const previewHtml = useMemo(() => renderMarkdown(markdown), [markdown]); return ( <div className="editor-layout"> <textarea value={markdown} onChange={e => setMarkdown(e.target.value)} /> <div className="preview" dangerouslySetInnerHTML={{ __html: previewHtml }} /> </div> ); }

这里useMemo很关键,它保证只有 markdown 变化时才重新渲染,避免每次组件重渲染都跑一遍解析。dangerouslySetInnerHTML这个名字听着吓人,但只要你的 Markdown 解析器禁用了原始 HTML(前面说的html: False),就是安全的。

导出按钮点击后,把 markdown 和岗位信息 POST 给后端,后端返回 PDF 的二进制流,前端用 Blob 接收并触发下载:

async function handleExport() { const resp = await fetch("/api/export", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ markdown, targetRole }) }); const blob = await resp.blob(); const url = URL.createObjectURL(blob); const a = document.createElement("a"); a.href = url; a.download = "resume.pdf"; a.click(); URL.revokeObjectURL(url); }

提示:URL.revokeObjectURL一定要调用,否则每次导出都会在内存里留一份 Blob,导出多次后页面会变卡。这个内存泄漏很隐蔽,我调试了半天才发现。

4. 常见问题排查与实战经验

4.1 AI 返回内容格式错乱的排查思路

最常见的问题是模型不听话,返回的内容里夹带了"好的,以下是润色后的简历:"这种废话,或者把 Markdown 结构改乱了。排查分三步走。

先看 prompt 有没有明确约束。如果 system prompt 里没写"只输出 Markdown,不要解释",模型大概率会加废话。加上这条约束后,90% 的情况能解决。

如果加了约束还不行,就在代码里做后处理。比如用正则把开头的解释性文字切掉,或者检测返回内容里有没有 Markdown 标题符号,没有就重试一次。我封装了一个校验函数:

def validate_output(text: str) -> bool: # 至少要有一个 Markdown 标题 if not re.search(r"^#{1,3}\s", text, re.MULTILINE): return False # 不能包含明显的对话性开头 if text.strip().startswith(("好的", "当然", "以下是")): return False return True

校验不通过就带着"请严格按要求输出"的提示重试,最多重试两次。实测下来,加了这层校验后,格式问题的投诉基本归零。

4.2 PDF 分页和排版错位的处理

简历最怕的就是内容被硬生生截断在两页之间,比如一段工作经历被切成两半。WeasyPrint 支持 CSS 的分页控制属性,用好了能解决大部分问题:

.experience-item { break-inside: avoid; /* 单个经历块不跨页 */ } h2 { break-after: avoid; /* 标题后面不立即分页 */ }

break-inside: avoid是神器,它保证一个完整的经历块要么全在第一页,要么全在第二页,不会被切开。但要注意别滥用,如果一个块本身超过一页高度,这个属性会导致内容溢出。所以简历里每个经历块要控制长度,太长的经历该拆就拆。

还有个常见问题是页边距。@page里的 margin 和 body 的 padding 会叠加,如果两个都设了,实际边距会比你预期的大。我的做法是只在@page里设 margin,body 的 padding 设成 0,这样边距完全可控。

4.3 常见问题速查表

把开发过程中遇到的高频问题整理成表,方便你对照排查:

问题现象可能原因解决方案
PDF 中文显示为方块系统缺中文字体安装 Noto Sans CJK,CSS 指定字体
AI 返回夹带解释文字prompt 约束不足强化 system prompt,加输出校验
导出 PDF 内容挤成一坨Markdown 换行未生效解析器开启 breaks 选项
并发请求时接口卡死用了同步 HTTP 调用改用 httpx 异步客户端
导出多次后页面卡顿Blob URL 未释放调用 revokeObjectURL
经历块被分页截断未设置分页控制加 break-inside: avoid
密钥泄露风险硬编码在代码里用 .env + pydantic-settings

4.4 几个让我印象深刻的实操心得

第一个心得:AI 润色要提供"目标岗位"这个上下文。一开始我只把简历文本丢给模型,润色出来的内容很泛。后来加了目标岗位,模型会自动往那个方向靠——投后端岗就强调架构和性能,投管理岗就强调协作和带团队。这个改动让润色质量上了一个台阶,成本却几乎没增加。

第二个心得:给用户"对比视图"比直接替换更友好。早期版本 AI 润色后直接覆盖原文,用户看不到改了什么,心里没底。后来改成左右对比,改动的地方高亮显示,用户可以逐条接受或拒绝。这个交互改动让用户信任度明显提升,因为 AI 不再是黑盒。

第三个心得:流式输出能极大改善等待体验。调用 DeepSeek 润色一份完整简历要好几秒,如果一直转圈,用户会以为卡死了。我改成了 SSE 流式返回,AI 生成一个字前端就显示一个字,虽然总时间没变,但感知上快了很多。实现上用 FastAPI 的StreamingResponse配合前端的EventSource就行。

第四个心得:别过度依赖 AI。有些用户就是想自己写,AI 只是辅助。所以我把 AI 润色做成可选功能,用户可以完全手写 Markdown 然后直接导出。工具要适应人,不是人适应工具。

5. 这个项目还能怎么扩展

做到现在这个版本,核心链路已经跑通了,但能扩展的方向还很多。我列几个自己打算继续做的,也给你一些思路。

多模板系统。现在只有一套默认样式,可以做成模板市场,用户选不同的视觉风格。实现上就是把 CSS 抽出来做成可切换的主题,后端根据用户选择加载不同模板。这个改动不大,但产品感会强很多。

简历评分功能。让 AI 不只是润色,还能给简历打分并指出问题,比如"缺少量化数据""动词太弱""技能栈与目标岗位匹配度低"。这个用同一套 DeepSeek 调用就能实现,只是 prompt 换成评分维度。

多语言支持。投外企需要英文简历,可以让 AI 直接翻译并本地化表达习惯。注意不是直译,英文简历的表达逻辑和中文差别很大,得让模型按目标语言的习惯重写。

版本管理。用户可能针对不同岗位维护多份简历,需要保存历史版本、对比差异。这个用简单的数据库存储就能实现,前端加个版本列表。

技术栈上,如果用户量上来了,可以考虑把 PDF 渲染做成异步任务队列,用 Celery 或 FastAPI 的 BackgroundTasks,避免长请求占用连接。AI 调用也可以加缓存,同一份内容重复润色直接返回结果,省钱又提速。

我在实际使用这个工具的过程中最大的感受是:AI 应用的价值不在于模型多强,而在于你把模型嵌进了一个多顺滑的流程里。DeepSeek 只是其中一环,真正让这个工具好用的是 Markdown 的灵活性、实时预览的即时反馈、PDF 排版的稳定性,以及那些不起眼但决定体验的细节——比如分页控制、字体配置、流式输出。把这些细节打磨好,一个简单的简历工具也能让人用得舒服。如果你也在做类似的项目,希望这些踩坑经验能帮你少走点弯路。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/11/4 9:37:13

LLM增强的生态位进化算法设计与实现

1. 项目概述"Partition to Evolve: Niching-enhanced Evolution with LLMs for Automated Algorithm Discovery"这个标题揭示了一个将进化计算与大语言模型(LLMs)相结合的创新研究框架。作为一名长期跟踪AI算法发展的从业者&#xff0c;我认为这个方向代表了自动化算…

作者头像 李华
网站建设 2026/11/4 9:37:11

SQL Server动态SQL生成JSON分页接口与存储过程封装

简介&#xff1a;一份聚焦 SQL 自动生成 JSON 数据的 docx 技术文档&#xff0c;面向数据库开发、后端接口开发及需要将关系表数据快速转为 JSON 的初中级技术人员&#xff0c;重点解决手工拼接 JSON 字符串繁琐、分页查询与字段动态适配困难等问题。压缩包共 1 个 docx 文件&a…

作者头像 李华
网站建设 2026/11/4 9:37:10

SHAP模型可解释性实战:从Shapley值到可视化避坑

1. 为什么模型越准&#xff0c;团队越不敢用 我见过太多这样的情况&#xff1a;一个模型在验证集上把指标刷到了 0.92&#xff0c;业务方兴冲冲拿去上线&#xff0c;结果风控同事一句"这模型为什么把这位老客户拒了"就把整件事卡住了。指标再漂亮&#xff0c;答不上&…

作者头像 李华
网站建设 2026/11/4 9:37:05

Synopsys License服务启动与Checkout失败排查指南

简介&#xff1a;Synopsys工具license配置学习文档面向需要搭建本地学习测试环境的IC设计初学者与工程师&#xff0c;围绕license文件生成、路径指向与feature缺失排错等常见问题展开&#xff0c;帮助读者理解license管理的基本流程&#xff0c;并明确仅限学习使用。资源包仅含…

作者头像 李华
网站建设 2026/11/4 9:37:05

Django与大数据构建智能营养分析系统

1. 项目概述与核心价值这个基于Django和大数据技术的食物营养分析系统&#xff0c;本质上解决的是现代人面临的"营养信息过载"问题。我们每天接触的食物种类繁多&#xff0c;但很少有人能准确掌握各类食物的营养成分构成及其对健康的影响。传统营养分析工具往往存在三…

作者头像 李华
网站建设 2026/11/4 9:36:36

三数之和问题:排序与双指针算法详解

1. 问题背景与核心挑战 三数之和问题&#xff08;LeetCode 15题&#xff09;是算法面试中的经典题目&#xff0c;要求在一个整数数组中找到所有不重复的三元组&#xff0c;使得这三个数之和等于零。这个问题看似简单&#xff0c;但实际蕴含着多个需要巧妙处理的算法难点。 我第…

作者头像 李华