1. 项目概述:一个AI驱动的面试刷题伴侣
最近在准备技术面试,尤其是前端和全栈岗位,刷LeetCode、牛客网成了日常。但刷题有个痛点:题目做完了,只能看个“通过/不通过”,代码质量怎么样、有没有更好的解法、面试官会怎么追问,心里完全没底。市面上一些工具能跑测试用例,但缺少那种“真人面试官”的反馈感。正好,我一直在用Claude、GPT这些大模型辅助编程和思考,就琢磨着:能不能让AI来当这个面试官?
于是,我花了几周时间,用Vue 3搭建了一个前端,后端用Node.js + Express,核心是接入了多个主流大模型的API,做了一个AI面试刷题系统。这个系统的核心功能很简单:你写代码解题,提交后,系统不仅会运行测试用例判断对错,还会调用你选择的AI模型(我接入了Claude 3系列、DeepSeek最新模型、阿里的通义千问以及OpenAI的GPT-4),让它从“面试官”的角度,给你的代码打分并生成详细的评价报告。
它适合所有正在准备技术面试的程序员,尤其是前端、后端和算法方向的求职者。你不需要再对着冰冷的“AC”(答案正确)发呆,而是能获得一份涵盖代码正确性、时间复杂度分析、空间复杂度优化、代码风格、可读性甚至潜在边界情况的立体化评估报告。我自己用它来复盘刷题记录,感觉比单纯刷题效率高了不止一倍,因为每一次提交都是一次微型的模拟面试。
2. 系统核心设计与架构选型
2.1 为什么选择多模型架构而非单一模型?
最初的想法很简单,接一个最强的模型,比如GPT-4,不就完了?但在实际设计时,我否定了这个方案。原因有几个:
- 模型特性差异:不同的模型擅长点不同。比如,Claude在代码逻辑和安全性分析上非常严谨,生成的评价往往一针见血;DeepSeek作为国产翘楚,对中文语境下的代码注释、命名规范理解更深入,且API成本极具优势;GPT-4则在创造性和多种解法的联想上表现突出。让用户有的选,可以根据自己侧重的反馈类型(如更看重性能优化还是代码规范)来切换模型。
- 成本与稳定性:依赖单一API服务有风险。如果某个服务商临时调整策略、出现高延迟或故障,整个系统就瘫痪了。多模型接入相当于有了“备胎”,DeepSeek和通义千问的API成本更低,可以作为高频使用的首选,Claude和GPT-4则在需要深度分析时启用。
- 避免模型偏见:同一个问题,不同模型的评价角度可能略有不同。对比阅读Claude和DeepSeek给出的评价,有时能发现一些自己忽略的盲点,这种多视角的反馈更有价值。
基于这些考虑,我设计了可插拔的模型适配层。后端定义一个统一的AIEvaluator接口,不同模型的实现类去适配这个接口。这样,增加一个新的模型支持,只需要实现这个接口即可,系统核心逻辑完全不用动。
2.2 前端技术栈:Vue 3 + TypeScript + Monaco Editor
前端是用户直接交互的地方,体验必须流畅。我选择了Vue 3的组合式API和<script setup>语法,代码组织更清晰。
- 状态管理:直接使用Vue 3的
reactive和ref管理组件状态,对于简单的跨组件状态(如用户选择的模型、主题设置),使用provide/inject。没有引入Pinia,因为当前应用复杂度还没到那个程度,保持轻量。 - 代码编辑器:这是核心组件。我选择了微软的Monaco Editor,也就是VS Code的内核。它支持语法高亮、智能提示、代码折叠、多光标等高级功能,和本地IDE的写代码体验几乎一致。通过
@monaco-editor/vue这个封装好的Vue组件,集成起来非常方便。 - UI组件库:我使用了Element Plus,它的组件丰富、文档清晰,能快速搭建出美观且一致的后台管理界面。比如题目列表的表格、提交历史的翻页、模型选择的单选组,用Element Plus能省下大量时间。
- TypeScript:整个项目用TypeScript重写。这对于一个涉及复杂状态(题目数据、评测结果、用户信息)和多个API接口调用的应用来说,能极大提升开发效率和代码可靠性,避免很多低级错误。
注意:Monaco Editor的包体积较大,直接全量引入会影响首屏加载。我通过
import()动态异步加载编辑器组件,并只启用必要的语言特性(如JavaScript/TypeScript/Python),有效控制了打包后的体积。
2.3 后端与服务设计:Node.js + Express + 任务队列
后端职责明确:用户认证、题目管理、代码运行、调用AI API、存储结果。
- Web框架:Express足够轻量灵活,路由中间件模式也符合我的开发习惯。配合
express-async-errors处理异步错误,让代码更整洁。 - 代码执行安全沙箱:这是技术难点和安全重点。绝对不能让用户提交的代码在服务器上直接
eval或child_process.exec执行,那等同于开放了服务器权限。我采用了isolated-vm这个库,它提供了一个真正的V8隔离沙箱环境。用户代码在一个全新的、资源受限的V8 Isolate中运行,无法访问Node.js的fs、net等模块,也无法无限循环阻塞进程。我设置了严格的CPU时间和内存限制,超限立即终止。 - AI API调用与任务队列:AI生成评价可能需要几秒到十几秒,不能阻塞HTTP请求。我引入了
Bull这个基于Redis的队列库。当用户提交代码后,后端立即响应“评测已开始”,同时将一个评测任务(包含代码、题目信息、所选模型)推入Redis队列。一个独立的Worker进程从队列中取出任务,依次执行:1. 在沙箱中运行代码验证基础用例;2. 调用对应AI API;3. 将原始结果和AI评价一起存入数据库。前端通过WebSocket或轮询获取最终结果。 - 数据库:使用PostgreSQL。题目表、提交记录表、用户表、AI评测报告表关系清晰。利用JSONB字段存储AI返回的复杂结构化评价数据,查询和更新都很方便。
3. 核心功能模块深度解析
3.1 智能评测流水线:从代码提交到生成报告
用户点击“提交”按钮后,背后是一套完整的自动化流水线:
- 预处理与安全检查:后端首先对用户代码进行简单的静态扫描,使用正则表达式匹配绝对禁止的模式,例如尝试导入
child_process、fs,或使用while(true)等可能造成死循环的代码片段。这一步是粗筛,快速拦截明显恶意代码。 - 测试用例执行:在
isolated-vm沙箱中,注入题目预定义的输入用例,执行用户函数,捕获输出。将输出与预期结果进行对比。这里对比不是简单的===,对于数组、对象等复杂结构,需要进行深度比较。此步骤产生基础分数(例如,20%的权重)。 - 构建AI提示词(Prompt):这是决定AI评价质量的关键。不能简单地把代码扔给AI说“评价一下”。我构建了一个结构化的Prompt模板:
[代码块]你是一位资深技术面试官。请对以下编程题解进行评价: 题目描述:[这里插入题目描述,包括输入输出示例] 用户提交的代码(语言:[JavaScript/Python]):
这个Prompt明确了角色、任务、评价维度和输出格式,让AI的反馈高度结构化,便于前端解析和展示。请从以下维度给出详细评价,并给出总体得分(百分制): 1. 正确性:是否通过了所有测试用例?逻辑是否有误? 2. 效率:时间复杂度和空间复杂度分析是否最优?有无优化空间? 3. 代码风格:命名、注释、格式是否符合规范? 4. 健壮性:是否考虑了边界条件(如空输入、极大值)? 5. 可读性:代码结构是否清晰,易于他人理解? 请以JSON格式回复,包含`score`(总分), `correctness`, `efficiency`, `style`, `robustness`, `readability`(各项子分数), 以及`detailed_feedback`(详细文本评价)。 - 调用AI API:根据用户选择,将构建好的Prompt发送至对应的AI服务商。这里需要处理各家的API差异,比如OpenAI是
messages数组,Claude也是类似,而DeepSeek的请求体格式可能略有不同。统一封装在各自的适配器中。 - 结果解析与存储:收到AI的JSON响应后,解析并提取分数和评价。与第2步的基础分数按权重(如AI评价占80%,基础用例占20%)合并,计算出最终得分。将所有数据(用户代码、测试结果、AI原始响应、解析后的结构化评价、最终分数)存入数据库。
- 实时反馈前端:通过WebSocket将评测进度(“运行测试中” -> “调用AI分析中” -> “完成”)和最终结果推送给前端页面。前端收到后,以清晰的面板展示分数雷达图和详细的评价文本。
3.2 多模型API适配层实现细节
为了让新增一个模型支持变得简单,我抽象了一个基础类:
interface AIEvaluationRequest { code: string; problemDescription: string; language: string; } interface AIEvaluationResponse { score: number; // 总分 breakdown: { // 分项得分 correctness: number; efficiency: number; style: number; robustness: number; readability: number; }; detailedFeedback: string; // 详细评价 rawResponse?: any; // 原始API响应,用于调试 } abstract class BaseAIModelAdapter { constructor(protected apiKey: string, protected config: any) {} abstract evaluate(request: AIEvaluationRequest): Promise<AIEvaluationResponse>; // 公共方法,如构建Prompt、处理错误 protected constructPrompt(req: AIEvaluationRequest): string { // 返回构建好的Prompt字符串 } }然后,为每个模型创建子类:
class ClaudeAdapter extends BaseAIModelAdapter { async evaluate(request: AIEvaluationRequest): Promise<AIEvaluationResponse> { const prompt = this.constructPrompt(request); const response = await fetch('https://api.anthropic.com/v1/messages', { method: 'POST', headers: { 'x-api-key': this.apiKey, 'anthropic-version': '2023-06-01', 'Content-Type': 'application/json', }, body: JSON.stringify({ model: 'claude-3-sonnet-20240229', // 可根据配置选择不同版本 max_tokens: 2000, messages: [{ role: 'user', content: prompt }] }) }); const data = await response.json(); // 解析Claude返回的JSON,转换为统一的AIEvaluationResponse格式 return this.parseResponse(data); } private parseResponse(claudeResponse: any): AIEvaluationResponse { // 具体解析逻辑,确保格式统一 } } class DeepSeekAdapter extends BaseAIModelAdapter { async evaluate(request: AIEvaluationRequest): Promise<AIEvaluationResponse> { const prompt = this.constructPrompt(request); const response = await fetch('https://api.deepseek.com/v1/chat/completions', { method: 'POST', headers: { 'Authorization': `Bearer ${this.apiKey}`, 'Content-Type': 'application/json', }, body: JSON.stringify({ model: 'deepseek-chat', // 或 deepseek-coder messages: [{ role: 'user', content: prompt }], response_format: { type: "json_object" } // 要求返回JSON }) }); const data = await response.json(); return this.parseResponse(data); } // ... parseResponse }在服务启动时,根据配置初始化这些适配器实例,并注册到一个ModelFactory中。当Worker处理任务时,只需从工厂获取对应的适配器实例调用evaluate方法即可。
3.3 前端交互与状态管理设计
前端页面主要分为几个核心区域:题目选择区、代码编辑区、控制台(运行结果/测试用例)和AI评测报告区。
- 响应式代码编辑器:使用Monaco Editor,并监听窗口
resize事件,动态调整编辑器尺寸。为用户提供了主题切换(vs-dark, vs-light)、字体大小调整、自动换行等常用功能。 - 实时运行反馈:当用户点击“运行”而非“提交”时,前端只将代码和当前输入的用例发送到后端一个快速执行端点(不经过AI和队列),秒级返回执行结果,在控制台显示。这给了用户一个即时验证的途径。
- 评测报告可视化:AI返回的结构化数据非常适合可视化。我使用ECharts绘制了一个雷达图,直观展示在“正确性”、“效率”等五个维度的得分。下方以手风琴(Accordion)形式展开详细的文本评价,关键建议用高亮标出。
- 提交历史与对比:所有提交记录都被保存。用户可以查看历史提交,并选择两次提交进行“对比视图”,系统会高亮显示代码差异,并并排展示两次的AI评价,清晰看到自己的改进轨迹。
状态管理上,核心的useCodingStore组合式函数大致如下:
// stores/useCodingStore.ts export const useCodingStore = () => { const currentCode = ref<string>(''); const selectedProblem = ref<Problem | null>(null); const selectedAIModel = ref<'claude' | 'deepseek' | 'tongyi' | 'gpt'>('deepseek'); // 默认DeepSeek,性价比高 const evaluationResult = ref<EvaluationResult | null>(null); const isEvaluating = ref<boolean>(false); const submitForEvaluation = async () => { if (!selectedProblem.value) return; isEvaluating.value = true; evaluationResult.value = null; try { // 1. 调用提交API,触发后端任务队列 const { submissionId } = await api.submitCode({ problemId: selectedProblem.value.id, code: currentCode.value, language: 'javascript', model: selectedAIModel.value }); // 2. 建立WebSocket连接或开始轮询,获取评测进度和结果 const result = await waitForEvaluationResult(submissionId); evaluationResult.value = result; } catch (error) { // 错误处理 } finally { isEvaluating.value = false; } }; return { currentCode, selectedProblem, selectedAIModel, evaluationResult, isEvaluating, submitForEvaluation }; };4. 开发中的关键挑战与解决方案
4.1 安全隔离:用户代码执行的“铁笼”
让服务器执行未知用户代码是最大的风险点。我调研了多种方案:
- Docker容器:为每次执行启动一个全新的Docker容器,用完即毁。安全性高,但启动开销大(秒级),不适合高并发场景,且需要管理Docker守护进程,增加了运维复杂度。
vm2或safe-eval等库:它们提供了一定隔离,但被证明存在逃逸漏洞,安全性不足以应对恶意代码。isolated-vm:它直接利用V8引擎的隔离能力,每个“隔离岛”拥有独立的堆栈和内存空间,与主Node.js环境完全隔离。性能开销极小(毫秒级初始化),资源限制精确。这是我最终选择的方案。
具体实现:
const ivm = require('isolated-vm'); const isolate = new ivm.Isolate({ memoryLimit: 128 }); // 限制128MB内存 const context = await isolate.createContext(); const jail = context.global; // 向沙箱暴露有限的、安全的函数,比如一个console.log的代理 await jail.set('log', function(...args) { console.log(...args.map(arg => ivm.ExternalCopy(arg).copy())); }); // 编译并运行用户代码 const script = await isolate.compileScript(userCode); const result = await script.run(context, { timeout: 5000 }); // 设置5秒超时遇到的坑:isolated-vm的API比较底层,需要手动处理错误和超时。特别是内存限制,如果用户代码分配内存过多,会抛出MemoryLimitExceeded错误,需要在try-catch中妥善处理,给前端返回友好的错误信息,而不是让整个Worker崩溃。
4.2 处理AI API的速率限制与稳定性
所有AI服务商都有速率限制(Rate Limit)。如果用户集中提交,短时间内大量调用API会导致限流,任务失败。
解决方案:
- 队列化与限流:
Bull队列本身就有并发控制功能。我为每个AI模型设置了一个独立的队列(如claude-eval、deepseek-eval),并设置每个队列的limiter,例如{ max: 5, duration: 10000 }表示10秒内最多处理5个任务。这保证了发送到API的请求不会超速。 - 指数退避重试:在Worker任务处理逻辑中,调用AI API时如果收到
429 Too Many Requests或网络错误,不会立即失败。我使用async-retry库包装API调用,配置指数退避重试策略(如最多重试3次,延迟时间依次为1秒、2秒、4秒)。很多临时性网络波动或轻微的速率限制,通过重试就能成功。 - 故障转移:在系统配置中,我可以为每个模型设置一个“备选模型”。当主模型连续失败多次后,系统可以自动将任务路由到备选模型,确保评测服务不中断。例如,将DeepSeek设置为GPT-4的备选,因为成本更低。
4.3 评测标准统一与提示词工程
不同AI模型对同一个Prompt的理解和输出格式会有差异。让Claude、DeepSeek、GPT-4都输出完全一致的JSON结构是个挑战。
我的做法:
- 在Prompt中强制要求JSON格式:如上文所示,在Prompt末尾明确要求“请以JSON格式回复”,并给出具体的字段名示例。对于支持
response_format参数的API(如OpenAI和DeepSeek的新版本),直接设置response_format: { type: "json_object" },能极大提高返回JSON的稳定性。 - 在后端进行健壮性解析:编写一个宽容的解析函数。首先尝试
JSON.parse。如果失败,则尝试用正则表达式从返回的文本中提取可能的JSON块。如果还不行,则尝试使用另一个AI(比如GPT-3.5-turbo)来对混乱的回复进行“结构化提取”。这是一个降级方案,确保无论如何都能得到一些可用的数据。 - 分数标准化:不同模型打分尺度可能不同,有的喜欢打80-90分,有的偏严格打60-70分。我在系统设置中提供了一个“分数校准”功能。管理员可以手动评审一批提交,给出“标准分”,系统会学习并生成一个线性转换系数,用于调整该模型的输出分数,使其更接近统一标准。这是一个持续优化的过程。
5. 部署、优化与未来展望
5.1 系统部署与性能考量
项目采用前后端分离部署。
- 前端:使用Vite构建,产物部署到Netlify或Vercel,享受全球CDN和自动HTTPS。
- 后端:使用Docker容器化。一个容器运行Express主服务,处理HTTP请求和WebSocket;另一个容器运行Node.js Worker进程,专门处理Bull队列任务。两者都连接到同一个Redis实例(作为队列和缓存)和PostgreSQL数据库。
- 数据库与缓存:使用云服务商的托管PostgreSQL和Redis服务,省去运维麻烦,并配置了读写分离和连接池。
性能优化点:
- 数据库查询优化:对提交历史表按
user_id和created_at建立复合索引,加快个人历史查询速度。对题目列表进行分页查询。 - 前端资源懒加载:Monaco Editor、ECharts等较大库都使用动态导入。
- WebSocket连接管理:使用
ws库,并设置心跳包防止连接超时。当用户离开评测页面时,前端主动关闭WebSocket以释放服务器资源。
5.2 实际使用体验与效果对比
我自己和几位一起找工作的朋友深度使用了几周,对比了不同模型的表现:
- DeepSeek:性价比之王。响应速度很快,代码分析准确,尤其是对中文注释的反馈很接地气。对于常见的算法题,它的评价已经非常够用。成本只有GPT-4的几十分之一,适合高频刷题。
- Claude 3 Sonnet:分析最严谨、最像技术专家。它会非常仔细地推敲代码逻辑,经常能指出一些极其隐蔽的边界条件错误,并且给出的优化建议非常具体,比如“这里可以用双指针将复杂度从O(n²)降到O(n)”。但响应稍慢,成本中等。
- GPT-4:创造力最强。当一道题有多种解法(如递归、迭代、动态规划)时,GPT-4最擅长列举并对比这些解法,甚至能联想到相关的实际应用场景。但在代码风格等细节上,有时不如Claude严谨。
- 通义千问:对国内互联网公司的常见编码规范和工程实践理解很好,给出的建议非常“本土化”,适合瞄准国内大厂面试的同学。
一个典型例子:一道“二叉树层序遍历”的题。我用递归实现了。DeepSeek的评价是“正确,但空间复杂度可能因递归栈而较高,建议考虑迭代写法”。Claude的评价是“递归解法简洁,但在最坏情况下(链状树)空间复杂度为O(n)。此外,函数未处理输入为null的根节点情况,可能导致错误。建议增加判空和迭代解法示例”。显然,Claude的反馈更细致、更严格。
5.3 可能的扩展方向
这个系统目前已经比较实用,但还有很多可以深化的地方:
- 支持更多编程语言:目前主要支持JavaScript/TypeScript和Python。可以通过沙箱预装更多语言运行时(如Java的OpenJDK、Go),并定制对应的AI Prompt来扩展。
- 模拟面试对话:不止于代码评价,可以开发一个“AI面试官对话模式”。AI根据题目逐步提问,引导用户思考,用户可以用自然语言回答思路,AI再追问或提示,模拟真实的面试互动。
- 知识点关联与弱点分析:系统可以记录用户所有提交,分析其常错的题目类型(如动态规划、链表),自动生成个人知识薄弱点报告,并推荐针对性练习题目。
- 企业定制版:为企业招聘团队提供定制服务,导入公司内部的真实面试题库和评分标准,用于候选人初筛或员工内部技能测评。
做这个项目的初衷是为了解决自己刷题时的痛点,但把它做出来、用起来之后,发现它带来的价值远超预期。它不仅仅是一个“评分工具”,更是一个24小时在线的、拥有不同风格和专长的“编程教练”。每次看到AI指出的那个自己没意识到的边界条件错误,或者那个更优雅的解法,都感觉收获满满。对于正在准备面试的朋友,我的建议是,工具能帮你提高效率,但核心还是理解与练习。这个系统最好的用法,是把它当作一个严格的“第一读者”,帮你发现盲区,而最终的总结和吸收,还得靠你自己。