diff --git a/README.md b/README.md index 2df3d87..cb9b92f 100644 --- a/README.md +++ b/README.md @@ -137,5 +137,5 @@ MIT License 项目初期,我们没有急着用trae写代码,而是先琢磨“为什么做”。身边同学求职时总抱怨:通用题库的问题和自己的简历不沾边,练完也没人指点改进。这让我们意识到,工具再便捷,没有精准需求导向就是空谈。我们决定不做简单的题库搬运,而是聚焦“个性化”,让AI能读懂简历、给出针对性反馈——这一定位成了后续开发的核心方向。 用trae开发的过程,更像是一场“想法落地的试炼”。这款软件降低了编程门槛,让我们不用纠结复杂的语法,能专注于功能逻辑的打磨。但真正的挑战不在代码编写,而在如何让工具“懂用户”。比如生成个性化面试问题时,我们反复调试prompt逻辑,引导AI从简历中抓取项目细节、技能亮点,甚至职业空白期等关键信息,再结合职位类型精准提问。一开始AI会给出通用问题,我们就不断优化指令,加入“追问数据成果”“聚焦技术难点”等具体要求,慢慢让回复更贴合实际面试场景。 反馈功能的设计,让我们对“实用”有了更深理解。我们不想让反馈只停留在“回答不错”的表面,而是希望能真正帮用户改进。借助trae的便捷调试功能,我们设计了多维度评价体系,从结构逻辑、语言表达等方面给出具体建议。比如提醒用户用STAR法则梳理回答,或结合Python、SQL技能突出专业优势。这个过程让我们明白,工具是实现想法的手段,而对用户需求的深度拆解,才是产品好用的关键。 - 测试时,同学说“AI提的问题他觉得十分符合当下面试的提问热点”,这句话让我们备受鼓舞。原来,trae这类工具能帮我们快速落地想法,但真正打动用户的,是我们站在求职者角度的思考。 +测试时,同学说“AI提的问题他觉得十分符合当下面试的提问热点”,这句话让我们备受鼓舞。原来,trae这类工具能帮我们快速落地想法,但真正打动用户的,是我们站在求职者角度的思考。 这次开发让我们领悟到:技术工具再强大,也只是辅助。作为大数据专业学生,我们的核心价值在于发现问题、拆解需求、用逻辑让工具服务于人。未来,我们想继续用trae优化功能,加入面试表现分析、薪资参考等模块,让这个工具真正成为求职者的助力,也让我们在实践中更懂技术与需求的平衡之道。 \ No newline at end of file diff --git a/app.py b/app.py index 6cb79d4..21c5512 100644 --- a/app.py +++ b/app.py @@ -1,10 +1,30 @@ +import logging +import sys + +logging.basicConfig( + level=logging.DEBUG, + format='%(asctime)s - %(levelname)s - %(message)s', + handlers=[ + logging.FileHandler('app_debug.log'), + logging.StreamHandler(sys.stdout) + ] +) + +logger = logging.getLogger(__name__) + from flask import Flask, render_template, request, jsonify, session from services.deepseek_service import deepseek_service from config import Config import uuid +import datetime app = Flask(__name__) app.secret_key = "interviewer-secret-key-change-in-production" +# 设置session过期时间为1小时 +app.permanent_session_lifetime = datetime.timedelta(hours=1) + +# 内存数据库存储面试数据,避免session过期导致的"面试不存在"错误 +interviews_db = {} INTERVIEW_PHASES = { "intro": "自我介绍", @@ -86,7 +106,7 @@ def start_interview(): interview_id = str(uuid.uuid4()) - session[f"interview_{interview_id}"] = { + interview_data = { "job_position": job_position, "difficulty": difficulty, "current_phase": "intro", @@ -95,6 +115,9 @@ def start_interview(): "is_active": True } + session[f"interview_{interview_id}"] = interview_data + interviews_db[interview_id] = interview_data + first_question = deepseek_service.generate_interview_question( job_position=job_position, difficulty=difficulty, @@ -125,11 +148,16 @@ def answer_question(): if not interview_id: return jsonify({"error": "无效的面试ID"}), 400 - interview_key = f"interview_{interview_id}" - if interview_key not in session: - return jsonify({"error": "面试不存在或已结束"}), 400 + # 首先从内存数据库查找面试数据 + if interview_id not in interviews_db: + # 如果内存数据库中没有,再检查session + interview_key = f"interview_{interview_id}" + if interview_key not in session: + return jsonify({"error": "面试不存在或已结束"}), 400 + # 如果session中有,同步到内存数据库 + interviews_db[interview_id] = session[interview_key] - interview_data = session[interview_key] + interview_data = interviews_db[interview_id] if not interview_data["is_active"]: return jsonify({"error": "面试已结束"}), 400 @@ -141,9 +169,12 @@ def answer_question(): if request_feedback: last_question = "" + logger.debug(f"查找最后一个问题,对话历史长度:{len(conversation_history)}") for msg in reversed(conversation_history): - if msg["role"] == "assistant" and "?" in msg["content"]: + logger.debug(f"检查消息:角色={msg['role']}, 内容={msg['content'][:50]}...") + if msg["role"] == "assistant" and ("?" in msg["content"] or "?" in msg["content"]): last_question = msg["content"] + logger.debug(f"找到最后一个问题:{last_question[:50]}...") break if last_question: @@ -161,7 +192,11 @@ def answer_question(): "ended": False }) except Exception as e: + logger.error(f"生成反馈失败:{str(e)}", exc_info=True) return jsonify({"error": f"生成反馈失败:{str(e)}"}), 500 + else: + logger.warning("没有找到最后一个问题") + return jsonify({"error": "没有找到相关问题"}), 400 conversation_history.append({"role": "user", "content": user_answer}) @@ -205,7 +240,7 @@ def answer_question(): next_question = deepseek_service.generate_interview_question( job_position=interview_data["job_position"], difficulty=interview_data["difficulty"], - conversation_history=conversation_history[:-1], + conversation_history=conversation_history, phase=next_phase ) conversation_history.append({ @@ -219,14 +254,16 @@ def answer_question(): next_question = deepseek_service.generate_interview_question( job_position=interview_data["job_position"], difficulty=interview_data["difficulty"], - conversation_history=conversation_history[:-1], + conversation_history=conversation_history, phase=current_phase ) conversation_history.append({"role": "assistant", "content": next_question}) except Exception as e: return jsonify({"error": f"生成问题失败:{str(e)}"}), 500 - session[interview_key] = interview_data + # 同时更新内存数据库和session中的数据 + interviews_db[interview_id] = interview_data + session[f"interview_{interview_id}"] = interview_data return jsonify({ "question": next_question, @@ -238,47 +275,95 @@ def answer_question(): @app.route("/api/interview/feedback", methods=["POST"]) def get_interview_feedback(): - data = request.json - interview_id = data.get("interview_id", "").strip() - conversation_history = data.get("conversation_history", []) + logger.info("接收到面试反馈请求") - if not interview_id: - return jsonify({"error": "无效的面试ID"}), 400 - - system_prompt = """作为一位专业的面试评估专家,请对整场面试进行全面评估。 + try: + # 首先确保能够获取请求数据 + if not request.is_json: + logger.warning("请求数据不是JSON格式") + return jsonify({"error": "请求数据必须是JSON格式"}), 400 + + data = request.json + logger.debug(f"请求数据:{data}") + + # 检查必要参数 + interview_id = data.get("interview_id", "").strip() + conversation_history = data.get("conversation_history", []) + + if not interview_id: + logger.warning("面试ID无效") + return jsonify({"error": "无效的面试ID"}), 400 + + if not conversation_history: + logger.warning("没有提供面试对话历史") + return jsonify({"error": "没有提供面试对话历史"}), 400 + + # 获取岗位信息 + job_position = "" + if interview_id in interviews_db: + job_position = interviews_db[interview_id].get("job_position", "") + + logger.debug(f"岗位信息:{job_position}") + logger.debug(f"对话历史长度:{len(conversation_history)}") + + # 构建系统提示 + system_prompt = """作为一位专业的面试评估专家,请对整场面试进行全面评估。 请提供: 1. 整体表现评分(0-100分)和评级(优秀/良好/一般/需改进) -2. 各轮回答的详细分析 -3. strengths(优势) -4. areas_for_improvement(需要改进的方面) -5. 具体的准备建议 +2. 各轮回答的详细分析(针对每个问题和回答给出具体评价) +3. strengths(优势)- 列出至少3点 +4. areas_for_improvement(需要改进的方面)- 列出至少3点 +5. 具体的准备建议(针对改进点给出可操作的建议) -请用中文回复,格式清晰、结构化。""" +请用中文回复,格式清晰、结构化,避免过于笼统的描述。""" - conversation_text = "\n\n".join([ - f"{msg['role']}:{msg['content']}" - for msg in conversation_history[-30:] - ]) - - messages = [ - {"role": "system", "content": system_prompt}, - {"role": "user", "content": f"请分析以下面试对话并给出综合反馈:\n\n{conversation_text}"} - ] - - try: + # 构建对话文本 + conversation_text = "\n\n".join([ + f"{'面试官' if msg['role'] == 'assistant' else '候选人'}:{msg['content']}" + for msg in conversation_history + ]) + + # 构建用户提示 + if job_position: + user_prompt = f"请分析以下针对{job_position}岗位的面试对话并给出综合反馈:\n\n{conversation_text}" + else: + user_prompt = f"请分析以下面试对话并给出综合反馈:\n\n{conversation_text}" + + # 构建完整的消息 + messages = [ + {"role": "system", "content": system_prompt}, + {"role": "user", "content": user_prompt} + ] + + logger.debug("准备调用DeepSeek API生成反馈") + logger.debug(f"API请求消息数量:{len(messages)}") + + # 调用DeepSeek API response = deepseek_service._call_api(messages) + logger.debug("DeepSeek API调用成功") + + # 处理API响应 + if not response or "choices" not in response or not response["choices"]: + logger.error("API响应格式错误:缺少choices字段") + return jsonify({"error": "生成反馈失败:API响应格式错误"}), 500 + feedback = response["choices"][0]["message"]["content"] + logger.info("反馈生成成功") + logger.debug(f"生成的反馈内容长度:{len(feedback)}字符") + logger.debug(f"生成的反馈内容开头:{feedback[:100]}...") return jsonify({"feedback": feedback}) except Exception as e: - return jsonify({"error": str(e)}), 500 + logger.error(f"生成面试反馈失败:{str(e)}", exc_info=True) + # 返回更具体的错误信息 + return jsonify({"error": f"生成面试反馈失败:{str(e)}", "details": str(type(e).__name__)}), 500 if __name__ == "__main__": app.run( host=Config.APP_HOST, port=Config.APP_PORT, - debug=Config.DEBUG + debug=False ) diff --git a/services/deepseek_service.py b/services/deepseek_service.py index cfbf38e..15dc8cc 100644 --- a/services/deepseek_service.py +++ b/services/deepseek_service.py @@ -34,11 +34,14 @@ class DeepSeekService: "stream": stream } - response = requests.post( + # 创建会话对象并设置超时 + session = requests.Session() + session.timeout = 120 # 会话级别的超时设置 + + response = session.post( f"{self.api_base}/chat/completions", headers=self._get_headers(), - json=payload, - timeout=30 + json=payload ) if response.status_code != 200: diff --git a/static/js/main.js b/static/js/main.js index 1ca8eca..e576385 100644 --- a/static/js/main.js +++ b/static/js/main.js @@ -1,5 +1,5 @@ let currentFeature = null; -let interviewId = null; +let interviewId = localStorage.getItem('interviewId') || null; let conversationKey = 'default'; function showFeature(feature) { @@ -35,9 +35,7 @@ function showFeature(feature) { if (targetSection) { document.getElementById(targetSection).style.display = 'block'; - if (feature === 'interview' || feature === 'feedback') { - resetInterview(); - } else if (feature === 'resume') { + if (feature === 'resume') { document.getElementById('resume-result').style.display = 'none'; } } @@ -130,6 +128,7 @@ async function startInterview() { } interviewId = data.interview_id; + localStorage.setItem('interviewId', interviewId); document.getElementById('interview-setup').style.display = 'none'; document.getElementById('interview-active').style.display = 'flex'; @@ -207,7 +206,8 @@ async function submitAnswer() { } if (data.ended) { - showFinalFeedback(data.conversation_history); + // 面试结束,调用endInterview获取生成的面试反馈 + endInterview(); } else if (data.question) { addInterviewMessage(data.question, 'interviewer'); updatePhaseBadge(data.phase); @@ -256,6 +256,23 @@ async function requestFeedback() { if (data.feedback) { addInterviewMessage(data.feedback, 'feedback'); + + // 添加返回面试的按钮 + const messagesContainer = document.getElementById('interview-messages'); + const returnButton = document.createElement('button'); + returnButton.className = 'return-interview-btn'; + returnButton.textContent = '返回面试'; + returnButton.onclick = function() { + // 移除按钮 + this.remove(); + // 可以在这里添加额外的逻辑,比如重新启用输入框等 + }; + + const buttonContainer = document.createElement('div'); + buttonContainer.className = 'return-button-container'; + buttonContainer.appendChild(returnButton); + messagesContainer.appendChild(buttonContainer); + messagesContainer.scrollTop = messagesContainer.scrollHeight; } } catch (error) { @@ -298,7 +315,8 @@ async function endInterview() { showFinalFeedback(data.feedback); } catch (error) { - alert('获取反馈失败:' + error.message); + console.error('获取反馈失败:', error); + alert('获取反馈失败,请稍后重试'); } finally { showLoading(false); } @@ -311,7 +329,19 @@ function showFinalFeedback(feedbackData) { const feedbackContent = document.getElementById('feedback-content'); if (typeof feedbackData === 'string') { - feedbackContent.innerHTML = formatSuggestions(feedbackData); + // 将Markdown风格的列表和标题转换为HTML + let formattedFeedback = feedbackData + // 转换标题 + .replace(/^###\s+(.*)$/gm, '
')
+ .replace(/\n/g, '
');
+
+ feedbackContent.innerHTML = `