
大家好我是你们熟悉的技术博主。最近在整理求职工具链时看到不少开发者讨论“AI 申请层”这个概念像 ApplyWise 这类应用本质上不是用 AI 自动把所有岗位投一遍而是把求职申请中最耗时间的“阅读理解、内容定制、信息管理”等环节抽象成一个可供系统调用的 AI 层。这篇文章我想换一个角度来写不评价某个具体产品的效果而是从工程实现出发完整拆解一个“求职申请 AI 层”需要哪些模块、如何设计数据结构、如何写可维护的代码以及落地时最容易踩的坑。无论你打算用 Claude、GPT 还是开源本地模型核心思路都可以复用。1. 为什么求职申请需要单独加一层 AI1.1 求职申请的流程并不只是“投简历”很多人会把求职工具简单理解为“批量发送简历”但真正认真写过简历、准备过面试的人都知道一次高质量职位申请通常包含以下步骤阅读职位描述理解岗位职责和硬性要求。对比自己的技能树找出匹配点和待补充项。针对职位调整简历中的项目描述、技术栈顺序、关键词。撰写一封不套模板的求职信或开场介绍。预判 HR 或技术面试会关注的问题。记录每家公司的进展状态避免遗漏。这些步骤有个共同特点重复性强、模板化程度高但每家公司细节又不同。以前只能靠人肉复制粘贴现在 AI 时代最合适的做法就是做一个介于“大模型能力”和“业务数据”之间的 AI Layer。1.2 AI Layer 的职责边界是什么很多试用过 AI 聊天工具的求职者会觉得直接把自己的简历复制给对话窗口再说一句“帮我根据 JD 改简历”就够了。现实中这种用法效果很不稳定原因有三个上下文窗口有限简历很长、JD 可能还包括多条文化要求一次对话塞太多内容模型容易丢失前面的信息。没有结构化数据管理用户每换一个职位都要重新粘贴一遍简历无法积累。没有流程编排真正的申请需要多次读写文件、保存版本、调整语气单纯靠对话无法完成。AI Layer 解决的不是生成能力而是把不稳定的“对话式修改”变成稳定的“函数式调用”。简单说上层是用户界面或定时任务下层是大模型 API中间这层负责解析输入、组装提示词、调用模型、校验输出、清洗格式、保存结果。这个分类就是 AI 应用工程实践的第一步。1.3 常见应用场景从实际使用看求职申请 AI 层至少覆盖以下功能简历解析从 PDF 或 Markdown 中提取教育经历、工作经历、技能列表。JD 解析提取岗位硬技能、加分项、团队介绍、职责关键词。匹配分析结合 JD 和简历给出岗位契合度报告。材料生成输出定制版简历摘要、个人简介、求职信、作品集说明。面试预测根据 JD 生成可能的面试问题与回答思路。申请状态管理将每一阶段的 AI 输出归档到本地方便追溯。你会发现这些场景并不需要模型拥有“强推理”能力核心是保证输入信息不丢失、输出格式可控。这就是工程问题而不是提示词玄学。2. 整体架构怎么设计2.1 先画一条清晰的调用链路在写代码之前我建议把所有功能抽象成一条流水线。假设用户现在要做一次申请流程可以是读取用户的简历文件解析成结构化 JSON。读取目标公司的职位 JD解析成结构化 JSON。将两份 JSON 交给“匹配分析 Agent”。根据匹配结果调用“简历优化器”生成新简历 Markdown。调用“求职信生成器”生成一段 cover letter。将生成的文档保存到按公司名命名的目录下。输出一份申请摘要方便用户核对。这条链路看似简单却已经包含了完整的 AI Agent 雏形。用户在最终确认前可以检查每一步的输出内容。2.2 模块划分建议从工程上我通常把 AI Layer 分成以下模块storage本地文件存储、JSON 读写、版本管理。parsers简历解析器、JD 解析器。schemas统一的数据模型比如简历模型、岗位模型、匹配报告模型。core核心 Agent负责调用大模型完成匹配、生成、改写。prompts所有提示词模板独立存放方便调优。runners命令行入口、Web API 入口或定时任务入口。这样设计的好处是替换底层模型时只需要改 core 模块提示词优化时不需要动业务代码。3. 环境准备与项目结构3.1 运行环境本文的示例代码以 Python 为主建议环境如下Python 3.10 或以上版本。安装 openai 或同类 SDK用于调用大模型 API。pydantic用于做结构化数据校验。python-dotenv用于管理 API Key 环境变量。可选fastapi 或 flask用于把 AI Layer 包装成 Web 服务。不同模型的 API 地址和参数会随版本变化这里不写死某一家。通用的做法是配置 MODEL_API_BASE 和 MODEL_NAME 两个环境变量方便切换。3.2 创建项目目录mkdir applywise-demo cd applywise-demo mkdir -p data/resumes data/jds data/output applywise/{core,parsers,prompts,schemas,storage,utils}目录说明data/resumes存放简历原始文件。data/jds存放职位描述。data/output存放最终的生成结果。applywise/core核心逻辑。applywise/parsers文件解析。applywise/prompts提示词。applywise/schemas数据模型。applywise/storage本地文件读写。applywise/utils通用工具。3.3 初始化 Python 依赖文件在项目根目录创建 requirements.txtopenai1.0.0 pydantic2.0.0 python-dotenv1.0.0 markdown3.4.0 pypdf3.17.0安装命令pip install -r requirements.txt注意pypdf 用于 PDF 文本提取如果简历是 Markdown 格式则可以不安装。没有实际版本依赖的项目可以在使用时按组件说明调整。3.4 创建环境变量文件touch .env写入MODEL_API_BASEhttps://api.example.com/v1 MODEL_NAMEyour-model-name MODEL_API_KEYyour-api-key4. 核心数据模型定义4.1 简历模型在 applywise/schemas/resume.py 中编写from typing import List, Optional from pydantic import BaseModel class Experience(BaseModel): company: str title: str duration: str highlights: List[str] [] class Education(BaseModel): school: str degree: str major: str year: str class Resume(BaseModel): name: str job_title: str skills: List[str] [] years_of_experience: float 0 experiences: List[Experience] [] education: List[Education] [] projects: List[str] [] summary: str 4.2 职位 JD 模型在 applywise/schemas/job.py 中编写from typing import List, Optional from pydantic import BaseModel class JobDescription(BaseModel): company: str position: str responsibilities: List[str] [] must_have_skills: List[str] [] nice_to_have_skills: List[str] [] experience_requirement: str culture_keywords: List[str] [] raw_text: str 4.3 匹配分析报告模型在 applywise/schemas/match_report.py 中编写from typing import List, Optional from pydantic import BaseModel class MatchItem(BaseModel): skill: str status: str # matched / missing / partially_matched evidence: str suggestion: str class MatchReport(BaseModel): overall_score: int 0 matched_items: List[MatchItem] [] missing_items: List[MatchItem] [] risk_analysis: str 为什么使用 pydantic因为在调用大模型时可以用结构化输出功能让模型返回 JSON之后再校验字典是否完整。这样避免了模型返回少一个键导致后续代码崩溃的问题。5. 提示词模板如何设计很多初学者喜欢把提示词写在代码字符串里例如prompt f你是我的求职助手这是我的简历{resume}帮我修改...这种写法在需求简单时还可以但当一个 AI Layer 有多个功能时提示词会越来越长代码也变得越来越难维护。工程上建议把提示词统一放到 prompts 目录。5.1 匹配分析提示词在 applywise/prompts/match_prompt.txt 中写入你是一名资深技术招聘顾问。下面你会收到一份候选人的结构化简历和一份职位描述的结构化信息。 请分析候选人是否适合这个职位只输出 JSON 格式结果。 JSON 格式要求 { overall_score: 0-100 的整数, matched_items: [ { skill: 技能名称, status: matched/partially_matched, evidence: 简历中对应的证据, suggestion: 如何进一步强化这个匹配点 } ], missing_items: [ { skill: 缺失技能名称, status: missing, evidence: , suggestion: 可以如何弥补或说明 } ], risk_analysis: 总结这个候选人申请该职位的风险点与优势 } 候选人简历 {resume_json} 职位描述 {job_json} 只输出 JSON不要输出解释。5.2 求职信提示词在 applywise/prompts/cover_letter_prompt.txt 中写入你是一名求职文案顾问。请根据以下候选人简历、目标职位和匹配分析写一封不超过 200 字的中文求职信。 语气要专业但不浮夸突出与职位相关的能力不要重复候选人简历里已经列出的所有细节。 候选人简历 {resume_json} 目标职位 {job_json} 匹配分析 {match_report_json} 输出格式为纯文本直接输出求职信正文。5.3 自定义变量格式为了让提示词文件可以被加载后替换引入一个简单的模板函数。在 applywise/utils/template.py 中编写from pathlib import Path def load_prompt(prompt_file: str, **kwargs) - str: project_root Path(__file__).resolve().parents[2] prompt_path project_root / applywise / prompts / prompt_file text prompt_path.read_text(encodingutf-8) for key, value in kwargs.items(): text text.replace({ key }, str(value)) return text这样代码中调用时只需要prompt load_prompt(match_prompt.txt, resume_jsonresume.model_dump_json(), job_jsonjob.model_dump_json())好处是以后想针对不同行业调整提示词直接改文案不用改代码。6. 核心 Agent 实现6.1 底层模型客户端在 applywise/core/llm_client.py 中统一封装模型调用import os from openai import OpenAI from dotenv import load_dotenv load_dotenv() _client None def get_client() - OpenAI: global _client if _client is None: _client OpenAI( base_urlos.getenv(MODEL_API_BASE), api_keyos.getenv(MODEL_API_KEY), ) return _client def chat(messages, temperature0.3): client get_client() resp client.chat.completions.create( modelos.getenv(MODEL_NAME), messagesmessages, temperaturetemperature, ) return resp.choices[0].message.content说明使用全局单例避免每次调用重新创建连接。temperature 设置为 0.3保证输出稳定性因为求职材料生成场景更强调准确性不太需要天马行空的创意。如果使用的是本地模型比如通过 Ollama 暴露的兼容接口只需要修改 base_url代码不需要改动。6.2 结构化输出解析大模型输出文本不稳定即使要求它只输出 JSON也可能带上 json 这样的代码块。所以需要写一个健壮的解析函数。在 applywise/utils/json_parser.py 中编写import json import re def parse_json_from_text(text: str) - dict: text text.strip() if text.startswith(): text re.sub(r^(?:json)?, , text) text re.sub(r$, , text) # 寻找第一个 { 和最后一个 } start text.find({) end text.rfind(}) if start ! -1 and end ! -1 and end start: json_str text[start:end1] return json.loads(json_str) raise ValueError(模型输出中没有找到合法 JSON 片段)6.3 匹配分析 Agent在 applywise/core/match_analyzer.py 中编写import json from applywise.core.llm_client import chat from applywise.schemas.resume import Resume from applywise.schemas.job import JobDescription from applywise.schemas.match_report import MatchReport from applywise.utils.template import load_prompt from applywise.utils.json_parser import parse_json_from_text class MatchAnalyzer: def __init__(self): pass def analyze(self, resume: Resume, job: JobDescription) - MatchReport: resume_json resume.model_dump_json() job_json job.model_dump_json() prompt load_prompt( match_prompt.txt, resume_jsonresume_json, job_jsonjob_json, ) raw_content chat([{role: user, content: prompt}]) data parse_json_from_text(raw_content) return MatchReport.model_validate(data)注意这一步并没有直接把简历和 JD 原文丢给模型而是先让解析器把它们变成 JSON。为什么这么做因为直接使用原文时模型的注意力会被无关内容分散。比如简历中有大量项目描述但岗位只关心“微服务架构经验”结构化后模型更容易提取。6.4 简历优化与求职信生成 Agent在 applywise/core/cover_letter.py 中编写from applywise.core.llm_client import chat from applywise.schemas.resume import Resume from applywise.schemas.job import JobDescription from applywise.schemas.match_report import MatchReport from applywise.utils.template import load_prompt class CoverLetterGenerator: def __init__(self): pass def generate(self, resume: Resume, job: JobDescription, report: MatchReport) - str: prompt load_prompt( cover_letter_prompt.txt, resume_jsonresume.model_dump_json(), job_jsonjob.model_dump_json(), match_report_jsonreport.model_dump_json(), ) raw_content chat([{role: user, content: prompt}]) return raw_content.strip()7. 文件解析模块实现7.1 简历文本解析如果用户的简历是 PDF 文件最稳妥的方式是先用 pypdf 提取纯文本再由大模型整理成结构化 JSON。注意直接依靠 PDF 内嵌元数据是不可靠的。在 applywise/parsers/resume_parser.py 中编写import json from pathlib import Path from pypdf import PdfReader from applywise.core.llm_client import chat from applywise.schemas.resume import Resume from applywise.utils.template import load_prompt def extract_text_from_pdf(pdf_path: str) - str: reader PdfReader(pdf_path) pages [] for page in reader.pages: pages.append(page.extract_text() or ) return \n.join(pages) def resume_from_pdf(pdf_path: str) - Resume: text extract_text_from_pdf(pdf_path) return resume_from_text(text) def resume_from_text(text: str) - Resume: prompt load_prompt(resume_struct_prompt.txt, resume_texttext) raw chat([{role: user, content: prompt}]) data json.loads(raw) return Resume.model_validate(data)7.2 简历整理提示词在 applywise/prompts/resume_struct_prompt.txt 中写入请把下面的简历文本转成结构化 JSON。要求 1. skills 字段提取所有编程语言、框架、工具统一为小写。 2. years_of_experience 根据已有经历推算。 3. experiences 中每个公司可以有多条 highlight。 4. 只输出 JSON。 简历文本 {resume_text}这个模块的作用是让后续所有 Agent 使用的简历数据格式始终一致而不是每次重复向模型发送又长又乱的原文。8. 一个完整可运行的实战案例下面我们把核心流程串起来写一个命令行脚本。用户在命令行传入简历 PDF 和职位 JD 文本AI Layer 输出匹配报告和求职信。8.1 创建 JD 文件示例在 data/jds/senior_backend_engineer.md 中创建公司某云计算公司 职位高级后端工程师 职责 - 负责分布式任务调度系统的设计、开发与维护。 - 参与系统性能调优保障高并发场景下的稳定性。 - 负责相关服务的监控、告警和故障恢复。 硬性要求 - 5年以上后端开发经验。 - 熟悉 Java 或 Go。 - 熟悉分布式系统常见协议与中间件。 - 有高并发系统设计经验。 加分项 - 有 Kubernetes 生产环境经验。 - 有开源项目维护经历。 - 熟悉消息队列原理。 团队文化 - 重视技术文档沉淀。 - 强调代码可读性和工程规范。8.2 创建入口脚本在项目根目录创建 run_apply.pyimport sys from pathlib import Path from applywise.schemas.resume import Resume from applywise.schemas.job import JobDescription from applywise.parsers.resume_parser import resume_from_pdf from applywise.core.match_analyzer import MatchAnalyzer from applywise.core.cover_letter import CoverLetterGenerator def load_job_from_markdown(path: str) - JobDescription: text Path(path).read_text(encodingutf-8) # 这里为简化示例写死字段实际项目中可以将 Markdown 交给 LLM 整理成 JobDescription lines [line.strip() for line in text.splitlines() if line.strip()] responsibilities [] must_have [] nice_to_have [] current_section None for line in lines: if line.startswith(职责): current_section resp continue if line.startswith(硬性要求): current_section must continue if line.startswith(加分项): current_section nice continue if line.startswith(团队文化): current_section None continue if line.startswith(- ) and current_section resp: responsibilities.append(line[2:].strip()) elif line.startswith(- ) and current_section must: must_have.append(line[2:].strip()) elif line.startswith(- ) and current_section nice: nice_to_have.append(line[2:].strip()) company 某云计算公司 position 高级后端工程师 for line in lines: if line.startswith(公司): company line.replace(公司, ).strip() if line.startswith(职位): position line.replace(职位, ).strip() return JobDescription( companycompany, positionposition, responsibilitiesresponsibilities, must_have_skillsmust_have, nice_to_have_skillsnice_to_have, experience_requirement5年, culture_keywords[文档, 工程规范], raw_texttext, ) def main(): resume_path sys.argv[1] if len(sys.argv) 1 else data/resumes/alice.pdf job_path sys.argv[2] if len(sys.argv) 2 else data/jds/senior_backend_engineer.md resume: Resume resume_from_pdf(resume_path) job: JobDescription load_job_from_markdown(job_path) analyzer MatchAnalyzer() report analyzer.analyze(resume, job) print(整体匹配度, report.overall_score) print(硬性技能缺失项) for item in report.missing_items: print(-, item.skill, 建议, item.suggestion) print(风险分析, report.risk_analysis) generator CoverLetterGenerator() cover_letter generator.generate(resume, job, report) print(\n 求职信 \n) print(cover_letter) if __name__ __main__: main()8.3 运行命令与预期效果python run_apply.py data/resumes/alice.pdf data/jds/senior_backend_engineer.md预期输出包含三部分整体匹配度比如 78。缺失技能列表比如没有 Kubernetes 生产经验。一封 200 字左右的求职信。在执行真实调用前建议先确保 API Key 有最低权限最好是只读访问模型接口不要使用具有管理权限的 Token。同时简历 PDF 里的敏感联系方式在格式化输出时会被保留本地运行没问题但如果部署为 Web 服务一定要增加访问权限控制。9. 把 AI Layer 包装成 Web 服务命令行工具适合自己用但如果想做成团队内部工具或者让非技术用户通过浏览器操作可以把核心模块用 FastAPI 包一层接口。在项目根目录创建 server.pyfrom fastapi import FastAPI, File, UploadFile, Form from pydantic import BaseModel from applywise.parsers.resume_parser import resume_from_pdf from applywise.core.match_analyzer import MatchAnalyzer from applywise.core.cover_letter import CoverLetterGenerator from applywise.schemas.job import JobDescription app FastAPI() class JobTextIn(BaseModel): raw_jd: str app.post(/api/match) async def match( resume_file: UploadFile File(...), jd_text: str Form(...), ): # 将上传文件临时保存 temp_path f/tmp/{resume_file.filename} with open(temp_path, wb) as f: content await resume_file.read() f.write(content) resume resume_from_pdf(temp_path) job JobDescription(raw_textjd_text) analyzer MatchAnalyzer() report analyzer.analyze(resume, job) return { overall_score: report.overall_score, risk_analysis: report.risk_analysis, }启动服务pip install fastapi uvicorn python-multipart uvicorn server:app --reload --port 8000接口调用示例curl -X POST http://localhost:8000/api/match \ -F resume_filedata/resumes/alice.pdf \ -F jd_text某云计算公司招聘高级后端工程师...使用 Web 服务时要特别注意上传的简历文件包含手机、邮箱、地址等个人隐私。不要让服务监听公网地址建议默认监听 127.0.0.1或者放在企业内网并通过网关鉴权。10. 常见问题与排查思路问题现象常见原因解决思路模型返回内容不是 JSON提示词约束不足或模型本身输出不稳定开启响应格式的 json_object 模式增加二次解析与重试逻辑匹配分数总是不符合预期resume 解析时丢失了关键项目背景检查结构化后的简历字段确认 skills 和 experiences 是否完整PDF 简历提取后乱码部分 PDF 是扫描件没有文本层先做 OCR再送入模型或者要求用户上传 Markdown/Word 版本求职信套话严重提示词缺少具体信息和匹配分析输入传入 MatchReport并限制“必须引用一个具体项目”API 调用超时简历过长、上下文过大先截断摘要只传核心字段或升级支持长上下文的模型输出内容包含不存在的项目经历模型“幻觉”用示例数据集里的信息填补在提示词中明确“不能编造简历中不存在的内容”并在生成后人工审核开发这类应用时我强烈建议对最终 AI 生成的内容做两层检查第一层是程序化校验比如判断输出是否为空、是否仍是原简历内容第二层是人工阅读。求职信和面试答案直接关系到面试第一印象不建议完全无人看管地自动发送。11. AI Layer 的工程落地建议11.1 数据安全优先处理简历这类高隐私数据时应该默认遵守最小权限原则全部解析在本地或企业内网完成。如果必须调用云端模型需要对简历做脱敏处理比如把手机号和邮箱替换为占位符。API Key 使用环境变量或密钥管理服务注入不要硬编码在源码中更不要把密钥提交到代码仓库。11.2 用量可控求职申请是低频但调用密集的场景比如一次匹配需要同时传入简历和 JDtoken 消耗可能会很高。建议做三层控制对单次任务设置最大输入长度。对大模型接口增加超时时间。对用户调用频率做限流。11.3 模板与代码分离很多 AI 应用的问题在于业务人员想优化文案却要被迫修改 Python 代码。在工程上把提示词抽离成独立文件既方便测试不同策略也方便和业务同事协作。11.4 做好请求日志每次调用大模型之前和之后都应该记录输入输出摘要。日志结构可以包含请求时间。任务类型。输入数据摘要不要记录完整简历。模型名称。输出成功与否。如果出错记录错误类型。日志的目的是帮助复现问题。模型输出差的原因通常来自三个地方输入数据不完整、提示词不合适、模型不适合当前任务。11.5 保留版本管理求职申请 AI 工具修改简历时会覆盖原始版本建议在生成文件时自动加入时间戳或公司名。例如data/output/{company}_{position}_resume.md data/output/{company}_{position}_cover_letter.md这样用户可以对比修改前后不会被 AI 改动得面目全非。12. 总结与下一步技术路线这篇文章用一个实际的求职申请 AI 层项目为线索从数据模型、提示词设计、模型客户端封装、文件解析到 Web 服务打包完整展示了一个 AI Agent 应用的基本工程结构。你完全可以基于这套代码继续扩展“面试问答生成”“申请状态跟踪”等功能。如果想让系统更智能可以尝试以下升级方向引入 RAG将公司官网、团队博客和以往面试经验作为知识库在生成求职信前先做检索。接入工作流引擎比如用 LangGraph 或自研状态机编排多轮调用。添加任务队列让用户在 Web 页面上传简历后不用等待同步调用而是先返回任务 ID后台异步执行。对生成内容做自动质量评估比如判断是否符合 JD 关键词覆盖率再决定是否重新生成。建议你先从本地命令行版本开始跑通全流程再加上 Web 展示层。记住AI Layer 不是替代求职者的判断力而是把重复劳动压缩到几分钟让你把精力花在真正需要人的经验和临场反应的地方。代码仓库里的逻辑都比较清晰动手改起来很容易。有搭建过程中遇到的其他问题也欢迎在评论区交流。