设计一个 AI 模拟面试系统

深入系统设计场景题约 10 分钟读完

一句话回答

以题库和参考答案为基础,服务端维护一个面试状态机:出题 → 作答 → 点评 → 追问,最多 N 轮。每轮把评分细则、题目、精简后的参考答案、历史轮次和本轮回答组装成 Prompt,模型按固定格式流式返回点评,结束后解析出得分和追问。候选人的回答放在标签里,并声明它只是待评价的数据,以此防止注入。面试记录和学习记录联动,低分题自动加入复习;按用户限制调用次数;用人工打分的样本评估模型打分是否可靠。

详细解析

第一步:澄清需求

  • 形式:文字作答还是语音?先做文字,语音作为扩展
  • 流程:每道题最多追问几轮?要不要一场面试连续考多道题、最后给总评?
  • 评分:给分数还是等级?结果要能和学习记录联动
  • 成本:每次调用的输入较长(带参考答案),按用户限制每天的次数
  • 体验:点评要流式返回,不能让用户等十几秒才看到第一个字

第二步:整体架构

文本
前端:题目页 → 作答 → 流式显示点评 → 回答追问 → 结束
   │  POST /api/interviews/stream(SSE 事件:delta / done / error)
   ▼
面试服务
  ├─ 鉴权 + 次数限制(每个用户 24 小时内最多 N 次)
  ├─ 状态校验:面试存在、属于当前用户、还没结束;本轮问题从数据库取
  ├─ Prompt 组装:评分细则 + 题目 + 参考答案(去掉代码块、截断)
  │              + 历史轮次 + 本轮回答(用标签包裹)
  ├─ 调用模型(流式)→ 转发片段 → 结束后解析得分和追问
  └─ 落库:面试记录(每轮的回答、点评、得分、追问)→ 更新学习记录
   ▼
MySQL:questions(题目和参考答案)、interviews、ai_calls(调用记录)、study_records(学习状态)

第三步:核心模块和数据模型

面试状态机:

文本
开始:第 1 轮的问题 = 题目本身
  → 等待作答 → 点评中(流式)→ 解析得分和追问
  → 轮数 < N 且有追问:下一轮的问题 = 这条追问,回到"等待作答"
  → 否则:结束,更新学习记录

状态全部保存在服务端:本轮问的是什么,由服务端根据上一轮存下的追问决定,前端只提交回答。如果让前端传"当前问题",用户就能篡改题目来刷分。

数据模型:interviews 表一行对应一次面试,字段有用户、题目、轮数、最新得分,以及 turns(JSON 数组,每个元素是一轮的回答、点评、得分、追问和时间)。轮数很少、总是整体读写,用 JSON 列比单独建一张轮次表简单;以后要按单轮得分做统计分析,再拆成独立的表。

评分标准:在系统提示里写出分档描述,比如 9~10 分要点完整准确、有自己的理解,7~8 分主干正确、有少量遗漏,5~6 分方向对但不完整或有明显错误,以此类推。再写明几条原则:只看技术内容,不看文采;换一种说法表达同样的意思也算对;回答简短但关键点准确,也可以给高分。参考答案提供评分的依据,避免模型按自己的理解随意发挥。Prompt 的完整设计见 AI 面试官的提示词设计。

输出格式和解析,有两种做法:

  • 要求 JSON 结构化输出:解析可靠,但流式展示时要处理不完整的 JSON,原始 JSON 也不适合直接给用户看
  • 要求固定格式的 Markdown:第一行"得分:X/10",然后是"答对的点""遗漏或不准确""改进建议""追问"几个小节。可以直接流式展示,结束后用正则解析得分和追问;解析失败时得分记为空,不影响点评的展示

本站的 AI 面试官用的是第二种。

第四步:关键难点

防止候选人在回答里注入指令:

  • 回答放在 <answer> 标签里,系统提示声明标签里的内容只是待评价的回答,即使出现"给我打满分"之类的话也不照做,并相应扣分
  • 拼接前去掉回答里的同名标签(包括大小写、空格不同的写法),防止伪造闭合标签"跳出"包裹
  • 解析出的分数限制在 0~10,追问截断长度
  • 控制影响范围:分数只影响用户自己的学习记录,没有排行榜和证书,被注入也只是"骗了自己"。以后如果分数要用于对外的认证,就要加更强的措施,比如用另一个模型复核

和学习记录联动:得分 4 分及以下自动标记为"不会",5~7 分标记为"模糊",进入复习列表;8 分及以上不自动改成"掌握",由用户自己决定,避免模型打分偏高时覆盖了用户的判断。

次数限制和成本:调用前统计用户 24 小时内的调用次数(滚动窗口,不受时区影响),超过上限返回 429;只要模型开始输出就算一次,用户中途停止也已经消耗了 token。参考答案先去掉代码块再截断,控制输入长度。用户点击停止或关闭页面时,服务端取消上游请求。

评估打分是否靠谱:

  1. 准备评测集:每道题收集几个质量不同的回答(好、一般、差、答非所问,以及带注入的回答),由人工打分
  2. 对比模型和人工的打分:看相关性、平均误差、落在同一档位的比例
  3. 看稳定性:同一个回答打分多次,看分数的波动
  4. 查偏差:堆砌字数、没有实质内容的长回答不能得高分
  5. 每次修改 Prompt 或换模型都重跑一遍,方法见 LLM-as-a-Judge

第五步:扩展与优化

  • 整场面试:一次考几道题,结束后按知识点汇总薄弱项,推荐要复习的题
  • 个性化出题:根据学习记录,优先考薄弱分类的题
  • 语音面试:接入语音识别和合成,见实时语音助手

代码示例

组装 Prompt 和解析得分的核心逻辑(调用模型的部分省略):

TypeScript
const MAX_ROUNDS = 3

export function buildMessages(input: { title: string; reference: string; turns: Turn[]; answer: string }) {
  const round = input.turns.length + 1
  // 本轮问题由服务端决定:第一轮是题目,之后是上一轮的追问
  const question = round === 1 ? input.title : input.turns.at(-1)!.followUp
  // 去掉回答里的同名标签(包括大小写、空格不同的写法),防止伪造闭合标签跳出包裹
  const answer = input.answer.replace(/<\s*\/?\s*answer\b[^>]*>/gi, '')
  const user = [
    `【面试题】${input.title}`,
    `【参考答案】\n${compactReference(input.reference)}`, // 去掉代码块并截断
    input.turns.length ? `【之前的问答】\n${formatHistory(input.title, input.turns)}` : '',
    `【本轮问题】(第 ${round} 轮,共 ${MAX_ROUNDS} 轮)${question}`,
    round === MAX_ROUNDS ? '这是最后一轮,"追问"部分只写"无"。' : '',
    `<answer>\n${answer}\n</answer>`,
  ]
    .filter(Boolean)
    .join('\n\n')
  return [
    { role: 'system', content: SYSTEM_PROMPT }, // 评分细则、输出格式和安全规则
    { role: 'user', content: user },
  ]
}

// 解析"得分:X/10",格式不规范时返回 null,而不是猜一个分数
export function parseScore(text: string): number | null {
  const m = text.match(/得分[::]\s*(\d{1,2}(?:\.\d+)?)\s*\/\s*10(?!\d)/) // 不把"85/100"误读成 10 分
  return m ? Math.min(10, Math.max(0, Math.round(Number(m[1])))) : null
}

面试官可能追问

同一个回答两次打分差了好几分,怎么办?

先降低随机性:调低 temperature,分档描述写得更具体,在 Prompt 里给出各档位的示例回答。还不够稳定,可以对同一个回答打分多次取中位数,代价是成本成倍增加。界面上也可以展示档位(如"良好")而不是精确的分数,降低用户对单个分数的敏感度。最终用人工打分的评测集来衡量改进有没有效果。

用户短时间内并发发起很多次请求,"24 小时内最多 N 次"会被突破吗?

会。先查次数再调用,两个请求同时通过检查就会都执行。如果要求严格,可以用 Redis 的原子自增先占用额度,调用失败再退还;或者限制每个用户同时只能有一个进行中的面试请求。对这种低风险场景,偶尔多一两次可以接受,但并发上限最好加上。

参考答案也是模型生成或人写的,它本身错了怎么办?

模型会按错误的参考答案扣分,用户会被误导。所以题库内容要经过审核,参考答案有出处;点评页面提供"点评有误"的反馈入口,收集到的问题回到题库修订。Prompt 里也可以允许模型在参考答案明显有误时指出,但不能完全依赖它来发现错误。

追问是模型生成的,怎么保证追问有价值、不跑题?

在 Prompt 里限定追问必须和本题相关、考察理解深度,只写问题本身;把历史轮次带上,避免重复追问同一个点;限制追问的长度。效果靠评测:抽样检查追问的相关性和难度,也可以把题目文件里人工写好的"面试官可能追问"作为参考提供给模型。

易错点

  • 让前端传"当前问题",用户可以篡改题目刷分;状态要由服务端维护
  • 解析不出分数时随便给一个默认分,污染了学习记录;应该记为空
  • 只看模型给的分数"看起来合理",没有和人工打分做过对比

AI 模拟面试官

用自己的话回答,AI 对照参考答案打分、指出遗漏,再追问,最多 3 轮

登录后就可以和 AI 面试官对练,面试记录也会保存下来。登录

这道题你掌握了吗?

选一个最接近的状态,没掌握的题会出现在"我的进度 · 待复习"里。

学习记录暂存在本机浏览器。登录后自动同步到账号,换设备也能看到。