ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

从0到1手写ReAct Agent:面试辅导项目实践与踩坑记录

从0到1手写ReAct Agent:面试辅导项目实践与踩坑记录 做Agent开发也有一年多了一直停留在看文档、跑Demo的阶段。这次决定拿一个真实场景练手选了码上面试这个题目——做一个能模拟技术面试官、帮程序员练习面试的 Agent 项目。一方面是面试辅导这个场景天然适合 Agent有明确的目标、需要连续对话、要调用工具评估代码另一方面作为大模型开发工程师我也想借这个项目把 Agent 的规划、记忆、工具调用、反思这几块能力完整过一遍而不是继续碎片化地刷教程。《码上面试》Agent 项目学习记录一主要是第一阶段的总结从需求拆解、技术选型到把提问-回答-评估这条主链路跑通再到实测中暴露出来的一堆问题。文章会尽量把踩坑过程写细包括每个环节当时为什么那样选、后来为什么改。如果你正准备从0到1搭一个自己的 Agent 项目或者面试辅导类产品这篇应该能帮你少走不少弯路。1. 为什么面试辅导必须做成 Agent而不是一个聊天模板1.1 需求缘起刷题的人最缺的不是题是被面试的感觉我最初的想法很简单写一个提示词模板往里塞用户的目标岗位和技能栈让大模型扮演面试官出题。结果试了一轮就发现不行——普通对话模板有几个硬伤。第一无状态。用户答完第一题模板不知道前面问过什么很容易重复出题。真实面试中面试官是会顺着你的回答追问的这个追问链条需要一个状态机来维护。第二无法评估。用户手写了代码答案模型只能看一眼说还不错不会真的把代码跑起来验证边界条件。要判断代码到底对不对必须有一个代码执行工具这已经超出对话模板的范畴了。第三用户不知道怎么引导。很多人用 ChatGPT 刷题问题出在不会问——不知道要设定什么角色、给什么上下文、怎么让 AI 像面试官一样逼问而不是像老师一样讲解。如果产品本身就是一个定制的面试官 Agent用户只需要坐上去答题这个门槛就被抹掉了。码上面试这个名字也是这个意思一方面指程序员码农的面试另一方面谐音马上面试——打开就能开练。1.2 Agent 和聊天模板的本质区别在哪里面试场景要跑起来需要四样东西恰好对应 Agent 的四个核心能力面试场景需求Agent 能力聊天模板能否满足记住答过什么、问过什么记忆Memory部分满足受上下文窗口限制决定下一题问什么、是否追问规划Planning弱靠人肉引导运行用户提交的代码、验证结果工具使用Tool Use不满足判断用户哪里答得不好、给出反馈反思Reflection弱只能给泛泛评价这个对照表想清楚之后结论就很明确了与其维护一大段复杂的角色提示词去假装Agent不如直接做一个有状态循环、带工具调用能力的轻量 Agent。所以我决定不依赖现成的 LangChain 之类重框架自己手写一个简版 ReAct Agent把每个环节控制在自己手里。1.3 第一版的功能边界先跑通主链路不做花活MVP 阶段我给自己划定的范围是支持用户选择目标职级初级/中级/高级和岗位方向后端/前端/算法Agent 自动出题出题后等用户提交代码答案用户提交代码后Agent 调用沙箱执行代码跑测试用例一轮回答完成后Agent 决定继续追问还是换下一题面试结束后输出一份评估报告正确性、复杂度、代码风格、沟通表达明确不做的不做海量题库题目由模型动态生成、不做音视频模拟、不做多 Agent 协作。一个 Agent 先把链路跑通再谈扩展。2. 技术选型的心路历程为什么最终手写 ReAct2.1 框架选型LangChain、LlamaIndex 还是自研选型的时候我把主流的 Agent 框架都简单评估了一遍。LangChain 确实功能全LCEL 表达式、各种 Chain、各种工具的集成都很完善但问题也出在太全上。默认抽象层级太多一个简单的 Agent 调用被包了好几层调试的时候经常需要在框架源码里翻半天才能找到真正的 LLM 调用逻辑。我不想在项目初期就把时间花在学框架上。LlamaIndex 更适合做 RAG 场景检索、索引这些做得很好但面试官 Agent 的核心是多轮状态管理 工具调用并不是检索增强用它有点牛刀不对马嘴。最后决定是自己写一个 30 行核心循环的 ReAct Agent。这个选择基于两点考虑面试场景的状态流出题→答题→评估→追问→换题用代码显式控制比在框架配置里声明更直观这个项目本身定位就是学习记录手写一遍才能把 Agent 的骨架吃透这个决定后来被证明是值得的因为后面调 Prompt、加工具、压 Context每一步我都知道整个链路是怎么回事排查问题没有黑盒。2.2 模型选择推理优先兼顾成本模型选了支持工具调用的推理类模型。原因很直接这个场景需要模型做几件事——根据对话历史判断当前该出题还是该追问、决定是否调用代码执行工具、对用户代码进行归纳分析。这些都属于推理密集型任务通用小模型在指令遵循和多步推理上明显吃力。实测下来在频繁调用工具的循环里模型的指令遵循能力比想象中更重要。如果你也准备做类似的 Agent建议优先关注这几个维度是否支持稳定的 JSON 结构化输出是否会自作主张跳过工具直接给结论上下文超过 20 轮后是否还稳定第一个版本先保证效果成本问题留到后面优化。用了一段时间后我发现真正吃成本的是代码评估环节——每次评估要往模型里塞一堆代码和期望输出Token 消耗很大这个后面要专门优化。2.3 工具清单最小够用宁可少不要多Agent 的工具设计我把控得很紧第一期只挂了三个code_executor把用户提交的代码放到沙箱里跑返回执行结果和测试用例通过情况question_bank查知识点覆盖情况避免连续两题都考同一个主题这个用简单关键词匹配实现没有接向量库report_writer面试结束时把所有问答记录打包生成结构化评估报告工具不是越多越好。每个工具都可能被模型误调用工具多了反而增加不确定性。比如我一开始还想加一个搜索题目解析的工具后来发现模型经常在用户还没答题的时候就忍不住调用它——这就把面试变成了开卷考试。后来干脆把那个工具下线了只保留执行类和分析类工具。2.4 整体结构一个循环 一张状态表整个 Agent 的运行时可以概括成一句话在一个 while 循环里不断让模型观察状态、决定动作、执行动作直到状态机进入终态。核心循环的伪代码如下async def run_interview(session_state: InterviewState): while session_state.status ! finished: # 1. 把状态和对话历史拼成消息发给模型 messages build_messages(session_state) # 2. 模型决定下一步动作ask_question / call_tool / judge / finish decision await llm.decide(messages, toolsTOOL_SCHEMAS) # 3. 执行动作 if decision.action call_tool: tool_result await dispatch_tool(decision.tool_name, decision.args) session_state.tool_logs.append(tool_result) # 工具结果回填给模型 elif decision.action ask_question: await send_to_user(decision.question) session_state.status waiting_answer break # 把控制权交回给用户等用户回答 elif decision.action finish: report await generate_report(session_state) session_state.status finished状态表是这个项目的灵魂我迭代了好几版。最终用的字段大概是下面这些状态字段类型说明current_roundint当前第几轮面试target_levelstr用户选择的职级影响出题难度question_historylist已出的题目列表防重current_questiondict当前题目内容含期望考察点user_answerstr用户最近的代码答案tool_logslist工具调用记录eval_scoreslist每道题的评分记录statusstrquestioning / waiting_answer / judging / finished一开始我没有设计 current_question 和 user_answer 这两个字段结果模型经常忘记用户刚提交的代码长什么样评估的时候只能凭印象说你的代码看起来不错。加了这个字段后把用户答案显式注入到评估 Prompt 里效果立刻不一样了。3. 从0到1跑通提问-回答-评估完整链路3.1 面试官 Prompt到底该怎么写Prompt 是 Agent 的灵魂尤其面试官这个角色写不好就会变成好为人师的啰嗦助手。我第一版 Prompt 写得非常细把面试流程、评分标准、注意事项全塞进去了结果模型表现得像个背诵课文的考生说话一股官腔。后来我换了一个思路不要试图用 Prompt 控制所有细节只定义角色边界和硬性规则剩下的让模型自己发挥。最终版的核心 Prompt 大概是这样的已精简你是一名资深技术面试官正在为一个{target_level}级别的{job_role}岗位进行面试。 你的责任 1. 根据用户当前的技术水平出合适的题目从易到难递进 2. 用户提交代码后调用 code_executor 工具运行代码并验证边界条件 3. 根据运行结果和代码质量决定是继续追问还是换下一题 4. 面试满 5 轮或用户要求结束时输出评估报告 硬性规则 - 用户提交代码前不能给出解题思路或提示 - 调用工具前必须先说明你为什么要调用这个工具一句即可 - 除非用户明确要求否则不要解释概念你是面试官不是培训老师 - 每次只问一个问题不要一口气抛三个子问题这里有个很关键的经验明确告诉模型不要做什么往往比要做什么更有效。特别是不要解释概念这条直接治好了模型忍不住给用户上课的毛病。3.2 循环里的追问和换题是怎么决策的面试官 Agent 和普通聊天工具最大的差异在于决策点。用户回答完一道题模型需要判断继续追问深挖还是给一道新题我把这个决策明确建模成一个评估-决策两步第一步模型先对当前回答做一个内部评估不出给用户看评估内容包括代码是否能通过基本测试等 code_executor 的结果是否覆盖了题目考察的核心知识点有没有明显可以追问的漏洞或优化空间第二步基于评估结果选择动作。这里我用了一个小技巧把动作选项压缩成三个降低模型的选择难度。ACTION_OPTIONS [ follow_up: 追问一个与当前题目相关但更难的问题, next_question: 换一道新题覆盖其他知识点, finish: 面试已满5轮或用户要求结束,进入评估阶段 ]这个设计的初衷是减少自由发挥的空间。最初我让模型自由决定接下来该说什么结果它经常在用户只回答了一半的时候就滔滔不绝地讲评把面试变成了单口相声。约束动作空间之后行为明显收敛了。3.3 出题逻辑如何避免每次都出两数之和第一期没接题库纯靠模型生成题目最担心的是出题质量不稳定。实测中我总结了一套有效的控制方法给一道题设置三个约束维度全部注入出题 Prompt。请出一道编程题要求 - 难度{target_level}初级基础语法与简单数据结构中级常见算法与设计模式高级复杂算法、并发或系统设计 - 知识点偏重{job_role}方向的核心技能 - 场景题目要贴近真实业务不要用纯抽象的算法题除非是面向算法岗第三个约束很有效果。以前模型出的题全是单调的算法题加了这个限制之后后端岗位会出设计一个带过期时间的本地缓存这种贴近工作的题前端岗位会出实现一个带防抖功能的搜索框这类题。对用户来说练这种题的收获感明显大于刷八股。防重复出题则是靠 question_bank 工具做简单关键词去重。模型出题前会先调用这个工具传一个知识点标签工具返回这个知识点最近已经考过建议换一个模型就会换方向。这个机制简单但足够用。3.4 代码评估先跑测试再谈主观代码评估这块我踩过最大的坑是模型会在没跑代码的情况下仅凭看到代码长得挺整齐就给出好评。这事特别误导人。解决方案是硬性流程用户提交代码后系统先把代码传给 code_executor并且要求代码执行结果必须先于模型评论呈现在 Prompt 里。工具调用返回后模型才能进入评估环节否则就一直停留在 waiting_answer 状态。code_executor 内部做的事情很简单# 简化版构建一个隔离执行环境把用户代码和三个测试用例放进去跑 test_cases generate_test_cases(current_question, target_level) result run_in_sandbox(codeuser_code, teststest_cases) # 返回值形如: # {passed: 2, total: 3, failures: [case2: 空数组场景超时], exec_log: ...}测试用例是模型生成题目时一起生成的。这里我也踩过一个坑让模型随意生成几个测试用例质量很差后来改成要求它输出格式固定的用例 JSON包含输入、期望输出、理由三个字段。理由字段用来防止模型瞎编测试用例——如果它说不清楚这个用例在测什么就重生成。评分我最后定成五个维度每个维度 1-5 分维度评分依据正确性测试用例通过率复杂度时间/空间复杂度是否在合理范围代码风格命名、结构、是否有明显坏味道沟通表达解题思路讲解是否清晰应变能力被追问时的反应质量评估报告在面试结束时由 report_writer 工具汇总生成。为了让报告更有参考价值我会把每一题的得分明细也存进 eval_scores最终报告按题目逐项列出而不是只给一个总分。4. 实测排坑记录Context溢出、JSON幻觉和流程失控4.1 上下文爆炸20轮之后模型失忆这个项目上线跑真实对话之后最明显的问题是面试进行到 20 轮左右模型开始忘记Earlier 的内容出现重复出题、前后矛盾的情况。原因不复杂——长对话的 Token 占用太大超出了模型的有效注意力范围而且我的系统 Prompt 本身就比较长。我试过单纯加大上下文窗口效果有限。后来采用的方案是分层记忆短期记忆当前这一轮对话的原始消息出题、回答、追问、回答……最新 4 轮中期记忆每完成一轮就调用模型把这一轮的关键信息压缩成一段 200 字以内的摘要存进 session 上下文长期记忆用户历史面试的评估报告摘要在面试开始时作为背景注入这样每次发送给模型的实际消息量被控制在一个比较稳定的范围内。压缩摘要这个操作本身会额外消耗一次模型调用但因为每轮只压一次综合成本还是比直接无限叠 Token 要低得多。压缩摘要的 Prompt 我也迭代了两次。第一版让模型总结对话内容结果摘要里全是用户回答了关于XX的问题几乎没有可用的技术信息。改成强制结构化输出后靠谱多了请把刚才这一轮面试的对话压缩成 JSON 摘要字段如下 { question_topic: 本题核心知识点, user_strength: 用户表现好的地方, user_weakness: 用户暴露的薄弱点, follow_up_given: 是否有追问追问了什么, score_hint: 一个0-5的粗略分数 }4.2 结构化输出不稳定模型非要带 Markdown 标记做 Agent 开发结构化输出绝对是一个绕不开的坑。我在出题和决策两个环节都要求模型输出 JSON结果模型经常给出下面这种玩意{action: ask_question, question: ...}看起来没问题对吧但偶尔它会这样这里是我决定调用的工具{action: call_tool, tool_name: ...}或者干脆把 JSON 包在 json 代码块里。这在解析环节造成了一堆偶发报错而且是非常难复现的那种——同样一段代码有时候能解析有时候不能。我的排查链路是这样的一开始以为是模型随机性问题重试就好了——确实能缓解但治标不治本后来想强制用 JSON Mode发现部分模型接口对 JSON Mode 的指令遵循不稳定最终方案是写了一个容错解析器先尝试解析失败后用正则把 Markdown 代码块剥掉再尝试一次还失败就把整段文本原样交给模型让它重新输出为 JSONdef safe_parse_json(text: str) - dict: try: return json.loads(text) except json.JSONDecodeError: # 剥掉 markdown 代码块标记 cleaned re.sub(r^(?:json)?|$, , text.strip(), flagsre.M) # 找到第一个 { 到最后一个 } 之间的内容 match re.search(r\{.*\}, cleaned, re.DOTALL) if match: try: return json.loads(match.group()) except json.JSONDecodeError: raise ValueError(f无法解析模型输出: {text[:200]})这个方案谈不上优雅但很实用。它把结构化输出的解析成功率从 80% 左右提升到了 99% 以上。维护成本几乎为零推荐遇到类似问题的朋友直接抄作业。4.3 模型假装执行了工具必须校验工具返回值这是我认为整个排坑过程里最有价值的一次发现。某个版本里模型的决策不总是走 call_tool 分支有时它会自说自话地在文本里写代码已运行运行结果为……然后给用户一个评价。这种幻觉式工具调用特别隐蔽因为乍一看输出很自然但仔细看会发现它的运行结果和真实测试用例通过情况对不上。排查过程是这样的第一步怀疑是 Prompt 不够强调必须调用工具。于是我在硬性规则里加了一条不经过 code_executor 工具不允许对代码做任何正确性评价。加了之后情况好了一些但仍有漏网。仔细看完整对话日志发现模型在等待代码执行结果的那轮消息里为了能继续对话会在没有工具返回的情况下直接编一个工具已执行的结果。这就是问题的根代码执行是异步的工具返回值还没到模型已经生成了下一轮回复。所以这个 Bug 的本质是执行链路的时序问题不是提示词问题。修复方案是在会话状态里加了一个 gate当前有 pending 工具调用且尚未拿到返回结果时禁止模型生成下一轮 Reply。伪代码如下if session_state.has_pending_tool_calls(): # 必须先执行工具把结果注入到上下文中 tool_results await execute_pending_tools(session_state) messages.append(roletool, contenttool_results) else: # 正常调用模型决策 decision await llm.decide(messages, toolsTOOLS)这个 gate 加上之后假装执行的问题彻底消失。经验是Agent 里工具调用的时序问题一定要靠系统状态约束不能指望模型自觉。4.4 流程失控面试官自己宣布面试结束还有一个挺搞笑的问题模型在第三轮甚至第二轮就自己宣布面试结束现在给你评估报告。原因我查了很久最后发现是模型对finish动作的理解太积极了——它把用户答完一道题当成了面试该结束了。这种情况本质上是状态机的转移条件定义得不够精确。后来我在决策逻辑上加了一个显式的守卫条件只有面试轮数不小于 5或用户明确说出结束时finish 动作才被允许出现在候选动作里其他场景下模型只能选 follow_up 或 next_question这个守卫逻辑直接写在代码里不放在 Prompt 里。因为 Prompt 是软约束代码是硬约束。凡是涉及流程终点的控制一律用代码不要用提示词。5. 当前版本实测效果与下一步规划5.1 真实使用的效果哪些地方达标了第一期版本我在小范围找了 5 个朋友试用3 个后端、1 个前端、1 个算法方向跑了几十场模拟面试。总体感受如下出题质量基本达标。贴近业务的题目设计让用户普遍反馈有练习的价值但高级别的系统设计题偶尔出得偏泛没有具体的边界条件这是下一步要优化的评估报告的价值超过了预期。五个维度的评分和每道题的明细比大多数人自己复盘的效果好。很多用户反馈看了报告才发现自己复杂度分析一直没讲到点子上追问逻辑还需要加强。目前 follow_up 的深度不稳定有时候追问的问题比原题还简单显得很跳跃5.2 已经暴露但还没解决的问题我列一下目前明确知道有问题、但还没排期修的问题现象初步方向并发能力单实例串行处理会话用户一多就排队需要引入会话级并发隔离参考AI Agent 怎么扛并发的思路做连接池与会话分片代码沙箱安全目前是本地进程执行存在安全隐患切 Docker 隔离限制 CPU/内存成本控制代码评估环节 Token 消耗过大先跑轻量模型做初筛大模型只做复核和评论记忆持久化重启进程后历史会话丢失引入向量库或 Redis 做会话级长期记忆5.3 后续学习方向我的补充计划打在这里这个项目第一期让我对 Agent 有了非常具体的体感。下一步我打算重点补三块一是把主流的 Agent 框架正式过一遍。手写 ReAct 让我理解了骨架现在回头去看 LangChain 这类框架至少能看懂每一层是干什么的不像之前完全是黑的。二是系统学习 Agent 的记忆设计。短期记忆、摘要压缩、向量召回这套分层方案第一期只是做到了能用距离好用还有距离。加上这个项目已经有真实的面试对话数据正好可以用来验证不同记忆策略的效果。三是把多 Agent 协作引入面试场景。现在的单 Agent 既当面试官又当评分员角色的职责混杂容易导致评估不客观。我计划把评估拆成一个独立的 Reviewer Agent在面试过程中只记录证据最后再独立输出评估结果避免面试官自己既当运动员又当裁判的问题。最后再分享一个做 Agent 项目的心得。很多人问我是先学框架还是先自己做我的答案是自己先写一个 30 行的 ReAct 循环跑通一个真实场景再回头玩框架。框架是用来解决规模化问题的但理解本身的练手过程不该被框架的抽象层掩盖掉。你只有亲手压过一次 Context、亲手和 JSON 解析搏斗过才能真正明白 Agent 项目里哪些坑是模型层面的、哪些是架构层面的、哪些是产品层面的。这份手感看多少篇教程都补不回来。
返回列表