公司动态

EduAgent 项目全解析(六):模拟面试 Agent——五阶段状态机

📅 2026/8/25 13:00:01
EduAgent 项目全解析(六):模拟面试 Agent——五阶段状态机
本篇拆解模拟面试 AgentInterview。它的核心是一个五阶段状态机热身 → 技术基础 → 项目深挖 → 反问收尾 → 结束。每次学员回答都会完整跑一遍 LangGraph 图图内的check_stage节点用轮数 回答质量 内容条件三重信号驱动阶段流转回答质量四档标签excellent/adequate/weak/no_answer驱动追问或换题决策最后生成五维度面试报告。这是状态机模式在 Agent 里最完整的应用。一、五阶段状态机classInterviewStage(str,Enum):面试的五个阶段状态机的状态取值。WARMUPwarmup# 热身邀请自我介绍TECH_BASEtech_base# 技术基础题库问答PROJECTproject# 项目深挖针对简历项目追问CLOSINGclosing# 反问收尾让学员提问FINISHEDfinished# 终态触发报告生成阶段流转顺序固定WARMUP → TECH_BASE → PROJECT → CLOSING → FINISHED但每一步推进都有条件不是简单的计数器阶段转移推进条件WARMUP → TECH_BASE学员完成自我介绍≥1轮TECH_BASE → PROJECT问过 ≥8 道题或题库问完PROJECT → CLOSING所有简历项目都深挖过任意 → FINISHED强制总轮数 ≥ 上限-2 或学员说结束面试CLOSING → FINISHED反问环节满 2 轮二、图拓扑一次对话跑一遍图finished其他STARTload_context加载简历/题库/历史check_stage阶段判断generate_report生成五维报告evaluate_answer评估回答质量save_report保存报告save_memory记忆保存generate_response生成下一轮面试题ENDdef_route_after_check_stage(state:InterviewState)-str:ifstate.get(current_stage)InterviewStage.FINISHED.value:returngenerate_report# 结束 → 报告路径returnevaluate_answer# 其他 → 正常对话路径关键理解模拟面试不是一个大图持续运行而是每一轮对话都完整执行一次图。上一轮的 State 通过 MemorySaver 按thread_id持久化下一轮ainvoke时自动加载——这就是每次跑一遍图还能记得上下文的原因。三、check_stage纯逻辑的阶段推进不调 LLMcheck_stage_node是状态机的心脏注释明确写着纯逻辑不调 LLM——阶段判断这种事用代码写死比让 LLM 决定更可靠、更省钱asyncdefcheck_stage_node(state:InterviewState)-dict:current_stagestate.get(current_stage,InterviewStage.WARMUP.value)stage_turnsstate.get(stage_turn_count,0)total_turnsstate.get(total_turn_count,0)max_turnsstate.get(max_turns,DEFAULT_MAX_TURNS)# 从后往前找学员最后一条消息检测结束面试关键词latest_student_msgformsginreversed(state.get(messages,[])):ifisinstance(msg,HumanMessage):latest_student_msg_msg_text(msg)break# 强制结束判断轮数快到上限或学员主动说要结束force_end_keywords[结束面试,不想继续,面试结束,结束吧,到此为止]is_forced_end(total_turnsmax_turns-2orany(kwinlatest_student_msgforkwinforce_end_keywords))ifis_forced_end:return{current_stage:InterviewStage.FINISHED.value}# CLOSING → FINISHED反问环节满 2 轮就结束ifcurrent_stageInterviewStage.CLOSING.value:ifstage_turnsSTAGE_MIN_TURNS[closing]:return{current_stage:InterviewStage.FINISHED.value}return{}# 常规推进到上限强制推到下限看内容条件stage_minSTAGE_MIN_TURNS.get(current_stage,2)stage_maxSTAGE_MAX_TURNS.get(current_stage,10)should_advanceFalseifstage_turnsstage_max:should_advanceTrue# 到上限 → 强制推进elifstage_turnsstage_min:should_advance_check_advance_condition(# 到下限 → 看内容current_stage,state,latest_student_msg)ifshould_advance:next_stage_next_stage(current_stage)return{current_stage:next_stage,stage_turn_count:0,# 新阶段轮数从 0 开始followup_count:0,# 追问计数清零current_question:None}# 清当前题新阶段重新选题return{}# 不推进保持当前阶段最小/最大轮数 内容条件的双阈值设计非常实用不到最小轮数不问内容条件防止问两题就跑超过最大轮数强制推进防止一直卡在某个阶段中间才看内容条件技术题问够 8 道、项目全深挖完。内容条件_check_advance_condition也是纯逻辑def_check_advance_condition(stage,state,latest_msg)-bool:ifstageInterviewStage.WARMUP.value:returnTrue# 学员发任意消息即视为完成自我介绍ifstageInterviewStage.TECH_BASE.value:bankstate.get(question_bank,[])asked_countsum(1forqinbankifq.get(asked,False))returnasked_countmin(8,len(bank))# 至少问过8道ifstageInterviewStage.PROJECT.value:projectsstate.get(resume_projects,[])ifnotprojects:returnTrue# 无简历项目直接推returnlen(state.get(projects_asked,[]))len(projects)returnFalse注意 TECH_BASE 用min(8, len(bank))——题库不足 8 道时问完即走不会死等。四、evaluate_answer回答质量四档标签classAnswerQuality(str,Enum):EXCELLENTexcellent# 优秀有技术细节/量化/原理ADEQUATEadequate# 基本及格方向对但缺深度WEAKweak# 较弱方向偏或太表面NO_ANSWERno_answer# 未作答明说不知道或为空evaluate_answer_node评估学员本次回答写入last_answer_quality供generate_response_node决定追问还是换题excellent认可 → 进入下一题adequate轻追问一次引导补充深度weak换个角度再问一次或直接讲解no_answer给出参考答案进入下一题。每个问题的追问次数有上限followup_count≤ 2防止在单题上无限纠缠。这个质量标签驱动交互策略的模式把面试官的经验变成了可执行的规则。五、generate_response按阶段生成不同响应generate_response_node按当前阶段路由到四个响应函数asyncdefgenerate_response_node(state:InterviewState)-dict:stagestate.get(current_stage)ifstageInterviewStage.WARMUP.value:content,updatesawait_respond_warmup(state)elifstageInterviewStage.TECH_BASE.value:content,updatesawait_respond_tech_base(state)elifstageInterviewStage.PROJECT.value:content,updatesawait_respond_project(state)else:# CLOSINGcontent,updatesawait_respond_closing(state)return{messages:[AIMessage(contentcontent)],**updates}5.1 技术基础题库 LLM 生成结合_respond_tech_base的策略优先用题库从question_bankseed_interview_questions.py预置的面试题库里挑一道未问过的题标记askedTrue题库不够用 LLM 按target_position现场出题结合追问如果上轮回答质量不佳且追问次数未超限先追问当前题。5.2 项目深挖从简历来_respond_project读resume_projects简历审查 Agent 的结果注入的从没深挖过的项目里挑一个针对其技术栈和亮点生成追问并记入projects_asked防止重复深挖。这就是简历 → 面试的联动面试题不是凭空生成而是从学员自己的简历里长出来的。5.3 记忆注入_build_system_with_summary把历史摘要existing_summary拼进 System Prompt让面试官记得学员之前说过什么——长面试不会前后矛盾。六、报告生成五维度评估面试结束进入 FINISHED后generate_report_node用结构化输出生成InterviewReportclassInterviewReport(BaseModel):dimensions:list[DimensionEval]# 五个维度的评估结果overall_score:int# 综合得分 0-100五维度加权平均strengths:list[str]# 2-3条核心优势improvements:list[str]# 2-3条重点提升方向overall_comment:str# 综合评语recommended_topics:list[str]# 建议重点复习的3-5个知识点next_step_advice:str# 下一步备考建议classDimensionEval(BaseModel):dimension:strscore:int# 得分 0-100comment:strhighlights:list[str]weaknesses:list[str]classReportWrapper(BaseModel):report:InterviewReport# ★ with_structured_output 需要顶层对象注意ReportWrapper这层包装with_structured_output要求顶层是对象而非裸列表/裸模型所以包了一层。这是 resume 的IssueList同款经验。报告生成失败时走降级fallback_usedTrue返回基础反馈“面试评估服务暂时不可用已记录本次面试对话请稍后查看报告。”七、记忆保存与上下文加载load_context_node加载简历审查结果resume_review_id→ 项目/技能、题库、历史摘要save_memory_node超过阈值触发摘要压缩把长面试压成学员画像摘要写库。整个面试的状态阶段、轮数、已问题目、追问计数、质量标签全部存在 State 里由 MemorySaver 按 thread_id 跨请求持久化——状态机 checkpointer 的组合让长对话天然成立。八、本篇小结知识点实现位置核心技巧状态机定义state.pyInterviewStage枚举 固定流转顺序阶段推进check_stage_node纯逻辑不调 LLM 最小/最大轮数双阈值内容条件_check_advance_condition题库问够 / 项目全深挖才推进质量标签evaluate_answer_node四档标签驱动追问/换题决策分阶段响应generate_response_node按阶段路由到 4 个响应函数简历联动_respond_project从简历项目生成追问防重复深挖报告生成generate_report_node结构化输出 顶层包装Interview Agent 的核心思想把面试官的经验翻译成状态机 规则 少量 LLM——阶段推进、轮数控制、追问次数、项目去重这些确定性逻辑全部用代码写死LLM 只负责出题和评估这种需要语义理解的部分。能用规则就用规则LLM 只做规则做不了的事——这是整个项目贯穿始终的工程哲学。下篇预告系列最后一篇《EduAgent 项目全解析七系统集成——Orchestrator 编排与统一 SSE 入口》看四个 Agent 如何被组装成一个完整系统Orchestrator 的三种执行模式单 Agent / Pipeline / 澄清、懒加载机制、简历审查模拟面试求职全链路 Pipeline、统一入口的五类零 Token 规则拦截 LLM 意图路由 SSE 流式分发。项目源码仅供参考欢迎评论区交流讨论。