面向千万毕业生的求职场景Agent 要当好“求职搭子”单靠一句自然的开场白远远不够。简历上传之后能不能读懂岗位 JD 每天都在变化模拟面试之后能不能把薄弱环节沉淀到下一轮计划里这些才是用户是否愿意长期使用的关键。真正决定产品口碑的部分往往在水面以下Agent 的记忆结构、检索质量、工具调用边界、中断恢复和人工兜底机制。本文不绑定某个具体产品而是从“求职搭子”这类任务型 Agent 的通用工程视角切入。我们会先拆解求职场景要完成哪些子任务再说明为什么需要“计划-行动-观察”循环然后搭建一个最小可运行的 Agent 示例最后补上 RAG、状态管理、隐私安全、评测和排错方案。学完这套结构后也可以把它迁移到简历诊断、模拟面试、岗位提醒等不同功能里。1. 先拆清求职搭子要做的事再谈大模型能力很多团队开发第一版 Agent 时容易陷进“模型很聪明对话很流畅”的误区。对求职用户来说一段热闹但没有任务的对话没有价值。求职搭子的本质是一个任务执行器它要把模糊的描述变成可操作的求职动作并且跟踪结果。1.1 用户需要的不是一个聊天机器人传统聊天机器人做的事情是“答”用户问一句模型答一句。求职搭子要做的事情是“办”用户说“我最近在投后端岗位有点焦虑”它可能需要完成以下动作读取简历、定位目标岗位、检索岗位 JD、计算匹配度、找出缺失技能、生成复习计划、把任务写进日历。这两类产品的差别直接决定技术架构。聊天机器人只需要对话模型和会话缓存任务型 Agent 至少需要以下能力识别用户这次请求属于简历、岗位、面试、提醒中的哪一类意图。从对话中抽取实体比如简历编号、职位编号、时间、公司名称。调用工具获取真实数据比如岗位接口、简历服务、日历服务。根据工具返回结果生成用户可读的结论而不是凭模型记忆编造。把结果写回记忆下一次对话能知道上次推进到哪个环节。如果只把大模型当作文本生成器那么产品只会“聊得热闹办不成事”。1.2 从功能表反推技术模块可以先把“求职搭子”的核心功能列成一张表每一行对应一组 Agent 子任务。技术设计前先做功能拆解是为了避免开发时只做“提示词工程”。功能用户典型说法Agent 要做什么需要的资源和工具简历诊断帮我看下这份简历哪里需要改读取简历、抽取技能和教育经历、对比岗位方向、给出修改建议简历解析服务、职位方向知识库岗位匹配这个岗位适合我吗获取简历和 JD、抽取关键词、计算匹配分、说明优劣势简历库、岗位库、匹配策略服务模拟面试按这个岗位面我一次检索 JD 和简历、生成题目、逐题点评、统计薄弱点面试题库、知识库、面试评估提示词求职提醒提醒我周四下午参加某公司二面理解时间/公司/环节、创建日程、设置提醒日历工具、定时任务服务求职问答银行科技岗一般问什么搜索相关面经、组织答案、注明信息时效岗位面经知识库、RAG 检索这些功能看起来都是“对话”但“岗位匹配”和“模拟面试”内部的工作流差异很大。岗位匹配是查询和计算模拟面试是多轮状态流转。因此代码里不能写死一套提示词而应该让 Agent 根据意图选择不同子流程。1.3 本文选择的技术主线为了让文章有可复现的落点下面的实现主线选择“简历与岗位匹配”这个最小场景。完整流程为用户提交请求并携带 resume_id 和 job_id。Agent 收到文本后判断是否必须调用工具。调用匹配工具读取数据库中的简历和岗位。工具返回关键词命中、缺失技能和评分。Agent 基于结果生成建议并询问下一步。这里的关键不是匹配算法本身而是 Agent 如何决定调用工具、如何把工具结果转化为回答、如何避免没有数据就编造。这一套机制可以复用到提醒、面试评估和简历诊断中。2. 要让 Agent 真正“做事”计划、调用工具、观察结果2.1 一次大模型调用解决不了所有求职任务写单轮 Prompt 时开发者会把“简历内容 JD 内容 指令”一次性塞给模型让它直接输出结论。这在简历很短、字段很稳定的演示环境里可行但在生产环境会遇到三个问题。第一上下文太长。一份完整简历可能包含项目经历、实习经历、专业技能、自我评价如果每次匹配都拼进上下文成本会线性上升。第二数据时效和精确性无法保证。岗位 JD 可能来自第三方招聘接口简历可能有多个版本模型无法单靠自身记忆知道当前数据库里存的是什么。第三错误责任不清。一旦结果错误我们无法判断是模型判断错误还是它根本没读取到正确数据。所以任务型 Agent 更适合采用“模型负责决策工具负责执行”的架构。模型看到用户问题后决定调用哪个工具、传什么参数工具返回结构化结果之后模型再把这些结果转成用户能看懂的语言。2.2 用 Function Calling 把能力暴露给模型在 OpenAI 兼容的接口里可以通过 tools 参数定义工具。以匹配工具为例它的核心作用是接收两个业务 ID返回一个结构化的匹配结果。{ type: function, function: { name: get_resume_job_match, description: 读取指定的简历和职位返回匹配度评分以及缺失技能, parameters: { type: object, properties: { resume_id: { type: string, description: 简历编号 }, job_id: { type: string, description: 职位编号 } }, required: [resume_id, job_id] } } }工具定义的关键不只是“有哪些参数”还包括 description 怎么写。模型需要从 description 里判断“这个用户请求该不该调用这个工具”。如果描述写成泛泛的“匹配函数”模型可能在一个只需解释面试流程的请求里误调用。推荐写法是明确写出触发条件和参数含义。模型返回的内容里如果包含tool_calls就说明它认为需要调用工具。此时业务代码应该停下对话生成先执行工具再把工具返回值以role: tool的消息追加回对话上下文。这个过程也叫“计划-行动-观察”循环模型先计划代码行动模型再观察工具输出并给出最终回复。2.3 记忆是求职搭子的隐形状态很多 Agent Demo 只实现了单轮 Function Calling跑完一次就结束。求职场景天然需要长期记忆因为用户可能今天上传简历明天咨询岗位后天参加模拟面试一周后再回来问“上一次建议我补什么”。推荐至少维护三层记忆记忆层存储内容典型字段短期会话记忆当前对话上下文消息列表、最近意图、待确认参数用户长期档案简历、求职偏好、学历技能user_id、resume_id、目标岗位、地域偏好业务状态记忆当前任务推进到哪一步thread_id、state、关联的 job_id、面试时间不能只在数据库里存“聊天记录”。例如模拟面试任务需要维护当前题目编号和用户回答如果只存聊天记录恢复线程时很难定位“这是第几题、是否已经点评”。建议单独设计任务状态表状态字段的值可以是idle、collecting、matching、interviewing、confirming_schedule。3. 从零搭建最小可运行的求职搭子 Agent3.1 环境准备和依赖下面示例使用 Python 和 OpenAI 兼容的 Chat Completions 接口。实际项目中可以替换为任意支持 Function Calling 或 Tool Calling 的大模型服务只要 endpoint 和模型名符合对应规范。建议的开发环境如下环境项建议值说明Python3.11 及以上类型标注和异步支持更友好API 客户端openai Python SDK用于 Chat Completions 和 Embeddings数据校验Pydantic校验模型结构化输出Web 框架FastAPI后期封装 HTTP 接口时使用数据库MySQL 或 PostgreSQL存储简历、职位、会话状态向量库Chroma / Milvus / pgvector简历和岗位语义检索requirements 文件可以先这样写落地前需要固定版本生成 lock 文件openai1.40.0 pydantic2.7.0 python-dotenv1.0.1 fastapi0.111.0 uvicorn[standard]0.30.0在 .env 文件中配置模型访问信息。不要把密钥写进代码库。LLM_API_KEYyour_api_key_here LLM_BASE_URLhttps://api.your-llm-provider.example.com/v1 LLM_MODELmodel-name EMBEDDING_MODELembedding-model-name如果原始资料没有给出明确的模型名称落地前要确认目标模型是否支持工具调用以及 Embedding 模型的最大输入 token。不同模型的参数差异会影响切分策略。3.2 实现一个通用 Agent 主循环下面代码的核心逻辑是循环请求模型如果模型返回tool_calls就执行对应工具并把结果追加回消息列表如果模型返回纯文本就结束循环。import json import os from openai import OpenAI client OpenAI( api_keyos.environ[LLM_API_KEY], base_urlos.environ.get(LLM_BASE_URL), ) TOOLS [ { type: function, function: { name: get_resume_job_match, description: 读取指定的简历和职位返回匹配度评分、命中关键词和缺失技能, parameters: { type: object, properties: { resume_id: {type: string, description: 简历编号}, job_id: {type: string, description: 职位编号} }, required: [resume_id, job_id] } } } ] def run_agent(user_input: str) - str: messages [ { role: system, content: ( 你是求职搭子。只要用户想判断某个岗位是否适合他 你就必须调用 get_resume_job_match 工具。 在没有工具结果之前不要直接下结论。 ) }, {role: user, content: user_input} ] for step in range(5): response client.chat.completions.create( modelos.environ[LLM_MODEL], messagesmessages, toolsTOOLS, tool_choiceauto, ) assistant_message response.choices[0].message # 转成普通字典避免不同 SDK 版本之间的兼容问题 messages.append(json.loads(assistant_message.model_dump_json())) if not assistant_message.tool_calls: return assistant_message.content or for tool_call in assistant_message.tool_calls: arguments json.loads(tool_call.function.arguments) if tool_call.function.name get_resume_job_match: result compute_match( resume_idarguments.get(resume_id), job_idarguments.get(job_id), ) else: result {error: unknown tool} messages.append({ role: tool, tool_call_id: tool_call.id, content: json.dumps(result, ensure_asciiFalse), }) return 任务未在限定步数内完成已转人工处理。这里需要特别说明循环退出的标志。模型返回的assistant_message同时包含content和tool_calls时应该优先处理tool_calls因为最终答案可能依赖工具返回值。如果只读取content会漏掉 Agent 的决策过程。3.3 用最简单的规则实现匹配工具示例的匹配工具不依赖复杂算法只做两件事从 JD 中提取关键词统计这些关键词是否出现在简历文本中。JOB_KEYWORD_BANK [ Python, Java, MySQL, Redis, Docker, Kubernetes, 数据分析, 大模型, 算法, 前端, ] def extract_job_keywords(jd_text: str) - list[str]: return [kw for kw in JOB_KEYWORD_BANK if kw in jd_text] def compute_match(resume_id: str, job_id: str) - dict: # 实际项目中从数据库读取 resume_text demo_resume_content(resume_id) job_description demo_job_content(job_id) keywords extract_job_keywords(job_description) matched [kw for kw in keywords if kw in resume_text] missing [kw for kw in keywords if kw not in resume_text] score 0.0 if keywords: score round(100 * len(matched) / len(keywords), 1) return { resume_id: resume_id, job_id: job_id, score: score, matched_keywords: matched, missing_keywords: missing, }这个规则实现适合演示。真实项目中更常见的是“关键词打分 Embedding 相似度 规则过滤”的组合关键词负责显性技能判断向量相似度负责语义匹配。比如简历里写“熟悉容器化部署”即使没有出现“Docker”字样向量检索也能关联上。值得注意的是不能把关键词银行做得过大否则 JD 里无关技能会干扰结果。需要结合岗位方向建立技能词库并为不同职能配置不同权重。3.4 运行示例与预期输出调用入口可以这样写if __name__ __main__: result run_agent(请帮我看看简历 R001 和岗位 J002 是否匹配) print(result)正常成功时会看到类似日志和输出[Trace] tool call: get_resume_job_match [Trace] arguments: {resume_id: R001, job_id: J002} [Trace] tool result: {score: 60.0, matched_keywords: [Python], missing_keywords: [Redis, Docker]} [Assistant] 根据岗位 JD 中的技能要求简历匹配度约 60%。Python 已经是加分项但岗位强调 Redis 和 Docker简历里还没有体现。需要我针对缺失技能生成一份准备计划吗如果用户没有提供 resume_id 或 job_id模型会尝试从上下文推断。此时两个参数可能是空值匹配结果没有意义。开发阶段可以增加一个校验工具或者要求模型在参数缺失时先向用户确认而不是继续调用工具。4. 简历、岗位和面试题库如何成为 Agent 的知识4.1 为什么不能把所有资料都写进 Prompt有人会问既然大模型能读文本为什么不把所有岗位 JD 和简历都放到 Prompt 里答案很直接放不下也放不准。一个真实的求职平台可能有数十万条岗位信息简历也有多个版本。Prompt 的上下文窗口有限费用和延迟都随长度增长。更关键的是检索和生成是两件事。让模型在海量资料中找到目标既浪费 token又容易受到无关信息干扰。正确做法是先用 RAG 召回最相关的资料片段再把小批资料交给 Agent 使用。需要先理解 RAG 在这里的角色它不是回答问题的最终模型而是 Agent 的“资料员”。Agent 判断该查资料后RAG 负责返回与问题最相关的段落。接下来Agent 可以用这些段落生成建议、生成面试题或组织回答。4.2 文档切分与向量化示例简历和 JD 的文本结构不同不能简单按固定长度截断。推荐先按段落边界切分再通过重叠窗口保留上下文。import re def chunk_document(document: str, max_chars: int 800, overlap: int 80) - list[str]: paragraphs re.split(r\n\s*\n, document) chunks [] current for paragraph in paragraphs: paragraph paragraph.strip() if not paragraph: continue if len(current) len(paragraph) 1 max_chars: current f{current}\n{paragraph} if current else paragraph else: if current: chunks.append(current) current paragraph if current: chunks.append(current) # 对过长段落做硬切分并保留 overlap final_chunks [] for chunk in chunks: if len(chunk) max_chars: final_chunks.append(chunk) continue start 0 while start len(chunk): end min(start max_chars, len(chunk)) final_chunks.append(chunk[start:end]) start end - overlap if start 0: start 0 if start len(chunk): break return final_chunks切分时不要只考虑字符长度。简历里的“项目经历”可能只有 200 字但内部信息高度浓缩面试经验帖可能是散列表单切成多个小块后不能丢失标题上下文。因此生产方案通常会给每个 chunk 带上业务元数据比如简历编号、板块标题、岗位类型。向量化代码非常短关键是选对 Embedding 模型和存储方案。def embed_document(document: str) - list[float]: resp client.embeddings.create( modelos.environ[EMBEDDING_MODEL], inputdocument, ) return resp.data[0].embedding def cosine_similarity(vec1: list[float], vec2: list[float]) - float: dot sum(a * b for a, b in zip(vec1, vec2)) norm1 sum(a * a for a in vec1) ** 0.5 norm2 sum(b * b for b in vec2) ** 0.5 if norm1 0 or norm2 0: return 0.0 return dot / (norm1 * norm2)线上服务不要每来一次请求都全量扫描向量应该把向量写入独立的向量库并用top-k召回。快速验证阶段可以用本地内存向量列表生产环境建议切换到支持索引和过滤的向量数据库并根据岗位方向、城市、公司名称做元数据过滤。4.3 存储层设计关系表与向量表如何配合Agent 需要同时处理两类数据一类是强业务结构的记录比如用户、简历、岗位、会话另一类是语义检索用的向量。强数据结构放在关系库向量结构放在向量库两者通过业务 ID 关联。下面是一组简化 DDLCREATE TABLE resume ( resume_id VARCHAR(64) PRIMARY KEY, user_id VARCHAR(64) NOT NULL, title VARCHAR(255), raw_text MEDIUMTEXT, parsed_json JSON, embedding_status TINYINT DEFAULT 0, created_at DATETIME DEFAULT CURRENT_TIMESTAMP, updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP ); CREATE TABLE job ( job_id VARCHAR(64) PRIMARY KEY, source VARCHAR(64), title VARCHAR(255), company_name VARCHAR(255), description MEDIUMTEXT, published_at DATETIME, created_at DATETIME DEFAULT CURRENT_TIMESTAMP ); CREATE TABLE thread ( thread_id VARCHAR(64) PRIMARY KEY, user_id VARCHAR(64) NOT NULL, resume_id VARCHAR(64), job_id VARCHAR(64), state VARCHAR(32) DEFAULT idle, created_at DATETIME DEFAULT CURRENT_TIMESTAMP, updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP );字段说明中要特别注意embedding_status。简历内容被修改后不能等下次对话才想起重新向量化。建议在更新简历后触发异步任务把该记录的向量状态置为待更新并在向量库中删除旧向量或生成新版本。否则 Agent 拿到的可能是旧版简历。面试题库也类似。题库内容要带来源、标签、章节、难度、发布日期等字段检索时只召回当前岗位方向的内容。没有元数据的纯文本向量库检索结果很难过滤。5. 从 Demo 到可上线还要补上这些内容如果代码停在“能调用一次工具”的 Demo 阶段离上线还差很多。下面这些点不是锦上添花而是决定用户是否会投诉、客服是否有线索、数据是否需要删除的关键措施。5.1 状态机、会话与任务恢复求职搭子的任务往往不是一次问答能结束的。比如模拟面试流程用户说“开始面试”Agent 出第一题用户回答Agent 判断答案并追问下一题最后输出综合评估。这个过程不能用单一的 Prompt 硬撑否则用户刷新页面或断线后状态会丢。推荐为每种业务定义状态机业务状态含义可执行动作idle无任务等待用户输入collecting正在收集简历或岗位信息追问缺失字段matching正在执行匹配调用匹配工具interviewing正在模拟面试出题、判题、推进题号scheduling正在创建日程确认时间、调用日历工具done任务完成展示总结等待新任务状态字段应该存到业务表里而不是只存在前端内存。用户再次打开页面时后端通过 thread_id 恢复状态Agent 才能继续上一题或重新生成建议。5.2 安全与个人隐私简历中包含姓名、电话、教育经历、项目经历等个人敏感信息。进入生产前需要处理几个问题传输加密应用层使用 HTTPS数据库连接使用 TLS。脱敏调用大模型前不要把完整手机号、身份证号等无关敏感信息发送给模型服务。最小授权简历更新接口、删除接口必须做身份鉴权不能只靠一个 resume_id 就能读取。用户删除权用户注销时需要级联删除简历文本、向量数据、会话记录和匹配报告。不要把“数据只有内部系统能访问”当作默认前提。日志、向量库、模型调用平台都可能成为数据落点需要单独确认。5.3 成本、延迟与模型分级Agent 每次任务可能多次调用模型。如果所有请求都使用同一个高能力模型费用会很快增长并且延迟可能超过用户耐心。可按任务难度分流任务类型推荐模型策略原因意图识别小模型或本地分类模型快速、便宜无法确定时再请求大模型工具参数抽取支持 Function Calling 的模型需要结构稳定输出最终回答生成高能力模型需要组织建议和上下文简历向量化Embedding 模型离线任务对实时延迟不敏感此外要设置单次 Agent 任务的最大循环次数和超时时间。生产环境建议用异步任务执行完整流程用户端先展示“正在处理”完成后通过轮询或消息推送返回结果。5.4 可观测和评测Agent 应用不能只看“大模型有没有回复”还要看每一步是否合理。推荐在每次任务执行时记录用户原始输入模型每次返回的 tool_calls 参数工具执行耗时和返回结果最终回答长度和生成 token 数当前 thread 状态变化这些日志能为排查“为什么这次回复错误”提供关键线索。评测方面需要准备至少几十条包含简历编号和岗位编号的真实测试样本每一条都要有预期结论。不能只凭“看起来自然”验收要检查是否调用了正确工具、评分是否准确、缺失技能是否合理。6. 高频问题排查与可复用清单6.1 常见现象、原因和处置开发过程中会反复遇到几类问题。把它们做成表格可以快速定位。问题现象常见原因检查方式处理建议模型不调用工具直接生成结论模型不支持 Function Calling或工具描述不清晰查看模型返回是否包含 tool_calls、看模型列表换支持工具调用的模型或强化 system 指令工具调用后报参数缺失用户没有提供 resume_id/job_id打印 arguments 参数增加参数抽取校验缺失时先反问用户工具结果返回后仍生成错误结论工具结果没有回填到上下文或 tool_call_id 不匹配查看消息列表中的 roletool 记录让工具结果尽量结构化保留原始命中字段检索结果不相关切分过长、没有元数据过滤、向量模型不适合检查召回 top-k 的内容细化切分、增加重排、缩小业务过滤范围用户第二次提问时找不到上次任务只存聊天记录没存业务状态查看 thread 表 state在 thread 表持久化 resume_id、job_id、state排查顺序建议先确认用户输入参数再确认文件路径或业务 ID 是否正确再确认配置和依赖版本然后看日志中的 tool_calls 与 tool 返回最后才考虑模型能力问题。不要一上来就改 Prompt。6.2 发布前检查清单下面这份清单可以直接用于项目自查模型支持工具调用并且已在指定 endpoint 上验证过。每个工具都有明确的触发条件和参数说明参数缺失时有追问逻辑。所有工具执行结果都带业务 ID能被回填到上下文单次任务有最大步数限制。数据落库包含 timeline 或创建时间能在出错时回放。用户简历、岗位文本等数据在发送给模型服务前经过最小化脱敏处理。向量索引和关系数据能同步更新简历变更会触发重新向量化。会话和任务状态写在服务端不依赖前端保持。日志覆盖用户原始输入、tool_calls、工具结果、最终输出不记录明文密钥。针对至少 50 条业务样本做了回归测试并记录评分和缺失技能是否准确。定义人工兜底入口当 Agent 多次循环未完成或用户主动触发“转人工”时能接管会话。这类求职搭子项目的长期价值不一定来自“第一次回答多机智”更可能来自“用户上传简历三个月后它还记得当初推荐的岗位方向并能根据面试结果动态调整下一轮计划”。建议新团队先用最小匹配流程跑通工具调用再加入 RAG 和状态机最后补安全、评测和人工兜底。每一步都把日志和验证做好Agent 能力才能从演示走向稳定服务。
