BLOG.zh-CN

发布时间:2026/10/24 7:36:58
BLOG.zh-CN

用 Agent 工具做一个 AI 求职工作台:作品「求职搭子」

现在的时代大家都可以用agent做出一个项目,也就是说你不必会很多东西,创新是你真正的价值,敢于用agent实现自己的创新想法才是难得可贵的。

先交代一下开发方式:这个项目整体上主要借助 Agent 工具完成开发。这篇文章会结合现有代码和验证记录,介绍它能做什么,并梳理其中值得继续学习的技术问题。

项目涉及前端、后端、模型调用、工作流编排和数据存储。把这些部分放到一个具体应用里,可以顺着用户的一次操作,看到它们如何协作,也能看到生成结果出错时,需要在哪里增加检查。

项目仓库:

  • GitHub 主仓库:W-sp-git / job-companion
  • Gitee 备用仓库:chen-xunxun / job-companion

1. 先确定一个具体场景:准备一次求职

实习和校招准备会反复用到同一批信息:岗位 JD、自己的项目与实习经历、不同版本的简历、面试记录,以及接下来要补的能力点。

这些信息之间其实有连续的关系。看到一个岗位,需要判断哪些经历能支撑它的要求;改完简历,需要围绕这份材料准备面试;面试暴露的问题,又应该变成下一次练习的内容。

「求职搭子」围绕这条流程组织功能:

保存岗位 → 整理真实经历 → 准备岗位简历 → 模拟面试 → 复盘与专项训练。

目前它是一个在本机启动、通过桌面浏览器使用的工作台。首页把岗位下一步、申请截止日期和待练习任务汇总起来,让准备工作有一个可以继续推进的入口。

本文界面截图中的公司、经历、性能数字及人数均为演示数据,不代表项目的真实使用规模或效果。

举一个假设的使用过程:准备投递 Java 后端实习时,先保存岗位要求,再整理一段自己确实做过的项目经历。系统逐项对照 JD 与材料,展示依据、判断原因和准备行动。用户可以基于这些材料生成简历版本,再关联这个岗位开始模拟面试,最后从复盘中选择一个问题进行专项练习。

岗位是否已经投递、有没有收到回复,由用户维护。系统保留相应记录;材料中没有提及的能力,会作为待确认事项继续了解。

2. 从一次操作,看清系统的分工

前端使用 React、TypeScript 和 Zustand,后端使用 FastAPI。面试和简历优化由 LangGraph 编排,业务数据保存在 SQLite,长期记忆使用 Mem0 与本地 Qdrant。

可以把主要关系简化成下面这张图:

这里需要分清三类数据。

业务记录保存岗位、经历、简历版本、面试报告和训练回答,让用户能回看和编辑。会话检查点保存某场面试的图状态,支持后续请求继续处理。长期记忆提取背景、目标和历史弱点,供其他会话检索。

例如,“这场面试现在问到哪一题”和“这个用户之前在哪类问题上需要加强”,分别对应会话状态与跨会话记忆。LangGraph 官方文档也区分了线程内的检查点与跨线程的数据存储;本项目使用 Mem0 承担长期记忆部分。LangGraph 持久化文档

用户直接修改岗位和经历时,走普通的数据接口;需要生成问题、分析材料或起草内容时,再调用模型。这样的分工让模型输出有明确的使用位置。

3. 面试为什么要用状态机编排

一次模拟面试有几个明确的步骤:开场、接收回答、评价回答、决定是否追问、进入下一题,最后生成报告。

当前实现把这些步骤拆成 LangGraph 节点。每收到一次用户请求,使用同一个thread_id读取对应状态,执行本轮需要的节点,然后结束这一轮图运行。

流程可以简写为:

开始本轮 ├─ 新会话 → 开场 ├─ 用户主动结束 → 报告 ├─ 已结束 → 结束本轮 └─ 提交回答 → 评分 ├─ 已到题数上限 → 报告 ├─ 需要追问且未到深度上限 → 追问 └─ 进入下一题

模型负责判断回答内容、生成问题和评价;题数上限、追问深度等流程约束由代码执行。下面是项目中评分之后的路由函数:

defafter_evaluate(state:InterviewState)->str:ifint(state.get("round",1))>=int(state.get("max_rounds",8)):return"report"ev=state.get("last_evaluation")or{}ifev.get("needs_follow_up")andint(state.get("follow_up_depth",0))<2:return"probe"return"next_question"

这里先检查题数,再检查追问条件。这样的顺序本身就是可以验证的业务规则。测试时只需要构造状态字典,就能检查边界条件,不需要调用真实模型。

对学习者来说,这提供了一个很具体的阅读入口:先弄清状态里有哪些字段,再看节点更新了什么,最后沿着条件边检查流程会走向哪里。

对应实现:面试状态图;对应测试:面试路由测试。

4. 简历优化:生成之后,如何核对事实

简历优化需要改善表达,也需要守住材料本身的事实。岗位要求里出现了某个技能,并不能直接成为简历里新增这个技能的依据。

当前实现从输入材料、生成流程和文件导出三个位置增加检查。

先区分草稿与用户确认的经历

经历素材有草稿和用户确认两种状态。草稿用于继续整理,只有用户确认的内容才会加入后续生成所用的候选材料快照。

这个状态表示用户核对过自己的陈述,真实性仍然由用户负责。简历版本还会保存生成时使用的事实快照,方便以后理解这一版内容基于哪些材料。

起草、独立评审和有限修订

简历流程按下面的顺序执行:

起草 → 评审 → 按条件修订 → 再次评审 → 定稿

起草和修订使用同一任务配置,评审使用另一任务配置。默认配置分别选择通义和智谱模型,评审接收原材料、当前草稿和检查结果,在单独构造的上下文中审查。

换一个模型可以增加检查视角,但评审结论仍需要核对。流程也设有迭代上限:到达上限就结束生成,能否导出还要继续检查。对应实现:简历优化状态图。

用规则检查具体的事实改写

代码会检查草稿中的技能、单位和项目是否有材料依据,并对量化数字进行检查。

仓库单测里有一组很直观的合成样例:原材料写的是“接口平均响应时间从 800ms 降至 200ms”,草稿改成了“响应时间优化至 10ms,吞吐提升 300%”。检查应当指出10ms和300%缺少依据。

数字检查还需要保留所属经历、单位和指标语境。同一个数值出现在联系方式或另一个项目里,不能直接成为这段经历的证据。

这些规则有局限:名称匹配可能受到同义表达影响,数字规则也不能完整理解一句话的含义。因此,检查结果需要允许用户核对,并与模型评审一起使用。

对应实现:事实与数字校验;对应样例:事实校验测试。

评审模型自己也可能误判

仓库的联调记录中出现过这样的问题:评审把原材料中教育与工作时间的重叠,或者缺少 JD 技能,判成了严重事实问题。

但时间重叠需要了解具体情况,缺少岗位要求的技能也不能直接说明简历内容虚构。于是当前实现要求高风险评审意见同时提供原材料引文和草稿引文,用于说明草稿相对来源改变了什么。

如果意见缺少可核对的差异依据,就降为待确认建议;技能、单位、项目和数字规则仍独立运行。记录中的原始日期最终被保留,时间线问题留给人工核对。2026 年 10 月 20 日联调记录

导出 DOCX 前,后端会重新运行检查。发现未解决的规则冲突,或尚未确认的高风险评审意见时,接口返回409和问题清单。用户可以先处理材料,再生成文件。

这里能学到的是:一个检查结果也需要说明依据,而最终操作需要有明确的放行条件。

5. 流式输出,还需要考虑断线之后的状态

面试时,用户需要看到逐步出现的问题,也需要知道系统正在评分、检索还是生成报告。

后端从 LangGraph 的messages、custom、updates三类流中取得模型文本、业务进度和节点更新,再转换成前端使用的 SSE 事件。LangGraph 的 stream-mode 文档说明了这些输出类型;本文描述的是仓库当前使用的接口方式。LangGraph 流式文档

事件页面上的用途
token追加面试问题文本
stage显示检索、评分等进度
score展示本题评分
report展示复盘报告
error/done处理错误或结束本轮接收

评分与报告的原始结构化输出不进入聊天文本流,只有指定的面试对话节点能向聊天区域发送 token。

前端使用fetch发送带请求体的 POST 请求,并读取响应流。原生EventSource的构造选项只提供 URL 和凭据开关,无法直接配置这个 POST 请求体,因此项目自行实现了所需的分帧解析。EventSource 构造接口文档

解析时还要考虑网络数据块和事件帧之间的边界:一次读取可能只有半帧,也可能有多帧,所以前端先累积缓冲区,再提取完整事件。

断线处理涉及另一个选择。当前后端把图运行放进独立的asyncio任务,由队列把事件转发给 SSE 连接。用户刷新页面或停止接收时,只要后端进程和模型调用正常,本轮任务仍会继续执行;之后页面可以读取已保存的状态与记录。

因此,这里的“停止生成”实际停止的是前端接收,模型调用可能继续产生费用。现有实现也没有覆盖后端进程崩溃后的任务自动重投。明确这些语义,才能正确理解界面操作和恢复能力。

对应实现:面试流式接口、前端 SSE 解析器。

6. 还有三个值得顺着代码学习的细节

模型按任务配置

常规模型调用通过get_llm(task)集中取得模型,业务节点声明当前任务;长期记忆则从同一配置层读取设置,在 Mem0 内单独接入模型。

任务主要职责
interview面试问题与追问
judge回答评分、结构化抽取与分析
draft/review简历起草修订与独立评审
ocr简历图像转写
mem0/cluster记忆提取与跨场弱点归并

不同任务的服务商和模型通过配置选择。这能把模型接入与业务节点分开,也方便以后针对质量、延迟和调用量进行对照测试。当前默认配置的存在,并不代表已经完成了不同模型组合的系统性效果比较。

对应实现:模型工厂、任务配置。

结构化输出要经过统一校验

评分、简历和岗位分析都有明确字段,后续代码需要能读取它们。项目把这类调用集中到call_structured,优先使用模型封装提供的结构化输出;不适用时尝试 JSON 模式,再做文本提取、必要的 JSON 修复与 Pydantic 校验,并在校验失败时有限重试。

网络、鉴权等模型服务错误会直接报错,不通过反复修复 JSON 来掩盖。Pydantic 的模型校验负责检查数据结构和字段约束,内容事实还需要前面的来源检查。Pydantic 模型文档

对应实现:结构化输出封装。

专项训练先固定评分标准

专项训练生成题目时,会一并保存 3–5 项评分标准,每项规定 0、1、2 分的行为表现。作答后,模型逐项判断并引用本次回答,代码检查引用是否能在归一化后的回答中找到,再汇总总分:

训练总分 = 各项得分之和 ÷(标准数量 × 2)× 10

找不到回答依据的得分条目会被置零。这样,两次练习可以在同一组标准下比较具体条目的变化。

引文存在仍不能保证评分判断正确,但它为复核提供了入口。这个分数反映本次标准的达成情况,使用时还需要结合题目和具体反馈。

对应实现:岗位分析与专项训练。

7. 目前验证到了什么程度

截至本文整理时,后端有 60 项无需 API Key 的自动化测试,覆盖面试路由、事实校验、结构化输出和工作台规则等。2026 年 10 月 21 日复跑,60 项全部通过。

真实模型调用与这些逻辑测试分别验证。仓库保存的 2026 年 10 月 20 日联调记录包括:PDF 与图片解析、简历起草和评审、DOCX 下载、岗位分析、模拟面试及恢复、专项训练、长期记忆和公司情报等流程。

其中一个具体结果是:测试 JD 被拆成 9 项要求,8 项找到了可核对材料;材料没有提及的 Kubernetes 保持待确认。专项训练的得分引文也逐项检查了是否能对回回答。详细联调记录

这些结果说明相应测试与样例已经跑过。换一份简历、换一个岗位或模型后,仍需要继续观察。目前也没有足够的真实用户数据,支持“提高面试通过率”或“显著缩短求职时间”这样的效果结论。

使用边界同样需要交代清楚:当前面向本机单人使用,默认监听127.0.0.1,还没有账号和权限隔离。业务记录与文件保存在本机,但解析、分析、生成、评分和记忆处理会使用配置的外部模型或相关服务;扫描件 OCR 会发送页面图像。部分生成链路会移除联系方式,不能把整个应用理解为完全离线或全面匿名。

语音训练、在线代码执行和 Offer 选择对比还没有加入。当前版本优先把桌面端的岗位、材料、面试与训练流程连接起来。

8. 作为第一个作品,我想继续学什么

Agent 工具帮助我完成了这个作品,也留下了许多可以继续学习的问题。对我来说,接下来很有价值的一件事,是沿着实际请求把实现读懂,并通过测试和修改确认自己的理解。

例如:

  • 从面试路由入手,解释每个状态字段、每条条件边,以及边界条件应该如何验证。
  • 从事实守卫入手,尝试同义技能名、不同单位和跨项目数字,观察规则什么时候误判或漏检。
  • 从 SSE 接口入手,实际检查刷新、断线、停止接收和后端异常时,页面与数据库分别发生了什么。
  • 给模型输出准备更多样的固定样例,逐步记录可用性、错误类型、等待时间和实际调用成本。

这也会影响后续如何迭代项目:一次生成看起来合理,可以成为继续验证的起点;要判断功能是否可靠,还需要看来源、边界情况和用户能否完成任务。

项目已经有一份目标用户试用指南,可以据此观察用户整理经历、修改简历和完成练习时遇到的具体问题。这些观察会为之后的改进提供依据。

9. 如何运行,以及从哪里开始读代码

运行需要 Python 3.11 及以上、Node.js 20 及以上、uv,以及 README 中列出的模型和搜索服务配置。

默认从 GitHub 主仓库克隆项目:

gitclone https://github.com/W-sp-git/job-companion.gitcdjob-companion

如果访问 GitHub 不便,可以改用 Gitee 备用仓库;两种克隆方式选择一种即可:

gitclone https://gitee.com/chen-xunxun/job-companion.gitcdjob-companion

启动后端前,复制示例配置,并在本机填写密钥:

cdbackend uvsynccp.env.example .env# 编辑 .env,填写模型和搜索服务配置uv run python-mscripts.clipinguv run uvicorn app.main:app--host127.0.0.1--port8010

再打开一个终端,进入仓库中的frontend目录:

npminstallnpmrun dev

随后通过终端提示的本机地址访问页面,默认是 localhost:5173。模型调用可能产生费用,具体取决于所选服务和实际调用量。

学习代码时,可以先读interview_graph.py与路由测试,再看factuality.py如何处理材料依据,最后对照后端api/interview.py和前端api/sse.ts,跟踪一次完整的流式请求。

完整配置、运行步骤和更多功能说明以 GitHub 主仓库 README 为主要入口,也可以通过 Gitee 备用仓库 获取项目。欢迎试用,也欢迎优先通过 GitHub Issue 指出无法完成的操作、缺少依据的生成内容,或值得继续讨论的实现方式。