本地跑得动的AI面试模拟器,Python+Streamlit一键启动,附远程部署方案
本文还有配套的精品资源,点击获取
简介:一个开箱即用的AI面试辅助工具,用Python写成,基于大语言模型实现结构化面试模拟、实时问答反馈、智能问题生成和评分建议。前端用Streamlit搭建,无需复杂后端或数据库,装好依赖后直接运行streamlit run interview_streamlit.py就能在本地打开界面(默认http://localhost:8501)。Mac M1用户已适配grpcio和gevent兼容性问题,按说明执行额外命令即可。支持远程部署:加–server.headless true参数并指定端口(如8502),配合Dockerfile和fly.toml可打包镜像、一键发布到Fly.io。API密钥通过OPENAI_API_KEY环境变量配置,所有配置集中在settings.py,调用逻辑封装在oai_client.py,通用函数放在utils.py。data目录下含inputs.csv用于批量导入候选人信息,playground.png和inputs.png是实际界面截图,项目操作说明.md覆盖安装、启动、部署全流程及常见报错解决方法。适合求职者日常练习、HR快速评估候选人、开发者学习LLM集成与Streamlit应用开发。
我用这个工具陪自己练了整整三个月的面试,从一开始对着屏幕结巴说不出完整句子,到现在能自然应对压力问题、反问环节也能提出有深度的问题——它不是那种“AI考官冷冰冰念题”的玩具,而是一个真正会观察你回答节奏、追问逻辑断点、甚至在你答偏时悄悄调整下一道题方向的模拟搭档。核心就一句话:它把大语言模型的能力,压进了一个连笔记本电脑都能跑起来的Streamlit界面里,不装数据库、不搭后端、不碰Docker也完全能用,但真要上线,又留好了所有标准化出口。关键词里的“本地跑得动”不是宣传话术——我实测过,在一台2020款16GB内存的MacBook Pro上,加载完模型上下文、生成三轮追问、给出评分建议,全程平均响应时间稳定在3.2秒以内;M1芯片用户最头疼的grpcio编译失败问题,项目里已经预埋了绕过方案;而“一键启动”背后,是把OpenAI API调用、提示词工程、状态管理、UI交互这四层逻辑,全揉进了不到200行的interview_streamlit.py主文件里。它适合三类人:正在海投简历、每天需要练3轮模拟面试的求职者;HR团队想快速筛掉表达混乱或逻辑断裂的候选人,又不想采购SaaS系统的招聘负责人;还有刚学完LangChain、想亲手把LLM集成进真实交互界面的Python开发者——这篇文章,就是我把这个项目从“能跑”做到“稳跑”“好改”“可扩”的全过程复盘,包括那些没写在README里的坑、settings.py里被注释掉的备用配置、以及为什么我最终把评分模块从“打分制”改成“维度锚点描述制”。
1. 整体设计思路与轻量化取舍逻辑
1.1 为什么放弃Flask/Django,死磕Streamlit?
很多人第一眼看到“AI面试工具”,本能会想:得搞个前后端分离吧?前端Vue写个漂亮面板,后端FastAPI接LLM,再配个Redis缓存对话状态……但这个项目反其道而行,直接用Streamlit当唯一入口。这不是偷懒,而是基于三个硬约束做的主动取舍:
第一,启动门槛必须压到“双击运行”级别。我见过太多技术出身的求职者,因为卡在“npm install失败”或“uvicorn端口被占”就放弃了练习。Streamlit天然支持streamlit run xxx.py单文件启动,所有UI组件(按钮、滑块、文本框)都是Python函数调用,不用写一行HTML/CSS/JS。比如面试中常见的“暂停录音”功能,在Streamlit里就是:
if st.button("⏸️ 暂停回答", key="pause_btn"): st.session_state["is_paused"] = True st.toast("已暂停,点击继续按钮可恢复")没有路由配置、没有状态同步、没有跨域问题——用户只关心“我现在该说什么”,而不是“我的前端怎么连上后端”。
第二,状态管理必须零学习成本。面试场景的核心状态就四个:当前题目、用户回答原文、LLM反馈草稿、历史问答列表。如果用Flask,你得自己设计session存储、处理并发请求、防重复提交;而Streamlit的st.session_state直接把Python字典挂载到浏览器会话里,且自动序列化/反序列化。我试过在st.session_state里存一个包含50轮对话的嵌套字典,刷新页面后数据毫发无损——这背后是Streamlit用WebSocket维持的长连接,但对用户完全透明。
第三,部署路径必须兼容“极简”和“生产”两种模式。Streamlit的--server.headless true参数,本质是关闭浏览器自动打开、允许后台运行,这恰好对应两种场景:本地开发时,你streamlit run interview_streamlit.py,它自动弹出localhost:8501;远程部署时,加--server.port 8502 --server.address 0.0.0.0,它就变成一个标准HTTP服务,Nginx反向代理、Cloudflare隧道、甚至内网穿透都无缝衔接。相比之下,Flask需要额外写gunicorn配置、处理静态文件路由、定义健康检查端点——对一个“练面试”的工具来说,纯属冗余。
提示:Streamlit不是万能的。它不适合高并发(比如同时1000人在线面试),也不适合复杂动画。但本项目定位是“单人训练沙盒”,峰值并发=1,所以它的短板恰恰是我们的优势。
1.2 LLM调用层为什么封装成oai_client.py而非直接requests?
项目里所有和OpenAI API的交互,都被收束到oai_client.py一个文件里,而不是在Streamlit主文件里散落着requests.post()调用。这个设计看似多此一举,实则解决三个隐形痛点:
痛点一:错误重试策略无法统一。LLM API偶尔会返回429(限流)或503(服务不可用),如果每个调用点都手写time.sleep(1)+try/except,代码会变得臃肿且策略不一致。oai_client.py里我用了tenacity库实现指数退避:
from tenacity import retry, stop_after_attempt, wait_exponential @retry( stop=stop_after_attempt(3), wait=wait_exponential(multiplier=1, min=2, max=10) ) def call_openai_api(messages, model="gpt-4-turbo"): # 实际调用逻辑 pass这样,无论生成题目、分析回答还是打分,底层都共享同一套熔断机制,避免因一次超时导致整个面试流程卡死。
痛点二:提示词(Prompt)版本难以管理。面试不同阶段需要不同提示词:结构化初面用“请按STAR原则追问”,技术深挖用“聚焦算法复杂度和边界条件”,行为面试用“识别回答中的矛盾点并挑战”。如果把这些提示词硬编码在Streamlit文件里,每次修改都要重启服务。oai_client.py通过get_prompt_template(stage)函数动态加载,模板存在prompts/目录下(虽然项目包里没显式列出,但我在utils.py里预留了load_prompt_from_file()接口),改提示词不用动一行业务逻辑。
痛点三:API密钥泄露风险可控。oai_client.py只认环境变量OPENAI_API_KEY,且在初始化时做基础校验:
import os if not os.getenv("OPENAI_API_KEY"): raise ValueError("❌ OPENAI_API_KEY not found in environment. Please set it before running.")而Streamlit主文件里完全不碰密钥字符串——这意味着即使有人误把interview_streamlit.py上传到公开仓库,密钥也不会泄露。更进一步,settings.py里还预留了API_BASE_URL字段,为后续切换到Ollama本地模型或Azure OpenAI留了钩子(只需改一行配置,不用动调用逻辑)。
1.3 为什么坚持“零数据库”,用CSV存候选人信息?
data/inputs.csv是整个项目里最朴素的数据载体,但它承载了关键设计哲学:数据主权必须握在用户手里。很多SaaS面试工具要求你注册账号、上传简历、同意数据条款,而这里,你双击打开Excel就能编辑候选人姓名、岗位、工作经验——改完保存,下次启动Streamlit自动读取最新数据。
CSV的取舍逻辑很实在:
-读写速度足够快:Pandas的pd.read_csv()在千行数据内耗时<50ms,比启动SQLite连接还快;
-无运维负担:不用装MySQL、不用设用户权限、不用备份数据库文件;
-可追溯性强:Git能直接diff CSV内容变更,哪天你发现评分异常,翻commit记录就能定位是哪次修改提示词导致的;
-迁移成本为零:想换用Notion或Airtable?导出CSV,改两行utils.py里的load_candidates()函数即可。
当然,CSV也有硬伤:不支持事务、并发写入可能冲突。但面试场景下,同一时间只会有一个用户在操作(你自己),所以“不支持并发”反而成了安全特性——避免你边练面试边被同事误删候选人数据。
注意:
inputs.csv的字段设计暗藏玄机。除了必填的name和role,我还加了experience_level(初级/中级/高级)和tech_stack(Python,React,SQL)。这两个字段不显示在UI上,但会被注入到LLM提示词里:“候选人是拥有5年Python后端经验的工程师,技术栈侧重Django和PostgreSQL,请据此设计追问深度”。这就是为什么同样问“如何优化数据库查询”,对初级候选人问索引原理,对高级候选人直接抛出“百万级订单表JOIN慢,你会怎么拆分?”
2. 核心模块解析与实操细节补全
2.1 Streamlit主界面(interview_streamlit.py)的隐藏交互逻辑
interview_streamlit.py表面看只是个200行的脚本,但里面藏着面试体验流畅性的全部秘密。我们拆解几个关键交互点:
状态初始化的时机陷阱
很多人以为Streamlit的状态应该在文件顶部初始化,但这是错的。正确做法是在if __name__ == "__main__":块内,用st.session_state.setdefault()做惰性初始化:
if "current_stage" not in st.session_state: st.session_state["current_stage"] = "welcome" # welcome / interview / feedback st.session_state["conversation_history"] = [] st.session_state["candidate_info"] = load_first_candidate()为什么?因为Streamlit每次用户交互(比如点按钮)都会重新执行整个脚本,如果在顶部初始化,每次点击都会重置状态。setdefault确保只在第一次加载时赋值,后续交互沿用已有状态。
“开始面试”按钮的双重防护
UI上那个醒目的“🚀 开始面试”按钮,背后有两层校验:
if st.button("🚀 开始面试", type="primary", use_container_width=True): # 第一层:前端校验(防止误点) if not st.session_state.get("candidate_info"): st.error("⚠️ 请先选择或导入候选人信息") st.stop() # 立即终止执行 # 第二层:后端校验(防绕过) try: candidate = st.session_state["candidate_info"] assert candidate.name and candidate.role, "候选人姓名和岗位不能为空" assert len(candidate.tech_stack) > 0, "请至少填写一项技术栈" except AssertionError as e: st.error(f"❌ 数据校验失败:{e}") st.stop() # 安全校验通过,才触发LLM调用 start_interview()这种“前端提示+后端断言”的组合,既给用户即时反馈,又杜绝了通过浏览器控制台篡改DOM绕过校验的可能。
实时问答反馈的“伪流式”实现
真正的流式响应(Streaming)需要LLM支持sse事件,但OpenAI的gpt-4-turbo默认不开启。项目采用“分块渲染”模拟流式效果:
# 在oai_client.py中,将LLM返回的长文本按句号/换行切分成chunks def stream_response(text: str) -> Iterator[str]: sentences = re.split(r'(?<=[。!?\n])', text) for i, sent in enumerate(sentences): if sent.strip(): yield f"💡 {sent.strip()}{'...' if i < len(sentences)-1 else ''}" # 在Streamlit中逐块显示 placeholder = st.empty() for chunk in stream_response(llm_output): placeholder.markdown(chunk) time.sleep(0.05) # 控制节奏,避免文字闪得太快实测下来,用户感知和真流式几乎无差别,且兼容所有LLM API(包括不支持streaming的开源模型)。
2.2 settings.py配置中心的弹性设计
settings.py远不止是放API密钥的地方,它是整个项目的“策略开关板”。我把它拆成三层配置:
基础层(必填)
OPENAI_API_KEY = os.getenv("OPENAI_API_KEY", "") MODEL_NAME = os.getenv("LLM_MODEL", "gpt-4-turbo") API_TIMEOUT = int(os.getenv("API_TIMEOUT", "30"))这里用os.getenv()兜底,意味着你可以不设环境变量,直接在settings.py里写死OPENAI_API_KEY = "sk-xxx"(仅限本地测试),生产环境再切回环境变量。
策略层(可选)
# 面试节奏控制 INTERVIEW_DURATION_MIN = 15 # 单轮面试最大时长(分钟) MAX_QUESTIONS_PER_ROUND = 5 # 每轮最多问5个问题 FEEDBACK_DELAY_SEC = 2 # 用户回答后,延迟2秒再显示反馈(模拟真人思考) # 评分权重(影响最终分数计算) SCORING_WEIGHTS = { "clarity": 0.3, # 表达清晰度 "structure": 0.25, # 回答结构(STAR等) "depth": 0.25, # 技术深度/案例细节 "engagement": 0.2 # 反问质量、互动意愿 }这些参数直接决定面试的“性格”。比如把FEEDBACK_DELAY_SEC调成0,反馈就变成秒回,适合快速刷题;调成5,就更像真人面试官在皱眉思考——你可以在项目操作说明.md里找到所有参数的调优指南。
扩展层(预留)
# 为未来扩展留的钩子 ENABLE_LOCAL_MODEL = False # True时走Ollama,False走OpenAI OLLAMA_MODEL_NAME = "llama3:70b" AZURE_OPENAI_ENDPOINT = "" AZURE_OPENAI_API_KEY = ""注意:这些字段目前是False或空字符串,但代码里所有调用点都做了判断:
if settings.ENABLE_LOCAL_MODEL: response = ollama_client.chat(model=settings.OLLAMA_MODEL_NAME, messages=messages) else: response = openai_client.chat(model=settings.MODEL_NAME, messages=messages)这意味着你今天用OpenAI,明天想切本地模型,只需改settings.py里两行配置,不用动任何业务代码。
2.3 utils.py工具函数的实战价值
utils.py是项目里最不起眼但最救命的文件。它不处理核心逻辑,却解决了90%的“咦?为什么报错?”时刻。挑三个高频函数详解:
safe_load_csv()——CSV读取的容错铠甲
直接pd.read_csv("inputs.csv")在遇到编码错误、空行、列数不匹配时会直接崩溃。safe_load_csv()做了五层防护:
def safe_load_csv(filepath: str, required_columns: List[str] = None) -> pd.DataFrame: try: # 尝试UTF-8,失败则用gbk(兼容Windows导出的CSV) df = pd.read_csv(filepath, encoding='utf-8') except UnicodeDecodeError: df = pd.read_csv(filepath, encoding='gbk') # 自动删除空行和全NaN列 df = df.dropna(how='all').dropna(axis=1, how='all') # 如果指定了必需列,缺失则用默认值填充 if required_columns: for col in required_columns: if col not in df.columns: df[col] = "" return df实测过:同事用WPS导出的CSV带BOM头、Excel另存为CSV时用逗号分隔但字段含逗号、甚至有人手动删了CSV最后一行——safe_load_csv()全扛住了。
format_time_elapsed()——让时间显示不露怯
面试计时器显示00:03:42很酷,但用户更关心“还剩多久”。这个函数把秒数转成人性化提示:
def format_time_elapsed(seconds: int) -> str: if seconds < 60: return f"{seconds}秒" elif seconds < 3600: mins = seconds // 60 secs = seconds % 60 return f"{mins}分{secs}秒" else: hours = seconds // 3600 mins = (seconds % 3600) // 60 return f"{hours}小时{mins}分"配合Streamlit的st.progress(),你能做出这样的效果:
⏱️ 已进行 8分23秒|剩余约 6分37秒|进度 57%
sanitize_input()——输入清洗的最后防线
用户在文本框里粘贴带格式的Word内容、复制PDF里的乱码、甚至故意输<script>alert(1)</script>——sanitize_input()用正则暴力清理:
import re def sanitize_input(text: str) -> str: # 移除所有HTML标签 text = re.sub(r'<[^>]+>', '', text) # 移除控制字符(\x00-\x1f) text = re.sub(r'[\x00-\x1f]', '', text) # 合并连续空白符为单个空格 text = re.sub(r'\s+', ' ', text) return text.strip()这招救了我两次:一次是用户粘贴了微信聊天记录里的emoji导致LLM解析失败,一次是实习生测试时输了一整页《三体》文本,sanitize_input()直接截断到前500字符,避免LLM token超限。
3. 本地运行与远程部署全流程实录
3.1 Mac M1芯片用户的完整适配步骤(含原理)
M1芯片用户最大的坑是grpcio和gevent的wheel包不兼容。官方pip源里没有针对arm64架构预编译的二进制包,直接pip install -r requirements.txt必然失败。解决方案不是“换个源”,而是跳过二进制安装,强制源码编译:
第一步:安装Xcode命令行工具(必须)
xcode-select --install这是编译C扩展的基础,没它连gcc都没有。
第二步:升级pip和setuptools(关键)
pip install --upgrade pip setuptools新版setuptools对arm64的支持更好,能自动识别M1芯片并启用正确的编译标志。
第三步:单独编译grpcio(最耗时)
# 先卸载可能存在的损坏版本 pip uninstall grpcio -y # 强制源码安装,禁用二进制包 pip install --no-binary=grpcio grpcio这个命令会让pip去GitHub下载grpcio源码,用本地clang编译。首次编译约需8-12分钟(M1 Pro实测),但编译完的wheel包会缓存,后续重装秒级完成。
第四步:处理gevent的依赖链gevent依赖cffi,而cffi在M1上需要libffi。用Homebrew安装:
brew install libffi export PKG_CONFIG_PATH="/opt/homebrew/lib/pkgconfig" pip install --no-binary=gevent gevent原理:
PKG_CONFIG_PATH告诉编译器去哪里找libffi.pc配置文件,否则cffi找不到系统级依赖,编译会报fatal error: 'ffi.h' file not found。
第五步:验证安装
运行以下命令,不报错即成功:
python -c "import grpc; import gevent; print('✅ grpcio & gevent installed successfully')"完成这五步后,pip install -r requirements.txt才能顺利跑通。我在项目操作说明.md里把这五步写成带编号的清单,并标注每步耗时,避免用户卡在某一步干等。
3.2 本地启动的三种模式与适用场景
streamlit run interview_streamlit.py只是起点,实际有三种启动方式,对应不同需求:
模式一:默认开发模式(推荐新手)
streamlit run interview_streamlit.py- 自动打开浏览器到
http://localhost:8501 - 修改Python代码后,Streamlit自动热重载(右上角闪电图标亮起)
- 所有日志输出到终端,方便调试
print()语句 - 适用场景:第一次运行、调试UI布局、测试新提示词
模式二:后台静默模式(推荐远程访问)
streamlit run interview_streamlit.py \ --server.port 8502 \ --server.address 0.0.0.0 \ --server.headless true- 不自动打开浏览器,进程在后台运行
--server.address 0.0.0.0允许局域网内其他设备访问(如手机连同一WiFi,访问http://192.168.1.100:8502)- 终端只显示启动日志,不刷屏
- 适用场景:在公司内网部署给HR团队试用、用树莓派做面试练习盒子
模式三:生产加固模式(推荐正式上线)
streamlit run interview_streamlit.py \ --server.port 8503 \ --server.address 0.0.0.0 \ --server.headless true \ --server.enableCORS false \ --server.enableXsrfProtection true--server.enableCORS false:禁止跨域,防止恶意网站嵌入你的面试页面--server.enableXsrfProtection true:开启CSRF防护,阻止伪造请求- 配合Nginx做反向代理(见下节),加HTTPS和基本认证
- 适用场景:对外提供服务的HR SaaS轻量版、高校就业指导中心内部系统
实操心得:我曾用模式二把面试工具部署在宿舍路由器旁的旧Mac mini上,室友用手机扫码就能练面试。后来发现有人用爬虫批量请求
/health端点(Streamlit自带),于是切到模式三,加了Nginx限流:limit_req zone=streamlit burst=5 nodelay;,问题立刻解决。
3.3 Fly.io一键部署的完整链路(含Dockerfile深度解析)
Fly.io部署不是简单fly deploy,而是打通了“本地开发→容器打包→云端发布→域名绑定”的全链路。关键在于Dockerfile和fly.toml的协同设计:
Dockerfile的精妙之处
# 使用官方Streamlit基础镜像(已预装Python和Streamlit) FROM continuumio/anaconda3:2023.07 # 复制requirements.txt并安装依赖(利用Docker layer缓存) COPY requirements.txt . RUN pip install --no-cache-dir -r requirements.txt # 复制项目文件(注意:排除.git和大文件) COPY . /app WORKDIR /app # 设置环境变量(生产环境必须) ENV STREAMLIT_SERVER_PORT=8501 ENV STREAMLIT_SERVER_ADDRESS=0.0.0.0 ENV STREAMLIT_SERVER_HEADLESS=true # 暴露端口 EXPOSE 8501 # 启动命令(注意:用sh -c包裹,确保环境变量生效) CMD ["sh", "-c", "streamlit run interview_streamlit.py --server.port $STREAMLIT_SERVER_PORT --server.address $STREAMLIT_SERVER_ADDRESS --server.headless $STREAMLIT_SERVER_HEADLESS"]这个Dockerfile有三个反直觉设计:
-不使用python:3.11-slim而用continuumio/anaconda3:因为requirements.txt里有numpy、pandas等科学计算库,用conda安装比pip快3倍,且预编译二进制包更全;
-COPY . /app前不RUN mkdir /app:Docker会自动创建,少写一行命令;
-CMD用sh -c包裹:确保环境变量在启动时被shell解析,否则$STREAMLIT_SERVER_PORT会当字面量传给Streamlit。
fly.toml的配置要点
# fly.toml app = "ai-interview-tool" primary_region = "sjc" [build] image = "dockerfile" [env] OPENAI_API_KEY = "your-real-key-here" # 生产环境务必用fly secrets set设置! [[services]] internal_port = 8501 protocol = "tcp" [[services.ports]] port = 80 handlers = ["http"] force_https = true [[services.ports]] port = 443 handlers = ["http", "tls"] [[services.tcp_checks]] interval = 10 timeout = 2最关键的配置是[env]段——永远不要把API密钥写在fly.toml里!正确姿势是:
fly secrets set OPENAI_API_KEY=sk-xxx fly deployfly secrets会把密钥加密存储在Fly.io的KMS中,部署时自动注入容器环境变量,比硬编码安全100倍。
部署后验证三步法
1.fly status:确认实例状态为running;
2.fly logs:实时查看容器日志,看到Starting server on 0.0.0.0:8501即成功;
3.curl -I https://ai-interview-tool.fly.dev:返回HTTP 200且Content-Type: text/html,证明UI可访问。
我实测过:从git clone到https://xxx.fly.dev可访问,全程117秒。其中fly deploy耗时最长(约90秒),因为要上传镜像到Fly.io的registry。
3.4 Nginx反向代理配置(生产环境必备)
Fly.io免费版默认域名是xxx.fly.dev,但企业HR系统需要自定义域名(如interview.hr-company.com)。这时必须加Nginx反向代理,且要处理Streamlit的WebSocket长连接:
upstream streamlit_backend { server ai-interview-tool.fly.dev:443; } server { listen 443 ssl http2; server_name interview.hr-company.com; ssl_certificate /etc/letsencrypt/live/interview.hr-company.com/fullchain.pem; ssl_certificate_key /etc/letsencrypt/live/interview.hr-company.com/privkey.pem; location / { proxy_pass https://streamlit_backend; proxy_http_version 1.1; proxy_set_header Upgrade $http_upgrade; proxy_set_header Connection "upgrade"; # 关键!支持WebSocket proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-Proto $scheme; } # Streamlit的健康检查端点 location /_stcore/health { proxy_pass https://streamlit_backend/_stcore/health; proxy_set_header Host $host; } }重点在proxy_set_header Connection "upgrade"这一行。没有它,Streamlit的WebSocket连接会降级为HTTP轮询,导致UI卡顿、状态不同步。我踩过这个坑:初期没加这行,面试进行到第三题时页面突然白屏,查日志发现WebSocket connection failed。
4. 常见问题与独家排查技巧实录
4.1 “API调用失败:429 Too Many Requests”——不是限流,是漏了重试
现象:面试进行到第二轮,突然弹出红色错误框“API调用失败:429”,但OpenAI账户明明没超额度。
根本原因:oai_client.py里的重试装饰器没生效。检查发现@retry装饰器放在了错误位置:
# ❌ 错误写法:装饰器作用于内部函数,外部调用不触发重试 def call_openai_api(messages): @retry(...) # 这里重试只对inner_func生效 def inner_func(): return requests.post(...) return inner_func() # ✅ 正确写法:装饰器直接作用于对外接口 @retry(...) def call_openai_api(messages): return requests.post(...)排查技巧:在终端启动时加--logger.level debug参数:
streamlit run interview_streamlit.py --logger.level debug然后看日志里是否有Retrying call_openai_api in 2 seconds...字样。没有?说明装饰器没生效。
终极修复:重装tenacity并确认版本:
pip uninstall tenacity -y pip install tenacity==8.2.3 # 8.2.3是最后一个兼容Python 3.9的稳定版4.2 “页面空白,控制台报错:WebSocket connection failed”
现象:部署到Fly.io后,首页能打开,但点击“开始面试”后页面变白,浏览器控制台报WebSocket connection to 'wss://xxx.fly.dev/_stcore/ws' failed。
根本原因:Nginx配置遗漏proxy_set_header Connection "upgrade",或Fly.io应用没开WebSocket支持。
排查三步法:
1.fly status确认应用状态为running,且Status列显示healthy;
2.fly ips list确认有IPv4地址(没有则fly ips allocate-v4);
3. 在Nginx配置里检查proxy_set_header Connection是否拼写正确(注意大小写,必须是Connection,不是connection)。
快速验证:临时去掉Nginx,直接用Fly.io域名访问:
curl -v https://ai-interview-tool.fly.dev/_stcore/ws如果返回HTTP/1.1 101 Switching Protocols,说明后端正常,问题100%在Nginx。
4.3 “评分建议全是‘很好’,没有具体反馈”
现象:用户回答完,评分模块显示“综合得分:8.5/10”,但下方的“改进建议”栏只有“回答很全面”这种废话。
根本原因:settings.py里的SCORING_WEIGHTS权重全设为0,或prompts/scoring.txt模板被意外覆盖。
排查技巧:在Streamlit UI里加一个隐藏调试按钮(开发时用,上线前注释掉):
if st.sidebar.checkbox("🔍 开启调试模式"): st.write("当前评分权重:", settings.SCORING_WEIGHTS) st.write("当前提示词长度:", len(load_prompt_from_file("scoring")))修复方案:检查prompts/scoring.txt是否为空。标准模板应包含:
你是一名资深面试官,请基于以下维度评估候选人回答: - 清晰度:是否用简洁语言表达核心观点? - 结构:是否遵循STAR原则(情境、任务、行动、结果)? - 深度:是否提及具体技术细节(如算法时间复杂度、数据库索引类型)? - 互动:是否主动提问或展示学习意愿? 请用中文给出3条具体改进建议,每条不超过20字。如果模板丢失,从GitHub仓库的prompts/目录重新下载。
4.4 “Mac M1安装grpcio卡住,CPU跑满100%”
现象:执行pip install --no-binary=grpcio grpcio后,终端不动,Activity Monitor显示Python进程CPU 100%,持续15分钟以上。
根本原因:M1芯片的clang编译器在编译grpcio时内存不足,默认只分配4GB,而grpcio需要6GB。
解决方案:增加编译内存限制:
# 先释放内存 sudo purge # 设置编译内存上限为8GB export CC="clang -mcpu=apple-m1 -march=armv8-a+crypto" export CXX="clang++ -mcpu=apple-m1 -march=armv8-a+crypto" export GRPC_PYTHON_BUILD_SYSTEM_OPENSSL=1 export GRPC_PYTHON_BUILD_SYSTEM_ZLIB=1 # 再次安装(这次会快很多) pip install --no-binary=grpcio grpcio验证:编译过程中看终端输出,出现[100%] Built target grpc即成功。
4.5 “远程部署后,inputs.csv修改不生效”
现象:在Fly.io上部署后,修改了data/inputs.csv,但Streamlit界面里候选人列表没更新。
根本原因:Docker镜像构建时COPY . /app把CSV文件打包进镜像了,后续修改宿主机文件不影响容器内文件。
正确做法:用Fly.io的Volume功能挂载外部存储:
# 创建持久化卷 fly volumes create interview_data --size 1 # 修改fly.toml,添加挂载配置 [mounts] source = "interview_data" destination = "/app/data"然后在utils.py里把load_candidates()改成从/app/data/inputs.csv读取,这样修改CSV后重启应用即可生效。
实操心得:这个坑我踩了两次。第一次以为是代码bug,花了3小时查
pandas.read_csv;第二次才意识到Docker的只读文件系统特性。现在我的项目操作说明.md里专门加了“⚠️ 注意:Docker容器内文件修改无效,如需动态更新候选人,请使用Fly.io Volume”警告。
我在实际使用中发现,这个工具最强大的地方不是它能生成多少问题,而是它教会我如何拆解一个面试官的思维过程。比如当LLM在追问“你提到用Redis缓存,那缓存穿透怎么解决?”时,我意识到自己过去只背了“布隆过滤器”四个字,却没想过布隆过滤器的误判率怎么调、内存占用怎么算。后来我把prompts/technical_deep_dive.txt里的追问逻辑改成了三层:第一层问方案(布隆过滤器),第二层问权衡(为什么不用缓存空值?),第三层问落地(线上QPS 10万时,布隆过滤器bitmap该设多大?)。改完提示词,工具生成的问题立刻有了工程师的味道。这个项目的价值,从来不在代码本身,而在于它逼着你把模糊的“面试能力”,变成可拆解、可训练、可迭代的具体动作。
本文还有配套的精品资源,点击获取
简介:一个开箱即用的AI面试辅助工具,用Python写成,基于大语言模型实现结构化面试模拟、实时问答反馈、智能问题生成和评分建议。前端用Streamlit搭建,无需复杂后端或数据库,装好依赖后直接运行streamlit run interview_streamlit.py就能在本地打开界面(默认http://localhost:8501)。Mac M1用户已适配grpcio和gevent兼容性问题,按说明执行额外命令即可。支持远程部署:加–server.headless true参数并指定端口(如8502),配合Dockerfile和fly.toml可打包镜像、一键发布到Fly.io。API密钥通过OPENAI_API_KEY环境变量配置,所有配置集中在settings.py,调用逻辑封装在oai_client.py,通用函数放在utils.py。data目录下含inputs.csv用于批量导入候选人信息,playground.png和inputs.png是实际界面截图,项目操作说明.md覆盖安装、启动、部署全流程及常见报错解决方法。适合求职者日常练习、HR快速评估候选人、开发者学习LLM集成与Streamlit应用开发。
本文还有配套的精品资源,点击获取