news 2026/9/30 5:55:26

《码上面试》Agent开发实战:手写ReAct构建AI面试官全解析

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
《码上面试》Agent开发实战:手写ReAct构建AI面试官全解析

我最近在做一个叫《码上面试》的小项目,核心思路是让 AI Agent 扮演面试官和陪练,帮程序员准备技术面试。整个项目从零起步,踩了不少坑,也积累了不少关于 Agent 开发的一手经验。我打算用几篇博文把它记录下来,这篇是第一篇,重点讲整体设计思路、技术选型,以及最初两周的搭建过程。如果你正好在学 Agent 开发,或者想做一个类似的 AI 工具,这篇应该能帮你避开不少弯路。

1. 为什么用 Agent 做“码上面试”

1.1 项目最初的“荒诞”想法

一开始我只是想要一个能陪我问八股文的聊天机器人。传统的问答模式很简单,把面试题塞给大模型,让它回答,再把参考答案调出来对比。但真用起来会发现一个问题:面试不是一问一答的考试,而是有追问、有打断、有场景模拟的动态过程。候选人说“我熟悉 MySQL 索引”,面试官会立刻追问“那你给我讲讲最左前缀匹配在联合索引里的底层实现”;候选人说“我用过 Redis 做缓存”,面试官可能追问“缓存穿透、击穿、雪崩你怎么区分,方案分别是什么”。

这种追问逻辑,传统 Chatbot 很难做到。因为它只是被动地等下一轮输入,不会主动决定“下一步该问什么”。而 Agent 不同,它有目标拆解和工具调用的能力。于是我把“码上面试”重新定义为一个 Agent 项目:一个能自主规划面试流程、自动追问、甚至能运行代码验证答案的智能体。

1.2 从传统 Chatbot 到 Agent 的关键转变

普通对话机器人是“输入一句话,输出一句话”,本质上是一个条件反射。Agent 则像一个有工作记忆的实习生:它接到任务“模拟一场 Java 后端面试”,会先把任务拆成几个阶段——开场自我介绍、基础语言问题、框架深挖、算法手写、反问环节。

然后每进行到一个阶段,它都会根据候选人之前的回答决定下一步动作:继续问、换个角度问、还是终止当前话题。这就是 Agent 和 Chatbot 最本质的区别:Agent 有“行动循环”,它能在每个决策点选择不同的工具(搜索题库、执行代码、查简历),而不是只会生成文本。

这个项目最具挑战的地方也在这里:如何让 Agent 的“面试官”角色足够逼真,而不是一上来就给答案的“解说员”。

1.3 核心需求清单

我梳理了第一版必须要做的功能,优先级从高到低:

  • 多轮模拟面试:支持角色扮演,Agent 按照面试官话术提问,候选人用键盘回答。
  • 追问衔接:根据候选人的答案,从题库中挑选合适的追问,而不是固定剧本。
  • 代码运行验证:当候选人写代码时,Agent 能把代码放到沙箱里跑一遍,判断结果。
  • 面试总结:结束后生成一份评估报告,标注候选人的优点、漏洞、建议学习方向。
  • 记忆能力:记录候选人在本次面试中说过的话,避免前后矛盾(比如前面说“精通 JVM”,后面连内存区域都说不清)。

这些需求单靠一个“调用大模型接口”的脚本做不到,必须有一个能管理状态、调用工具、做决策的 Agent 框架。这让我开始认真研究当前主流的 Agent 技术栈。

2. 技术选型:主流 Agent 框架到底怎么选

2.1 我对比过的框架梯队

说实话,2024 年之后 Agent 框架的更新速度真的离谱。我前后试了 LangChain、AutoGen、MetaGPT、Dify,还有 TTFT 一类的新项目,简单整理下我的真实感受:

框架定位优点缺点
LangChain底层工具箱生态最全,LCEL 表达清晰抽象太重,版本升级频繁,API 变动大
AutoGen多 Agent 对话设计思路新颖,适合多角色博弈概念复杂,调试难度高,黑盒行为多
MetaGPT软件公司模拟对“标准化流程”的模拟很惊艳偏重项目交付场景,面试场景不太匹配
Dify低代码平台可视化编排,适合快速验证灵活度不够,想加自定义逻辑比较头疼
自研 ReAct 壳极简方案逻辑完全可控,适合学习需要自己处理一切边界情况

我的感受是,主流框架各有各的“脾气”。LangChain 看着功能多,但很多模块我根本用不上,还得天天跟着它升级改代码。AutoGen 的多 Agent 对话很有意思,但我想象了一下面试场景——面试官、候选人、评审员三个 Agent 互相说话,这种结构反而会让延迟变高一倍,每次追问都要等好几个模型调用。

2.2 为什么最终选了“手写 ReAct”而不是套框架

我在第 2 周几乎决定用 LangChain 了,直到我画了一遍流程图。画到“工具调用失败后如何重试”这个分支时,发现我需要查 LangChain 的 AgentExecutor 源码才能知道它到底怎么处理异常。相比之下,用一个极简的 ReAct 循环(Reason + Act + Observation),所有流程都在自己眼皮底下,出现问题一眼就能定位。

ReAct 模式的核心思想很简单:让模型先推理(Reason),再行动(Act),然后观察结果(Observation),循环往复。这个思路来自 2022 年的经典论文,到现在依然是 Agent 的主流范式。我自己手写一个轻量级 ReAct 壳也就 300 行代码,但换来的是完全的可控性。

比如说,面试 Agent 的决策总是围绕“下一步动作”展开,我可以用一段很清晰的代码表达:

while True: # Reason: 让大模型基于历史对话,推理当前状态应该做什么 thought = llm_chat(memory, current_tool_schema) # Act: 解析模型输出中的动作字段 action = extract_action(thought) # "continue_ask", "run_code", "finish", "deep_support" if action == "finish": break # Observation: 执行动作后,把结果反馈给模型 observation = execute_action(action, context) memory.add(observation)

这个骨架看起来简单,但它解决了两个核心问题:一是让模型在“提问”和“评价”之间交替,符合面试官行为;二是所有动作都有真实结果反馈,比如跑代码的输出,而不是模型自己编造的“代码应该能跑”。

2.3 基础架构:一个最小可跑的 Agent 骨架

我的架构分四层:

  • 最外层是 HTTP 接口层:用 FastAPI 暴露一个/interview端点,前端页面通过 WebSocket 实时收发消息。
  • 第二层是 Agent 主循环:负责维护状态、调用大模型、解析决策、执行动作。这一层我完全没有用框架,纯手写。
  • 第三层是工具集:包括题库检索工具、代码沙箱工具、简历解析工具。
  • 第四层是记忆层:用 Redis 存短期会话状态,用 SQLite 存每次面试的记录摘要。

这套架构撑起整个“码上面试”项目,规模不大,但我已经能很清晰地控制它。如果你也想做类似的项目,我建议你从一开始就至少分开这三层,别把工具调用代码跟主循环糊在一起,不然调试的时候会想哭。

3. 核心模块设计与实现

3.1 面试官 Agent 的提示词工程

提示词是决定 Agent 表现得是不是一个合格面试官的关键。我踩过最大的坑是:如果只告诉模型“你是一个面试官”,它大概率会变成一个“表扬型”面试官——候选人说一句,它就夸一句“很棒”“Good”,然后很快给出答案。这完全不是真实的面试。

我后来把提示词拆成了角色设定、行为约束、追问策略三块。其中一段核心提示词是这样的:

你正在为一家大型互联网公司进行后端 Java 岗位的技术面试。 你的目标是:评估候选人的真实水平,不要轻易表扬,也不要直接给答案。 当候选人给出模糊回答时,你有两个选择: 1. 追问:针对细节继续提问,直到你确认他确实理解或确实不懂。 2. 跳转:当某个知识点完全答不出来时,换一个相关但有梯度的新题。 注意:候选人每一次回答后,你必须先在内部做出判断(掌握/一般/不会), 然后基于判断给出下一个动作,而不是机械地把题库里的下一题读出来。

这段提示词里最重要的不是“你是面试官”这句,而是“基于判断给出下一个动作”。为了让模型做到这一点,我在模型输出格式里强制它先输出一个_assessment字段,再输出_action:

{ "_assessment": "candidate_knows_basic_mysql_index_but_confused_on_lexical_order", "_action": "deep_support", "_question": "那联合索引 idx(a,b,c) 的情况下,where b=1 and a=2 能用到索引吗?为什么?" }

有了这个结构,模型就不再是瞎聊,而是真的像面试官一样带着一个判断清单在面试。

3.2 工具调用:让 Agent 真正“码上”能跑

“码上面试”的核心场景是手写代码。如果 Agent 只能聊算法思路,那跟 Web 搜题没区别。所以我在工具层做了两个代码相关工具:

  • write_code_to_file:把候选人给出的代码保存到临时文件。
  • run_code_sandbox:在 Docker 容器里执行该代码,返回 stdout 和异常堆栈。

为什么用 Docker 沙箱?因为候选人可能上传任意代码,里面可能包含恶意的文件读写甚至网络请求。虽然我们的用户都是正经程序员,但安全习惯还是要养成。我用的是官方 python:3.10-slim 镜像,每次执行挂载一个临时目录,用完自动销毁容器。执行命令大致如下:

docker run --rm --network=none -v /tmp/interview_code_dir:/workspace -w /workspace python:3.10-slim python main.py

重点关注--network=none,直接切断网络,防止代码偷偷请求外部服务。这是我在做 Agent 安全时学到的重要原则:工具调用必须做最小权限隔离,哪怕牺牲一点便利性。

当候选人的代码运行结果报错时,Agent 不会直接把错误原样贴给候选人,而是会在内部判断:错误是候选人代码逻辑有问题,还是题目理解偏差?然后根据情况决定是让候选人自己调试,还是给一点提示。这需要把带错误堆栈的观察结果连同“该不该给提示”的指令一起交给模型推理。

3.3 记忆与上下文管理

做 Agent 的人都避不开记忆的问题。我在“码上面试”里把记忆分成了三个层级:

  • 短期会话记忆:保存在 Redis,TTL 设为 30 分钟,记录本次面试的完整对话。
  • 中期要点记忆:每个候选项回答完之后,我让 Agent 提炼一个 50 字以内的要点摘要,存到结构化字段里,防止 Token 过多导致上下文爆炸。
  • 长期用户画像:面试结束后把候选人的表现存入 SQLite,下次再来面试时,Agent 会先读取画像,说一句“上次你在并发编程方面比较薄弱,今天我们重点考察这里”。

这个分层的意义在于:模型调用时不可能把所有的历史问答都塞进去。我统计过,一次 20 轮面试的完整记录大约要 1 万 5 千个 Token,直接全量传给 LLM,成本高且容易偏离重点。用“中期要点记忆”把每次回答压缩成总结,再配合“短期完整对话”用于追问,效果平衡了很多。

如果你也在做 Agent 记忆,建议一开始就考虑“总结记忆 + 原始记忆”双通道。不要只留原始数据,模型会在几轮之后彻底迷失。

3.4 题库与知识库接入

为了让 Agent 能随时追问,我准备了一个小型题库,按知识点分 tag。每个题目至少包含:难度、父子题关系、标准答案、常见误区。Agent 拿到一个题目之后,可以通过“相关题目检索”工具,找到同知识点的更深入题目。

我用的是最基础的向量检索:SentenceTransformer把题目文本向量化,存入 Chroma,拿到候选人的回答后,用余弦相似度找出最相近的追问题目。这一步没什么高深的,但一定要提前做好。

后来我发现一个很有意思的点:面试官在追问时不是只选最相似的,而是要选“难度阶梯”上的下一题。比如候选人刚才答对了“什么是索引”,接下来应该问“最左前缀原则”,而不是问“B+树的叶子节点存了什么”。这就是“面试梯度”问题,单纯靠向量相似度找不出来。我最终的做法是把题目表中的level字段和相似度联合排序,让 Agent 在顶层只做决策,具体选哪道题交给工具逻辑保证梯度合理。

4. 完整实操过程

4.1 环境准备与依赖

我的开发环境是 MacBook Pro M2,系统是 macOS Sonoma。主要的依赖如下:

  • Python 3.11
  • FastAPI + Uvicorn
  • OpenAI SDK(兼容接口)
  • Redis(本地用 Docker 跑)
  • Chroma(向量库)
  • Docker CLI(跑代码沙箱)

没有直接用某个 Agent 框架,只在项目里写了几个抽象类。这样做的劣势是开发速度稍微慢一点,但优势是我能完全掌控整个流程的每一个环节。对于学习 Agent 而言,这个阶段“慢就是快”。

4.2 逐步搭建过程记录

我按以下顺序推进,大家可以参考:

  1. 先写一个最丑的 main.py:把大模型的对话接起来,能一问一答,就说明基本链路通。
  2. 加 ReAct 循环:不要想太多,先让模型输出动作字段,然后手动执行动作。
  3. 接入题库检索:优先用简单的trie或sqlite直接查,等有了数据再升级到向量检索。
  4. 接入 Docker 沙箱:写个脚本,在容器里执行候选人的代码,并返回输出。
  5. 优化提示词:通过 10 轮模拟面试来调整,每次结束后看模型的_assessment是否准确。
  6. 加记忆层:先做 Redis 存储完整对话,再加总结记忆。
  7. 加前端页面:用 Vue3 + WebSocket,模拟一个聊天界面。

每一步之间都用 Git 打标签,第 1 步花了我半天,第 3 步因为心太急,想直接上向量检索,结果浪费了一整天。所以这里一定要提醒大家:先用最容易跑通的方式实现,别一开始就追求高级方案。最低成本的路径往往才是最快路径。

4.3 一次真实的模拟面试运行

下面是一次简单运行的日志片段(脱敏后):

用户进入面试,Agent 读取用户画像:无历史记录,第一次面试。 Agent 决策:开场自我介绍。 候选人:我是 3 年后端开发,主要做 Java,用过 MySQL、Redis,熟悉 Spring Cloud。 Agent _assessment: candidate_general_java_exp, might_lack_deep_concurrency Agent _action: deep_support Agent 提问:你刚才说熟悉并发编程,实际项目中遇到过并发问题吗?具体怎么解决的? 候选人:用 synchronized 和 ReentrantLock 处理,但不确定两者的区别。 Agent _assessment: know_usage_but_confused_principle Agent _action: continue_ask Agent 提问:为什么 ReentrantLock 需要手动释放锁?如果忘了释放会发生什么? ...

可以看到,Agent 会依据候选人的回答实时调整追问方向,而不是按固定顺序念题。这就是 ReAct 模式的直观效果。不过要说明的是,这一步我花了不少时间打磨提示词和评估逻辑,并不是第一次跑就这么顺利。

5. 踩坑实录与问题排查

5.1 问题一:Agent 陷入循环对话

现象:模型在一个知识点上反复追问,同一个问题换个说法问了三次,候选人都答不上来了,它还在继续问。

原因:我在提示词里给了它两个动作“追问 / 跳转”,但它缺少一个“终止话题”的动作。当模型看到候选人的_assessment是“不会”时,它没有合适的出口,只能继续追问,试图得到一个“会”的答案。

解决办法:加了_action: stop_and_switch动作,规定当候选人连续两次答不出同一知识点时,必须停止并换一个新的话题。同时在提示词中明确说明“无法通过追问让候选人从不会变成会”,要求模型尊重事实。

5.2 问题二:工具调用总是失败

现象:调用代码沙箱时,模型输出的 JSON 格式不合法,内部json.loads直接报错。后来发现最频繁出错的字段是代码内容里带了换行符和引号,模型无法正确转义。

解决办法:不直接要求模型输出“完整代码”的 JSON,而是让它只输出一个代码路径,代码内容由前端页面上传时跟候选人键入内容一起传过来。这相当于把 Agent 从“生成代码”中解放出来,代码生成与代码执行解耦。如果你做 Agent 时也遇到工具参数解析问题,建议把大参数(如代码、长文本)跟短参数(动作类型、题目ID)分开传递,别挤在一个 JSON 里。

5.3 问题三:记忆混乱与上下文溢出

现象:面试进行到第 15 轮时,Agent 开始重复提问或者把候选人之前说过的观点记混,甚至把候选人的项目经历说成了自己的。

原因:全量对话拼接超过了 8K 上下文窗口,早期信息被截断,模型丢失了关键约束。

解决办法:做了二次压缩。每轮面试之后,我让模型生成一段“候选人能力画像”,包括已覆盖知识点、表现等级、需要深入的方向。后续调用只传这份画像 + 最近三句对话,而不是全部历史。这个方案非常有效,而且让每次回答的 Token 消耗下降了近一半。

5.4 常见问题速查表

问题表现原因解决办法
循环追问同一知识点反复问缺少终止动作增加stop_and_switch动作
JSON 解析失败工具调用报错大模型生成非法 JSON拆分参数,代码和动作分别传递
记忆混乱重复提问、事实混淆上下文过长引入画像 + 最近对话的压缩记忆
模型直接给答案不像面试官提示词没有设行为约束要求先输出_assessment,再输出问题
沙箱执行卡死迟迟无结果候选人代码有死循环设置超时参数--stop-timeout
追问没有梯度问完简单直接问超难只按相似度选题联合 level 字段排序

最后再分享一个小技巧:如果你想加速 Agent 的开发迭代,一定要给自己的项目建立一个“模拟面试录播回放”功能。每次面试结束后,把 Agent 的思考链、动作、候选人的回答全部存下来,随时回放。这比看日志高效得多,它让你能一眼看出模型是在哪个环节做出了错误判断。我自己就是靠这个功能,在三天内把追问准确率从 43% 提到了 72%。

后续我会继续更新第二篇,重点讲怎么把面试评估报告做得更可靠,以及如何引入多个 Agent 做交叉评审。如果你也正在做 Agent 相关的学习项目,欢迎在评论区分享你遇到的问题,我们一起琢磨。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/30 5:53:21

从二进制到补码、浮点与文件签名:底层数据表示全解析

上次带一个刚转行做后端的同学看串口抓包日志,他盯着满屏的 0 和 1 冒出一句:计算机为什么非得用二进制?十进制不是更贴近人的习惯吗?这个问题听着像入门第一课的课后题,可它牵出来的东西一点都不浅——内存怎么存数、…

作者头像 李华
网站建设 2026/9/30 5:52:35

Saddle实战:可视化任务流平台如何破解AI/MLOps落地难题

1. AI/MLOps这块硬骨头,到底难啃在哪先说一个我观察到的现象:很多团队在模型训练阶段一马平川,一到上线就进入"鬼打墙"状态。训练好的模型孤零零躺在模型仓库里,算法工程师说不清"我这段预处理逻辑线上跑没跑"…

作者头像 李华
网站建设 2026/9/30 5:52:34

Linux ln命令详解:硬链接、符号链接与生产实践

1. 从一次"删了源文件,链接就废了"的线上事故说起几年前我接手过一个发布流程的重构,前任留下的部署脚本里有一堆软链接:/opt/app/current指向/opt/app/releases/20230512这类目录,灰度切流全靠改这个链接。某次清理磁盘…

作者头像 李华
网站建设 2026/9/30 5:52:27

Nginx静态网站部署实战:从安装配置到性能优化

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华