1. 从一堆「简历-最终版2.docx」说起这个项目到底在解决什么问题如果你找过工作大概率经历过这样的场景电脑桌面上躺着「简历.docx」「简历-修改版.docx」「简历-最终版.docx」「简历-最终版2.docx」「简历-最终版2-真的不改了.docx」投出去的每一份简历都对应着一个不同的岗位方向改到最后自己都记不清哪一版投了哪家公司。更麻烦的是现在很多在线简历工具会把你的信息上传到云端你根本不知道自己的手机号、工作经历、薪资期望被存到了哪里、用来做了什么。ResumeForge 这个项目就是冲着这两个痛点来的。它是一个本地优先的 AI 求职工作台核心思路很简单所有简历数据存在你自己电脑上的 SQLite 数据库里AI 只负责帮你优化措辞、匹配岗位、生成针对性版本但绝不替你编造经历。项目技术栈是 React 前端 FastAPI 后端 SQLite 存储整体架构轻量一台普通笔记本就能跑起来。这个项目适合几类人正在找工作、需要管理多个简历版本的求职者对数据隐私敏感、不想把个人信息交给第三方平台的人以及想学 React FastAPI SQLite 全栈开发、找一个真实项目练手的开发者。哪怕你之前没接触过 FastAPI 或者 SQLite跟着思路走也能理解整个系统是怎么搭起来的。我之所以关注到这个项目是因为它踩中了一个很实际的矛盾AI 改简历确实好用但用不好就容易变成「AI 替你编」。ResumeForge 的做法是把 AI 限制在「润色」和「匹配」两个环节所有事实性内容必须由你本人确认数据库里存的是你的真实经历AI 生成的内容会明确标注哪些是建议、哪些是原文。这个设计思路值得展开聊聊。2. 整体架构设计为什么选 React FastAPI SQLite 这套组合2.1 本地优先意味着什么「本地优先」这个词这两年出现频率很高但落到实际项目里它意味着一系列具体的技术决策。ResumeForge 的选择是数据库文件放在本地磁盘上后端服务跑在 localhost前端通过浏览器访问本地端口。你的简历数据不会离开你的电脑除非你主动导出或者调用云端 AI 接口。这和在线简历工具的逻辑完全不同。在线工具的数据存在厂商服务器上你登录才能用断网就废厂商跑路你的数据也跟着没。本地优先的方案里SQLite 数据库就是一个文件你可以随便复制、备份、加密甚至直接扔进网盘同步。FastAPI 后端只负责读写这个文件React 前端只负责展示和交互三层各司其职。提示本地优先不等于完全离线。ResumeForge 在调用 AI 优化功能时仍然需要联网请求大模型接口但你的原始简历数据只在本地流转发给 AI 的只是你需要优化的那段文本。2.2 为什么是 SQLite 而不是 MySQL 或 PostgreSQL这个问题我被问过很多次。SQLite 的优势在本地优先场景下非常明显零配置、单文件、无需独立服务进程、跨平台。你不需要在电脑上装一个数据库服务也不需要配置用户名密码和端口Python 标准库直接就能操作 SQLite 文件。对比一下几种常见选择数据库部署复杂度适合场景ResumeForge 是否适用SQLite极低单文件本地应用、小型工具非常适用MySQL中等需独立服务多用户 Web 应用过重PostgreSQL中等偏高复杂查询、高并发过重JSON 文件极低极简配置存储查询能力弱SQLite 的另一个好处是生态成熟。你可以用 DB Browser for SQLite 这个可视化工具直接打开数据库文件像操作 Excel 一样查看和编辑简历数据。对于不熟悉命令行的用户来说这一点很友好。宝塔面板用户也可以在面板里安装 SQLite 管理工具不过对于本地桌面应用来说直接装 DB Browser 更直接。2.3 FastAPI 在后端扮演的角色FastAPI 负责三件事接收前端请求、操作 SQLite 数据库、调用 AI 接口。它的优势是异步支持好、自动生成 API 文档、类型提示友好。对于 ResumeForge 这种需要同时处理文件读写和外部 API 调用的场景FastAPI 的异步能力可以避免请求阻塞。一个典型的 FastAPI 项目目录结构大概是这样resumeforge/ ├── app/ │ ├── main.py # 入口注册路由和中间件 │ ├── models.py # SQLAlchemy 数据模型 │ ├── schemas.py # Pydantic 请求/响应模型 │ ├── database.py # SQLite 连接和会话管理 │ ├── routers/ │ │ ├── resumes.py # 简历 CRUD 接口 │ │ ├── ai.py # AI 优化接口 │ │ └── jobs.py # 岗位匹配接口 │ └── services/ │ ├── ai_service.py │ └── resume_service.py ├── data/ │ └── resumeforge.db # SQLite 数据库文件 └── requirements.txt这个结构的好处是职责清晰。routers 只负责处理 HTTP 请求和返回响应services 里放业务逻辑models 定义数据结构。后面要加新功能比如导出 PDF 或者接入新的 AI 模型只需要在对应目录里加文件不会把代码搅成一团。2.4 React 前端的选型考量React 在这个项目里承担的是交互层的角色。简历编辑需要频繁的状态更新比如你改了一个字段右侧预览要实时变化AI 优化需要展示加载状态和对比结果多版本管理需要列表和详情切换。这些场景用 React 的组件化和状态管理来做比较顺手。项目里用到了 SSEServer-Sent Events来实现 AI 生成内容的流式展示。这个选择很实际AI 优化简历措辞时如果等全部生成完再显示用户会盯着空白屏幕好几秒用 SSE 可以让文字像打字机一样逐字出现体验好很多。相比 WebSocketSSE 更轻量单向推送足够用不需要额外维护双向连接。3. 数据库设计简历数据怎么存才合理3.1 核心表结构拆解ResumeForge 的数据库设计围绕「简历」这个核心实体展开。最基础的表包括resumes 表存简历的基本信息比如简历名称、目标岗位、创建时间、更新时间sections 表存简历的各个模块比如教育经历、工作经历、项目经验、技能标签entries 表存每个模块下的具体条目比如某一段工作经历的具体描述ai_suggestions 表存 AI 生成的优化建议关联到具体的 entry标注是否被采纳这种三层结构的好处是灵活。一份简历可以有多个模块一个模块可以有多个条目每个条目可以独立接受 AI 优化。你改某一段工作描述时不会影响到其他部分。3.2 为什么不用一个大 JSON 字段存所有内容有人可能会想简历数据直接用一个 JSON 字段存不就行了简单是简单但会带来几个问题。第一查询困难。你想找出所有包含「Python」技能的简历JSON 字段需要全表扫描加字符串匹配而结构化表可以直接用 SQL 查询。第二更新粒度粗。改一个标点符号就要重写整个 JSON并发场景下容易冲突。第三AI 建议无法关联。你需要知道某条建议是针对哪个条目的JSON 里很难做这种细粒度关联。SQLite 支持 JSON 字段类型但 ResumeForge 选择了结构化表设计这是从长期可维护性考虑的。当然如果你只是做个极简原型JSON 字段也不是不能用只是后面扩展会麻烦。3.3 SQLite 的 PRAGMA 调优SQLite 默认配置偏保守本地应用可以适当调优。几个常用的 PRAGMA 设置# database.py 中的连接初始化 from sqlalchemy import create_engine, event engine create_engine(sqlite:///./data/resumeforge.db) event.listens_for(engine, connect) def set_sqlite_pragma(dbapi_connection, connection_record): cursor dbapi_connection.cursor() cursor.execute(PRAGMA journal_modeWAL) # 写前日志提升并发读写 cursor.execute(PRAGMA synchronousNORMAL) # 平衡安全与性能 cursor.execute(PRAGMA foreign_keysON) # 开启外键约束 cursor.close()WAL 模式允许读写同时进行对于「一边编辑简历一边后台保存」的场景很有用。synchronousNORMAL 在本地磁盘上足够安全又能减少磁盘 I/O。foreign_keysON 确保关联数据的一致性比如删除简历时自动清理关联的条目。注意WAL 模式会生成额外的 -wal 和 -shm 文件备份数据库时要一起复制否则可能丢失最近未合并的数据。3.4 数据加密的考虑SQLite 数据库文件本身是明文的任何能访问你电脑的人都能用 DB Browser 打开看。如果你对隐私要求极高可以考虑 SQLCipher 这类加密方案它在 SQLite 基础上加了透明加密层。不过加密会带来性能开销和密钥管理问题对于大多数个人求职场景把数据库文件放在系统用户目录下、配合操作系统登录密码已经能挡住大部分风险。4. AI 集成怎么让 AI 帮忙但不让它乱编4.1 AI 在求职工作台里的合理边界这是 ResumeForge 最核心的设计理念。AI 在简历场景里能做很多事但有些事不该让它做。合理的边界是可以做润色措辞、调整语气、匹配岗位关键词、生成自我介绍草稿、检查语法错误不该做编造工作经历、虚构项目成果、夸大技能水平、生成不存在的证书ResumeForge 的做法是在 AI 接口层面做限制。调用 AI 时系统会把用户的原始文本作为「事实依据」传给模型并在 prompt 里明确要求「只能基于给定事实进行改写不得添加原文没有的信息」。AI 返回的结果会标记为「建议」用户需要手动点击「采纳」才会写入数据库。4.2 Prompt 设计的实操细节一个实际可用的简历优化 prompt 大概长这样你是一个简历润色助手。请基于以下原始描述进行优化要求 1. 保持所有事实信息不变不得添加原文没有的经历、数据或成果 2. 使用更专业的动词和量化表达如果原文有数据的话 3. 每条描述控制在两行以内 4. 如果原文信息不足以优化直接返回原文并说明原因 原始描述 {user_input} 目标岗位{target_role}这个 prompt 的关键在于第 1 条和第 4 条。第 1 条是硬约束第 4 条给了 AI 一个「不优化」的出口避免它为了完成任务而强行编造。实测下来加了第 4 条之后AI 编造内容的概率明显下降。4.3 流式输出的实现AI 生成内容时用 SSE 做流式展示。FastAPI 端的实现大概是这样from fastapi import APIRouter from fastapi.responses import StreamingResponse import httpx router APIRouter() router.post(/ai/optimize) async def optimize_resume(payload: OptimizeRequest): async def event_stream(): async with httpx.AsyncClient(timeout60) as client: async with client.stream( POST, https://api.example.com/v1/chat/completions, json{model: gpt-4, messages: [...], stream: True}, headers{Authorization: fBearer {api_key}} ) as response: async for line in response.aiter_lines(): if line.startswith(data: ): data line[6:] if data ! [DONE]: yield fdata: {data}\n\n return StreamingResponse(event_stream(), media_typetext/event-stream)前端用 EventSource 接收const eventSource new EventSource(/api/ai/optimize); eventSource.onmessage (event) { const data JSON.parse(event.data); setOptimizedText(prev prev data.content); }; eventSource.onerror () eventSource.close();这套组合的好处是用户体验流畅AI 一边生成一边显示不用等全部完成。注意 SSE 连接需要处理超时和重连生产环境里建议加心跳机制。4.4 AI 建议的存储与追溯每次 AI 生成的建议都会存进 ai_suggestions 表字段包括关联的 entry_id、原始文本、建议文本、使用的模型、生成时间、是否采纳。这样做的好处是你可以回溯某条工作描述改过几次、每次 AI 给了什么建议、你最终采纳了哪个版本。这个设计还有一个隐藏价值积累一段时间后你可以分析哪些类型的建议采纳率高从而优化自己的 prompt 或者调整 AI 模型选择。数据在自己手里想怎么分析都行。5. 实操部署从零把 ResumeForge 跑起来5.1 环境准备与依赖安装先确认本地有 Python 3.10 和 Node.js 18。然后按步骤来# 克隆项目 git clone https://github.com/yourname/resumeforge.git cd resumeforge # 后端依赖 python -m venv venv source venv/bin/activate # Windows 用 venv\Scripts\activate pip install fastapi uvicorn sqlalchemy httpx pydantic # 前端依赖 cd frontend npm install数据库文件不需要手动创建FastAPI 启动时会自动建表。如果你想先看看表结构可以用 DB Browser for SQLite 打开 data/resumeforge.db或者用命令行sqlite3 data/resumeforge.db .schema5.2 启动后端与前端后端启动uvicorn app.main:app --reload --port 8000前端启动npm run dev浏览器打开 http://localhost:5173 就能看到界面。FastAPI 的自动文档在 http://localhost:8000/docs调试接口很方便。提示如果前端请求后端遇到跨域问题在 FastAPI 里加 CORSMiddleware允许 localhost:5173 访问。5.3 数据库初始化与迁移第一次启动时用 SQLAlchemy 的 create_all 自动建表from app.database import engine, Base from app import models Base.metadata.create_all(bindengine)后面如果改了表结构简单场景可以直接删库重建本地数据记得先导出复杂场景建议引入 Alembic 做迁移。对于个人项目Alembic 有点重手动写迁移 SQL 也够用。5.4 配置 AI 接口在项目根目录建一个 .env 文件AI_API_KEYyour_key_here AI_BASE_URLhttps://api.example.com/v1 AI_MODELgpt-4后端用 python-dotenv 读取。注意 .env 要加进 .gitignore别把密钥提交到仓库。6. 常见问题与排查技巧实录6.1 数据库锁问题SQLite 在并发写入时会锁库报错「database is locked」。解决办法开启 WAL 模式前面提过设置合理的 busy_timeout避免长事务。cursor.execute(PRAGMA busy_timeout5000) # 等待5秒再报错如果还是频繁锁检查是不是有未关闭的数据库连接。SQLAlchemy 的 session 用完要 close或者用 context manager 自动管理。6.2 AI 返回内容格式不对有时候 AI 会返回 Markdown 格式或者带解释性文字而不是纯文本。解决办法是在 prompt 里明确要求「只返回优化后的文本不要加任何解释和格式标记」后端再做一层清洗去掉 和多余空行。6.3 前端 SSE 连接断开SSE 默认超时时间因浏览器而异长时间生成容易断。可以在后端定期发送注释行作为心跳yield : heartbeat\n\n前端监听 onerror 事件自动重连。如果用的是 EventSource它本身有重连机制但要注意重连后可能重复接收内容需要在后端做去重或者前端做状态判断。6.4 简历导出 PDF 乱码如果后面加导出功能中文字体是个坑。用 WeasyPrint 或者 Playwright 生成 PDF 时需要指定支持中文的字体否则会显示方块。建议在 CSS 里明确 font-family或者把字体文件打包进项目。6.5 常见问题速查表问题现象可能原因解决方向启动报错 no module named app工作目录不对在项目根目录执行 uvicorn数据库文件找不到路径配置错误检查 database.py 里的路径AI 接口 401密钥无效或未加载检查 .env 和读取逻辑前端白屏依赖未安装或端口冲突重装依赖检查端口占用SSE 无输出代理缓冲或超时关闭代理缓冲加心跳中文乱码编码或字体问题统一 UTF-8指定中文字体7. 这个项目还能怎么扩展ResumeForge 目前的定位是本地优先的简历管理工具但它的架构留了不少扩展空间。比如可以加岗位匹配功能把招聘网站的 JD 粘贴进来用 AI 分析你的简历和 JD 的匹配度给出关键词补充建议。这个功能不需要爬虫手动粘贴就行合规又实用。另一个方向是多格式导出。除了 PDF还可以支持 Markdown、HTML、甚至直接生成适合在线招聘平台的纯文本格式。不同平台对简历格式要求不一样一键切换能省不少事。如果你对 AI Agent 感兴趣可以试试让 AI 自动分析简历的薄弱环节比如「工作经历缺少量化数据」「技能标签和目标岗位不匹配」然后给出具体的修改建议。这比单纯的润色更有价值但需要更精细的 prompt 设计和结果校验。数据库层面可以加一个版本对比功能。每次修改都存一个快照用 diff 算法展示两个版本之间的差异让你清楚看到自己改了什么、AI 建议了什么。这个功能对反复打磨简历的人很实用。最后再分享一个小技巧SQLite 数据库文件可以直接用 git 管理每次修改后 commit 一下天然就有了版本历史。配合 git diff 还能看到数据层面的变化。当然简历数据敏感仓库记得设为私有。
阅读完成 · 觉得有帮助?