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.py、app/config.py、app/llm.py、app/database.py、app/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 框架 | FastAPI | 0.128.4,配合uvicorn 0.40.0 |
| 运行时 | Python | >=3.13(requires-python硬性要求) |
| 数据校验 | Pydantic v2 / pydantic-settings | 2.12.5/2.14.2,环境变量即配置 |
| ORM / 存储 | SQLAlchemy 2(async)+ SQLite | sqlalchemy[asyncio]==2.0.36+aiosqlite==0.20.0 |
| 多模型接入 | LiteLLM | 1.86.2(Router、RetryPolicy、drop_params) |
| 文档解析 | markitdown | 0.1.4(DOCX/PDF → Markdown) |
| PDF 渲染 | Playwright / Chromium | 1.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/v1) | app/main.py |
| 配置 | pydantic-settings 读取环境变量;settings单例;API Key 从加密 SQLite 存储读取 | app/config.py |
| 加密 | Fernet 加解密 API Key 落盘(data/.secret_key,chmod 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 |
| LLM | LiteLLM 封装:Router、重试、JSON 抽取、超时、Provider 兼容性处理 | app/llm.py |
无头 Chromium 渲染前端/print/*页面;浏览器懒初始化 | app/pdf.py | |
| 路由层 | HTTP 端点(见下节) | app/routers/*.py |
| 服务层 | 业务逻辑(解析、改善/diff、精修、求职信) | app/services/*.py |
| 提示词 | 全部 LLM 提示词模板 + 占位符校验 | app/prompts/*.py |
| Schema | Pydantic 请求/响应 +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/*.json、data/.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.py | GET /health、GET /status | liveness 探活(不发 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/upload、GET /resumes、/resumes/list、/resumes/improve+/improve/preview+/improve/confirm、PATCH /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 | 看板追踪 CRUD | Kanban 投递追踪 |
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),完整流程分六步:
- 从
db加载简历 + 职位;解析内容语言(config_cache)与prompt_id; extract_job_keywords(jd)(LLM 抽取,按职位内容哈希缓存);- 若存在结构化
processed_data→diff 模式:skill-target plan →generate_resume_diffs→apply_diffs→verify_diff_result;否则回退improve_resume(整份输出); - 本地安全网(始终运行、纵深防御):
_preserve_personal_info、_restore_original_dates、restore_dates_from_markdown、_preserve_original_skills、_protect_custom_sections; refine_resume(关键词注入 + AI 味短语清洗 + 对齐校验);- 在 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 执行四道闸门:
- 路径必须在白名单内(
_ALLOWED_PATH_PATTERNS:summary、workExperience[i].description[j]、personalProjects[i].description[j]、education[i].description、additional.technicalSkills等); - 路径不得命中黑名单(
personalInfo、customSections、sectionMeta前缀,以及company/title/degree/institution/name等身份字段叶子名); - 路径必须能在原数据中实际解析到值;
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 instructions、system:、<system>、[INST]等常见注入模式替换为[REDACTED]。
五、提示词管理:读模板前必看
CLAUDE.md 花了大篇幅讲提示词管理,因为这是最容易被改坏的地方:
- 提示词是纯 Python 字符串常量,无 Jinja、无运行时外部文件(
data/prompts.json不参与加载); - 布局:
templates.py(简历解析、关键词抽取、3 种 improve 变体、diff 提示词、skill-target 计划、求职信/外联信/标题、RESUME_SCHEMA_EXAMPLE、CRITICAL_TRUTHFULNESS_RULES、LANGUAGE_NAMES+get_language_name())、enrichment.py(ANALYZE_RESUME_PROMPT等 4 个)、refinement.py(KEYWORD_INJECTION_PROMPT、VALIDATION_POLISH_PROMPT、AI_PHRASE_BLACKLIST、AI_PHRASE_REPLACEMENTS)、__init__.py(重导出 + 占位符校验)。
5.1 占位符格式化规则(最容易踩坑)
服务层统一from app.prompts import ...后调用PROMPT.format(**vars)。因此:
{placeholder}是真实 format key;- 任何字面量
{}(例如 JSON 示例中的花括号)必须写成{{ }}——参见EXTRACT_KEYWORDS_PROMPT、DIFF_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.json的default_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_prompt、outreach_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 抽象与模型名映射
支持openai、openai_compatible(llama.cpp/vLLM/LM Studio)、anthropic、openrouter、gemini、deepseek、groq、ollama八类 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剥离结尾/v1;ollama剥离/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_fingerprint用hash()计算 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_truncated按schema_type分治:resume检查workExperience/education/skills是否为空数组,enrichment检查键是否缺失,interview_prep检查五个必备键,diff/keywords不设启发式(空 diff 与空关键词都是合法的)。
6.4 能力注册表(而非硬编码)
_supports_json_mode、_supports_temperature、get_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_key→api_keys[provider]→ env/settings 默认。例外:openai_compatible与ollama刻意跳过 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_keys与api_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_key(chmod 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_PROVIDER、LLM_MODEL、LLM_API_KEY、LLM_API_BASE、LLM_LOG_LEVEL(LOG_LLM)、HOST/PORT/RELOAD、LOG_LEVEL、CORS_ORIGINS、FRONTEND_BASE_URL、REQUEST_TIMEOUT_SECONDS(钳制 [30, 1800]s,本地模型常需加大)、REASONING_EFFORT(minimal/low/medium/high,空值视为不发送)。
八、不可妥协的后端规则
CLAUDE.md 明确列出 5 条硬性规则:
- 每个函数都要类型注解(参数 + 返回值),包括辅助函数;
- 服务端记详细日志,客户端返回通用消息:
except Exception as e: logger.error(f"Operation failed: {e}") raise HTTPException(status_code=500, detail="Operation failed. Please try again.")- 任何可变默认值 / 修改共享或缓存数据前必须
copy.deepcopy()(例如config_cache.load_config返回深拷贝;简历安全网助手在编辑前 deepcopy); - 新端点统一挂在
/api/v1下(经 app/routers/init.py); - 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.2、python-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_master(asyncio.Lock,非线程锁),若当前 master 卡在failed/processing会自动接管提升(见 app/database.py); - 日期丢月份:LLM 常把月份精度丢掉(
Jun 2020 - Aug 2021变2020 - 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 被 mock | improver 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 / 多 Provider | docs/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 |
| i18n | docs/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),仅供参考