跑完这篇教程,你会得到一个能直接用的面试模拟器:把目标岗位的 JD 和自己的简历丢进去,它会按「自我介绍 → 项目追问 → 编码题 → 场景设计 → 反问收尾」的顺序一轮轮提问,答完自动给出带证据的评分和一份 Markdown 复盘报告。全程在你自己的终端里跑,代码不到两百行。
和市面上现成的模拟面试产品相比,这套东西的价值在于可改:你可以换提示词、加自己的题库、把评分维度改成目标岗位真正在意的东西,也能把每次的薄弱点积累成错题本,面试前一天拿来热身。
前置条件清单
- 一台能联网的电脑,Windows / macOS / Linux 都可以
- Python 3.10 或更高版本(安装方式与当前版本号以 Python 官方文档为准)
- 一个支持 Chat Completions 风格接口的模型服务,拿到三样东西:接口地址(Base URL)、API Key、模型名
- 终端基本操作:会
cd、会设置环境变量 - 心里有一份目标岗位的 JD 文本,和你自己简历的要点
模型选择上,面试官角色适合用表达自然、追问能力强的模型;评分环节适合用更稳、更听话的模型。两者可以不是同一个,后面会讲怎么分开配置。具体哪个模型便宜好用、上下文多长,以各家官方页面当前的信息为准,这里不做推荐。
步骤一:搭好目录,把密钥关进环境变量
先建目录:
```bash
mkdir -p ai-interview/prompts ai-interview/sessions
cd ai-interview
python -m venv .venv
source .venv/bin/activate # Windows 用 .venv\Scripts\activate
pip install openai
```
openai 这个包是通用客户端,只要服务商兼容这套接口就能用。装哪个版本、要不要额外依赖,以官方文档当前版本为准。
接着建一个 .env.example,把配置写成环境变量,密钥永远不要写进代码:
```bash
.env.example —— 复制成 .env 后填入自己的值
export LLM_BASE_URL="https://你的服务商接口地址/v1"
export LLM_API_KEY="你的密钥"
export LLM_MODEL="服务商文档里的模型名"
```
```bash
cp .env.example .env
编辑 .env 填好三行,然后
source .env
```
顺手加一个 .gitignore,写两行 .env 和 .venv/,避免哪天手滑把密钥推到公开仓库。
配置读进代码里:
```python
config.py
import os
BASE_URL = os.environ.get("LLM_BASE_URL", "")
API_KEY = os.environ.get("LLM_API_KEY", "")
MODEL = os.environ.get("LLM_MODEL", "")
TIMEOUT = 60
if not (BASE_URL and API_KEY and MODEL):
raise SystemExit("请先 source .env,确认三个环境变量都已设置")
```
步骤二:封装一个带重试的调用函数
网络抖动、限流、偶发超时都会打断面试,所以调用层要自带重试。把它单独放一个文件,后面所有环节都复用它:
```python
llm.py
import time
from openai import OpenAI
import config
client = OpenAI(base_url=config.BASE_URL, api_key=config.API_KEY, timeout=config.TIMEOUT)
def chat(messages, temperature=0.7, retries=3):
"""统一的模型调用入口,失败时退避重试。"""
last_err = None
for i in range(retries):
try:
resp = client.chat.completions.create(
model=config.MODEL,
messages=messages,
temperature=temperature,
)
return resp.choices[0].message.content
except Exception as e: # 限流、超时、网络错误都在这里兜住
last_err = e
time.sleep(2 ** i) # 1s, 2s, 4s 退避
raise RuntimeError(f"模型调用连续失败:{last_err}")
```
评分环节建议把 temperature 设成 0 或接近 0,让输出稳定;面试官环节设 0.5~0.8,让追问更自然。
步骤三:写一份「面试官规则」,这是整套东西的核心
代码只是壳,提示词才是面试官本人。新建 prompts/interviewer.txt,把规则写死:
```text
你是一名有十年经验的技术面试官,正在面试一名后端 / 算法方向的候选人。
你必须遵守:
1. 每次回复只包含一个问题,控制在 80 字以内,不要一次抛出多个问题。
2. 面试进行中不要给出答案、不要点评、不要说「很好」「不错」这类空话。
3. 候选人回答含糊时——没有具体数字、没讲技术取舍、没提失败经历——先用一句话点出含糊之处,再追问一个更具体的问题。
4. 同一个话题的追问不超过两轮,之后换话题,避免钻牛角尖。
5. 出编码题时要给出可验证的题目描述和两个边界用例,题目里不要暗示解法。
6. 涉及候选人的项目经历时,优先问「你具体做了什么」「数据是多少」「如果重来会怎么改」。
7. 全程使用中文,语气专业、平实,像真实面试,不要过度鼓励。
```
然后在代码里把 JD 和简历拼到规则后面:
```python
interviewer.py(片段)
import os
from datetime import datetime
from llm import chat
def build_system(jd: str, resume: str) -> str:
rules = open("prompts/interviewer.txt", encoding="utf-8").read()
return (
rules
+ "\n\n【岗位描述】\n" + jd
+ "\n\n【候选人简历要点】\n" + resume
)
```
读文件时务必显式写 encoding="utf-8",Windows 上默认编码不是 UTF-8,中文会乱码。
步骤四:用一个「阶段机」控制面试节奏
只靠模型自由发挥,它很容易在第一个话题上聊十分钟。解决办法是把整场面试切成固定轮次的阶段,到点就注入一句系统提示推动换场:
```python
stages.py
STAGES = [
("开场与自我介绍", 2),
("项目与基础概念追问", 5),
("编码题", 3),
("场景与系统设计", 3),
("候选人反问与收尾", 1),
]
def stage_of(turn_index: int) -> str:
acc = 0
for name, n in STAGES:
acc += n
if turn_index < acc:
return name
return "收尾"
def stage_hint(name: str) -> dict:
return {"role": "system", "content": f"现在进入「{name}」环节。"}
```
轮次数字按你的时间和目标调整:如果只有十五分钟热身,把编码题和场景设计各砍到一轮。
步骤五:主循环,顺便把每轮问答落盘
```python
main.py
import os
from datetime import datetime
from llm import chat
from interviewer import build_system
from stages import stage_of, stage_hint
def save_markdown(path, turns):
lines = [f"# 面试记录 {path}\n"]
for i, t in enumerate(turns, 1):
lines.append(f"## 第 {i} 轮 · {t['stage']}\n")
lines.append(f"面试官: {t['q']}\n")
lines.append(f"我: {t['a']}\n")
with open(path, "w", encoding="utf-8") as f:
f.write("\n".join(lines))
def run_session(jd, resume, max_turns=14):
messages = [
{"role": "system", "content": build_system(jd, resume)},
{"role": "user", "content": "开始面试。"},
]
turns, current_stage = [], None
os.makedirs("sessions", exist_ok=True)
path = f"sessions/{datetime.now():%Y%m%d-%H%M%S}.md"
for i in range(max_turns):
stage = stage_of(i)
if stage != current_stage:
current_stage = stage
messages.append(stage_hint(stage))
print(f"\n===== 环节:{stage} =====")
question = chat(messages, temperature=0.6)
print("\n面试官:" + question)
answer = input("\n我(输入 exit 结束):").strip()
if answer.lower() in {"exit", "quit", "退出"}:
break
messages.append({"role": "assistant", "content": question})
messages.append({"role": "user", "content": answer})
turns.append({"stage": stage, "q": question, "a": answer})
save_markdown(path, turns) # 每轮保存,中途关掉也不丢
return turns, path
if __name__ == "__main__":
jd = open("jd.txt", encoding="utf-8").read()
resume = open("resume.txt", encoding="utf-8").read()
turns, path = run_session(jd, resume)
print(f"\n面试结束,记录已保存到 {path}")
```
注意角色对应关系:AI 说的每句话都记成 assistant,你说的记成 user。这一点搞反,模型会开始替你答题——这是新手最常见的翻车方式。
步骤六:编码题改成「贴代码 + 单独点评」
面试过程中不要点评,但编码题结束后建议单独发一次调用做代码评审,用低温度保证稳定:
```python
review.py
from llm import chat
CODE_REVIEW_PROMPT = """你是一名资深工程师,正在评审候选人现场写的代码。
请按顺序输出四段,每段不超过 120 字:
1. 正确性:给出一个能推翻它或验证它的具体用例;
2. 复杂度:时间与空间,写出推导过程;
3. 工程习惯:命名、边界处理、异常处理中需要改的一点;
4. 一条最值得改的具体建议。
除非候选人要求,不要重写完整代码。"""
def review_code(question: str, code: str) -> str:
messages = [
{"role": "system", "content": CODE_REVIEW_PROMPT},
{"role": "user", "content": f"【题目】\n{question}\n\n【候选人代码】\n{code}"},
]
return chat(messages, temperature=0.1)
```
如果你习惯在本地 IDE 写题,就把代码粘贴进终端时用三反引号包起来,模型解析代码块更稳。
步骤七:评分与复盘报告,要求「说出证据」
评分最大的毛病是模型给谁都打 7 分。破解办法只有一个:强制它引用你的原话作为证据,没有证据就不许给分。
```python
report.py
import json
from llm import chat
RUBRIC = {
"技术深度": "能否讲到实现细节、边界条件与取舍理由",
"表达结构": "结论是否先行,层次是否清楚",
"项目真实性": "数字、职责边界、失败经验是否自洽",
"编码能力": "正确性、复杂度分析、边界处理",
"主动沟通": "是否主动澄清需求,反问质量如何",
}
def build_prompt(turns):
transcript = "\n\n".join(f"问:{t['q']}\n答:{t['a']}" for t in turns)
dims = "\n".join(f"- {k}:{v}" for k, v in RUBRIC.items())
return (
"你是面试评估官。请根据面试记录,对下列维度各打 0-10 的整数分。\n"
"硬性要求:每个分数必须附上一条引用候选人原话的证据;找不到证据就写「无证据」,该维度不超过 5 分。\n"
"只输出 JSON,不要任何多余文字,格式:\n"
'{"scores":[{"dimension":"","score":0,"evidence":"","suggestion":""}],'
'"weakest":"","summary":""}\n\n'
f"维度说明:\n{dims}\n\n面试记录:\n{transcript}"
)
def parse_json(raw: str) -> dict:
text = raw.strip()
if text.startswith("```"):
text = text.split("```")[1]
if text.startswith("json"):
text = text[4:]
return json.loads(text)
def make_report(turns):
raw = chat([{"role": "user", "content": build_prompt(turns)}], temperature=0)
data = parse_json(raw)
lines = ["# 复盘报告\n"]
for s in data["scores"]:
lines.append(f"## {s['dimension']}:{s['score']}/10")
lines.append(f"- 证据:{s['evidence']}")
lines.append(f"- 建议:{s['suggestion']}\n")
lines.append("## 最弱项\n" + data["weakest"])
lines.append("\n## 总评\n" + data["summary"])
return "\n".join(lines)
```
如果服务商支持结构化输出(例如 JSON Schema 强约束),优先用它,能省掉解析兜底。支持情况以官方文档当前版本为准。
步骤八:把薄弱点沉淀成错题本
复盘报告生成后,追加一行到 weak_points.md:日期、岗位、最弱维度、那条证据。下一场面试开始时,把最近三次的错题本内容拼进 system 提示:
```python
def build_system(jd, resume, weak_points=""):
rules = open("prompts/interviewer.txt", encoding="utf-8").read()
base = rules + "\n\n【岗位描述】\n" + jd + "\n\n【候选人简历要点】\n" + resume
if weak_points:
base += (
"\n\n【历史薄弱点(本次请重点考察,但不要直接告诉候选人)】\n"
+ weak_points
)
return base
```
跑过三五场之后,你会明显感觉到它开始往你不熟的地方钻,这比随机问一堆问题有用得多。
常见坑与排错
模型一味夸人。 在面试官规则里明确禁止过程点评,并加一句「如果候选人回答没有具体信息,直接指出并追问」。规则越具体,跑偏越少。
模型自己把答案说了。 通常是角色搞反了,或者提示词里出现了「请给出参考答案」。检查消息里的 assistant / user 归属;参考答案另存一个文件,面试过程中不要读。
追问变复读。 模型会反复问同一个角度。办法是限制追问轮数(规则第 4 条),并在阶段切换时注入新环节提示。
上下文越来越长、越来越慢。 超过一定轮次后,把早期问答压缩成一段摘要,只保留最近几轮全文。具体留多少轮,取决于你用的模型上下文长度。
评分全是中位数。 强制引用原话证据是最有效的约束;另外可以要求它输出「最弱的一项」并说明理由,逼它做区分。
报错就中断。 重试逻辑之外,每轮即时落盘。哪怕进程被杀,前面答过的内容还在 sessions/ 里。
中文乱码。 所有 open() 都加 encoding="utf-8";Windows 终端可以先执行 chcp 65001。
密钥泄露。 只用环境变量,.env 进 .gitignore。如果不小心提交过一次,立刻去服务商后台吊销重建。
终端粘贴长代码出错。 把代码写进 .py 文件,然后让脚本读文件内容,比直接在终端粘贴几百行更稳。
下一步建议
跑通最小版本后,可以按这几个方向扩展:
1. 加语音输入。 接一个语音转文字模型,把 input() 换成录音识别,训练口头表达能力。这类接口的参数与调用方式以各家官方文档为准。
2. 多模型交叉评审。 用两个不同模型分别评分,分数差异大的维度往往就是最值得复盘的地方。
3. 按 JD 批量出题。 把目标公司的岗位描述批量喂进去,先离线生成一份专属题库,再让人工挑出好题固化进提示词。
4. 加「澄清需求」环节。 在编码题开头故意给一个模糊的需求,看你是否会主动问清输入范围、数据规模、性能要求——真实面试里这一项经常决定评价高低。
5. 固定一套热身流程。 面试前用 15 分钟跑一轮:两分钟自我介绍、一道编码题、一次反问练习,然后只看复盘报告的最弱项,不做多余复习。
真正的提升不来自多跑几十场,而来自每次跑完都认真读一遍那份带证据的复盘报告,把最弱的那一条改掉,然后下一场专门去盯它。
