Resume Matcher 后端架构深度解析:FastAPI + LiteLLM 驱动的多模型简历定制流水线
2026/9/11 3:00:55 网站建设 项目流程

Resume Matcher 后端架构深度解析:FastAPI + 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

本文以仓库apps/backend/CLAUDE.md(后端开发指南)为骨架,结合apps/backend下源码(app/main.pyapp/config.pyapp/llm.pyapp/database.pyapp/services/*app/prompts/*等)进行源码级展开。读者将掌握:后端模块如何划分、改善(improve/tailor)核心流水线如何串联提示词与 LLM 调用、API Key 如何加密落库、多 Provider 接入如何做重试与超时治理,以及本地开发、测试与常见陷阱。

Resume Matcher 的后端是一个典型的「FastAPI 组装 + 服务层编排 + LLM 驱动」单体:所有 HTTP 入口统一挂在/api/v1前缀下,路由层只做参数校验与编排,业务逻辑下沉到app/services/,而真正与多模型打交道的是app/llm.py这个基于 LiteLLM 的薄封装。整条「上传简历 → 解析 → 匹配职位 → 生成改善 → 确认落库 → 渲染 PDF」的流水线,都以 改善(improve) 为核心串联起来。

一、技术栈总览

根据 CLAUDE.md 与 pyproject.toml 的锁定版本,后端依赖栈如下:

组件选型版本 / 说明
Web 框架FastAPI0.128.4,配合uvicorn 0.40.0
运行时Python>=3.13requires-python硬性要求)
数据校验Pydantic v2 / pydantic-settings2.12.5/2.14.2,环境变量即配置
ORM / 存储SQLAlchemy 2(async)+ SQLitesqlalchemy[asyncio]==2.0.36+aiosqlite==0.20.0
多模型接入LiteLLM1.86.2(Router、RetryPolicy、drop_params)
文档解析markitdown0.1.4(DOCX/PDF → Markdown)
PDF 渲染Playwright / Chromium1.58.0(无头浏览器渲染前端/print/*页)
加密cryptography(Fernet)48.0.1(API Key 落盘加密)
依赖管理uv项目版本1.2.0;另有requirements.txt提供精确 pin

注意:CLAUDE.md 明确提示uv.lock.gitignore忽略,因此依赖解析无法从 VCS 复现,必须依赖pyproject.toml/requirements.txt中的精确版本号(详见「关键陷阱」一节)。

二、架构地图:模块职责划分

CLAUDE.md 给出了一张清晰的后端模块职责表,结合源码可以进一步印证每个模块的实际作用:

模块职责关键文件(仓库根相对路径)
入口 / 装配App 实例、lifespan、CORS、路由挂载(统一/api/v1app/main.py
配置pydantic-settings 读取环境变量;settings单例;API Key 从加密 SQLite 存储读取app/config.py
加密Fernet 加解密 API Key 落盘(data/.secret_keychmod 600,gitignored)app/crypto.py
配置缓存data/config.json的共享 TTL 缓存读取(5 分钟);get_content_language()app/config_cache.py
数据库Async SQLAlchemy/SQLite 门面;表resumes/jobs/improvements/applications/api_keys;返回普通 dict;全局db单例app/database.py、app/models.py、app/db_engine.py
Tracker看板式投递追踪端点app/routers/applications.py、app/schemas/applications.py
LLMLiteLLM 封装:Router、重试、JSON 抽取、超时、Provider 兼容性处理app/llm.py
PDF无头 Chromium 渲染前端/print/*页面;浏览器懒初始化app/pdf.py
路由层HTTP 端点(见下节)app/routers/*.py
服务层业务逻辑(解析、改善/diff、精修、求职信)app/services/*.py
提示词全部 LLM 提示词模板 + 占位符校验app/prompts/*.py
SchemaPydantic 请求/响应 +ResumeData模型app/schemas/*.py

从 app/main.py 的 lifespan 可以看到启动时的关键动作:确保data/目录存在、执行 TinyDB→SQLite 幂等迁移、执行遗留明文 Key 折叠迁移;随后才是路由挂载(app/routers/init.py 统一导出的*_router)。

2.1 数据目录与持久化设计

data/目录承载全部本地数据,CLAUDE.md 的说明可归纳为:

  • resume_matcher.db:SQLite 主存储(文档表 + 应用追踪表);
  • config.json:非敏感配置(provider、model、feature prompts、language 等);
  • .secret_key:Fernet 密钥,用于加密 API Key;
  • uploads/:用户上传的原始文件目录;
  • database.json:遗留 TinyDB 文件——首次启动时被导入 SQLite 并重命名为database.json.migrated

.gitignore会忽略*.db*data/*.jsondata/.secret_key,但uploads/不在忽略之列——CLAUDE.md 特别警告不要提交用户上传文件。

db.reset_database()(app/database.py)的行为值得注意:它会清空文档表 +applications(保留api_keys),并清空uploads/。即「重置数据但保留凭据」。

双引擎设计是 app/database.py 的一个精妙点:同一 SQLite 文件背后有两个引擎——aiosqlite异步引擎服务文档表与应用表;同步引擎服务加密的api_keys表。原因在于get_llm_config() → resolve_api_key()同步的 LLM 热路径,同步引擎避免在异步上下文里额外绕行。

三、API 路由层(统一/api/v1前缀)

CLAUDE.md 列出主要路由器(实际 app/main.py 挂载了 7 个,含 resume_wizard):

路由端点职责
health.pyGET /healthGET /statusliveness 探活(不发 LLM 调用);LLM 健康 + DB 统计
config.py/config/llm-api-key/config/llm-test/config/features/config/language/config/prompts/config/feature-prompts/config/api-keys/config/reset配置读写、实时健康检查、按 Provider 的 Key CRUD、数据重置
resumes.py/resumes/uploadGET /resumes/resumes/list/resumes/improve+/improve/preview+/improve/confirmPATCH /resumes/{id}/{id}/pdf/{id}/retry-processing、求职信/外联信/标题 PATCH 与按需生成、/{id}/job-description/{id}/cover-letter/pdf最大的路由器,覆盖简历 CRUD、改善流水线、PDF 与内容生成
jobs.py/jobs/upload(批量 JD 文本 → job_ids)、GET /jobs/{id}职位描述管理
enrichment.py/enrichment/analyze/{id}/enhance/apply/{id}/regenerate/apply-regenerated/{id}AI 内容增强(分析、增强、应用、重新生成)
applications.py看板追踪 CRUDKanban 投递追踪
resume_wizard.py向导式简历生成问答式构建简历

两个值得留意的接口细节:

  • /config/reset的确认令牌放在JSON body{"confirm": "RESET_ALL_DATA"}),而不是 query 参数;
  • /config/llm-api-key已不再写入任何 Key——所有 Key 都走/config/api-keys(per-provider CRUD),这是「Key 只进加密存储」设计的一部分(见第五节)。

四、核心流水线:improve 的请求 / 数据流

CLAUDE.md 明确指出POST /resumes/improve/preview规范路径(canonical path),完整流程分六步:

  1. db加载简历 + 职位;解析内容语言(config_cache)与prompt_id
  2. extract_job_keywords(jd)(LLM 抽取,按职位内容哈希缓存);
  3. 若存在结构化processed_datadiff 模式:skill-target plan →generate_resume_diffsapply_diffsverify_diff_result;否则回退improve_resume(整份输出);
  4. 本地安全网(始终运行、纵深防御):_preserve_personal_info_restore_original_datesrestore_dates_from_markdown_preserve_original_skills_protect_custom_sections
  5. refine_resume(关键词注入 + AI 味短语清洗 + 对齐校验);
  6. 在 job 上持久化preview_hash/improve/confirm重新校验该哈希(并要求personalInfo未变)后才持久化定稿简历 + 一条improvements记录。

整条 preview 被asyncio.wait_for包裹,超时上限默认 240 秒(见 app/config.py,request_timeout_seconds被钳制在 [30, 1800] 秒)。

4.1 diff 模式:比整份重写更安全的改善

app/services/improver.py 是服务层最大的文件,核心是基于 diff 的改善:不要求 LLM 重写整份简历,而是输出一份定点修改清单(path + action + value)。apply_diffs(improver.py)对每条 change 执行四道闸门:

  1. 路径必须在白名单内(_ALLOWED_PATH_PATTERNSsummaryworkExperience[i].description[j]personalProjects[i].description[j]education[i].descriptionadditional.technicalSkills等);
  2. 路径不得命中黑名单personalInfocustomSectionssectionMeta前缀,以及company/title/degree/institution/name等身份字段叶子名);
  3. 路径必须能在原数据中实际解析到值
  4. replace动作要求original文本与原值逐字匹配(忽略大小写)。

支持的动作包括replace/append/reorder/add_skill。其中reorder还有一个针对真实场景的「抢救(salvage)」逻辑(issue #736 相关):当 LLM 把新增/删除项混进 reorder 列表时,不再整体丢弃,而是按请求顺序放置已有项、为技能列表插入通过校验的新技能、把遗漏的原项追加到末尾——任何真实条目都不会被静默丢失

verify_diff_result(improver.py)则做零 LLM 成本的本地质量检查:无变更告警、章节数量是否被破坏、身份字段是否被改动、字数是否异常膨胀(超过 1.8 倍)、是否发明了度量值(\d+%/\d+x/$\d+等原文本没有的数字)。

4.2 skill-target 前置规划与验证

在生成 diff 之前,系统会先让 LLM 产出「技能目标计划」(generate_skill_target_plan),随后由本地函数verify_skill_target_plan(improver.py)做过滤分类:

  • 简历中已存在的技能 →existing(低风险);
  • JD 的 required/preferred 技能 →jd_added(供用户审阅);
  • 简历正文中出现过的技能 →supported_by_resume
  • 其余 →unsupported直接拒绝。

add_skill动作只能添加通过了该验证集的技能,从而从根上防止 LLM 编造技能

4.3 提示词注入防御

_sanitize_user_input(improver.py)会在 JD 文本进入提示词前,用正则把ignore previous instructionssystem:<system>[INST]等常见注入模式替换为[REDACTED]

五、提示词管理:读模板前必看

CLAUDE.md 花了大篇幅讲提示词管理,因为这是最容易被改坏的地方:

  • 提示词是纯 Python 字符串常量,无 Jinja、无运行时外部文件(data/prompts.json不参与加载);
  • 布局:templates.py(简历解析、关键词抽取、3 种 improve 变体、diff 提示词、skill-target 计划、求职信/外联信/标题、RESUME_SCHEMA_EXAMPLECRITICAL_TRUTHFULNESS_RULESLANGUAGE_NAMES+get_language_name())、enrichment.pyANALYZE_RESUME_PROMPT等 4 个)、refinement.pyKEYWORD_INJECTION_PROMPTVALIDATION_POLISH_PROMPTAI_PHRASE_BLACKLISTAI_PHRASE_REPLACEMENTS)、__init__.py(重导出 + 占位符校验)。

5.1 占位符格式化规则(最容易踩坑)

服务层统一from app.prompts import ...后调用PROMPT.format(**vars)。因此:

  • {placeholder}是真实 format key;
  • 任何字面量{}(例如 JSON 示例中的花括号)必须写成{{ }}——参见EXTRACT_KEYWORDS_PROMPTDIFF_IMPROVE_PROMPT与 enrichment 提示词;
  • 唯一的例外是PARSE_RESUME_PROMPT:它通过{schema}嵌入 schema,因此不要双重转义(见 app/services/parser.py 的调用)。

5.2 improve 提示词选择

  • IMPROVE_RESUME_PROMPTS = {nudge, keywords, full}IMPROVE_PROMPT_OPTIONS是 UI 列表,默认DEFAULT_IMPROVE_PROMPT_ID = "keywords"
  • 生效 id 来自config.jsondefault_prompt_id(会对照 option ids 校验)或请求的prompt_id
  • CRITICAL_TRUTHFULNESS_RULES[id]通过{critical_truthfulness_rules}注入每份 improve 提示词,防止 LLM 虚构经历;
  • 语言:每个生成型提示词都接收{output_language}get_language_name(code)返回全名),支持en/es/zh/ja/pt/fr(见 app/prompts/templates.py)。

5.3 用户可编辑的自定义功能提示词

求职信与群发外联信提示词可在config.json中覆盖(cover_letter_promptoutreach_message_prompt)。保存时(PUT /config/feature-prompts)由validate_prompt_placeholders()校验必须包含REQUIRED_FEATURE_PROMPT_PLACEHOLDERS = {job_description, resume_data, output_language},缺失返回 HTTP 422;空字符串 = 使用默认。运行时 app/services/cover_letter.py 的_resolve_feature_prompt选择「自定义或默认」,若自定义提示词.format()失败则回退内置默认并告警。

六、LLM 集成:app/llm.py源码级剖析

CLAUDE.md 对app/llm.py的要点概括,对应源码均可逐一验证:

6.1 Provider 抽象与模型名映射

支持openaiopenai_compatible(llama.cpp/vLLM/LM Studio)、anthropicopenroutergeminideepseekgroqollama八类 Provider。get_model_name()(app/llm.py)把 provider/model 映射为 LiteLLM 前缀格式:openai_compatible → openai/anthropic → anthropic/gemini → gemini/openrouter → openrouter/(且 OpenRouter 永远强制加前缀,因为其模型名是嵌套式如openrouter/anthropic/claude-3.5-sonnet)、ollama → ollama_chat/(路由到/api/chat)。

_normalize_api_base(app/llm.py)解决了一个真实痛点:用户在代理/聚合器上粘贴的 base URL 常自带/v1,而 LiteLLM 某些 Provider 处理器内部会再拼/v1,导致/v1/v1/...404。处理策略是分 Provider 剥离后缀:openai/openai_compatible原样保留(OpenAI 客户端自身能正确解析);anthropic/gemini/openrouter剥离结尾/v1ollama剥离/v1/api/chat/api/generate/api

6.2 Router:传输层重试集中管理

_build_router(app/llm.py)构建缓存的litellm.Router

  • num_retries=3,配RetryPolicy:认证错误 / 非法请求 / 内容策略违规 = 0 次重试;超时 / 500 = 2 次;限流 = 3 次;
  • disable_cooldowns=True:单部署无 fallback 时冷却会导致整体黑屏,故禁用(注释明确提示「添加 fallback 部署后再启用」);
  • Router 只在配置指纹变化时重建(_config_fingerprinthash()计算 API Key 的进程内哈希,原始 Key 永不进入指纹字符串)。

CLAUDE.md 特意强调:传输层重试已内置于 Router,调用方不要再重复重试

6.3complete()complete_json():内容质量重试

  • complete():普通补全,_extract_choice_text兜底抽取reasoning_content/thinking(DeepSeek R1、OpenAI o1/o3、Anthropic extended thinking),并剥离<think>标签;
  • complete_json()(app/llm.py):在 Router 传输重试之上再加应用层内容质量重试——JSON 解析失败、截断检测命中时按_get_retry_temperature提升温度重试([0.1, 0.3, 0.5, 0.7]),并把「仅输出 JSON」的提示追加进消息;
  • JSON 抽取走花括号配平的_extract_json(带 10 层递归与 1MB 大小限制);
  • _appears_truncatedschema_type分治:resume检查workExperience/education/skills是否为空数组,enrichment检查键是否缺失,interview_prep检查五个必备键,diff/keywords不设启发式(空 diff 与空关键词都是合法的)。

6.4 能力注册表(而非硬编码)

_supports_json_mode_supports_temperatureget_safe_max_tokens都查询litellm.get_model_info的能力注册表,Ollama/本地模型不在注册表时走保守回退。litellm.drop_params = True让不支持的参数(如reasoning_effort)被静默丢弃而不是抛UnsupportedParamsError

6.5 自适应超时

_calculate_timeout:基准 30s(健康检查)/ 120s(普通补全)/ 180s(JSON),乘以 token 因子(相对 4096)与 Provider 因子(ollama 2x、openrouter 1.5x、anthropic 1.2x……)。

6.6 Key 解析与秘密保护

resolve_api_key(app/llm.py)是 Key 解析的唯一真源:优先级为顶层api_keyapi_keys[provider]→ env/settings 默认。例外openai_compatibleollama刻意跳过 env 级LLM_API_KEY回退,防止付费 Key 泄漏到本地服务器。空 Key 的openai_compatible会注入哨兵值sk-no-key以通过 OpenAI 客户端的非空校验。所有错误文本在到达客户端前经_scrub_secrets打码(匹配sk-...AIza...Bearer ...)。

6.7 密钥生命周期:加密存储,永不回写

密钥体系是 CLAUDE.md 的重点规则之一,值得完整展开:

  • API Key 只存在于加密的api_keysSQLite 表(per-provider,经_PROVIDER_KEY_MAP映射,如gemini → google);
  • load_config_file()把解密后的 Key 注入返回的 dict;save_config_file()写入前剥离api_keysapi_key——秘密永远不会回写到config.json(见 app/config.py);
  • PUT /config/llm-api-key不再写入任何 Key,全部走PUT /config/api-keys
  • migrate_legacy_keys()(幂等、非覆盖)把遗留明文 Key 折叠进加密存储——仅在对应 Provider 槽位为空时写入,随后从config.json删除,消除「一个共享 Key 顶替所有 Provider」的 legacy-shadow 缺陷;
  • 加密使用 Fernet(app/crypto.py):密钥位于data/.secret_keychmod 600、gitignored),写入采用「临时文件 + fsync + 原子 rename/hard-link」防止半写读取;解密失败(密钥轮换/丢失)时视为空并提示重新录入,绝不崩溃;
  • API Key 直接作为参数传给 LiteLLM 调用(从不经os.environ),避免异步竞态。

七、必备命令与本地开发

原文档的 Essential Commands 完整保留如下(从仓库根目录执行):

cd apps/backend uv sync # install deps (creates .venv) uv run uvicorn app.main:app --reload --port 8000 # dev server on :8000 uv run app # console script (app.main:main, uses HOST/PORT/RELOAD) uv run playwright install chromium # one-time, required for PDF endpoints

补充说明:

  • 配置通过.env提供(Settings使用pydantic-settings,见 app/config.py,env_file=".env");交互式 API 文档位于/docs
  • uv run app走的是 pyproject.toml 里声明的 console scriptapp = "app.main:main",内部由settings.host/settings.port/settings.reload驱动;
  • 常用环境变量:LLM_PROVIDERLLM_MODELLLM_API_KEYLLM_API_BASELLM_LOG_LEVELLOG_LLM)、HOST/PORT/RELOADLOG_LEVELCORS_ORIGINSFRONTEND_BASE_URLREQUEST_TIMEOUT_SECONDS(钳制 [30, 1800]s,本地模型常需加大)、REASONING_EFFORTminimal/low/medium/high,空值视为不发送)。

八、不可妥协的后端规则

CLAUDE.md 明确列出 5 条硬性规则:

  1. 每个函数都要类型注解(参数 + 返回值),包括辅助函数;
  2. 服务端记详细日志,客户端返回通用消息
except Exception as e: logger.error(f"Operation failed: {e}") raise HTTPException(status_code=500, detail="Operation failed. Please try again.")
  1. 任何可变默认值 / 修改共享或缓存数据前必须copy.deepcopy()(例如config_cache.load_config返回深拷贝;简历安全网助手在编辑前 deepcopy);
  2. 新端点统一挂在/api/v1下(经 app/routers/init.py);
  3. Schema / 提示词变更必须同步反映到对应docs/agent/文档。

九、关键陷阱(Gotchas)

  • uv.lock 被 gitignored:依赖解析不可从 VCS 复现,依赖精确 pin(pyproject.toml/requirements.txt);
  • litellm ↔ python-dotenv 陷阱:litellm<1.84.0硬 pin 了python-dotenv==1.0.1会与其他 pin 冲突;当前锁定litellm==1.86.2python-dotenv==1.2.2不要在未复查 dotenv 的情况下把 litellm 降到 1.84 以下;
  • Key 与普通配置分离:Key 只在加密api_keys表;config.json只放 provider/model/base/features 等非敏感项;写入config.json后记得invalidate_config_cache()
  • Master 简历不变量:全局恰好一条is_master=True。并发上传使用create_resume_atomic_masterasyncio.Lock,非线程锁),若当前 master 卡在failed/processing会自动接管提升(见 app/database.py);
  • 日期丢月份:LLM 常把月份精度丢掉(Jun 2020 - Aug 20212020 - 2021)。restore_dates_from_markdown(app/services/parser.py)+_restore_original_dates会从原始 Markdown 重新插回月份——修改解析/改善流程时必须保留这一行为;
  • 单 worker 假设:缓存与锁假定单个 uvicorn worker / 协作式 async,不要引入跨 worker 共享可变状态而不重新审视config_cache与 master 锁;
  • PDF 依赖前端在跑FRONTEND_BASE_URL(默认http://localhost:3000),Chromium 渲染/print/*页面,浏览器在首次 PDF 请求时懒初始化;
  • improve/confirm 必须有前置 preview:会校验preview_hash,任意构造的 payload 被拒(400)。

十、测试体系

后端测试是刻意投入(见 docs/agent/testing-strategy.md 的完整评估与路线图)。栈为 pytest + pytest-asyncio + httpx + respx,配置在pyproject.toml[tool.pytest.ini_options]。运行方式:

cd apps/backend uv run pytest # 默认排除 LLM 裁判评测(addopts -m "not eval") uv run --with pytest-cov pytest --cov=app --cov-report=term-missing # 覆盖率(临时插件,不改 pyproject) uv run pytest -m eval # 按需跑 LLM-as-judge 评测(需开发者自己的 Key)

目录布局(apps/backend/tests/):

目录覆盖对象说明
unit/纯函数diff、llmprovider/key 助手、parser 日期恢复、真实 SQLite CRUD
service/服务层,LLM 被 mockimprover diff 流程、提示词构造
integration/httpxASGITransport端点config/health/jobs/resume/upload;test_llm_contract.py(基于 respx 的真实llm.py);test_pipeline_e2e.py(上传→定制→渲染,真实路由 + 临时 DB)
evals/提示词质量纯结构评分器(始终运行)+ 门控 LLM 裁判(@pytest.mark.eval

关键设施:conftest.py 的isolated_dbfixture 会把全局db单例替换为临时文件SQLite(而非:memory:——SQLite 连接池会让各连接持有独立内存库,async + sync 双引擎就无法共享状态),并跨所有 router 模块打补丁,让端点/e2e 测试跑在真实(但隔离)的数据库上。respx拦截 HTTP 传输,使llm.py的真实路由逻辑对着假 Ollama / OpenAI 服务器运行(坑:litellm 1.86 需要disable_aiohttp_transport=True才能被 respx 拦截)。CLAUDE.md 还强调每个测试必须anti-theater——目标坏了它就必须失败。

本地推送门禁:.githooks/pre-push会跑上述套件 + locale parity 检查并阻止红推送(git config core.hooksPath .githooks)。项目刻意不设 GitHub Actions PR 门禁(外部 PR 量太大)。

十一、文档导航与任务入口

CLAUDE.md 末尾提供按任务索引的文档表(路径已转换为仓库根相对路径):

主题文档
项目总览docs/agent/README.md
后端架构 / 模块docs/agent/architecture/backend-guide.md · docs/agent/architecture/backend-architecture.md
LLM / 多 Providerdocs/agent/llm-integration.md
提示词流水线(diff/retry 设计)docs/agent/architecture/prompt-workflow-design.md
API 契约docs/agent/apis/front-end-apis.md · docs/agent/apis/api-flow-maps.md · docs/agent/apis/backend-requirements.md
编码规范docs/agent/coding-standards.md
范围与原则docs/agent/scope-and-principles.md · docs/agent/workflow.md
AI 增强docs/agent/features/enrichment.md
JD 匹配docs/agent/features/jd-match.md
自定义章节docs/agent/features/custom-sections.md
i18ndocs/agent/features/i18n.md
PDF / 模板docs/agent/design/pdf-template-guide.md · docs/agent/design/template-system.md

最后,CLAUDE.md 明确划定了「不在范围内」的改动:未经明确请求,不要动.github/workflows/、CI/CD、Docker 行为,以及删除/禁用既有测试(新增与修复测试是被鼓励的)。这是理解该项目协作边界的重要信号——后端代码的高质量依赖这些约定被持续遵守。

【免费下载链接】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),仅供参考

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询