
简介这份压缩包是一套面向计算机专业学生的智能面试系统毕业设计/课程作业源码资源运用自然语言处理、机器学习与计算机视觉等AI技术解决传统面试效率低、评价主观性强、难以量化的问题覆盖从简历筛选到表情识别的常见流程。包内共13个文件容量约468KB以Python源码为核心包含Streamlit可视化面试界面、OpenAI客户端调用逻辑、CSV数据样例以及Dockerfile等部署配置和Markdown文档结构紧凑。目前已有79人学习下载。读者可从中获得一个可运行的面试评估项目骨架既能通过数据文件管理面试题库与候选人输入也能借助NLP模块完成对话理解与情感分析利用视觉模块捕捉非言语信号并附部署配置与使用说明便于本地或云端复现是课程设计或毕业设计的实用参考。1. 智能面试系统拆包这个 zip 里装的不是 CV是一套 LLM 文本面试 MVP拿到「毕设课程作业_智能面试系统.zip」这个名字时多数人会默认里面是简历解析加计算机视觉表情识别的全家桶。拆开之后发现恰恰相反这套系统的主线是「大模型对话式面试」用 Streamlit 搭交互前端用一份 CSV 当题库把候选人作答发给大模型 API再把逐题点评和评分渲染回页面。它没有视频通话也没有人脸情绪分析但它能真实跑通「面试官提问、候选人回答、AI 逐条评估」这条闭环作为毕业设计演示和课程作业绰绰有余。适合两类人急着交 demo 的学生以及想低成本验证 AI 面试流程是否可行的从业者。下面从文件结构、主流程、接口封装、部署配置和评分落盘五个方向逐个拆。2. 项目结构与本地启动八个文件里每一份的职责2.1 文件清单与职责映射先把压缩包里的文件摊开看整体结构不复杂核心代码只有三个 Python 文件。我按自己拆这类项目的习惯整理成一张职责表文件类型职责interview_streamlit.py主程序页面渲染、会话状态管理、面试流程控制oai_client.py封装层大模型 API 调用、流式响应解析、超时与重试utils.py工具层Prompt 组装、评分结果解析、数据预处理settings.py配置层环境变量读取、默认参数、路径常量data/ 与 inputs.csv数据面试题目、候选人和预期考察点Dockerfile部署构建容器镜像fly.toml部署平台运行配置requirements.txt依赖锁定 Python 第三方库这种分层方式很典型主程序不直接碰大模型接口所有 API 调用收敛在 oai_client.py 里Prompt 模板和解析逻辑放 utils.pysettings.py 管全局配置。好处是答辩时可以说清楚「每一层都能单独替换」比如把 oai_client.py 换成其他模型服务商页面代码一行不用改。这个解耦思路本身就是一个加分项。2.2 本地跑通最小链路先把依赖装上再启动页面。requirements.txt 里无非是 streamlit、openai、pandas 这类常用库。打开终端在解压目录下执行cd 毕设课程作业_智能面试系统 python -m venv .venv source .venv/bin/activate pip install -r requirements.txt export OPENAI_API_KEYsk-你的密钥 streamlit run interview_streamlit.py --server.port 8501这里的--server.port指定页面端口默认 8501 被占用时改成 8502 即可。设置OPENAI_API_KEY环境变量是关键settings.py 里通常用os.getenv(OPENAI_API_KEY)读取不设这个变量程序会直接报错。跑起来后浏览器访问http://localhost:8501页面会显示面试欢迎语和「开始面试」按钮。本地跑通的意义在于先验证链路页面按钮 → 调 oai_client.py → 大模型返回 → 渲染回答。如果这一步卡住问题大概率集中在密钥无效、网络不通或依赖版本冲突。建议先跑一个最小 Python 脚本来测试 oai_client.py 是否正常再回到 Streamlit 页面里调。2.3 题库数据与候选人信息inputs.csv 是面试内容的来源格式一般长这样question,type,focus 请做一下自我介绍,opening,表达能力与逻辑性 你为什么应聘这个岗位,motivation,岗位匹配度 介绍一个你最有成就感的项目,behavior,项目深度与技术栈这个 CSV 会被 pandas 读成 DataFrame然后按行作为面试问题发给候选人。type 字段决定问题类型focus 字段传给 utils.py 拼进 Prompt让大模型在点评时围绕考察点打分。注意读文件时用encodingutf-8-sig不然从 Windows 传上来的 CSV 首列会带\ufeff字符问题名称前面多一个看不见的符号后面匹配时容易翻车。这一条经验后面避坑章节会展开说明。3. interview_streamlit.py 主流程session_state 保活、提问轮次与评分输出3.1 为什么必须用 session_state 存会话数据Streamlit 有个让新手头疼的机制页面上的任何按钮点击、输入框回车都会触发整个脚本从上到下重新执行一遍。如果不做状态管理候选人刚回答完第一题脚本一重跑之前的回答就被清空了界面永远停留在第一轮。解决方案是用st.session_state它是 Streamlit 提供的会话级字典在同一个浏览器标签页里持续存活。我拆过不少 Streamlit 项目见过把 LLM 请求直接写在if st.button()里的写法没有 session_state 保护后果是刷新一次页面就触发一次重复计费。这个项目把面试阶段和消息历史都存在st.session_state里结构大致是这样import streamlit as st from oai_client import chat_with_model from utils import build_prompt, parse_evaluation if stage not in st.session_state: st.session_state[stage] welcoming if messages not in st.session_state: st.session_state[messages] [] st.set_page_config(page_titleAI 面试官, layoutcentered) st.title(智能面试系统) if st.session_state[stage] welcoming: st.write(欢迎参加本次 AI 面试请准备好后点击开始。) if st.button(开始面试): st.session_state[stage] questioning first_question load_first_question() st.session_state[messages].append({role: assistant, content: first_question}) st.rerun()逻辑说明stage字段记录当前处于欢迎页还是答题页messages保存对话历史。用户点击「开始面试」后程序从题库取出第一个问题以 assistant 角色写入 messages然后st.rerun()强制页面刷新进入答题阶段。参数说明page_title控制浏览器标签栏标题layoutcentered让页面内容居中视觉上更像聊天界面而不是宽屏后台。3.2 两轮提问与逐题评分输出进入答题阶段后页面上显示当前问题和一个文本框。候选人提交答案系统把历史消息全部发给大模型让模型先点评再出下一题。代码实现是if st.session_state[stage] questioning: for msg in st.session_state[messages]: with st.chat_message(msg[role]): st.markdown(msg[content]) answer st.chat_input(在这里输入你的回答) if answer: st.session_state[messages].append({role: user, content: answer}) prompt build_prompt(st.session_state[messages], current_focus项目深度) with st.spinner(AI 面试官正在思考...): reply chat_with_model(prompt) evaluation, next_question parse_evaluation(reply) st.session_state[messages].append({role: assistant, content: evaluation}) st.session_state[messages].append({role: assistant, content: next_question}) st.rerun()逻辑说明st.chat_message负责把历史消息按角色渲染成气泡st.chat_input是底部输入框用户按回车提交答案后新回答追加进 messages。build_prompt负责把对话历史转成模型所需的 Prompt 结构chat_with_model走 oai_client.py 的封装parse_evaluation从模型的返回里拆出「点评」和「下一题」。参数说明current_focus是当前题的考察点它会拼进 Prompt 让评分更有针对性st.spinner在请求期间显示加载动画但注意它不能和流式输出同时用分辨率会下降这一点在避坑章节单独说。这个两轮循环的设计很聪明模型每次回复包含一段点评和一个新问题页面把点评以 assistant 消息渲染出来新问题再引导候选人继续回答。整个面试流程靠stage字段驱动答完所有题目后把stage改为finished显示汇总评分和落盘按钮。4. oai_client.py 与 utils.py大模型接口的流式应答、超时重试与 Prompt 结构4.1 客户端封装与流式调用oai_client.py 是整个系统的咽喉。它把聊天补全接口包了一层外部只暴露一个chat_with_model(prompt)函数。内部采用流式输出原因是面试场景下候选人需要「即时看到反馈」如果一次性等模型生成完再返回体验会很差。流式写法如下from openai import OpenAI def chat_with_model(prompt, modelgpt-4o-mini, temperature0.7): client OpenAI() response client.chat.completions.create( modelmodel, messagesprompt, temperaturetemperature, streamTrue, ) collected [] for chunk in response: delta chunk.choices[0].delta.content if delta: collected.append(delta) return .join(collected)逻辑说明streamTrue让接口以增量方式返回内容每次 chunk 携带一小段文本循环里用delta.content把片段拼接起来。这样做有两个好处一是用户可以观察到「打字机」式的实时输出缓解等待焦虑二是如果模型中途报错程序能尽快感知到流中断而不是傻等超时。参数说明temperature0.7是默认值面试场景建议放在 0.6 到 0.8 之间太低回答刻板太高容易跑题model参数可以根据自己的 API 权限更换。4.2 超时与重试参数实际面试中网络抖动很常见大模型接口偶尔会返回 5xx 或连接超时。如果客户端没有重试机制候选人会被直接踢出错页。oai_client.py 里带了一层带间隔的重试封装我按常见做法补全成这样import time def chat_with_model_with_retry(prompt, max_retries2, timeout60): for attempt in range(max_retries 1): try: return chat_with_model(prompt, timeouttimeout) except Exception as e: if attempt max_retries: raise time.sleep(2 ** attempt)逻辑说明第一次失败后等待 1 秒重试第二次失败等待 2 秒最多重试 2 次。用指数退避而不是固定间隔是为了避免系统在接口抖动时造成瞬时流量放大。参数说明timeout60表示单次请求最大等待时间max_retries2是重试上限建议不要超过 3否则用户等待时间过长。这里有个隐藏坑流式请求的超时要从开始建连算起如果模型生成速度慢60 秒不一定够线上环境建议把这个值放在 settings.py 里做成可配置项。4.3 Prompt 结构从模板到上下文utils.py 里的build_prompt是决定面试质量的关键。不要简单地把历史消息拼接后直接丢给模型那样模型的点评会缺乏依据。我拆解这个项目的做法是分三段组装def build_prompt(messages, focus): system_instruction { role: system, content: ( 你是一名经验丰富的技术面试官。请根据候选人的回答给出点评 点评不超过 150 字重点围绕 focus 展开。 点评完毕后用「下一题」开头提出一个新的面试问题。 ) } return [system_instruction] messages逻辑说明system_instruction用 system 角色声明面试官身份明确输出格式为「点评 下一题」。后续messages是完整的对话历史模型能看到前面的问题、回答和点评这样新题目能基于已有上下文避免重复提问。参数说明focus来自 inputs.csv 里的考察点字段它会动态替换进 system 指令中让模型围绕特定能力维度打分。这里有个值得注意的设计细节「下一题」这个前缀字符串承担了两个功能它既是给模型的格式约束也是给parse_evaluation的切分符号。utils.py 里用字符串查找来分离点评和问题def parse_evaluation(reply): marker 下一题 if marker in reply: evaluation, next_question reply.split(marker, 1) return evaluation.strip(), marker next_question.strip() return reply.strip(), 面试已结束逻辑说明找到「下一题」标记后左边是点评右边是新问题。如果模型返回里没有这个标记说明模型判断面试已结束此时只返回点评下一题为空。这样做的好处是不依赖 JSON 结构规避了模型偶尔输出不合法 JSON 的问题属于一种比较稳的文本协议。缺点是对 Prompt 格式依赖较强模型如果不按格式输出解析就会失效避坑章节会给出兜底方案。5. 部署与运行避坑清单密钥注入、冷启动与 rerun 轰炸5.1 现象刷新页面一次大模型接口就多一次计费第一次跑起来的时候我一边调试一边刷新页面第二天看账单才发现调用了四十多次接口实际完整面试只走了三轮。原因是页面上的每次st.rerun()都会导致整个脚本重新执行而 Streamlit 会按代码路径依次执行。只要if st.button()之下的代码没有守卫点击按钮后脚本执行到该处就会再次触发接口调用。解决方法是把所有接口调用放在 session_state 条件的保护之下。比如「开始面试」按钮只负责改变stage和初始化 messages真正的模型调用发生在if st.session_state[stage] questioning分支内部并且每次调用前检查该问题是否已经答过。最基本的原则是模型调用必须由用户输入驱动而不是由页面渲染驱动。从那以后我每次在 Streamlit 里接 LLM都会先在意识里过一遍「哪些代码会在重跑时重复执行」。5.2 现象本地秒开部署到云平台后第一个请求卡了半分钟本地跑这套系统很流畅部署到 Fly.io 之后点击「开始面试」等了 20 多秒才出第一句话。原因是云平台默认按需启动实例长时间没有流量时机器会休眠收到请求后需要冷启动拉镜像、启动 Python 进程、加载依赖库、初始化客户端一套流程下来自然慢。再加上数据文件和依赖都打进镜像首次加载额外耗时。解决思路有两个方向。如果预算允许在 fly.toml 里配置最少常驻一个实例[http_service] internal_port 8501 force_https true [machine] min_machines_running 1min_machines_running 1表示始终保持一台机器在线避免冷启动。同时把internal_port设为 8501与 Streamlit 默认端口对齐。另一条路是减小镜像体积Dockerfile 里尽量用精简基础镜像并把 requirements.txt 里的依赖逐条确认去掉用不到的包。依赖越小冷启动越快。这是性价比最高的优化手段比加钱升配更划算。5.3 现象CSV 文件读进来后第一列的名称前面多了一个隐藏字符用 pandas 读 inputs.csv 后打印列名发现第一列变成了\ufeffquestion看起来像乱码实际上是 UTF-8 编码的 BOM 头。Windows 上编辑 CSV 后用 UTF-8 保存很容易带上这个标记。排查时肉眼看不见只有比对列名时才暴露。解决方法是读文件时指定utf-8-sig编码df pd.read_csv(data/inputs.csv, encodingutf-8-sig)utf-8-sig会自动剥离开头的 BOM 字符把列名还原成正常的question。建议在 settings.py 里把编码写成一个常量比如CSV_ENCODING utf-8-sig这样所有读取 CSV 的地方都引用这个常量不会漏改。5.4 现象Docker 构建时 pip 安装依赖超时在 CI 或云平台上构建镜像pip install -r requirements.txt经常卡在下载阶段最后报 ReadTimeoutError。多数情况是默认的 PyPI 源访问不稳定尤其在大文件依赖上更明显。这不是代码问题但会让部署流程卡死。解决办法是给 pip 配上国内镜像源在 requirements.txt 同目录下写一个 pip.conf或者在 Dockerfile 里这样写RUN pip install -r requirements.txt -i https://pypi.tuna.tsinghua.edu.cn/simple-i参数指定安装源构建速度会明显提升。注意这个镜像源只是构建期用运行时不影响。这个坑很隐蔽因为本地开发时 Mac 或 Windows 上 pip 通常能正常下载只有换到云上构建时才暴雷。5.5 现象模型返回里的 JSON 解析直接抛异常系统升级时把 Prompt 改成让模型输出 JSON 格式的评分结果运行几天后偶发json.loads报错。排查发现是模型偶尔会在 JSON 前后加 markdown 围栏或者输出里包含多余逗号甚至直接生成一段散文而不是 JSON。解决方法是先做文本清理再按字典解析。我常用的兜底写法是import json, re def parse_json_response(text): try: return json.loads(text) except json.JSONDecodeError: match re.search(r\{.*\}, text, re.DOTALL) if match: return json.loads(match.group(0)) raise逻辑说明第一层直接json.loads失败后用正则把{...}的子串提取出来再解析。re.DOTALL保证跨行匹配即使模型在 JSON 之间插入了换行也能兜住。如果正则也找不到才选择报错。这个兜底方案不能 100% 解决脏数据但能把失败率降到个位数以内。如果仍频繁失败就需要回到 Prompt 层面把输出格式约束写得更严格甚至用 few-shot 例子示范。6. 把面试评分落成 CSV 记录一次面试变成可回看的复盘文件这套系统跑通后下一步自然是把面试结果沉淀下来。每次面试结束后页面显示总分和分项点评但刷新一下就没有了。我给这个项目补了一个简单的落盘函数把每次面试的记录追加到一个本地 CSV方便后续做数据复盘和答辩展示。import csv import json import time def save_interview_record(candidate_id, messages, final_score, output_pathdata/interview_records.csv): row [ time.strftime(%Y-%m-%d %H:%M:%S), candidate_id, final_score, json.dumps(messages, ensure_asciiFalse), ] with open(output_path, a, newline, encodingutf-8-sig) as f: writer csv.writer(f) writer.writerow(row)调用时放在面试结束的 stage 分支里if st.session_state[stage] finished: save_interview_record( candidate_idst.session_state[candidate_id], messagesst.session_state[messages], final_scorest.session_state[score] )这里的messages用json.dumps序列化后塞进一个单元格保留完整的对话上下文。用ensure_asciiFalse防止中文被转义成\u序列否则复盘时还要再转回来。newline是写入 CSV 的固定写法避免 Windows 下每行之间出现空行。复盘时直接从 CSV 读回 DataFrame可以按日期统计面试次数、按候选人分析题型得分分布甚至可以用另一份 Prompt 把 JSON 字段拆开做自动化分析。这一层数据沉淀虽然简陋但足够支撑答辩时的「数据可视化」和「系统优化」部分。这套 zip 里的代码没有把落盘做完整但核心链路都在补数据导出比从零写要省事太多。我的建议是拿到源码先别急着改把 interview_streamlit.py 从头读一遍弄清楚 stage 切换的每个节点再动手追加功能。有一次我为了赶演示跳过排查直接改 Prompt结果模型频繁输出脏格式debug 了一整晚后来才发现 parse_evaluation 里那句切分逻辑对格式的依赖程度比我预想的高。从那以后我每次改 Prompt都会先跑一遍解析单元测试确认切分逻辑和 Prompt 模板是同步更新的。希望这套拆解能帮你少走几步弯路源码在压缩包里拉下来按第 2 章的命令跑一遍很快能见到第一个 AI 面试官上线。本文还有配套的精品资源点击获取