AI 模拟面试怎么做:智蛙公考智能体多轮对话 + 实时追问的工程实现

「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_factory=list)  # 压缩后的历史
    answers: list = field(default_factory=list)         # 结构化留存每题作答

每一轮交互,代码先看 state 决定该干嘛,再把「当前状态 + 必要上下文」组装成 prompt 喂给模型。模型不需要从一长串 history 里推断「我现在该问啥」------流程控制权在代码手里,模型只管把当前这一步说好。

状态流转大致是:

OPENING → MAIN_Q → (要不要追问?)

├─ 是 → FOLLOW_UP → (还追吗? 看 follow_up_count)

└─ 否 → TRANSITION → 下一题 MAIN_Q → ... → CLOSING → SCORING

好处很直接:永远不会跑飞、不会漏题、不会重复问、岗位设定全程钉死。模型生成内容里就算偶尔飘,状态机也会把它拽回正轨。

三、追问决策:别让模型「一把梭」

最容易出问题的环节是追问。常见错误做法是:把用户回答丢给模型,直接说「请追问」。结果就是模型为了追问而追问,经常追一些无关痛痒甚至answer里根本没提的东西,很假。

我们把追问拆成两步:先决策,再生成。

第一步:判断「该不该追、从哪追」

这一步本质是个分析任务,单独一次调用,temperature=0,输出结构化结果:

复制代码
FOLLOW_UP_DECISION_PROMPT = """
你是资深公考面试考官。考生对题目「{question}」的作答如下:
---
{answer}
---
请判断是否需要追问,并以 JSON 输出:
{{
  "need_follow_up": true/false,
  "reason": "为什么追问/不追问",
  "weak_points": ["作答中空泛、回避、自相矛盾或值得深挖的点,最多2个"],
  "suggested_angle": "若追问,从哪个角度切入最有价值"
}}
判断标准:
- 作答空泛、停留在口号(如'要加强监管'但没说怎么加强)→ 追"具体怎么做"
- 作答回避了题目某个关键面 → 追被回避的点
- 作答出现自相矛盾 → 追矛盾处
- 作答已经充分、具体、闭环 → 不追问,need_follow_up=false
"""

注意最后一条:作答得好就不追问。 真考官不会对一个答得滴水不漏的人硬挑刺。让模型有「不追问」这个合法选项,整个交互的真实感会上一个台阶。

第二步:基于决策结果生成追问

只有 need_follow_up=true 时才进第二次调用,把 suggested_angle 和 weak_points 作为约束生成自然语言追问。这次可以给点温度(temp≈0.6)让措辞自然。

复制代码
def decide_and_generate_follow_up(state, answer):
    decision = llm_json(FOLLOW_UP_DECISION_PROMPT.format(
        question=state.questions[state.cur_q_index], answer=answer))
    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

决策与生成分离,和我们做申论批改时「评分与点评分离」是同一个思想:把需要稳定判断的部分摁到 temp=0、结构化输出;把需要自然表达的部分单独放开。两个目标解耦,互不拖累。

四、长对话的上下文压缩

状态机解决了「流程不跑飞」,但模型生成每一步时仍需要一些历史上下文(比如追问时要看到用户原答案)。如果把全量 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 | ~4200 | 3.2 |
| 状态机 + digest 压缩 | ~900 | 4.4 |

token 砍到约 1/4,追问质量反而升了------因为模型注意力不再被海量原文稀释。原始作答全文我们另存一份(answers),只在离线评分时才完整调出来用,不参与实时对话。

五、评分与对话解耦

最后一步:打分。我们不在对话过程里实时打分,而是面试全部结束后,拿离线存下的 answers(每题原文 + 追问回应)单独走一遍评分 pipeline。

原因有二:

实时打分会干扰对话节奏,且边聊边打的分前后标准不一致;

评分需要看完整作答原文 + temp=0 + rubric 档位表(这套和我们申论批改一模一样,不展开,可参见上一篇《用大模型做主观题批改怎么保证评分一致性》)。

对话归对话,评分归评分。两条 pipeline 各自优化,互不污染。

六、踩过的坑

坑1:追问跑偏,问到用户没说的内容。 早期没有「决策」这步,直接让模型追问,它经常脑补------用户压根没提风险管控,它追「你刚才说的风险怎么控制」。解法就是上面的两步法,先抽 weak_points 锚定在原答案里,追问必须挂靠到考生真实说过的点。

坑2:上下文爆炸 + 注意力漂移。 全量 history append 到后面,模型开始重复问、忘岗位。解法是状态机 + digest 压缩,把「记流程」这件事从模型手里收回到代码里。

坑3:打断/追问时机靠模型自己判断不靠谱。 什么时候该过题、每题追几次,这种节奏控制别交给模型即兴发挥。用 max_follow_up、follow_up_count 这种硬计数器在代码里卡死,模型只决定「这一次追不追」,不决定「整体节奏」。

坑4:评分维度和对话过程耦合,分会飘。 一旦让模型边对话边给印象分,最终分受对话顺序影响很大。务必把评分拆成独立的离线环节,用固定 rubric 一次性评,别让对话过程污染评分。

七、小结

做一个「不像问卷」的 AI 模拟面试,关键不在模型多强,在于把控制逻辑从模型手里拿回来:

状态机控流程:第几题、追几次、什么岗,全由代码维护,模型只生成当前步;

追问两步法:先判断该不该追、从哪追(temp=0 结构化),再生成追问(放开措辞);

外置 digest 压上下文:每题压成短摘要,原文另存供离线评分,长对话不膨胀;

评分与对话解耦:面试结束后离线统一打分,rubric 固定,不被对话过程带偏。

这套思路适用于一切「多轮、有追问、要评估」的对话式 AI------面试、AI 问诊、口语陪练、销售陪练,底层都是同一个问题:怎么让一个无状态的模型,跑出一场有状态、有节奏、有判断的对话。 答案是别指望模型自己撑住,用工程把状态机、决策点、上下文、评分一个个框出来。

我们在公考赛道做 AI 模拟面试和 AI 批改(智蛙面试 / 智蛙公考),上面这些都是真刀真枪踩出来的。做对话式 AI 评估场景的同行,评论区交流。

相关推荐
帅次2 小时前
Android 高级工程师面试:Java 基础知识 近1年高频追问 22 题
android·java·面试
林希_Rachel_傻希希7 小时前
web性能优化之————图片效果
前端·javascript·面试
sugar__salt7 小时前
手撕字符串算法:反转、回文、验证回文 Ⅱ 完整拆解
javascript·算法·面试·职场和发展
骑士雄师10 小时前
java面试记录: sychonized 锁,熔断组件,分布式锁
java·开发语言·面试
贺国亚10 小时前
AI制品Registry与发布门禁
面试
AI人工智能+电脑小能手11 小时前
【大白话说Java面试题 第151题】【06_Spring篇】第11题:说一下 Spring Bean 的生命周期?
java·开发语言·后端·spring·面试
白露与泡影13 小时前
2026大厂Java后端面试实战记录(含答案):八股/场景/项目/AI全覆盖,短期速通
java·人工智能·面试
禅思院13 小时前
AI对话前端从入门到崩溃:一个长对话引发的五层优化战争【引子】
前端·面试·架构
林希_Rachel_傻希希15 小时前
web性能之相关路径——AI总结
前端·javascript·面试