十年匠心定制 · 商业建站与技术教学双线并行 咨询热线:400-886-1026 service@lmnt.cn
ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

Resume Matcher 后端架构指南:基于 FastAPI + SQLite + LiteLLM 的本地优先简历定制服务

Resume Matcher 后端架构指南:基于 FastAPI + SQLite + LiteLLM 的本地优先简历定制服务 Resume Matcher 后端架构指南基于 FastAPI SQLite LiteLLM 的本地优先简历定制服务【免费下载链接】Resume-MatcherThe #1 AI Harness for Building Resumes, PDFs, Cover Letters more, locally with 100 LLMs support.项目地址: https://gitcode.com/GitHub_Trending/re/Resume-Matcher本篇技术指南围绕 docs/agent/architecture/backend-guide.md 展开系统讲解 Resume Matcher 后端apps/backend的完整架构与实现从技术选型、目录结构、异步 SQLAlchemy 数据层、Fernet 加密的 API Key 存储与 TinyDB 迁移到 LiteLLM 多提供商接入、Prompt 规范、REST 端点、数据流与错误处理。读完本文你将掌握如何在本地运行该 FastAPI 应用、理解双引擎单文件数据库设计与密钥安全机制并能遵循既定约定扩展新端点。一、技术栈总览Resume Matcher 后端是一个精简、本地优先Lean, local-first的 FastAPI 应用专门服务于简历定制resume tailoring。其技术选型如下表组件技术Web 框架FastAPI数据库SQLiteSQLAlchemy 2.0 async aiosqliteAI 接入LiteLLM支持 100 提供商文档解析markitdown校验Pydantic密钥加密Fernetcryptography以上依赖在 apps/backend/pyproject.toml 中有精确锁定如fastapi0.128.4、sqlalchemy[asyncio]2.0.36、aiosqlite0.20.0、litellm1.86.2、markitdown[docx]0.1.4、cryptography48.0.1并声明requires-python 3.13。项目提供了uv管理的控制台入口[project.scripts] app app.main:main这也是uv run uvicorn app.main:app能直接启动的原因。二、目录结构与模块职责后端代码集中在apps/backend/app/下各文件职责清晰apps/backend/app/ ├── main.py # 入口点lifespanTinyDB→SQLite 导入、遗留明文 key 折叠 ├── config.py # 从 env/文件读取设置加密 API Key 的读写 ├── crypto.py # Fernet 加密/解密 API Key静态存储加密 ├── database.py # 异步 SQLAlchemy/SQLite 门面返回纯 dict ├── models.py # SQLAlchemy 声明式 Base ORM 模型 ├── db_engine.py # SQLite 引擎/会话工厂async sync PRAGMA ├── llm.py # 多提供商 LLM ├── routers/ # health, config, resumes, jobs, applications, enrichment, resume_wizard ├── services/ # parser, improver, cover_letter, ats, refiner, interview_prep, resume_wizard ├── schemas/ # Pydantic 模型models.py, applications.py, enrichment.py, refinement.py, resume_wizard.py ├── scripts/ # migrate_tinydb_to_sqlite.py一次性导入器 └── prompts/ # templates.py 等提示词模板从 入口文件 apps/backend/app/main.py 可以看到应用通过lifespan在启动时依次执行创建数据目录settings.data_dir.mkdir(parentsTrue, exist_okTrue)调用migrate_tinydb()导入遗留 TinyDB 数据库幂等失败即快速报错避免以空库启动造成数据丢失假象调用migrate_legacy_keys()将遗留明文 API Key 折叠进加密存储幂等、不覆盖关闭时释放 PDF 渲染器与数据库引擎资源。主应用注册了全部路由统一前缀/api/v1并启用 CORS 中间件allow_origins来自settings.effective_cors_origins可用CORS_ORIGINS环境变量配置。三、数据库层异步门面 双引擎单文件3.1 门面设计方法名不变返回纯 dictdatabase.py是一个异步Database门面全局单例db所有方法保持与旧 TinyDB 封装相同的名称与签名但返回纯 dict绝不返回 ORM 行。这种行为保持型替换behavior-preserving replacement让原有约 50 处调用点只需要补一个await即可完成迁移。核心方法清单见 apps/backend/app/database.pyawait db.create_resume(content, content_type, filename, is_master, processed_data) await db.get_resume(resume_id) → dict | None await db.list_resumes() → list[dict] await db.update_resume(resume_id, updates) await db.delete_resume(resume_id) → bool await db.set_master_resume(resume_id) # 同一时刻只允许一个 master await db.create_job(content, resume_id) await db.create_application(...) / list_applications / bulk_update_applications get_api_key_ciphertexts() / replace_api_keys(...) # 同步加密的 api_keys 表ORM 模型定义在 apps/backend/app/models.py包含五张表resumes、jobs、improvements、applicationsKanban 求职追踪卡片、api_keys加密存储。数据库文件位于data/resume_matcher.db。一个值得注意的兼容性细节original_markdown字段保留了 TinyDB 时代的缺省语义——为None时门面直接省略该键而不是输出null见 database.py 中的 _resume_to_dict保证老客户端行为不变。3.2 双引擎、单文件的设计一个 SQLite 文件由两个引擎支撑apps/backend/app/db_engine.py异步引擎aiosqlite服务文档表resumes/jobs/improvements与applications同步引擎标准sqlite3服务加密的api_keys表因为该表在同步的 LLM 热路径上被读取调用链get_llm_config → load_config_file → resolve_api_key用同步引擎避免把 async 穿透到llm.py。两个引擎指向同一文件并统一应用 PRAGMA见_apply_sqlite_pragmasPRAGMA journal_modeWAL; -- 支持两个引擎对同一文件的并发读写 PRAGMA foreign_keysON; -- SQLite 默认关闭需显式开启 PRAGMA busy_timeout5000; -- 化解短暂锁竞争表结构创建通过同步引擎完成init_models_sync并含一个幂等的加列迁移若resumes表缺少interview_prep列则ALTER TABLE补上create_all不会修改已存在的表。从源码结构可以推断这是为老版本本地数据库平滑升级预留的兼容机制。3.3 单 Master 不变式与动态字段全局唯一 Master 简历是数据层的核心不变式由两层机制共同保障应用层asyncio.Lock_master_resume_lock串行化并发的 master 晋升操作见 database.py并在set_master_resume中先降级再晋升且flush后再提交保证不会违反唯一索引存储层resumes表上的部分唯一索引partial unique indexux_resumes_single_mastersqlite_wheretext(is_master 1)确保同一时刻至多一行is_master为真见 models.py。jobs表的动态流水线字段preview_hash/preview_hashes、job_keywords、company/role等存放在metadata_jsonJSON 列中读取时扁平化到顶层更新时非核心键合并回 JSON从而复刻 TinyDB 的动态语义见 database.py 的 _job_to_dict 与 models.py 的 Job。3.4 求职追踪applications的一致性设计applications表为 Kanban 追踪器服务具备三处关键设计去重UniqueConstraint(job_id, resume_id)唯一约束 应用层 select-then-insert双重保证同一 jobresume 不会产生重复卡片并发冲突时通过捕获IntegrityError返回既有卡片见 create_application列内排序position字段维护每列每个 status内 0..n-1 连续序列update_application移动卡片时通过停放到 10_000_000 → 重排旧列 → 插入新列三步实现服务端重排见 database.py状态机APPLICATION_STATUSES定义了稳定键saved / applied / no_response / response / interview / accepted / rejected与 i18n 标签解耦。四、API Key 加密存储与 TinyDB 迁移4.1 Fernet 加密明文只在调用时存在于内存crypto.py 使用对称加密 Fernet 对 API Key 静态加密对称密钥位于data/.secret_key首次启动自动生成、chmod 600、gitignore 忽略、原子写入——先mkstemp写入临时文件并fsync再os.link/os.replace原子落位每条 Provider Key 按提供商独立加密存储在api_keys表明文仅在调用时刻存在于内存容错设计密钥缺失时按需生成密钥被轮换/丢失导致解密失败时将该 Key 视为空并提示用户重新录入绝不崩溃InvalidToken仅记录警告日志并返回空串见 crypto.py 的 decrypt。在配置读写层面apps/backend/app/config.pyload_config_file()读取 config.json 后注入解密后的api_keyssave_config_file()则在落盘前剥离api_keys与遗留api_key字段保证明文密钥永不进入 config.json。写入走POST /config/api-keys按提供商分别加密而旧的PUT /config/llm-api-key已不再持久化密钥。4.2 遗留密钥折叠与 TinyDB 迁移migrate_legacy_keys()config.py负责把 config.json 中的遗留明文api_keys映射与单一api_key折叠进加密存储仅在目标提供商槽位为空时写入幂等、非覆盖随后从 config.json 中清除这些字段从而消除每个提供商共享同一把密钥的遗留影子 bug。注意_LEGACY_PROVIDER_KEY_MAP中gemini提供商对应的密钥存储名是google。scripts/migrate_tinydb_to_sqlite.py 是 TinyDB → SQLite 的一次性导入器安全地在每次启动时运行无遗留文件 → no-opSQLite 已有数据 → 跳过视为已迁移否则 → 将 resumes/jobs/improvements 1:1 复制保留主键与时间戳并强制单 master 不变式若遗留库有多个 master保留created_at最早者并降级其余随后将遗留文件重命名为database.json.migrated作为回滚工件。可手动运行uv run python -m app.scripts.migrate_tinydb_to_sqlite。五、LLM 接入LiteLLM 多提供商封装5.1 能力清单特性说明API Key 传递直接传给 litellm避免全局环境变量导致的竞态JSON Mode对受支持的提供商自动启用response_format{type:json_object}重试逻辑2 次应用层重试temperature 0.1→0.3→0.5→0.7 递增超时30s健康检查、120s普通补全、180sJSON 补全这些常量定义在 apps/backend/app/llm.pyLLM_TIMEOUT_HEALTH_CHECK 30、LLM_TIMEOUT_COMPLETION 120、LLM_TIMEOUT_JSON 180。超时还会按 token 数与提供商缩放_calculate_timeouttoken 因子max(1.0, max_tokens/4096)提供商因子如ollama: 2.0本地模型更慢、openrouter: 1.5延迟波动大。5.2 配置解析与密钥解析get_llm_config()llm.py从 config.json 读取 provider/model/api_base/reasoning_effort密钥优先级为顶层api_keyapi_keys[provider] env/settingsreasoning_effort优先取 config.json。它还内置了一次性迁移对旧版 gpt-5 用户若 config.json 中缺失reasoning_effort键则自动持久化minimal。resolve_api_key()是密钥解析的唯一事实来源例外情形是openai_compatible与ollama本地/自建服务常无需鉴权这两类提供商跳过环境变量默认值回退防止LLM_API_KEY中真实的付费 API Key 泄漏到本地端点见 llm.py。_normalize_api_base()处理各提供商对api_base的预期差异OpenAI 兼容端点原样保留避免破坏 llama.cpp 等http://localhost:8080/v1而 Anthropic / Gemini / OpenRouter 若用户粘贴的 base 已含/v1则剥离防止产生/v1/v1/...404对应 issue #751。get_model_name()负责加提供商前缀OpenRouter 始终加openrouter/嵌套模型名如openrouter/anthropic/claude-3.5-sonnetOllama 使用ollama_chat/前缀路由到/api/chat。5.3 Router 缓存与传输层重试_build_router()基于配置构建 LiteLLMRouter并通过_config_fingerprint()provider|model|key hash|api_base做指纹缓存配置变化时才重建。Router 使用RetryPolicy区分错误类型重试TimeoutErrorRetries2、RateLimitErrorRetries3、InternalServerErrorRetries2AuthenticationError/BadRequestError/ContentPolicyViolationError不重试disable_cooldownsTrue单部署无 fallback 时cooldown 会在瞬时故障下黑掉后端。5.4 结构化输出JSON 补全的完整韧性complete_json()llm.py是简历解析等结构化任务的核心层层设防JSON Mode 自动启用_supports_json_mode查询 LiteLLM 模型注册表Ollama 原生支持formatjson直接返回 True截断检测_appears_truncated按 schema 类型resume/enrichment/interview_prep检查空数组或缺失键命中则带提示词重试多级降级JSON 解析失败或提供商 400 拒绝response_format如 LM Studio对应 issue #857时禁用 JSON Mode 回退到纯提示词模式重试提取安全_extract_json限制递归深度10 层与内容大小1MB支持剥离think.../think思维标签deepseek-r1 等、去除 markdown 代码块、括号配平截取max_tokens 钳制get_safe_max_tokens依据模型注册表的max_output_tokens钳制请求默认预算 8192注册表缺失时保守回退 4096。普通补全complete()同样经由 Router并处理推理模型的reasoning_content/thinking回退DeepSeek R1、OpenAI o1/o3、Anthropic extended thinking以及_extract_choice_text对content→text→delta的多路径提取。六、Prompt 编写规范prompts/目录集中管理所有提示词模板。编写 Prompt 时必须遵循三条约定使用{variable}单花括号做变量替换提示词内包含 JSON schema 示例以 Output ONLY the JSON object 结尾引导模型输出纯 JSON。这与llm.py中complete_json的实现互为印证json_system会在系统提示词末尾追加 You must respond with valid JSON only. No explanations, no markdown.二者共同保证结构化输出可被json.loads直接消费。实际模板可参考 apps/backend/app/prompts/templates.py 及按功能拆分的enrichment.py、refinement.py、resume_wizard.py。七、API 端点速查所有端点挂在/api/v1前缀下路由注册见 main.pyGET /api/v1/health # 存活探针不调用 LLM GET /api/v1/status # 完整状态LLM DB 各自隔离部分失败返回 200 GET/PUT /api/v1/config/llm-api-key # 不再持久化密钥 GET/POST/DELETE /api/v1/config/api-keys # 按提供商加密存储密钥 POST /api/v1/resumes/upload # PDF/DOCX 上传 POST /api/v1/resumes/improve # 定制LLM GET /api/v1/resumes/{id}/pdf DELETE /api/v1/resumes/{id} GET /api/v1/applications # Kanban 追踪器分组列表 POST/PATCH/DELETE/bulk两个健康检查端点的分工值得注意apps/backend/app/routers/health.py/health纯存活探针供 Docker HEALTHCHECK 使用不调用 LLM/status全面状态。LLM 健康探测与数据库统计各自 try/except 隔离——LLM 探测失败只降级自己的字段DB 统计失败返回空统计_EMPTY_DB_STATS端点本身仍返回 200 的降级状态由前端据此展示setup_required或ready。八、数据流上传与定制上传链路文件 → markitdown → Markdown → LLM 解析 → JSON → SQLite经由db门面定制链路简历 职位描述 → 提取关键词LLM→ 定制简历LLM→ 存储。Router 调用 serviceservice 调用app/llm.py持久化统一走异步db门面。另外/improve/confirm端点还会尽力而为地在求职追踪器中自动创建一张applied状态的卡片打通定制简历 → 投递记录的闭环对应的自动建卡去重逻辑见 database.py 的 create_application。九、错误处理约定后端遵循细节留服务端、通用信息给客户端的原则except Exception as e: logger.error(fFailed: {e}) raise HTTPException(500, Operation failed.)LLM 层进一步强化了这一点完整异常栈写入服务端日志logging.exception而客户端只收到LLM completion failed. Please check your API configuration and try again.见 llm.py 的 complete。对于健康检查失败路径还通过_scrub_secrets用正则sk-...、AIza...、Bearer ...把上游错误信息中可能泄露的 API Key 片段替换为redacted防止设置页成为密钥回读通道见 llm.py。十、本地运行cd apps/backend cp .env.example .env uv run uvicorn app.main:app --reload --port 8000可用的环境变量与默认值见 config.py 的 Settings变量默认值说明LLM_PROVIDERopenai可选openai / openai_compatible / anthropic / openrouter / gemini / deepseek / groq / ollamaLLM_MODELgpt-5-nano-2025-08-07具体模型名LLM_API_KEY空环境级密钥config.json 加密存储优先LLM_API_BASE空Ollama 或自定义端点LLM_LOG_LEVEL/LOG_LLMWARNINGLiteLLM 日志级别LOG_LEVELINFO应用日志级别HOST/PORT0.0.0.0/8000服务监听地址FRONTEND_BASE_URLhttp://localhost:3000追加进 CORS 白名单CORS_ORIGINSlocalhost:3000 / 127.0.0.1:3000CORS 白名单列表REQUEST_TIMEOUT_SECONDS240单次定制请求硬超时钳制在 [30, 1800] 秒REASONING_EFFORT空minimal / low / medium / high空串视为不发送该参数注意request_timeout_seconds必须与前端两层超时Next.jsproxyTimeout与客户端AbortController保持同步三层中任意一层最短即最先中止只改后端无法解决超时问题对应 issue #776 的教训。十一、如何新增端点按既定约定扩展 API 只需三步在app/routers/下创建新 router在app/schemas/models.py中添加 Pydantic 模型请求/响应校验在app/main.py中注册 routerapp.include_router(xxx_router, prefix/api/v1)。若新端点涉及数据读写应复用app/database.py的全局db门面保证返回纯 dict、沿用单 master 等不变式若涉及 LLM 调用统一走app/llm.py的complete/complete_json以获得 JSON Mode、重试与超时治理。结语Resume Matcher 后端以本地优先、数据可控、密钥安全为设计主线异步门面把 SQLite 的复杂性与调用方解耦双引擎设计兼顾了文档读写与 LLM 热路径的密钥读取Fernet 加密与自动迁移让老用户平滑过渡而 LiteLLM 封装则让 100 提供商含 Ollama 等本地模型只需改配置即可接入。理解 backend-guide.md 中勾勒的这份骨架再结合 app 源码 与 后端测试单元、service、integration 三层齐全深入阅读即可在数小时内掌握该后端的全貌并开始二次开发。【免费下载链接】Resume-MatcherThe #1 AI Harness for Building Resumes, PDFs, Cover Letters more, locally with 100 LLMs support.项目地址: https://gitcode.com/GitHub_Trending/re/Resume-Matcher创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
返回列表