DeepSeek/GPT-4 落地实战:我如何用 Node.js + AI 手搓一个“面试神器”
作为一个程序员,每次找工作最头疼的两件事:一是排版简历,二是准备面试。
写简历时,Word 排版容易乱,Markdown 稍微复杂点就显得简陋;想用 LaTeX 这种“学术界标准”来装点门面,但配置环境和调样式能把人逼疯。 准备面试时,背了一堆八股文,真到了面试官面前却张口结舌,找不到人做 Mock Interview(模拟面试)。
前段时间 DeepSeek 和 GPT-4 的 API 能力越来越强,我突发奇想:为什么不让 AI 帮我把这两件事都解决了?
于是我肝了几个周末,开发了 My-CV —— 一个集成了 LaTeX 自动编译 和 AI 模拟面试 的全栈项目。今天就来聊聊这个项目背后的技术实现,特别是如何利用 LLM 构建一个垂直场景的 AI 应用。
🏗️ 整体架构
为了快速落地,我选择了最熟悉的技术栈:
- 前端: React + TypeScript + Tailwind CSS + Radix UI
- 后端: Node.js (Express)
- 核心服务:
- PDF 生成服务: EJS 模板引擎 + TeX Live (XeLaTeX)
- AI 服务: OpenAI SDK (兼容 DeepSeek/GPT-4)
- 数据库: SQLite (轻量级,方便部署)
🛠️ 技术难点一:如何搞定“高颜值”简历?
市面上很多简历工具是基于 HTML/Canvas 转 PDF 的,最大的问题是分页截断和文字失真。为了达到“出版级”的质量,我选择了 LaTeX。
但是,让用户自己写 LaTeX 是不可能的。我的解决方案是:表单 -> JSON -> EJS 模板 -> LaTeX 源码 -> PDF。
1. 模板引擎注入
我定义了一套标准的 Resume JSON Schema,然后写了 .tex.ejs 模板文件。
在后端,利用 ejs 将用户数据注入到 LaTeX 模板中:
// server/src/services/generator.ts 片段
const templateContent = await fs.readFile(templatePath, 'utf8');
// 数据清洗,防止 LaTeX 特殊字符炸掉编译
const sanitizedData = sanitizeDataForLatex(normalizedData);
// 渲染 LaTeX 源码
const texContent = ejs.render(templateContent, sanitizedData);
2. 异步编译队列
LaTeX 编译是非常消耗 CPU 的操作(且慢)。如果直接在 HTTP 请求中同步等待,用户体验会很差,并发高了服务器也会挂。
所以我手写了一个简单的内存任务队列 (Worker) 来处理编译任务:
// server/src/services/worker.ts 片段
export class QueueWorker {
private async loop() {
while (this.isRunning) {
// 1. 获取任务
const job = await CompilationQueue.getNextJob();
if (job) {
try {
// 2. 调用 LaTeX 编译器生成 PDF
await GeneratorService.generateResume(job.templateId, job.data, job.jobId);
// 3. 生成缩略图 & 更新状态
await LatexCompiler.generateThumbnail(job.jobId);
await CompilationQueue.markCompleted(job.jobId, resultUrl);
} catch (err) {
await CompilationQueue.markFailed(job.jobId, err.message);
}
} else {
// 队列为空,休眠
await new Promise(resolve => setTimeout(resolve, 1000));
}
}
}
}
这样,前端只需要轮询任务状态即可,保证了服务的稳定性。
🤖 技术难点二:构建“有灵魂”的 AI 面试官
这是最有趣的部分。如何让 AI 真的像面试官一样提问,而不是像 ChatGPT 一样跟你闲聊?
1. Prompt Engineering (角色扮演)
我设计了一个 System Prompt,核心要点是:One question at a time(一次只问一个问题)和基于简历。
这是实际代码中的 Prompt 构建逻辑:
// server/src/services/interview.ts
const getSystemPrompt = (resumeContent: any) => {
return `You are an experienced technical interviewer.
Your task is to conduct a realistic mock interview based on the candidate's resume.
Candidate Resume Data:
${JSON.stringify(resumeContent, null, 2)}
Rules:
1. Act as a professional interviewer. Be objective, slightly critical, but polite.
2. Ask ONE question at a time. Do not overwhelm the candidate.
3. Focus your questions on the projects and skills listed in the resume.
4. If the candidate's answer is too vague, ask follow-up questions.
5. **LANGUAGE**: Conduct the interview in the language of the candidate's resume.
Current stage: Start the interview. Briefly introduce yourself and ask the first question.`;
};
把用户的简历 JSON 直接塞进 Prompt 里,LLM 就能非常精准地针对你的项目经历发问。比如你写了“优化了 Webpack 打包速度”,它就会问你“具体用了哪些插件?优化前后构建时间对比如何?”。
2. 结构化评分报告
面试结束后,我们需要 AI 给出一个详细的评分报告。这里利用了 LLM 的 JSON Mode。
我定义了评分的 Prompt,要求 AI 必须返回 JSON 格式:
const feedbackPrompt = `
...
CRITICAL EVALUATION RULES:
1. **NO HALLUCINATION**: Do NOT invent, guess, or assume any responses.
2. **SCORING**: Score from 0-100.
Please output the evaluation in JSON format:
{
"score": (integer 0-100),
"feedback": "Overall summary...",
"strengths": ["point 1", "point 2"],
"improvements": ["point 1", "point 2"]
}
`;
// 调用 LLM 时开启 json_object 模式
const rawFeedback = await LLMService.chat([
{ role: 'user', content: feedbackPrompt }
], true);
这样前端拿到数据后,可以直接渲染成漂亮的图表和列表,而不是一坨纯文本。
🚀 成果与体验
经过一番折腾,现在的效果是这样的:
- 左边填表,右边实时预览 LaTeX 级别的 PDF。
- 点击 "Start Mock Interview",AI 会阅读你的简历,开始对你进行“拷打”。
- 面试结束后,你会收到一份包含评分、优缺点分析的详细报告。
我在做这个项目的过程中,深刻体会到:AI 原生应用 (AI Native App) 的核心不在于算法模型,而在于如何把 LLM 的能力(理解、生成、角色扮演)无缝嵌入到传统的业务流程(如简历生成、面试准备)中去。
如果你也对 AI 落地应用感兴趣,或者单纯想体验一下被 AI 面试官“拷打”的感觉,欢迎来玩:
(目前项目还在快速迭代中,欢迎在评论区提 Feature Request,如果有 Bug 请轻喷 😂)