「AI 模拟面试」这两年是个热词但大部分 demo 拆开看都是同一套准备一个题库模型挨个抛题用户答完抛下一题最后甩一段总结。这玩意儿严格说不是面试是带语音的问卷。真正的面试官不是这么干的。你答「我觉得应该加强监管」他会追一句「具体怎么加强谁来执行」你答得心虚开始绕他能听出来从你绕的那个点切进去。追问才是面试的灵魂。 而追问恰恰是工程上最难做稳的部分。我们在做智蛙公考 AI 智能体的模拟面试时结构化面试 无领导小组把这套啃下来了。这篇讲讲多轮对话和实时追问的工程实现纯技术不灌鸡汤。一、第一个坑多轮对话开下去模型会「失忆」最朴素的实现是把整场对话塞进 messages 数组一路 append 下去system: 你是一名公考面试考官...assistant: 请回答第一题谈谈你对躺平的看法user: 考生作答 300 字assistant: 追问user: 考生作答 200 字...这个做法开三五轮还行再往下就开始出问题模型忘了当前是第几题把已经问过的题又问一遍模型忘了岗位设定本来面的是税务岗问着问着开始问教育理念上下文越堆越长注意力漂移追问质量肉眼可见地下降token 成本随轮次线性甚至超线性上涨。根因对话流程的「状态」不该靠模型在长 history 里自己记而应该外置成结构化数据由代码维护。 这是整篇文章的核心思路。二、面试状态机把一场面试结构化我们把一场面试建模成一个状态机流程由代码驱动模型只负责「在当前状态下生成自然语言」。from dataclasses import dataclass, field from enum import Enum class Phase(Enum): OPENING opening # 开场/暖场 MAIN_Q main_question # 主问题作答 FOLLOW_UP follow_up # 追问 TRANSITION transition # 过题 CLOSING closing # 结束语 SCORING scoring # 评分离线 dataclass class InterviewState: position: str # 岗位设定全程不变 questions: list # 预设主问题列表 cur_q_index: int 0 # 当前第几题 phase: Phase Phase.OPENING follow_up_count: int 0 # 当前题已追问几次 max_follow_up: int 2 # 每题最多追问 history_digest: list field(default_factorylist) # 压缩后的历史 answers: list field(default_factorylist) # 结构化留存每题作答每一轮交互代码先看 state 决定该干嘛再把「当前状态 必要上下文」组装成 prompt 喂给模型。模型不需要从一长串 history 里推断「我现在该问啥」——流程控制权在代码手里模型只管把当前这一步说好。状态流转大致是OPENING → MAIN_Q → (要不要追问?)├─ 是 → FOLLOW_UP → (还追吗? 看 follow_up_count)└─ 否 → TRANSITION → 下一题 MAIN_Q → ... → CLOSING → SCORING好处很直接永远不会跑飞、不会漏题、不会重复问、岗位设定全程钉死。模型生成内容里就算偶尔飘状态机也会把它拽回正轨。三、追问决策别让模型「一把梭」最容易出问题的环节是追问。常见错误做法是把用户回答丢给模型直接说「请追问」。结果就是模型为了追问而追问经常追一些无关痛痒甚至answer里根本没提的东西很假。我们把追问拆成两步先决策再生成。第一步判断「该不该追、从哪追」这一步本质是个分析任务单独一次调用temperature0输出结构化结果FOLLOW_UP_DECISION_PROMPT 你是资深公考面试考官。考生对题目「{question}」的作答如下 --- {answer} --- 请判断是否需要追问并以 JSON 输出 {{ need_follow_up: true/false, reason: 为什么追问/不追问, weak_points: [作答中空泛、回避、自相矛盾或值得深挖的点最多2个], suggested_angle: 若追问从哪个角度切入最有价值 }} 判断标准 - 作答空泛、停留在口号如要加强监管但没说怎么加强→ 追具体怎么做 - 作答回避了题目某个关键面 → 追被回避的点 - 作答出现自相矛盾 → 追矛盾处 - 作答已经充分、具体、闭环 → 不追问need_follow_upfalse 注意最后一条作答得好就不追问。 真考官不会对一个答得滴水不漏的人硬挑刺。让模型有「不追问」这个合法选项整个交互的真实感会上一个台阶。第二步基于决策结果生成追问只有 need_follow_uptrue 时才进第二次调用把 suggested_angle 和 weak_points 作为约束生成自然语言追问。这次可以给点温度temp≈0.6让措辞自然。def decide_and_generate_follow_up(state, answer): decision llm_json(FOLLOW_UP_DECISION_PROMPT.format( questionstate.questions[state.cur_q_index], answeranswer)) if not decision[need_follow_up] or state.follow_up_count state.max_follow_up: state.phase Phase.TRANSITION return None state.follow_up_count 1 state.phase Phase.FOLLOW_UP return gen_follow_up_question(state, decision) # temp≈0.6决策与生成分离和我们做申论批改时「评分与点评分离」是同一个思想把需要稳定判断的部分摁到 temp0、结构化输出把需要自然表达的部分单独放开。两个目标解耦互不拖累。四、长对话的上下文压缩状态机解决了「流程不跑飞」但模型生成每一步时仍需要一些历史上下文比如追问时要看到用户原答案。如果把全量 history 一直带着长面试照样上下文膨胀。做法外置一份 history_digest每过完一题就把那一题压缩成一条摘要存进去原始长文本不再进 prompt。def digest_one_question(state, question, answer, follow_ups): summary llm(f把以下一题面试问答压缩成不超过80字的客观摘要 保留考生核心观点、明显短板、追问及其回应。不要评价。 题目{question} 作答{answer} 追问记录{follow_ups}) state.history_digest.append({ q_index: state.cur_q_index, summary: summary }) state.answers.append({ # 原文另存供离线评分用不进对话 prompt question: question, answer: answer, follow_ups: follow_ups })后续轮次的 prompt 里只带 history_digest几条 80 字摘要而不是全部原文。效果方案第8轮 prompt token追问相关性人工评1-5全量 history 直接 append~42003.2状态机 digest 压缩~9004.4token 砍到约 1/4追问质量反而升了——因为模型注意力不再被海量原文稀释。原始作答全文我们另存一份answers只在离线评分时才完整调出来用不参与实时对话。五、评分与对话解耦最后一步打分。我们不在对话过程里实时打分而是面试全部结束后拿离线存下的 answers每题原文 追问回应单独走一遍评分 pipeline。原因有二实时打分会干扰对话节奏且边聊边打的分前后标准不一致评分需要看完整作答原文 temp0 rubric 档位表这套和我们申论批改一模一样不展开可参见上一篇《用大模型做主观题批改怎么保证评分一致性》。对话归对话评分归评分。两条 pipeline 各自优化互不污染。六、踩过的坑坑1追问跑偏问到用户没说的内容。 早期没有「决策」这步直接让模型追问它经常脑补——用户压根没提风险管控它追「你刚才说的风险怎么控制」。解法就是上面的两步法先抽 weak_points 锚定在原答案里追问必须挂靠到考生真实说过的点。坑2上下文爆炸 注意力漂移。 全量 history append 到后面模型开始重复问、忘岗位。解法是状态机 digest 压缩把「记流程」这件事从模型手里收回到代码里。坑3打断/追问时机靠模型自己判断不靠谱。 什么时候该过题、每题追几次这种节奏控制别交给模型即兴发挥。用 max_follow_up、follow_up_count 这种硬计数器在代码里卡死模型只决定「这一次追不追」不决定「整体节奏」。坑4评分维度和对话过程耦合分会飘。 一旦让模型边对话边给印象分最终分受对话顺序影响很大。务必把评分拆成独立的离线环节用固定 rubric 一次性评别让对话过程污染评分。七、小结做一个「不像问卷」的 AI 模拟面试关键不在模型多强在于把控制逻辑从模型手里拿回来状态机控流程第几题、追几次、什么岗全由代码维护模型只生成当前步追问两步法先判断该不该追、从哪追temp0 结构化再生成追问放开措辞外置 digest 压上下文每题压成短摘要原文另存供离线评分长对话不膨胀评分与对话解耦面试结束后离线统一打分rubric 固定不被对话过程带偏。这套思路适用于一切「多轮、有追问、要评估」的对话式 AI——面试、AI 问诊、口语陪练、销售陪练底层都是同一个问题怎么让一个无状态的模型跑出一场有状态、有节奏、有判断的对话。 答案是别指望模型自己撑住用工程把状态机、决策点、上下文、评分一个个框出来。我们在公考赛道做 AI 模拟面试和 AI 批改智蛙面试 / 智蛙公考上面这些都是真刀真枪踩出来的。做对话式 AI 评估场景的同行评论区交流。
AI 模拟面试怎么做:智蛙公考智能体多轮对话 + 实时追问的工程实现
发布时间:2026/7/1 9:15:20
「AI 模拟面试」这两年是个热词但大部分 demo 拆开看都是同一套准备一个题库模型挨个抛题用户答完抛下一题最后甩一段总结。这玩意儿严格说不是面试是带语音的问卷。真正的面试官不是这么干的。你答「我觉得应该加强监管」他会追一句「具体怎么加强谁来执行」你答得心虚开始绕他能听出来从你绕的那个点切进去。追问才是面试的灵魂。 而追问恰恰是工程上最难做稳的部分。我们在做智蛙公考 AI 智能体的模拟面试时结构化面试 无领导小组把这套啃下来了。这篇讲讲多轮对话和实时追问的工程实现纯技术不灌鸡汤。一、第一个坑多轮对话开下去模型会「失忆」最朴素的实现是把整场对话塞进 messages 数组一路 append 下去system: 你是一名公考面试考官...assistant: 请回答第一题谈谈你对躺平的看法user: 考生作答 300 字assistant: 追问user: 考生作答 200 字...这个做法开三五轮还行再往下就开始出问题模型忘了当前是第几题把已经问过的题又问一遍模型忘了岗位设定本来面的是税务岗问着问着开始问教育理念上下文越堆越长注意力漂移追问质量肉眼可见地下降token 成本随轮次线性甚至超线性上涨。根因对话流程的「状态」不该靠模型在长 history 里自己记而应该外置成结构化数据由代码维护。 这是整篇文章的核心思路。二、面试状态机把一场面试结构化我们把一场面试建模成一个状态机流程由代码驱动模型只负责「在当前状态下生成自然语言」。from dataclasses import dataclass, field from enum import Enum class Phase(Enum): OPENING opening # 开场/暖场 MAIN_Q main_question # 主问题作答 FOLLOW_UP follow_up # 追问 TRANSITION transition # 过题 CLOSING closing # 结束语 SCORING scoring # 评分离线 dataclass class InterviewState: position: str # 岗位设定全程不变 questions: list # 预设主问题列表 cur_q_index: int 0 # 当前第几题 phase: Phase Phase.OPENING follow_up_count: int 0 # 当前题已追问几次 max_follow_up: int 2 # 每题最多追问 history_digest: list field(default_factorylist) # 压缩后的历史 answers: list field(default_factorylist) # 结构化留存每题作答每一轮交互代码先看 state 决定该干嘛再把「当前状态 必要上下文」组装成 prompt 喂给模型。模型不需要从一长串 history 里推断「我现在该问啥」——流程控制权在代码手里模型只管把当前这一步说好。状态流转大致是OPENING → MAIN_Q → (要不要追问?)├─ 是 → FOLLOW_UP → (还追吗? 看 follow_up_count)└─ 否 → TRANSITION → 下一题 MAIN_Q → ... → CLOSING → SCORING好处很直接永远不会跑飞、不会漏题、不会重复问、岗位设定全程钉死。模型生成内容里就算偶尔飘状态机也会把它拽回正轨。三、追问决策别让模型「一把梭」最容易出问题的环节是追问。常见错误做法是把用户回答丢给模型直接说「请追问」。结果就是模型为了追问而追问经常追一些无关痛痒甚至answer里根本没提的东西很假。我们把追问拆成两步先决策再生成。第一步判断「该不该追、从哪追」这一步本质是个分析任务单独一次调用temperature0输出结构化结果FOLLOW_UP_DECISION_PROMPT 你是资深公考面试考官。考生对题目「{question}」的作答如下 --- {answer} --- 请判断是否需要追问并以 JSON 输出 {{ need_follow_up: true/false, reason: 为什么追问/不追问, weak_points: [作答中空泛、回避、自相矛盾或值得深挖的点最多2个], suggested_angle: 若追问从哪个角度切入最有价值 }} 判断标准 - 作答空泛、停留在口号如要加强监管但没说怎么加强→ 追具体怎么做 - 作答回避了题目某个关键面 → 追被回避的点 - 作答出现自相矛盾 → 追矛盾处 - 作答已经充分、具体、闭环 → 不追问need_follow_upfalse 注意最后一条作答得好就不追问。 真考官不会对一个答得滴水不漏的人硬挑刺。让模型有「不追问」这个合法选项整个交互的真实感会上一个台阶。第二步基于决策结果生成追问只有 need_follow_uptrue 时才进第二次调用把 suggested_angle 和 weak_points 作为约束生成自然语言追问。这次可以给点温度temp≈0.6让措辞自然。def decide_and_generate_follow_up(state, answer): decision llm_json(FOLLOW_UP_DECISION_PROMPT.format( questionstate.questions[state.cur_q_index], answeranswer)) if not decision[need_follow_up] or state.follow_up_count state.max_follow_up: state.phase Phase.TRANSITION return None state.follow_up_count 1 state.phase Phase.FOLLOW_UP return gen_follow_up_question(state, decision) # temp≈0.6决策与生成分离和我们做申论批改时「评分与点评分离」是同一个思想把需要稳定判断的部分摁到 temp0、结构化输出把需要自然表达的部分单独放开。两个目标解耦互不拖累。四、长对话的上下文压缩状态机解决了「流程不跑飞」但模型生成每一步时仍需要一些历史上下文比如追问时要看到用户原答案。如果把全量 history 一直带着长面试照样上下文膨胀。做法外置一份 history_digest每过完一题就把那一题压缩成一条摘要存进去原始长文本不再进 prompt。def digest_one_question(state, question, answer, follow_ups): summary llm(f把以下一题面试问答压缩成不超过80字的客观摘要 保留考生核心观点、明显短板、追问及其回应。不要评价。 题目{question} 作答{answer} 追问记录{follow_ups}) state.history_digest.append({ q_index: state.cur_q_index, summary: summary }) state.answers.append({ # 原文另存供离线评分用不进对话 prompt question: question, answer: answer, follow_ups: follow_ups })后续轮次的 prompt 里只带 history_digest几条 80 字摘要而不是全部原文。效果方案第8轮 prompt token追问相关性人工评1-5全量 history 直接 append~42003.2状态机 digest 压缩~9004.4token 砍到约 1/4追问质量反而升了——因为模型注意力不再被海量原文稀释。原始作答全文我们另存一份answers只在离线评分时才完整调出来用不参与实时对话。五、评分与对话解耦最后一步打分。我们不在对话过程里实时打分而是面试全部结束后拿离线存下的 answers每题原文 追问回应单独走一遍评分 pipeline。原因有二实时打分会干扰对话节奏且边聊边打的分前后标准不一致评分需要看完整作答原文 temp0 rubric 档位表这套和我们申论批改一模一样不展开可参见上一篇《用大模型做主观题批改怎么保证评分一致性》。对话归对话评分归评分。两条 pipeline 各自优化互不污染。六、踩过的坑坑1追问跑偏问到用户没说的内容。 早期没有「决策」这步直接让模型追问它经常脑补——用户压根没提风险管控它追「你刚才说的风险怎么控制」。解法就是上面的两步法先抽 weak_points 锚定在原答案里追问必须挂靠到考生真实说过的点。坑2上下文爆炸 注意力漂移。 全量 history append 到后面模型开始重复问、忘岗位。解法是状态机 digest 压缩把「记流程」这件事从模型手里收回到代码里。坑3打断/追问时机靠模型自己判断不靠谱。 什么时候该过题、每题追几次这种节奏控制别交给模型即兴发挥。用 max_follow_up、follow_up_count 这种硬计数器在代码里卡死模型只决定「这一次追不追」不决定「整体节奏」。坑4评分维度和对话过程耦合分会飘。 一旦让模型边对话边给印象分最终分受对话顺序影响很大。务必把评分拆成独立的离线环节用固定 rubric 一次性评别让对话过程污染评分。七、小结做一个「不像问卷」的 AI 模拟面试关键不在模型多强在于把控制逻辑从模型手里拿回来状态机控流程第几题、追几次、什么岗全由代码维护模型只生成当前步追问两步法先判断该不该追、从哪追temp0 结构化再生成追问放开措辞外置 digest 压上下文每题压成短摘要原文另存供离线评分长对话不膨胀评分与对话解耦面试结束后离线统一打分rubric 固定不被对话过程带偏。这套思路适用于一切「多轮、有追问、要评估」的对话式 AI——面试、AI 问诊、口语陪练、销售陪练底层都是同一个问题怎么让一个无状态的模型跑出一场有状态、有节奏、有判断的对话。 答案是别指望模型自己撑住用工程把状态机、决策点、上下文、评分一个个框出来。我们在公考赛道做 AI 模拟面试和 AI 批改智蛙面试 / 智蛙公考上面这些都是真刀真枪踩出来的。做对话式 AI 评估场景的同行评论区交流。