
在实际的求职准备中真正稀缺的不是题库而是针对“目标公司、目标岗位”的定向模拟。招聘网站上的职位描述里已经包含了技术栈、业务方向、任职要求和加分项这些信息足够生成一场有质量的技术面试。问题是大多数人没有精力把每一段职位描述拆解成考点、追问和评估标准。如果IDE里有一个工具允许你直接粘贴复制好的职位描述两分钟后就能进入模拟面试并且马上看到回答的评估和建议整个准备效率会高很多。这篇文章要实现的正是这样一个示例项目一个VS Code扩展读取剪贴板中的职位描述通过大模型API解析岗位信息生成结构化面试题然后在IDE侧边栏里完成“准备倒计时 - 逐题回答 - 自动评估”的完整流程。文章会包含完整的工程结构、代码片段、参数解释、运行验证和常见问题排查。这套思路不绑定特定大模型厂商只要API兼容常见的聊天补全接口都可以接入。1. 先理解模拟面试插件要拆解成哪几个环节1.1 为什么不能只用一句提示词让大模型出题很多人会觉得这个工具就是一个大模型Prompt把职位描述复制进去让模型“针对这个岗位出10道面试题”就可以了。但实际使用中会立刻发现几个问题。第一个问题是题目范围过于泛化。模型很容易生成“进程和线程的区别”“MySQL索引原理”这类通用八股虽然没错但跟这家公司当前岗位的业务背景几乎没有关联。第二个问题是输出格式不稳定。每次生成的题目类型、难度、数量可能完全不同后续无法用统一逻辑处理。第三个问题是缺少评估依据。面试题如果只是展示出来用户回答完没有人告诉他答得怎么样模拟面试的价值就减少了大半。更稳的做法是把流程拆成三阶段信息抽取阶段从职位描述中提炼公司、岗位、技术栈、职责、加分项。生成题目阶段基于结构化信息生成技术题、场景题、行为题并附带期望关键词。评估反馈阶段结合题目和用户回答输出评分、优点、不足和建议。每一阶段使用不同的系统提示词并且约定JSON输出格式。这样做的好处是即使某一阶段失败也可以单独重试每一阶段的结果都能被IDE插件结构化展示后续扩展不同题库策略时不需要改动整个流程。1.2 IDE插件在整个链路中的分工这个项目选择IDE扩展而不是普通Web应用是因为IDE扩展能访问开发者本地环境。比如用户当前打开的工程目录、package.json、源码文件、终端输出这些上下文都可以作为面试题生成的补充依据。在完整架构里IDE插件负责四件事交互入口命令、按钮、侧边栏Webview。本地数据获取读取剪贴板、配置文件、打开的文件内容。状态管理维护“空闲 - 解析中 - 面试中 - 评估中 - 完成”的状态机。调用后端LLM把组装好的Prompt发给模型再把结果解析后展示。模型服务本身不一定要跑在插件进程里可以是一个独立的HTTP服务也可以直连云厂商API。插件只需要封装一个统一的chatCompletion函数后续切换模型或内网服务时只改配置。1.3 学习环境与生产环境要区别对待如果是学习Demo可以在VS Code设置里直接放API Key所有请求从插件进程发起。但生产环境要考虑多用户、审计、限流、密钥保护通常建议把模型调用收敛到后端服务插件只发业务数据不直接持有模型密钥。这里用表格整理差异维度学习环境生产环境API KeyVS Code设置或本地环境变量保存在后端服务或密钥管理系统模型调用插件进程直接fetch后端服务统一转发做限流和审计数据隐私职位描述和回答发送到模型厂商根据合规要求脱敏或使用私有化模型错误处理打印到输出控制台结构化错误码 日志采集多人使用单人本机团队共享需要账号体系和权限控制回滚升级直接改代码调试版本发布、灰度、回滚计划熟悉这套分层后后面的实现都按“学习环境先跑通生产环境留好接口”的思路来写。2. 准备开发环境与基础工程结构2.1 工具链和版本选择示例项目采用VS Code扩展 TypeScript Node.js原因是对前端开发者友好调试插件不需要额外的IDE而且VS Code扩展API对Webview支持完善。推荐版本Node.js 18及以上npm 9及以上VS Code 1.80及以上。TypeScript可选如果不想编译可以直接写JavaScript但工程化上TypeScript更合适。生成项目脚手架可以使用官方工具npm install -g yo generator-code yo code在向导里选择“New Extension (TypeScript)”输入扩展名称、标识符、描述。脚手架会生成一个带Hello World的扩展项目。为了保持最小实现也可以手动创建以下结构mock-interview/ ├── package.json ├── tsconfig.json ├── src/ │ ├── extension.ts │ ├── llm.ts │ └── interviewProvider.ts └── media/ ├── main.js ├── sidebar.html └── style.css这个结构比脚手架默认结构更聚焦。extension.ts负责激活扩展、注册命令llm.ts负责所有模型调用interviewProvider.ts负责侧边栏Webview的创建与消息处理media目录放前端页面。2.2 package.json 中需要配置的扩展点在写代码前先把package.json的扩展点定义好。侧边栏需要注册一个WebviewView命令需要注册interview.start。{ name: mock-interview, displayName: Mock Interview, version: 0.0.1, engines: { vscode: ^1.80.0 }, main: ./out/extension.js, contributes: { commands: [ { command: mock-interview.start, title: Mock Interview: Start from Clipboard } ], viewsContainers: { activitybar: [ { id: mockInterview, title: Mock Interview, icon: media/icon.svg } ] }, views: { mockInterview: [ { type: webview, id: mockInterviewPanel, name: Interview Panel } ] }, configuration: { title: Mock Interview, properties: { mockInterview.apiBaseUrl: { type: string, default: https://api.example.com/v1, description: OpenAI compatible API base URL }, mockInterview.apiKey: { type: string, default: , description: API Key. Prefer to use secrets API in production. }, mockInterview.model: { type: string, default: gpt-4o-mini, description: Model name }, mockInterview.prepareSeconds: { type: number, default: 120, description: Preparation countdown seconds } } } }, scripts: { compile: tsc -p ./, watch: tsc -watch -p ./ }, devDependencies: { types/node: ^20.0.0, types/vscode: ^1.80.0, typescript: ^5.0.0 } }配置项中prepareSeconds就是标题里“2分钟”的实现来源。默认120秒可以在设置里调小或调大。注意apiKey出现在configuration里只是为了开发调试方便。发布插件或团队使用时应改为通过context.secrets保存避免明文存到配置文件中。2.3 准备统一的大模型访问模块为了避免在多个地方重复写网络请求把模型调用封装成llm.ts。示例中采用OpenAI兼容的/chat/completions接口可以通过apiBaseUrl指向任何兼容服务包括本地部署的服务。// src/llm.ts import * as vscode from vscode; export interface ChatMessage { role: system | user | assistant; content: string; } export async function chatCompletion( context: vscode.ExtensionContext, messages: ChatMessage[], temperature 0.3, maxTokens 2000 ): Promisestring { const config vscode.workspace.getConfiguration(mockInterview); const apiBaseUrl config.getstring(apiBaseUrl, ); const apiKey await context.secrets.get(mockInterview.apiKey) || config.getstring(apiKey, ); const model config.getstring(model, gpt-4o-mini); const url ${apiBaseUrl.replace(/\/$/, )}/chat/completions; const response await fetch(url, { method: POST, headers: { Content-Type: application/json, Authorization: Bearer ${apiKey} }, body: JSON.stringify({ model, messages, temperature, max_tokens: maxTokens, response_format: { type: json_object } }) }); if (!response.ok) { const text await response.text(); throw new Error(LLM API error ${response.status}: ${text}); } const data await response.json(); return data.choices?.[0]?.message?.content ?? ; }这段代码有几个关键点response_format: { type: json_object }要求模型返回JSON但这不是所有服务都支持若服务不支持需要删除该参数并在解析时做容错。context.secrets.get(...)优先读取安全存储没有时才回退到配置项。返回值是字符串解析JSON放在调用方做方便统一处理异常。如果API服务不支持response_format可以用另一种方式在提示词里强制要求“只输出JSON不要markdown代码块”然后从返回文本中截取第一个{到最后一个}。后面的解析函数会用到这个策略。3. 实现核心功能粘贴职位描述、生成面试题、开始面试3.1 注册命令并从剪贴板读取职位描述现在实现extension.ts。激活扩展时创建一个InterviewProvider并注册到registerWebviewViewProvider同时注册命令mock-interview.start。// src/extension.ts import * as vscode from vscode; import { InterviewProvider } from ./interviewProvider; export function activate(context: vscode.ExtensionContext) { const provider new InterviewProvider(context); context.subscriptions.push( vscode.window.registerWebviewViewProvider(mockInterviewPanel, provider, { webviewOptions: { retainContextWhenHidden: true } }) ); context.subscriptions.push( vscode.commands.registerCommand(mock-interview.start, async () { const text await vscode.env.clipboard.readText(); if (!text || text.trim().length 20) { vscode.window.showWarningMessage(剪贴板中找不到有效的职位描述。); return; } provider.startFromText(text); vscode.commands.executeCommand(mockInterviewPanel.focus); }) ); } export function deactivate() {}startFromText会通知Webview进入parsing状态并将文本发给WebviewWebview再通过postMessage请求扩展端解析。这里采用消息循环而不是直接传参是因为Webview的创建可能是异步的统一走消息通道更可靠。点击命令后插件会读取剪贴板内容并检查长度是否至少20个字符。如果内容太短很可能是误复制了标题或一行字提示用户重新复制。3.2 设计职位解析与面试题生成的提示词为了让大模型输出稳定要编写结构化的系统提示词。第一步是解析职位描述输出JSON。这里给一个可用的提示词示例你是资深技术面试官。用户会提供一段职位描述。请解析并返回JSON不要输出多余文字。 JSON结构 { company: 公司名称未知为空字符串, position: 岗位名称, techStack: [技术栈1, 技术栈2], responsibilities: [职责1, 职责2], requirements: [要求1, 要求2], seniority: 初级|中级|高级|未知, interviewTopics: [数据结构, 并发, 项目经验] }第二步是基于解析结果生成面试题基于以下岗位信息生成8道面试题其中6道技术题、1道场景设计题、1道行为题。 返回JSON每个问题包含 { questions: [ { id: q1, type: tech | scenario | behavior, question: 问题描述尽量结合具体业务, difficulty: easy | medium | hard, expectedKeywords: [关键词1, 关键词2] } ] }在实际代码里这两个提示词会分别发送两次chatCompletion调用。第一次调用得到岗位信息JSON第二次把岗位信息JSON和职位描述原文一起发给模型生成题目。为什么要先解析再生成而不是一次搞定因为第二次生成题目时可以把第一次解析出的结构化字段放进Prompt模型更容易围绕关键技术栈出题而不是迷失在职位描述原文的长文本里。当然这会多一次API调用但结果稳定性收益大于成本。3.3 在 Webview 中实现两分钟准备倒计时面试流程页面放在sidebar.html中。Webview本质是一个内嵌iframe可以自由写HTML/CSS/JS。倒计时逻辑建议不依赖setInterval的累计值而是用时间戳差值计算剩余秒数避免浏览器节流导致倒计时变慢。// media/main.js 片段 let countdownEnd 0; let timerId null; function startCountdown(seconds) { countdownEnd Date.now() seconds * 1000; if (timerId) { clearInterval(timerId); } timerId setInterval(() { const remain Math.max(0, Math.round((countdownEnd - Date.now()) / 1000)); const min String(Math.floor(remain / 60)).padStart(2, 0); const sec String(remain % 60).padStart(2, 0); document.getElementById(countdown).textContent ${min}:${sec}; if (remain 0) { clearInterval(timerId); timerId null; notifyExtension(prepareFinished); } }, 200); }使用Date.now()计算剩余时间即使标签页被后台节流回到页面时也能显示正确的剩余时间。倒计时结束后通知扩展端进入答题阶段。答题阶段按顺序显示题目列表用户在一个textarea中输入回答点击“下一题”。这里不需要录音文本输入已经足够跑通流程。如果想要语音输入可以后续接浏览器MediaRecorder和语音识别服务。3.4 自动评估回答并展示结果所有题目回答完后把所有题目和答案组装成数组发给扩展端扩展端调用评估提示词。评估提示词示例你是严谨的技术面试官。用户正在准备一场模拟面试。请结合每个问题的期望关键词评估用户的回答。 返回JSON { overallScore: 0-100, summary: 总体评价, questionEvaluations: [ { id: q1, score: 0-100, reason: 评分理由, improvement: 改进建议 } ] }评估结果展示在Webview下方包括总分、每题得分、改进建议。为了让结果更有参考价值应该在评估时把“岗位要求”也作为上下文传入这样模型不会只根据单个回答打分而会考虑岗位匹配度。4. 关键代码实现与参数说明4.1 核心状态机与消息协议Webview和扩展端通过postMessage通信为了调试方便可以定义固定消息类型export type MessageType | startFromClipboard | parseStarted | parseCompleted | parseFailed | questionsGenerated | answerSubmitted | evaluationStarted | evaluationCompleted | evaluationFailed;状态顺序idle - parsing - ready - preparing - interviewing - evaluating - result。每个状态对应Webview中一个显示区域。在InterviewProvider中通过onDidReceiveMessage处理来自Webview的请求。下面是消息处理的核心骨架// src/interviewProvider.ts 片段 this._view.webview.onDidReceiveMessage( async (message) { switch (message.type) { case parseJobDescription: await this.handleParseJobDescription(message.text); break; case generateQuestions: await this.handleGenerateQuestions(message.jobInfo); break; case evaluateAnswers: await this.handleEvaluateAnswers(message.qaPairs); break; } }, undefined, context.subscriptions );每个handle*方法内部都会调用llm.ts并把进度通过postMessage发回Webview。4.2 LLM 调用函数的参数选择chatCompletion中的temperature和maxTokens需要根据场景调整阶段temperaturemaxTokens说明解析职位描述01000希望稳定解析不要发挥生成面试题0.72000需要一定多样性