这套流程能做出什么
走完这篇教程,你会得到一份可以直接拿进面试间的题库文件(JSON 或 CSV),每道题都长这样:
- 题目本身:一句能直接念出口的话,比如"请讲一次你负责的系统在线上出现性能问题,你是怎么定位并解决的"
- 考察维度:对应你岗位能力模型里的哪一格,比如"故障排查"
- 难度分档:1 到 5
- 加分要点:候选人说到哪些具体行为、数据、决策,说明他确实做过
- 扣分信号:哪些回答方式说明他只是听过、没做过
- 评分锚点:1 分、3 分、5 分分别对应什么样的回答水平,每档一句话
- 追问链:2 到 3 个往下挖的问题,防止候选人用套话蒙混
一个人对着 JD 手写这些内容,一个岗位三十道题,通常要花掉大半天到两天。用 AI 生成初稿、人来定稿,时间能压到一两个小时,而且评分标准的颗粒度往往比匆忙手写的更细。
需要说清楚的是:AI 负责"出题 + 起草评分标准",人负责"判断这套标准对不对"。把 AI 产出的东西直接当标准答案用,是这套流程里成本最高的错误。
前置条件清单
动手前,确认这几样都在手边:
1. 一份真实的岗位 JD,越具体越好。描述里如果只有"负责相关业务开发",AI 也只能出泛泛的题。
2. 一个可调用的大模型 API Key,以及对应的调用地址。模型名、计费方式、参数上限以官方文档当前版本为准。
3. Python 环境(3.9 以上比较省事),能 pip install 装包。
4. 一个表格工具:Excel、飞书表格、Google Sheets 都行,用于人工评审。
5. 一位懂这个岗位的评审人,哪怕就是你自己。评分标准必须由懂业务的人过一遍。
6. 合规底线清单。涉及性别、年龄、婚育、籍贯、宗教、健康状况的问题一律不要,这类内容在很多地区会直接带来法律风险。
第 1 步:把 JD 翻译成能力维度表
先别急着让 AI 出题。第一步是人工定出考察维度,这是整个题库的骨架。
把 JD 丢给 AI,让它提炼 5 到 8 个能力维度,然后你自己删改。最后产出一张这样的表,存成 dimensions.csv:
```csv
dimension,weight,description
故障排查,0.25,能从监控、日志、链路数据中定位线上问题根因
系统设计,0.25,能给出有取舍的架构方案并说明边界条件
协作沟通,0.15,能推动跨团队事项落地,处理分歧
代码质量,0.15,对可测试性、可维护性有稳定习惯
业务理解,0.10,能把技术方案和业务指标挂钩
学习迁移,0.10,能把新领域的方法迁移到当前问题
```
权重加起来等于 1。这张表决定了后面每一道题为什么存在,也是最后算总分的依据。
维度数量控制在 8 个以内。维度一多,每个维度出的题就少,评分的信度反而下降。
第 2 步:约定题库的数据结构
在写 prompt 之前先定字段。字段定得越死,后面解析和入库越省事。建议用这套:
| 字段 | 类型 | 说明 |
|---|---|---|
| id | string | 形如 fault-003,前段是维度缩写 |
| dimension | string | 考察维度,必须来自维度表 |
| difficulty | int | 1-5 |
| question | string | 题干,可直接朗读 |
| strong_points | string[] | 加分要点,3-5 条 |
| weak_signals | string[] | 扣分信号,3-5 条 |
| scoring_anchors | object | 键为 "1" / "3" / "5",值为描述 |
| followups | string[] | 追问,2-3 个 |
| time_minutes | int | 建议用时 |
为什么要留 weak_signals?因为面试里更常见的情况是"听起来很流畅,但一追问就空了"。提前把"空"长什么样写下来,面试官才有统一的判断依据。
第 3 步:写出题 Prompt,把评分标准一起要出来
这是整篇教程里最值得反复打磨的一段。可以直接复制下面这个模板,把方括号里的内容替换掉:
```text
你是一位资深【岗位名称】面试官,正在为【级别,如:三年经验】候选人设计面试题。
岗位背景:
【JD 摘要,两三句话】
本次考察维度:{dimension}
维度说明:{description}
请设计 {n} 道面试题,要求:
1. 全部采用行为面试法,问候选人"过去实际做过什么"。
禁止出现"你会怎么做""你怎么看""如果……"这类假设性问题。
2. 输出一个 JSON 数组,每个元素包含字段:
- question:题干,口语化,能直接念出来
- difficulty:1 到 5 的整数
- strong_points:3 到 5 条,每条不超过 20 字,描述候选人回答里出现的具体行为、数字或决策
- weak_signals:3 到 5 条,描述可观察到的空泛回答特征
- scoring_anchors:对象,必须包含 "1"、"3"、"5" 三档,每档一句话,
描述候选人达到什么程度给这个分
- followups:2 到 3 个追问,用于核实细节
- time_minutes:建议面试时长,整数
3. 评分锚点只能写可观察的行为或结果。
禁止使用"优秀""良好""思路清晰""有深度"这类无法验证的词。
一档锚点里至少要包含一个动作动词或一个可量化的结果。
4. 不要涉及性别、年龄、婚育、籍贯、宗教、健康等个人特征。
5. 已有题目如下,新题不要与之重复:
{existing_questions}
只输出 JSON,不要任何解释文字。
```
模板里那句"已有题目如下"很关键。分批生成时把前面批次的题干回传,能明显压低重复率。
第 4 步:批量生成并结构化落盘
按维度循环调用,一次出一个维度的题,比一次性要求"给我全部题"稳定得多。下面是一段可用的脚本:
```python
import json
import os
import time
from openai import OpenAI # 以官方文档当前版本为准
client = OpenAI(
api_key=os.environ["LLM_API_KEY"],
base_url=os.environ.get("LLM_BASE_URL"),
)
MODEL = os.environ["LLM_MODEL"]
PROMPT_TEMPLATE = open("prompt.txt", encoding="utf-8").read()
def generate(dimension: str, description: str, n: int, existing: list[str]) -> list[dict]:
prompt = PROMPT_TEMPLATE.format(
dimension=dimension,
description=description,
n=n,
existing_questions="\n".join(existing) or "(暂无)",
)
resp = client.chat.completions.create(
model=MODEL,
messages=[{"role": "user", "content": prompt}],
temperature=0.8,
response_format={"type": "json_object"},
)
text = resp.choices[0].message.content.strip()
text = text.removeprefix("``json").removeprefix("`").removesuffix("``")
data = json.loads(text)
return data if isinstance(data, list) else data.get("questions", [])
def main():
dims = [
{"name": "故障排查", "description": "能从监控、日志中定位根因"},
{"name": "系统设计", "description": "能给出有取舍的架构方案"},
]
results, existing = [], []
for d in dims:
for attempt in range(3):
try:
items = generate(d["name"], d["description"], 6, existing)
for i, item in enumerate(items, 1):
item["id"] = f"{d['name']}-{i:03d}"
item["dimension"] = d["name"]
results.extend(items)
existing.extend(x["question"] for x in items)
break
except Exception as exc: # JSON 截断、网络抖动都落这里
print(f"{d['name']} 第 {attempt + 1} 次失败:{exc}")
time.sleep(3)
with open("question_bank.jsonl", "w", encoding="utf-8") as f:
for item in results:
f.write(json.dumps(item, ensure_ascii=False) + "\n")
print(f"共生成 {len(results)} 道题")
if __name__ == "__main__":
main()
```
几个细节值得留意:
temperature调高一点(0.7 到 1.0),题目多样性会更好;生成评分锚点时反而可以调低。- 明确要求 JSON 输出模式,能省掉大量解析麻烦。不同服务商对这个参数的支持方式不一样,以官方文档当前版本为准。
- 解析失败就重试,别让一次网络抖动毁掉整批任务。
第 5 步:机器初筛,把明显不合格的挑出来
生成完先跑一遍自动检查,减少人工评审的负担:
```python
import json
from difflib import SequenceMatcher
REQUIRED = ["id", "dimension", "difficulty", "question",
"strong_points", "weak_signals", "scoring_anchors",
"followups", "time_minutes"]
def similar(a: str, b: str) -> float:
return SequenceMatcher(None, a, b).ratio()
def check(path: str):
items = [json.loads(line) for line in open(path, encoding="utf-8")]
problems = []
for i, item in enumerate(items):
missing = [k for k in REQUIRED if k not in item]
if missing:
problems.append((item.get("id", i), f"缺字段:{missing}"))
anchors = item.get("scoring_anchors", {})
if set(anchors) < {"1", "3", "5"}:
problems.append((item.get("id", i), "评分锚点不全"))
for level, desc in anchors.items():
if len(str(desc)) < 8:
problems.append((item.get("id", i), f"{level} 档描述过短"))
if not 1 <= item.get("difficulty", 0) <= 5:
problems.append((item.get("id", i), "难度越界"))
for i in range(len(items)):
for j in range(i + 1, len(items)):
r = similar(items[i]["question"], items[j]["question"])
if r > 0.75:
problems.append((items[i]["id"], f"与 {items[j]['id']} 相似度 {r:.2f}"))
for pid, msg in problems:
print(pid, msg)
print(f"共 {len(problems)} 条待处理")
check("question_bank.jsonl")
```
相似度阈值 0.75 只是个起点,跑一批之后按实际情况调。
第 6 步:人工评审,重点看评分标准
自动检查只能拦住格式问题,内容质量必须人来判。建议逐题问四个问题:
1. 这个问题在面试里问得出口吗? 有的 AI 题干又长又书面,念出来很别扭,改成口语。
2. 评分锚点能不能区分出"做过"和"没做过"? 比如 5 分档写"能说出完整方案",这个太虚。改成"能说出具体采用了哪种方案、当时对比过的另一种方案是什么、最终效果指标怎么变的"。
3. 加分要点是不是在描述行为? 出现"思路清晰""表达流畅"就删掉,换成"能主动说出方案被否决的原因"。
4. 难度分布合理吗? 六道题全是最难的,面试前三十分钟就把候选人打崩了,拿不到有效信息。
导出成 CSV 给面试官用:
```python
import csv
import json
items = [json.loads(line) for line in open("question_bank.jsonl", encoding="utf-8")]
with open("interview_sheet.csv", "w", newline="", encoding="utf-8-sig") as f:
w = csv.writer(f)
w.writerow(["题号", "维度", "难度", "题目", "1分锚点", "3分锚点", "5分锚点",
"追问1", "追问2", "建议时长", "实得分", "备注"])
for it in items:
a = it["scoring_anchors"]
fu = it["followups"] + [""] * (2 - len(it["followups"]))
w.writerow([it["id"], it["dimension"], it["difficulty"], it["question"],
a["1"], a["3"], a["5"], fu[0], fu[1],
it["time_minutes"], "", ""])
```
utf-8-sig 这个编码别写错,否则用 Excel 打开会乱码。
第 7 步:用真实面试数据回流校准
题库不是一次做完就锁死的。面了十几个人之后,把每题的打分汇总起来看两件事:
- 均分和标准差。所有候选人都拿 4 分的题,区分度接近零,考虑替换或降级为暖场题。
- 和总分的相关性。某题得分和最终录用结论明显不相关的,说明它可能没考察到真正重要的东西。
```python
import pandas as pd
df = pd.read_csv("scores.csv") # 列:candidate_id, question_id, dimension, score
stats = df.groupby("question_id")["score"].agg(["mean", "std", "count"])
stats = stats[stats["count"] >= 5].sort_values("std")
print(stats.head(10)) # 标准差小的排前面,优先复查
```
加权总分的算法也一并固化下来,避免每个面试官口径不同:
```python
WEIGHTS = {"故障排查": 0.25, "系统设计": 0.25, "协作沟通": 0.15,
"代码质量": 0.15, "业务理解": 0.10, "学习迁移": 0.10}
def weighted_score(rows):
"""rows: [{'dimension': ..., 'score': 1-5}, ...]"""
per_dim = {}
for r in rows:
per_dim.setdefault(r["dimension"], []).append(r["score"])
total, wsum = 0.0, 0.0
for dim, scores in per_dim.items():
w = WEIGHTS.get(dim, 0)
total += (sum(scores) / len(scores)) * w
wsum += w
return round(total / wsum, 2) if wsum else 0.0
```
常见坑与排错
AI 出的题全是"你会怎么设计一个 XXX"
这是模型偷懒的默认姿势。这类问题候选人可以凭想象回答,面试官无法验证真假。在 prompt 里明确禁止假设句,并加一条"题干里必须出现'请讲一次''描述一个你亲自参与的'这类引导语"。
评分标准写成"优秀 / 良好 / 一般"
同样要在 prompt 里硬性约束:"每档锚点必须包含至少一个动作动词或可量化结果"。生成后可以用脚本扫一遍,出现"清晰""深入""优秀"等词就标红。
JSON 解析老失败
常见原因是模型输出被 token 上限截断,或者前面加了一段解释文字。应对方式:要求只输出 JSON;开启 JSON 输出模式;把单次生成的题目数降到 5 到 8 道;解析失败时重试而不是硬解析。
题目之间高度重复
分批生成时务必把已有题干回传给模型;生成完再跑一次相似度检查。同一个维度下相似度超过阈值的,保留分更高的那道。
难度全部堆在中间
在 prompt 里显式给出分布要求,比如"难度 1-5 的题目数量分别为 1、2、2、1、0"。
把 AI 的答案要点当成标准答案
模型对具体技术事实可能存在错误表述。评分锚点里如果涉及具体协议、具体算法细节,必须由懂这个领域的评审人核对。一份标注"待评审"的题库,比一份看起来完备但没人看过的题库安全得多。
合规问题漏网
建一个关键词黑名单(婚育、年龄、籍贯、宗教等),生成后自动扫一遍,再人工过一遍。这一步不能省。
下一步建议
做版本管理。题库按岗位和级别分目录,每次改动留记录。面试官反馈某道题问不出东西,能追溯到是哪一版加的。
接向量检索去重。题目数量上到几百道以后,字符串相似度就不够用了。把题干转成向量存进向量库,新增题目先做一次近邻检索。
把评分表接进面试记录。面试官当场在表格里打分、填备注,数据自然沉淀下来,第 7 步的统计就不用再手工整理。
做面试官打分校准。找一段录制的模拟回答,让几位面试官各自打分,看彼此偏差有多大。偏差大的维度,回头改评分锚点。
扩展到其他题型。同一套结构可以套到笔试题、案例分析题、take-home 作业的评分表上。真正通用的不是题目,是"可观察行为 + 分档锚点"这套写法。
