面壁 OfferPilot:一个求职者为自己做的 AI 面试工具
从需求到实现,从自用到开源
一、故事从一次求职开始
1.1 求职者的困境
两周前,我开始准备新一轮的求职。
作为 Java 后端开发者,每次改简历的时候都会想:我这简历到底行不行?跟这个岗位匹配吗?
试着把简历发给朋友看,朋友说"还行",但说不出具体哪里行、哪里不行。刷八股文又觉得没有针对性——你根本不知道面试官会从哪个角度问。想找人 Mock 面试,时间凑不上,最后不了了之。
我相信这不是我一个人的问题。每个求职者都经历过这种状态:你花了很多时间准备,但不确定这些准备是不是对的。
1.2 灵光一现
2024 年,大语言模型的能力已经足够成熟了。我试着把简历和 JD 扔给 ChatGPT,让它帮我分析匹配度。结果出乎意料的好——它指出了几个我完全没注意到的短板。
但 ChatGPT 是通用对话工具,不是专门为面试准备的。每次都要复制粘贴简历、调整 Prompt,用起来很别扭。
为什么不做个专门的产品呢?
上传简历 → 自动解析 → 选目标岗位 → 生成匹配报告 → 模拟面试。把整个流程串起来,做成一个完整的工具。给自己用。
1.3 面壁:名字的由来
项目起名的时候,想了很久。
面壁,取自"面壁思过"。
面壁是达摩祖师的典故——面壁九年,潜心修炼。放在求职的场景里,就是面对 AI,反思自己的不足,查漏补缺。汉语里"面壁"本身就带有一丝苦修的意味,恰好契合准备面试的过程:枯燥、反复、但值得。
英文名OfferPilot则直白一些——Pilot 是领航员,目标是帮你拿到 Offer。
1.4 从自用到开源
项目最开始就是给自己写的。
没有规划开源,没有想过去 GitHub 上拿 Star。就是单纯地觉得:我需要一个工具,市面上没有现成的,那我写一个。
写着写着发现,这个工具确实能帮到我。那它应该也能帮到其他人。
于是决定认真做:前后端分离、部署上线、修 bug、优化体验。等真正稳定了,才决定开源。
开源是结果,不是目的。
二、这个项目能做什么
2.1 简历解析:从 PDF 到结构化数据
用户上传 PDF 简历,系统自动解析出技术栈、项目经历、工作经历、教育背景,以结构化卡片展示。
产品层面:解析结果一眼看清自己的简历信息。支持多简历管理,可以随时切换默认简历,方便管理不同版本的简历。
技术层面:解析采用三阶段管线——PDFBox 提取文本 → Tess4j OCR 降级 → DeepSeek AI 结构化。PDFBox 处理绝大多数 PDF,当文本提取过短(疑似扫描件)时自动降级到 OCR,AI 负责从非结构化文本中提取结构化字段。调用时使用不含 RAG 的 ChatClient,避免知识库内容污染简历解析结果。
2.2 评估报告:AI 分析匹配度
选定简历和目标职位后,AI 综合简历和 JD 生成匹配度评估报告。
产品层面:报告包含综合匹配分数、技术栈对比分析(哪些匹配、哪些缺失、建议补什么)、亮点提炼和短板提醒。报告异步生成,生成完成后可在报告列表查看。
技术层面:报告使用专用的 Prompt 模板,综合简历文本和 JD 描述,调用 DeepSeek 输出结构化 JSON。生成过程在独立线程池中异步执行,避免阻塞主请求。事务机制确保报告状态与内容一致写入。
2.3 模拟面试:三面制 AI 陪练
三面制,每轮 10 题:技术一面考察基础能力,技术二面深入底层原理,技术三面考察架构设计。
产品层面:AI 逐题实时反馈 + 评分,SSE 流式输出,像打字一样逐字显示回答。过程中断后 1 小时内可以继续,支持跳过和提前结束,结束后生成三轮评分汇总。
技术层面:面试会话使用状态机管理(IDLE → IN_PROGRESS → COMPLETED / EXPIRED),每次答题前后端通过 SSE 长连接通信,后端使用虚拟线程处理流式 AI 调用,避免阻塞 Tomcat 线程池。对话历史持久化到 PostgreSQL,支持中断恢复。
2.4 知识库:把笔记带进面试
上传技术文档,面试时 AI 自动检索相关内容增强回答。
产品层面:支持 Markdown 和 TXT 格式,上传后 AI 自动完成分片、向量化、建索引,无需手动干预。面试答题时,系统自动匹配知识库中与当前问题最相关的内容注入上下文,让回答更有针对性。
技术层面:上传后经 TokenTextSplitter 分片(500 字符/片,50 字符重叠),BGE 本地 ONNX 模型嵌入为 512 维向量,存入 pgvector HNSW 索引。检索时先向量检索 top-K,再经 BGE 交叉编码器重排序确保精度。检索严格按 user_id 过滤,数据隔离。
2.5 API Key 配置:自备 Key 不限量
用户可以在平台配置自己的 DeepSeek API Key,配置后不再受平台免费额度限制。
产品层面:配置页面提供 API Key 输入框,配置后实时生效,支持随时更换或删除。用量统计页面展示今日 Token 消耗、月度汇总和趋势图,用量透明可见。
技术层面:API Key 使用 AES-256-GCM 加密后存入数据库,每次加密生成随机 IV,相同明文每次密文不同。解密仅在内存中临时进行,用完即弃,不写日志。调用时通过 ApiKeyRoutingAdvisor 在 Advisor 链末端拦截请求,判断是否使用用户自备 Key 直连 DeepSeek API。
2.6 如果你是开发者
这个项目本身也是一个学习 AI Agent 开发的完整案例:Advisor Chain 管道设计、RAG 全链路、SSE 流式架构、对话记忆管理、API Key 多租户路由。每一个模块都可以独立拆出来学习和复用。
三、技术选型:每一层都是权衡
3.1 技术栈全景
| 层次 | 选型 | 为何选它 |
|---|---|---|
| 后端框架 | Spring Boot 4.1.1 | 最新稳定版,虚拟线程原生支持 |
| AI 框架 | Spring AI 2.0.1 | Spring 生态原生集成,Advisor 机制灵活 |
| 运行环境 | JDK 21 | 虚拟线程正式 GA,适合 SSE 长连接场景 |
| 数据库 | PostgreSQL 16 + pgvector | 结构化数据 + 向量检索一体化 |
| LLM | DeepSeek | 性价比高,OpenAI 兼容,接入成本低 |
| 文件存储 | MinIO | S3 兼容,Docker 一键部署,社区版免费 |
| 认证 | Sa-Token + JWT | 双 Token 机制开箱即用 |
| 前端 | Vue 3 + TypeScript + Vite | 主流技术栈,Element Plus 生态成熟 |
| 向量嵌入 | BGE bge-small-zh-v1.5 (ONNX) | 本地运行,零 API 费用,数据不出服务器 |
| 向量重排 | BGE bge-reranker-base (ONNX) | 交叉编码器,先检索后重排,兼顾效率与精度 |
3.2 几个关键决策背后的思考
简历解析:为什么不用商业 API?
市面上有成熟的文档解析 API——上传 PDF,返回结构化 JSON,非常方便。但有两个问题:一是按调用次数收费,量大了成本不低;二是依赖第三方服务,一旦对方接口变更或下线,整个功能就废了。
我的方案是三阶段管线:PDFBox 提取文本 → Tess4j OCR 降级 → DeepSeek AI 结构化。PDFBox 处理绝大多数 PDF,当文本提取过短(疑似扫描件)时自动降级到 OCR,AI 负责从非结构化文本中提取技术栈、项目经历等结构化字段。成本可控,不依赖单一服务。
解析管线的大致实现:
// 第一阶段:PDFBox 提取文本Stringtext=extractTextWithPdfBox(inputStream);// 第二阶段:文本过短则 OCR 降级if(text.length()<MIN_TEXT_LENGTH){text=extractTextWithOcr(inputStream);}// 第三阶段:AI 结构化解析Stringprompt=buildResumeParsePrompt(text);ChatResponseresponse=chatClientNoRag.prompt(prompt).call().chatResponse();ResumeParseResultresult=objectMapper.readValue(response.getResult().getOutput().getText(),ResumeParseResult.class);注意这里用的是chatClientNoRag(不含 RAG 的 ChatClient),避免知识库内容污染简历解析结果。
SSE 还是 WebSocket?
模拟面试的场景是:用户答题 → AI 流式生成 → 前端实时展示。这是一个单向数据流(服务器推给客户端),不需要客户端持续推送。
WebSocket 是全双工协议,引入它意味着需要处理心跳、重连、连接池管理——针对这个场景,这些都是不必要的复杂度。SSE 天然适合服务器推送,基于 HTTP 协议,浏览器原生支持,配合 Nginx 只需要一行proxy_buffering off。
为什么选 Spring AI 而不是 LangChain4j?
两个都是 Java 生态的 AI 框架,选 Spring AI 的原因很直接:项目本身基于 Spring Boot,Spring AI 的 Advisor 机制与业务需求高度吻合。
我要的不是一个简单的"调 API"的封装,而是一个可编排的调用管道——安全校验、额度控制、Prompt 优化、RAG 检索、日志记录、Key 路由,每个环节独立,可以插拔、可以排序。Spring AI 的 Advisor 接口就是这个模式,而 Spring 生态对"拦截器链"这种模式有天然的优势。
为什么选 DeepSeek?
选 DeepSeek 的原因很务实:性价比。
国外大模型的 API 价格对个人项目来说仍然偏高——每月跑几百次测试、几十轮面试对话,Token 消耗量不小。DeepSeek 的定价大约是 GPT-4 的十分之一,而且提供 OpenAI 兼容接口,接入成本极低。如果未来需要换模型,只需改一行 API 地址和 Key。
另外,这个项目的核心是 AI Agent 的调用链路设计,而不是某个特定模型的能力。模型只是一个"引擎",随时可以换。选择 DeepSeek 意味着可以用更低的成本跑通全链路,把预算花在更有价值的地方——比如多测几轮 Advisor 链的编排。
为什么不是微服务?
这是一个一定会被问到的问题。Nacos、Redis、MySQL 读写分离、API 网关、分布式事务——这些技术栈在今天的后端项目中非常成熟,对于需要高可用、高并发的业务场景来说不可或缺。但面壁从一开始就没有走这条路,原因很简单:这不是本次开源的重点。
这个项目的核心价值在于 AI Agent 的调用链路设计——Advisor Chain 的编排、RAG 的全链路实现、SSE 流式架构、对话记忆管理。这些是真正值得关注和学习的部分。引入微服务架构,意味着要处理服务发现、配置中心、分布式会话、服务间调用等额外复杂度,而这些与 AI Agent 的核心逻辑没有直接关系。
少依赖基础设施也是在控制成本。当前架构下,Spring Boot 单体 + PostgreSQL + MinIO,三台 Docker 容器就能跑完整套系统。一台 2C4G 服务器月租不到一百块,个人开发者完全负担得起。
当然,如果未来有更高的性能和稳定性要求,引入更完备的基础设施是完全合理的——这个项目本身的设计足够模块化,Advisor 链、RAG 服务、面试状态机等模块按领域划分,边界清晰,迁移到微服务架构并不困难。如果有人想在此基础上做性能优化或二次开发,欢迎 fork 改造。如果有人只是想借鉴产品需求或产品设计思路,也完全没问题。
技术选型没有绝对的对错,只有合不合适的场景。这个阶段,我把精力放在 AI Agent 的核心技术上,希望让更多人能轻松部署和上手学习,而不是被基础设施的门槛挡在门外。
四、核心实现:把设计落地
4.1 Advisor Chain:AI 调用的 7 层管道
这是整个 AI 调用链的核心抽象。借鉴了 AOP 拦截器链的思想,将一次 AI 调用拆解为 7 个独立环节,按顺序执行:
SafeValidAdvisor(0) → 安全校验,过滤非法输入 TokenUsageAdvisor(0) → 前置校验额度 + 后置累计用量 ReReadingAdvisor(1) → Prompt 重读优化(RE² 技术) MessageChatMemoryAdvisor(2) → 对话记忆自动注入 RetrievalAugmentationAdvisor(3) → 自动 RAG 检索 + 增强 MyLogAdvisor(4) → 请求耗时日志 ApiKeyRoutingAdvisor(5) → 用户 Key / 平台 Key 路由每个 Advisor 只做一件事。比如TokenUsageAdvisor不关心 Prompt 内容,只负责校验额度;ApiKeyRoutingAdvisor不关心回答质量,只负责判断走哪个 Key。
在代码中,所有的 Advisor 在AiCoreConfig中组装:
@BeanpublicChatClientchatClient(ChatClient.Builderbuilder,Optional<RetrievalAugmentationAdvisor>ragAdvisor,MessageChatMemoryAdvisormemoryAdvisor,UserTokenUsageServicetokenUsageService,ObjectMapperobjectMapper){List<Advisor>advisors=newArrayList<>();advisors.add(newSafeValidAdvisor());advisors.add(newTokenUsageAdvisor(tokenUsageService));advisors.add(newReReadingAdvisor());advisors.add(memoryAdvisor);ragAdvisor.ifPresent(advisors::add);// RAG 可选,无 VectorStore 时不注入advisors.add(newMyLogAdvisor());advisors.add(newApiKeyRoutingAdvisor(objectMapper));returnbuilder.defaultAdvisors(advisors.toArray(newAdvisor[0])).build();}注意ragAdvisor是Optional的,意味着如果 pgvector 不可用,RAG 环节自动跳过,整个链路仍然正常工作。这种设计保证了系统在基础设施不完整时也能降级运行。
ApiKeyRoutingAdvisor是链路中比较特殊的一个——它会在调用链的末端拦截请求,判断用户是否配置了自己的 API Key:
@OverridepublicChatClientResponseadviseCall(ChatClientRequestrequest,CallAdvisorChainchain){StringapiKey=extractApiKey(request);if(apiKey!=null){// 用户自备 Key:跳过 ChatModel,直连 DeepSeek APIreturncallDeepSeekDirectly(request,apiKey);}// 使用平台默认 Key:透传给 ChatModelreturnchain.nextCall(request);}这种设计让免费用户和使用自备 Key 的高级用户走不同的路径,但上层的调用方不需要关心这个差异——Advisor 链对调用方是完全透明的。
4.2 双 ChatClient:为什么需要两个
ChatClient是 Spring AI 的核心入口,所有的 AI 调用都通过它。但不同的场景需要不同的 Advisor 组合。
面试场景需要完整的 Advisor 链,包括 RAG(知识库检索增强)。但简历解析如果也走 RAG,知识库的内容会污染结构化提取的结果——比如用户上传了一份 Java 技术文档,结果简历解析时 AI 把文档内容也当作参考信息,提取出来的技术栈就偏了。
解决方案是创建两个 ChatClient Bean:
@BeanChatClientchatClient(ChatClient.Builderbuilder,MessageChatMemoryAdvisormemoryAdvisor,Optional<RetrievalAugmentationAdvisor>ragAdvisor,UserTokenUsageServicetokenUsageService,ObjectMapperobjectMapper){List<Advisor>advisors=newArrayList<>();advisors.add(newSafeValidAdvisor());advisors.add(newTokenUsageAdvisor(tokenUsageService));advisors.add(newReReadingAdvisor());advisors.add(memoryAdvisor);ragAdvisor.ifPresent(advisors::add);// 含 RAG,无 VectorStore 时自动跳过advisors.add(newMyLogAdvisor());advisors.add(newApiKeyRoutingAdvisor(objectMapper));returnbuilder.defaultAdvisors(advisors.toArray(newAdvisor[0])).build();}@BeanChatClientchatClientNoRag(ChatClient.Builderbuilder,UserTokenUsageServicetokenUsageService,ObjectMapperobjectMapper){returnbuilder.defaultAdvisors(newSafeValidAdvisor(),newTokenUsageAdvisor(tokenUsageService),newReReadingAdvisor(),newMyLogAdvisor(),newApiKeyRoutingAdvisor(objectMapper))// 不含 RAG.build();}4.3 虚拟线程 + SSE:面试实时反馈的架构设计
模拟面试的"逐题反馈"场景,本质上是长连接 + 流式推送。每个用户的每次答题,后端都需要:
- 组装 Prompt(当前轮次、历史对话、RAG 检索结果)
- 调用 DeepSeek API(流式返回)
- 逐字推送到前端
如果使用 Tomcat 线程池处理这些请求,一个 SSE 连接就占用一个线程,连接数一多线程池就满了。JDK 21 的虚拟线程正是为了解决这个问题——虚拟线程在 IO 阻塞时会自动挂起并释放载体线程,非常适合 SSE 这种等待 IO 的场景。
@Bean(name = "sseTaskExecutor", destroyMethod = "close") public Executor sseTaskExecutor() { return Executors.newThreadPerTaskExecutor( Thread.ofVirtual().name("sse-").factory()); }每个 SSE 连接持有一个独立的虚拟线程,不会阻塞 Tomcat 线程池。客户端断开时,通过onCancel和onDispose回调及时清理资源,防止连接泄漏。
SSE 端点的实现大致如下:
@PostMapping("/{id}/answer")publicSseEmitteranswer(@PathVariableLongid,@RequestBodyAnswerRequestrequest){// 校验会话状态InterviewSessionsession=interviewService.validateSession(id);// 创建 SSE 发射器,超时 30 分钟SseEmitteremitter=newSseEmitter(1800000L);sseTaskExecutor.execute(()->{try{// 流式调用 AI,逐字推送到前端chatClient.prompt().user(request.getAnswer()).advisors(a->a.param("user_id",session.getUserId())).stream().chatResponse().doOnNext(response->{Stringcontent=response.getResult().getOutput().getText();emitter.send(SseEmitter.event().data(content));}).blockLast();emitter.complete();}catch(Exceptione){emitter.completeWithError(e);}});// 客户端断开时清理emitter.onCompletion(()->cleanup(session));emitter.onTimeout(()->cleanup(session));returnemitter;}4.4 面试状态机:中断恢复的实现
模拟面试不是一次性对话——用户可能中途离开、网络中断、或者想先退出去准备一下再回来。这就要求面试会话必须能暂停和恢复。面试会话使用状态机管理,核心状态流转:
IDLE → IN_PROGRESS → COMPLETED ↑ ↓ └── EXPIRED(超过 1 小时未操作)每次答题前检查会话是否过期:
if(session.getExpiredAt().isBefore(LocalDateTime.now())){session.setStatus(EXPIRED);sessionRepository.updateById(session);// 持久化过期状态thrownewBusinessException("面试已过期,请重新开始");}1 小时窗口的设计:每轮面试约 20-30 分钟,1 小时足够完成一轮。过长则失去安全性意义——如果用户离开座位太久,会话应该自动失效,而不是一直等着。
4.5 RAG 知识库:从文档到检索的全链路
知识库的功能听起来简单——上传文档,面试时自动检索——但背后的链路并不短。从原始文档到可检索的向量索引,再到面试时的实时检索和重排,中间经过多个环节:
文档上传 → TokenTextSplitter 分片(500 字符/片,50 字符重叠) → BGE 本地 ONNX 模型嵌入(512 维向量) → pgvector HNSW 索引(余弦距离) → 面试时自动检索 → BGE 交叉编码器重排序 → 结果注入 Prompt 上下文为什么用本地嵌入模型?最开始考虑过调用第三方嵌入 API,但每次文档上传都要网络请求,延迟高,还可能产生额外费用。BGE 的 ONNX 量化版只有 23MB,在 2C4G 服务器上推理延迟约 50-100ms,足够了。
重排的作用:向量检索(bi-encoder)速度快但精度有限,top-K 结果中可能混入不相关的内容。交叉编码器(cross-encoder)逐对计算 query 与 doc 的相关性,精度更高。先检索后重排,两阶段兼顾效率与精度。
检索时通过user_id过滤确保数据隔离:
publicList<Document>search(Stringquery,LonguserId,inttopK){if(vectorStore==null){returnList.of();// 无 VectorStore 时降级}FilterExpressionBuilderbuilder=newFilterExpressionBuilder();Filter.Expressionfilter=builder.eq("user_id",userId.toString()).build();SearchRequestsearchRequest=SearchRequest.builder().query(query).topK(topK>0?topK:5).similarityThreshold(0.5).filterExpression(filter).build();returnvectorStore.similaritySearch(searchRequest);}每个用户只能检索到自己知识库中的文档,过滤条件由 Spring AI 的RetrievalAugmentationAdvisor自动注入,无需在每个查询中手动拼接 SQL。
4.6 API Key 加密:AES-256-GCM 的实现
用户配置的 DeepSeek API Key 相当于用户的资产——泄漏了可能被他人盗用产生费用。因此 API Key 不能明文存储,必须在入库前加密,且解密只在内存中临时进行,用完即弃。采用 AES-256-GCM 加密后存入数据库:
publicStringencrypt(StringplainText){byte[]iv=newbyte[12];// GCM 推荐 12 字节 IVsecureRandom.nextBytes(iv);cipher.init(Cipher.ENCRYPT_MODE,secretKey,newGCMParameterSpec(128,iv));byte[]cipherText=cipher.doFinal(plainText.getBytes(StandardCharsets.UTF_8));returnBase64.getEncoder().encodeToString(concat(iv,cipherText));}选择 GCM 模式的原因:提供认证加密(Authenticated Encryption),同时保证机密性和完整性。每次加密生成随机 IV,相同明文每次密文不同。加密密钥通过环境变量注入,不写死在代码中。
这里踩过一个坑:SecureRandom.getInstanceStrong()在低熵 Docker 容器上可能阻塞,导致应用启动挂起。后来改用new SecureRandom()解决。
五、部署与 CI/CD
5.1 生产环境架构
服务器配置:火山引擎 2C4G,宝塔面板管理。
Nginx(反向代理) ├── / → 前端 SPA 静态文件 └── /api/ → 代理到后端 :8080(SSE 需 proxy_buffering off) Spring Boot(Java 21) 端口 8080,prod 环境 Docker 容器 ├── PostgreSQL 16 + pgvector └── MinIO 对象存储5.2 CI/CD 自动化
GitHub Actions 每次 push 自动执行:
push / PR → 启动 PostgreSQL + pgvector 容器 → JDK 21 环境准备 → Maven 编译打包 → 运行全部单元测试 → 构建结果通知CI 配置中有几个细节需要处理:
- 数据库连接:CI 中 PostgreSQL 使用默认端口 5432,通过
SPRING_DATASOURCE_URL环境变量覆盖默认配置。 - ONNX 模型下载:CI 网络环境不一定能访问 HuggingFace,测试类通过
@MockitoBean模拟嵌入模型,避免下载 40MB 的模型文件。 - DeepSeek API Key:测试类中覆盖为 dummy 值,不依赖 GitHub Secrets。
@SpringBootTest(properties={"spring.ai.deepseek.api-key=dummy-test-key","spring.ai.rag.reranker.model-uri=file:/nonexistent/reranker/model.onnx","spring.ai.rag.reranker.tokenizer-uri=file:/nonexistent/reranker/tokenizer.json"})classOfferPilotApplicationTests{@MockitoBeanprivateEmbeddingModelembeddingModel;// 避免下载 ONNX 模型@TestvoidcontextLoads(){// 验证 Spring 上下文能正常加载}}@MockitoBean是 Spring Boot 3.4+ 引入的注解,相比旧版的@MockBean,它更轻量,且不会触发原 Bean 的初始化过程。
六、并发安全与线上踩坑
6.1 Token 用量统计的竞态问题
user_token_usage表按(user_id, usage_date)唯一约束记录每日用量。两个并发请求同时插入同一天的记录,会导致唯一约束冲突。
解决方式:用INSERT ... ON CONFLICT DO UPDATE替代先查后改,一步到位:
INSERTINTOuser_token_usage(user_id,usage_date,prompt_tokens,completion_tokens)VALUES(?,?,?,?)ONCONFLICT(user_id,usage_date)DOUPDATESETprompt_tokens=user_token_usage.prompt_tokens+EXCLUDED.prompt_tokens,completion_tokens=user_token_usage.completion_tokens+EXCLUDED.completion_tokens;6.2 SSE 连接泄漏
流式场景中,客户端可能在不经意间断开连接(浏览器关闭、网络中断)。如果不及时释放资源,每个断开的连接都会留下一个僵尸线程。
在ApiKeyRoutingAdvisor中通过Flux.create()的onCancel/onDispose回调清理 HTTP 连接:
emitter.onCancel(()->{httpRequest.abort();// 客户端断开时关闭 HTTP 连接});emitter.onDispose(()->{inputStream.close();// 订阅取消时清理资源});6.3 SecureRandom 阻塞
第一次部署到服务器时,应用启动后卡住了五分钟,没有任何日志输出。排查后发现是SecureRandom.getInstanceStrong()在低熵 Docker 容器中阻塞,等待熵池填充。
改成new SecureRandom()后问题解决。这个坑在开发环境不会出现(开发机有鼠标、键盘等输入设备提供熵源),但生产服务器的 Docker 容器中一定会遇到。
七、项目价值与产品价值
7.1 产品价值:帮求职者解决实际问题
面壁不是什么颠覆性的产品,它解决的是一个很具体的问题:求职者不知道自己的简历和面试准备到底够不够。
它做的事情很简单:
- 简历不够好 → AI 告诉你哪里不够
- 不知道面什么 → AI 根据你的简历和 JD 出题
- 没人 Mock 面试 → AI 陪练,逐题实时反馈
它不是替代面试官,而是帮你在真正面试前多一次"照镜子"的机会。
7.2 项目价值:一个可学习的 AI Agent 实践案例
从技术角度看,这个项目覆盖了 AI Agent 开发的几个核心领域:
- AI 调用管道设计:Advisor Chain 模式,可插拔、可编排
- RAG 完整实现:从文档分片到向量检索到重排,全链路闭环
- 流式架构:SSE + 虚拟线程,适合 IO 密集型实时推送场景
- 安全设计:AES-256-GCM 加密、数据隔离、双 Token 认证
- 生产部署:从 CI/CD 到上线运维,全流程实践
如果你正在学习 AI Agent 开发,这个项目可以作为一个"能跑起来的参考案例"来读。
7.3 一些遗憾
- 目前只支持 DeepSeek,后续可以接入更多模型
- 面试题目不能自定义,用户无法上传自己的题库
- 不支持批量上传简历
- 没有 HTTPS(后续配域名后再上)
当然,这些遗憾也正是项目继续迭代的方向。如果你有好的想法或建议,欢迎在 GitHub 上提 Issue 或 PR。
但回过头来看,这个项目从最初的一个想法,到能跑起来的工具,再到部署上线、开源分享,已经超出了我最初的预期。它证明了:一个普通开发者,用业余时间,借助现有的 AI 能力和开源生态,完全可以做出一个有价值的产品。
八、关于作者
我是eyki,技术开发者。
这个项目从我个人求职面试的需求出发,经历了需求分析、技术选型、编码实现、上线部署到最终开源的全过程。如果你也在准备面试,或者对这个项目感兴趣,欢迎在 GitHub 上交流:
- GitHub:https://github.com/OneyTo7/offer-pilot
- 在线体验:http://14.103.58.208
如果觉得有用,欢迎 Star 支持,也欢迎提交 Issue 和 PR。
九、截图预览
🏠 首页 / 登录
注册登录页面,简洁大气的 Landing 页
📄 简历管理
上传简历后自动解析,结构化展示技术栈、项目经历
🎯 目标职位
为简历设定目标公司和职位,输入 JD 描述
📊 评估报告
AI 一键生成匹配度评估,技术栈对比一目了然
🎙️ 模拟面试
三面制 AI 面试,SSE 流式实时反馈
🤖 AI 大模型配置
支持自配 API Key,用量透明展示
📚 知识库
上传文档,AI 自动索引,面试时检索增强
开源地址
GitHub:https://github.com/OneyTo7/offer-pilot
如果觉得有用,欢迎 Star ⭐ 支持!
关于作者
- 平台微信加好友进群,欢迎交流技术问题、面试经验、项目合作
许可证
MIT License — 可自由使用、修改、商用。