ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

本地优先AI求职工作台:React+FastAPI+SQLite全栈实战

本地优先AI求职工作台:React+FastAPI+SQLite全栈实战

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 的组件化和状态管理来做比较顺手。

项目里用到了 SSE(Server-Sent Events)来实现 AI 生成内容的流式展示。这个选择很实际:AI 优化简历措辞时,如果等全部生成完再显示,用户会盯着空白屏幕好几秒;用 SSE 可以让文字像打字机一样逐字出现,体验好很多。相比 WebSocket,SSE 更轻量,单向推送足够用,不需要额外维护双向连接。

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_mode=WAL") # 写前日志,提升并发读写 cursor.execute("PRAGMA synchronous=NORMAL") # 平衡安全与性能 cursor.execute("PRAGMA foreign_keys=ON") # 开启外键约束 cursor.close()

WAL 模式允许读写同时进行,对于「一边编辑简历一边后台保存」的场景很有用。synchronous=NORMAL 在本地磁盘上足够安全,又能减少磁盘 I/O。foreign_keys=ON 确保关联数据的一致性,比如删除简历时自动清理关联的条目。

注意: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(timeout=60) as client: async with client.stream( "POST", "https://api.example.com/v1/chat/completions", json={"model": "gpt-4", "messages": [...], "stream": True}, headers={"Authorization": f"Bearer {api_key}"} ) as response: async for line in response.aiter_lines(): if line.startswith("data: "): data = line[6:] if data != "[DONE]": yield f"data: {data}\n\n" return StreamingResponse(event_stream(), media_type="text/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 ".schema"

5.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(bind=engine)

后面如果改了表结构,简单场景可以直接删库重建(本地数据记得先导出),复杂场景建议引入 Alembic 做迁移。对于个人项目,Alembic 有点重,手动写迁移 SQL 也够用。

5.4 配置 AI 接口

在项目根目录建一个 .env 文件:

AI_API_KEY=your_key_here AI_BASE_URL=https://api.example.com/v1 AI_MODEL=gpt-4

后端用 python-dotenv 读取。注意 .env 要加进 .gitignore,别把密钥提交到仓库。

6. 常见问题与排查技巧实录

6.1 数据库锁问题

SQLite 在并发写入时会锁库,报错「database is locked」。解决办法:开启 WAL 模式(前面提过),设置合理的 busy_timeout,避免长事务。

cursor.execute("PRAGMA busy_timeout=5000") # 等待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 还能看到数据层面的变化。当然,简历数据敏感,仓库记得设为私有。

RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进