【自学AI】13 实现一个生产级 AI 简历押题系统的完整复盘

4020 字
20 分钟
【自学AI】13 实现一个生产级 AI 简历押题系统的完整复盘

写在前面#

学 AI 这几个月,一直在看教程、跑 Demo、调 API。

但有一个问题始终没解决:怎样从”能跑的 Demo”到”能上线的产品”?

教程里的例子都很简单——调个 API,拿到结果,打印出来,完事。 但真实场景里,你要考虑:用户输入不规范怎么办?AI 超时了怎么办?扣费扣错了怎么办?

这次做 AI 简历押题系统,从需求分析到架构设计,从 Prompt 工程到代码实现,从成本控制到错误处理—— 完整走了一遍”从零到一”的过程。

这篇不是代码教程,是复盘,是工程思维的提炼

如果你只想学”怎么调 AI API”,这篇可能不适合你。 但如果你想知道”怎样设计一个靠谱的 AI 应用”,这篇会对你有用。


一、七节课的完整地图#

先看全景图,我们经历的完整旅程:

章节核心问题关键产出
1 功能设计我们要做什么?需求分析 + 架构设计 + 数据结构
2 Prompt 设计怎样让 AI 做得更好?三个专用 Prompt:简历分析 / 题目生成 / 匹配度评估
3 基础 Controller怎样搭建框架?SSE 流式推送 + RxJS Subject 事件总线
4 简历解析怎样处理多种输入?三种方式:直接文本 / 上传文件 / 简历库
5 扣费与退费怎样确保财务安全?原子操作 + 自动退款 + 消费记录审计
6 简历解析详解怎样让解析更鲁棒?验证 + 截断 + 边界情况处理
7 AI 集成与生成怎样生成高质量结果?两步法 + 进度模拟 + JSON 格式约束

这不是线性的”写完一个功能再写下一个”,而是增量开发—— 每一步都是完整的、可测试的、有价值的。

《易经·系辞》:“易则易知,简则易从。” 把大系统拆成小模块,每个模块独立可测,整个系统自然清晰。


二、五个关键技术决策#

做这个系统的时候,有五个地方停下来想了很久—— 不是因为技术难,是因为每个选择背后,都有成本、体验、可靠性的权衡。

决策 1:为什么用 SSE 而不是轮询?#

AI 生成至少需要 5 分钟,用户盯着白屏等 5 分钟是什么体验?

一开始想用轮询——前端每秒发一个请求问”好了没?” 但算了一下:5 分钟 = 300 秒 = 300 个请求。

服务器要处理 300 个重复请求,用户要等 2-3 秒才看到更新—— 这不是好的解法。

SSE 的优势:

  • 后端主动推送,前端被动接收
  • 一个连接,持续推送
  • 实时性好(100 毫秒内看到更新),资源消耗少

核心代码很简单:

const subject = new Subject<ProgressEvent>();
subject.subscribe({
next: (event) => {
res.write(`data: ${JSON.stringify(event)}\n\n`); // 推送给前端
},
});

后端每次有进度更新,就往 subject 里扔一个事件,前端自动收到。

这个决策的本质: 对于长时间运行的异步任务,主动推送比被动轮询更高效。


决策 2:为什么分两步而不是一步?#

这是整个系统最重要的架构决策。

一开始想一步到位——让 AI 一次性生成”题目 + 分析”。 但实际测试发现,一步法有几个问题:

问题原因
Prompt 太长要同时说清楚”怎么生成题目”和”怎么分析简历”,Prompt 超过 2000 tokens
输出太长一次性输出 5000+ tokens,生成时间超过 10 分钟
注意力分散AI 同时做两件事,质量都下降
失败后果严重如果第 9 分钟失败了,前面 9 分钟全白费

改成两步之后:

// 第一步:生成题目(7 分钟)
const questionsResult = await aiService.generateResumeQuizQuestionsOnly({...});
// 第二步:生成分析(2 分钟)
const analysisResult = await aiService.generateResumeQuizAnalysisOnly({...});

数据对比:

方式tokens等待时间失败后果
一步法5000-800010 分钟以上全部白费
两步法6000-9000(分两次)约 9 分钟至少有一半结果

两步法看起来多了一次 API 调用,但总时间反而更短—— 因为两个中等请求比一个超长请求更快。

而且用户体验更好:第一步完成后,用户已经看到”题目已生成”,心里有数。

《易经·履卦》:“履道坦坦,幽人贞吉。” 一步一步踩实,比一次性跳过去更稳。


决策 3:为什么简历有三种方式而不是只有一种?#

最简单的做法是:只支持”粘贴文本”,用户自己复制粘贴简历。

但这样用户体验很差——大部分人的简历是 PDF 或 DOCX 文件, 让他们先打开文件、复制内容、再粘贴,多了两步。

所以我们支持了三种方式:

方式适用场景优点缺点
直接粘贴文本用户手头有纯文本简历最简单,无需处理需要手动复制粘贴
上传文件用户有 PDF/DOCX 文件最常见,符合习惯需要下载+解析,复杂度高
从简历库选择用户之前在简历汪保存过最快,直接查库依赖外部系统

这是优雅的降级设计——让用户选择最方便的方式,而不是强制一种。

文件解析的技术栈:

// 依赖三个包
pnpm add axios@1.13.2 pdf-parse@1.1.1 mammoth@1.11.0
// 根据文件类型选择解析方式
switch (fileType) {
case 'PDF':
text = await this.parsePdf(buffer);
break;
case 'DOCX':
text = await this.parseDocx(buffer);
break;
}

这个决策的本质: 支持多种输入方式是产品体验的基础,不要偷懒只做一种。


决策 4:为什么扣费要提前,失败后再退费?#

扣费时机有三种选择:

时机问题
等生成成功了再扣费中间 5 分钟,系统其他部分是否在计数?是否会超配额?状态不一致
用后付费万一余额不足怎么办?生成完了才发现没钱,用户白等了
先扣费,失败则退费(我们的选择)快速检查余额 + 失败自动退还 + 逻辑清晰

完整的扣费生命周期:

原子扣费(resumeRemainingCount - 1)
创建消费记录(status: PENDING)
AI 生成(可能失败)
保存结果
消费记录 → SUCCESS
↕(任何一步失败)
退款(resumeRemainingCount + 1)
消费记录 → FAILED + isRefunded: true

原子操作的关键代码:

// ✅ 正确方式(原子操作)
const user = await this.userModel.findOneAndUpdate(
{
_id: userId,
resumeRemainingCount: { $gt: 0 }, // 条件检查
},
{
$inc: { resumeRemainingCount: -1 }, // 原子递减
},
{ new: false }, // 返回更新前的文档
);
if (!user) {
throw new BadRequestException('次数不足');
}

为什么要原子操作?

假设两个请求同时进来,用户只剩 1 次:

  • 请求 A 读到余额 = 1,扣费,余额变成 0
  • 请求 B 也读到余额 = 1(因为 A 还没写回去),也扣费,余额变成 -1

这就是并发问题

原子操作把”检查余额”和”扣费”合并成一个不可分割的动作, 数据库保证同一时间只有一个请求能成功。

这个决策的本质: 先扣费、后生成、失败退款,是最清晰、最可靠的方式。


决策 5:为什么要进度条?#

AI 生成至少需要 5 分钟,用户盯着白屏等 5 分钟是什么体验?

进度条的核心问题是:AI 生成是异步的,我们不知道它什么时候完成,没办法给出真实的进度百分比。

解决方案:模拟进度

private getStagePrompt(progressSubject: Subject<ProgressEvent>): void {
const progressMessages = [
{ progress: 0.05, message: '🤖 AI 正在深度理解您的简历内容...' },
{ progress: 0.1, message: '📊 AI 正在分析您的技术栈和项目经验...' },
{ progress: 0.15, message: '🔍 AI 正在识别您的核心竞争力...' },
// ... 共 18 条消息,覆盖 0-90%
];
let progress = 0;
const interval = setInterval(() => {
progress += 1;
this.emitProgress(progressSubject, progress, progressMessages[progress].message);
if (progress === progressMessages.length - 1) {
clearInterval(interval);
}
}, Math.floor(Math.random() * (2000 - 800 + 1)) + 800); // 每 0.8-2 秒随机更新
}

关键设计:

  • 随机间隔(0.8-2 秒):比固定 1 秒更自然
  • 消息内容有变化:从”理解简历”到”设计问题”到”质量检查”
  • 进度只到 90%:最后 10% 留给真实完成事件

这样用户看到的是:进度条在动,消息在变,AI 真的在干活—— 虽然我们不知道真实进度,但用户感受到的是”有进展”。

这个决策的本质: 不确定性的进度条,用模拟 + 随机间隔 + 有意义的消息,让用户感到”AI 真的在干活”。


三、贯穿始终的工程思维#

如果你只记住代码,这章就浪费了。 我要让你记住的,是背后的工程思维

思维 1:先设计,后编码#

我们没有上来就写代码,而是先花了大量时间做设计。

为什么?

  • 代码是容易改的,但如果架构错了,代价很高
  • 需求理解不清楚,代码写出来也是错的
  • 数据结构设计不好,后续会很痛苦

正确的顺序:

需求分析 → 架构设计 → 数据库设计 → 代码实现

《易经·系辞》:“形而上者谓之道,形而下者谓之器。” 道是架构,器是代码。道不清楚,器再精致也没用。


思维 2:每一步都能独立测试#

我们没有说”全部写完再测试”,而是:

  • 框架搭好后,能调用接口吗?能收到 SSE 响应吗?
  • 简历能解析吗?能验证格式吗?
  • 扣费能成功吗?退费能成功吗?
  • 复杂的文件解析对吗?边界情况处理了吗?
  • AI 能生成内容吗?JSON 格式正确吗?

这就是增量开发——每一步都是完整的、可测试的、有价值的。

不是等到全部写完才发现”哦,这里有个 Bug”, 而是每写完一个模块,立刻测试,立刻发现问题。


思维 3:用户体验很重要#

为什么要有进度条?为什么要分两步?为什么要支持三种方式?

都是为了用户体验

站在用户的角度想:

  • “我点了生成,然后什么都没发生,5 分钟都没动静,我以为系统死了”(没有进度条)
  • “我只支持上传文件,但我想直接粘贴简历”(方式太少)
  • “生成成功了,但配额没了,也没有提示”(体验差)

好的产品,永远是从用户的角度出发。


思维 4:失败处理和回滚#

我们在扣费那一节讲了很多关于”失败怎么办”的问题。

本质思想是:在分布式系统中,失败是常见的,关键是怎样恢复。

场景处理方式
扣费成功了,但 AI 超时了自动退费
AI 生成完了,但数据库连接失败了自动退费
退费本身失败了(最严重)告警 + 人工介入

这不是说”万一出错,用户就认了”, 而是说”有任何错误,我们都有备选方案”。

嵌套 try-catch 的价值:

try {
// 扣费 + 生成
} catch (error) {
try {
await refundCount(userId, 'resume'); // 退款
} catch (refundError) {
// ⚠️ 退款失败是严重问题,必须告警
this.logger.error(`🚨 退款流程失败!需要人工介入!`);
// TODO: 发送告警通知
}
}

这就是健壮的系统设计


四、从这章能学到什么#

如果我问你”这个项目学到了什么”,可能有几种回答:

回答类型内容
表面的回答”我学会了怎样调用 AI API、怎样生成面试题”
深层的回答”我学到了怎样设计一个完整的 AI 应用,从需求分析到架构设计,从 Prompt 工程到代码实现,从成本控制到错误处理”

这两个回答差别很大。

第一个,你只是会用 API。 第二个,你能独立设计和实现类似的系统

具体来说,你学到了:

能力体现在哪里
产品思维理解用户需求,设计好的功能流程
架构思维把大功能拆成小模块,让每个模块独立运作
Prompt 工程写好的 Prompt,让 AI 输出更高质量的结果
成本意识估算 Token、优化成本
可靠性设计处理失败、自动恢复、确保用户数据安全
渐进式开发每一步都能测试、持续集成用户反馈

这些,都是超越代码的能力


五、完整技术栈#

类别技术用途
后端框架NestJS 11.0.1后端框架
数据库MongoDB + Mongoose数据存储
异步处理RxJS流处理和事件总线
数据验证class-validator输入验证
文件处理axios + pdf-parse + mammoth下载和解析文件
AI 集成LangChain 1.1.1 + @langchain/deepseekAI 应用框架
实时通信SSE + RxJS Subject流式推送进度
架构模式MVC + DI + Repository + Strategy分层架构

六、下一步能做什么#

到这里,你已经实现了一个完整的 AI 应用。但这只是开始。

1. 优化质量#

  • 收集用户反馈,迭代 Prompt
  • 做 A/B 测试,对比不同 Prompt 的效果
  • 加入人工审核,确保质量

2. 优化成本#

  • 分析 Token 消耗,找到优化空间
  • 考虑用更便宜的模型(但要测试质量)
  • 加入缓存,避免重复计算

3. 优化体验#

  • 加入实时反馈(用户可以标记”这道题不好”)
  • 支持导出(用户可以下载为 PDF)
  • 支持分享(用户可以分享给朋友)

4. 扩展功能#

  • “专项面试”模块(针对某个特定技术深度)
  • “模拟面试”模块(AI 作为面试官)
  • “薪资谈判”模块(怎样谈出高薪)

这些都是基于现有基础,可以逐步建立。


学完这章,我的几个感悟#

架构设计比代码实现更重要#

代码是容易改的,但架构错了,代价很高。

这次做系统,最花时间的不是写代码,是想清楚:

  • 扣费时机怎么设计?
  • 两步法还是一步法?
  • 失败了怎么回滚?

想清楚之后,代码反而写得很快。

用户体验是产品的灵魂#

技术再好,用户体验差,也没人用。

进度条、三种输入方式、友好的错误提示—— 这些看起来是”细节”,但恰恰是用户感受最深的地方。

失败处理和成功处理一样重要#

很多教程只讲”成功的路径”,不讲”失败了怎么办”。

但真实系统里,失败是常见的——网络超时、数据库连接失败、AI 返回格式错误……

怎样优雅地处理失败、怎样自动恢复、怎样保护用户数据—— 这些才是系统是否靠谱的关键。

工程思维是可以迁移的#

这次学到的思维方式——先设计后编码、增量开发、原子操作、失败回滚—— 不只适用于 AI 应用,适用于任何复杂系统。

技术会变,但工程思维不会变。


写在最后#

从”能跑的 Demo”到”能上线的产品”,中间隔着的不是代码量,是工程思维。

这个系统,代码可能只有 2000 行,但背后的思考—— 架构设计、成本控制、失败处理、用户体验—— 是这 2000 行代码的地基。

地基稳了,房子才能盖得高。

遇到问题时,先想想背后的原因,再想怎样解决。 很多时候,理解原因比找到解决方案更重要。


昇哥 · 2026年4月 学 AI 简历押题项目途中,把踩过的坑和想清楚的事写下来

支持与分享

如果这篇文章对你有帮助,欢迎分享给更多人或赞助支持!

【自学AI】13 实现一个生产级 AI 简历押题系统的完整复盘
https://blog.fridolph.top/posts/2026-03-03__ai-zongjie/
作者
Fridolph
发布于
2026-03-03
许可协议
CC BY-NC-SA 4.0

评论区

Profile Image of the Author
Fridolph
热爱 Coding、音乐和羽毛球的 90 后全栈工程师
公告
欢迎访问我的小站 ^_^ 我是昇哥,热爱Coding,喜爱音乐、羽毛球和摄影的 90后全栈工程师
分类
标签

文章目录